diff --git a/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx b/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx index 662f195a30..0e5710aecd 100644 --- a/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx +++ b/apps/desktop/src/app/contrib/hooks/use-background-sync.test.tsx @@ -1,11 +1,17 @@ import { act, cleanup, renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { $sidebarShowArchived } from '@/store/layout' import { $changeEventsAvailable, $cronChangeTick, $sessionsChangeTick } from '@/store/live-sync' import { $activeSessionId } from '@/store/session' +import { loadArchivedSessions } from '@/store/sidebar-archive' import { useBackgroundSync } from './use-background-sync' +vi.mock('@/store/sidebar-archive', () => ({ + loadArchivedSessions: vi.fn() +})) + const noop = () => undefined const requestGateway = async () => ({ sessions: [] }) @@ -45,6 +51,8 @@ describe('useBackgroundSync profile-scoped session refresh', () => { $changeEventsAvailable.set(false) $cronChangeTick.set(0) $sessionsChangeTick.set(0) + $sidebarShowArchived.set(false) + vi.mocked(loadArchivedSessions).mockReset() }) afterEach(() => { @@ -103,4 +111,27 @@ describe('useBackgroundSync profile-scoped session refresh', () => { await act(async () => undefined) expect(refreshSessions).toHaveBeenCalledTimes(1) }) + + it('reloads archived sessions after an external session change while the Archived view is open', async () => { + $changeEventsAvailable.set(true) + $sidebarShowArchived.set(true) + render('default', 'local', async () => undefined) + + await act(async () => { + $sessionsChangeTick.set(1) + }) + + expect(loadArchivedSessions).toHaveBeenCalledTimes(1) + }) + + it('does not reload archived sessions when the Archived view is closed', async () => { + $changeEventsAvailable.set(true) + render('default', 'local', async () => undefined) + + await act(async () => { + $sessionsChangeTick.set(1) + }) + + expect(loadArchivedSessions).not.toHaveBeenCalled() + }) }) diff --git a/apps/desktop/src/app/contrib/hooks/use-background-sync.ts b/apps/desktop/src/app/contrib/hooks/use-background-sync.ts index 06dd870a9e..d2ef0bc56a 100644 --- a/apps/desktop/src/app/contrib/hooks/use-background-sync.ts +++ b/apps/desktop/src/app/contrib/hooks/use-background-sync.ts @@ -6,6 +6,7 @@ import { getLatestSessionMessages, type ProfileScope } from '@/hermes' import { preserveLocalAssistantErrors, sealOpenToolParts, toChatMessages } from '@/lib/chat-messages' import { createClientSessionState } from '@/lib/chat-runtime' import { sessionMessagesSignature } from '@/lib/session-signatures' +import { $sidebarShowArchived } from '@/store/layout' import { $changeEventsAvailable, $cronChangeTick, $sessionsChangeTick } from '@/store/live-sync' import { $onBattery, batteryPollInterval } from '@/store/power' import { refreshActiveProfile } from '@/store/profile' @@ -28,6 +29,7 @@ import { SESSION_WATCHDOG_TIMEOUT_MS, setSessionStalled } from '@/store/session-states' +import { loadArchivedSessions } from '@/store/sidebar-archive' import type { ClientSessionState } from '../../types' import type { GatewayRequester } from '../types' @@ -753,11 +755,19 @@ export function useBackgroundSync({ lastRunAt = Date.now() void refreshSessions() void refreshMessagingSessions() + + // Archived sessions use their own capped query. Reload it only while the + // sidebar is showing that view, matching the on-demand fetch contract. + if ($sidebarShowArchived.get()) { + void loadArchivedSessions() + } + // The project tree is a grouping of the same stored rows, so a session // created/deleted/renamed/re-homed outside this window goes stale in the // Projects sidebar without this (#100354). refreshProjectTree() keeps the // cached tree on failure, so a not-yet-ready backend costs nothing. void refreshProjectTree() + requestActiveTranscriptRefresh(true) // Bot canonical chats live in workspace tiles, never in the main-pane // selection — without this they never see background deliveries diff --git a/apps/desktop/src/store/sidebar-archive.test.ts b/apps/desktop/src/store/sidebar-archive.test.ts new file mode 100644 index 0000000000..e7e14985ab --- /dev/null +++ b/apps/desktop/src/store/sidebar-archive.test.ts @@ -0,0 +1,26 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { listAllProfileSessions, type SessionInfo } from '@/hermes' + +import { $archivedSessions, loadArchivedSessions } from './sidebar-archive' + +vi.mock('@/hermes', () => ({ + listAllProfileSessions: vi.fn() +})) + +describe('loadArchivedSessions', () => { + beforeEach(() => { + $archivedSessions.set([]) + vi.mocked(listAllProfileSessions).mockReset() + }) + + it('keeps the last successful result when a refresh fails', async () => { + const existing = { id: 'archived-1', title: 'Keep me' } as SessionInfo + $archivedSessions.set([existing]) + vi.mocked(listAllProfileSessions).mockRejectedValue(new Error('offline')) + + await loadArchivedSessions() + + expect($archivedSessions.get()).toEqual([existing]) + }) +}) diff --git a/apps/desktop/src/store/sidebar-archive.ts b/apps/desktop/src/store/sidebar-archive.ts index 52652b9c46..549a9f57b8 100644 --- a/apps/desktop/src/store/sidebar-archive.ts +++ b/apps/desktop/src/store/sidebar-archive.ts @@ -23,7 +23,8 @@ export async function loadArchivedSessions(): Promise { $archivedSessions.set(result.sessions) } catch { - $archivedSessions.set([]) + // A background refresh must not turn a usable Archived view into an empty + // one when the backend is temporarily unavailable. Keep the last good set. } finally { $archivedSessionsLoading.set(false) }