fix: restore cached reply duration when switching conversations
Keep execution statistics by conversation and project so returning to a viewed conversation immediately restores its reply duration while refreshing in the background. Preserve late-result isolation and clear failed scopes. Validation: 15 focused tests, typecheck and lint passed. Full suite: 4878 passed, 9 navigation settings assertion failures and 1 Agent package timeout outside this change. Release note: Switching back to a viewed conversation now immediately displays its cached cumulative Agent reply duration while refreshing the statistics.
This commit is contained in:
@@ -49,6 +49,31 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('useExecutionStats', () => {
|
||||
it.each(['conversation', 'project'] as const)(
|
||||
'restores cached statistics immediately when returning to a %s', async (scope) => {
|
||||
const { result, rerender } = renderStats()
|
||||
await act(async () => {})
|
||||
const cached = result.current
|
||||
getExecutionStats.mockResolvedValue(stats(20))
|
||||
rerender({
|
||||
...initialProps, conversationId: 'chat-b',
|
||||
projectId: scope === 'project' ? 'project-b' : 'project-a'
|
||||
})
|
||||
await act(async () => {})
|
||||
const conversation = deferred()
|
||||
const project = deferred()
|
||||
getExecutionStats.mockReturnValueOnce(conversation.promise).mockReturnValueOnce(project.promise)
|
||||
rerender(initialProps)
|
||||
expect(result.current.conversation).toBe(cached.conversation)
|
||||
expect(result.current.project).toEqual(scope === 'project' ? cached.project : stats(20))
|
||||
await act(async () => {
|
||||
conversation.resolve(stats(30))
|
||||
project.resolve(stats(40))
|
||||
})
|
||||
expect(result.current).toEqual({ conversation: stats(30), project: stats(40) })
|
||||
}
|
||||
)
|
||||
|
||||
it('retains snapshot references for identical cached polling results', async () => {
|
||||
getExecutionStats.mockImplementation(async () => stats(10))
|
||||
const { result } = renderStats()
|
||||
|
||||
@@ -8,10 +8,9 @@ export function useExecutionStats(
|
||||
revision: string,
|
||||
enabled: boolean
|
||||
): { conversation?: ExecutionStats; project?: ExecutionStats } {
|
||||
const [snapshot, setSnapshot] = useState<{
|
||||
conversationId?: string; projectId?: string
|
||||
conversation?: ExecutionStats; project?: ExecutionStats
|
||||
}>({})
|
||||
const [snapshots, setSnapshots] = useState(() => new Map<string, ExecutionStats>())
|
||||
const conversationKey = JSON.stringify([projectId, conversationId])
|
||||
const projectKey = JSON.stringify([projectId])
|
||||
useEffect(() => {
|
||||
if (!enabled || !projectId) return
|
||||
let cancelled = false
|
||||
@@ -26,17 +25,20 @@ export function useExecutionStats(
|
||||
window.goodbuddy.tasks.getExecutionStats({ projectId })
|
||||
])
|
||||
pending = false
|
||||
if (!cancelled) startTransition(() => setSnapshot((current) => {
|
||||
const next = {
|
||||
conversationId, projectId,
|
||||
conversation: conversation.status === 'fulfilled' ? conversation.value : undefined,
|
||||
project: project.status === 'fulfilled' ? project.value : undefined
|
||||
}
|
||||
if (!cancelled) startTransition(() => setSnapshots((current) => {
|
||||
if (cancelled) return current
|
||||
const next = new Map(current)
|
||||
// IPC clones cached snapshots. Preserve references when their contents are unchanged
|
||||
// so idle polling does not rerender App and rebuild task duration maps.
|
||||
return current.conversationId === conversationId && current.projectId === projectId &&
|
||||
JSON.stringify(current.conversation) === JSON.stringify(next.conversation) &&
|
||||
JSON.stringify(current.project) === JSON.stringify(next.project) ? current : next
|
||||
for (const [key, result] of [[conversationKey, conversation], [projectKey, project]] as const) {
|
||||
const value = result.status === 'fulfilled' ? result.value : undefined
|
||||
if (JSON.stringify(current.get(key)) === JSON.stringify(value)) continue
|
||||
if (value) next.set(key, value)
|
||||
else next.delete(key)
|
||||
}
|
||||
return next.size === current.size &&
|
||||
next.get(conversationKey) === current.get(conversationKey) &&
|
||||
next.get(projectKey) === current.get(projectKey) ? current : next
|
||||
}))
|
||||
}
|
||||
void refresh()
|
||||
@@ -50,10 +52,10 @@ export function useExecutionStats(
|
||||
window.removeEventListener('focus', onFocus)
|
||||
document.removeEventListener('visibilitychange', onFocus)
|
||||
}
|
||||
}, [conversationId, projectId, revision, enabled])
|
||||
}, [conversationId, projectId, conversationKey, projectKey, revision, enabled])
|
||||
return {
|
||||
conversation: enabled && snapshot.projectId === projectId && snapshot.conversationId === conversationId
|
||||
? snapshot.conversation : undefined,
|
||||
project: enabled && snapshot.projectId === projectId ? snapshot.project : undefined
|
||||
conversation: enabled && projectId && conversationId
|
||||
? snapshots.get(conversationKey) : undefined,
|
||||
project: enabled && projectId ? snapshots.get(projectKey) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user