Skip to content

Commit 73db830

Browse files
committed
fix(web): avoid fake session recency and preserve interrupted tool states
1 parent 8d5b2d5 commit 73db830

6 files changed

Lines changed: 249 additions & 3 deletions

File tree

web/package-lock.json

Lines changed: 148 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@types/react": "^18.3.18",
4141
"@types/react-dom": "^18.3.5",
4242
"@vitejs/plugin-react": "^4.3.4",
43+
"@vitest/coverage-v8": "^4.1.5",
4344
"electron": "^33.2.0",
4445
"electron-builder": "^25.1.8",
4546
"happy-dom": "^20.9.0",

web/src/stores/useSessionStore.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ describe('useSessionStore', () => {
134134
expect(session.time).toBe('2026-05-09T09:30:00.000Z')
135135
})
136136

137+
it('fetchSessions uses stable fallback time when created_at and updated_at are both invalid', async () => {
138+
const mockListSessions = vi.fn().mockResolvedValue({
139+
payload: {
140+
sessions: [{
141+
id: 'sess-invalid-time',
142+
title: 'InvalidTime',
143+
created_at: 'not-a-date',
144+
updated_at: '',
145+
}],
146+
},
147+
})
148+
const mockBindStream = vi.fn().mockResolvedValue({})
149+
const mockLoadSession = vi.fn().mockResolvedValue({ payload: { messages: [] } })
150+
const mockAPI = { listSessions: mockListSessions, bindStream: mockBindStream, loadSession: mockLoadSession } as any
151+
152+
await useSessionStore.getState().fetchSessions(mockAPI)
153+
154+
const session = useSessionStore.getState().projects[0].sessions[0]
155+
expect(session.time).toBe('1970-01-01T00:00:00.000Z')
156+
})
157+
137158
it('switchSession concurrently fetches todos and checkpoints', async () => {
138159
const mockBindStream = vi.fn().mockResolvedValue({})
139160
const mockLoadSession = vi.fn().mockResolvedValue({

web/src/stores/useSessionStore.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ function selectSessionDisplayTime(session: APISessionSummary): Date {
120120
}
121121
if (updatedValid) return updated
122122
if (createdValid) return created
123-
return new Date()
123+
// 当后端时间字段都无效时,不伪造“当前时间”,避免损坏记录冒充“刚刚更新”并扰乱排序。
124+
return new Date(0)
124125
}
125126

126127
export type BackendMessage = {

web/src/utils/eventBridge.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,80 @@ describe('eventBridge', () => {
263263
expect(useChatStore.getState().messages[0].toolStatus).toBe('done')
264264
})
265265

266+
it('StopReasonDecided keeps running tool calls unresolved for non-fatal reasons', () => {
267+
const api = createMockGatewayAPI()
268+
useChatStore.getState().addMessage({
269+
id: 'tool-running-nonfatal',
270+
role: 'tool',
271+
type: 'tool_call',
272+
content: '',
273+
toolName: 'bash',
274+
toolCallId: 'tc-nonfatal',
275+
toolStatus: 'running',
276+
timestamp: Date.now(),
277+
})
278+
279+
handleGatewayEvent({
280+
type: EventType.StopReasonDecided,
281+
payload: { payload: { runtime_event_type: EventType.StopReasonDecided, payload: { reason: 'user_interrupt' } } },
282+
session_id: 'sess-1',
283+
run_id: 'run-1',
284+
}, api)
285+
286+
expect(useChatStore.getState().messages[0].toolStatus).toBe('running')
287+
})
288+
289+
it('StopReasonDecided marks running tool calls as error on fatal_error', () => {
290+
const api = createMockGatewayAPI()
291+
useChatStore.getState().addMessage({
292+
id: 'tool-running-fatal',
293+
role: 'tool',
294+
type: 'tool_call',
295+
content: '',
296+
toolName: 'bash',
297+
toolCallId: 'tc-fatal',
298+
toolStatus: 'running',
299+
timestamp: Date.now(),
300+
})
301+
302+
handleGatewayEvent({
303+
type: EventType.StopReasonDecided,
304+
payload: {
305+
payload: {
306+
runtime_event_type: EventType.StopReasonDecided,
307+
payload: { reason: 'fatal_error', detail: 'fatal' },
308+
},
309+
},
310+
session_id: 'sess-1',
311+
run_id: 'run-1',
312+
}, api)
313+
314+
expect(useChatStore.getState().messages[0].toolStatus).toBe('error')
315+
})
316+
317+
it('RunCanceled does not convert running tool calls to done', () => {
318+
const api = createMockGatewayAPI()
319+
useChatStore.getState().addMessage({
320+
id: 'tool-running-canceled',
321+
role: 'tool',
322+
type: 'tool_call',
323+
content: '',
324+
toolName: 'bash',
325+
toolCallId: 'tc-canceled',
326+
toolStatus: 'running',
327+
timestamp: Date.now(),
328+
})
329+
330+
handleGatewayEvent({
331+
type: EventType.RunCanceled,
332+
payload: { payload: { runtime_event_type: EventType.RunCanceled, payload: {} } },
333+
session_id: 'sess-1',
334+
run_id: 'run-1',
335+
}, api)
336+
337+
expect(useChatStore.getState().messages[0].toolStatus).toBe('running')
338+
})
339+
266340
it('BudgetChecked updates runtime insight budget state', () => {
267341
const api = createMockGatewayAPI()
268342
handleGatewayEvent({

web/src/utils/eventBridge.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,9 @@ export function handleGatewayEvent(frame: MessageFrame, gatewayAPI: GatewayAPI)
523523
const detail = strField(eventPayload, 'detail')
524524
useChatStore.getState().setStopReason(reason)
525525
useChatStore.getState().setGenerating(false)
526-
useChatStore.getState().finalizeRunningToolCalls(reason === 'fatal_error' ? 'error' : 'done')
526+
if (reason === 'fatal_error') {
527+
useChatStore.getState().finalizeRunningToolCalls('error')
528+
}
527529
if (reason === 'fatal_error') {
528530
uiStore.showToast(detail || '模型调用失败,请检查配置', 'error')
529531
}
@@ -537,7 +539,6 @@ export function handleGatewayEvent(frame: MessageFrame, gatewayAPI: GatewayAPI)
537539

538540
case EventType.RunCanceled: {
539541
useChatStore.getState().resetGeneratingState()
540-
useChatStore.getState().finalizeRunningToolCalls('done')
541542
uiStore.showToast('Run cancelled', 'info')
542543
break
543544
}

0 commit comments

Comments
 (0)