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. | 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({ · 6 unmodified lines | |||
| 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 { · 300 unmodified lines | |||
| 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, |