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 {