fix(desktop): refresh archived sessions on external changes
Co-authored-by: DavidMetcalfe <80915+DavidMetcalfe@users.noreply.github.com>
This commit is contained in:
@@ -1,11 +1,17 @@
|
|||||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { $sidebarShowArchived } from '@/store/layout'
|
||||||
import { $changeEventsAvailable, $cronChangeTick, $sessionsChangeTick } from '@/store/live-sync'
|
import { $changeEventsAvailable, $cronChangeTick, $sessionsChangeTick } from '@/store/live-sync'
|
||||||
import { $activeSessionId } from '@/store/session'
|
import { $activeSessionId } from '@/store/session'
|
||||||
|
import { loadArchivedSessions } from '@/store/sidebar-archive'
|
||||||
|
|
||||||
import { useBackgroundSync } from './use-background-sync'
|
import { useBackgroundSync } from './use-background-sync'
|
||||||
|
|
||||||
|
vi.mock('@/store/sidebar-archive', () => ({
|
||||||
|
loadArchivedSessions: vi.fn()
|
||||||
|
}))
|
||||||
|
|
||||||
const noop = () => undefined
|
const noop = () => undefined
|
||||||
const requestGateway = async () => ({ sessions: [] })
|
const requestGateway = async () => ({ sessions: [] })
|
||||||
|
|
||||||
@@ -45,6 +51,8 @@ describe('useBackgroundSync profile-scoped session refresh', () => {
|
|||||||
$changeEventsAvailable.set(false)
|
$changeEventsAvailable.set(false)
|
||||||
$cronChangeTick.set(0)
|
$cronChangeTick.set(0)
|
||||||
$sessionsChangeTick.set(0)
|
$sessionsChangeTick.set(0)
|
||||||
|
$sidebarShowArchived.set(false)
|
||||||
|
vi.mocked(loadArchivedSessions).mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -103,4 +111,27 @@ describe('useBackgroundSync profile-scoped session refresh', () => {
|
|||||||
await act(async () => undefined)
|
await act(async () => undefined)
|
||||||
expect(refreshSessions).toHaveBeenCalledTimes(1)
|
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 { preserveLocalAssistantErrors, sealOpenToolParts, toChatMessages } from '@/lib/chat-messages'
|
||||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||||
import { sessionMessagesSignature } from '@/lib/session-signatures'
|
import { sessionMessagesSignature } from '@/lib/session-signatures'
|
||||||
|
import { $sidebarShowArchived } from '@/store/layout'
|
||||||
import { $changeEventsAvailable, $cronChangeTick, $sessionsChangeTick } from '@/store/live-sync'
|
import { $changeEventsAvailable, $cronChangeTick, $sessionsChangeTick } from '@/store/live-sync'
|
||||||
import { $onBattery, batteryPollInterval } from '@/store/power'
|
import { $onBattery, batteryPollInterval } from '@/store/power'
|
||||||
import { refreshActiveProfile } from '@/store/profile'
|
import { refreshActiveProfile } from '@/store/profile'
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
SESSION_WATCHDOG_TIMEOUT_MS,
|
SESSION_WATCHDOG_TIMEOUT_MS,
|
||||||
setSessionStalled
|
setSessionStalled
|
||||||
} from '@/store/session-states'
|
} from '@/store/session-states'
|
||||||
|
import { loadArchivedSessions } from '@/store/sidebar-archive'
|
||||||
|
|
||||||
import type { ClientSessionState } from '../../types'
|
import type { ClientSessionState } from '../../types'
|
||||||
import type { GatewayRequester } from '../types'
|
import type { GatewayRequester } from '../types'
|
||||||
@@ -753,11 +755,19 @@ export function useBackgroundSync({
|
|||||||
lastRunAt = Date.now()
|
lastRunAt = Date.now()
|
||||||
void refreshSessions()
|
void refreshSessions()
|
||||||
void refreshMessagingSessions()
|
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
|
// 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
|
// created/deleted/renamed/re-homed outside this window goes stale in the
|
||||||
// Projects sidebar without this (#100354). refreshProjectTree() keeps the
|
// Projects sidebar without this (#100354). refreshProjectTree() keeps the
|
||||||
// cached tree on failure, so a not-yet-ready backend costs nothing.
|
// cached tree on failure, so a not-yet-ready backend costs nothing.
|
||||||
void refreshProjectTree()
|
void refreshProjectTree()
|
||||||
|
|
||||||
requestActiveTranscriptRefresh(true)
|
requestActiveTranscriptRefresh(true)
|
||||||
// Bot canonical chats live in workspace tiles, never in the main-pane
|
// Bot canonical chats live in workspace tiles, never in the main-pane
|
||||||
// selection — without this they never see background deliveries
|
// selection — without this they never see background deliveries
|
||||||
|
|||||||
26
apps/desktop/src/store/sidebar-archive.test.ts
Normal file
26
apps/desktop/src/store/sidebar-archive.test.ts
Normal file
@@ -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])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -23,7 +23,8 @@ export async function loadArchivedSessions(): Promise<void> {
|
|||||||
|
|
||||||
$archivedSessions.set(result.sessions)
|
$archivedSessions.set(result.sessions)
|
||||||
} catch {
|
} 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 {
|
} finally {
|
||||||
$archivedSessionsLoading.set(false)
|
$archivedSessionsLoading.set(false)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user