authorgravatar for 206951365+cursor[bot]@users.noreply.github.comcursor[bot] <206951365+cursor[bot]@users.noreply.github.com> 2026-05-07 11:54:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-07 11:54:46-07:00
log2ba58076fa19e11811b021a479400a8810b9930d
tree8ccb325962aa523896db0146cb139a15736c105a
parentc27109cc20cd9c56bec3ce9f396f4c7250429ab2
signature Signed by PGP key B5690EEEBB952194

Avoid timeline timer rerender commits (#2580)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>

1 files changed, 47 insertions(+), 13 deletions(-)

apps/web/src/components/chat/MessagesTimeline.tsx+47-13
...@@ -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}
521521
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 the524// 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// ---------------------------------------------------------------------------
527526
528/** Live "Working for Xs" label. */527/** Live "Working for Xs" label. */
529function WorkingTimer({ createdAt }: { createdAt: string }) {528function 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}
537545
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 = durationStart576
554 ? formatElapsed(durationStart, new Date(nowMs).toISOString())577 return <span ref={textRef}>{initialText}</span>;
555 : null;
556 return <>{formatMessageMeta(createdAt, elapsed, timestampFormat)}</>;
557}578}
558579
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}
862883
884function formatWorkingTimerNow(startIso: string): string {
885 return formatWorkingTimer(startIso, new Date().toISOString()) ?? "0s";
886}
887
888function 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
863function formatMessageMeta(897function formatMessageMeta(
864 createdAt: string,898 createdAt: string,
865 duration: string | null,899 duration: string | null,