diff --git a/packages/client/src/api/hermes/chat.ts b/packages/client/src/api/hermes/chat.ts index b8a8c30..26fbcb7 100644 --- a/packages/client/src/api/hermes/chat.ts +++ b/packages/client/src/api/hermes/chat.ts @@ -15,6 +15,7 @@ export interface StartRunRequest { input: string | ContentBlock[] instructions?: string session_id?: string + profile?: string model?: string provider?: string model_groups?: Array<{ provider: string; models: string[] }> @@ -77,6 +78,7 @@ export interface RunEvent { let chatRunSocket: Socket | null = null let globalListenersRegistered = false +let chatRunSocketProfile: string | null = null /** * Session event handlers map @@ -429,29 +431,36 @@ export function getChatRunSocket(): Socket | null { return chatRunSocket } -export function connectChatRun(): Socket { - if (chatRunSocket?.connected) return chatRunSocket +export function connectChatRun(requestedProfile?: string | null): Socket { + const normalizedRequestedProfile = requestedProfile?.trim() || null + if (chatRunSocket?.connected && (!normalizedRequestedProfile || chatRunSocketProfile === normalizedRequestedProfile)) { + return chatRunSocket + } // Clean up old socket to prevent duplicate event listeners if (chatRunSocket) { chatRunSocket.removeAllListeners() chatRunSocket.disconnect() globalListenersRegistered = false + chatRunSocketProfile = null } const baseUrl = getBaseUrlValue() const token = getApiKey() // Get active profile from store (authoritative source) - let profile = 'default' + let profile = normalizedRequestedProfile || 'default' try { - const { useProfilesStore } = require('@/stores/hermes/profiles') - const profilesStore = useProfilesStore() - profile = profilesStore.activeProfileName || 'default' + if (!normalizedRequestedProfile) { + const { useProfilesStore } = require('@/stores/hermes/profiles') + const profilesStore = useProfilesStore() + profile = profilesStore.activeProfileName || 'default' + } } catch { // Fallback to localStorage during early initialization - profile = localStorage.getItem('hermes_active_profile_name') || 'default' + profile = normalizedRequestedProfile || localStorage.getItem('hermes_active_profile_name') || 'default' } + chatRunSocketProfile = profile chatRunSocket = io(`${baseUrl}/chat-run`, { auth: { token }, @@ -506,6 +515,7 @@ export function disconnectChatRun(): void { if (chatRunSocket) { chatRunSocket.disconnect() chatRunSocket = null + chatRunSocketProfile = null globalListenersRegistered = false sessionEventHandlers.clear() } @@ -533,11 +543,12 @@ function removeSocketListener(socket: Socket, event: string, handler: (...args: export function resumeSession( sessionId: string, onResumed: (data: { session_id: string; messages: any[]; isWorking: boolean; isAborting?: boolean; events: any[]; inputTokens?: number; outputTokens?: number; contextTokens?: number; queueLength?: number; queueMessages?: RunEvent['queued_messages'] }) => void, + profile?: string | null, ): Socket { - const socket = connectChatRun() + const socket = connectChatRun(profile) socket.once('resumed', onResumed) - socket.emit('resume', { session_id: sessionId }) + socket.emit('resume', { session_id: sessionId, ...(profile ? { profile } : {}) }) return socket } @@ -555,7 +566,7 @@ export function startRunViaSocket( } let closed = false - const socket = connectChatRun() + const socket = connectChatRun(body.profile) const handleSocketError = (err: Error) => { if (closed) return closed = true diff --git a/packages/client/src/api/hermes/sessions.ts b/packages/client/src/api/hermes/sessions.ts index d00fbf2..e5b196b 100644 --- a/packages/client/src/api/hermes/sessions.ts +++ b/packages/client/src/api/hermes/sessions.ts @@ -62,10 +62,11 @@ export async function fetchSessions(source?: string, limit?: number, profile?: s /** * Fetch Hermes sessions only (exclude api_server source) */ -export async function fetchHermesSessions(source?: string, limit?: number): Promise { +export async function fetchHermesSessions(source?: string, limit?: number, profile?: string | null): Promise { const params = new URLSearchParams() if (source) params.set('source', source) if (limit) params.set('limit', String(limit)) + if (profile) params.set('profile', profile) const query = params.toString() const res = await request<{ sessions: SessionSummary[] }>(`/api/hermes/sessions/hermes${query ? `?${query}` : ''}`) return res.sessions @@ -82,9 +83,12 @@ export async function searchSessions(q: string, source?: string, limit?: number, return res.results } -export async function fetchSession(id: string): Promise { +export async function fetchSession(id: string, profile?: string | null): Promise { try { - const res = await request<{ session: SessionDetail }>(`/api/hermes/sessions/${id}`) + const params = new URLSearchParams() + if (profile) params.set('profile', profile) + const query = params.toString() + const res = await request<{ session: SessionDetail }>(`/api/hermes/sessions/${id}${query ? `?${query}` : ''}`) return res.session } catch { return null diff --git a/packages/client/src/components/hermes/chat/ChatPanel.vue b/packages/client/src/components/hermes/chat/ChatPanel.vue index 2a6230b..9478688 100644 --- a/packages/client/src/components/hermes/chat/ChatPanel.vue +++ b/packages/client/src/components/hermes/chat/ChatPanel.vue @@ -16,6 +16,7 @@ import { type DropdownOption, } from "naive-ui"; import { computed, nextTick, onMounted, onUnmounted, ref, watch } from "vue"; +import { useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { copyToClipboard } from "@/utils/clipboard"; import FolderPicker from "./FolderPicker.vue"; @@ -30,6 +31,7 @@ const chatStore = useChatStore(); const appStore = useAppStore(); const profilesStore = useProfilesStore(); const sessionBrowserPrefsStore = useSessionBrowserPrefsStore(); +const router = useRouter(); const message = useMessage(); const { t } = useI18n(); @@ -58,8 +60,13 @@ const showSessions = ref( let mobileQuery: MediaQueryList | null = null; const isMobile = ref(false); -function handleSessionClick(sessionId: string) { - chatStore.switchSession(sessionId); +async function handleSessionClick(sessionId: string) { + const session = chatStore.sessions.find((item) => item.id === sessionId); + await router.push({ + name: "hermes.session", + params: { sessionId }, + query: session?.profile ? { profile: session.profile } : undefined, + }); if (mobileQuery?.matches) showSessions.value = false; } @@ -242,12 +249,17 @@ function handleNewChatProviderChange(value: string) { newChatModel.value = newChatModelOptions.value[0]?.value || ""; } -function confirmNewChat() { - chatStore.newChat({ +async function confirmNewChat() { + const session = chatStore.newChat({ profile: newChatProfile.value, provider: newChatProvider.value, model: newChatModel.value, }); + await router.push({ + name: "hermes.session", + params: { sessionId: session.id }, + query: session.profile ? { profile: session.profile } : undefined, + }); showNewChatModal.value = false; } @@ -255,6 +267,28 @@ function handleApproval(choice: "once" | "session" | "always" | "deny") { chatStore.respondApproval(choice); } +function sessionProfile(sessionId: string): string | null { + return chatStore.sessions.find((session) => session.id === sessionId)?.profile || null; +} + +function buildSessionUrl(sessionId: string, profile?: string | null): string { + const href = router.resolve({ + name: "hermes.session", + params: { sessionId }, + query: profile ? { profile } : undefined, + }).href; + return `${window.location.origin}${window.location.pathname}${href}`; +} + +async function copySessionLink(id?: string) { + const sessionId = id || chatStore.activeSessionId; + if (sessionId) { + const ok = await copyToClipboard(buildSessionUrl(sessionId, sessionProfile(sessionId))); + if (ok) message.success(t("common.copied")); + else message.error(t("common.copied") + " ✗"); + } +} + async function copySessionId(id?: string) { const sessionId = id || chatStore.activeSessionId; if (sessionId) { @@ -397,6 +431,7 @@ const contextMenuOptions = computed(() => { }, ], }) + options.push({ label: t("chat.copySessionLink"), key: "copy-link" }) options.push({ label: t("chat.copySessionId"), key: "copy-id" }) return options }); @@ -428,7 +463,9 @@ async function handleContextMenuSelect(key: string) { sessionBrowserPrefsStore.togglePinned(contextSessionId.value); return; } - if (key === "copy-id") { + if (key === "copy-link") { + copySessionLink(contextSessionId.value); + } else if (key === "copy-id") { copySessionId(contextSessionId.value); } else if (parseExportKey(key)) { const { mode, ext } = parseExportKey(key)!; diff --git a/packages/client/src/components/hermes/group-chat/GroupChatPanel.vue b/packages/client/src/components/hermes/group-chat/GroupChatPanel.vue index 902a0ee..925f6f5 100644 --- a/packages/client/src/components/hermes/group-chat/GroupChatPanel.vue +++ b/packages/client/src/components/hermes/group-chat/GroupChatPanel.vue @@ -1,17 +1,20 @@ diff --git a/packages/client/src/views/hermes/GroupChatView.vue b/packages/client/src/views/hermes/GroupChatView.vue index c9b6881..de7176b 100644 --- a/packages/client/src/views/hermes/GroupChatView.vue +++ b/packages/client/src/views/hermes/GroupChatView.vue @@ -1,13 +1,42 @@