From b55ac45264e949927190849e13c9aac4e2069aa4 Mon Sep 17 00:00:00 2001 From: bmoore210 <266365592+bmoore210@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:44:48 -0700 Subject: [PATCH] fix(desktop): scope session list to active profile + longer timeout The desktop sidebar fetched the unified cross-profile session list as profile='all' and filtered it client-side by the active profile. On a large multi-profile install the active profile's rows could be windowed out of the cross-profile recency page entirely, so switching to a profile agent showed an empty history panel (and the 'all' fetch could exceed the 15s IPC timeout on startup). Scope the fetch to the active profile so its own page comes back on its merits, and bump the session-list IPC timeout to 60s. profileScope is now a refreshSessions dep, so the existing gateway-open effect re-pulls on profile switch. --- apps/desktop/src/app/desktop-controller.tsx | 18 ++++++-- apps/desktop/src/hermes.test.ts | 49 +++++++++++++++++++++ apps/desktop/src/hermes.ts | 7 ++- 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/hermes.test.ts diff --git a/apps/desktop/src/app/desktop-controller.tsx b/apps/desktop/src/app/desktop-controller.tsx index f02824e2925..15466d20950 100644 --- a/apps/desktop/src/app/desktop-controller.tsx +++ b/apps/desktop/src/app/desktop-controller.tsx @@ -29,7 +29,14 @@ import { unpinSession } from '../store/layout' import { $filePreviewTarget, $previewTarget, closeActiveRightRailTab } from '../store/preview' -import { $activeGatewayProfile, $freshSessionRequest, normalizeProfileKey, refreshActiveProfile } from '../store/profile' +import { + $activeGatewayProfile, + $freshSessionRequest, + $profileScope, + ALL_PROFILES, + normalizeProfileKey, + refreshActiveProfile +} from '../store/profile' import { $activeSessionId, $currentCwd, @@ -157,6 +164,7 @@ export function DesktopController() { const selectedStoredSessionId = useStore($selectedStoredSessionId) const terminalTakeover = useStore($terminalTakeover) const panesFlipped = useStore($panesFlipped) + const profileScope = useStore($profileScope) const routedSessionId = routeSessionId(location.pathname) const routeToken = `${location.pathname}:${location.search}:${location.hash}` @@ -288,7 +296,11 @@ export function DesktopController() { // the same rows tagged profile="default". Cron sessions are excluded here // and fetched separately (refreshCronSessions) so the scheduler's // always-newest rows can't consume the recents page budget. - const result = await listAllProfileSessions(limit, 1, 'exclude', 'recent', 'all', { + // Scope the fetch to the active profile (not always 'all') so a profile + // with few recent sessions isn't windowed out of the cross-profile + // recency page — the empty-history-on-profile-switch bug. + const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope + const result = await listAllProfileSessions(limit, 1, 'exclude', 'recent', sessionProfile, { excludeSources: ['cron'] }) @@ -305,7 +317,7 @@ export function DesktopController() { void refreshCronSessions() void refreshCronJobs() - }, [refreshCronSessions, refreshCronJobs]) + }, [profileScope, refreshCronSessions, refreshCronJobs]) const loadMoreSessions = useCallback(() => { bumpSessionsLimit() diff --git a/apps/desktop/src/hermes.test.ts b/apps/desktop/src/hermes.test.ts new file mode 100644 index 00000000000..0dcf58b3640 --- /dev/null +++ b/apps/desktop/src/hermes.test.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { listAllProfileSessions, listSessions } from './hermes' + +const emptySessionsResponse = { + limit: 0, + offset: 0, + sessions: [], + total: 0 +} + +describe('Hermes REST session helpers', () => { + let api: ReturnType + + beforeEach(() => { + api = vi.fn().mockResolvedValue(emptySessionsResponse) + Object.defineProperty(window, 'hermesDesktop', { + configurable: true, + value: { api } + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + Reflect.deleteProperty(window, 'hermesDesktop') + }) + + it('uses a longer timeout for the single-profile session list', async () => { + await listSessions(50, 1) + + expect(api).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/sessions?limit=50&offset=0&min_messages=1&archived=exclude&order=recent', + timeoutMs: 60_000 + }) + ) + }) + + it('uses a longer timeout for the all-profile session list', async () => { + await listAllProfileSessions(50, 1) + + expect(api).toHaveBeenCalledWith( + expect.objectContaining({ + path: '/api/profiles/sessions?limit=50&offset=0&min_messages=1&archived=exclude&order=recent&profile=all', + timeoutMs: 60_000 + }) + ) + }) +}) diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index 33aa9fea320..631a9c0e977 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -42,6 +42,7 @@ import type { } from '@/types/hermes' const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000 +const SESSION_LIST_REQUEST_TIMEOUT_MS = 60_000 export type { ActionResponse, @@ -136,7 +137,8 @@ export async function listSessions( order: 'created' | 'recent' = 'recent' ): Promise { const result = await window.hermesDesktop.api({ - path: `/api/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}&archived=${archived}&order=${order}` + path: `/api/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}&archived=${archived}&order=${order}`, + timeoutMs: SESSION_LIST_REQUEST_TIMEOUT_MS }) return { @@ -176,7 +178,8 @@ export async function listAllProfileSessions( const result = await window.hermesDesktop.api({ path: `/api/profiles/sessions?limit=${limit}&offset=0&min_messages=${Math.max(0, minMessages)}` + - `&archived=${archived}&order=${order}&profile=${encodeURIComponent(profile)}${sourceParam}${excludeParam}` + `&archived=${archived}&order=${order}&profile=${encodeURIComponent(profile)}${sourceParam}${excludeParam}`, + timeoutMs: SESSION_LIST_REQUEST_TIMEOUT_MS }) return {