From 529065f023ebdf8a34345635c08c23b5d4ec2502 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Thu, 21 May 2026 09:48:31 +0800 Subject: [PATCH] scope session search by selected profile (#889) --- packages/client/src/api/hermes/sessions.ts | 3 +- .../src/components/hermes/chat/ChatPanel.vue | 8 +++--- .../hermes/chat/SessionSearchModal.vue | 28 +++++++++++++++++-- packages/client/src/stores/hermes/chat.ts | 2 ++ .../server/src/controllers/hermes/sessions.ts | 11 ++++++-- .../server/src/db/hermes/session-store.ts | 18 +++++++++--- 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/packages/client/src/api/hermes/sessions.ts b/packages/client/src/api/hermes/sessions.ts index 66602f0..9ae8fae 100644 --- a/packages/client/src/api/hermes/sessions.ts +++ b/packages/client/src/api/hermes/sessions.ts @@ -71,11 +71,12 @@ export async function fetchHermesSessions(source?: string, limit?: number): Prom return res.sessions } -export async function searchSessions(q: string, source?: string, limit?: number): Promise { +export async function searchSessions(q: string, source?: string, limit?: number, profile?: string): Promise { const params = new URLSearchParams() params.set('q', q) 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<{ results: SessionSearchResult[] }>(`/api/hermes/search/sessions?${query}`) return res.results diff --git a/packages/client/src/components/hermes/chat/ChatPanel.vue b/packages/client/src/components/hermes/chat/ChatPanel.vue index e7441ee..2a6230b 100644 --- a/packages/client/src/components/hermes/chat/ChatPanel.vue +++ b/packages/client/src/components/hermes/chat/ChatPanel.vue @@ -86,7 +86,7 @@ const showRenameModal = ref(false); const renameValue = ref(""); const renameSessionId = ref(null); const renameInputRef = ref | null>(null); -const sessionProfileFilter = ref(null); +const sessionProfileFilter = computed(() => chatStore.sessionProfileFilter); const profileFilterOptions = computed(() => [ { label: t("chat.allProfiles"), value: "__all__" }, ...profilesStore.profiles.map((profile) => ({ @@ -96,8 +96,8 @@ const profileFilterOptions = computed(() => [ ]); async function handleProfileFilterChange(value: string) { - sessionProfileFilter.value = value === "__all__" ? null : value; - await chatStore.loadSessions(sessionProfileFilter.value); + chatStore.sessionProfileFilter = value === "__all__" ? null : value; + await chatStore.loadSessions(chatStore.sessionProfileFilter); } function sortSessionsWithActiveFirst(items: Session[]): Session[] { @@ -311,7 +311,7 @@ async function handleBatchDelete() { // Remove deleted sessions from local store (without calling API again) // Use loadSessions to refresh from server instead of manual filtering - await chatStore.loadSessions(); + await chatStore.loadSessions(chatStore.sessionProfileFilter); message.success(t("chat.batchDeleteSuccess", { count: result.deleted })); if (result.failed > 0) { diff --git a/packages/client/src/components/hermes/chat/SessionSearchModal.vue b/packages/client/src/components/hermes/chat/SessionSearchModal.vue index d4f0684..e4c8c06 100644 --- a/packages/client/src/components/hermes/chat/SessionSearchModal.vue +++ b/packages/client/src/components/hermes/chat/SessionSearchModal.vue @@ -19,6 +19,7 @@ const recentSessions = ref([]) const searchResults = ref([]) const activeIndex = ref(0) const inputRef = ref | null>(null) +const profileFilter = computed(() => chatStore.sessionProfileFilter || undefined) let debounceTimer: ReturnType | null = null let requestSeq = 0 @@ -79,7 +80,9 @@ async function loadRecentSessions() { const seq = ++requestSeq loading.value = true try { - const sessions = await fetchSessions(undefined, 8) + const sessions = profileFilter.value + ? await fetchSessions(undefined, 8, profileFilter.value) + : await fetchSessions(undefined, 8) if (seq !== requestSeq) return recentSessions.value = sessions searchResults.value = [] @@ -99,7 +102,9 @@ async function runSearch(text: string) { loading.value = true try { const results = text.trim() - ? await searchSessions(text.trim(), undefined, 10) + ? profileFilter.value + ? await searchSessions(text.trim(), undefined, 10, profileFilter.value) + : await searchSessions(text.trim(), undefined, 10) : [] if (seq !== requestSeq) return searchResults.value = results @@ -116,7 +121,7 @@ async function runSearch(text: string) { async function ensureChatSessionsLoaded() { if (chatStore.sessions.length === 0) { - await chatStore.loadSessions() + await chatStore.loadSessions(chatStore.sessionProfileFilter) } } @@ -125,6 +130,23 @@ async function openItem(item: SearchItem) { sessionSearchOpen.value = false await ensureChatSessionsLoaded() + if (!chatStore.sessions.some(session => session.id === item.id) && typeof chatStore.addOrUpdateSession === 'function') { + chatStore.addOrUpdateSession({ + id: item.id, + profile: item.profile || 'default', + title: item.title || '', + source: item.source, + messages: [], + createdAt: Math.round(item.started_at * 1000), + updatedAt: Math.round((item.last_active || item.ended_at || item.started_at) * 1000), + model: item.model, + provider: item.provider || item.billing_provider || '', + messageCount: item.message_count, + endedAt: item.ended_at != null ? Math.round(item.ended_at * 1000) : null, + lastActiveAt: item.last_active != null ? Math.round(item.last_active * 1000) : undefined, + workspace: item.workspace || null, + }) + } await chatStore.switchSession(item.id, messageId) if (router.currentRoute.value.name !== 'hermes.chat') { await router.push({ name: 'hermes.chat' }) diff --git a/packages/client/src/stores/hermes/chat.ts b/packages/client/src/stores/hermes/chat.ts index f865977..ab2c78e 100644 --- a/packages/client/src/stores/hermes/chat.ts +++ b/packages/client/src/stores/hermes/chat.ts @@ -333,6 +333,7 @@ export const useChatStore = defineStore('chat', () => { const streamStates = ref void }>>(new Map()) /** sessionId → server-reported isWorking status */ const serverWorking = ref>(new Set()) + const sessionProfileFilter = ref(null) /** sessionId → queued message count */ const queueLengths = ref>(new Map()) /** sessionId → queued user messages not yet visible in the transcript */ @@ -1945,6 +1946,7 @@ export const useChatStore = defineStore('chat', () => { isStreaming, isRunActive, isSessionLive, + sessionProfileFilter, compressionState, abortState, isAborting, diff --git a/packages/server/src/controllers/hermes/sessions.ts b/packages/server/src/controllers/hermes/sessions.ts index 644e1fc..a07b05d 100644 --- a/packages/server/src/controllers/hermes/sessions.ts +++ b/packages/server/src/controllers/hermes/sessions.ts @@ -168,9 +168,16 @@ export async function listHermesSessions(ctx: any) { export async function search(ctx: any) { const q = typeof ctx.query.q === 'string' ? ctx.query.q : '' const limit = ctx.query.limit ? parseInt(ctx.query.limit as string, 10) : undefined - const profile = getActiveProfileName() + const profile = typeof ctx.query.profile === 'string' && ctx.query.profile.trim() + ? ctx.query.profile.trim() + : undefined const results = localSearchSessions(profile, q, limit && limit > 0 ? limit : 20) - ctx.body = { results: filterPendingDeletedSessions(results) } + const knownProfiles = profile ? null : new Set(listProfileNamesFromDisk()) + ctx.body = { + results: filterPendingDeletedSessions(results.filter(s => + !knownProfiles || knownProfiles.has(s.profile || 'default'), + )), + } } export async function get(ctx: any) { diff --git a/packages/server/src/db/hermes/session-store.ts b/packages/server/src/db/hermes/session-store.ts index 1d599a6..53d5f22 100644 --- a/packages/server/src/db/hermes/session-store.ts +++ b/packages/server/src/db/hermes/session-store.ts @@ -260,11 +260,12 @@ export function listSessions(profile?: string, source?: string, limit = 2000): H return rows.map(mapSessionRow) } -export function searchSessions(profile: string, query: string, limit = 20): HermesSessionSearchRow[] { +export function searchSessions(profile: string | null | undefined, query: string, limit = 20): HermesSessionSearchRow[] { if (!isSqliteAvailable()) return [] + const profileFilter = profile?.trim() const trimmed = query.trim() if (!trimmed) { - return listSessions(profile, undefined, limit).map(s => ({ ...s, snippet: s.preview || '', matched_message_id: null })) + return listSessions(profileFilter, undefined, limit).map(s => ({ ...s, snippet: s.preview || '', matched_message_id: null })) } const db = getDb()! const lowered = trimmed.toLowerCase() @@ -273,12 +274,21 @@ export function searchSessions(profile: string, query: string, limit = 20): Herm // Step 1: Find matching sessions const sessionRows = db.prepare( `SELECT * FROM ${SESSIONS_TABLE} - WHERE profile = ? AND ( + WHERE 1 = 1 + ${profileFilter ? 'AND profile = ?' : ''} + AND ( LOWER(title) LIKE ? OR LOWER(preview) LIKE ? OR id IN (SELECT DISTINCT session_id FROM ${MESSAGES_TABLE} WHERE LOWER(content) LIKE ? OR LOWER(COALESCE(tool_name, '')) LIKE ?) ) ORDER BY last_active DESC LIMIT ?`, - ).all(profile, pattern, pattern, pattern, pattern, limit) as Record[] + ).all(...[ + ...(profileFilter ? [profileFilter] : []), + pattern, + pattern, + pattern, + pattern, + limit, + ]) as Record[] if (sessionRows.length === 0) return []