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}