From 74821803c97a38558233782d159ed9d1ca3ca21a Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 11:04:25 -0700 Subject: [PATCH 1/5] fix: add global spinner animation to prevent stuck loaders The `.animate-spin` CSS utility class and `@keyframes spin` were only defined in TaskCard.css but used by 25+ components (~60 instances). On pages without a TaskCard rendered, the class didn't exist in the stylesheet, causing all spinners to appear frozen. Moves both the keyframes and utility class to the global styles.css so they're available from the first paint regardless of which components are loaded. Closes Runfusion/Fusion#39 Co-Authored-By: Claude Opus 4.6 --- packages/dashboard/app/styles.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 27f06b5fc..3e875404a 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -164,6 +164,17 @@ html { --transition-normal: 0.2s ease; --transition-slow: 0.3s ease; +/* Global spinner animation — used by 25+ components via className="animate-spin" + or inline animation: "spin ...". Must live here (not in TaskCard.css) so the + keyframes and utility class are available before any task cards are loaded. */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +.animate-spin { + animation: spin 1s linear infinite; +} + /* Backward-compatible aliases */ --radius: var(--radius-md); --shadow: var(--shadow-lg); From c68bfff4f5bb90a78a2b05cb3cba8427d09636ad Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 11:43:24 -0700 Subject: [PATCH 2/5] fix: move spinner keyframes outside :root selector block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @keyframes spin and .animate-spin rules were incorrectly placed inside the first :root {} block, which is invalid CSS — @keyframes cannot be nested inside selector blocks. Browsers silently ignore them in that position, so the spinners would still get stuck on pages without component CSS loaded. Moves the rules to the stylesheet top level between the two :root blocks. Also fixes the mobile-css regression test that parses the first :root block with a non-greedy regex. Addresses review feedback from Greptile (P1) and CodeRabbit on PR #40. Co-Authored-By: Claude Opus 4.6 --- packages/dashboard/app/styles.css | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/dashboard/app/styles.css b/packages/dashboard/app/styles.css index 3e875404a..3cad2dc9f 100644 --- a/packages/dashboard/app/styles.css +++ b/packages/dashboard/app/styles.css @@ -164,17 +164,6 @@ html { --transition-normal: 0.2s ease; --transition-slow: 0.3s ease; -/* Global spinner animation — used by 25+ components via className="animate-spin" - or inline animation: "spin ...". Must live here (not in TaskCard.css) so the - keyframes and utility class are available before any task cards are loaded. */ -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} -.animate-spin { - animation: spin 1s linear infinite; -} - /* Backward-compatible aliases */ --radius: var(--radius-md); --shadow: var(--shadow-lg); @@ -186,6 +175,18 @@ html { --xsmall-breakpoint: 640px; } +/* Global spinner animation — used by 25+ components via className="animate-spin" + or inline animation: "spin ...". Must live at the stylesheet top level (not + inside any selector block) so the keyframes and utility class are available + before any task cards are loaded. */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +.animate-spin { + animation: spin 1s linear infinite; +} + :root { --bg: #0d1117; --surface: #161b22; From 053ad59d423ab8ed513f85ca90f42b9defac60e2 Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 13:03:03 -0700 Subject: [PATCH 3/5] fix: suppress pre-existing no-explicit-any lint in chat.ts test mock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The options parameter in __setCreateFnAgent's inline arrow already uses any to match the module-level let (which has its own suppress comment). Add the missing eslint-disable-next-line so CI passes. Unrelated to the spinner fix — pre-existing on main. Co-Authored-By: Claude Opus 4.6 --- packages/dashboard/src/chat.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dashboard/src/chat.ts b/packages/dashboard/src/chat.ts index a6988c53f..c6a94c24f 100644 --- a/packages/dashboard/src/chat.ts +++ b/packages/dashboard/src/chat.ts @@ -1231,6 +1231,7 @@ export function __setCreateFnAgent(mock: typeof createFnAgent): void { // hit the real engine. Mirror the same fake into the resolved-session slot // so existing test setups that only call `__setCreateFnAgent` continue to // work. + // eslint-disable-next-line @typescript-eslint/no-explicit-any createResolvedAgentSession = (async (options: any) => mock(options)) as typeof createResolvedAgentSession; } From 8f47e8ddd85744d034da09b46da076f651502c08 Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 13:50:02 -0700 Subject: [PATCH 4/5] =?UTF-8?q?ci:=20re-trigger=20checks=20=E2=80=94=20fla?= =?UTF-8?q?ky=20in-process-runtime=20test=20(unrelated=20to=20CSS=20change?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 From b09f7d0602264e538ac79ea3a291addfb9aaef4e Mon Sep 17 00:00:00 2001 From: Timothy Laurent Date: Tue, 5 May 2026 13:55:35 -0700 Subject: [PATCH 5/5] fix: add listMissions to MissionStore mock to prevent flaky test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MissionLoop calls this.missionStore.listMissions() during startup recovery, but the mock TaskStore's getMissionStore() return value didn't include this method. When the runtime startup sequence raced ahead, it would hit "listMissions is not a function" — making the test flaky. Co-Authored-By: Claude Opus 4.6 --- .../engine/src/runtimes/__tests__/in-process-runtime.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/engine/src/runtimes/__tests__/in-process-runtime.test.ts b/packages/engine/src/runtimes/__tests__/in-process-runtime.test.ts index 7af193ed1..460235ac9 100644 --- a/packages/engine/src/runtimes/__tests__/in-process-runtime.test.ts +++ b/packages/engine/src/runtimes/__tests__/in-process-runtime.test.ts @@ -57,6 +57,7 @@ vi.mock("@fusion/core", async () => { self.getTask = mockTaskStoreGetTask; self.getSettings = vi.fn().mockImplementation(async () => structuredClone(mockTaskStoreSettings)); self.getMissionStore = vi.fn().mockReturnValue({ + listMissions: vi.fn().mockReturnValue([]), getMissionWithHierarchy: vi.fn().mockReturnValue(null), findNextPendingSlice: vi.fn().mockReturnValue(null), activateSlice: vi.fn(),