| ... | @@ -520,19 +520,27 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin | ... | @@ -520,19 +520,27 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin |
| 520 | } | 520 | } |
| 521 | | 521 | |
| 522 | // --------------------------------------------------------------------------- | 522 | // --------------------------------------------------------------------------- |
| 523 | // Self-ticking components — bypass LegendList memoisation entirely. | 523 | // Self-ticking labels — update their own text nodes so elapsed-time display |
| 524 | // Each owns a `nowMs` state value consumed in the render output so the | 524 | // does not create a React commit every second while a response is streaming. |
| 525 | // React Compiler cannot elide the re-render as a no-op. | | |
| 526 | // --------------------------------------------------------------------------- | 525 | // --------------------------------------------------------------------------- |
| 527 | | 526 | |
| 528 | /** Live "Working for Xs" label. */ | 527 | /** Live "Working for Xs" label. */ |
| 529 | function WorkingTimer({ createdAt }: { createdAt: string }) { | 528 | function WorkingTimer({ createdAt }: { createdAt: string }) { |
| 530 | const [nowMs, setNowMs] = useState(() => Date.now()); | 529 | const textRef = useRef<HTMLSpanElement>(null); |
| | 530 | const initialText = formatWorkingTimerNow(createdAt); |
| | 531 | |
| 531 | useEffect(() => { | 532 | useEffect(() => { |
| 532 | const id = setInterval(() => setNowMs(Date.now()), 1000); | 533 | const updateText = () => { |
| | 534 | if (textRef.current) { |
| | 535 | textRef.current.textContent = formatWorkingTimerNow(createdAt); |
| | 536 | } |
| | 537 | }; |
| | 538 | updateText(); |
| | 539 | const id = setInterval(updateText, 1000); |
| 533 | return () => clearInterval(id); | 540 | return () => clearInterval(id); |
| 534 | }, [createdAt]); | 541 | }, [createdAt]); |
| 535 | return <>{formatWorkingTimer(createdAt, new Date(nowMs).toISOString()) ?? "0s"}</>; | 542 | |
| | 543 | return <span ref={textRef}>{initialText}</span>; |
| 536 | } | 544 | } |
| 537 | | 545 | |
| 538 | /** Live timestamp + elapsed duration for a streaming assistant message. */ | 546 | /** Live timestamp + elapsed duration for a streaming assistant message. */ |
| ... | @@ -545,15 +553,28 @@ function LiveMessageMeta({ | ... | @@ -545,15 +553,28 @@ function LiveMessageMeta({ |
| 545 | durationStart: string | null | undefined; | 553 | durationStart: string | null | undefined; |
| 546 | timestampFormat: TimestampFormat; | 554 | timestampFormat: TimestampFormat; |
| 547 | }) { | 555 | }) { |
| 548 | const [nowMs, setNowMs] = useState(() => Date.now()); | 556 | const textRef = useRef<HTMLSpanElement>(null); |
| | 557 | const initialText = formatLiveMessageMetaNow(createdAt, durationStart, timestampFormat); |
| | 558 | |
| 549 | useEffect(() => { | 559 | useEffect(() => { |
| 550 | const id = setInterval(() => setNowMs(Date.now()), 1000); | 560 | const updateText = () => { |
| | 561 | if (textRef.current) { |
| | 562 | textRef.current.textContent = formatLiveMessageMetaNow( |
| | 563 | createdAt, |
| | 564 | durationStart, |
| | 565 | timestampFormat, |
| | 566 | ); |
| | 567 | } |
| | 568 | }; |
| | 569 | updateText(); |
| | 570 | if (!durationStart) { |
| | 571 | return; |
| | 572 | } |
| | 573 | const id = setInterval(updateText, 1000); |
| 551 | return () => clearInterval(id); | 574 | return () => clearInterval(id); |
| 552 | }, [durationStart]); | 575 | }, [createdAt, durationStart, timestampFormat]); |
| 553 | const elapsed = durationStart | 576 | |
| 554 | ? formatElapsed(durationStart, new Date(nowMs).toISOString()) | 577 | return <span ref={textRef}>{initialText}</span>; |
| 555 | : null; | | |
| 556 | return <>{formatMessageMeta(createdAt, elapsed, timestampFormat)}</>; | | |
| 557 | } | 578 | } |
| 558 | | 579 | |
| 559 | // --------------------------------------------------------------------------- | 580 | // --------------------------------------------------------------------------- |
| ... | @@ -860,6 +881,19 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null { | ... | @@ -860,6 +881,19 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null { |
| 860 | return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; | 881 | return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; |
| 861 | } | 882 | } |
| 862 | | 883 | |
| | 884 | function formatWorkingTimerNow(startIso: string): string { |
| | 885 | return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s"; |
| | 886 | } |
| | 887 | |
| | 888 | function formatLiveMessageMetaNow( |
| | 889 | createdAt: string, |
| | 890 | durationStart: string | null | undefined, |
| | 891 | timestampFormat: TimestampFormat, |
| | 892 | ): string { |
| | 893 | const elapsed = durationStart ? formatElapsed(durationStart, new Date().toISOString()) : null; |
| | 894 | return formatMessageMeta(createdAt, elapsed, timestampFormat); |
| | 895 | } |
| | 896 | |
| 863 | function formatMessageMeta( | 897 | function formatMessageMeta( |
| 864 | createdAt: string, | 898 | createdAt: string, |
| 865 | duration: string | null, | 899 | duration: string | null, |