| ... | ... | @@ -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. |
| 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({ |
| 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 { |
| 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, |