Skip to content

Commit d7ca1bf

Browse files
brandonkachenclaude
andcommitted
feat: show elapsed time in streaming messages
Created a shared useElapsedTime hook that both the status indicator and message display can use. Now streaming AI messages show elapsed time (e.g., "5s") that updates every second while the message is being generated. Once complete, it switches to showing the completion time and credits as before. Changes: - Added useElapsedTime hook for tracking elapsed seconds - Updated StatusIndicator to use the shared hook - Updated MessageBlock to show elapsed time for streaming messages - Passed streamStartTime through message renderer chain 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent dca0058 commit d7ca1bf

File tree

5 files changed

+82
-37
lines changed

5 files changed

+82
-37
lines changed

cli/src/chat.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,7 @@ export const App = ({
10081008
collapsedAgents,
10091009
streamingAgents,
10101010
isWaitingForResponse,
1011+
streamStartTime: mainAgentStreamStartTime,
10111012
setCollapsedAgents,
10121013
setFocusedAgentId,
10131014
registerAgentRef,

cli/src/components/message-block.tsx

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { type ReactNode } from 'react'
44
import { pluralize } from '@codebuff/common/util/string'
55

66
import { BranchItem } from './branch-item'
7+
import { useElapsedTime } from '../hooks/use-elapsed-time'
78
import { getToolDisplayInfo } from '../utils/codebuff-client'
89
import {
910
renderMarkdown,
@@ -32,6 +33,7 @@ interface MessageBlockProps {
3233
isComplete?: boolean
3334
completionTime?: string
3435
credits?: number
36+
streamStartTime: number | null
3537
theme: ChatTheme
3638
textColor: string
3739
timestampColor: string
@@ -55,6 +57,7 @@ export const MessageBlock = ({
5557
isComplete,
5658
completionTime,
5759
credits,
60+
streamStartTime,
5861
theme,
5962
textColor,
6063
timestampColor,
@@ -66,6 +69,10 @@ export const MessageBlock = ({
6669
onToggleCollapsed,
6770
registerAgentRef,
6871
}: MessageBlockProps): ReactNode => {
72+
// Calculate elapsed time for streaming AI messages
73+
const elapsedSeconds = useElapsedTime(
74+
isAi && isLoading && !isComplete ? streamStartTime : null,
75+
)
6976
const computeBranchChar = (indentLevel: number, isLastBranch: boolean) =>
7077
`${' '.repeat(indentLevel)}${isLastBranch ? '└─ ' : '├─ '}`
7178

@@ -514,20 +521,40 @@ export const MessageBlock = ({
514521
)
515522
})()
516523
)}
517-
{isAi && isComplete && (completionTime || credits) && (
518-
<text
519-
wrap={false}
520-
attributes={TextAttributes.DIM}
521-
style={{
522-
fg: theme.statusSecondary,
523-
marginTop: 0,
524-
marginBottom: 0,
525-
alignSelf: 'flex-start',
526-
}}
527-
>
528-
{completionTime}
529-
{credits && ` • ${credits} credits`}
530-
</text>
524+
{isAi && (
525+
<>
526+
{/* Show elapsed time while streaming */}
527+
{isLoading && !isComplete && elapsedSeconds > 0 && (
528+
<text
529+
wrap={false}
530+
attributes={TextAttributes.DIM}
531+
style={{
532+
fg: theme.statusSecondary,
533+
marginTop: 0,
534+
marginBottom: 0,
535+
alignSelf: 'flex-start',
536+
}}
537+
>
538+
{elapsedSeconds}s
539+
</text>
540+
)}
541+
{/* Show completion time and credits when complete */}
542+
{isComplete && (completionTime || credits) && (
543+
<text
544+
wrap={false}
545+
attributes={TextAttributes.DIM}
546+
style={{
547+
fg: theme.statusSecondary,
548+
marginTop: 0,
549+
marginBottom: 0,
550+
alignSelf: 'flex-start',
551+
}}
552+
>
553+
{completionTime}
554+
{credits && ` • ${credits} credits`}
555+
</text>
556+
)}
557+
</>
531558
)}
532559
</>
533560
)

cli/src/components/status-indicator.tsx

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useState } from 'react'
22

33
import { ShimmerText } from './shimmer-text'
4+
import { useElapsedTime } from '../hooks/use-elapsed-time'
45
import { getCodebuffClient } from '../utils/codebuff-client'
56
import { logger } from '../utils/logger'
67

@@ -49,29 +50,7 @@ export const StatusIndicator = ({
4950
streamStartTime?: number | null
5051
}) => {
5152
const isConnected = useConnectionStatus()
52-
const [elapsedSeconds, setElapsedSeconds] = useState<number>(0)
53-
54-
55-
// Update elapsed time every second while streaming
56-
useEffect(() => {
57-
if (!streamStartTime) {
58-
setElapsedSeconds(0)
59-
return
60-
}
61-
62-
const updateElapsed = () => {
63-
const elapsed = Math.floor((Date.now() - streamStartTime) / 1000)
64-
setElapsedSeconds(elapsed)
65-
}
66-
67-
// Update immediately
68-
updateElapsed()
69-
70-
// Then update every second
71-
const interval = setInterval(updateElapsed, 1000)
72-
73-
return () => clearInterval(interval)
74-
}, [streamStartTime])
53+
const elapsedSeconds = useElapsedTime(streamStartTime)
7554

7655
if (clipboardMessage) {
7756
return <span fg={theme.statusAccent}>{clipboardMessage}</span>

cli/src/hooks/use-elapsed-time.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useState } from 'react'
2+
3+
/**
4+
* Hook to track elapsed time from a start timestamp
5+
* Updates every second while the start time is set
6+
*
7+
* @param startTime - Timestamp in milliseconds (Date.now()) when timing started, or null/undefined to reset
8+
* @returns elapsedSeconds - Number of seconds elapsed since startTime, or 0 if no startTime
9+
*/
10+
export const useElapsedTime = (startTime: number | null | undefined): number => {
11+
const [elapsedSeconds, setElapsedSeconds] = useState<number>(0)
12+
13+
useEffect(() => {
14+
if (!startTime) {
15+
setElapsedSeconds(0)
16+
return
17+
}
18+
19+
const updateElapsed = () => {
20+
const elapsed = Math.floor((Date.now() - startTime) / 1000)
21+
setElapsedSeconds(elapsed)
22+
}
23+
24+
// Update immediately
25+
updateElapsed()
26+
27+
// Then update every second
28+
const interval = setInterval(updateElapsed, 1000)
29+
30+
return () => clearInterval(interval)
31+
}, [startTime])
32+
33+
return elapsedSeconds
34+
}

cli/src/hooks/use-message-renderer.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface UseMessageRendererProps {
2323
collapsedAgents: Set<string>
2424
streamingAgents: Set<string>
2525
isWaitingForResponse: boolean
26+
streamStartTime: number | null
2627
setCollapsedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
2728
setFocusedAgentId: React.Dispatch<React.SetStateAction<string | null>>
2829
registerAgentRef: (agentId: string, element: any) => void
@@ -42,6 +43,7 @@ export const useMessageRenderer = (
4243
collapsedAgents,
4344
streamingAgents,
4445
isWaitingForResponse,
46+
streamStartTime,
4547
setCollapsedAgents,
4648
setFocusedAgentId,
4749
registerAgentRef,
@@ -352,6 +354,7 @@ export const useMessageRenderer = (
352354
isComplete={message.isComplete}
353355
completionTime={message.completionTime}
354356
credits={message.credits}
357+
streamStartTime={streamStartTime}
355358
theme={theme}
356359
textColor={textColor}
357360
timestampColor={timestampColor}
@@ -402,6 +405,7 @@ export const useMessageRenderer = (
402405
isComplete={message.isComplete}
403406
completionTime={message.completionTime}
404407
credits={message.credits}
408+
streamStartTime={streamStartTime}
405409
theme={theme}
406410
textColor={textColor}
407411
timestampColor={timestampColor}

0 commit comments

Comments
 (0)