From ac5bce0ebbaea3c225fd542ba37b1cac71779262 Mon Sep 17 00:00:00 2001 From: KevinYoung-Kw Date: Wed, 15 Apr 2026 13:18:34 +0800 Subject: [PATCH 1/2] fix: persist elapsed timer across session switches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **问题** 切换 Session 后,`StreamingMessage` 底部的计时器会从 0 重新开始计数。根因是 `ElapsedTimer` 组件在 mount 时用 `Date.now()` 本地初始化开始时间,Session 切换导致组件 remount 后时间重置。 **修复** - `stream-session-manager` 的 `SessionStreamSnapshot` 已包含 `startedAt` 字段,记录流的真实开始时间 - 将 `startedAt` 从 `ChatView` → `MessageList` → `StreamingMessage` → `StreamingStatusBar` → `ElapsedTimer` 逐级透传 - `ElapsedTimer` 改为基于传入的 `startedAt` 计算 elapsed,组件 remount 后仍能恢复真实累计时长 **影响范围** 仅影响流式响应状态下的底部计时器显示,不改变任何持久化逻辑或计时行为。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/chat/ChatView.tsx | 1 + src/components/chat/MessageList.tsx | 3 +++ src/components/chat/StreamingMessage.tsx | 18 +++++++++--------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 5155df93..6f4d8fc5 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -651,6 +651,7 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal onLoadMore={loadEarlierMessages} rewindPoints={rewindPoints} sessionId={sessionId} + startedAt={streamSnapshot?.startedAt} isAssistantProject={isAssistantProject} assistantName={assistantName} /> diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx index ff8c3840..91205fa6 100644 --- a/src/components/chat/MessageList.tsx +++ b/src/components/chat/MessageList.tsx @@ -172,6 +172,7 @@ interface MessageListProps { /** SDK rewind points — only emitted for visible prompt-level user messages (not tool results or auto-triggers), mapped by position */ rewindPoints?: RewindPoint[]; sessionId?: string; + startedAt?: number; /** Whether this is an assistant workspace project */ isAssistantProject?: boolean; /** Assistant name for avatar display */ @@ -193,6 +194,7 @@ export function MessageList({ onLoadMore, rewindPoints = [], sessionId, + startedAt, isAssistantProject, assistantName, }: MessageListProps) { @@ -315,6 +317,7 @@ export function MessageList({ content={streamingContent} isStreaming={isStreaming} sessionId={sessionId} + startedAt={startedAt} toolUses={toolUses} toolResults={toolResults} streamingToolOutput={streamingToolOutput} diff --git a/src/components/chat/StreamingMessage.tsx b/src/components/chat/StreamingMessage.tsx index 849d5317..152af613 100644 --- a/src/components/chat/StreamingMessage.tsx +++ b/src/components/chat/StreamingMessage.tsx @@ -106,6 +106,7 @@ interface StreamingMessageProps { content: string; isStreaming: boolean; sessionId?: string; + startedAt?: number; toolUses?: ToolUseInfo[]; toolResults?: ToolResultInfo[]; streamingToolOutput?: string; @@ -196,17 +197,15 @@ function ThinkingPhaseLabel() { return {text}; } -function ElapsedTimer() { - const [elapsed, setElapsed] = useState(0); - const startRef = useRef(0); +function ElapsedTimer({ startedAt }: { startedAt: number }) { + const [elapsed, setElapsed] = useState(() => Math.floor((Date.now() - startedAt) / 1000)); useEffect(() => { - startRef.current = Date.now(); const interval = setInterval(() => { - setElapsed(Math.floor((Date.now() - startRef.current) / 1000)); + setElapsed(Math.floor((Date.now() - startedAt) / 1000)); }, 1000); return () => clearInterval(interval); - }, []); + }, [startedAt]); const mins = Math.floor(elapsed / 60); const secs = elapsed % 60; @@ -218,7 +217,7 @@ function ElapsedTimer() { ); } -function StreamingStatusBar({ statusText, onForceStop }: { statusText?: string; onForceStop?: () => void }) { +function StreamingStatusBar({ statusText, onForceStop, startedAt }: { statusText?: string; onForceStop?: () => void; startedAt?: number }) { const displayText = statusText || 'Thinking'; // Parse elapsed seconds from statusText like "Running bash... (45s)" @@ -241,7 +240,7 @@ function StreamingStatusBar({ statusText, onForceStop }: { statusText?: string; )} | - + {isCritical && onForceStop && (