From 2742e19a2ebf4725224ac160ebb9c2d8e9263c9b Mon Sep 17 00:00:00 2001 From: shumengya Date: Sun, 21 Jun 2026 18:06:59 +0800 Subject: [PATCH] feat: show granular run status and elapsed time in chat footer Co-authored-by: Cursor --- .../components/chat/RunStatusBar.module.css | 5 +++ frontend/src/components/chat/RunStatusBar.tsx | 40 ++++++++++++++++-- frontend/src/context/ChatContext.tsx | 35 +++++++++++++++- frontend/src/routes/ChatPage.tsx | 2 + frontend/src/types/runPhase.ts | 13 ++++++ frontend/src/utils/runPhase.ts | 41 +++++++++++++++++++ 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 frontend/src/types/runPhase.ts create mode 100644 frontend/src/utils/runPhase.ts diff --git a/frontend/src/components/chat/RunStatusBar.module.css b/frontend/src/components/chat/RunStatusBar.module.css index e5c50a6..d406f88 100644 --- a/frontend/src/components/chat/RunStatusBar.module.css +++ b/frontend/src/components/chat/RunStatusBar.module.css @@ -45,3 +45,8 @@ .retryMeta { color: var(--text-tertiary, #9ca3af); } + +.elapsed { + color: var(--text-tertiary, #9ca3af); + font-variant-numeric: tabular-nums; +} diff --git a/frontend/src/components/chat/RunStatusBar.tsx b/frontend/src/components/chat/RunStatusBar.tsx index 49f5096..67acfd6 100644 --- a/frontend/src/components/chat/RunStatusBar.tsx +++ b/frontend/src/components/chat/RunStatusBar.tsx @@ -1,40 +1,74 @@ +import { useEffect, useState } from "react"; import type { RetryState } from "../../types/events"; +import type { RunPhaseState } from "../../types/runPhase"; +import { formatRunPhaseLabel } from "../../utils/runPhase"; import styles from "./RunStatusBar.module.css"; interface RunStatusBarProps { isStreaming: boolean; isCompacting: boolean; + runPhase: RunPhaseState | null; retryState: RetryState | null; onAbort: () => void; onAbortRetry: () => void; } +function useElapsedSeconds(startedAt: number | undefined, active: boolean): number { + const [elapsed, setElapsed] = useState(0); + + useEffect(() => { + if (!active || startedAt == null) { + setElapsed(0); + return; + } + + const tick = () => setElapsed(Math.max(0, Math.floor((Date.now() - startedAt) / 1000))); + tick(); + const id = window.setInterval(tick, 1000); + return () => window.clearInterval(id); + }, [active, startedAt]); + + return elapsed; +} + export function RunStatusBar({ isStreaming, isCompacting, + runPhase, retryState, onAbort, onAbortRetry, }: RunStatusBarProps) { - const retryActive = retryState?.active; - if (!isStreaming && !isCompacting && !retryActive) return null; + const retryActive = Boolean(retryState?.active); + const visible = isStreaming || isCompacting || retryActive; let label = ""; + let startedAt: number | undefined; + if (retryActive) { const attempt = retryState?.attempt ?? 1; const max = retryState?.maxAttempts ?? attempt; label = `自动重试中 (${attempt}/${max})`; + startedAt = retryState?.startedAt; } else if (isCompacting) { label = "整理上下文中…"; - } else { + } else if (runPhase) { + label = formatRunPhaseLabel(runPhase.phase, runPhase.detail); + startedAt = runPhase.startedAt; + } else if (isStreaming) { label = "生成中…"; } + const elapsed = useElapsedSeconds(startedAt, visible); + + if (!visible) return null; + return (