Unified · Split
| @@ -520,19 +520,27 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin · 519 unmodified lines | ||
| 520 | 520 | } |
| 521 | 521 | |
| 522 | 522 | // --------------------------------------------------------------------------- |
| 523 | −// Self-ticking components — bypass LegendList memoisation entirely. | |
| 524 | −// Each owns a `nowMs` state value consumed in the render output so the | |
| 525 | −// React Compiler cannot elide the re-render as a no-op. | |
| 523 | +// Self-ticking labels — update their own text nodes so elapsed-time display | |
| 524 | +// does not create a React commit every second while a response is streaming. | |
| 526 | 525 | // --------------------------------------------------------------------------- |
| 527 | 526 | |
| 528 | 527 | /** Live "Working for Xs" label. */ |
| 529 | 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 | 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 | 540 | return () => clearInterval(id); |
| 534 | 541 | }, [createdAt]); |
| 535 | − return <>{formatWorkingTimer(createdAt, new Date(nowMs).toISOString()) ?? "0s"}</>; | |
| 542 | + | |
| 543 | + return <span ref={textRef}>{initialText}</span>; | |
| 536 | 544 | } |
| 537 | 545 | |
| 538 | 546 | /** Live timestamp + elapsed duration for a streaming assistant message. */ |
| @@ -545,15 +553,28 @@ function LiveMessageMeta({ · 6 unmodified lines | ||
| 545 | 553 | durationStart: string | null | undefined; |
| 546 | 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 | 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 | 574 | return () => clearInterval(id); |
| 552 | − }, [durationStart]); | |
| 553 | − const elapsed = durationStart | |
| 554 | − ? formatElapsed(durationStart, new Date(nowMs).toISOString()) | |
| 555 | − : null; | |
| 556 | − return <>{formatMessageMeta(createdAt, elapsed, timestampFormat)}</>; | |
| 575 | + }, [createdAt, durationStart, timestampFormat]); | |
| 576 | + | |
| 577 | + return <span ref={textRef}>{initialText}</span>; | |
| 557 | 578 | } |
| 558 | 579 | |
| 559 | 580 | // --------------------------------------------------------------------------- |
| @@ -860,6 +881,19 @@ function formatWorkingTimer(startIso: string, endIso: string): string | null { · 300 unmodified lines | ||
| 860 | 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 | 897 | function formatMessageMeta( |
| 864 | 898 | createdAt: string, |
| 865 | 899 | duration: string | null, |