Skip to content

Commit 650598d

Browse files
committed
Refactor timer to its own component. Pass the start time around
1 parent b74a112 commit 650598d

File tree

5 files changed

+95
-45
lines changed

5 files changed

+95
-45
lines changed

cli/src/chat.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ export const Chat = ({
208208
const mainAgentTimer = useElapsedTime()
209209
const activeSubagentsRef = useRef<Set<string>>(activeSubagents)
210210

211+
// Extract just the startTime for passing to components
212+
const timerStartTime = mainAgentTimer.startTime
213+
211214
useEffect(() => {
212215
isChainInProgressRef.current = isChainInProgress
213216
}, [isChainInProgress])
@@ -663,7 +666,7 @@ export const Chat = ({
663666
const hasStatus = useHasStatus({
664667
isActive: isStatusActive,
665668
clipboardMessage,
666-
timer: mainAgentTimer,
669+
timerStartTime,
667670
nextCtrlCWillExit,
668671
})
669672

@@ -765,7 +768,7 @@ export const Chat = ({
765768
<StatusIndicator
766769
clipboardMessage={clipboardMessage}
767770
isActive={isStatusActive}
768-
timer={mainAgentTimer}
771+
timerStartTime={timerStartTime}
769772
nextCtrlCWillExit={nextCtrlCWillExit}
770773
/>
771774
)
@@ -841,7 +844,7 @@ export const Chat = ({
841844
collapsedAgents={collapsedAgents}
842845
streamingAgents={streamingAgents}
843846
isWaitingForResponse={isWaitingForResponse}
844-
timer={mainAgentTimer}
847+
timerStartTime={timerStartTime}
845848
setCollapsedAgents={setCollapsedAgents}
846849
setFocusedAgentId={setFocusedAgentId}
847850
userOpenedAgents={userOpenedAgents}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useEffect, useState } from 'react'
2+
import { TextAttributes } from '@opentui/core'
3+
4+
import { useTheme } from '../hooks/use-theme'
5+
6+
interface ElapsedTimerProps {
7+
startTime: number | null
8+
suffix?: string
9+
attributes?: number
10+
}
11+
12+
/**
13+
* Self-contained timer component that updates every second.
14+
* Only this component re-renders, not its parents.
15+
*/
16+
export const ElapsedTimer = ({
17+
startTime,
18+
suffix = '',
19+
attributes,
20+
}: ElapsedTimerProps) => {
21+
const theme = useTheme()
22+
const [elapsedSeconds, setElapsedSeconds] = useState<number>(0)
23+
24+
useEffect(() => {
25+
if (!startTime) {
26+
setElapsedSeconds(0)
27+
return
28+
}
29+
30+
const updateElapsed = () => {
31+
const elapsed = Math.floor((Date.now() - startTime) / 1000)
32+
setElapsedSeconds(elapsed)
33+
}
34+
35+
// Update immediately
36+
updateElapsed()
37+
38+
// Then update every second
39+
const interval = setInterval(updateElapsed, 1000)
40+
41+
return () => clearInterval(interval)
42+
}, [startTime])
43+
44+
if (!startTime || elapsedSeconds === 0) {
45+
return null
46+
}
47+
48+
return (
49+
<span fg={theme.secondary} attributes={attributes}>
50+
{elapsedSeconds}s{suffix}
51+
</span>
52+
)
53+
}

cli/src/components/message-block.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TextAttributes } from '@opentui/core'
33
import React, { memo, type ReactNode } from 'react'
44

55
import { AgentBranchItem } from './agent-branch-item'
6+
import { ElapsedTimer } from './elapsed-timer'
67
import { renderToolComponent } from './tools/registry'
78
import { ToolCallItem } from './tools/tool-call-item'
89
import { useTheme } from '../hooks/use-theme'
@@ -14,7 +15,6 @@ import {
1415
type MarkdownPalette,
1516
} from '../utils/markdown-renderer'
1617

17-
import type { ElapsedTimeTracker } from '../hooks/use-elapsed-time'
1818
import type { ContentBlock } from '../types/chat'
1919
import type { ThemeColor } from '../types/theme-system'
2020

@@ -35,7 +35,7 @@ interface MessageBlockProps {
3535
isComplete?: boolean
3636
completionTime?: string
3737
credits?: number
38-
timer: ElapsedTimeTracker
38+
timerStartTime: number | null
3939
textColor?: ThemeColor
4040
timestampColor: string
4141
markdownOptions: { codeBlockWidth: number; palette: MarkdownPalette }
@@ -58,7 +58,7 @@ export const MessageBlock = memo(
5858
isComplete,
5959
completionTime,
6060
credits,
61-
timer,
61+
timerStartTime,
6262
textColor,
6363
timestampColor,
6464
markdownOptions,
@@ -71,9 +71,6 @@ export const MessageBlock = memo(
7171
const theme = useTheme()
7272
const resolvedTextColor = textColor ?? theme.foreground
7373

74-
// Get elapsed time from timer for streaming AI messages
75-
const elapsedSeconds = timer.elapsedSeconds
76-
7774
const renderContentWithMarkdown = (
7875
rawContent: string,
7976
isStreaming: boolean,
@@ -711,18 +708,20 @@ export const MessageBlock = memo(
711708
{isAi && (
712709
<>
713710
{/* Show elapsed time while streaming */}
714-
{isLoading && !isComplete && elapsedSeconds > 0 && (
711+
{isLoading && !isComplete && (
715712
<text
716713
attributes={TextAttributes.DIM}
717714
style={{
718715
wrapMode: 'none',
719-
fg: theme.secondary,
720716
marginTop: 0,
721717
marginBottom: 0,
722718
alignSelf: 'flex-start',
723719
}}
724720
>
725-
{elapsedSeconds}s
721+
<ElapsedTimer
722+
startTime={timerStartTime}
723+
attributes={TextAttributes.DIM}
724+
/>
726725
</text>
727726
)}
728727
{/* Show completion time and credits when complete */}

cli/src/components/message-renderer.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
} from '../utils/markdown-renderer'
1212
import { getDescendantIds, getAncestorIds } from '../utils/message-tree-utils'
1313

14-
import type { ElapsedTimeTracker } from '../hooks/use-elapsed-time'
1514
import type { ChatMessage } from '../types/chat'
1615
import type { ChatTheme } from '../types/theme-system'
1716

@@ -25,7 +24,7 @@ interface MessageRendererProps {
2524
collapsedAgents: Set<string>
2625
streamingAgents: Set<string>
2726
isWaitingForResponse: boolean
28-
timer: ElapsedTimeTracker
27+
timerStartTime: number | null
2928
setCollapsedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
3029
setFocusedAgentId: React.Dispatch<React.SetStateAction<string | null>>
3130
userOpenedAgents: Set<string>
@@ -43,7 +42,7 @@ export const MessageRenderer = (props: MessageRendererProps): ReactNode => {
4342
collapsedAgents,
4443
streamingAgents,
4544
isWaitingForResponse,
46-
timer,
45+
timerStartTime,
4746
setCollapsedAgents,
4847
setFocusedAgentId,
4948
setUserOpenedAgents,
@@ -95,7 +94,7 @@ export const MessageRenderer = (props: MessageRendererProps): ReactNode => {
9594
setUserOpenedAgents={setUserOpenedAgents}
9695
setFocusedAgentId={setFocusedAgentId}
9796
isWaitingForResponse={isWaitingForResponse}
98-
timer={timer}
97+
timerStartTime={timerStartTime}
9998
onToggleCollapsed={onToggleCollapsed}
10099
/>
101100
)
@@ -119,7 +118,7 @@ interface MessageWithAgentsProps {
119118
setUserOpenedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
120119
setFocusedAgentId: React.Dispatch<React.SetStateAction<string | null>>
121120
isWaitingForResponse: boolean
122-
timer: ElapsedTimeTracker
121+
timerStartTime: number | null
123122
onToggleCollapsed: (id: string) => void
124123
}
125124

@@ -139,7 +138,7 @@ const MessageWithAgents = memo(
139138
setUserOpenedAgents,
140139
setFocusedAgentId,
141140
isWaitingForResponse,
142-
timer,
141+
timerStartTime,
143142
onToggleCollapsed,
144143
}: MessageWithAgentsProps): ReactNode => {
145144
const SIDE_GUTTER = 1
@@ -161,7 +160,7 @@ const MessageWithAgents = memo(
161160
setUserOpenedAgents={setUserOpenedAgents}
162161
setFocusedAgentId={setFocusedAgentId}
163162
isWaitingForResponse={isWaitingForResponse}
164-
timer={timer}
163+
timerStartTime={timerStartTime}
165164
onToggleCollapsed={onToggleCollapsed}
166165
/>
167166
)
@@ -272,7 +271,7 @@ const MessageWithAgents = memo(
272271
isComplete={message.isComplete}
273272
completionTime={message.completionTime}
274273
credits={message.credits}
275-
timer={timer}
274+
timerStartTime={timerStartTime}
276275
textColor={textColor}
277276
timestampColor={timestampColor}
278277
markdownOptions={markdownOptions}
@@ -310,7 +309,7 @@ const MessageWithAgents = memo(
310309
isComplete={message.isComplete}
311310
completionTime={message.completionTime}
312311
credits={message.credits}
313-
timer={timer}
312+
timerStartTime={timerStartTime}
314313
textColor={textColor}
315314
timestampColor={timestampColor}
316315
markdownOptions={markdownOptions}
@@ -343,7 +342,7 @@ const MessageWithAgents = memo(
343342
setUserOpenedAgents={setUserOpenedAgents}
344343
setFocusedAgentId={setFocusedAgentId}
345344
isWaitingForResponse={isWaitingForResponse}
346-
timer={timer}
345+
timerStartTime={timerStartTime}
347346
onToggleCollapsed={onToggleCollapsed}
348347
/>
349348
</box>
@@ -369,7 +368,7 @@ interface AgentMessageProps {
369368
setUserOpenedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
370369
setFocusedAgentId: React.Dispatch<React.SetStateAction<string | null>>
371370
isWaitingForResponse: boolean
372-
timer: ElapsedTimeTracker
371+
timerStartTime: number | null
373372
onToggleCollapsed: (id: string) => void
374373
}
375374

@@ -388,7 +387,7 @@ const AgentMessage = memo(
388387
setUserOpenedAgents,
389388
setFocusedAgentId,
390389
isWaitingForResponse,
391-
timer,
390+
timerStartTime,
392391
onToggleCollapsed,
393392
}: AgentMessageProps): ReactNode => {
394393
const agentInfo = message.agent!
@@ -581,7 +580,7 @@ const AgentMessage = memo(
581580
setUserOpenedAgents={setUserOpenedAgents}
582581
setFocusedAgentId={setFocusedAgentId}
583582
isWaitingForResponse={isWaitingForResponse}
584-
timer={timer}
583+
timerStartTime={timerStartTime}
585584
onToggleCollapsed={onToggleCollapsed}
586585
/>
587586
</box>

cli/src/components/status-indicator.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import React, { useEffect, useState } from 'react'
22

3+
import { ElapsedTimer } from './elapsed-timer'
34
import { ShimmerText } from './shimmer-text'
45
import { useTheme } from '../hooks/use-theme'
56
import { getCodebuffClient } from '../utils/codebuff-client'
67

7-
import type { ElapsedTimeTracker } from '../hooks/use-elapsed-time'
8-
98
const useConnectionStatus = () => {
109
const [isConnected, setIsConnected] = useState(true)
1110

@@ -38,17 +37,16 @@ const useConnectionStatus = () => {
3837
export const StatusIndicator = ({
3938
clipboardMessage,
4039
isActive = false,
41-
timer,
40+
timerStartTime,
4241
nextCtrlCWillExit,
4342
}: {
4443
clipboardMessage?: string | null
4544
isActive?: boolean
46-
timer: ElapsedTimeTracker
45+
timerStartTime: number | null
4746
nextCtrlCWillExit: boolean
4847
}) => {
4948
const theme = useTheme()
5049
const isConnected = useConnectionStatus()
51-
const elapsedSeconds = timer.elapsedSeconds
5250

5351
if (nextCtrlCWillExit) {
5452
return <span fg={theme.secondary}>Press Ctrl-C again to exit</span>
@@ -69,19 +67,16 @@ export const StatusIndicator = ({
6967
}
7068

7169
if (isActive) {
72-
// If we have elapsed time > 0, show it
73-
if (elapsedSeconds > 0) {
74-
return <span fg={theme.secondary}>{elapsedSeconds}s</span>
70+
if (!timerStartTime || Date.now() - timerStartTime < 1000) {
71+
return (
72+
<ShimmerText
73+
text="thinking..."
74+
interval={160}
75+
primaryColor={theme.secondary}
76+
/>
77+
)
7578
}
76-
77-
// Otherwise show thinking...
78-
return (
79-
<ShimmerText
80-
text="thinking..."
81-
interval={160}
82-
primaryColor={theme.secondary}
83-
/>
84-
)
79+
return <ElapsedTimer startTime={timerStartTime} />
8580
}
8681

8782
return null
@@ -90,17 +85,18 @@ export const StatusIndicator = ({
9085
export const useHasStatus = (params: {
9186
isActive: boolean
9287
clipboardMessage?: string | null
93-
timer?: ElapsedTimeTracker
88+
timerStartTime?: number | null
9489
nextCtrlCWillExit: boolean
9590
}): boolean => {
96-
const { isActive, clipboardMessage, timer, nextCtrlCWillExit } = params
91+
const { isActive, clipboardMessage, timerStartTime, nextCtrlCWillExit } =
92+
params
9793

9894
const isConnected = useConnectionStatus()
9995
return (
10096
isConnected === false ||
10197
isActive ||
10298
Boolean(clipboardMessage) ||
103-
Boolean(timer?.startTime) ||
99+
Boolean(timerStartTime) ||
104100
nextCtrlCWillExit
105101
)
106102
}

0 commit comments

Comments
 (0)