fix(desktop): refresh archived sessions on external changes

Co-authored-by: DavidMetcalfe <80915+DavidMetcalfe@users.noreply.github.com>
This commit is contained in:
KoNit-K
2026-09-15 09:38:37 +08:00
committed by Teknium
parent a40d90e9be
commit dbda53b8be
4 changed files with 69 additions and 1 deletions
@@ -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()
})
})
@@ -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
@@ -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])
})
})
+2 -1
View File
@@ -23,7 +23,8 @@ export async function loadArchivedSessions(): Promise<void> {
$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)
}