diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx index aae481b5..45124d1b 100644 --- a/desktop/src/__tests__/pages.test.tsx +++ b/desktop/src/__tests__/pages.test.tsx @@ -283,11 +283,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -322,11 +320,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -369,11 +365,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -435,11 +429,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, sendMessage, @@ -504,11 +496,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, sendMessage, @@ -559,11 +549,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, sendMessage, @@ -614,7 +602,6 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [ { name: 'cost', description: 'Show token usage and costs' }, @@ -624,7 +611,6 @@ describe('Content-only pages render without errors', () => { })), ], agentTaskNotifications: {}, - elapsedTimer: null, }, }, sendMessage, @@ -677,11 +663,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, sendMessage, @@ -761,11 +745,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 50_000, output_tokens: 1_000 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -816,11 +798,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -879,11 +859,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -958,11 +936,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1040,11 +1016,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 72_000, output_tokens: 2_000 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1105,11 +1079,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1201,11 +1173,9 @@ describe('Content-only pages render without errors', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 26_000, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/components/chat/AskUserQuestion.test.tsx b/desktop/src/components/chat/AskUserQuestion.test.tsx index 5c8c6316..1d1d92b6 100644 --- a/desktop/src/components/chat/AskUserQuestion.test.tsx +++ b/desktop/src/components/chat/AskUserQuestion.test.tsx @@ -62,11 +62,9 @@ describe('AskUserQuestion', () => { }, }, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/components/chat/ChatInput.test.tsx b/desktop/src/components/chat/ChatInput.test.tsx index 958dc0af..aa30691a 100644 --- a/desktop/src/components/chat/ChatInput.test.tsx +++ b/desktop/src/components/chat/ChatInput.test.tsx @@ -170,11 +170,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -235,11 +233,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, [historySessionId]: { messages: [{ id: 'history-message', type: 'assistant_text', content: 'ready', timestamp: 1 }], @@ -252,11 +248,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -342,11 +336,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -396,11 +388,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -440,11 +430,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -484,11 +472,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, permissionMode: 'plan', }, }, @@ -551,11 +537,9 @@ describe('ChatInput file mentions', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/components/chat/ElapsedTime.tsx b/desktop/src/components/chat/ElapsedTime.tsx new file mode 100644 index 00000000..ab525d5d --- /dev/null +++ b/desktop/src/components/chat/ElapsedTime.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react' + +function formatElapsed(seconds: number): string { + if (seconds < 60) return `${seconds}s` + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${m}m ${s}s` +} + +export function ElapsedTime({ startedAt }: { startedAt: number }) { + const [now, setNow] = useState(() => Date.now()) + + useEffect(() => { + setNow(Date.now()) + const timer = window.setInterval(() => setNow(Date.now()), 1000) + return () => window.clearInterval(timer) + }, [startedAt]) + + const elapsedSeconds = Math.max(0, Math.floor((now - startedAt) / 1000)) + if (elapsedSeconds === 0) return null + + return ( + + {formatElapsed(elapsedSeconds)} + + ) +} diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 244ff413..84770dff 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -34,12 +34,10 @@ function makeSessionState(overrides: Partial = {}): PerSessionS activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', apiRetry: null, slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, composerPrefill: null, ...overrides, } diff --git a/desktop/src/components/chat/StreamingIndicator.test.tsx b/desktop/src/components/chat/StreamingIndicator.test.tsx new file mode 100644 index 00000000..11081817 --- /dev/null +++ b/desktop/src/components/chat/StreamingIndicator.test.tsx @@ -0,0 +1,130 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { act, cleanup, render, screen } from '@testing-library/react' +import '@testing-library/jest-dom' + +vi.mock('../../api/websocket', () => ({ + wsManager: { + connect: vi.fn(), + disconnect: vi.fn(), + onMessage: vi.fn(() => () => {}), + clearHandlers: vi.fn(), + send: vi.fn(), + }, +})) + +import { StreamingIndicator } from './StreamingIndicator' +import { useChatStore, type PerSessionState } from '../../stores/chatStore' +import { useSettingsStore } from '../../stores/settingsStore' +import { useTabStore } from '../../stores/tabStore' +import type { SessionState } from '../../types/chat' + +const NOW = 1_000_000 + +function sessionWithTurn(sessionId: string, turn: SessionState['turn']): PerSessionState { + return { + messages: [], + chatState: 'thinking', + connectionState: 'connected', + streamingText: '', + streamingToolInput: '', + activeToolUseId: null, + activeToolName: null, + activeThinkingId: null, + pendingPermission: null, + tokenUsage: { input_tokens: 0, output_tokens: 0 }, + statusVerb: '', + slashCommands: [], + agentTaskNotifications: {}, + mirror: { + id: sessionId, + version: 1, + permission: { desired: 'default', effective: 'default' }, + runtime: { desired: null, effective: null }, + title: { text: null, source: null }, + workspace: { workDir: '/repo', projectRoot: '/repo', workDirExists: true }, + process: { phase: 'running', generation: 1 }, + turn, + pendingPermissions: [], + slashCommands: [], + activeGoal: null, + backgroundTasks: [], + team: null, + clientCount: 1, + }, + } +} + +function showTab(sessionId: string) { + act(() => { + useTabStore.setState({ + activeTabId: sessionId, + tabs: [ + { sessionId: 'a', title: 'A', type: 'session', status: 'running' }, + { sessionId: 'b', title: 'B', type: 'session', status: 'running' }, + ], + }) + }) +} + +describe('StreamingIndicator elapsed time', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(NOW) + useSettingsStore.setState({ locale: 'en' }) + useChatStore.setState({ + sessions: { + a: sessionWithTurn('a', { phase: 'running', startedAt: NOW - 65_000, activity: 'thinking' }), + b: sessionWithTurn('b', { phase: 'running', startedAt: NOW - 3_000, activity: 'thinking' }), + }, + }) + showTab('a') + }) + + afterEach(() => { + cleanup() + vi.useRealTimers() + }) + + it('counts from the turn start the server reported and ticks every second', () => { + render() + expect(screen.getByText('1m 5s')).toBeInTheDocument() + + act(() => { + vi.advanceTimersByTime(1000) + }) + expect(screen.getByText('1m 6s')).toBeInTheDocument() + }) + + it('shows the time of the session switched to', () => { + render() + showTab('b') + + expect(screen.getByText('3s')).toBeInTheDocument() + expect(screen.queryByText('1m 5s')).not.toBeInTheDocument() + }) + + it('keeps counting from the same start after a reconnect snapshot', () => { + render() + act(() => { + vi.advanceTimersByTime(2000) + useChatStore.setState({ + sessions: { + ...useChatStore.getState().sessions, + a: sessionWithTurn('a', { phase: 'running', startedAt: NOW - 65_000, activity: 'streaming' }), + }, + }) + }) + + expect(screen.getByText('1m 7s')).toBeInTheDocument() + }) + + it('shows no time when the server has not started a turn', () => { + useChatStore.setState({ sessions: { a: sessionWithTurn('a', { phase: 'idle' }) } }) + render() + + act(() => { + vi.advanceTimersByTime(5000) + }) + expect(screen.queryByText(/^\d+s$/)).not.toBeInTheDocument() + }) +}) diff --git a/desktop/src/components/chat/StreamingIndicator.tsx b/desktop/src/components/chat/StreamingIndicator.tsx index a2e35d70..38494533 100644 --- a/desktop/src/components/chat/StreamingIndicator.tsx +++ b/desktop/src/components/chat/StreamingIndicator.tsx @@ -3,13 +3,7 @@ import { RefreshCw } from 'lucide-react' import { useChatStore } from '../../stores/chatStore' import { useTabStore } from '../../stores/tabStore' import { useTranslation, type TranslationKey } from '../../i18n' - -function formatElapsed(seconds: number): string { - if (seconds < 60) return `${seconds}s` - const m = Math.floor(seconds / 60) - const s = seconds % 60 - return `${m}m ${s}s` -} +import { ElapsedTime } from './ElapsedTime' function translateServerVerb( t: (key: TranslationKey) => string, @@ -40,7 +34,8 @@ export function StreamingIndicator() { const chatState = sessionState?.chatState ?? 'idle' const statusVerb = sessionState?.statusVerb ?? '' const apiRetry = sessionState?.apiRetry ?? null - const elapsedSeconds = sessionState?.elapsedSeconds ?? 0 + const turn = sessionState?.mirror?.turn + const turnStartedAt = turn && turn.phase !== 'idle' ? turn.startedAt : undefined const tokenUsage = sessionState?.tokenUsage ?? { input_tokens: 0, output_tokens: 0 } useEffect(() => { @@ -101,11 +96,7 @@ export function StreamingIndicator() {
✦ {verb}... - {elapsedSeconds > 0 && ( - - {formatElapsed(elapsedSeconds)} - - )} + {turnStartedAt !== undefined && } {tokenUsage.output_tokens > 0 && ( · ↓ {tokenUsage.output_tokens} diff --git a/desktop/src/components/chat/chatBlocks.test.tsx b/desktop/src/components/chat/chatBlocks.test.tsx index 83add1f8..a4d5dfae 100644 --- a/desktop/src/components/chat/chatBlocks.test.tsx +++ b/desktop/src/components/chat/chatBlocks.test.tsx @@ -214,11 +214,9 @@ describe('chat blocks', () => { }, }, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/components/workspace/WorkspacePanel.test.tsx b/desktop/src/components/workspace/WorkspacePanel.test.tsx index 7d0772c1..6ac9be58 100644 --- a/desktop/src/components/workspace/WorkspacePanel.test.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.test.tsx @@ -1255,11 +1255,9 @@ describe('WorkspacePanel', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/pages/ActiveSession.test.tsx b/desktop/src/pages/ActiveSession.test.tsx index 7db6f21c..a3252cd6 100644 --- a/desktop/src/pages/ActiveSession.test.tsx +++ b/desktop/src/pages/ActiveSession.test.tsx @@ -126,11 +126,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -193,11 +191,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -260,11 +256,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -333,11 +327,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -394,11 +386,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -454,11 +444,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -532,11 +520,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -586,11 +572,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -676,11 +660,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -730,11 +712,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -782,11 +762,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -839,11 +817,9 @@ describe('ActiveSession task polling', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index d5a740b4..f9b48c2c 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -155,13 +155,11 @@ function makeSession(overrides: Partial = {}): PerSessionState activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', apiRetry: null, slashCommands: [], agentTaskNotifications: {}, backgroundAgentTasks: {}, - elapsedTimer: null, ...overrides, } } @@ -623,11 +621,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -707,11 +703,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -820,11 +814,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [{ name: 'old-command', description: 'Old command' }], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1234,11 +1226,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1298,11 +1288,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1328,11 +1316,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1622,11 +1608,9 @@ describe('chatStore history mapping', () => { activeThinkingId: 'thinking-1', pendingPermission: null, tokenUsage: { input_tokens: 12, output_tokens: 34 }, - elapsedSeconds: 5, statusVerb: 'Thinking', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1694,11 +1678,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1736,11 +1718,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: 'Compacting conversation', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1787,11 +1767,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1831,11 +1809,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -1881,11 +1857,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2112,11 +2086,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2156,11 +2128,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2264,11 +2234,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2553,11 +2521,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2598,6 +2564,21 @@ describe('chatStore history mapping', () => { vi.useRealTimers() }) + it('leaves the store unchanged while a turn runs', () => { + vi.useFakeTimers() + useChatStore.setState({ sessions: { [TEST_SESSION_ID]: makeSession({ chatState: 'idle' }) } }) + sendSnapshot(mirrorState()) + + useChatStore.getState().sendMessage(TEST_SESSION_ID, 'hello') + sendPatch(2, { turn: { phase: 'running', startedAt: Date.now(), activity: 'thinking' } }) + const sessions = useChatStore.getState().sessions + + vi.advanceTimersByTime(5000) + + expect(useChatStore.getState().sessions).toBe(sessions) + vi.useRealTimers() + }) + it('shows worktree creation while the CLI starts and clears it once it runs', () => { useChatStore.setState({ sessions: { [TEST_SESSION_ID]: makeSession({ chatState: 'idle' }) } }) @@ -2661,11 +2642,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2712,11 +2691,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2743,11 +2720,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2781,11 +2756,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) @@ -2821,11 +2794,9 @@ describe('chatStore history mapping', () => { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, }, }, }) diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 297b1997..ffc44088 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -67,14 +67,12 @@ export type PerSessionState = { description?: string } | null tokenUsage: TokenUsage - elapsedSeconds: number statusVerb: string apiRetry?: ApiRetryState | null slashCommands: Array<{ name: string; description: string; argumentHint?: string }> agentTaskNotifications: Record backgroundAgentTasks?: Record activeGoal?: ActiveGoalState | null - elapsedTimer: ReturnType | null composerPrefill?: { text: string attachments?: UIAttachment[] @@ -97,14 +95,12 @@ const DEFAULT_SESSION_STATE: PerSessionState = { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', apiRetry: null, slashCommands: [], agentTaskNotifications: {}, backgroundAgentTasks: {}, activeGoal: null, - elapsedTimer: null, composerPrefill: null, composerInsertion: null, composerDraft: null, @@ -763,8 +759,6 @@ export const useChatStore = create((set, get) => ({ prewarmPending.delete(sessionId) reconnectingSessions.delete(sessionId) reloadAtTurnEnd.delete(sessionId) - const session = get().sessions[sessionId] - if (session?.elapsedTimer) clearInterval(session.elapsedTimer) if (pendingDeltaBySession.has(sessionId)) { const text = consumePendingDelta(sessionId) set((s) => ({ sessions: updateSessionIn(s.sessions, sessionId, (sess) => ({ streamingText: sess.streamingText + text })) })) @@ -843,14 +837,6 @@ export const useChatStore = create((set, get) => ({ ...(isMemberSession ? { pending: true } : {}), }) - if (!isMemberSession && session.elapsedTimer) clearInterval(session.elapsedTimer) - - const timer = !isMemberSession - ? setInterval(() => { - set((st) => ({ sessions: updateSessionIn(st.sessions, sessionId, (sess) => ({ elapsedSeconds: sess.elapsedSeconds + 1 })) })) - }, 1000) - : null - return { sessions: { ...s.sessions, @@ -858,11 +844,9 @@ export const useChatStore = create((set, get) => ({ ...session, messages: newMessages, chatState: 'thinking', - elapsedSeconds: 0, streamingText: '', statusVerb: isMemberSession ? '' : randomSpinnerVerb(), apiRetry: null, - elapsedTimer: timer, connectionState: isMemberSession ? 'connected' : session.connectionState, }, }, @@ -932,7 +916,6 @@ export const useChatStore = create((set, get) => ({ set((s) => { const session = s.sessions[sessionId] if (!session) return s - if (session.elapsedTimer) clearInterval(session.elapsedTimer) return { sessions: { ...s.sessions, @@ -941,7 +924,6 @@ export const useChatStore = create((set, get) => ({ chatState: 'idle', pendingPermission: null, apiRetry: null, - elapsedTimer: null, }, }, } @@ -1014,7 +996,6 @@ export const useChatStore = create((set, get) => ({ set((state) => { const session = state.sessions[sessionId] if (!session) return state - if (session.elapsedTimer) clearInterval(session.elapsedTimer) return { sessions: updateSessionIn(state.sessions, sessionId, () => ({ messages: mergeBackgroundTaskMessages(uiMessages, restoredBackgroundTasks), @@ -1028,7 +1009,6 @@ export const useChatStore = create((set, get) => ({ streamingText: '', streamingToolInput: '', pendingPermission: null, - elapsedTimer: null, statusVerb: '', apiRetry: null, })), @@ -1414,13 +1394,11 @@ export const useChatStore = create((set, get) => ({ } else if (text !== session.streamingText) { update(() => ({ streamingText: text })) } - if (session.elapsedTimer) clearInterval(session.elapsedTimer) update(() => ({ tokenUsage: msg.usage, chatState: 'idle', activeThinkingId: null, pendingPermission: null, - elapsedTimer: null, apiRetry: null, })) useTabStore.getState().updateTabStatus(sessionId, 'idle') @@ -1461,13 +1439,6 @@ export const useChatStore = create((set, get) => ({ } }) useTabStore.getState().updateTabStatus(sessionId, 'error') - { - const session = get().sessions[sessionId] - if (session?.elapsedTimer) { - clearInterval(session.elapsedTimer) - update(() => ({ elapsedTimer: null })) - } - } break case 'team_created': @@ -1500,8 +1471,6 @@ export const useChatStore = create((set, get) => ({ }) } if (msg.subtype === 'session_cleared') { - const session = get().sessions[sessionId] - if (session?.elapsedTimer) clearInterval(session.elapsedTimer) update(() => ({ messages: [], streamingText: '', @@ -1511,8 +1480,6 @@ export const useChatStore = create((set, get) => ({ activeThinkingId: null, pendingPermission: null, chatState: 'idle', - elapsedTimer: null, - elapsedSeconds: 0, statusVerb: '', apiRetry: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, @@ -1801,8 +1768,6 @@ function applyMirror(sessionId: string, next: SessionState): void { messages = dropTailCompactingCompactSummary(messages) } - let elapsedTimer = current.elapsedTimer - let elapsedSeconds = current.elapsedSeconds if (!isRunning) { const pendingText = `${streamingText}${consumePendingDelta(sessionId)}` if (wasRunning && pendingText.trim()) { @@ -1811,15 +1776,6 @@ function applyMirror(sessionId: string, next: SessionState): void { } else { streamingText = pendingText } - if (elapsedTimer) clearInterval(elapsedTimer) - elapsedTimer = null - } else if (!elapsedTimer) { - elapsedSeconds = next.turn.startedAt ? Math.max(0, Math.floor((now - next.turn.startedAt) / 1000)) : 0 - elapsedTimer = setInterval(() => { - useChatStore.setState((st) => ({ - sessions: updateSessionIn(st.sessions, sessionId, (sess) => ({ elapsedSeconds: sess.elapsedSeconds + 1 })), - })) - }, 1000) } const previousVerb = specialStatusVerb(previous) @@ -1836,8 +1792,6 @@ function applyMirror(sessionId: string, next: SessionState): void { permissionMode: displayedPermissionMode(next), pendingPermission: pendingPermissionFromMirror(next), chatState: chatStateFromMirror(next), - elapsedTimer, - elapsedSeconds, ...(nextVerb ? { statusVerb: nextVerb } : previousVerb ? { statusVerb: '' } : {}), ...(!isRunning ? { activeThinkingId: null, apiRetry: null, statusVerb: nextVerb } : {}), }, diff --git a/desktop/src/stores/teamStore.ts b/desktop/src/stores/teamStore.ts index 7c30d7d9..81e239ee 100644 --- a/desktop/src/stores/teamStore.ts +++ b/desktop/src/stores/teamStore.ts @@ -26,11 +26,9 @@ function createMemberSessionState() { activeThinkingId: null, pendingPermission: null, tokenUsage: { input_tokens: 0, output_tokens: 0 }, - elapsedSeconds: 0, statusVerb: '', slashCommands: [], agentTaskNotifications: {}, - elapsedTimer: null, } }