diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index 4b53c14c3ed5..1991dc502283 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -17,6 +17,7 @@ import { prStatusIndicator, PrStatusTooltipContent, terminalStatusFromRunningIds, + synchronizeTerminalPulse, ThreadStatusLabel, ThreadWorktreeIndicator, useLinkedThreadPullRequest, @@ -832,7 +833,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP } > {terminalStatus.label} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index bf99a1423378..7cbae330c318 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -205,6 +205,7 @@ import { prStatusIndicator, resolveThreadPullRequestBadge, terminalStatusFromRunningIds, + synchronizeTerminalPulse, type TerminalStatusIndicator, useLinkedThreadPullRequest, } from "./ThreadStatusIndicators"; @@ -1518,7 +1519,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { data-testid={`sidebar-terminal-status-${thread.id}`} className={cn("inline-flex shrink-0 items-center justify-center", terminalStatus.colorClass)} > - + ) : null; // Same pen the new-thread draft rows lead with, so both kinds of unsent diff --git a/apps/web/src/components/ThreadStatusIndicators.test.ts b/apps/web/src/components/ThreadStatusIndicators.test.ts index 60b17b19aa6b..5406cba08c0d 100644 --- a/apps/web/src/components/ThreadStatusIndicators.test.ts +++ b/apps/web/src/components/ThreadStatusIndicators.test.ts @@ -1,14 +1,31 @@ import { ProjectId, type PullRequestSummary, type VcsStatusResult } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; +import type { AnimationEvent } from "react"; import { ChangeRequestStatusIcon, prStatusIndicator, resolveThreadPullRequestBadgePresentation, + synchronizeTerminalPulse, } from "./ThreadStatusIndicators"; import { newestPullRequestSummary } from "../state/pullRequests"; import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; +describe("synchronizeTerminalPulse", () => { + it("pins only the status pulse to the document clock", () => { + const pulse = { animationName: "status-pulse", startTime: 975 } as CSSAnimation; + const otherCss = { animationName: "other-animation", startTime: 125 } as CSSAnimation; + const otherAnimation = { startTime: 250 } as Animation; + + synchronizeTerminalPulse({ + animationName: "status-pulse", + currentTarget: { getAnimations: () => [pulse, otherCss, otherAnimation] }, + } as AnimationEvent); + + expect([pulse.startTime, otherCss.startTime, otherAnimation.startTime]).toEqual([0, 125, 250]); + }); +}); + describe("ChangeRequestStatusIcon", () => { it.each([ ["open", "open", false, PullRequestGlyph.pullRequest], diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index b8d8a00ae8bb..33e5c491229b 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -16,7 +16,7 @@ import { } from "@t3tools/shared/threadPullRequests"; import { FolderGit2Icon, TerminalIcon } from "lucide-react"; import { useRender } from "@base-ui/react/use-render"; -import { useMemo, type MouseEvent, type ReactElement } from "react"; +import { useMemo, type AnimationEvent, type MouseEvent, type ReactElement } from "react"; import { cn } from "../lib/utils"; import { useEnvironment, usePrimaryEnvironmentId } from "../state/environments"; import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; @@ -394,6 +394,17 @@ export function terminalStatusFromRunningIds( }; } +/** Align newly started pulses with the document clock without a timer or frame loop. */ +export function synchronizeTerminalPulse(event: AnimationEvent) { + if (event.animationName !== "status-pulse") return; + + for (const animation of event.currentTarget.getAnimations()) { + if ("animationName" in animation && animation.animationName === "status-pulse") { + animation.startTime = 0; + } + } +} + export function ThreadWorktreeIndicator({ thread, }: { @@ -582,7 +593,8 @@ export function ThreadRowTrailingStatus({ thread }: { thread: SidebarThreadSumma } > {terminalStatus.label}