|
1 | | -import React, { useEffect, useMemo, useRef, useState } from "react" |
| 1 | +import React, { useEffect, useRef, useState } from "react" |
2 | 2 | import { useTranslation } from "react-i18next" |
3 | 3 |
|
4 | 4 | import MarkdownBlock from "../common/MarkdownBlock" |
5 | | -import { vscode } from "@src/utils/vscode" |
6 | | - |
7 | | -interface ReasoningMeta { |
8 | | - startedAt?: number |
9 | | - elapsedMs?: number |
10 | | -} |
| 5 | +import { Clock, Lightbulb } from "lucide-react" |
11 | 6 |
|
12 | 7 | interface ReasoningBlockProps { |
13 | 8 | content: string |
14 | 9 | ts: number |
15 | 10 | isStreaming: boolean |
16 | 11 | isLast: boolean |
17 | | - metadata?: { reasoning?: ReasoningMeta } | Record<string, any> |
| 12 | + metadata?: any |
18 | 13 | } |
19 | 14 |
|
20 | 15 | /** |
21 | | - * Render reasoning with a heading and a persistent timer. |
| 16 | + * Render reasoning with a heading and a simple timer. |
22 | 17 | * - Heading uses i18n key chat:reasoning.thinking |
23 | | - * - Timer shown beside the heading and persists via message.metadata.reasoning { startedAt, elapsedMs } |
| 18 | + * - Timer runs while reasoning is active (no persistence) |
24 | 19 | */ |
25 | | -export const ReasoningBlock = ({ content, ts, isStreaming, isLast, metadata }: ReasoningBlockProps) => { |
| 20 | +export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockProps) => { |
26 | 21 | const { t } = useTranslation() |
27 | 22 |
|
28 | | - const persisted: ReasoningMeta = (metadata?.reasoning as ReasoningMeta) || {} |
29 | | - const startedAtRef = useRef<number>(persisted.startedAt ?? Date.now()) |
30 | | - const [elapsed, setElapsed] = useState<number>(persisted.elapsedMs ?? 0) |
31 | | - const postedRef = useRef<boolean>(false) |
32 | | - const intervalRef = useRef<number | null>(null) |
| 23 | + const startTimeRef = useRef<number>(Date.now()) |
| 24 | + const [elapsed, setElapsed] = useState<number>(0) |
33 | 25 |
|
34 | | - // Initialize startedAt on first mount if missing (persist to task) - guard with postedRef |
| 26 | + // Simple timer that runs while streaming |
35 | 27 | useEffect(() => { |
36 | | - if (!persisted.startedAt && isLast && !postedRef.current) { |
37 | | - postedRef.current = true |
38 | | - vscode.postMessage({ |
39 | | - type: "updateMessageReasoningMeta", |
40 | | - messageTs: ts, |
41 | | - reasoningMeta: { startedAt: startedAtRef.current }, |
42 | | - }) |
43 | | - } |
44 | | - }, [ts, isLast, persisted.startedAt]) |
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 | | - // ensure no duplicate intervals |
54 | | - if (intervalRef.current) { |
55 | | - clearInterval(intervalRef.current) |
56 | | - intervalRef.current = null |
57 | | - } |
58 | | - intervalRef.current = window.setInterval(tick, 1000) |
59 | | - return () => { |
60 | | - if (intervalRef.current) { |
61 | | - clearInterval(intervalRef.current) |
62 | | - intervalRef.current = null |
63 | | - } |
| 28 | + if (isLast && isStreaming) { |
| 29 | + const tick = () => setElapsed(Date.now() - startTimeRef.current) |
| 30 | + tick() |
| 31 | + const id = setInterval(tick, 1000) |
| 32 | + return () => clearInterval(id) |
64 | 33 | } |
65 | 34 | }, [isLast, isStreaming]) |
66 | 35 |
|
67 | | - // Cleanup on unmount (safety net) |
68 | | - useEffect(() => { |
69 | | - return () => { |
70 | | - if (intervalRef.current) { |
71 | | - clearInterval(intervalRef.current) |
72 | | - intervalRef.current = null |
73 | | - } |
74 | | - } |
75 | | - }, []) |
76 | | - |
77 | | - // Persist final elapsed when streaming stops |
78 | | - const wasActiveRef = useRef<boolean>(false) |
79 | | - useEffect(() => { |
80 | | - const active = isLast && isStreaming |
81 | | - if (wasActiveRef.current && !active) { |
82 | | - const finalMs = Date.now() - startedAtRef.current |
83 | | - setElapsed(finalMs) |
84 | | - vscode.postMessage({ |
85 | | - type: "updateMessageReasoningMeta", |
86 | | - messageTs: ts, |
87 | | - reasoningMeta: { startedAt: startedAtRef.current, elapsedMs: finalMs }, |
88 | | - }) |
89 | | - } |
90 | | - wasActiveRef.current = active |
91 | | - }, [isLast, isStreaming, ts]) |
92 | | - |
93 | | - const displayMs = useMemo(() => { |
94 | | - if (isLast && isStreaming) return elapsed |
95 | | - return persisted.elapsedMs ?? elapsed |
96 | | - }, [elapsed, isLast, isStreaming, persisted.elapsedMs]) |
97 | | - |
98 | | - const seconds = Math.max(0, Math.floor((displayMs || 0) / 1000)) |
| 36 | + const seconds = Math.floor(elapsed / 1000) |
99 | 37 | const secondsLabel = t("chat:reasoning.seconds", { count: seconds }) |
100 | 38 |
|
101 | 39 | return ( |
102 | 40 | <div className="py-1"> |
103 | 41 | <div className="flex items-center justify-between mb-2.5"> |
104 | 42 | <div className="flex items-center gap-2"> |
105 | | - <span className="codicon codicon-light-bulb text-vscode-charts-yellow" /> |
| 43 | + <Lightbulb className="w-4" /> |
106 | 44 | <span className="font-bold text-vscode-foreground">{t("chat:reasoning.thinking")}</span> |
107 | 45 | </div> |
108 | | - <span className="text-vscode-foreground tabular-nums flex items-center gap-1"> |
109 | | - <span className="codicon codicon-clock text-base" /> |
110 | | - {secondsLabel} |
111 | | - </span> |
| 46 | + {elapsed > 0 && ( |
| 47 | + <span className="text-vscode-foreground tabular-nums flex items-center gap-1"> |
| 48 | + <Clock className="w-4" /> |
| 49 | + {secondsLabel} |
| 50 | + </span> |
| 51 | + )} |
112 | 52 | </div> |
113 | 53 | {(content?.trim()?.length ?? 0) > 0 && ( |
114 | 54 | <div className="px-3 italic text-vscode-descriptionForeground"> |
|
0 commit comments