Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions apps/desktop/src/api/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,17 @@ export function setSessionUnreadRemote(id: string, unread: boolean, profile?: st
})
}

export function searchSessions(query: string): Promise<SessionSearchResponse> {
export function searchSessions(query: string, profile?: ProfileScope): Promise<SessionSearchResponse> {
const scoped = sessionScoped(profile)
const params = new URLSearchParams({ q: query })

if (scoped.profile) {
params.set('profile', scoped.profile)
}

return hermesApi<SessionSearchResponse>({
path: `/api/sessions/search?q=${encodeURIComponent(query)}`
...scoped,
path: `/api/sessions/search?${params.toString()}`
})
}

Expand Down
20 changes: 15 additions & 5 deletions apps/desktop/src/app/chat/sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ export function ChatSidebar({
const newSessionKbd = newSessionCombo ? comboTokens(newSessionCombo) : []
const [searchQuery, setSearchQuery] = useState('')
const [serverMatches, setServerMatches] = useState<SessionSearchResult[]>([])
const [serverMatchesScope, setServerMatchesScope] = useState('')
const [searchPending, setSearchPending] = useState(false)
const [newSessionKbdFlash, setNewSessionKbdFlash] = useState(false)
const [messagingLoadMorePending, setMessagingLoadMorePending] = useState<Record<string, boolean>>({})
Expand Down Expand Up @@ -647,24 +648,32 @@ export function ChatSidebar({

// Full-text search across *all* sessions (not just the loaded page) so 699
// sessions stay findable. Debounced; loaded sessions are matched instantly
// client-side and merged ahead of the server hits.
// client-side and merged ahead of the server hits. Server hits are scoped to
// the current profile scope: a concrete profile searches only its own state.db,
// and All Profiles searches everything.
const searchScope = showAllProfiles ? ALL_PROFILES : profileScope

useEffect(() => {
if (!trimmedQuery) {
setServerMatches([])
setServerMatchesScope('')
setSearchPending(false)

return
}

let cancelled = false

setServerMatches([])
setServerMatchesScope(searchScope)
setSearchPending(true)

const id = window.setTimeout(() => {
void searchSessions(trimmedQuery)
void searchSessions(trimmedQuery, showAllProfiles ? undefined : profileScope)
.then(res => {
if (!cancelled) {
setServerMatches(res.results)
setServerMatchesScope(searchScope)
}
})
.catch(() => undefined)
Expand All @@ -679,13 +688,14 @@ export function ChatSidebar({
cancelled = true
window.clearTimeout(id)
}
}, [trimmedQuery])
}, [trimmedQuery, searchScope, showAllProfiles, profileScope])

const searchResults = useMemo(() => {
if (!trimmedQuery) {
return []
}

const matchesForCurrentScope = serverMatchesScope === searchScope ? serverMatches : []
const out = new Map<string, SessionInfo>()

for (const s of sortedSessions) {
Expand All @@ -694,7 +704,7 @@ export function ChatSidebar({
}
}

for (const match of serverMatches) {
for (const match of matchesForCurrentScope) {
if (out.has(match.session_id)) {
continue
}
Expand All @@ -704,7 +714,7 @@ export function ChatSidebar({
}

return [...out.values()]
}, [trimmedQuery, sortedSessions, serverMatches, sessionByAnyId])
}, [trimmedQuery, sortedSessions, serverMatches, serverMatchesScope, searchScope, sessionByAnyId])

const unpinnedAgentSessions = useMemo(
() => sortedSessions.filter(s => !isPinnedSession(s)),
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/hermes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
listSidebarSessions,
pluginSocket,
resetSidebarBatchCapability,
searchSessions,
setApiRequestConnection,
setApiRequestProfile,
speakText,
Expand Down Expand Up @@ -84,6 +85,17 @@ describe('Hermes REST helpers', () => {
)
})

it('routes a session search to the selected profile backend', async () => {
api.mockResolvedValue({ results: [] })

await searchSessions('attendance audit', 'sabby')

expect(api).toHaveBeenCalledWith({
path: '/api/sessions/search?q=attendance+audit&profile=sabby',
profile: 'sabby'
})
})

it('batches the sidebar slices into a single request with per-slice limits + excludes', async () => {
api.mockResolvedValue({ recents: { sessions: [] }, cron: { sessions: [] }, messaging: { sessions: [] } })

Expand Down