From d5820908ffdf0afa7fe881021f2f3b6888b56c16 Mon Sep 17 00:00:00 2001 From: prince kumar Date: Thu, 22 Oct 2026 21:58:39 +0530 Subject: [PATCH 1/5] perf(desktop): stop ticking the turn's elapsed time in the store A 1s interval in chatStore wrote elapsedSeconds into the session while a turn ran, so every subscriber of the session re-rendered once a second. Both intervals (the one started on send and the one started when a mirror turn begins running) are gone, together with the elapsedSeconds and elapsedTimer fields and the clearInterval calls that went with them. The elapsed time now comes from turn.startedAt in the server's session state. --- desktop/src/stores/chatStore.test.ts | 59 +++++++--------------------- desktop/src/stores/chatStore.ts | 46 ---------------------- desktop/src/stores/teamStore.ts | 2 - 3 files changed, 15 insertions(+), 92 deletions(-) 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, } } From 1a61ec4fc22ad9d1e49410fb19a38eb91594096b Mon Sep 17 00:00:00 2001 From: prince kumar Date: Thu, 22 Oct 2026 21:59:58 +0530 Subject: [PATCH 2/5] perf(desktop): show the elapsed time from its own 1s timer ElapsedTime keeps its own 1s timer and computes the value from the turn start the server reports, so only it re-renders each second, and the time stays right after switching sessions, a reconnect or a refresh. Test fixtures drop the removed session fields. --- .../components/chat/AskUserQuestion.test.tsx | 2 - .../src/components/chat/ChatInput.test.tsx | 16 --- desktop/src/components/chat/ElapsedTime.tsx | 27 ++++ .../src/components/chat/MessageList.test.tsx | 2 - .../chat/StreamingIndicator.test.tsx | 130 ++++++++++++++++++ .../components/chat/StreamingIndicator.tsx | 17 +-- .../src/components/chat/chatBlocks.test.tsx | 2 - 7 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 desktop/src/components/chat/ElapsedTime.tsx create mode 100644 desktop/src/components/chat/StreamingIndicator.test.tsx 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, }, }, }) From b7252fc776bc6a0728c454fa53edbe09ae50253c Mon Sep 17 00:00:00 2001 From: prince kumar Date: Thu, 22 Oct 2026 22:00:29 +0530 Subject: [PATCH 3/5] test(desktop): drop the removed elapsed fields from page fixtures --- desktop/src/__tests__/pages.test.tsx | 30 ---------------------------- 1 file changed, 30 deletions(-) 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, }, }, }) From 4979f32b039a9f69167267a7711ff10dafd9f893 Mon Sep 17 00:00:00 2001 From: prince kumar Date: Thu, 22 Oct 2026 22:01:00 +0530 Subject: [PATCH 4/5] test(desktop): drop the removed elapsed fields from workspace fixtures --- desktop/src/components/workspace/WorkspacePanel.test.tsx | 2 -- 1 file changed, 2 deletions(-) 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, }, }, }) From 4620ce4cf563c06c65e864315c23f8887dafff1a Mon Sep 17 00:00:00 2001 From: prince kumar Date: Thu, 22 Oct 2026 22:01:29 +0530 Subject: [PATCH 5/5] test(desktop): drop the removed elapsed fields from session fixtures --- desktop/src/pages/ActiveSession.test.tsx | 24 ------------------------ 1 file changed, 24 deletions(-) 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, }, }, })