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.
This commit is contained in:
bmoore210 2026-06-07 01:44:48 -07:00 committed by Teknium
parent 330ca4585b
commit b55ac45264
3 changed files with 69 additions and 5 deletions

View file

@ -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()

View file

@ -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<typeof vi.fn>
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
})
)
})
})

View file

@ -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<PaginatedSessions> {
const result = await window.hermesDesktop.api<PaginatedSessions>({
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<PaginatedSessions>({
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 {