authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-04 09:12:28-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 09:12:28-07:00
logc7c1dfe4df99edf65a49d8a31b39ef1361f37f44
tree225c1fad845e5917f553ab4df56613fea9ef55b0
parentc75299ee2085a121bceb6df76796e971fe92b5b6
signature Signed by PGP key B5690EEEBB952194

perf(web): stop continuous chat status animations (#9709)


6 files changed, 20 insertions(+), 159 deletions(-)

apps/web/src/components/chat/ComposerActivityStatus.tsx+1-1
...@@ -6,7 +6,7 @@ export function ComposerActivityRow({ phase }: { readonly phase: ThreadSyncPhase...@@ -6,7 +6,7 @@ export function ComposerActivityRow({ phase }: { readonly phase: ThreadSyncPhase
6 return (6 return (
7 <ComposerBanner.Row>7 <ComposerBanner.Row>
8 <ComposerBanner.Icon>8 <ComposerBanner.Icon>
9 <LoaderCircleIcon className="motion-safe:animate-spin" />9 <LoaderCircleIcon />
10 </ComposerBanner.Icon>10 </ComposerBanner.Icon>
11 <ComposerBanner.Content>11 <ComposerBanner.Content>
12 <span12 <span
apps/web/src/components/chat/ComposerServerUpdateStatus.tsx+1-1
...@@ -12,7 +12,7 @@ export function ComposerServerUpdateIcon({...@@ -12,7 +12,7 @@ export function ComposerServerUpdateIcon({
12 readonly status: ServerUpdateState["status"];12 readonly status: ServerUpdateState["status"];
13}) {13}) {
14 if (status === "running") {14 if (status === "running") {
15 return <LoaderCircleIcon aria-hidden className="motion-safe:animate-spin" />;15 return <LoaderCircleIcon aria-hidden />;
16 }16 }
17 if (status === "failed") {17 if (status === "failed") {
18 return <CircleAlertIcon aria-hidden className="text-error" />;18 return <CircleAlertIcon aria-hidden className="text-error" />;
apps/web/src/components/chat/MessagesTimeline.test.tsx+1-3
...@@ -1203,7 +1203,7 @@ describe("MessagesTimeline", () => {...@@ -1203,7 +1203,7 @@ describe("MessagesTimeline", () => {
1203 expect(markup).not.toContain('aria-label="Hidden work includes a failure"');1203 expect(markup).not.toContain('aria-label="Hidden work includes a failure"');
1204 });1204 });
12051205
1206 it("shows the animated one-line label for a live tool group", () => {1206 it("shows the one-line label for a live tool group", () => {
1207 const turnId = TurnId.make("turn-live");1207 const turnId = TurnId.make("turn-live");
1208 const markup = renderToStaticMarkup(1208 const markup = renderToStaticMarkup(
1209 <MessagesTimeline1209 <MessagesTimeline
...@@ -1240,7 +1240,6 @@ describe("MessagesTimeline", () => {...@@ -1240,7 +1240,6 @@ describe("MessagesTimeline", () => {
12401240
1241 expect(markup).toContain("Working for");1241 expect(markup).toContain("Working for");
1242 expect(markup).toContain("Running pnpm");1242 expect(markup).toContain("Running pnpm");
1243 expect(markup).toContain("live-activity-focus");
1244 });1243 });
12451244
1246 it("scopes a live row failure to the tool named by the row", () => {1245 it("scopes a live row failure to the tool named by the row", () => {
...@@ -1358,7 +1357,6 @@ describe("MessagesTimeline", () => {...@@ -1358,7 +1357,6 @@ describe("MessagesTimeline", () => {
13581357
1359 expect(markup).toContain("Running pnpm");1358 expect(markup).toContain("Running pnpm");
1360 expect(markup).toContain("lucide-terminal");1359 expect(markup).toContain("lucide-terminal");
1361 expect(markup).toContain("live-activity-focus");
1362 expect(markup).not.toContain("Ran pnpm");1360 expect(markup).not.toContain("Ran pnpm");
1363 expect(markup).not.toContain("Thinking");1361 expect(markup).not.toContain("Thinking");
1364 expect(markup).not.toContain('data-timeline-row-kind="thinking"');1362 expect(markup).not.toContain('data-timeline-row-kind="thinking"');
apps/web/src/components/chat/MessagesTimeline.tsx+13-60
...@@ -1614,17 +1614,9 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin...@@ -1614,17 +1614,9 @@ function WorkingTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "workin
1614 className="relative shrink-0 overflow-hidden whitespace-nowrap transition-opacity duration-150 starting:opacity-0 motion-reduce:transition-none"1614 className="relative shrink-0 overflow-hidden whitespace-nowrap transition-opacity duration-150 starting:opacity-0 motion-reduce:transition-none"
1615 >1615 >
1616 {isPreparingWorktree ? (1616 {isPreparingWorktree ? (
1617 <>1617 "Setting up worktree…"
1618 Setting up worktree…
1619 <ActivityShimmerOverlay>Setting up worktree…</ActivityShimmerOverlay>
1620 </>
1621 ) : isCompacting ? (1618 ) : isCompacting ? (
1622 <>1619 <CompactingLabel />
1623 <CompactingLabel />
1624 <ActivityShimmerOverlay>
1625 <CompactingLabel />
1626 </ActivityShimmerOverlay>
1627 </>
1628 ) : row.createdAt ? (1620 ) : row.createdAt ? (
1629 <>1621 <>
1630 Working for <WorkingTimer createdAt={row.createdAt} />1622 Working for <WorkingTimer createdAt={row.createdAt} />
...@@ -1878,19 +1870,6 @@ function ExpandedWorkGroupEntries({...@@ -1878,19 +1870,6 @@ function ExpandedWorkGroupEntries({
18781870
1879const workEntryKey = (entry: TimelineWorkEntry) => entry.id;1871const workEntryKey = (entry: TimelineWorkEntry) => entry.id;
18801872
1881function ActivityShimmerOverlay({ children }: { children: ReactNode }) {
1882 return (
1883 <span
1884 aria-hidden
1885 className="live-activity-focus pointer-events-none absolute inset-y-0 select-none"
1886 >
1887 <span className="live-activity-focus-counter block">
1888 <span className="live-activity-focus-aligned block text-foreground">{children}</span>
1889 </span>
1890 </span>
1891 );
1892}
1893
1894const failedToolIconClassName = "text-tool-error-icon/40";1873const failedToolIconClassName = "text-tool-error-icon/40";
18951874
1896/** Image icons and the gradient computer-use mark cannot take a currentColor1875/** Image icons and the gradient computer-use mark cannot take a currentColor
...@@ -1914,7 +1893,7 @@ function LiveActivityRow({...@@ -1914,7 +1893,7 @@ function LiveActivityRow({
1914 failed?: boolean;1893 failed?: boolean;
1915}) {1894}) {
1916 return (1895 return (
1917 <div className="relative min-h-6 w-fit max-w-full min-w-0 overflow-hidden rounded-md text-sm leading-relaxed">1896 <div className="min-h-6 w-fit max-w-full min-w-0 overflow-hidden rounded-md text-sm leading-relaxed">
1918 <LiveActivityContent1897 <LiveActivityContent
1919 label={label}1898 label={label}
1920 iconName={iconName}1899 iconName={iconName}
...@@ -1922,15 +1901,6 @@ function LiveActivityRow({...@@ -1922,15 +1901,6 @@ function LiveActivityRow({
1922 failed={failed}1901 failed={failed}
1923 announceFailure={failed}1902 announceFailure={failed}
1924 />1903 />
1925 <ActivityShimmerOverlay>
1926 <LiveActivityContent
1927 label={label}
1928 iconName={iconName}
1929 toolIcon={toolIcon}
1930 failed={failed}
1931 highlighted
1932 />
1933 </ActivityShimmerOverlay>
1934 </div>1904 </div>
1935 );1905 );
1936}1906}
...@@ -1941,14 +1911,12 @@ function LiveActivityContent({...@@ -1941,14 +1911,12 @@ function LiveActivityContent({
1941 toolIcon,1911 toolIcon,
1942 failed = false,1912 failed = false,
1943 announceFailure = false,1913 announceFailure = false,
1944 highlighted = false,
1945}: {1914}: {
1946 label: string;1915 label: string;
1947 iconName: WorkEntryIconName | undefined;1916 iconName: WorkEntryIconName | undefined;
1948 toolIcon?: ToolActivityIcon | undefined;1917 toolIcon?: ToolActivityIcon | undefined;
1949 failed?: boolean;1918 failed?: boolean;
1950 announceFailure?: boolean;1919 announceFailure?: boolean;
1951 highlighted?: boolean;
1952}) {1920}) {
1953 const showTrailingFailureMark =1921 const showTrailingFailureMark =
1954 failed && iconName !== undefined && !toolIconAcceptsTint(iconName, toolIcon);1922 failed && iconName !== undefined && !toolIconAcceptsTint(iconName, toolIcon);
...@@ -1958,14 +1926,14 @@ function LiveActivityContent({...@@ -1958,14 +1926,14 @@ function LiveActivityContent({
1958 className={cn(1926 className={cn(
1959 "flex min-h-6 min-w-0 items-center gap-1.5 py-0.5",1927 "flex min-h-6 min-w-0 items-center gap-1.5 py-0.5",
1960 iconName ? "px-0.5" : "px-1",1928 iconName ? "px-0.5" : "px-1",
1961 highlighted ? "text-foreground" : "text-secondary-label",1929 "text-secondary-label",
1962 )}1930 )}
1963 >1931 >
1964 {iconName ? (1932 {iconName ? (
1965 <span1933 <span
1966 className={cn(1934 className={cn(
1967 "flex size-6 shrink-0 items-center justify-center",1935 "flex size-6 shrink-0 items-center justify-center",
1968 highlighted ? "text-foreground" : failed ? failedToolIconClassName : "text-icon-muted",1936 failed ? failedToolIconClassName : "text-icon-muted",
1969 )}1937 )}
1970 role={announceFailure ? "img" : undefined}1938 role={announceFailure ? "img" : undefined}
1971 aria-label={announceFailure ? "Tool call failed" : undefined}1939 aria-label={announceFailure ? "Tool call failed" : undefined}
...@@ -1974,16 +1942,13 @@ function LiveActivityContent({...@@ -1974,16 +1942,13 @@ function LiveActivityContent({
1974 icon={toolIcon}1942 icon={toolIcon}
1975 fallbackName={iconName}1943 fallbackName={iconName}
1976 className="block size-4 shrink-0 stroke-[1.8]"1944 className="block size-4 shrink-0 stroke-[1.8]"
1977 muted={!highlighted}1945 muted
1978 />1946 />
1979 </span>1947 </span>
1980 ) : null}1948 ) : null}
1981 <span className="min-w-0 flex-1 truncate">{label}</span>1949 <span className="min-w-0 flex-1 truncate">{label}</span>
1982 {showTrailingFailureMark ? (1950 {showTrailingFailureMark ? (
1983 <XIcon1951 <XIcon aria-hidden className={cn("size-3 shrink-0", failedToolIconClassName)} />
1984 aria-hidden
1985 className={cn("size-3 shrink-0", !highlighted && failedToolIconClassName)}
1986 />
1987 ) : null}1952 ) : null}
1988 </span>1953 </span>
1989 );1954 );
...@@ -2002,24 +1967,12 @@ function LiveWorkEntryTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "...@@ -2002,24 +1967,12 @@ function LiveWorkEntryTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "
2002 aria-expanded={row.expanded}1967 aria-expanded={row.expanded}
2003 onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)}1968 onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)}
2004 >1969 >
2005 {row.active ? (1970 <LiveActivityRow
2006 <LiveActivityRow1971 label={label}
2007 label={label}1972 iconName={workEntryIconName(row.entry)}
2008 iconName={workEntryIconName(row.entry)}1973 toolIcon={row.entry.toolIcon ?? row.entry.toolSource?.icon}
2009 toolIcon={row.entry.toolIcon ?? row.entry.toolSource?.icon}1974 failed={failed}
2010 failed={failed}1975 />
2011 />
2012 ) : (
2013 <div className="min-h-6 w-fit max-w-full min-w-0 overflow-hidden rounded-md text-sm leading-relaxed">
2014 <LiveActivityContent
2015 label={label}
2016 iconName={workEntryIconName(row.entry)}
2017 toolIcon={row.entry.toolIcon ?? row.entry.toolSource?.icon}
2018 failed={failed}
2019 announceFailure={failed}
2020 />
2021 </div>
2022 )}
2023 </button>1976 </button>
2024 );1977 );
2025}1978}
apps/web/src/index.css+2-94
...@@ -425,80 +425,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil...@@ -425,80 +425,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil
425 }425 }
426}426}
427427
428@keyframes live-activity-focus {
429 0% {
430 transform: translateX(0);
431 }
432 100% {
433 transform: translateX(100%);
434 }
435}
436
437@keyframes live-activity-focus-counter {
438 0% {
439 transform: translateX(0);
440 }
441 100% {
442 transform: translateX(-100%);
443 }
444}
445
446@utility live-activity-focus {
447 --live-activity-focus-width: 4.5rem;
448
449 right: auto;
450 left: calc(-1 * var(--live-activity-focus-width));
451 width: calc(100% + var(--live-activity-focus-width) + var(--live-activity-focus-width));
452 -webkit-mask-image: linear-gradient(
453 to right,
454 transparent 0,
455 rgb(0 0 0 / 12%) 0.675rem,
456 rgb(0 0 0 / 55%) 1.575rem,
457 black 2.25rem,
458 rgb(0 0 0 / 55%) 2.925rem,
459 rgb(0 0 0 / 12%) 3.825rem,
460 transparent var(--live-activity-focus-width),
461 transparent 100%
462 );
463 -webkit-mask-repeat: no-repeat;
464 mask-image: linear-gradient(
465 to right,
466 transparent 0,
467 rgb(0 0 0 / 12%) 0.675rem,
468 rgb(0 0 0 / 55%) 1.575rem,
469 black 2.25rem,
470 rgb(0 0 0 / 55%) 2.925rem,
471 rgb(0 0 0 / 12%) 3.825rem,
472 transparent var(--live-activity-focus-width),
473 transparent 100%
474 );
475 mask-repeat: no-repeat;
476 animation: live-activity-focus 2.2s linear infinite;
477 will-change: transform;
478
479 @media (prefers-reduced-motion: reduce) {
480 animation: none;
481 opacity: 0;
482 will-change: auto;
483 }
484}
485
486@utility live-activity-focus-counter {
487 width: 100%;
488 animation: live-activity-focus-counter 2.2s linear infinite;
489 will-change: transform;
490
491 @media (prefers-reduced-motion: reduce) {
492 animation: none;
493 will-change: auto;
494 }
495}
496
497@utility live-activity-focus-aligned {
498 width: calc(100% - var(--live-activity-focus-width) - var(--live-activity-focus-width));
499 margin-left: var(--live-activity-focus-width);
500}
501
502@property --assistant-citation-highlight-opacity {428@property --assistant-citation-highlight-opacity {
503 syntax: "<number>";429 syntax: "<number>";
504 inherits: true;430 inherits: true;
...@@ -1906,24 +1832,6 @@ code {...@@ -1906,24 +1832,6 @@ code {
1906 color: var(--code-foreground) !important;1832 color: var(--code-foreground) !important;
1907}1833}
19081834
1909@keyframes ultrathink-rainbow {
1910 0% {
1911 background-position: 0% 50%;
1912 }
1913 100% {
1914 background-position: 200% 50%;
1915 }
1916}
1917
1918@keyframes ultrathink-chroma-shift {
1919 0% {
1920 filter: hue-rotate(0deg) saturate(1.2);
1921 }
1922 100% {
1923 filter: hue-rotate(360deg) saturate(1.2);
1924 }
1925}
1926
1927.ultrathink-frame {1835.ultrathink-frame {
1928 --ultrathink-spectrum: linear-gradient(1836 --ultrathink-spectrum: linear-gradient(
1929 120deg,1837 120deg,
...@@ -1946,6 +1854,7 @@ code {...@@ -1946,6 +1854,7 @@ code {
1946 padding: 2px;1854 padding: 2px;
1947 background-image: var(--ultrathink-spectrum);1855 background-image: var(--ultrathink-spectrum);
1948 background-size: 220% 220%;1856 background-size: 220% 220%;
1857 background-position: 0% 50%;
1949 pointer-events: none;1858 pointer-events: none;
1950 filter: saturate(0.82) brightness(0.92);1859 filter: saturate(0.82) brightness(0.92);
1951 -webkit-mask:1860 -webkit-mask:
...@@ -1956,7 +1865,6 @@ code {...@@ -1956,7 +1865,6 @@ code {
1956 linear-gradient(#000 0 0) content-box,1865 linear-gradient(#000 0 0) content-box,
1957 linear-gradient(#000 0 0);1866 linear-gradient(#000 0 0);
1958 mask-composite: exclude;1867 mask-composite: exclude;
1959 animation: ultrathink-rainbow 10s linear infinite;
1960}1868}
19611869
1962.ultrathink-frame > * {1870.ultrathink-frame > * {
...@@ -1964,7 +1872,7 @@ code {...@@ -1964,7 +1872,7 @@ code {
1964}1872}
19651873
1966.ultrathink-chroma {1874.ultrathink-chroma {
1967 animation: ultrathink-chroma-shift 10s linear infinite;1875 filter: saturate(1.2);
1968}1876}
19691877
1970@keyframes preview-loading-progress {1878@keyframes preview-loading-progress {
docs/user/composer.md+2
...@@ -174,6 +174,8 @@ On web and desktop, loading and syncing statuses fill the available banner width...@@ -174,6 +174,8 @@ On web and desktop, loading and syncing statuses fill the available banner width
174stash tab. Task progress appears above the composer, while the timeline's working timer shows174stash tab. Task progress appears above the composer, while the timeline's working timer shows
175only elapsed time.175only elapsed time.
176176
177Loading, syncing, and server-update icons are static. Live tool labels do not shimmer.
178
177On web and desktop, additional notices peek out above the attached banner. Hover over the peek179On web and desktop, additional notices peek out above the attached banner. Hover over the peek
178to reveal them, or focus **Show other notices** with `Tab` and press `Enter` or `Space`. Press180to reveal them, or focus **Show other notices** with `Tab` and press `Enter` or `Space`. Press
179`Escape` to close the stack and return focus to that control. On a touchscreen, tap the peek to181`Escape` to close the stack and return focus to that control. On a touchscreen, tap the peek to