diff --git a/packages/dashboard/app/__tests__/spinner-animation.css.test.ts b/packages/dashboard/app/__tests__/spinner-animation.css.test.ts
new file mode 100644
index 000000000..e4231946f
--- /dev/null
+++ b/packages/dashboard/app/__tests__/spinner-animation.css.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from "vitest";
+import { readFileSync } from "fs";
+import { resolve } from "path";
+
+function extractBlock(content: string, pattern: RegExp): string {
+ const match = content.match(pattern);
+ expect(match?.index).toBeDefined();
+
+ const start = match!.index! + match![0].length;
+ let index = start;
+ let depth = 1;
+
+ while (index < content.length && depth > 0) {
+ if (content[index] === "{") depth += 1;
+ if (content[index] === "}") depth -= 1;
+ index += 1;
+ }
+
+ expect(depth).toBe(0);
+ return content.slice(match!.index!, index);
+}
+
+describe("global spinner animation utility", () => {
+ const css = readFileSync(resolve(__dirname, "../styles.css"), "utf8");
+
+ it("keeps top-level spin keyframes rotating to 360deg", () => {
+ const topLevelSpinBlock = extractBlock(css, /@keyframes\s+spin\s*\{/);
+
+ expect(topLevelSpinBlock).toContain("transform: rotate(360deg);");
+ expect(css.indexOf("@keyframes spin")).toBeLessThan(css.indexOf(":root {\n --bg:"));
+ });
+
+ it("keeps the shared animate-spin and spin utilities running infinitely", () => {
+ const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
+ const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
+
+ expect(animateSpinBlock).toContain("animation: spin 1s linear infinite;");
+ expect(spinBlock).toContain("animation: spin 1s linear infinite;");
+ });
+
+ it("anchors SVG spinners around their own center", () => {
+ const animateSpinBlock = css.match(/\.animate-spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
+ const spinBlock = css.match(/\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
+ const svgSpinnerBlock = css.match(/svg\.animate-spin,\s*svg\.spin\s*\{[\s\S]*?\}/)?.[0] ?? "";
+
+ expect(animateSpinBlock).toContain("transform-origin: center;");
+ expect(spinBlock).toContain("transform-origin: center;");
+ expect(svgSpinnerBlock).toContain("transform-box: fill-box;");
+ });
+});
diff --git a/packages/dashboard/app/components/BackgroundTasksIndicator.tsx b/packages/dashboard/app/components/BackgroundTasksIndicator.tsx
index 205d16915..f8009805b 100644
--- a/packages/dashboard/app/components/BackgroundTasksIndicator.tsx
+++ b/packages/dashboard/app/components/BackgroundTasksIndicator.tsx
@@ -119,7 +119,7 @@ export function BackgroundTasksIndicator({
title={`${total} background AI task${total !== 1 ? "s" : ""}${needsInput > 0 ? ` (${needsInput} need${needsInput !== 1 ? "" : "s"} input)` : ""}`}
>
{generating > 0 && (
-
+
)}
{needsInput > 0 && generating === 0 && }
AI {total}
@@ -188,11 +188,8 @@ export function BackgroundTasksIndicator({
{isGenerating && (
)}
{isAwaiting && !activeElsewhere && (
diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css
index c6ffe4bb6..27a19d042 100644
--- a/packages/dashboard/app/styles.css
+++ b/packages/dashboard/app/styles.css
@@ -187,10 +187,17 @@ html {
}
.animate-spin {
animation: spin 1s linear infinite;
+ transform-origin: center;
}
.spin {
animation: spin 1s linear infinite;
+ transform-origin: center;
+}
+
+svg.animate-spin,
+svg.spin {
+ transform-box: fill-box;
}
:root {