From f012e34a3870d303d45e0a52ccac94930fc7553b Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Wed, 14 Jan 2026 10:01:15 +0100 Subject: [PATCH] feat: add working timer indicator --- src/App.tsx | 1 + src/components/Messages.tsx | 71 +++++++++++++++++++++++++++++- src/styles/messages.css | 86 +++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b125c5c94..fde32927d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -683,6 +683,7 @@ function MainApp() { const messagesNode = ( (null); const [expandedItems, setExpandedItems] = useState>(new Set()); + const [workingSince, setWorkingSince] = useState(null); + const [elapsedMs, setElapsedMs] = useState(0); + const [lastDurationMs, setLastDurationMs] = useState(null); const toggleExpanded = (id: string) => { setExpandedItems((prev) => { const next = new Set(prev); @@ -214,6 +218,52 @@ export function Messages({ items, isThinking }: MessagesProps) { }; }, [items.length, isThinking]); + useEffect(() => { + setWorkingSince(null); + setElapsedMs(0); + setLastDurationMs(null); + }, [threadId]); + + useEffect(() => { + if (isThinking) { + if (!workingSince) { + setWorkingSince(Date.now()); + setElapsedMs(0); + setLastDurationMs(null); + } + return undefined; + } + if (workingSince) { + setLastDurationMs(Date.now() - workingSince); + setWorkingSince(null); + setElapsedMs(0); + } + return undefined; + }, [isThinking, workingSince]); + + useEffect(() => { + if (!isThinking || !workingSince) { + return undefined; + } + const interval = window.setInterval(() => { + setElapsedMs(Date.now() - workingSince); + }, 1000); + return () => window.clearInterval(interval); + }, [isThinking, workingSince]); + + const elapsedSeconds = Math.max(0, Math.floor(elapsedMs / 1000)); + const elapsedMinutes = Math.floor(elapsedSeconds / 60); + const elapsedRemainder = elapsedSeconds % 60; + const formattedElapsed = `${elapsedMinutes}:${String(elapsedRemainder).padStart(2, "0")}`; + const lastDurationSeconds = lastDurationMs + ? Math.max(0, Math.floor(lastDurationMs / 1000)) + : 0; + const lastDurationMinutes = Math.floor(lastDurationSeconds / 60); + const lastDurationRemainder = lastDurationSeconds % 60; + const formattedLastDuration = `${lastDurationMinutes}:${String( + lastDurationRemainder, + ).padStart(2, "0")}`; + return (
Codex is thinking...
+
+ +
+ {elapsedSeconds}s + · + {formattedElapsed} +
+ Working… +
+ )} + {!isThinking && lastDurationMs !== null && items.length > 0 && ( +
+ + + Done in {formattedLastDuration} + + +
)} {!items.length && (
diff --git a/src/styles/messages.css b/src/styles/messages.css index 04ee4c974..440070a3f 100644 --- a/src/styles/messages.css +++ b/src/styles/messages.css @@ -24,6 +24,92 @@ margin-bottom: 12px; } +.working { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 0; + margin: 4px 24px 12px 0; + color: var(--text-fainter); + font-size: 12px; + letter-spacing: 0.02em; +} + +.working-spinner { + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.2); + border-top-color: var(--text-stronger); + animation: working-spin 1s linear infinite; +} + +.working-timer { + display: inline-flex; + align-items: center; + gap: 6px; + font-variant-numeric: tabular-nums; + color: var(--text-stronger); +} + +.working-timer-divider { + opacity: 0.6; +} + +.working-text { + position: relative; + color: transparent; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.3), + rgba(255, 255, 255, 0.95), + rgba(255, 255, 255, 0.3) + ); + background-size: 200% 100%; + -webkit-background-clip: text; + background-clip: text; + animation: working-shimmer 2.2s ease-in-out infinite; +} + +.turn-complete { + display: flex; + align-items: center; + gap: 10px; + margin: 6px 24px 12px 24px; + color: var(--text-faint); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.turn-complete-line { + flex: 1; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18)); +} + +.turn-complete-label { + white-space: nowrap; +} + +@keyframes working-spin { + to { + transform: rotate(360deg); + } +} + +@keyframes working-shimmer { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + .message { display: flex; margin-bottom: 12px;