|
| 1 | +import React, { useEffect, useMemo, useRef, useState } from "react" |
| 2 | +import { useTranslation } from "react-i18next" |
| 3 | + |
1 | 4 | import MarkdownBlock from "../common/MarkdownBlock" |
| 5 | +import { vscode } from "@src/utils/vscode" |
2 | 6 |
|
3 | 7 | interface ReasoningBlockProps { |
4 | 8 | content: string |
| 9 | + ts: number |
| 10 | + isStreaming: boolean |
| 11 | + isLast: boolean |
| 12 | + metadata?: Record<string, any> |
| 13 | +} |
| 14 | + |
| 15 | +function formatDuration(ms: number): string { |
| 16 | + const totalSeconds = Math.max(0, Math.floor(ms / 1000)) |
| 17 | + const minutes = Math.floor(totalSeconds / 60) |
| 18 | + const seconds = totalSeconds % 60 |
| 19 | + return `${minutes}:${seconds.toString().padStart(2, "0")}` |
5 | 20 | } |
6 | 21 |
|
7 | 22 | /** |
8 | | - * Render reasoning as simple italic text, matching how <thinking> content is shown. |
9 | | - * No borders, boxes, headers, timers, or collapsible behavior. |
| 23 | + * Render reasoning with a heading and a persistent timer. |
| 24 | + * - Heading uses i18n key chat:reasoning.thinking |
| 25 | + * - Timer persists via message.metadata.reasoning { startedAt, elapsedMs } |
10 | 26 | */ |
11 | | -export const ReasoningBlock = ({ content }: ReasoningBlockProps) => { |
| 27 | +export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: ReasoningBlockProps) => { |
| 28 | + const { t } = useTranslation() |
| 29 | + |
| 30 | + const persisted = (metadata?.reasoning as { startedAt?: number; elapsedMs?: number } | undefined) || {} |
| 31 | + const startedAtRef = useRef<number>(persisted.startedAt ?? Date.now()) |
| 32 | + const [elapsed, setElapsed] = useState<number>(persisted.elapsedMs ?? 0) |
| 33 | + |
| 34 | + // Initialize startedAt on first mount if missing (persist to task) |
| 35 | + useEffect(() => { |
| 36 | + if (!persisted.startedAt && isLast) { |
| 37 | + vscode.postMessage({ |
| 38 | + type: "updateMessageReasoningMeta", |
| 39 | + messageTs: ts, |
| 40 | + reasoningMeta: { startedAt: startedAtRef.current }, |
| 41 | + } as any) |
| 42 | + } |
| 43 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 44 | + }, [ts]) |
| 45 | + |
| 46 | + // Tick while active (last row and streaming) |
| 47 | + useEffect(() => { |
| 48 | + const active = isLast && isStreaming |
| 49 | + if (!active) return |
| 50 | + |
| 51 | + const tick = () => setElapsed(Date.now() - startedAtRef.current) |
| 52 | + tick() |
| 53 | + const id = setInterval(tick, 1000) |
| 54 | + return () => clearInterval(id) |
| 55 | + }, [isLast, isStreaming]) |
| 56 | + |
| 57 | + // Persist final elapsed when streaming stops |
| 58 | + const wasActiveRef = useRef<boolean>(false) |
| 59 | + useEffect(() => { |
| 60 | + const active = isLast && isStreaming |
| 61 | + if (wasActiveRef.current && !active) { |
| 62 | + const finalMs = Date.now() - startedAtRef.current |
| 63 | + setElapsed(finalMs) |
| 64 | + vscode.postMessage({ |
| 65 | + type: "updateMessageReasoningMeta", |
| 66 | + messageTs: ts, |
| 67 | + reasoningMeta: { startedAt: startedAtRef.current, elapsedMs: finalMs }, |
| 68 | + } as any) |
| 69 | + } |
| 70 | + wasActiveRef.current = active |
| 71 | + }, [isLast, isStreaming, ts]) |
| 72 | + |
| 73 | + const displayMs = useMemo(() => { |
| 74 | + if (isLast && isStreaming) return elapsed |
| 75 | + return persisted.elapsedMs ?? elapsed |
| 76 | + }, [elapsed, isLast, isStreaming, persisted.elapsedMs]) |
| 77 | + |
12 | 78 | return ( |
13 | 79 | <div className="px-3 py-1"> |
| 80 | + <div className="flex items-center justify-between mb-1"> |
| 81 | + <div className="flex items-center gap-2"> |
| 82 | + <span className="codicon codicon-light-bulb text-muted-foreground" /> |
| 83 | + <span className="font-medium text-vscode-foreground">{t("chat:reasoning.thinking")}</span> |
| 84 | + </div> |
| 85 | + <span className="text-xs text-muted-foreground tabular-nums">{formatDuration(displayMs)}</span> |
| 86 | + </div> |
14 | 87 | <div className="italic text-muted-foreground"> |
15 | 88 | <MarkdownBlock markdown={content} /> |
16 | 89 | </div> |
|
0 commit comments