From 0f1cd0a36acb1704a510a6a54ffd18200f4b2653 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 11:49:42 -0700 Subject: [PATCH 01/24] FN-7585: unify border, radius, and height across task-detail Priority/Execution-mode/Oversight controls Unifies the visual styling of the task-detail modal's Priority, Execution-mode, and Oversight quick-control chips so the cluster reads as one consistent control group. - Add a shared --detail-control-border-radius token (resolving to --radius-md) alongside the existing --detail-priority-control-min-height token - Override .detail-priority-chip's inherited transparent border with a visible --btn-border-width/--border pairing so the "normal" priority level renders as a bordered box instead of borderless text - Pin the same border-width/color/radius trio on .detail-execution-mode-toggle so a future change to .btn defaults can't desync the cluster - Apply the same trio to .detail-oversight-chip, overriding .card-oversight-badge's transparent border (covers the neutral "off" tint too) - Apply the same trio to the mobile .detail-oversight-menu-trigger swap-in so the mobile overflow-trigger variant matches the desktop chip - Add a changeset (patch) documenting the fix for @runfusion/fusion - Extend TaskDetailModal.responsive-and-dependencies.test.tsx coverage for the unified styling Files changed: .changeset/FN-7585-unify-task-detail-quick-control-styling.md | 7 +++ packages/dashboard/app/components/TaskDetailModal.css | 55 ++++++++++++++++++++++ packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx | 27 +++++++++++ 3 files changed, 89 insertions(+) Fusion-Task-Id: FN-7585 Fusion-Task-Lineage: 0cca8c82-c9fb-4410-af48-51861a743f96 Co-authored-by: Fusion (runfusion.ai) --- ...unify-task-detail-quick-control-styling.md | 7 +++ .../app/components/TaskDetailModal.css | 55 +++++++++++++++++++ ...Modal.responsive-and-dependencies.test.tsx | 27 +++++++++ 3 files changed, 89 insertions(+) create mode 100644 .changeset/FN-7585-unify-task-detail-quick-control-styling.md diff --git a/.changeset/FN-7585-unify-task-detail-quick-control-styling.md b/.changeset/FN-7585-unify-task-detail-quick-control-styling.md new file mode 100644 index 0000000000..b536ea7b2a --- /dev/null +++ b/.changeset/FN-7585-unify-task-detail-quick-control-styling.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Unify border, radius, and height of the task-detail Priority/Execution/Oversight controls. +category: fix +dev: Adds a shared --detail-control-border-radius token alongside --detail-priority-control-min-height so .detail-priority-chip, .detail-execution-mode-toggle, .detail-oversight-chip, and .detail-oversight-menu-trigger all resolve the same border-width/color/radius/height. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index ebac44574b..d3300f17c2 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -370,8 +370,23 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P /* FNXC:TaskDetail 2026-06-22-20:00: Priority chip and speed (execution-mode) toggle share one min-height token so they render at identical, equal height. Reduced from the old calc(space-2xl + space-xs) (~too tall) to a compact 30px that stays legible and tappable. Both controls also get trimmed vertical padding to match. + + FNXC:TaskDetail 2026-07-05-00:00: + FN-7585 — Priority, Execution-mode, and Oversight (both the desktop chip and + the mobile overflow-trigger variant) previously diverged: Priority inherited + `.card-priority-badge`'s pill radius with a *transparent* border (so the + common `normal` level rendered as borderless text), while Execution-mode and + the Oversight mobile trigger are `.btn.btn-sm` pills with `--radius-md` and a + visible `--border` color. Add one shared `--detail-control-border-radius` + token here so all four controls resolve the same border-radius from a single + source; each control below also pins the same `--btn-border-width` width and + `--border` color so the cluster reads as one uniform control group. Only the + border/radius/size are unified — per-level tint backgrounds (priority + low/high/urgent, oversight observe/steer/autonomous/off, execution-mode + fast) are untouched. */ --detail-priority-control-min-height: 30px; + --detail-control-border-radius: var(--radius-md); display: flex; align-items: stretch; @@ -384,6 +399,16 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P min-height: var(--detail-priority-control-min-height); padding-block: var(--space-xs); box-sizing: border-box; + /* + FNXC:TaskDetail 2026-07-05-00:00: + FN-7585 — override `.card-priority-badge`'s transparent border with a real, + visible border so the `normal` level (no `--low/--high/--urgent` tint) still + renders as a bordered box instead of borderless text, matching the + Execution-mode toggle and Oversight chip/trigger. + */ + border-width: var(--btn-border-width); + border-color: var(--border); + border-radius: var(--detail-control-border-radius); } .detail-priority-chip--saving { @@ -425,6 +450,15 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P min-height: var(--detail-priority-control-min-height); padding-block: var(--space-xs); box-sizing: border-box; + /* + FNXC:TaskDetail 2026-07-05-00:00: + FN-7585 — pin the same shared border/radius token set as the Priority and + Oversight controls even though `.btn` already applies a border, so a future + change to `.btn`'s defaults cannot silently desync this cluster. + */ + border-width: var(--btn-border-width); + border-color: var(--border); + border-radius: var(--detail-control-border-radius); } .detail-execution-mode-toggle svg { @@ -461,12 +495,23 @@ already declares (see TaskCard.css) so oversight-level color stays a single semantic source, and the same `--detail-priority-control-min-height` chip height token as the priority/execution-mode controls above so the cluster renders at a uniform height. + +FNXC:TaskDetail 2026-07-05-00:00: +FN-7585 — like `.detail-priority-chip`, override `.card-oversight-badge`'s +transparent border with the shared `--btn-border-width`/`--border`/ +`--detail-control-border-radius` trio so every oversight level (including +`--off`, which has a neutral tint) renders the same bordered box as Priority +and Execution-mode. The mobile `.detail-oversight-menu-trigger` swap below +gets the identical trio so both oversight variants match. */ .detail-oversight-chip { gap: var(--space-xs); min-height: var(--detail-priority-control-min-height); padding-block: var(--space-xs); box-sizing: border-box; + border-width: var(--btn-border-width); + border-color: var(--border); + border-radius: var(--detail-control-border-radius); } .detail-oversight-chip--saving { @@ -613,6 +658,16 @@ since the trigger lives in the header cluster rather than the footer, unlike min-height: var(--detail-priority-control-min-height); padding-block: var(--space-xs); box-sizing: border-box; + /* + FNXC:TaskDetail 2026-07-05-00:00: + FN-7585 — mobile swap-in for `.detail-oversight-chip` (JS `isOversightMenuMobile` + swap in TaskDetailModal.tsx). Pin the same shared border/radius trio so the + mobile overflow-trigger variant matches the desktop chip and the + Priority/Execution-mode controls, per the FN-7585 surface enumeration. + */ + border-width: var(--btn-border-width); + border-color: var(--border); + border-radius: var(--detail-control-border-radius); } .detail-oversight-menu-trigger svg { diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index 5bf7f21caa..9e348aadaa 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -226,6 +226,33 @@ describe("TaskDetailModal", () => { expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/); }); + it("unifies border/radius/height across the Priority, Execution-mode, and Oversight quick controls (FN-7585)", () => { + const css = readDashboardStylesSource(); + + const inlineControlsBlock = getStandaloneCssRuleBlock(css, ".detail-meta-inline-controls"); + const priorityChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip"); + const executionToggleBlock = getExactCssRuleBlock(css, ".detail-execution-mode-toggle"); + const oversightChipBlock = getExactCssRuleBlock(css, ".detail-oversight-chip"); + const oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger"); + + // The cluster declares one shared border-radius token; all four controls + // must reference it rather than four independent literal radii. + expect(inlineControlsBlock).toContain("--detail-control-border-radius: var(--radius-md);"); + for (const block of [priorityChipBlock, executionToggleBlock, oversightChipBlock, oversightTriggerBlock]) { + expect(block).toContain("border-radius: var(--detail-control-border-radius);"); + expect(block).toContain("border-width: var(--btn-border-width);"); + expect(block).toContain("border-color: var(--border);"); + // Same height token as the rest of the invariant. + expect(block).toContain("min-height: var(--detail-priority-control-min-height);"); + expect(block).toContain("box-sizing: border-box;"); + } + + // Guard against regressing back to four independent literal radius values + // (e.g. reintroducing a bare `var(--radius-pill)` on only the chips). + expect(priorityChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/); + expect(oversightChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/); + }); + it("keeps grouped timestamp metadata inline on desktop and mobile", () => { const css = readDashboardStylesSource(); From aa6d21eecaa319faddd86ffe2bafe7d6f2565561 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 11:57:36 -0700 Subject: [PATCH 02/24] FN-7583: route Android back gesture through fusion:native-back so task detail returns to board Fixes the Android predictive back-gesture (edge swipe) not returning from a task detail view to the board, even though the hardware/legacy Back button worked correctly. - Patch the generated Android manifest post-sync to set android:enableOnBackInvokedCallback="true" on the tag, opting into AndroidX's OnBackPressedDispatcher for predictive-back gesture completion (mitigating ionic-team/capacitor-plugins#2418 via the disableBackButtonHandler toggle shipped in @capacitor/app@7.1.0). - Wire the patch script into Capacitor's capacitor:sync:after npm-script hook so it runs automatically after every cap sync (and therefore after cap run android / build:mobile). - Ensure both the back gesture and the hardware Back button funnel through the same AndroidBackButtonManager backButton listener, dispatching the shared fusion:native-back event consumed by the dashboard's nav-history stack. - Add regression coverage: a unit test for the manifest patch script's idempotent opt-in behavior, and a dashboard test asserting the task detail view returns to the board on fusion:native-back. - Document the Android manifest patch rationale and the unchanged dashboard-side invariant in packages/mobile/README.md. Files changed: .../__tests__/TaskDetail.swipe-back.test.tsx | 13 +++ packages/mobile/README.md | 31 +++++++ packages/mobile/capacitor.config.ts | 13 +++ packages/mobile/package.json | 2 + packages/mobile/scripts/patch-android-manifest.ts | 90 ++++++++++++++++++ packages/mobile/src/__tests__/native-shell.test.ts | 102 +++++++++++++++++++++ 6 files changed, 251 insertions(+) Fusion-Task-Id: FN-7583 Fusion-Task-Lineage: ad0cb02c-6e49-448a-8c93-607cd5ae657b Co-authored-by: Fusion (runfusion.ai) --- .../__tests__/TaskDetail.swipe-back.test.tsx | 13 +++ packages/mobile/README.md | 31 ++++++ packages/mobile/capacitor.config.ts | 13 +++ packages/mobile/package.json | 2 + .../mobile/scripts/patch-android-manifest.ts | 90 ++++++++++++++++ .../mobile/src/__tests__/native-shell.test.ts | 102 ++++++++++++++++++ 6 files changed, 251 insertions(+) create mode 100644 packages/mobile/scripts/patch-android-manifest.ts diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx index f6045f3b19..6b14800011 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx @@ -1,5 +1,18 @@ /** * Focused regression coverage for mobile task-detail swipe-back behavior. + * + * FNXC:TaskDetailAndroidBack 2026-07-05-11:45: + * FN-7583 diagnosed the Android back-GESTURE regression as native-delivery-only: the + * generated AndroidManifest.xml never opted into `android:enableOnBackInvokedCallback`, + * so AndroidX's dispatcher didn't route the predictive-back gesture to + * `@capacitor/app`'s registered callback (fixed via `packages/mobile/scripts/ + * patch-android-manifest.ts`). The dashboard-side dismissal invariant covered below + * (board main-panel / list-mobile / modal / nested detail, via both `popstate` and + * `dispatchNativeAndroidBack()`) was already correct and required NO change for this + * fix — once the gesture reaches the native `backButton` listener, it dispatches the + * exact same `fusion:native-back` event the hardware Back button already used, so this + * suite's existing coverage continues to prove the shared invariant for gesture, button, + * and browser Back alike. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 6dfbe5cbc3..47925cc929 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -14,6 +14,37 @@ Mobile uses a shell-level onboarding flow for first-run connection setup before Native wrappers are isolated under `src/plugins/native-shell.ts`, `src/plugins/connection-profiles.ts`, and `src/plugins/qr-scanner.ts` so dashboard code never calls vendor-specific APIs directly. +## Android Back: hardware button and predictive-back GESTURE both dismiss task detail + +`AndroidBackButtonManager` (`src/plugins/native-shell.ts`) subscribes to `@capacitor/app`'s +`backButton` event and dispatches a single cancelable `fusion:native-back` browser event. +The dashboard's shared nav-history stack (`useNavigationHistory`) consumes that event to +dismiss the top task-detail surface (board main-panel, list-mobile, modal, and nested +detail) and return to the board/previous detail — the exact same invariant the browser +`popstate`/swipe-back path already uses. If nothing handles the event (`!defaultPrevented`), +Capacitor's own fallback runs unchanged: `history.back()` when `canGoBack`, else `exitApp()`. + +**FN-7583:** the Android system back **gesture** (predictive back / edge swipe, Android +13+, default-prominent at this project's `targetSdk 35`) previously did not reach that +listener, even though the hardware/legacy Back button did — the app never opted the +generated manifest into Android's predictive-back framework +(`android:enableOnBackInvokedCallback="true"`), so `OnBackPressedDispatcher` silently +dropped the gesture-completion callback while still delivering button presses via the +ordinary `Window`/`Activity` path. Because the native `android/` project is generated by +`cap sync` and is git-ignored, the fix lives in tracked source: +`scripts/patch-android-manifest.ts` idempotently adds that manifest attribute to the +generated `AndroidManifest.xml`, and is wired into Capacitor's own `capacitor:sync:after` +npm-script hook (see `package.json`) so it runs automatically on every `cap sync` — +including `build:mobile`, `dev:android`, and `cap run android` (which syncs first). No +dashboard-side change was needed: once the gesture reaches the native `backButton` +listener, gesture and button converge on the identical `fusion:native-back` dispatch. + +Manual invocation (e.g. after a bare `npx cap sync` outside `build:mobile`): + +```bash +pnpm --filter @fusion/mobile patch:android-manifest +``` + ### Regression coverage locked by tests `packages/mobile/src/__tests__/connection-profiles.test.ts`, `native-shell.test.ts`, and `qr-scanner.test.ts` now lock these contracts: diff --git a/packages/mobile/capacitor.config.ts b/packages/mobile/capacitor.config.ts index ffc245fe56..33fd67cede 100644 --- a/packages/mobile/capacitor.config.ts +++ b/packages/mobile/capacitor.config.ts @@ -22,6 +22,19 @@ const config: CapacitorConfig = { detectViewportFitCoverChanges: false, initialViewportFitCover: false, }, + // FNXC:TaskDetailAndroidBack 2026-07-05-11:40: + // FN-7583: keep the @capacitor/app native backButton handler ENABLED (the default, + // pinned explicitly below). `AndroidBackButtonManager` in `src/plugins/native-shell.ts` + // relies on the plugin's "backButton" event to dispatch the shared `fusion:native-back` + // event that the dashboard's nav-history stack consumes for both the hardware Back + // button and the Android 13+ predictive-back GESTURE (once the gesture is actually + // delivered — see `scripts/patch-android-manifest.ts` for the manifest opt-in that + // makes that so). Setting `disableBackButtonHandler: true` would stop the plugin from + // emitting "backButton" entirely, breaking BOTH the button and the gesture routing — + // never flip this without replacing `AndroidBackButtonManager`'s dispatch seam too. + App: { + disableBackButtonHandler: false, + }, }, server: { url: liveReloadEnabled diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 110d355594..aecd168db0 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -19,6 +19,8 @@ "dev:ios": "tsx scripts/live-reload.ts --platform ios", "dev:android": "tsx scripts/live-reload.ts --platform android", "build:mobile": "pnpm --filter @fusion/dashboard build && npx cap sync", + "patch:android-manifest": "npx tsx scripts/patch-android-manifest.ts", + "capacitor:sync:after": "npx tsx scripts/patch-android-manifest.ts", "test": "vitest run --silent=passed-only --reporter=dot", "typecheck": "tsc --noEmit" }, diff --git a/packages/mobile/scripts/patch-android-manifest.ts b/packages/mobile/scripts/patch-android-manifest.ts new file mode 100644 index 0000000000..caa84d668e --- /dev/null +++ b/packages/mobile/scripts/patch-android-manifest.ts @@ -0,0 +1,90 @@ +/** + * FNXC:TaskDetailAndroidBack 2026-07-05-11:40: + * FN-7583: the Android system back GESTURE (predictive back / edge swipe, Android 13+, + * default-prominent at this project's `targetSdk 35`) was not reaching the same + * `@capacitor/app` `OnBackPressedCallback` ("backButton" JS event) that the hardware/ + * legacy Back button already routes through — even though both the button and the + * gesture are supposed to funnel through AndroidX's `OnBackPressedDispatcher`. Per + * Android's predictive-back docs (https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture) + * and the confirmed upstream defect (ionic-team/capacitor-plugins#2418, mitigated by + * ionic-team/capacitor-plugins#2390's `disableBackButtonHandler` toggle shipped in + * `@capacitor/app@7.1.0`), an app must opt in via `android:enableOnBackInvokedCallback="true"` + * on the manifest `` tag for the gesture-completion callback to reliably + * reach a registered `OnBackPressedCallback`. Without that opt-in, the OS falls back to + * "legacy" gesture dispatch, which silently drops the callback invocation for the + * gesture while the hardware Back button (delivered via the ordinary Window/Activity + * input path) keeps working — exactly the divergence this task reported. + * + * The native `android/` project is generated by `cap sync` and is git-ignored (see + * AGENTS.md / `.gitignore`), so durable changes cannot live there directly. Instead this + * script idempotently patches the generated manifest immediately after every sync, and + * is wired into Capacitor's own `capacitor:sync:after` npm-script hook (see + * `package.json`), which the Capacitor CLI runs automatically after `cap sync` — and + * therefore after `cap run android` (which syncs first) and `build:mobile` too. + * + * This keeps a SINGLE dismissal invariant: gesture and button both end up delivered to + * `AndroidBackButtonManager`'s `backButton` listener in + * `packages/mobile/src/plugins/native-shell.ts`, which dispatches the cancelable + * `fusion:native-back` event consumed by the dashboard's shared nav-history stack. + */ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ENABLE_ATTR = 'android:enableOnBackInvokedCallback="true"'; + +export function patchManifestSource(xml: string): { changed: boolean; xml: string } { + const applicationTagMatch = xml.match(/]*>/); + if (!applicationTagMatch) { + return { changed: false, xml }; + } + + const applicationTag = applicationTagMatch[0]; + if (applicationTag.includes("android:enableOnBackInvokedCallback")) { + // Already opted in (idempotent — do not duplicate on repeat `cap sync` runs). + return { changed: false, xml }; + } + + const patchedTag = applicationTag.replace(/>$/, ` ${ENABLE_ATTR}>`); + const patchedXml = xml.slice(0, applicationTagMatch.index) + patchedTag + xml.slice((applicationTagMatch.index ?? 0) + applicationTag.length); + return { changed: true, xml: patchedXml }; +} + +export function resolveManifestPath(mobilePackageDir: string): string { + return join(mobilePackageDir, "android", "app", "src", "main", "AndroidManifest.xml"); +} + +export function patchAndroidManifest(mobilePackageDir: string): { patched: boolean; skipped: boolean; path: string } { + const manifestPath = resolveManifestPath(mobilePackageDir); + + if (!existsSync(manifestPath)) { + // No Android platform added yet (e.g. iOS-only sync) — safe no-op, not an error. + return { patched: false, skipped: true, path: manifestPath }; + } + + const original = readFileSync(manifestPath, "utf8"); + const { changed, xml } = patchManifestSource(original); + + if (changed) { + writeFileSync(manifestPath, xml, "utf8"); + } + + return { patched: changed, skipped: false, path: manifestPath }; +} + +function isMainModule(): boolean { + return process.argv[1] === fileURLToPath(import.meta.url); +} + +if (isMainModule()) { + const mobilePackageDir = dirname(fileURLToPath(import.meta.url)).replace(/[/\\]scripts$/, ""); + const result = patchAndroidManifest(mobilePackageDir); + + if (result.skipped) { + console.log(`[patch-android-manifest] no android/ project at ${result.path}; skipping (nothing to patch yet)`); + } else if (result.patched) { + console.log(`[patch-android-manifest] added android:enableOnBackInvokedCallback="true" to ${result.path}`); + } else { + console.log(`[patch-android-manifest] ${result.path} already opts into enableOnBackInvokedCallback; no change`); + } +} diff --git a/packages/mobile/src/__tests__/native-shell.test.ts b/packages/mobile/src/__tests__/native-shell.test.ts index 999ca392eb..623fa4f115 100644 --- a/packages/mobile/src/__tests__/native-shell.test.ts +++ b/packages/mobile/src/__tests__/native-shell.test.ts @@ -1,5 +1,9 @@ +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { buildMobileShellHandoff } from "../plugins/shell-handoff.js"; +import { patchAndroidManifest, patchManifestSource } from "../../scripts/patch-android-manifest.js"; type BackButtonListener = (event: { canGoBack: boolean }) => void; @@ -259,4 +263,102 @@ describe("MobileNativeShellBridge", () => { expect(capacitorState.exitApp).toHaveBeenCalledTimes(1); }); + + /* + FNXC:TaskDetailAndroidBack 2026-07-05-11:45: + FN-7583 — the Android back GESTURE (predictive back / edge swipe, Android 13+) reached + the OS but was never delivered to `AndroidBackButtonManager`'s `backButton` listener + because the generated AndroidManifest.xml never opted into + `android:enableOnBackInvokedCallback="true"`. The raw OS gesture cannot be dispatched + from a unit test, so these tests drive the actual seam the fix introduced + (`patch-android-manifest.ts`) and assert it converges on the exact contract the button + path above already proves: once the manifest opts in, gesture-completion delivery uses + the SAME `OnBackPressedCallback` -> "backButton" -> `dispatchNativeBackEvent()` -> + `fusion:native-back` chain, with the same cancelable-event and empty-stack fallback + semantics. This test fails against the pre-fix tree (no `patch-android-manifest.ts` + module, so the import above throws) and passes after. + */ + describe("Android Back: gesture-delivery manifest opt-in (patch-android-manifest)", () => { + let workDir: string; + + beforeEach(() => { + workDir = mkdtempSync(join(tmpdir(), "fusion-fn7583-manifest-")); + }); + + afterEach(() => { + rmSync(workDir, { recursive: true, force: true }); + }); + + function writeManifest(xml: string): string { + const manifestDir = join(workDir, "android", "app", "src", "main"); + mkdirSync(manifestDir, { recursive: true }); + const manifestPath = join(manifestDir, "AndroidManifest.xml"); + writeFileSync(manifestPath, xml, "utf8"); + return manifestPath; + } + + const baseManifest = ` + + + + + +`; + + it("opts the generated manifest into predictive-back gesture delivery (the seam the gesture needs)", () => { + const manifestPath = writeManifest(baseManifest); + + const result = patchAndroidManifest(workDir); + + expect(result.patched).toBe(true); + expect(result.skipped).toBe(false); + const patchedXml = readFileSync(manifestPath, "utf8"); + expect(patchedXml).toMatch(/]*android:enableOnBackInvokedCallback="true"[^>]*>/); + }); + + it("is idempotent across repeated cap sync runs (does not duplicate the attribute)", () => { + writeManifest(baseManifest); + + const first = patchAndroidManifest(workDir); + const second = patchAndroidManifest(workDir); + + expect(first.patched).toBe(true); + expect(second.patched).toBe(false); + const manifestPath = join(workDir, "android", "app", "src", "main", "AndroidManifest.xml"); + const xml = readFileSync(manifestPath, "utf8"); + expect(xml.match(/android:enableOnBackInvokedCallback/g)).toHaveLength(1); + }); + + it("leaves an already-opted-in manifest untouched", () => { + const alreadyOptedIn = baseManifest.replace( + ' { + const result = patchAndroidManifest(workDir); + + expect(result.skipped).toBe(true); + expect(result.patched).toBe(false); + }); + + it("patchManifestSource preserves the rest of the manifest verbatim aside from the opt-in attribute", () => { + const { changed, xml } = patchManifestSource(baseManifest); + + expect(changed).toBe(true); + expect(xml).toContain(''); + expect(xml).toContain('android:label="@string/app_name"'); + }); + }); }); From da36094d7475efb29b2c403fcba09888539d46f6 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 12:24:12 -0700 Subject: [PATCH 03/24] FN-7586: add iOS edge-swipe-back gesture parity via AppDelegate patch Enables the WKWebView interactive-pop (edge-swipe-back) gesture on iOS, matching the Android predictive-back parity already in place, by patching the generated AppDelegate.swift during cap sync. - Add packages/mobile/scripts/patch-ios-webview.ts: patches AppDelegate.swift to cast the root view controller to CAPBridgeViewController and set webView?.allowsBackForwardNavigationGestures = true before the didFinishLaunchingWithOptions return, is idempotent, and no-ops safely when no ios/ project exists yet - Wire capacitor:sync:after to run both patch-android-manifest.ts and patch-ios-webview.ts so cap sync keeps both native back-gesture opt-ins in sync; add patch:ios-webview script - Add unit tests covering patch, idempotency, already-patched, missing-project, and source-preservation cases for the new iOS webview patch, alongside the existing Android manifest patch tests - Add TaskDetail.swipe-back.test.tsx coverage proving the shared popstate-driven nav-history dismissal stack (no iOS-specific native-back emitter needed) already satisfies the gesture's dismissal contract - Add mobile-scripts.test.ts dashboard coverage and update MOBILE.md / packages/mobile/README.md documenting the new iOS gesture opt-in Files changed: MOBILE.md | 12 ++ .../dashboard/app/__tests__/mobile-scripts.test.ts | 31 +++++ .../__tests__/TaskDetail.swipe-back.test.tsx | 131 +++++++++++++++++++++ packages/mobile/README.md | 36 ++++++ packages/mobile/package.json | 3 +- packages/mobile/scripts/patch-ios-webview.ts | 119 +++++++++++++++++++ packages/mobile/src/__tests__/native-shell.test.ts | 122 +++++++++++++++++++ 7 files changed, 453 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7586 Fusion-Task-Lineage: 248092a1-de1b-49b6-94ef-70675a7b93a1 Co-authored-by: Fusion (runfusion.ai) --- MOBILE.md | 12 ++ .../app/__tests__/mobile-scripts.test.ts | 31 +++++ .../__tests__/TaskDetail.swipe-back.test.tsx | 131 ++++++++++++++++++ packages/mobile/README.md | 36 +++++ packages/mobile/package.json | 3 +- packages/mobile/scripts/patch-ios-webview.ts | 119 ++++++++++++++++ .../mobile/src/__tests__/native-shell.test.ts | 122 ++++++++++++++++ 7 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 packages/mobile/scripts/patch-ios-webview.ts diff --git a/MOBILE.md b/MOBILE.md index ccc13e3f3d..7b4d565ce0 100644 --- a/MOBILE.md +++ b/MOBILE.md @@ -89,6 +89,16 @@ Implementation notes: - Active-profile deletion fallback is shell-owned: deleting the active profile promotes the first remaining profile, and deleting the final profile resets to a clean empty state. - The dashboard consumes this through the shared `window.fusionShell` connection APIs. +### Native Back Handling (Android Back + iOS Edge-Swipe-Back) + +Task-detail dismissal via native "back" (Android hardware Back / predictive-back gesture, +iOS edge-swipe-back, or plain browser swipe-back) converges on a single shared invariant: +the dashboard's `useNavigationHistory` nav-history stack. See `packages/mobile/README.md` +→ "Native Back Handling" for the full Android (`fusion:native-back`) vs. iOS (`popstate`) +routing details and the tracked post-`cap sync` patch scripts (`scripts/ +patch-android-manifest.ts`, `scripts/patch-ios-webview.ts`) that keep each platform's native +gesture delivery enabled across `cap sync` regenerations. + ### Planning Mode Planning Mode opens directly into the composer pane on mobile when no planning sessions exist, avoiding an empty-sidebar dead end. On desktop/tablet the split view is unaffected. Once sessions are saved, mobile shows the session list as usual and the user can navigate between list and detail panes. @@ -186,3 +196,5 @@ Mobile package scripts (`packages/mobile/package.json`): - `dev:ios` - `dev:android` - `build:mobile` +- `patch:ios-webview` — idempotently enables the iOS WKWebView edge-swipe-back gesture in the generated `ios/App/App/AppDelegate.swift` (safe no-op if `ios/` doesn't exist yet) +- `capacitor:sync:after` — Capacitor's own post-`cap sync` hook; currently runs the iOS webview patch so `cap sync` regeneration can't silently drop the gesture opt-in diff --git a/packages/dashboard/app/__tests__/mobile-scripts.test.ts b/packages/dashboard/app/__tests__/mobile-scripts.test.ts index c16e85021f..51e6da5a88 100644 --- a/packages/dashboard/app/__tests__/mobile-scripts.test.ts +++ b/packages/dashboard/app/__tests__/mobile-scripts.test.ts @@ -43,4 +43,35 @@ describe("mobile pipeline scripts", () => { expect(packageJson.scripts?.["mobile:ios"] ?? "").toContain("ios"); expect(packageJson.scripts?.["mobile:android"] ?? "").toContain("android"); }); + + /* + FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: + FN-7586: `WKWebView.allowsBackForwardNavigationGestures` defaults to false and no + capacitor.config toggle exists for it (confirmed against the installed @capacitor/cli + declarations), so the fix is a tracked post-`cap sync` patch script + (`packages/mobile/scripts/patch-ios-webview.ts`) wired into Capacitor's own + `capacitor:sync:after` npm hook — mirroring FN-7583's Android manifest-patch precedent — + so `cap sync` regeneration of the git-ignored `ios/` project can never silently drop the + gesture opt-in. This asserts the patch/wiring is present in tracked source; it fails on the + pre-fix tree (no patch script, no hook) and passes after. + */ + describe("iOS edge-swipe-back gesture patch wiring (FN-7586)", () => { + const mobilePackagePath = resolve(__dirname, "../../../mobile/package.json"); + const patchScriptPath = resolve(__dirname, "../../../mobile/scripts/patch-ios-webview.ts"); + + it("ships a tracked post-cap-sync iOS WKWebView patch script", () => { + const source = readFileSync(patchScriptPath, "utf8"); + + expect(source).toContain("allowsBackForwardNavigationGestures"); + expect(source).toContain("CAPBridgeViewController"); + }); + + it("wires the iOS webview patch into a Capacitor sync hook so cap sync cannot drop it", () => { + const mobilePackageJson = JSON.parse(readFileSync(mobilePackagePath, "utf8")) as WorkspacePackageJson; + const scripts = mobilePackageJson.scripts ?? {}; + + const syncAfterHook = scripts["capacitor:sync:after"] ?? ""; + expect(syncAfterHook).toContain("patch-ios-webview"); + }); + }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx index 6b14800011..4071908d83 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx @@ -802,4 +802,135 @@ describe("Task detail mobile swipe-back", () => { expect(window.history.pushState).not.toHaveBeenCalled(); expect(screen.queryByTestId("task-detail-modal")).toBeNull(); }); + + /* + FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: + FN-7586 diagnosed the iOS edge-swipe-back gap as native-delivery-only: WKWebView's + `allowsBackForwardNavigationGestures` defaults to `false` and Capacitor never sets it, so + the gesture never fires at all today (fixed via `packages/mobile/scripts/ + patch-ios-webview.ts`, an AppDelegate patch wired into the `capacitor:sync:after` hook). + Once enabled, iOS drives ordinary WKWebView back-forward navigation, which is delivered to + the page as a `popstate` — the EXACT SAME seam desktop/browser swipe-back already uses. + This suite's existing `dispatchPopState` coverage above (board main-panel, list-mobile, + nested modal) therefore already proves the shared nav-history dismissal invariant for the + iOS gesture too, and required NO dashboard-side change. This describe block adds + iOS-labeled regression coverage (same `popstate` seam, explicitly named) plus the one gap + not yet covered above: the empty-stack fallback via `popstate` (no Fusion nav entry -> + safe no-op, matching how native Android Back already no-ops on an empty stack). + */ + describe("iOS edge-swipe-back (popstate seam)", () => { + it("dismisses the board main-panel task detail on iOS edge-swipe-back (popstate)", async () => { + const task = makeTask("FN-1", "iOS Board Detail"); + mockUseTasks.mockImplementation(() => ({ + tasks: [task], + createTask: mockCreateTask, + moveTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + refreshTasks: vi.fn(), + })); + + await renderAppAndWait("board-view"); + fireEvent.click(screen.getByTestId("open-task-FN-1")); + + await waitFor(() => { + expect(screen.getByTestId("task-detail-main-panel-content")).toBeInTheDocument(); + }); + + dispatchPopState({ navIndex: 0 }); + + await waitFor(() => { + expect(screen.queryByTestId("task-detail-main-panel-content")).toBeNull(); + expect(screen.getByTestId("board-view")).toBeInTheDocument(); + }); + }); + + it("dismisses the list-mobile task detail on iOS edge-swipe-back (popstate)", async () => { + const task = makeTask("FN-1", "iOS Mobile List Detail"); + mockUseTasks.mockImplementation(() => ({ + tasks: [task], + createTask: mockCreateTask, + moveTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + refreshTasks: vi.fn(), + })); + localStorage.setItem("kb-dashboard-view-mode", "project"); + localStorage.setItem(scopedKey("kb-dashboard-task-view", DEFAULT_PROJECT_ID), "list"); + + await renderAppAndWait("list-view"); + fireEvent.click(screen.getByTestId("list-open-FN-1")); + + await waitFor(() => { + expect(screen.getByTestId("task-detail-modal")).toBeInTheDocument(); + }); + + dispatchPopState({ navIndex: 0 }); + + await waitFor(() => { + expect(screen.queryByTestId("task-detail-modal")).toBeNull(); + expect(screen.getByTestId("list-view")).toBeInTheDocument(); + }); + }); + + it("restores the previous modal detail when iOS edge-swipe-back (popstate) closes a nested task detail", async () => { + const task = makeTask("FN-1", "iOS Parent Task"); + mockUseTasks.mockImplementation(() => ({ + tasks: [task, makeTask("FN-2", "iOS Nested Task")], + createTask: mockCreateTask, + moveTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + refreshTasks: vi.fn(), + })); + localStorage.setItem("kb-dashboard-view-mode", "project"); + localStorage.setItem(scopedKey("kb-dashboard-task-view", DEFAULT_PROJECT_ID), "list"); + + await renderAppAndWait("list-view"); + fireEvent.click(screen.getByTestId("list-open-FN-1")); + await waitFor(() => { + expect(screen.getByRole("dialog", { name: "iOS Parent Task" })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId("task-detail-open-nested")); + await waitFor(() => { + expect(screen.getByRole("dialog", { name: "iOS Nested Task" })).toBeInTheDocument(); + }); + + dispatchPopState({ navIndex: 1 }); + + await waitFor(() => { + expect(screen.getByRole("dialog", { name: "iOS Parent Task" })).toBeInTheDocument(); + }); + expect(screen.queryByRole("dialog", { name: "iOS Nested Task" })).toBeNull(); + }); + + it("safely no-ops on iOS edge-swipe-back (popstate) when Fusion owns no nav entry", async () => { + await renderAppAndWait("board-view"); + + expect(() => dispatchPopState(null)).not.toThrow(); + + await waitFor(() => { + expect(screen.getByTestId("board-view")).toBeInTheDocument(); + }); + }); + }); }); diff --git a/packages/mobile/README.md b/packages/mobile/README.md index 47925cc929..acbc994cdc 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -53,6 +53,42 @@ pnpm --filter @fusion/mobile patch:android-manifest - bridge reads (`getState`, `listProfiles`) plus connection-manager event dispatch - malformed/empty QR payload handling and unavailable-scanner fallback behavior +## Native Back Handling (Android Back + iOS Edge-Swipe-Back) + +Mobile task-detail dismissal converges on a SINGLE shared invariant regardless of how the +user triggers "back": the dashboard's `useNavigationHistory` nav-history stack +(`packages/dashboard/app/hooks/useNavigationHistory.ts`), which board main-panel, +list-mobile, modal, and nested task-detail surfaces already consume for desktop/browser +swipe-back via ordinary `popstate`. + +- **Android hardware Back button + gesture (FN-7583):** `AndroidBackButtonManager` in + `src/plugins/native-shell.ts` listens for `@capacitor/app`'s `backButton` event and + dispatches a cancelable `fusion:native-back` custom event; `useNavigationHistory` consumes + it to dismiss the top task-detail surface (falling back to native browser-history `back()` + or app exit when Fusion owns no nav entry). The Android 13+ predictive-back **gesture** + additionally requires the generated `AndroidManifest.xml` to opt into + `android:enableOnBackInvokedCallback="true"` for AndroidX's dispatcher to route the + gesture-completion callback to the same `backButton` listener — since `android/` is + generated by `cap sync` and git-ignored, this opt-in lives in a tracked post-`cap sync` + patch (`scripts/patch-android-manifest.ts`), wired into the `capacitor:sync:after` npm hook. +- **iOS edge-swipe-back (FN-7586):** iOS has **no** Capacitor `backButton` event, so it does + NOT go through `fusion:native-back`. Instead, the WKWebView edge-swipe-back gesture (once + enabled) drives ordinary back-forward navigation, which the browser delivers as a + `popstate` — the exact same seam desktop/browser swipe-back already uses, and that + `useNavigationHistory.handlePopState` already consumes correctly. The gesture itself is OFF + by default: `WKWebView.allowsBackForwardNavigationGestures` defaults to `false`, Capacitor's + `CAPBridgeViewController` never sets it, and no `capacitor.config.ts` `ios` toggle exists + for it. Since `ios/` is also generated by `cap sync` and git-ignored, this is enabled via a + tracked post-`cap sync` patch (`scripts/patch-ios-webview.ts`) that idempotently patches the + generated `AppDelegate.swift` to set `webView?.allowsBackForwardNavigationGestures = true` + once the storyboard's root `CAPBridgeViewController` has loaded — also wired into the same + `capacitor:sync:after` npm hook. Empty-stack behavior (no Fusion nav entry) is unaffected: + the gesture just performs ordinary WKWebView history navigation with no forced trap/exit. + +Both patch scripts are safe no-ops when their respective native platform directory +(`android/` / `ios/`) hasn't been added yet, and are idempotent across repeated `cap sync` +runs (see `src/__tests__/native-shell.test.ts`). + ## Push Notifications `PushNotificationManager` supports two complementary notification channels: diff --git a/packages/mobile/package.json b/packages/mobile/package.json index aecd168db0..fe473d5575 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -20,7 +20,8 @@ "dev:android": "tsx scripts/live-reload.ts --platform android", "build:mobile": "pnpm --filter @fusion/dashboard build && npx cap sync", "patch:android-manifest": "npx tsx scripts/patch-android-manifest.ts", - "capacitor:sync:after": "npx tsx scripts/patch-android-manifest.ts", + "patch:ios-webview": "npx tsx scripts/patch-ios-webview.ts", + "capacitor:sync:after": "npx tsx scripts/patch-android-manifest.ts && npx tsx scripts/patch-ios-webview.ts", "test": "vitest run --silent=passed-only --reporter=dot", "typecheck": "tsc --noEmit" }, diff --git a/packages/mobile/scripts/patch-ios-webview.ts b/packages/mobile/scripts/patch-ios-webview.ts new file mode 100644 index 0000000000..6f906b7394 --- /dev/null +++ b/packages/mobile/scripts/patch-ios-webview.ts @@ -0,0 +1,119 @@ +/** + * FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: + * FN-7586: the iOS edge-swipe-back gesture (WKWebView interactive pop / back-forward + * navigation gesture) is a guaranteed no-op on a stock Capacitor 7 iOS build of this app. + * `WKWebView.allowsBackForwardNavigationGestures` defaults to `false` + * (https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu), + * `CAPBridgeViewController.prepareWebView` (in `@capacitor/ios`) sets several WKWebView + * properties from `capacitor.config.ts` (`allowsLinkPreview`, `scrollView.isScrollEnabled`, + * background color, content-inset behavior) but never touches this one, and the installed + * `@capacitor/cli`'s `ios` config block (see `declarations.d.ts`) exposes no first-class + * toggle for it either. Without the gesture enabled, iOS never even reaches + * `window.history.back()` / `popstate` — so the dashboard's shared nav-history dismissal + * stack (`useNavigationHistory.handlePopState`), which board/list/modal/nested task-detail + * surfaces already converge on for desktop/browser swipe-back, never gets a chance to run. + * + * The native `ios/` project is generated by `cap sync` (via `cap add ios`, which extracts + * `@capacitor/cli`'s bundled `ios-pods-template.tar.gz`) and is git-ignored (see AGENTS.md / + * `.gitignore`), so durable changes cannot live there directly — mirroring FN-7583's Android + * manifest-patch precedent (`patch-android-manifest.ts`), this script idempotently patches + * the generated `AppDelegate.swift` immediately after every sync, wired into Capacitor's own + * `capacitor:sync:after` npm-script hook (see `package.json`), which the Capacitor CLI runs + * automatically after `cap sync` (and therefore after `cap run ios` / `build:mobile` too). + * + * Patch target: the stock template has NO custom `CAPBridgeViewController` subclass — the + * generated `Main.storyboard` wires `customClass="CAPBridgeViewController"` directly onto the + * scene's view controller. `AppDelegate.didFinishLaunchingWithOptions` is therefore the + * stable, minimal patch point: by the time it runs, `UIApplicationMain` has already loaded + * the storyboard's root view controller into `window.rootViewController`. The patch casts it + * to `CAPBridgeViewController`, forces `loadViewIfNeeded()` so `webView` is non-nil (the + * bridge view controller creates its WKWebView lazily in `loadView()`), then sets + * `webView?.allowsBackForwardNavigationGestures = true`. + * + * This keeps a SINGLE dismissal invariant across platforms: once the gesture is delivered, + * iOS drives ordinary WKWebView back-forward navigation -> `popstate`, the exact same arm + * browser/desktop swipe-back already uses and that `useNavigationHistory.handlePopState` + * already consumes for board main-panel / list-mobile / modal / nested task-detail surfaces. + * iOS has no Capacitor `backButton` event (that's Android-only, see + * `src/plugins/native-shell.ts`'s `AndroidBackButtonManager`), so this does NOT add an iOS + * `fusion:native-back` emitter — it reuses the existing `popstate` arm of the shared + * invariant instead of introducing a second, divergent back-handling path. + */ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const MARKER = "allowsBackForwardNavigationGestures"; + +const DID_FINISH_LAUNCHING_SIGNATURE = + /func application\(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: \[UIApplication\.LaunchOptionsKey: Any\]\?\) -> Bool \{/; + +function buildInjectedSnippet(): string { + return ` + // FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: + // FN-7586: enable the WKWebView edge-swipe-back gesture so it drives ordinary + // back-forward navigation (-> popstate), matching Android Back's fusion:native-back + // arm and desktop/browser swipe-back. See packages/mobile/scripts/patch-ios-webview.ts + // for the full rationale (idempotent — this line is the presence marker). + if let bridgeViewController = self.window?.rootViewController as? CAPBridgeViewController { + bridgeViewController.loadViewIfNeeded() + bridgeViewController.webView?.${MARKER} = true + } +`; +} + +export function patchAppDelegateSource(swift: string): { changed: boolean; swift: string } { + if (swift.includes(MARKER)) { + // Already opted in (idempotent — do not duplicate on repeat `cap sync` runs). + return { changed: false, swift }; + } + + const signatureMatch = swift.match(DID_FINISH_LAUNCHING_SIGNATURE); + if (!signatureMatch || signatureMatch.index === undefined) { + // Unrecognized AppDelegate shape (e.g. a hand-customized template) — do not guess. + return { changed: false, swift }; + } + + const insertAt = signatureMatch.index + signatureMatch[0].length; + const patched = swift.slice(0, insertAt) + buildInjectedSnippet() + swift.slice(insertAt); + return { changed: true, swift: patched }; +} + +export function resolveAppDelegatePath(mobilePackageDir: string): string { + return join(mobilePackageDir, "ios", "App", "App", "AppDelegate.swift"); +} + +export function patchIOSWebView(mobilePackageDir: string): { patched: boolean; skipped: boolean; path: string } { + const appDelegatePath = resolveAppDelegatePath(mobilePackageDir); + + if (!existsSync(appDelegatePath)) { + // No iOS platform added yet (e.g. Android-only sync) — safe no-op, not an error. + return { patched: false, skipped: true, path: appDelegatePath }; + } + + const original = readFileSync(appDelegatePath, "utf8"); + const { changed, swift } = patchAppDelegateSource(original); + + if (changed) { + writeFileSync(appDelegatePath, swift, "utf8"); + } + + return { patched: changed, skipped: false, path: appDelegatePath }; +} + +function isMainModule(): boolean { + return process.argv[1] === fileURLToPath(import.meta.url); +} + +if (isMainModule()) { + const mobilePackageDir = dirname(fileURLToPath(import.meta.url)).replace(/[/\\]scripts$/, ""); + const result = patchIOSWebView(mobilePackageDir); + + if (result.skipped) { + console.log(`[patch-ios-webview] no ios/ project at ${result.path}; skipping (nothing to patch yet)`); + } else if (result.patched) { + console.log(`[patch-ios-webview] enabled allowsBackForwardNavigationGestures in ${result.path}`); + } else { + console.log(`[patch-ios-webview] ${result.path} already enables allowsBackForwardNavigationGestures; no change`); + } +} diff --git a/packages/mobile/src/__tests__/native-shell.test.ts b/packages/mobile/src/__tests__/native-shell.test.ts index 623fa4f115..b261b755d9 100644 --- a/packages/mobile/src/__tests__/native-shell.test.ts +++ b/packages/mobile/src/__tests__/native-shell.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { buildMobileShellHandoff } from "../plugins/shell-handoff.js"; import { patchAndroidManifest, patchManifestSource } from "../../scripts/patch-android-manifest.js"; +import { patchAppDelegateSource, patchIOSWebView } from "../../scripts/patch-ios-webview.js"; type BackButtonListener = (event: { canGoBack: boolean }) => void; @@ -264,6 +265,7 @@ describe("MobileNativeShellBridge", () => { expect(capacitorState.exitApp).toHaveBeenCalledTimes(1); }); + /* /* FNXC:TaskDetailAndroidBack 2026-07-05-11:45: FN-7583 — the Android back GESTURE (predictive back / edge swipe, Android 13+) reached @@ -361,4 +363,124 @@ describe("MobileNativeShellBridge", () => { expect(xml).toContain('android:label="@string/app_name"'); }); }); + + /* + FNXC:TaskDetailIOSSwipeBack 2026-07-05-12:10: + FN-7586 — the iOS edge-swipe-back GESTURE (WKWebView interactive pop) is a no-op on a + stock Capacitor 7 iOS build: `WKWebView.allowsBackForwardNavigationGestures` defaults to + `false`, `CAPBridgeViewController.prepareWebView` never sets it, and no capacitor.config + toggle exists. The raw WKWebView gesture cannot be dispatched from a unit test, so these + tests drive the actual seam the fix introduces (`patch-ios-webview.ts`) and assert it + converges on the exact contract the popstate-based coverage in + `TaskDetail.swipe-back.test.tsx` already proves: once AppDelegate enables the gesture, iOS + drives ordinary WKWebView back-forward navigation -> `popstate` -> the shared nav-history + dismissal stack, with no iOS-specific `fusion:native-back` emitter introduced. This test + fails against the pre-fix tree (no `patch-ios-webview.ts` module, so the import above + throws) and passes after. + */ + describe("iOS Back: edge-swipe-back gesture opt-in (patch-ios-webview)", () => { + let workDir: string; + + beforeEach(() => { + workDir = mkdtempSync(join(tmpdir(), "fusion-fn7586-appdelegate-")); + }); + + afterEach(() => { + rmSync(workDir, { recursive: true, force: true }); + }); + + function writeAppDelegate(swift: string): string { + const appDir = join(workDir, "ios", "App", "App"); + mkdirSync(appDir, { recursive: true }); + const appDelegatePath = join(appDir, "AppDelegate.swift"); + writeFileSync(appDelegatePath, swift, "utf8"); + return appDelegatePath; + } + + const baseAppDelegate = `import UIKit +import Capacitor + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + } + +} +`; + + it("enables the WKWebView edge-swipe-back gesture in the generated AppDelegate (the seam the gesture needs)", () => { + const appDelegatePath = writeAppDelegate(baseAppDelegate); + + const result = patchIOSWebView(workDir); + + expect(result.patched).toBe(true); + expect(result.skipped).toBe(false); + const patchedSwift = readFileSync(appDelegatePath, "utf8"); + expect(patchedSwift).toContain("as? CAPBridgeViewController"); + expect(patchedSwift).toContain("webView?.allowsBackForwardNavigationGestures = true"); + // Injected before the existing `return true` so the override still returns true. + expect(patchedSwift.indexOf("allowsBackForwardNavigationGestures")).toBeLessThan( + patchedSwift.indexOf("return true"), + ); + }); + + it("is idempotent across repeated cap sync runs (does not duplicate the patch)", () => { + writeAppDelegate(baseAppDelegate); + + const first = patchIOSWebView(workDir); + const second = patchIOSWebView(workDir); + + expect(first.patched).toBe(true); + expect(second.patched).toBe(false); + const appDelegatePath = join(workDir, "ios", "App", "App", "AppDelegate.swift"); + const swift = readFileSync(appDelegatePath, "utf8"); + expect(swift.match(/allowsBackForwardNavigationGestures/g)).toHaveLength(1); + }); + + it("leaves an already-patched AppDelegate untouched", () => { + const alreadyPatched = baseAppDelegate.replace( + "// Override point for customization after application launch.", + "// Override point for customization after application launch.\n self.window?.rootViewController.map { _ in }\n // allowsBackForwardNavigationGestures already set elsewhere", + ); + writeAppDelegate(alreadyPatched); + + const result = patchIOSWebView(workDir); + + expect(result.patched).toBe(false); + const swift = readFileSync(join(workDir, "ios", "App", "App", "AppDelegate.swift"), "utf8"); + expect(swift.match(/allowsBackForwardNavigationGestures/g)).toHaveLength(1); + }); + + it("no-ops safely (does not throw) when no ios/ project has been added yet", () => { + const result = patchIOSWebView(workDir); + + expect(result.skipped).toBe(true); + expect(result.patched).toBe(false); + }); + + it("patchAppDelegateSource preserves the rest of AppDelegate verbatim aside from the injected snippet", () => { + const { changed, swift } = patchAppDelegateSource(baseAppDelegate); + + expect(changed).toBe(true); + expect(swift).toContain("func applicationWillResignActive(_ application: UIApplication) {"); + expect(swift).toContain("class AppDelegate: UIResponder, UIApplicationDelegate {"); + }); + + it("does not change patchAppDelegateSource when the didFinishLaunchingWithOptions signature is unrecognized", () => { + const unrecognized = "import UIKit\nclass AppDelegate {}\n"; + + const { changed, swift } = patchAppDelegateSource(unrecognized); + + expect(changed).toBe(false); + expect(swift).toBe(unrecognized); + }); + }); }); From 8a7507ad36ac118f07224c2329a051a868bb2811 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 12:38:01 -0700 Subject: [PATCH 04/24] FN-7587: add predictive-back slide/fade animation for mobile task-detail dismissal Adds a presentation-only enter animation for mobile task-detail surfaces (modal and board main-panel), layered on top of the existing FN-7583/FN-7586 dismissal routing, without altering close/back timing. - Gate a new `.task-detail-modal--mobile-transition` class in TaskDetailModal.tsx via a local resize listener at the 768px breakpoint, mirroring the existing OVERSIGHT_MENU_MOBILE_BREAKPOINT pattern - Add matching `.task-detail-main-panel--mobile-transition` modifier in MainContent.tsx gated by the existing isMobile prop - Add slide/fade keyframe animations in TaskDetailModal.css and styles.css, both honoring prefers-reduced-motion - Add regression tests covering the modal and board-panel mobile transition behavior - Document the Capacitor WebView limitation preventing a true interactive predictive-back in packages/mobile/README.md Files changed: .../dashboard/app/components/TaskDetailModal.css | 33 ++ .../dashboard/app/components/TaskDetailModal.tsx | 31 +- ...skDetail.mobile-transition.board-panel.test.tsx | 333 +++++++++++++++++++++ .../TaskDetail.mobile-transition.test.tsx | 156 ++++++++++ .../app/components/dashboard/MainContent.tsx | 10 +- packages/dashboard/app/styles.css | 35 +++ packages/mobile/README.md | 30 ++ 7 files changed, 626 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7587 Fusion-Task-Lineage: cc5f08df-4aaf-447d-9c30-237b32191d3f Co-authored-by: Fusion (runfusion.ai) --- .../app/components/TaskDetailModal.css | 33 ++ .../app/components/TaskDetailModal.tsx | 31 +- ...ail.mobile-transition.board-panel.test.tsx | 333 ++++++++++++++++++ .../TaskDetail.mobile-transition.test.tsx | 156 ++++++++ .../app/components/dashboard/MainContent.tsx | 10 +- packages/dashboard/app/styles.css | 35 ++ packages/mobile/README.md | 30 ++ 7 files changed, 626 insertions(+), 2 deletions(-) create mode 100644 packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx create mode 100644 packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.test.tsx diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index d3300f17c2..c4418e0345 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -1715,6 +1715,39 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a } +/* +FNXC:TaskDetailSwipeBack 2026-07-05-12:30: +FN-7587 — non-interactive predictive-back polish: a short slide/fade enter animation for the +mobile list/modal/nested task-detail surface, layered purely on top of the unchanged +FN-7583/FN-7586 dismissal routing (popstate / fusion:native-back / useNavigationHistory stack). +Gated to mobile via `.task-detail-modal--mobile-transition` (TaskDetailModal.tsx local resize +listener); desktop never receives this class. A true finger-tracked interactive predictive-back +is not feasible from a Capacitor single-page WebView today — see task FN-7587 notes. +*/ +@media (max-width: 768px) { + .task-detail-modal--mobile-transition { + animation: task-detail-modal-mobile-slide-fade-in var(--duration-normal) ease-out; + } +} + +@keyframes task-detail-modal-mobile-slide-fade-in { + from { + opacity: 0; + transform: translateX(var(--space-xl, 24px)); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .task-detail-modal--mobile-transition { + animation: none; + } +} + .detail-actions-menu-item-danger { color: var(--color-error); } diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 294362fe83..010c127e3b 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -88,6 +88,8 @@ const ACTIVITY_VIEW_MENU_MAX_HEIGHT = 320; const ACTIVITY_VIEW_MENU_OPEN_VIEWPORT_GUARD_MS = 350; // FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 — mobile breakpoint for collapsing the oversight action cluster into an overflow menu; matches the `@media (max-width: 768px)` breakpoint used across TaskDetailModal.css. const OVERSIGHT_MENU_MOBILE_BREAKPOINT = 768; +// FNXC:TaskDetailSwipeBack 2026-07-05-12:30: FN-7587 — mobile breakpoint gating the presentation-only predictive-back slide/fade transition on the modal/list/nested task-detail surface; matches OVERSIGHT_MENU_MOBILE_BREAKPOINT/the `@media (max-width: 768px)` convention already used in this file. +const TASK_DETAIL_MOBILE_TRANSITION_BREAKPOINT = 768; type ActivityViewMenuPosition = { top: number; @@ -5997,6 +5999,30 @@ export function TaskDetailModal({ onClose, ...props }: TaskDetailModalProps) { useModalResizePersist(modalRef, true, "task-detail-modal-size"); useMobileScrollLock(true); const overlayDismissProps = useOverlayDismiss(onClose); + /* + FNXC:TaskDetailSwipeBack 2026-07-05-12:30: + FN-7587 — track the mobile breakpoint locally (mirrors the OVERSIGHT_MENU_MOBILE_BREAKPOINT + resize-listener pattern above) so the list/modal/nested task-detail surface gets the same + presentation-only predictive-back slide/fade enter transition as the board main-panel + (MainContent.tsx), without threading a new isMobile prop through App.tsx/AppModals.tsx. This + is presentation-only: it never touches onClose/onRequestClose timing or the underlying + useNavigationHistory dismissal routing, and honors prefers-reduced-motion (see + TaskDetailModal.css). Defaults false so JSDOM/unit tests keep exercising the desktop (no + animation) branch unless a test explicitly narrows the viewport. + */ + const [isMobileTransition, setIsMobileTransition] = useState(false); + useEffect(() => { + const updateIsMobileTransition = () => { + setIsMobileTransition(window.innerWidth <= TASK_DETAIL_MOBILE_TRANSITION_BREAKPOINT); + }; + + updateIsMobileTransition(); + window.addEventListener("resize", updateIsMobileTransition); + + return () => { + window.removeEventListener("resize", updateIsMobileTransition); + }; + }, []); return (
-
+
vi.fn()); +vi.mock("../../sse-bus", () => ({ + subscribeSse: (...args: any[]) => mockSubscribeSse(...args), +})); + +vi.mock("../../api", async (importOriginal) => { + const { createDashboardApiMock } = await import("../../test/mockApi"); + return createDashboardApiMock(() => importOriginal(), { + fetchTasks: vi.fn(() => Promise.resolve([])), + fetchConfig: vi.fn(() => Promise.resolve({ maxConcurrent: 2, rootDir: "/workspace/project" })), + fetchSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })), + updateSettings: vi.fn(() => Promise.resolve({ ...defaultSettings })), + fetchGlobalSettings: vi.fn(() => Promise.resolve({})), + fetchAuthStatus: vi.fn(() => Promise.resolve({ providers: [] })), + fetchModels: vi.fn(() => Promise.resolve({ models: [], favoriteProviders: [], favoriteModels: [] })), + fetchGitRemotes: vi.fn(() => Promise.resolve([])), + fetchAgents: vi.fn(() => Promise.resolve([])), + fetchTaskDetail: vi.fn((id: string) => Promise.resolve({ id, title: `Task ${id}` })), + fetchUnreadCount: vi.fn(() => Promise.resolve({ unreadCount: 0 })), + fetchPluginDashboardViews: vi.fn(() => Promise.resolve([])), + fetchExecutorStats: vi.fn(() => Promise.resolve({ + globalPause: false, + enginePaused: false, + maxConcurrent: 2, + lastActivityAt: new Date().toISOString(), + })), + fetchScripts: vi.fn(() => Promise.resolve({})), + runScript: vi.fn(() => Promise.resolve({ sessionId: "sess-1", command: "echo" })), + killPtyTerminalSession: vi.fn(() => Promise.resolve({ killed: true })), + }); +}); + +const mockCreateTask = vi.fn(); +const mockUseTasks = vi.fn(() => ({ + tasks: [], + createTask: mockCreateTask, + moveTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + refreshTasks: vi.fn(), +})); +vi.mock("../../hooks/useTasks", () => ({ + useTasks: (_options?: any) => mockUseTasks(), +})); + +vi.mock("../../hooks/useInsights", () => ({ + useInsights: () => ({ + sections: [], loading: false, error: null, latestRun: null, + isRunInFlight: false, runError: null, refresh: vi.fn(), + runInsights: vi.fn(), dismiss: vi.fn(), createTask: vi.fn(), + dismissStates: new Map(), createTaskStates: new Map(), + totalCount: 0, dismissedCount: 0, + }), +})); + +vi.mock("../../hooks/useRemoteNodeData", () => ({ + useRemoteNodeData: vi.fn(() => ({ + projects: [], tasks: [], health: null, loading: false, + error: null, refresh: vi.fn(), + })), +})); + +vi.mock("../../hooks/useRemoteNodeEvents", () => ({ + useRemoteNodeEvents: vi.fn(() => ({ isConnected: false, lastEvent: null })), +})); + +vi.mock("../../hooks/useBackgroundSessions", () => ({ + useBackgroundSessions: vi.fn(() => ({ + sessions: [], generating: false, needsInput: false, + planningSessions: [], dismissSession: vi.fn(), + })), +})); + +const mockNodeContextValue = { + currentNode: null, currentNodeId: null, isRemote: false, + setCurrentNode: vi.fn(), clearCurrentNode: vi.fn(), +}; +vi.mock("../../context/NodeContext", () => ({ + NodeProvider: ({ children }: { children: React.ReactNode }) => children, + useNodeContext: vi.fn(() => mockNodeContextValue), +})); + +vi.mock("../../components/model-onboarding-state", () => ({ + isOnboardingResumable: () => false, + getOnboardingResumeStep: () => null, + getOnboardingState: () => null, + saveOnboardingState: vi.fn(), + clearOnboardingState: vi.fn(), + isOnboardingCompleted: () => false, + markOnboardingCompleted: vi.fn(), + markStepSkipped: vi.fn(), + getOnboardingCompletedAt: () => null, + getSkippedSteps: () => [], + getStepData: () => null, + ONBOARDING_FLOW_STEPS: ["ai-setup", "github", "project-setup", "agent", "first-task"], +})); + +vi.mock("../../components/Board", () => ({ + Board: ({ tasks, onOpenDetail }: { tasks: Task[]; onOpenDetail: (task: Task) => void }) => ( +
+ {tasks.map((task) => ( + + ))} +
+ ), +})); + +vi.mock("../../components/ListView", () => ({ + ListView: () =>
, +})); + +vi.mock("../../components/TaskDetailModal", () => ({ + TaskDetailModal: () => null, + TaskDetailContent: ({ task }: { task: { id: string; title?: string } }) => ( +
+

{task.title ?? task.id}

+
+ ), +})); + +vi.mock("../../components/SettingsModal", () => ({ + SettingsModal: () => null, + SettingsView: () =>
Settings
, +})); + +vi.mock("../../components/GitHubImportModal", () => ({ GitHubImportModal: () => null })); +vi.mock("../../components/PlanningModeModal", () => ({ PlanningModeModal: () => null })); +vi.mock("../../components/AgentsView", () => ({ AgentsView: () =>
Agents
})); +vi.mock("../../components/ResearchView", () => ({ ResearchView: () =>
Research
})); +vi.mock("../../components/EvalsView", () => ({ EvalsView: () =>
Evals
})); +vi.mock("../../components/TodoView", () => ({ TodoView: () =>
Todo
})); +vi.mock("../../components/QuickChatFAB", () => ({ QuickChatFAB: () => null })); +vi.mock("../../components/ScriptsModal", () => ({ ScriptsModal: () => null })); +vi.mock("../../components/TerminalModal", () => ({ TerminalModal: () => null })); +vi.mock("../../components/FileBrowser", () => ({ FileBrowserModal: () => null })); +vi.mock("../../components/ActivityLogModal", () => ({ ActivityLogModal: () => null })); +vi.mock("../../components/GitManagerModal", () => ({ GitManagerModal: () => null })); +vi.mock("../../components/SchedulesModal", () => ({ SchedulesModal: () => null })); +vi.mock("../../components/WorkflowEditorModal", () => ({ WorkflowEditorModal: () => null })); +vi.mock("../../components/AgentsModal", () => ({ AgentsModal: () => null })); +vi.mock("../../components/SubtaskBreakdownModal", () => ({ SubtaskBreakdownModal: () => null })); +vi.mock("../../components/UsageModal", () => ({ UsageModal: () => null })); +vi.mock("../../components/ModelOnboardingModal", () => ({ ModelOnboardingModal: () => null })); +vi.mock("../../components/SetupWizardModal", () => ({ SetupWizardModal: () => null })); +vi.mock("../../components/GroupTaskModal", () => ({ GroupTaskModal: () => null })); +vi.mock("../../components/ProjectSelector", () => ({ ProjectSelector: () =>
})); +vi.mock("../../components/ProjectCard", () => ({ ProjectCard: () =>
})); +vi.mock("../../components/Sidebar", () => ({ Sidebar: () =>
})); +vi.mock("../../components/Header", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + Header: () =>
, + }; +}); +vi.mock("../../components/MobileNavBar", () => ({ MobileNavBar: () => null })); +vi.mock("../../components/RightDock", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + RightDock: () => null, + RightDockExpandModal: () => null, + }; +}); + +const mockUseProjects = vi.fn(() => ({ projects: [], loading: false, error: null })); +const mockCurrentProjectState = { + currentProject: { + id: "proj-1", + name: "Test Project", + path: "/test", + status: "active", + isolationMode: "in-process", + createdAt: "", + updatedAt: "", + } as ProjectInfo, + loading: false, + setCurrentProject: vi.fn(), + clearCurrentProject: vi.fn(), +}; +vi.mock("../../hooks/useProjects", () => ({ useProjects: () => mockUseProjects() })); +vi.mock("../../hooks/useCurrentProject", () => ({ + useCurrentProject: () => mockCurrentProjectState, +})); +vi.mock("../../hooks/useNodes", () => ({ + useNodes: vi.fn(() => ({ + nodes: [], loading: false, error: null, + refresh: vi.fn(), register: vi.fn(), update: vi.fn(), unregister: vi.fn(), healthCheck: vi.fn(), + })), +})); + +const mockUseViewportMode = vi.fn(() => "desktop"); +vi.mock("../../hooks/useViewportMode", () => ({ + MOBILE_MEDIA_QUERY: "(max-width: 768px), (max-height: 480px)", + getViewportMode: () => mockUseViewportMode(), + isMobileViewport: () => mockUseViewportMode() === "mobile", + useViewportMode: (..._args: unknown[]) => mockUseViewportMode(..._args), +})); + +const mockUseMobileKeyboard = vi.fn(() => ({ + keyboardOverlap: 0, viewportHeight: null, viewportOffsetTop: 0, keyboardOpen: false, +})); +vi.mock("../../hooks/useMobileKeyboard", () => ({ + useMobileKeyboard: (..._args: unknown[]) => mockUseMobileKeyboard(..._args), +})); + +import { App } from "../../App"; + +function makeBoardTask(id: string, title: string): Task { + return { + id, + title, + description: "Test task description", + column: "todo", + status: "todo", + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + } as Task; +} + +async function renderAppAndWait(expectedTestId: string = "board-view") { + const result = render(); + await waitFor(() => { + expect(screen.getByTestId(expectedTestId)).toBeTruthy(); + }); + return result; +} + +describe("Board main-panel task-detail — mobile transition class gating (MainContent.tsx)", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockSubscribeSse.mockReset(); + mockSubscribeSse.mockReturnValue(vi.fn()); + mockUseTasks.mockReset(); + }); + + it("applies the mobile transition class to the board main-panel surface when the viewport is mobile", async () => { + mockUseViewportMode.mockReturnValue("mobile"); + const task = makeBoardTask("FN-1", "Board Detail"); + mockUseTasks.mockImplementation(() => ({ + tasks: [task], + createTask: mockCreateTask, + moveTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + refreshTasks: vi.fn(), + })); + + await renderAppAndWait("board-view"); + fireEvent.click(screen.getByTestId("open-task-FN-1")); + + await waitFor(() => { + expect(screen.getByTestId("task-detail-main-panel-content")).toBeInTheDocument(); + }); + expect(document.querySelector(".task-detail-main-panel--mobile-transition")).toBeInTheDocument(); + }); + + it("does NOT apply the mobile transition class to the board main-panel surface on desktop", async () => { + mockUseViewportMode.mockReturnValue("desktop"); + const task = makeBoardTask("FN-1", "Board Detail"); + mockUseTasks.mockImplementation(() => ({ + tasks: [task], + createTask: mockCreateTask, + moveTask: vi.fn(), + deleteTask: vi.fn(), + mergeTask: vi.fn(), + retryTask: vi.fn(), + updateTask: vi.fn(), + duplicateTask: vi.fn(), + archiveTask: vi.fn(), + unarchiveTask: vi.fn(), + archiveAllDone: vi.fn(), + refreshTasks: vi.fn(), + })); + + await renderAppAndWait("board-view"); + fireEvent.click(screen.getByTestId("open-task-FN-1")); + + await waitFor(() => { + expect(screen.getByTestId("task-detail-main-panel-content")).toBeInTheDocument(); + }); + expect(document.querySelector(".task-detail-main-panel")).toBeInTheDocument(); + expect(document.querySelector(".task-detail-main-panel--mobile-transition")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.test.tsx new file mode 100644 index 0000000000..f79052fdf1 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.test.tsx @@ -0,0 +1,156 @@ +/** + * Focused regression coverage for the FN-7587 mobile task-detail predictive-back + * slide/fade transition polish. + * + * FNXC:TaskDetailSwipeBack 2026-07-05-12:45: + * This suite asserts ONLY the presentation-layer invariant this task adds: + * - the mobile transition class is applied to the modal/list/nested surface when the + * viewport is mobile; + * - the class is absent on desktop, and does not linger after re-rendering desktop-width; + * - the CSS neutralizes the animation under `prefers-reduced-motion: reduce` + * (jsdom cannot execute CSS keyframe animations, so this is asserted statically + * against the stylesheet source, mirroring the project's existing + * `TaskDetailModal.css.test.ts` / `TaskDetailModal.github-tracking-enable.css.test.ts` + * pattern of asserting CSS text rather than computed animation state). + * + * Board main-panel gating (MainContent.tsx) is covered separately in + * `TaskDetail.mobile-transition.board-panel.test.tsx` because that surface requires the + * full App-level mock harness (Board/ListView/TaskDetailModal module mocks + real + * lucide-react icons via Header), which conflicts with this file's TaskDetailModal-focused + * `test-helpers` harness (fixed lucide-react icon allowlist) if combined in one module. + * + * This suite deliberately does NOT re-derive dismissal-routing coverage — that remains the + * sole responsibility of `TaskDetail.swipe-back.test.tsx` and `navigation-history.test.tsx`, + * which this task runs unmodified (see PROMPT.md Step 0/3) to prove the animation layer does + * not perturb the `useNavigationHistory` / `popstate` / `fusion:native-back` invariant. + */ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { + makeTask, + noop, + noopMove, + noopDelete, + noopMerge, + noopOpenDetail, + setupTaskDetailModalHooks, +} from "./TaskDetailModal.test-helpers"; +import { TaskDetailModal } from "../TaskDetailModal"; + +const MOBILE_WIDTH = 375; +const DESKTOP_WIDTH = 1024; + +function setViewportWidth(width: number): void { + Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: width }); + window.dispatchEvent(new Event("resize")); +} + +describe("Task-detail mobile predictive-back transition — CSS invariants", () => { + it("styles.css neutralizes the board main-panel transition under prefers-reduced-motion", () => { + const css = readFileSync(resolve(__dirname, "../../styles.css"), "utf8"); + expect(css).toContain(".task-detail-main-panel--mobile-transition"); + expect(css).toContain("@keyframes task-detail-mobile-slide-fade-in"); + const reducedMotionBlock = css.slice(css.indexOf("@media (prefers-reduced-motion: reduce) {\n .task-detail-main-panel--mobile-transition")); + expect(reducedMotionBlock.slice(0, 200)).toContain("animation: none;"); + }); + + it("TaskDetailModal.css neutralizes the modal/list/nested transition under prefers-reduced-motion", () => { + const css = readFileSync(resolve(__dirname, "../TaskDetailModal.css"), "utf8"); + expect(css).toContain(".task-detail-modal--mobile-transition"); + expect(css).toContain("@keyframes task-detail-modal-mobile-slide-fade-in"); + const reducedMotionBlock = css.slice(css.indexOf("@media (prefers-reduced-motion: reduce) {\n .task-detail-modal--mobile-transition")); + expect(reducedMotionBlock.slice(0, 200)).toContain("animation: none;"); + }); +}); + +describe("TaskDetailModal wrapper — mobile transition class gating (modal/list/nested surface)", () => { + setupTaskDetailModalHooks(); + + beforeEach(() => { + setViewportWidth(MOBILE_WIDTH); + }); + + afterEach(() => { + setViewportWidth(DESKTOP_WIDTH); + }); + + it("applies the mobile transition class to the modal surface when the viewport is mobile", async () => { + setViewportWidth(MOBILE_WIDTH); + + render( + , + ); + + await waitFor(() => { + expect(document.querySelector(".task-detail-modal--mobile-transition")).toBeInTheDocument(); + }); + }); + + it("does NOT apply the mobile transition class on desktop", async () => { + setViewportWidth(DESKTOP_WIDTH); + + render( + , + ); + + await waitFor(() => { + expect(document.querySelector(".task-detail-modal")).toBeInTheDocument(); + }); + expect(document.querySelector(".task-detail-modal--mobile-transition")).not.toBeInTheDocument(); + }); + + it("does not leave a lingering transition class after re-rendering at desktop width", async () => { + setViewportWidth(MOBILE_WIDTH); + + const { rerender } = render( + , + ); + + await waitFor(() => { + expect(document.querySelector(".task-detail-modal--mobile-transition")).toBeInTheDocument(); + }); + + setViewportWidth(DESKTOP_WIDTH); + rerender( + , + ); + + await waitFor(() => { + expect(document.querySelector(".task-detail-modal--mobile-transition")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/dashboard/app/components/dashboard/MainContent.tsx b/packages/dashboard/app/components/dashboard/MainContent.tsx index 403408d45f..428ba00ac4 100644 --- a/packages/dashboard/app/components/dashboard/MainContent.tsx +++ b/packages/dashboard/app/components/dashboard/MainContent.tsx @@ -751,7 +751,15 @@ export function MainContent({ } return ( -
+ {/* + FNXC:TaskDetailSwipeBack 2026-07-05-12:30: + FN-7587 — presentation-only predictive-back polish layered on top of the unchanged + FN-7583/FN-7586 dismissal routing (popstate / fusion:native-back / useNavigationHistory + stack). The `--mobile-transition` modifier only adds a CSS enter animation gated to the + existing `isMobile` prop; it never defers or reorders when onRequestClose/onBackToBoard + fire, and honors prefers-reduced-motion (see styles.css). + */} +
Date: Sun, 5 Jul 2026 13:11:15 -0700 Subject: [PATCH 05/24] FN-7592: replace overseer badge text label with a colored Eye icon Swaps the planner-overseer status badge from an uppercase text pill to a compact icon glyph, keeping accessibility text on aria-label/title. - Render a small lucide-react Eye icon instead of the state-label text inside the overseer badge - Keep the readable state name on aria-label and the composed tooltip on title for accessibility - Add per-state coloring (watching/steering/recovering/awaiting-confirmation) keyed off the data-planner-overseer-state attribute in TaskCard.css, sized tightly around the icon - Update TaskCard tests to assert the icon renders and the accessible name moved to aria-label instead of textContent Files changed: packages/dashboard/app/components/TaskCard.css | 40 ++++++++++++++++++++++ packages/dashboard/app/components/TaskCard.tsx | 14 ++++++-- .../app/components/__tests__/TaskCard.test.tsx | 25 +++++++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-7592 Fusion-Task-Lineage: d7ca93a6-9236-4fa0-a829-80f5b99dbd5f Co-authored-by: Fusion (runfusion.ai) --- .../dashboard/app/components/TaskCard.css | 40 +++++++++++++++++++ .../dashboard/app/components/TaskCard.tsx | 14 ++++++- .../components/__tests__/TaskCard.test.tsx | 25 +++++++++--- 3 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index 1443982d39..e2d664f432 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -397,6 +397,46 @@ not a restyle of the ordinary manual-approval badge. border-color: color-mix(in srgb, var(--color-warning) 45%, transparent); } +/* +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7592 replaces the overseer badge's uppercase text label with a small `Eye` icon so it +reads as a compact glyph rather than a wide pill. Size the badge to the icon (no min-width, +tight padding) and color it per `PlannerOverseerState` via the `data-planner-overseer-state` +attribute so operators can distinguish watching/steering/recovering/awaiting-confirmation at +a glance without reading text. Colors reuse existing semantic tokens: neutral/info for the +passive "watching" state, warning for the more active "steering"/"recovering" states, and the +triage token for "awaiting-confirmation" (a human-decision hold), matching the hue conventions +used elsewhere in this file (e.g. .card-oversight-badge--*, .card-status-badge--triage). +*/ +.card-planner-overseer-state { + padding: calc(var(--space-xs) / 2); + line-height: 0; +} + +.card-planner-overseer-state svg { + width: 12px; + height: 12px; +} + +.card-planner-overseer-state[data-planner-overseer-state="watching"] { + background: color-mix(in srgb, var(--color-info) 15%, transparent); + color: var(--color-info); + border-color: color-mix(in srgb, var(--color-info) 40%, transparent); +} + +.card-planner-overseer-state[data-planner-overseer-state="steering"], +.card-planner-overseer-state[data-planner-overseer-state="recovering"] { + background: color-mix(in srgb, var(--color-warning) 18%, transparent); + color: var(--color-warning); + border-color: color-mix(in srgb, var(--color-warning) 45%, transparent); +} + +.card-planner-overseer-state[data-planner-overseer-state="awaiting-confirmation"] { + background: color-mix(in srgb, var(--triage) 18%, transparent); + color: var(--triage); + border-color: color-mix(in srgb, var(--triage) 45%, transparent); +} + .card.awaiting-input { border-left: 3px solid var(--color-warning); background: color-mix(in srgb, var(--color-warning) 6%, transparent); diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 2bcb016f3d..e312111e4e 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { memo, useCallback, useState, useRef, useEffect, useLayoutEffect, useMemo, type CSSProperties, type ReactElement } from "react"; import { createPortal } from "react-dom"; -import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle, ArrowUpRight } from "lucide-react"; +import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, RotateCw, Zap, GitBranch, GitPullRequest, AlertTriangle, ArrowUpRight, Eye } from "lucide-react"; import type { Task, TaskDetail, Column, ColumnId, PrInfo, IssueInfo, TaskPriority, GithubIssueAction, MergeResult, PlannerOversightLevel } from "@fusion/core"; import { DEFAULT_PLANNER_OVERSIGHT_LEVEL, @@ -2889,15 +2889,25 @@ function TaskCardComponent({ board payload) plus a repaint-correct memo comparator; FN-7516 owns the styled badge/design and surface-by-surface rendering. This is a minimal, type-safe, guarded read only — nothing renders for an absent field or the "idle" state. + + FNXC:PlannerOversight 2026-07-05-00:00: + FN-7592 replaces the uppercase text label with a small state-colored `Eye` icon so + the badge reads as a compact glyph. The readable label and composed tooltip stay + available for accessibility: `aria-label` carries the state name (screen readers) + and `title` keeps the existing tooltip (hover). Per-state color comes from the + `data-planner-overseer-state` attribute in TaskCard.css — do not fork the label + logic here; `plannerOverseerStateLabel`/`plannerOverseerBadgeTooltip` remain the + single source of truth. */} {task.plannerOverseerState && task.plannerOverseerState.state !== "idle" && ( - {plannerOverseerStateLabel(task.plannerOverseerState.state, t)} + )} {showStalledReview && stalledReview && ( diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 0f48411f2d..9cd637d2ab 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -26,7 +26,9 @@ vi.mock("lucide-react", () => ({ Zap: () => , AlertTriangle: () => null, ArrowUpRight: () => null, - Eye: () => null, + // FN-7592: the overseer badge now renders an icon child instead of a text label, + // so tests must see a real SVG (like Zap) rather than a no-op render. + Eye: () => , })); vi.mock("../ProviderIcon", () => ({ @@ -317,9 +319,13 @@ describe("TaskCard", () => { render(); + // FN-7592: the badge is now an icon-only glyph. The readable label moved from + // textContent to aria-label; the composed tooltip is unchanged on title. const badge = screen.getByTestId("planner-overseer-state-badge"); - expect(badge.textContent).not.toBe("awaiting-confirmation"); - expect(badge.textContent).toBe("Awaiting confirmation"); + expect(badge.querySelector("svg")).toBeInTheDocument(); + expect(badge.getAttribute("aria-label")).not.toBe("awaiting-confirmation"); + expect(badge.getAttribute("aria-label")).toBe("Awaiting confirmation"); + expect(badge.getAttribute("data-planner-overseer-state")).toBe("awaiting-confirmation"); const title = badge.getAttribute("title") ?? ""; expect(title).not.toBe("Planner overseer: awaiting-confirmation"); @@ -343,8 +349,12 @@ describe("TaskCard", () => { }, }); const { unmount } = render(); + // FN-7592: icon-only badge — assert the accessible name via aria-label and the + // per-state color hook via data-planner-overseer-state, not raw text content. let badge = screen.getByTestId("planner-overseer-state-badge"); - expect(badge.textContent).toBe("Overseer watching"); + expect(badge.querySelector("svg")).toBeInTheDocument(); + expect(badge.getAttribute("aria-label")).toBe("Overseer watching"); + expect(badge.getAttribute("data-planner-overseer-state")).toBe("watching"); expect(badge.getAttribute("title")).not.toMatch(/undefined/); unmount(); @@ -361,7 +371,12 @@ describe("TaskCard", () => { }); render(); badge = screen.getByTestId("planner-overseer-state-badge"); - expect(badge.textContent).toBe("Overseer recovering"); + expect(badge.querySelector("svg")).toBeInTheDocument(); + expect(badge.getAttribute("aria-label")).toBe("Overseer recovering"); + // Distinct states expose distinct data-planner-overseer-state values, which is the + // hook TaskCard.css keys per-state color off of (jsdom cannot compute color-mix()). + expect(badge.getAttribute("data-planner-overseer-state")).toBe("recovering"); + expect(badge.getAttribute("data-planner-overseer-state")).not.toBe("watching"); const title = badge.getAttribute("title") ?? ""; expect(title).not.toMatch(/undefined/); expect(title.length).toBeGreaterThan(0); From cf3fe8b485f964bea9cf014d04c5a521386e5c5a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 13:15:56 -0700 Subject: [PATCH 06/24] FN-7591: make dashboard create surfaces resolve intake column from workflow instead of hard-coding triage Fixes dashboard task creation so new cards land in the selected/default workflow's intake column instead of always forcing legacy triage, letting workflows like Coding (Ideas) park new cards in 'ideas' until an operator promotes them. - InlineCreateCard, QuickEntryBox, and NewTaskModal no longer hard-code column:"triage"; InlineCreateCard now forwards workflowId at create time instead of applying it post-create. - Fixed a glue-layer regression in useTaskHandlers.ts (handleBoardQuickCreate/handleModalCreate) that re-forced column:"triage" even after UI surfaces stopped sending it. - Added/updated tests covering the store's intake-column resolution and the dashboard create surfaces/hooks. - Documented the new manual-intake-column parking behavior in dashboard-guide.md and workflow-steps.md. - Added a patch changeset for @runfusion/fusion. Files changed: .changeset/fn-7591-coding-ideas-intake.md | 7 +++ docs/dashboard-guide.md | 4 ++ docs/workflow-steps.md | 1 + packages/core/src/__tests__/store-create-intake-column.test.ts | 20 ++++++++ packages/dashboard/app/App.tsx | 5 +- packages/dashboard/app/components/InlineCreateCard.tsx | 28 ++++------ packages/dashboard/app/components/NewTaskModal.tsx | 5 +- packages/dashboard/app/components/QuickEntryBox.tsx | 5 +- packages/dashboard/app/components/TodoView.tsx | 7 ++- packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx | 59 +++++++++++++++++++++- packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx | 14 +++-- packages/dashboard/app/components/__tests__/TodoView.test.tsx | 10 ++-- packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx | 45 ++++++++++++++++- packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts | 27 ++++++++-- packages/dashboard/app/hooks/useTaskHandlers.ts | 8 ++- 15 files changed, 207 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-7591 Fusion-Task-Lineage: 510f0e6a-89e7-468f-a6df-ad6aebd5c33a Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7591-coding-ideas-intake.md | 7 +++ docs/dashboard-guide.md | 4 ++ docs/workflow-steps.md | 1 + .../store-create-intake-column.test.ts | 20 +++++++ packages/dashboard/app/App.tsx | 5 +- .../app/components/InlineCreateCard.tsx | 28 ++++----- .../dashboard/app/components/NewTaskModal.tsx | 5 +- .../app/components/QuickEntryBox.tsx | 5 +- .../dashboard/app/components/TodoView.tsx | 7 ++- .../__tests__/InlineCreateCard.test.tsx | 59 ++++++++++++++++++- .../__tests__/QuickEntryBox.test.tsx | 14 +++-- .../components/__tests__/TodoView.test.tsx | 10 ++-- ...ckcreate-workflow-lane-visibility.test.tsx | 45 +++++++++++++- .../hooks/__tests__/useTaskHandlers.test.ts | 27 +++++++-- .../dashboard/app/hooks/useTaskHandlers.ts | 8 ++- 15 files changed, 207 insertions(+), 38 deletions(-) create mode 100644 .changeset/fn-7591-coding-ideas-intake.md diff --git a/.changeset/fn-7591-coding-ideas-intake.md b/.changeset/fn-7591-coding-ideas-intake.md new file mode 100644 index 0000000000..6db08f8a77 --- /dev/null +++ b/.changeset/fn-7591-coding-ideas-intake.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: New tasks created under the Coding (Ideas) workflow now land in the Ideas column and wait for you to promote them. +category: fix +dev: Dashboard create surfaces (InlineCreateCard, QuickEntryBox, NewTaskModal, insight/todo → task) no longer hard-code column:"triage"; the store now resolves the selected/default workflow's intake column. InlineCreateCard forwards workflowId at create time instead of applying it post-create. Also fixed a glue-layer regression in `useTaskHandlers.ts` (`handleBoardQuickCreate`/`handleModalCreate`) that re-forced column:"triage" even after the UI surfaces stopped sending it. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 8992295765..40cc1440e8 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -432,6 +432,10 @@ When quick-create task creation, Planning Mode, or Subtask Breakdown runs from a The **New Task** dialog's workflow selector also defaults to the current or last selected Board/List workflow lane for the current project. If no valid lane has been selected, or the remembered lane was deleted, the selector falls back to the project default workflow and task creation omits an explicit `workflowId`. + + +Create requests never send an explicit `column`. The task store resolves the landing column from the (selected or project-default) workflow's intake column, so most tasks still land in `triage` under the default Coding workflow, byte-identical to before. A workflow with a **manual intake column** — for example the built-in **Coding (Ideas)** workflow's `ideas` column (`autoTriage: false`) — parks new cards there instead: they wait for you to promote them into `todo` and are not auto-planned by the triage service until you do. + Optional workflow steps declared by the active workflow are available from the quick-add action row and the **New Task** dialog's inline quick buttons. For example, the coding workflow's browser verification option appears as a quick drop-down when that workflow is active; each option is seeded from the workflow step's `defaultOn` setting and is sent with the task's `enabledWorkflowSteps` payload at creation time. diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index 9e453b6d98..48db90a417 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -82,6 +82,7 @@ Use this inventory as the documentation map for current workflow behavior: | Routing boundary | Agents may select/change a workflow only for explicit user requests or tasks they created; no-commit markers do not imply Quick fix or any other workflow. | This page, [Selecting workflows](#selecting-workflows); [Agents](./agents.md#interactive-cli-chat). | | Dashboard board/list/graph selection | Board/List/Header/Graph share durable per-project workflow selection; stale saved ids fall back to a valid workflow. Board adds a dashboard-only **All workflows** aggregate and task workflow-name badges; Graph uses **All workflows** for the full active graph. | [Dashboard Guide → Board View](./dashboard-guide.md#board-view), [Graph View](./dashboard-guide.md#graph-view), and [Workflow Selection and Editor](./dashboard-guide.md#workflow-selection-and-editor). | | Create/planning forwarding | Quick-create task creation, Planning Mode, Subtask Breakdown, and the New Task dialog forward the active real workflow id when creating tasks; **All workflows** quick-create chooses a real workflow intake/default column instead of saving a synthetic aggregate id. | [Dashboard Guide → Planning Mode](./dashboard-guide.md#planning-mode). | +| Manual-intake column parking | Dashboard create surfaces never send an explicit `column`; the store resolves the landing column from the (selected or project-default) workflow's intake column. A workflow whose intake column sets `autoTriage: false` (e.g. built-in Coding (Ideas)'s `ideas` column) parks new cards there instead of auto-planning them, until an operator promotes the card. | [Dashboard Guide → Create/Planning Forwarding](./dashboard-guide.md#planning-mode). | ### Skill-backed workflow steps diff --git a/packages/core/src/__tests__/store-create-intake-column.test.ts b/packages/core/src/__tests__/store-create-intake-column.test.ts index 02b9a14452..f2c94ea21a 100644 --- a/packages/core/src/__tests__/store-create-intake-column.test.ts +++ b/packages/core/src/__tests__/store-create-intake-column.test.ts @@ -36,6 +36,26 @@ describe("createTask intake-column wiring (Coding (Ideas))", () => { expect(task.column).toBe("ideas"); }); + it("lands a task explicitly selecting builtin:coding in triage even when the project default is coding-ideas", async () => { + const store = harness.store(); + await store.setDefaultWorkflowId("builtin:coding-ideas"); + const task = await store.createTask({ + description: "explicit default coding workflow task", + workflowId: "builtin:coding", + }); + expect(task.column).toBe("triage"); + }); + + it("does not throw and falls back to triage when workflowId is explicitly null (\"No workflow\")", async () => { + const store = harness.store(); + await store.setDefaultWorkflowId("builtin:coding-ideas"); + const task = await store.createTask({ + description: "explicit no-workflow task", + workflowId: null, + }); + expect(task.column).toBe("triage"); + }); + it("writes a bootstrap PROMPT.md for an ideas-column task (unplanned)", async () => { const store = harness.store(); const task: Task = await store.createTask({ diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index 03532054fe..8264e175c1 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -792,10 +792,13 @@ function AppInner() { const handleInsightTaskCreate = useCallback( async ({ insightId, title, description }: { insightId: string; title: string; description: string }) => { + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + Do not hard-code `column: "triage"` — this surface has no workflow picker, so it inherits the project-default workflow, and the store resolves the landing column from that workflow's intake column (e.g. Coding (Ideas) → "ideas") instead of forcing triage. + */ await createTask({ title, description, - column: "triage", source: { sourceType: "dashboard_ui", sourceMetadata: { diff --git a/packages/dashboard/app/components/InlineCreateCard.tsx b/packages/dashboard/app/components/InlineCreateCard.tsx index ebf302edba..134559d580 100644 --- a/packages/dashboard/app/components/InlineCreateCard.tsx +++ b/packages/dashboard/app/components/InlineCreateCard.tsx @@ -6,7 +6,7 @@ import { Brain, Link, ListTree, Zap, ChevronDown, ChevronUp, Bot, Maximize2, Min import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings, type ResolvedWorkflowOptionalStep } from "@fusion/core"; import { getErrorMessage } from "@fusion/core"; import type { ToastType } from "../hooks/useToast"; -import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, selectTaskWorkflow, fetchWorkflowOptionalSteps, DuplicateCandidatesError } from "../api"; +import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, fetchWorkflowOptionalSteps, DuplicateCandidatesError } from "../api"; import type { CreateTaskInput, ModelInfo, Agent, NodeInfo, DuplicateMatch } from "../api"; import { useNodes } from "../hooks/useNodes"; import { ModelSelectionModal } from "./ModelSelectionModal"; @@ -394,24 +394,15 @@ export function InlineCreateCard({ }); }, []); + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + submitTask no longer applies the selected workflow post-create via selectTaskWorkflow — handleSubmit now forwards workflowId inside the CreateTaskInput so the store materializes the workflow and resolves the intake column (e.g. Coding (Ideas) → "ideas") atomically at create time. A post-create selectTaskWorkflow call would race the store's intake-column resolution and re-introduce the auto-triage bug this task fixes. + */ const submitTask = useCallback(async (input: CreateTaskInput) => { setSubmitting(true); try { const task = await onSubmit(input); - // Apply custom workflow if selected (non-blocking — task already exists) - if (selectedWorkflowId) { - try { - await selectTaskWorkflow(task.id, selectedWorkflowId, projectId); - } catch (err) { - if (addToast) { - addToast(getErrorMessage(err) || "Failed to apply workflow", "error"); - } else { - console.warn("Failed to apply workflow:", getErrorMessage(err)); - } - } - } - // Upload pending images as attachments if (pendingImages.length > 0) { const failures: string[] = []; @@ -478,15 +469,18 @@ export function InlineCreateCard({ onSubmit, addToast, projectId, - selectedWorkflowId, ]); const handleSubmit = useCallback(async () => { if (!description.trim() || submitting) return; + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + Do not hard-code `column: "triage"` here — the store resolves the landing column from the forwarded (or project-default) workflow's intake column, e.g. Coding (Ideas) → "ideas". Forwarding `workflowId` at create time (instead of applying it post-create via selectTaskWorkflow) lets the store materialize the workflow and land the card in its resolved intake column atomically, so a manual-intake workflow parks the card for the operator instead of being auto-triaged. + */ const input: CreateTaskInput = { description: description.trim(), - column: "triage", + ...(selectedWorkflowId ? { workflowId: selectedWorkflowId } : {}), dependencies: dependencies.length ? dependencies : undefined, ...(selectedAgentId ? { assignedAgentId: selectedAgentId } : {}), modelPresetId: selectedPresetId, @@ -517,7 +511,7 @@ export function InlineCreateCard({ } await submitTask(input); - }, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, optionalSteps.length, enabledOptionalStepIds, priority, effectiveNodeId, projectId, addToast, submitTask]); + }, [description, submitting, selectedWorkflowId, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, optionalSteps.length, enabledOptionalStepIds, priority, effectiveNodeId, projectId, addToast, submitTask]); const handleDuplicateProceed = useCallback(async () => { const matches = duplicateMatches; diff --git a/packages/dashboard/app/components/NewTaskModal.tsx b/packages/dashboard/app/components/NewTaskModal.tsx index ab0251100f..2cca65baf3 100644 --- a/packages/dashboard/app/components/NewTaskModal.tsx +++ b/packages/dashboard/app/components/NewTaskModal.tsx @@ -791,10 +791,13 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, const validatorSlashIdx = validatorModel.indexOf("/"); const planningSlashIdx = planningModel.indexOf("/"); + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + Do not hard-code `column: "triage"` — the store resolves the landing column from the (materialized) workflowId below, so a manual-intake workflow (e.g. Coding (Ideas) → "ideas") parks the card for the operator instead of being auto-triaged. + */ const createInput: NewTaskCreateInput = { title: undefined, description: trimmedDesc, - column: "triage", dependencies: dependencies.length ? dependencies : undefined, // U6/R3: forward the workflow selection only when the user changed it. // - undefined → omit (store inherits the project default, today's behavior) diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index ad8745a4e5..87ca77fb7d 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -715,9 +715,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, const originalDescription = description; setDescription(""); try { + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + Do not hard-code `column: "triage"` — the store resolves the landing column from the forwarded (or project-default) workflow's intake column, so a manual-intake workflow (e.g. Coding (Ideas) → "ideas") parks the card for the operator instead of being auto-triaged. + */ const createdTask = await onCreate({ description: trimmed, - column: "triage", ...(selectedWorkflowForCreate !== undefined ? { workflowId: selectedWorkflowForCreate } : {}), dependencies: dependencies.length ? dependencies : undefined, ...(selectedAgentId ? { assignedAgentId: selectedAgentId } : {}), diff --git a/packages/dashboard/app/components/TodoView.tsx b/packages/dashboard/app/components/TodoView.tsx index 916962ee7c..c76e777438 100644 --- a/packages/dashboard/app/components/TodoView.tsx +++ b/packages/dashboard/app/components/TodoView.tsx @@ -299,9 +299,12 @@ export function TodoView({ const handleCreateTaskFromItem = useCallback(async (item: TodoItem) => { try { + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + Do not hard-code `column: "triage"` — this surface has no workflow picker, so it inherits the project-default workflow, and the store resolves the landing column from that workflow's intake column (e.g. Coding (Ideas) → "ideas") instead of forcing triage. + */ const input: TaskCreateInput = { description: item.text, - column: "triage", source: { sourceType: "dashboard_ui" }, }; const task: Task = await createTask(input, projectId); @@ -314,9 +317,9 @@ export function TodoView({ const handleCreateTaskAndAssign = useCallback(async (item: TodoItem, agentId: string) => { try { + // FNXC:CodingIdeasWorkflow 2026-07-05-00:00: same rationale as handleCreateTaskFromItem above — omit column so the project-default workflow's intake column resolves it. const input: TaskCreateInput = { description: item.text, - column: "triage", assignedAgentId: agentId, source: { sourceType: "dashboard_ui" }, }; diff --git a/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx b/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx index f08f49d86a..c4c5d35c34 100644 --- a/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import { InlineCreateCard } from "../InlineCreateCard"; import type { Task, Column } from "@fusion/core"; -import { fetchModels, fetchSettings, fetchAgents, checkDuplicateTasks, fetchWorkflows, fetchWorkflowOptionalSteps } from "../../api"; +import { fetchModels, fetchSettings, fetchAgents, checkDuplicateTasks, fetchWorkflows, fetchWorkflowOptionalSteps, selectTaskWorkflow } from "../../api"; import { useNodes } from "../../hooks/useNodes"; import type { ModelInfo } from "../../api"; import { scopedKey } from "../../utils/projectStorage"; @@ -1773,4 +1773,61 @@ describe("InlineCreateCard node override", () => { }); }); +/* +FN-7591: InlineCreateCard must forward a selected workflow inside the create-time CreateTaskInput +(so the store materializes it and resolves the workflow's intake column atomically) instead of hard-coding +column:"triage" and applying the workflow post-create via selectTaskWorkflow. +*/ +describe("InlineCreateCard workflow selection at create time (FN-7591)", () => { + beforeEach(() => { + vi.mocked(fetchWorkflows).mockResolvedValue([ + { id: "wf-a", name: "Workflow A" }, + { id: "builtin:coding-ideas", name: "Coding (Ideas)" }, + ]); + }); + + it("submits workflowId in the create input and omits column:triage when a workflow is selected", async () => { + const { props } = renderCard(); + expandCard(); + + fireEvent.change(screen.getByPlaceholderText("What needs to be done?"), { target: { value: "Idea for later" } }); + const select = await screen.findByLabelText("Workflow") as HTMLSelectElement; + fireEvent.change(select, { target: { value: "builtin:coding-ideas" } }); + + fireEvent.click(screen.getByTestId("save-button")); + + await waitFor(() => expect(props.onSubmit).toHaveBeenCalled()); + const submitted = vi.mocked(props.onSubmit).mock.calls[0][0]; + expect(submitted.workflowId).toBe("builtin:coding-ideas"); + expect(submitted.column).toBeUndefined(); + }); + + it("omits workflowId when no workflow is explicitly selected (inherits project default)", async () => { + const { props } = renderCard(); + expandCard(); + + fireEvent.change(screen.getByPlaceholderText("What needs to be done?"), { target: { value: "Plain task" } }); + fireEvent.click(screen.getByTestId("save-button")); + + await waitFor(() => expect(props.onSubmit).toHaveBeenCalled()); + const submitted = vi.mocked(props.onSubmit).mock.calls[0][0]; + expect(submitted.workflowId).toBeUndefined(); + expect(submitted.column).toBeUndefined(); + }); + + it("does not call the redundant post-create selectTaskWorkflow for the create path", async () => { + const { props } = renderCard(); + expandCard(); + + fireEvent.change(screen.getByPlaceholderText("What needs to be done?"), { target: { value: "Idea for later" } }); + const select = await screen.findByLabelText("Workflow") as HTMLSelectElement; + fireEvent.change(select, { target: { value: "builtin:coding-ideas" } }); + + fireEvent.click(screen.getByTestId("save-button")); + + await waitFor(() => expect(props.onSubmit).toHaveBeenCalled()); + expect(selectTaskWorkflow).not.toHaveBeenCalled(); + }); +}); + }); diff --git a/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx b/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx index cd5b40d954..d7f30b1f1a 100644 --- a/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx +++ b/packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx @@ -1373,7 +1373,9 @@ describe("QuickEntryBox", () => { }); }); - it("creates task on Enter key with TaskCreateInput", async () => { + // FN-7591: QuickEntryBox must not force column:"triage" — the store resolves the landing column from + // the (selected or default) workflow's intake column, so a manual-intake workflow parks the card instead. + it("creates task on Enter key with TaskCreateInput and without forcing column:triage", async () => { const { props } = renderQuickEntryBox({}); const textarea = screen.getByTestId("quick-entry-input"); @@ -1384,9 +1386,10 @@ describe("QuickEntryBox", () => { expect(props.onCreate).toHaveBeenCalledWith( expect.objectContaining({ description: "New task description", - column: "triage", }), ); + const submitted = vi.mocked(props.onCreate).mock.calls[0][0]; + expect(submitted.column).toBeUndefined(); }); }); @@ -1639,6 +1642,8 @@ describe("QuickEntryBox", () => { fireEvent.change(screen.getByTestId("quick-entry-input"), { target: { value: "Create in selected workflow" } }); clickSave(); await waitFor(() => expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ workflowId: "wf-default" }))); + // FN-7591: forwarding workflowId at create time must not carry a hard-coded column:"triage". + expect(vi.mocked(onCreate).mock.calls[0][0].column).toBeUndefined(); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument(); expect(onPlanningMode).not.toHaveBeenCalled(); @@ -3722,7 +3727,7 @@ describe("QuickEntryBox", () => { expect(localStorage.getItem(QUICK_ENTRY_STORAGE_KEY)).toBeNull(); }); - it("clicking save action creates the task", async () => { + it("clicking save action creates the task without forcing column:triage", async () => { const { props } = renderQuickEntryBox({}); expandQuickEntry(); const textarea = screen.getByTestId("quick-entry-input"); @@ -3734,9 +3739,10 @@ describe("QuickEntryBox", () => { expect(props.onCreate).toHaveBeenCalledWith( expect.objectContaining({ description: "Task to save", - column: "triage", }), ); + const submitted = vi.mocked(props.onCreate).mock.calls[0][0]; + expect(submitted.column).toBeUndefined(); }); }); diff --git a/packages/dashboard/app/components/__tests__/TodoView.test.tsx b/packages/dashboard/app/components/__tests__/TodoView.test.tsx index 2c1f06f4bc..0574ea136f 100644 --- a/packages/dashboard/app/components/__tests__/TodoView.test.tsx +++ b/packages/dashboard/app/components/__tests__/TodoView.test.tsx @@ -446,7 +446,9 @@ describe("TodoView", () => { expect(mockCreateTask).not.toHaveBeenCalled(); }); - it("clicking Create Task button calls createTask with item text", async () => { + // FN-7591: TodoView create handlers must not force column:"triage" — the store resolves the landing + // column from the project-default workflow's intake column instead. + it("clicking Create Task button calls createTask with item text and without forcing column:triage", async () => { const onTaskCreated = vi.fn(); mockCreateTask.mockResolvedValueOnce({ id: "FN-123" }); render(); @@ -455,7 +457,7 @@ describe("TodoView", () => { await waitFor(() => { expect(mockCreateTask).toHaveBeenCalledWith( - { description: "Buy groceries", column: "triage", source: { sourceType: "dashboard_ui" } }, + { description: "Buy groceries", source: { sourceType: "dashboard_ui" } }, "project-1", ); }); @@ -474,7 +476,7 @@ describe("TodoView", () => { expect(screen.getByText("Builder")).toBeInTheDocument(); }); - it("selecting an agent creates task assigned to that agent", async () => { + it("selecting an agent creates task assigned to that agent without forcing column:triage", async () => { const onTaskCreated = vi.fn(); mockCreateTask.mockResolvedValueOnce({ id: "FN-234" }); render(); @@ -486,7 +488,7 @@ describe("TodoView", () => { await waitFor(() => { expect(mockCreateTask).toHaveBeenCalledWith( - { description: "Buy groceries", column: "triage", assignedAgentId: "agent-1", source: { sourceType: "dashboard_ui" } }, + { description: "Buy groceries", assignedAgentId: "agent-1", source: { sourceType: "dashboard_ui" } }, "project-1", ); }); diff --git a/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx b/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx index 1f76f1c3f1..28964c172a 100644 --- a/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx @@ -94,6 +94,22 @@ const CUSTOM_WORKFLOW = { ], }; +/* +FNXC:CodingIdeasWorkflow 2026-07-05-00:00: +A task created under the Coding (Ideas) workflow (manual "ideas" intake, autoTriage:false) must render in the board's +"ideas" lane, not "triage" — mirrors the real builtin:coding-ideas workflow's intake column id/flag shape. +*/ +const CODING_IDEAS_WORKFLOW = { + id: "builtin:coding-ideas", + name: "Coding (Ideas)", + columns: [ + { id: "ideas", name: "Ideas", flags: { intake: true } }, + { id: "todo", name: "Todo", flags: { hold: true } }, + { id: "done", name: "Done", flags: { complete: true } }, + { id: "archived", name: "Archived", flags: { archived: true } }, + ], +}; + function mkTask(overrides: Partial & { id: string }): Task { return { title: overrides.id, @@ -113,7 +129,7 @@ function workflowPayload(taskWorkflowIds: Record, flagEnabled = return { flagEnabled, defaultWorkflowId: DEFAULT_WORKFLOW.id, - workflows: flagEnabled ? [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW] : [], + workflows: flagEnabled ? [DEFAULT_WORKFLOW, CUSTOM_WORKFLOW, CODING_IDEAS_WORKFLOW] : [], taskWorkflowIds, }; } @@ -269,6 +285,33 @@ describe("workflow lane quick-create visibility", () => { expect(screen.getByText(title)).toBeTruthy(); }); + it("Board renders a task created under the Coding (Ideas) workflow in the ideas lane", async () => { + const refetch = deferred(); + fetchBoardWorkflowsMock + .mockResolvedValueOnce(workflowPayload({})) + .mockResolvedValueOnce(workflowPayload({})) + .mockReturnValueOnce(refetch.promise); + + render(); + await screen.findByTestId("workflow-switcher"); + selectWorkflow(CODING_IDEAS_WORKFLOW.id); + + await act(async () => { + fireEvent.click(screen.getByTestId("quick-create-ideas")); + }); + + const ideasColumn = screen.getByTestId("column-ideas"); + expect(within(ideasColumn).getByText("Created builtin:coding-ideas")).toBeTruthy(); + expect(JSON.parse(ideasColumn.getAttribute("data-task-ids") ?? "[]")).toContain("FN-new"); + + await act(async () => { + refetch.resolve(workflowPayload({ "FN-new": CODING_IDEAS_WORKFLOW.id })); + await refetch.promise; + }); + + expect(within(screen.getByTestId("column-ideas")).getByText("Created builtin:coding-ideas")).toBeTruthy(); + }); + it("leaves the legacy flag-off Board quick-create path unchanged", async () => { const inputs: TaskCreateInput[] = []; fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({}, false)); diff --git a/packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts b/packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts index d5cb65a18e..d1c59a3a9a 100644 --- a/packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useTaskHandlers.test.ts @@ -37,7 +37,10 @@ describe("useTaskHandlers", () => { vi.clearAllMocks(); }); - it("handleBoardQuickCreate calls createTask with triage column and returns task", async () => { + // FN-7591: handleBoardQuickCreate/handleModalCreate must NOT force column:"triage" — the store resolves the + // landing column from the (selected or default) workflow's intake column, so a manual-intake workflow + // (e.g. Coding (Ideas) → "ideas") parks the card instead of being auto-triaged. + it("handleBoardQuickCreate forwards createTask without forcing a column", async () => { const options = createOptions(); const { result } = renderHook(() => useTaskHandlers(options)); const input: TaskCreateInput = { description: "Do work" }; @@ -47,11 +50,27 @@ describe("useTaskHandlers", () => { created = await result.current.handleBoardQuickCreate(input); }); - expect(options.createTask).toHaveBeenCalledWith({ description: "Do work", column: "triage", source: { sourceType: "dashboard_ui" } }); + expect(options.createTask).toHaveBeenCalledWith({ description: "Do work", source: { sourceType: "dashboard_ui" } }); expect(created).toEqual(CREATED_TASK); }); - it("handleModalCreate calls createTask with triage column and returns task", async () => { + it("handleBoardQuickCreate forwards an explicit workflowId without forcing a column", async () => { + const options = createOptions(); + const { result } = renderHook(() => useTaskHandlers(options)); + const input: TaskCreateInput = { description: "Do work", workflowId: "builtin:coding-ideas" }; + + await act(async () => { + await result.current.handleBoardQuickCreate(input); + }); + + expect(options.createTask).toHaveBeenCalledWith({ + description: "Do work", + workflowId: "builtin:coding-ideas", + source: { sourceType: "dashboard_ui" }, + }); + }); + + it("handleModalCreate forwards createTask without forcing a column", async () => { const options = createOptions(); const { result } = renderHook(() => useTaskHandlers(options)); @@ -60,7 +79,7 @@ describe("useTaskHandlers", () => { created = await result.current.handleModalCreate({ description: "From modal" }); }); - expect(options.createTask).toHaveBeenCalledWith({ description: "From modal", column: "triage", source: { sourceType: "dashboard_ui" } }); + expect(options.createTask).toHaveBeenCalledWith({ description: "From modal", source: { sourceType: "dashboard_ui" } }); expect(created).toEqual(CREATED_TASK); }); diff --git a/packages/dashboard/app/hooks/useTaskHandlers.ts b/packages/dashboard/app/hooks/useTaskHandlers.ts index 5593e0f717..51fddf72df 100644 --- a/packages/dashboard/app/hooks/useTaskHandlers.ts +++ b/packages/dashboard/app/hooks/useTaskHandlers.ts @@ -32,16 +32,20 @@ export function useTaskHandlers(options: UseTaskHandlersOptions): UseTaskHandler addToast, } = options; + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + These wrappers previously forced `column: "triage"` (handleBoardQuickCreate defaulted to it when the caller omitted column; handleModalCreate hard-coded it unconditionally), which overrode InlineCreateCard/NewTaskModal even after those callers stopped sending an explicit column. Both must now forward the caller's `column` untouched (usually omitted) so the store resolves the landing column from the (selected or default) workflow's intake column — e.g. Coding (Ideas) → "ideas" — instead of always forcing legacy triage. + */ const handleBoardQuickCreate = useCallback( async (input: TaskCreateInput): Promise => { - return createTask({ ...input, column: input.column ?? "triage", source: { sourceType: "dashboard_ui" } }); + return createTask({ ...input, source: { sourceType: "dashboard_ui" } }); }, [createTask], ); const handleModalCreate = useCallback( async (input: TaskCreateInput): Promise => { - const task = await createTask({ ...input, column: "triage", source: { sourceType: "dashboard_ui" } }); + const task = await createTask({ ...input, source: { sourceType: "dashboard_ui" } }); return task; }, [createTask], From f30d55fae785f88ea0b4e4ca91ad6652721400ba Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 13:44:50 -0700 Subject: [PATCH 07/24] FN-7593: move Before/After Transformation section to top of task definitions Reorders task-definition prompt templates so the Before -> After Transformation section appears before other sections, making the expected change visible first. - Move the Before -> After Transformation section ahead of other sections in agent-prompts.ts task-definition templates - Update docs/task-management.md to reflect the new section order - Add/extend tests in agent-prompts.test.ts and triage.test.ts covering the new ordering - Add changeset fn-7593-before-after-top.md documenting the change Files changed: .changeset/fn-7593-before-after-top.md | 7 +++++++ docs/task-management.md | 2 +- packages/core/src/__tests__/agent-prompts.test.ts | 20 ++++++++++++++++++++ packages/core/src/agent-prompts.ts | 20 +++++++++++++------- packages/engine/src/__tests__/triage.test.ts | 17 +++++++++++++++++ 5 files changed, 58 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-7593 Fusion-Task-Lineage: d0d5eb4d-2fe0-456c-b061-5c078b78911b Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7593-before-after-top.md | 7 +++++++ docs/task-management.md | 2 +- .../core/src/__tests__/agent-prompts.test.ts | 20 +++++++++++++++++++ packages/core/src/agent-prompts.ts | 20 ++++++++++++------- packages/engine/src/__tests__/triage.test.ts | 17 ++++++++++++++++ 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 .changeset/fn-7593-before-after-top.md diff --git a/.changeset/fn-7593-before-after-top.md b/.changeset/fn-7593-before-after-top.md new file mode 100644 index 0000000000..43186a0490 --- /dev/null +++ b/.changeset/fn-7593-before-after-top.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Move the Before → After transformation summary to the top of generated task definitions. +category: fix +dev: Reorders the standard and fast triage `PROMPT.md` templates in packages/core/src/agent-prompts.ts so `## Before → After Transformation` is the first content section, ahead of `## Review Level` and `## Mission`, matching FN-7499's glance-verification intent. diff --git a/docs/task-management.md b/docs/task-management.md index f592e41fe2..11ea9b8497 100644 --- a/docs/task-management.md +++ b/docs/task-management.md @@ -552,8 +552,8 @@ The task detail modal exposes multiple tabs: After planning, each task gets a structured `PROMPT.md` with sections like: -- Mission - Before → after transformation summary +- Mission - Dependencies - Context to read first - File scope diff --git a/packages/core/src/__tests__/agent-prompts.test.ts b/packages/core/src/__tests__/agent-prompts.test.ts index 1f625b0f64..a34837b925 100644 --- a/packages/core/src/__tests__/agent-prompts.test.ts +++ b/packages/core/src/__tests__/agent-prompts.test.ts @@ -312,6 +312,26 @@ describe("resolveAgentPrompt", () => { } }); + it("places the Before → After Transformation section at the top of the definition, ahead of Mission and Review Level (FN-7593)", () => { + const standardPrompt = resolveAgentPrompt("triage"); + const fastPrompt = builtinSeamPrompt("planning-fast"); + + const standardTransformationIdx = standardPrompt.indexOf("## Before → After Transformation"); + const standardReviewLevelIdx = standardPrompt.indexOf("## Review Level"); + const standardMissionIdx = standardPrompt.indexOf("## Mission"); + expect(standardTransformationIdx).toBeGreaterThan(-1); + expect(standardReviewLevelIdx).toBeGreaterThan(-1); + expect(standardMissionIdx).toBeGreaterThan(-1); + expect(standardTransformationIdx).toBeLessThan(standardReviewLevelIdx); + expect(standardTransformationIdx).toBeLessThan(standardMissionIdx); + + const fastTransformationIdx = fastPrompt.indexOf("## Before → After Transformation"); + const fastMissionIdx = fastPrompt.indexOf("## Mission"); + expect(fastTransformationIdx).toBeGreaterThan(-1); + expect(fastMissionIdx).toBeGreaterThan(-1); + expect(fastTransformationIdx).toBeLessThan(fastMissionIdx); + }); + it("triage planning prompt is sourced from workflow IR without an engine duplicate", () => { const corePrompt = resolveAgentPrompt("triage"); const planningPrompt = resolvePlanningPromptFromIr(BUILTIN_CODING_WORKFLOW_IR); diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index 39d75641a9..14014c0046 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -221,6 +221,9 @@ Keep the prompt lean, but preserve mandatory planning contracts: duplicate searc FNXC:FastPlanning 2026-07-04-16:25: Fast mode skips heavyweight planning ceremony, but every generated task still needs the same glanceable Before → After Transformation section as standard planning so operators can validate intent quickly. + +FNXC:FastPlanning 2026-07-05-12:00: +Per FN-7593, the transformation summary must sit at the top of the PROMPT.md (before Mission), matching the standard-mode placement, so operators get the same glance-first ordering in fast mode. */ const FAST_TRIAGE_PROMPT_TEXT = `You are a task specification agent for "fn". This task is running in **fast mode**. @@ -235,7 +238,7 @@ Write a lean, executable PROMPT.md quickly. Preserve safety gates, but skip heav Before writing a spec, call \`fn_task_list\` for active work, then call \`fn_task_search\` with 2-4 targeted keyword phrases from the title/description, such as file paths, symptoms, and symbols. For any likely match in \`done\` or \`archived\`, call \`fn_task_show\` and inspect it before deciding. If an existing task covers the same work, do not write PROMPT.md; write exactly \`DUPLICATE: {existing-task-id}\`. ## Required PROMPT.md shape -Write PROMPT.md with Mission, Before → After Transformation, Dependencies, Context to Read First, File Scope, Steps, Documentation Requirements, Completion Criteria, Git Commit Convention, and Do NOT. Include \`## Before → After Transformation\` after Mission with concise Before and After bullets stating current state, target state, and why it satisfies the user's request at a glance. In \`## Steps\`, every executable heading MUST use \`### Step N: \` (for example, \`### Step 1: Preflight\`); Do not write bare \`### Preflight\` / \`### Implementation\` headings. Do not add review-level, triage subtask, or proactive subtask headings. +Write PROMPT.md with Before → After Transformation, Mission, Dependencies, Context to Read First, File Scope, Steps, Documentation Requirements, Completion Criteria, Git Commit Convention, and Do NOT. Put \`## Before → After Transformation\` at the top, before \`## Mission\`, with concise Before/After bullets: current state, target state, why it satisfies the user's request at a glance. In \`## Steps\`, every executable heading MUST use \`### Step N: \` (e.g. \`### Step 1: Preflight\`). Do not write bare \`### Preflight\` / \`### Implementation\` headings, and do not add review-level, triage subtask, or proactive subtask headings. ## Surface Enumeration For bug fixes and UI-affordance add/remove tasks, the spec MUST include a \`## Surface Enumeration\` section. The workflow Plan Review gate validates this before execution when plan review is enabled. @@ -297,6 +300,11 @@ Follow this structure exactly: **Created:** {YYYY-MM-DD} **Size:** {S | M | L} +## Before → After Transformation + +- **Before:** {Briefly describe the current state, missing capability, broken behavior, or operator pain point} +- **After:** {Briefly describe the target state and how it satisfies the user's request at a glance} + ## Review Level: {0-3} ({None | Plan Only | Plan and Code | Full}) **Assessment:** {1-2 sentences explaining the score} @@ -306,11 +314,6 @@ Follow this structure exactly: {One paragraph: what you're building and why it matters} -## Before → After Transformation - -- **Before:** {Briefly describe the current state, missing capability, broken behavior, or operator pain point} -- **After:** {Briefly describe the target state and how it satisfies the user's request at a glance} - ## Surface Enumeration {Required for bug-fix tasks and UI-affordance add/remove tasks (adding, removing, or restructuring icons, buttons, chevrons/arrows, toggles, badges, menu entries, click targets): a checklist enumerating every surface the fixed invariant must hold across. Include every provider/bridge for streaming and agent paths; desktop AND mobile breakpoints; empty/undefined/duplicate/populated data states; and every hook/component/module that shares the affected logic. For UI-affordance add/remove tasks, enumerate every component that renders the affordance by searching the codebase for the icon/class/testid — not just the component the user pointed at. Explicitly check for leftover shells after removal (empty buttons, orphaned click targets, now-unused wrappers, dangling aria-labels) across both desktop and mobile breakpoints. Use the canonical checklist in docs/testing.md as the starting point.} @@ -437,11 +440,14 @@ If this task REMOVES existing functionality (deleting modules, settings, API end ## Transformation summary requirement -Every normal implementation, documentation, or decision task definition MUST include \`## Before → After Transformation\` after \`## Mission\`. Keep it concise: use brief Before and After bullets (or equivalent short prose) that name the current state, the target state, and why that target satisfies the user's request at a glance. +Every normal implementation, documentation, or decision task definition MUST include \`## Before → After Transformation\` at the top of the definition, immediately after the \`# Task\` title and \`Created\`/\`Size\` metadata, before \`## Review Level\` and \`## Mission\`. Keep it concise: use brief Before and After bullets (or equivalent short prose) that name the current state, the target state, and why that target satisfies the user's request at a glance. ## Testing requirements diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index e497d912b1..506bab4734 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -753,6 +753,23 @@ describe("FN-5893 invariant regression wording", () => { expect(FAST_PLANNING_PROMPT).not.toContain("## Proactive Subtask Breakdown"); }); + it("places Before → After Transformation at the top of the definition, ahead of Mission and Review Level (FN-7593)", () => { + const standardTransformationIdx = STANDARD_PLANNING_PROMPT.indexOf("## Before → After Transformation"); + const standardReviewLevelIdx = STANDARD_PLANNING_PROMPT.indexOf("## Review Level"); + const standardMissionIdx = STANDARD_PLANNING_PROMPT.indexOf("## Mission"); + expect(standardTransformationIdx).toBeGreaterThan(-1); + expect(standardReviewLevelIdx).toBeGreaterThan(-1); + expect(standardMissionIdx).toBeGreaterThan(-1); + expect(standardTransformationIdx).toBeLessThan(standardReviewLevelIdx); + expect(standardTransformationIdx).toBeLessThan(standardMissionIdx); + + const fastTransformationIdx = FAST_PLANNING_PROMPT.indexOf("## Before → After Transformation"); + const fastMissionIdx = FAST_PLANNING_PROMPT.indexOf("## Mission"); + expect(fastTransformationIdx).toBeGreaterThan(-1); + expect(fastMissionIdx).toBeGreaterThan(-1); + expect(fastTransformationIdx).toBeLessThan(fastMissionIdx); + }); + it("requires invariant-level regression coverage in standard, fast, and core triage prompts", () => { for (const prompt of [ TRIAGE_POLICY_PROMPT, From 8b4e5224eacd379f5e10f0bd99e584c7009c8f1d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 16:10:36 -0700 Subject: [PATCH 08/24] fix(FN-7591): stop intake-column cards vanishing from the workflow board Tasks added to a workflow whose intake column differs from the default (e.g. Coding (Ideas) -> "ideas") disappeared from the board until a manual reload. The board resolves a card's lane from the board-workflows taskWorkflowIds map, which only refetches on mount/focus/workflow-CRUD SSE -- never on task creation. A freshly created card was absent from that map, fell back to the default workflow (no "ideas" column), and was dropped from every lane. - Board.tsx: force one board-workflows refetch (deferred a tick, signature-guarded) whenever a rendered task is missing from taskWorkflowIds, so its real workflow + intake column resolve for any create surface. - Board.tsx: re-home a selected-workflow task whose column the workflow no longer declares into the intake lane instead of a phantom bucket. - useBoardWorkflows.ts: widen refreshBoardWorkflows type to accept forceFresh. - Add regression tests for tasks arriving via the tasks prop (SSE / non-board create surfaces) and the orphan-column safety net. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/fn-7591-intake-card-disappears.md | 7 ++ packages/dashboard/app/components/Board.tsx | 51 +++++++- ...ckcreate-workflow-lane-visibility.test.tsx | 112 ++++++++++++++++++ .../dashboard/app/hooks/useBoardWorkflows.ts | 5 +- 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 .changeset/fn-7591-intake-card-disappears.md diff --git a/.changeset/fn-7591-intake-card-disappears.md b/.changeset/fn-7591-intake-card-disappears.md new file mode 100644 index 0000000000..819f32512c --- /dev/null +++ b/.changeset/fn-7591-intake-card-disappears.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix tasks vanishing from the board after being added to a workflow like Coding (Ideas). +category: fix +dev: Board.tsx forces a board-workflows refetch (deferred one tick, signature-guarded) whenever a rendered task is missing from the taskWorkflowIds map, so its real workflow and intake column resolve regardless of which create surface added it; the single-workflow grouping also re-homes a task whose column its workflow no longer declares into the intake lane instead of dropping it. Fixes the FN-7591 regression where intake-column cards (column "ideas") fell back to the default workflow, which has no such column, and were filtered out until a manual reload. diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index f343fa8cfb..04d6b5934b 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -454,6 +454,46 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o : boardWorkflows.defaultWorkflowId; }, [boardWorkflows, knownWorkflowIds]); + /* + FNXC:WorkflowBoard 2026-07-05-14:20: + Invariant: every rendered task must resolve to its REAL workflow, or the board silently drops it. + A task created into a workflow whose intake column differs from the default (e.g. Coding (Ideas) → "ideas", per FN-7591) disappears until the next mount/focus/workflow-CRUD refetch. Cause: the task list (SSE) updates before the board-workflows `taskWorkflowIds` map, so getEffectiveTaskWorkflowId falls back to `defaultWorkflowId` (plain Coding), whose columns do not declare the intake column; the aggregate grouping then `continue`-skips the card and the single-workflow grouping files it into a never-rendered phantom bucket. The board's own quick-create handlers dodge this via applyOptimisticTaskWorkflow, but the shared create surfaces (QuickEntryBox / NewTaskModal / InlineCreateCard→TodoView / insight→task) route through useTaskHandlers and never seed the map. Fix at the invariant, not the create surface: whenever a rendered task is absent from taskWorkflowIds, force ONE board-workflows refetch so its persisted workflow selection (and intake column) resolves. Signature-guarded on the sorted unmapped-id set so we never spin an infinite refetch loop, and only run in workflow mode once the payload has loaded. + + The refetch is deferred by one macrotask and re-checked against the latest state at fire time: the board's own quick-create commits the new task one microtask before applyOptimisticTaskWorkflow seeds it, so a synchronous refetch here would double-fire alongside the optimistic path. Deferring lets the seed land first — an already-mapped task is then skipped — so this only fetches for tasks that truly arrived without a workflow mapping. + */ + const boardWorkflowsRef = useRef(boardWorkflows); + boardWorkflowsRef.current = boardWorkflows; + const tasksRef = useRef(tasks); + tasksRef.current = tasks; + const lastUnmappedTaskSignatureRef = useRef(null); + const unmappedRefetchTimerRef = useRef | null>(null); + useEffect(() => { + if (!boardWorkflows || !workflowMode) return; + const unmapped = tasks + .filter((task) => boardWorkflows.taskWorkflowIds[task.id] === undefined) + .map((task) => task.id) + .sort(); + if (unmapped.length === 0) { + lastUnmappedTaskSignatureRef.current = null; + return; + } + const signature = unmapped.join(","); + if (signature === lastUnmappedTaskSignatureRef.current) return; + lastUnmappedTaskSignatureRef.current = signature; + if (unmappedRefetchTimerRef.current) clearTimeout(unmappedRefetchTimerRef.current); + unmappedRefetchTimerRef.current = setTimeout(() => { + unmappedRefetchTimerRef.current = null; + const latestWorkflows = boardWorkflowsRef.current; + if (!latestWorkflows) return; + const stillUnmapped = tasksRef.current.some((task) => latestWorkflows.taskWorkflowIds[task.id] === undefined); + if (stillUnmapped) refreshBoardWorkflows({ forceFresh: true }); + }, 0); + }, [boardWorkflows, refreshBoardWorkflows, tasks, workflowMode]); + + useEffect(() => () => { + if (unmappedRefetchTimerRef.current) clearTimeout(unmappedRefetchTimerRef.current); + }, []); + const resolveWorkflowQuickCreateTarget = useCallback((targetWorkflowId: string, preferredColumnId?: string | null): ColumnId | undefined => { if (targetWorkflowId === ALL_WORKFLOWS_BOARD_VIEW_ID) return undefined; const workflow = boardWorkflows?.workflows.find((candidate) => candidate.id === targetWorkflowId); @@ -578,8 +618,15 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o const grouped: Record = {}; if (!selectedWorkflow) return grouped; for (const column of selectedWorkflow.columns) grouped[column.id] = []; + /* + FNXC:WorkflowBoard 2026-07-05-14:20: + Safety net (defense in depth for the taskWorkflowIds refetch above): a card that passed the selected-workflow membership filter genuinely belongs on THIS board, so it must always land in a rendered lane. If its stored `column` is not one this workflow declares (a workflow edited to drop a column, or a create/refetch race that lands an intake-column card before its lane is known), re-home it for DISPLAY into the workflow's intake/first visible column instead of a `??=`-created bucket that is never rendered. Display-only — the task's stored column is untouched. + */ for (const task of selectedWorkflowTasks) { - (grouped[task.column] ??= []).push(task); + const columnId = grouped[task.column] !== undefined + ? task.column + : (selectedWorkflowCreateColumnId ?? task.column); + (grouped[columnId] ??= []).push(task); } for (const column of selectedWorkflow.columns) { /* @@ -592,7 +639,7 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o : sortTasksForDisplayColumn(grouped[column.id] ?? [], column.id as ColumnType); } return grouped; - }, [doneSortMode, selectedWorkflow, selectedWorkflowTasks]); + }, [doneSortMode, selectedWorkflow, selectedWorkflowCreateColumnId, selectedWorkflowTasks]); // Card-placed field defs grouped by workflow id (U13/KTD-14). Only recomputes // when the board-workflows payload changes, not on every SSE task tick. diff --git a/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx b/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx index 28964c172a..b6c8167549 100644 --- a/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx +++ b/packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx @@ -183,6 +183,9 @@ function BoardHarness({ createdTaskId = "FN-new", createReturnsTask = true, onCr onNewTask={vi.fn()} autoMerge onToggleAutoMerge={vi.fn()} + showWorktreeGrouping={false} + planAutoApproveEnabled={false} + onTogglePlanAutoApprove={vi.fn()} workflowColumnsEnabled settingsLoaded /> @@ -360,3 +363,112 @@ describe("workflow lane quick-create visibility", () => { expect(readWorkflowCache()?.taskWorkflowIds["FN-new"]).toBe(DEFAULT_WORKFLOW.id); }); }); + +/* +FNXC:WorkflowBoard 2026-07-05-14:20: +Regression coverage for the disappearing intake-column card (Coding (Ideas) → "ideas", FN-7591 fallout). +Surface enumeration: + - Create-path independence: tasks that arrive via the `tasks` prop (SSE / QuickEntryBox / NewTaskModal / InlineCreateCard→TodoView / insight→task) — i.e. NOT the board's own optimistic-seeding quick-create — must still resolve their real workflow. Invariant: an unmapped rendered task forces one board-workflows refetch (Part A), and once mapped renders in its intake lane instead of being dropped. + - No infinite loop: if the refetch never maps the task, the signature guard fires the refetch at most once per distinct unmapped-id set. + - Orphan column safety net (Part B): a task that belongs to the selected workflow but whose stored column the workflow no longer declares renders in the intake lane, never vanishing. +*/ +function boardProps(tasks: Task[]) { + return { + tasks, + projectId: PROJECT_ID, + maxConcurrent: 2, + onMoveTask: vi.fn(), + onOpenDetail: vi.fn(), + addToast: vi.fn(), + onQuickCreate: vi.fn(), + onNewTask: vi.fn(), + autoMerge: true, + onToggleAutoMerge: vi.fn(), + showWorktreeGrouping: false, + planAutoApproveEnabled: false, + onTogglePlanAutoApprove: vi.fn(), + workflowColumnsEnabled: true as const, + settingsLoaded: true as const, + }; +} + +describe("workflow lane visibility for externally-arriving tasks (FN-7591 disappearing-card fix)", () => { + it("force-refetches board-workflows and renders an intake-column task that arrives via the tasks prop (non-board create surface)", async () => { + // Model the server: board-workflows derives taskWorkflowIds from the current store tasks, + // so once the ideas task exists it maps to Coding (Ideas) on the next fetch. + const serverMappedIds = new Set(); + fetchBoardWorkflowsMock.mockImplementation(() => { + const map: Record = {}; + for (const id of serverMappedIds) map[id] = CODING_IDEAS_WORKFLOW.id; + return Promise.resolve(workflowPayload(map)); + }); + + const { rerender } = render(); + await screen.findByTestId("workflow-switcher"); + selectWorkflow(CODING_IDEAS_WORKFLOW.id); + + const callsBeforeArrival = fetchBoardWorkflowsMock.mock.calls.length; + + // A card lands in the "ideas" intake column via a surface that does NOT optimistically + // seed taskWorkflowIds (the store already persisted its workflow selection). + serverMappedIds.add("FN-ext"); + const ideasTask = mkTask({ id: "FN-ext", title: "Ext ideas card", column: "ideas" }); + await act(async () => { + rerender(); + }); + + // Part A: an unmapped rendered task forces a fresh board-workflows fetch. + await waitFor(() => expect(fetchBoardWorkflowsMock.mock.calls.length).toBeGreaterThan(callsBeforeArrival)); + + // Once mapped, the card renders in the ideas lane instead of being dropped. + await waitFor(() => { + const ideasColumn = screen.getByTestId("column-ideas"); + expect(within(ideasColumn).getByText("Ext ideas card")).toBeTruthy(); + }); + }); + + it("fires a bounded number of refetches for a persistently-unmapped task (no infinite loop)", async () => { + // The server never maps FN-ext, so it stays unmapped after every refetch. + fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({})); + + const { rerender } = render(); + await screen.findByTestId("workflow-switcher"); + selectWorkflow(CODING_IDEAS_WORKFLOW.id); + + const ideasTask = mkTask({ id: "FN-ext", title: "Ext ideas card", column: "ideas" }); + await act(async () => { + rerender(); + }); + + // Let the single deferred refetch fire; the signature guard blocks reschedules for the same set. + await waitFor(() => expect(fetchBoardWorkflowsMock.mock.calls.length).toBeGreaterThanOrEqual(2)); + const settled = fetchBoardWorkflowsMock.mock.calls.length; + + // Extra renders with the SAME unmapped-id set must not schedule further refetches. + for (let i = 0; i < 3; i++) { + await act(async () => { + rerender(); + }); + } + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 5)); + }); + expect(fetchBoardWorkflowsMock.mock.calls.length).toBe(settled); + }); + + it("renders a selected-workflow task whose column the workflow no longer declares in the intake lane (never dropped)", async () => { + // FN-orphan is correctly mapped to Coding (Ideas) but sits in a column the workflow does not declare. + fetchBoardWorkflowsMock.mockResolvedValue(workflowPayload({ "FN-orphan": CODING_IDEAS_WORKFLOW.id })); + const orphan = mkTask({ id: "FN-orphan", title: "Orphan column card", column: "removed-column" }); + + render(); + await screen.findByTestId("workflow-switcher"); + selectWorkflow(CODING_IDEAS_WORKFLOW.id); + + // Part B safety net: re-homed for display into the intake ("ideas") lane, not dropped. + await waitFor(() => { + const ideasColumn = screen.getByTestId("column-ideas"); + expect(within(ideasColumn).getByText("Orphan column card")).toBeTruthy(); + }); + }); +}); diff --git a/packages/dashboard/app/hooks/useBoardWorkflows.ts b/packages/dashboard/app/hooks/useBoardWorkflows.ts index 8d2a44e469..e1abdbe765 100644 --- a/packages/dashboard/app/hooks/useBoardWorkflows.ts +++ b/packages/dashboard/app/hooks/useBoardWorkflows.ts @@ -54,8 +54,9 @@ export interface UseBoardWorkflowsResult { /** True when the dashboard-only aggregate workflow view is selected. */ isAllWorkflowsSelected: boolean; setSelectedWorkflowId: Dispatch>; - /** Force a fresh fetch (used on switcher open, since task assignment changes emit no workflow SSE). */ - refreshBoardWorkflows: () => void; + /** Force a fresh fetch (used on switcher open, and when the board detects a rendered + * task missing from `taskWorkflowIds`, since task→workflow assignment emits no workflow SSE). */ + refreshBoardWorkflows: (options?: { forceFresh?: boolean }) => void; /** * Raw state setter, exposed so Board can apply optimistic task→workflow assignment. * Planning does not use this. From 09a1c9d843e39e0718346da3076508cb36f44c3e Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 16:33:21 -0700 Subject: [PATCH 09/24] FN-7596: regression-test the Coding (Ideas) manual-intake lifecycle end-to-end Adds cross-layer regression coverage for the manual-intake parking lifecycle (create -> parked -> operator Start promotion -> poll-time todo-discovery), and clarifies the workflow-steps doc to describe the tested lifecycle. - packages/core: covers store create -> moveTask promotion out of the parked intake column - packages/engine: covers triage poll ordering/discovery of the still-unplanned bootstrap-stub card - packages/dashboard: covers TaskCard's Start affordance for parked cards - docs: documents the full regression-tested lifecycle for manual-intake column parking (FN-7596) Files changed: docs/workflow-steps.md | 2 +- .../__tests__/store-create-intake-column.test.ts | 26 ++++ .../app/components/__tests__/TaskCard.test.tsx | 153 +++++++++++++++++++++ packages/engine/src/__tests__/triage.test.ts | 119 +++++++++++++++- 4 files changed, 298 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7596 Fusion-Task-Lineage: 267c3d9a-6181-4ca5-b871-7009c0204372 Co-authored-by: Fusion (runfusion.ai) --- docs/workflow-steps.md | 2 +- .../store-create-intake-column.test.ts | 26 +++ .../components/__tests__/TaskCard.test.tsx | 153 ++++++++++++++++++ packages/engine/src/__tests__/triage.test.ts | 119 +++++++++++++- 4 files changed, 298 insertions(+), 2 deletions(-) diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index 48db90a417..746a4a1c05 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -82,7 +82,7 @@ Use this inventory as the documentation map for current workflow behavior: | Routing boundary | Agents may select/change a workflow only for explicit user requests or tasks they created; no-commit markers do not imply Quick fix or any other workflow. | This page, [Selecting workflows](#selecting-workflows); [Agents](./agents.md#interactive-cli-chat). | | Dashboard board/list/graph selection | Board/List/Header/Graph share durable per-project workflow selection; stale saved ids fall back to a valid workflow. Board adds a dashboard-only **All workflows** aggregate and task workflow-name badges; Graph uses **All workflows** for the full active graph. | [Dashboard Guide → Board View](./dashboard-guide.md#board-view), [Graph View](./dashboard-guide.md#graph-view), and [Workflow Selection and Editor](./dashboard-guide.md#workflow-selection-and-editor). | | Create/planning forwarding | Quick-create task creation, Planning Mode, Subtask Breakdown, and the New Task dialog forward the active real workflow id when creating tasks; **All workflows** quick-create chooses a real workflow intake/default column instead of saving a synthetic aggregate id. | [Dashboard Guide → Planning Mode](./dashboard-guide.md#planning-mode). | -| Manual-intake column parking | Dashboard create surfaces never send an explicit `column`; the store resolves the landing column from the (selected or project-default) workflow's intake column. A workflow whose intake column sets `autoTriage: false` (e.g. built-in Coding (Ideas)'s `ideas` column) parks new cards there instead of auto-planning them, until an operator promotes the card. | [Dashboard Guide → Create/Planning Forwarding](./dashboard-guide.md#planning-mode). | +| Manual-intake column parking | Dashboard create surfaces never send an explicit `column`; the store resolves the landing column from the (selected or project-default) workflow's intake column. A workflow whose intake column sets `autoTriage: false` (e.g. built-in Coding (Ideas)'s `ideas` column) parks new cards there instead of auto-planning them, until an operator promotes the card. The full lifecycle — create → parked → operator "Start" promotion → poll-time todo-discovery of the still-unplanned (bootstrap-stub) card — is regression-tested at the engine (triage poll ordering/discovery), UI (`TaskCard` Start affordance), and store (create → `moveTask` promotion) layers (FN-7596). | [Dashboard Guide → Create/Planning Forwarding](./dashboard-guide.md#planning-mode). | ### Skill-backed workflow steps diff --git a/packages/core/src/__tests__/store-create-intake-column.test.ts b/packages/core/src/__tests__/store-create-intake-column.test.ts index f2c94ea21a..382400c501 100644 --- a/packages/core/src/__tests__/store-create-intake-column.test.ts +++ b/packages/core/src/__tests__/store-create-intake-column.test.ts @@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import type { Task } from "../types.js"; import { createTaskStoreTestHarness } from "./store-test-helpers.js"; +import { buildBootstrapPrompt } from "../mesh-task-replication.js"; /* FNXC:CodingIdeasWorkflow 2026-07-04-11:30: @@ -80,4 +81,29 @@ describe("createTask intake-column wiring (Coding (Ideas))", () => { // A direct todo create is NOT an intake column, so it must NOT get the bootstrap stub. expect(prompt).not.toBe(`# ${task.id}\n\n${task.description}\n`); }); + + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + FN-7596 pins the store-level contract the engine's todo-discovery poll (packages/engine/src/triage.ts eligibleTodoTasks) depends on: promoting a parked Ideas card via moveTask alone must NOT plan it. Only the triage service's bootstrap-prompt discovery loop plans a promoted-but-unplanned todo card; moveTask is a pure column transition. + */ + it("promotes an Ideas-parked task to todo without planning it (still bootstrap-stub PROMPT.md)", async () => { + const store = harness.store(); + // Custom (non-legacy) column transitions are validated against the workflow IR + // only when the workflowColumns compatibility flag is enabled (KTD-1). + await store.updateGlobalSettings({ experimentalFeatures: { workflowColumns: true } }); + const task = await store.createTask({ + description: "ideas lifecycle promotion task", + workflowId: "builtin:coding-ideas", + }); + expect(task.column).toBe("ideas"); + + const moved = await store.moveTask(task.id, "todo", { moveSource: "user" }); + expect(moved.column).toBe("todo"); + + const prompt = await readFile( + join(harness.rootDir(), ".fusion", "tasks", task.id, "PROMPT.md"), + "utf-8", + ); + expect(prompt).toBe(buildBootstrapPrompt(task.id, task.title, task.description)); + }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 9cd637d2ab..83c87f28a4 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -6411,3 +6411,156 @@ describe("TaskCard custom field badges (U13/KTD-14)", () => { expect(screen.queryByTestId("card-field-badges")).toBeNull(); }); }); + +/* +FNXC:CodingIdeasWorkflow 2026-07-05-00:00: +FN-7596 regression-tests the TaskCard "Start" affordance that promotes a Coding (Ideas) manual-intake card. `showStartAction` requires taskColumnFlags.intake and a non-"triage" column; `startTargetColumn` derives the destination from `taskMoveColumns` (first non-intake/non-archived/non-hiddenFromBoard column) rather than a hard-coded "todo" string, per the FNXC comment at its call site. +*/ +describe("TaskCard Start affordance (FN-7596)", () => { + it("renders the Start button for a manual-intake column with onMoveTask provided", () => { + render( + , + ); + + expect(screen.getByTestId("card-start-FN-001")).toBeInTheDocument(); + }); + + it("omits the Start button when the column is not flagged as an intake", () => { + render( + , + ); + + expect(screen.queryByTestId("card-start-FN-001")).toBeNull(); + }); + + it("omits the Start button for the triage column even when intake is flagged", () => { + render( + , + ); + + expect(screen.queryByTestId("card-start-FN-001")).toBeNull(); + }); + + it("omits the Start button when no onMoveTask handler is provided", () => { + render( + , + ); + + expect(screen.queryByTestId("card-start-FN-001")).toBeNull(); + }); + + it("derives the Start target from taskMoveColumns instead of a hard-coded 'todo' string", async () => { + const onMoveTask = vi.fn().mockResolvedValue(makeTask({ column: "custom-working-stage" as any })); + const addToast = vi.fn(); + // The intake column itself, plus a non-intake working column that is NOT literally + // named "todo", must win over any coincidental fallback — proving derivation, not a hard-coded string. + const taskMoveColumns = [ + { id: "ideas" as any, label: "Ideas", flags: { intake: true } }, + { id: "custom-working-stage" as any, label: "Custom Working Stage", flags: {} }, + { id: "todo" as any, label: "Todo", flags: {} }, + ]; + + render( + , + ); + + fireEvent.click(screen.getByTestId("card-start-FN-001")); + + await waitFor(() => expect(onMoveTask).toHaveBeenCalledWith("FN-001", "custom-working-stage")); + }); + + it("falls back to 'todo' when taskMoveColumns metadata is unavailable", async () => { + const onMoveTask = vi.fn().mockResolvedValue(makeTask({ column: "todo" })); + render( + , + ); + + fireEvent.click(screen.getByTestId("card-start-FN-001")); + + await waitFor(() => expect(onMoveTask).toHaveBeenCalledWith("FN-001", "todo")); + }); + + it("disables the button and shows the Starting label while the move is in flight, then shows a success toast", async () => { + let resolveMove: (task: ReturnType) => void = () => {}; + const onMoveTask = vi.fn().mockImplementation( + () => new Promise((resolve) => { resolveMove = resolve; }), + ); + const addToast = vi.fn(); + + render( + , + ); + + const startButton = screen.getByTestId("card-start-FN-001"); + fireEvent.click(startButton); + + await waitFor(() => expect(startButton).toBeDisabled()); + expect(startButton.textContent).toContain("Starting"); + + resolveMove(makeTask({ column: "todo" })); + + await waitFor(() => expect(addToast).toHaveBeenCalledWith(expect.stringContaining("FN-001"), "success")); + await waitFor(() => expect(startButton).not.toBeDisabled()); + }); + + it("shows an error toast when the Start move fails", async () => { + const onMoveTask = vi.fn().mockRejectedValue(new Error("move blocked")); + const addToast = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByTestId("card-start-FN-001")); + + await waitFor(() => expect(addToast).toHaveBeenCalledWith("move blocked", "error")); + }); +}); diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index 506bab4734..37e0cb310d 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import type { TaskStore, Task, TaskDetail, Settings } from "@fusion/core"; -import { builtinSeamPrompt, computePlanApprovalFingerprint, MAX_TASK_LIST_TEXT_CHARS, renderTriagePolicyPlaceholders, resolveAgentPrompt } from "@fusion/core"; +import { builtinSeamPrompt, buildBootstrapPrompt, computePlanApprovalFingerprint, MAX_TASK_LIST_TEXT_CHARS, renderTriagePolicyPlaceholders, resolveAgentPrompt } from "@fusion/core"; import { TriageProcessor, buildSpecificationPrompt, @@ -2392,6 +2392,123 @@ describe("TriageProcessor", () => { }); }); + /* + FNXC:CodingIdeasWorkflow 2026-07-05-00:00: + FN-7596 pins the Coding (Ideas) manual-intake lifecycle at the poll-dispatch boundary: an `ideas`-column card must stay parked (never auto-dispatched via `eligibleTriageTasks`, which only matches `column === "triage"`), while a promoted `todo`-column card whose PROMPT.md is still the bootstrap stub must be discovered and specified via `eligibleTodoTasks`'s bootstrap-prompt file check. A `todo` card with a real (non-bootstrap) spec must NOT be re-dispatched, guarding against double-specifying an already-planned card. + */ + describe("Coding (Ideas) manual-intake discovery (FN-7596)", () => { + it("excludes a parked ideas-column task from the poll's specify-dispatch set", async () => { + const tasks: Task[] = [ + createTriageTask({ id: "FN-IDEAS-PARKED", column: "ideas" as any, priority: "urgent" }), + ]; + + const triageStore = createMockStore({ + listTasks: vi.fn().mockResolvedValue(tasks), + getSettings: vi.fn().mockResolvedValue({ + maxConcurrent: 10, + maxTriageConcurrent: 10, + pollIntervalMs: 10_000, + groupOverlappingFiles: false, + autoMerge: true, + }), + }); + const triageProcessor = new TriageProcessor(triageStore, rootDir); + const specifySpy = vi + .spyOn(triageProcessor, "specifyTask") + .mockResolvedValue(undefined); + + (triageProcessor as any).running = true; + await (triageProcessor as any).poll(); + + expect(specifySpy).not.toHaveBeenCalled(); + }); + + it("discovers a promoted todo-column task whose PROMPT.md is still the bootstrap stub", async () => { + const tempRoot = await createTriageFixtureRoot("fusion-triage-ideas-discovery-"); + const promotedId = "FN-IDEAS-PROMOTED"; + try { + const promotedTask = createTriageTask({ + id: promotedId, + title: "Promoted from Ideas intake", + description: "Promoted intake task", + column: "todo", + priority: "urgent", + }); + await mkdir(join(tempRoot, ".fusion", "tasks", promotedId), { recursive: true }); + await writeFile( + join(tempRoot, ".fusion", "tasks", promotedId, "PROMPT.md"), + buildBootstrapPrompt(promotedId, promotedTask.title, promotedTask.description), + "utf-8", + ); + + const triageStore = createMockStore({ + listTasks: vi.fn().mockResolvedValue([promotedTask]), + getSettings: vi.fn().mockResolvedValue({ + maxConcurrent: 10, + maxTriageConcurrent: 10, + pollIntervalMs: 10_000, + groupOverlappingFiles: false, + autoMerge: true, + }), + }); + const triageProcessor = new TriageProcessor(triageStore, tempRoot); + const specifySpy = vi + .spyOn(triageProcessor, "specifyTask") + .mockResolvedValue(undefined); + + (triageProcessor as any).running = true; + await (triageProcessor as any).poll(); + + expect(specifySpy).toHaveBeenCalledTimes(1); + expect(specifySpy).toHaveBeenCalledWith(expect.objectContaining({ id: promotedId })); + } finally { + await cleanupTriageFixtureRoot(tempRoot); + } + }); + + it("does not re-dispatch a todo-column task whose PROMPT.md already carries a real (non-bootstrap) spec", async () => { + const tempRoot = await createTriageFixtureRoot("fusion-triage-ideas-planned-"); + const plannedId = "FN-IDEAS-PLANNED"; + try { + const plannedTask = createTriageTask({ + id: plannedId, + title: "Already planned todo task", + description: "Already planned intake task", + column: "todo", + priority: "urgent", + }); + await mkdir(join(tempRoot, ".fusion", "tasks", plannedId), { recursive: true }); + await writeFile( + join(tempRoot, ".fusion", "tasks", plannedId, "PROMPT.md"), + `# Task: ${plannedId} - Already planned todo task\n\n## Mission\n\nThis task carries a real spec, not the bootstrap stub.\n`, + "utf-8", + ); + + const triageStore = createMockStore({ + listTasks: vi.fn().mockResolvedValue([plannedTask]), + getSettings: vi.fn().mockResolvedValue({ + maxConcurrent: 10, + maxTriageConcurrent: 10, + pollIntervalMs: 10_000, + groupOverlappingFiles: false, + autoMerge: true, + }), + }); + const triageProcessor = new TriageProcessor(triageStore, tempRoot); + const specifySpy = vi + .spyOn(triageProcessor, "specifyTask") + .mockResolvedValue(undefined); + + (triageProcessor as any).running = true; + await (triageProcessor as any).poll(); + + expect(specifySpy).not.toHaveBeenCalled(); + } finally { + await cleanupTriageFixtureRoot(tempRoot); + } + }); + }); + it("runs deterministic validation without calling the spec reviewer", async () => { const taskId = "FN-001"; const testRootDir = await createTriageFixtureRoot("fusion-triage-plan-validation-"); From c0abca90d8e7a5e6cf504dc07fe32bcdef0f26be Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 16:51:05 -0700 Subject: [PATCH 10/24] FN-7598: add planner-oversight discovery pointers to README and docs hub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs-only change adding front-door discovery for the already-shipped planner-oversight feature (FN-7508 → FN-7583), which previously had no entry point outside internal reference docs. - Add a README.md feature table row and a new "Planner oversight" section describing oversight levels (off/observe/steer/autonomous) and the always-on human-confirmation gate for merge/PR and destructive actions, linking to Settings Reference and Dashboard Guide - Add a README.md capabilities bullet cross-linking the new section - Add a docs/README.md hub row pointing to Settings Reference, Dashboard Guide, and Architecture for planner oversight, and extend the 'power user' reading path - Add a one-line pointer in docs/getting-started.md workflow section noting per-task/workflow oversight controls Files changed: README.md | 11 +++++++++++ docs/README.md | 9 ++++++--- docs/getting-started.md | 3 +++ 3 files changed, 20 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-7598 Fusion-Task-Lineage: 8141b44c-f007-45c0-a057-f4eeb34ae8d4 Co-authored-by: Fusion (runfusion.ai) --- README.md | 11 +++++++++++ docs/README.md | 9 ++++++--- docs/getting-started.md | 3 +++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7092cce277..b75e16bd9c 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ Every task shows its plan, its reviews, its diffs, and its file changes in real |---|---| | 🧠 **AI planning** | Describe a task in plain language. Planning agents turn it into a `PROMPT.md` plan with steps, file scope, and acceptance criteria. | | 🔁 **Selectable workflows** | Built-ins cover coding, quick fixes, review-heavy work, stepwise execution, plugin-gated Compound Engineering, and PR lifecycle fragments. Pick a workflow per task or author custom ones in the [Workflow Editor](./docs/workflow-editor.md). | +| 🛡️ **Planner oversight** | Per-task or per-workflow oversight level (`off` / `observe` / `steer` / `autonomous`) governs how closely a planner overseer watches and intervenes — merge/PR and destructive actions always require explicit human confirmation. See [Settings Reference](./docs/settings-reference.md#workflow-settings) and [Dashboard Guide](./docs/dashboard-guide.md). | | 🌳 **Worktree isolation** | Each task runs in its own branch and worktree (`fusion/{task-id}`). Parallel tasks. Zero conflicts. Optional [worktrunk](https://github.com/max-sixty/worktrunk) delegation via [`worktrunk.enabled`](./docs/settings-reference.md#worktree-backend-settings) (see [WorktreeBackend abstraction](./docs/architecture.md#worktreebackend-abstraction)). | | ⚡ **Smart merge controls** | Passing every gate? Fusion squash-merges and moves on. Opt into manual approval anywhere, inherit the live global auto-merge default, or set explicit per-task auto/manual overrides. | | 🛰️ **Multi-node mesh** | Laptop, Mac mini, Linux server, cloud VM, phone — all synced. Desktop, mobile, web. | @@ -385,6 +386,15 @@ Fusion workflows define how a task moves from idea to delivery. The default codi Read [Workflow Steps](./docs/workflow-steps.md) for runtime semantics, built-in workflow behavior, and workflow-step templates; read [Workflow Editor](./docs/workflow-editor.md) for the dashboard authoring guide. + + +### Planner oversight + +Each workflow (and optionally each task) can set a **planner oversight** level — `off`, `observe`, `steer`, or `autonomous` (default) — controlling how closely a planner overseer watches and intervenes in that task's execution. Even at `autonomous`, merge/PR progression and any destructive or external-service side effect always require an explicit, recorded human confirmation before they run. Notification verbosity is controlled separately. Set the default in the **Workflow Editor → Values** tab, or override per task from the New Task dialog / Task Detail edit form. Read [Settings Reference](./docs/settings-reference.md#workflow-settings) for the full setting semantics and [Dashboard Guide](./docs/dashboard-guide.md) for the UI controls. + --- ## Multi-node. One board. Every platform. @@ -504,6 +514,7 @@ npx companies.sh add paperclipai/companies/gstack - **Visual Workflow Editor** — Inspect read-only built-ins, duplicate/customize workflows, and edit graph nodes, columns, task fields, typed settings, and per-project values ([Workflow Editor](./docs/workflow-editor.md)) - **Workflow Steps** — Configurable quality gates (pre-merge: blocks merge; post-merge: informational), plus workflow-declared optional steps such as opt-in [Browser Verification](./docs/workflow-steps.md#workflow-declared-optional-steps) - **Workflow-native policy** — Fast-mode planning (`leanPlanning` / `autoApproveSpec`), typed triage thresholds, review/approval, step execution, and model/fallback lanes are workflow settings, not hard-coded engine constants ([Settings Reference](./docs/settings-reference.md#workflow-native-triage-policy-settings); [workflow settings](./docs/settings-reference.md#workflow-settings)) +- **Planner oversight** — Workflow-native `plannerOversightLevel` (`off`/`observe`/`steer`/`autonomous`), with an optional per-task override and a separate notification-verbosity setting; merge/PR progression and destructive actions always require explicit human confirmation, even at `autonomous` ([overview](#planner-oversight); [Settings Reference](./docs/settings-reference.md#workflow-settings)) - **GitHub + PR lifecycle** — Import issues, create PRs, display real-time PR/issue badges, and use workflow-mode PR lifecycle graph fragments where enabled - **Dashboard** — Real-time kanban/list/graph views, agent management, terminal, git manager, mission planner, chat, workflow editor, custom provider setup, and one-click update action - **Missions** — Hierarchical planning (Mission → Milestone → Slice → Feature → Task) with autopilot, validation contracts, fix-feature retries, mission-goal linking, and blocked-handoff semantics diff --git a/docs/README.md b/docs/README.md index 9b0c003a59..28d5bfba19 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,11 +43,14 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow | [Multi-Project](./multi-project.md) | Central registry architecture, project management, isolation modes, and migration paths | ### Configuration & Agents -| Guide | Description | -|---|---| + | [Settings Reference](./settings-reference.md) | Global/project settings, workflow setting values, model/fallback lane hierarchy, defaults, and API endpoints | | [MCP](./mcp.md) | Model Context Protocol server configuration, secret references, validation, CLI, dashboard, and import/export workflows | | [Agents](./agents.md) | Agent management, presets, prompts, heartbeat behavior, spawning, and mailbox workflows | +| Planner Oversight (see [Settings Reference](./settings-reference.md#workflow-settings), [Dashboard Guide](./dashboard-guide.md), [Architecture](./architecture.md)) | Workflow-native oversight levels (`off`/`observe`/`steer`/`autonomous`), per-task overrides, notification verbosity, the human-confirmation gate on merge/PR and destructive actions, and the Task Detail overseer controls/Intervention Timeline | ### Architecture & Development | Guide | Description | @@ -151,5 +154,5 @@ FN-7088 links previously-unlinked first-class testing and baseline docs here so - **New user:** Getting Started → Dashboard Guide → Task Management - **Workflow author:** Dashboard Guide → Workflow Editor → Workflow Steps → Settings Reference -- **Power user / automation owner:** Settings Reference → Workflow Steps → Agents +- **Power user / automation owner:** Settings Reference → Workflow Steps → Agents → Planner Oversight (Settings Reference § Workflow Settings) - **Maintainer / contributor:** Architecture → Multi-Project → Contributing diff --git a/docs/getting-started.md b/docs/getting-started.md index 0436bb6002..74bd81d7ba 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -137,6 +137,9 @@ Most tasks can use the default **Coding** workflow. When the workflow selector i Built-ins include task-selectable Coding, Legacy coding, Quick fix, Review-heavy, plugin-gated Compound engineering, Coding (per-step review), and Design workflows, plus PR lifecycle fragments for workflow authors. For the full catalog and runtime behavior, see [Workflow Steps](./workflow-steps.md#workflow-overview). To inspect built-ins or author custom workflows, open the dashboard [Workflow Editor](./workflow-editor.md). + +Workflows (and individual tasks) also have a **Planner oversight** level (`off`/`observe`/`steer`/`autonomous`) that controls how closely a planner overseer watches and can intervene; see [Settings Reference](./settings-reference.md#workflow-settings) for the setting semantics and [Dashboard Guide](./dashboard-guide.md) for the UI controls. + ## Understand the Task Lifecycle Fusion uses six default lifecycle columns: From 20379e81c5a6b40a9eb40b365ae7a145c9c7207a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 16:56:22 -0700 Subject: [PATCH 11/24] FN-7597: style task-detail Priority dropdown to match Oversight dropdown Aligns the task-detail Priority dropdown's size, border, and typography with the Oversight dropdown so both controls read as one consistent style. - Give the untinted `normal` priority level a neutral, token-based chip background (scoped to `.detail-priority-chip.card-priority-badge--normal`) instead of an empty bordered shell, matching the Oversight `--off` chip treatment. - Remove the Priority-only forced uppercase text-transform on the select/option so it relies on the ancestor label's uppercase transform like the Oversight select does. - Add regression coverage asserting shared box-size/border tokens across the Priority chip, Oversight chip, and mobile Oversight overflow trigger, no duplicated text-transform overrides, preserved low/high/urgent semantic tints, and unaffected --saving state. - Add a patch changeset documenting the fix. Files changed: .changeset/fn-7597-priority-dropdown-matches-oversight.md | 7 +++ packages/dashboard/app/components/TaskDetailModal.css | 27 ++++++++++-- packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx | 51 ++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-7597 Fusion-Task-Lineage: d703e59a-35d8-4788-9ad2-1462d6f3c588 Co-authored-by: Fusion (runfusion.ai) --- ...597-priority-dropdown-matches-oversight.md | 7 +++ .../app/components/TaskDetailModal.css | 27 ++++++++-- ...Modal.responsive-and-dependencies.test.tsx | 51 +++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-7597-priority-dropdown-matches-oversight.md diff --git a/.changeset/fn-7597-priority-dropdown-matches-oversight.md b/.changeset/fn-7597-priority-dropdown-matches-oversight.md new file mode 100644 index 0000000000..4d89e6363c --- /dev/null +++ b/.changeset/fn-7597-priority-dropdown-matches-oversight.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Task-detail Priority dropdown now matches the Oversight dropdown's size, border, and typography. +category: fix +dev: Removed the Priority-only forced select/option uppercase, added a neutral chip background scoped to `.detail-priority-chip.card-priority-badge--normal` for the untinted `normal` level, and reused the FN-7585 shared `--btn-border-width`/`--border`/`--detail-control-border-radius`/`--detail-priority-control-min-height` tokens so both dropdowns render as one control style across desktop and the mobile oversight-overflow surface. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index c4418e0345..93f0b22277 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -415,13 +415,27 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P opacity: 0.75; } +/* +FNXC:TaskDetail 2026-07-05-12:00: +FN-7597 — the untinted `normal` priority level has no `--low/--high/--urgent` +tint, so without this rule it renders as a bordered-but-empty shell while the +Oversight chip's `--off` level always gets a neutral, token-based background +(`.card-oversight-badge--off` in TaskCard.css). Give `.detail-priority-chip`'s +`normal` state the SAME neutral `color-mix(... var(--text-muted) ...)` +treatment so the two dropdowns read as one consistent control style. Scoped +to the detail chip (not a global `.card-priority-badge--normal` rule) so the +read-only TaskCard priority badge (out of scope) is untouched. +*/ +.detail-priority-chip.card-priority-badge--normal { + background: color-mix(in srgb, var(--text-muted) 12%, transparent); + color: var(--text-muted); +} + .detail-priority-select { border: 0; background: transparent; color: inherit; font: inherit; - text-transform: uppercase; - letter-spacing: inherit; cursor: pointer; padding: 0; min-height: inherit; @@ -437,10 +451,17 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P cursor: wait; } +/* +FNXC:TaskDetail 2026-07-05-12:00: +FN-7597 — drop the Priority-only forced uppercase on the select/option so its +typography matches `.detail-oversight-select` exactly (which relies on the +ancestor `.card-*-badge` label's own `text-transform: uppercase` instead of +redeclaring it here). Visual case is unchanged since the label already +uppercases its content; this only removes a duplicated, drift-prone override. +*/ .detail-priority-select option { color: var(--text); background: var(--surface); - text-transform: uppercase; } .detail-execution-mode-toggle { diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index 9e348aadaa..f988e2b5ce 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -253,6 +253,57 @@ describe("TaskDetailModal", () => { expect(oversightChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/); }); + it("renders the Priority dropdown chip like the Oversight dropdown chip, on every surface (FN-7597)", () => { + const css = readDashboardStylesSource(); + + const priorityChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip"); + const oversightChipBlock = getExactCssRuleBlock(css, ".detail-oversight-chip"); + const oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger"); + const prioritySelectBlock = getExactCssRuleBlock(css, ".detail-priority-select"); + const oversightSelectBlock = getExactCssRuleBlock(css, ".detail-oversight-select"); + const prioritySelectOptionBlock = getExactCssRuleBlock(css, ".detail-priority-select option"); + const oversightSelectOptionBlock = getExactCssRuleBlock(css, ".detail-oversight-select option"); + + // Same box size AND same border source for the desktop Priority chip vs. + // BOTH oversight surfaces (desktop chip and the mobile overflow trigger). + for (const block of [priorityChipBlock, oversightChipBlock, oversightTriggerBlock]) { + expect(block).toContain("min-height: var(--detail-priority-control-min-height);"); + expect(block).toContain("border-width: var(--btn-border-width);"); + expect(block).toContain("border-color: var(--border);"); + expect(block).toContain("border-radius: var(--detail-control-border-radius);"); + expect(block).toContain("box-sizing: border-box;"); + } + + // Same select typography: neither select force-uppercases its own text + // or options; both rely on the ancestor chip label's uppercase transform, + // so a regression re-adding a Priority-only override fails this. + expect(prioritySelectBlock).not.toMatch(/text-transform\s*:/); + expect(oversightSelectBlock).not.toMatch(/text-transform\s*:/); + expect(prioritySelectOptionBlock).not.toMatch(/text-transform\s*:/); + expect(oversightSelectOptionBlock).not.toMatch(/text-transform\s*:/); + expect(prioritySelectBlock).toContain("font: inherit;"); + expect(oversightSelectBlock).toContain("font: inherit;"); + + // The untinted `normal` priority level must resolve a real, non-transparent + // neutral chip background (not a borderless/background-less shell), just + // like the Oversight chip's neutral `--off` background. + const priorityNormalBlock = getExactCssRuleBlock(css, ".detail-priority-chip.card-priority-badge--normal"); + const oversightOffBlock = getExactCssRuleBlock(css, ".card-oversight-badge--off"); + expect(priorityNormalBlock).toMatch(/background:\s*color-mix\(in srgb, var\(--text-muted\)/); + expect(oversightOffBlock).toMatch(/background:\s*color-mix\(in srgb, var\(--text-muted\)/); + + // The semantic priority tints (info/warning/error family) must survive — + // this task must not flatten low/high/urgent to the same neutral tone. + expect(css).toMatch(/\.card-priority-badge--low\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--color-info\)/); + expect(css).toMatch(/\.card-priority-badge--high\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--color-warning\)/); + expect(css).toMatch(/\.card-priority-badge--urgent\s*\{[^}]*background:\s*color-mix\(in srgb, var\(--color-error\)/); + + // `--saving` only dims opacity; it must never change box size/border. + const prioritySavingBlock = getExactCssRuleBlock(css, ".detail-priority-chip--saving"); + expect(prioritySavingBlock.replace(/\s+/g, "")).toBe("opacity:0.75;"); + expect(prioritySavingBlock).not.toMatch(/border|min-height|padding/); + }); + it("keeps grouped timestamp metadata inline on desktop and mobile", () => { const css = readDashboardStylesSource(); From e0f3d3d14c6529615cd7e92402c5c586b2787239 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 16:58:42 -0700 Subject: [PATCH 12/24] FN-7599: rename triage column label to Planning in default workflows Renames the default-workflow intake column's display label from "Triage" to "Planning" across the built-in coding, stepwise-coding, and PR workflows, while keeping the column id as `triage` for lifecycle/DB/type stability. - builtin-coding-workflow-ir.ts: intake column name "Triage" -> "Planning" - builtin-pr-workflow-ir.ts: intake column name "Triage" -> "Planning" - builtin-stepwise-coding-workflow-ir.ts: intake column name "Triage" -> "Planning" - Added regression tests asserting the intake column is labeled "Planning" with id "triage" in builtin-coding and hand-authored default workflows (stepwise-coding, pr-workflow) - Added changeset (patch) documenting the label change for @runfusion/fusion Files changed: .changeset/fn-7599-planning-column-rename.md | 7 +++++++ .../core/src/__tests__/builtin-coding-workflow-ir.test.ts | 7 +++++++ packages/core/src/__tests__/builtin-workflows.test.ts | 12 ++++++++++++ packages/core/src/builtin-coding-workflow-ir.ts | 3 ++- packages/core/src/builtin-pr-workflow-ir.ts | 2 +- packages/core/src/builtin-stepwise-coding-workflow-ir.ts | 2 +- 6 files changed, 30 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-7599 Fusion-Task-Lineage: 5de8abc4-2407-4f9a-b97c-bd5b900d8fd9 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7599-planning-column-rename.md | 7 +++++++ .../src/__tests__/builtin-coding-workflow-ir.test.ts | 7 +++++++ .../core/src/__tests__/builtin-workflows.test.ts | 12 ++++++++++++ packages/core/src/builtin-coding-workflow-ir.ts | 3 ++- packages/core/src/builtin-pr-workflow-ir.ts | 2 +- .../core/src/builtin-stepwise-coding-workflow-ir.ts | 2 +- 6 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-7599-planning-column-rename.md diff --git a/.changeset/fn-7599-planning-column-rename.md b/.changeset/fn-7599-planning-column-rename.md new file mode 100644 index 0000000000..59cac03411 --- /dev/null +++ b/.changeset/fn-7599-planning-column-rename.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Default workflow boards now label the intake column "Planning" instead of "Triage". +category: fix +dev: Renamed the `name` of the `id: "triage"` intake column to "Planning" in builtin-coding, builtin-stepwise-coding, and builtin-pr workflow IRs (column id unchanged; linear built-ins inherit via canonicalBuiltinWorkflowColumns). COLUMN_LABELS.triage was already "Planning". diff --git a/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts b/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts index 5aacff6035..cfd6b91ecb 100644 --- a/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts +++ b/packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts @@ -107,6 +107,13 @@ describe("builtin coding workflow ir", () => { expect(ids).toEqual(["triage", "todo", "in-progress", "in-review", "done", "archived"]); }); + // FNXC:Workflows 2026-07-05-00:00: FN-7599 — the intake column displays as "Planning" while its id stays "triage" for lifecycle/DB/type stability. + it("labels the intake column 'Planning' while keeping the 'triage' id (FN-7599)", () => { + if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") throw new Error("expected v2"); + const byId = new Map(BUILTIN_CODING_WORKFLOW_IR.columns.map((c) => [c.id, c])); + expect(byId.get("triage")).toEqual({ id: "triage", name: "Planning", traits: [{ trait: "intake" }] }); + }); + it("maps default-workflow traits to columns verbatim (R12)", () => { if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") throw new Error("expected v2"); const byId = new Map(BUILTIN_CODING_WORKFLOW_IR.columns.map((c) => [c.id, c])); diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index 0fbcff0039..cdbf473a0b 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -10,6 +10,7 @@ import { } from "../builtin-workflows.js"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "../builtin-stepwise-coding-workflow-ir.js"; +import { BUILTIN_PR_WORKFLOW_IR } from "../builtin-pr-workflow-ir.js"; import { BROWSER_VERIFICATION_GROUP_ID, BROWSER_VERIFICATION_STEP_NODE_ID } from "../builtin-browser-verification-group.js"; import { CODE_REVIEW_STEP_NODE_ID } from "../builtin-code-review-group.js"; import { PLAN_REVIEW_GROUP_ID, PLAN_REVIEW_STEP_NODE_ID } from "../builtin-plan-review-group.js"; @@ -417,6 +418,17 @@ describe("built-in workflows", () => { ]); }); + // FNXC:Workflows 2026-07-05-00:00: FN-7599 — hand-authored default workflows (stepwise-coding, pr-workflow) + // must also label the intake column "Planning" while keeping the "triage" id, matching builtin-coding. + it("hand-authored default workflows label the intake column 'Planning' (FN-7599)", () => { + for (const ir of [BUILTIN_STEPWISE_CODING_WORKFLOW_IR, BUILTIN_PR_WORKFLOW_IR]) { + expect(ir.version).toBe("v2"); + if (ir.version !== "v2") throw new Error("expected v2"); + const triageColumn = ir.columns.find((column) => column.id === "triage"); + expect(triageColumn).toEqual({ id: "triage", name: "Planning", traits: [{ trait: "intake" }] }); + } + }); + it("builtin:coding catalog entry is backed by the stepwise final-review IR", () => { const coding = getBuiltinWorkflow("builtin:coding"); expect(coding).toBeDefined(); diff --git a/packages/core/src/builtin-coding-workflow-ir.ts b/packages/core/src/builtin-coding-workflow-ir.ts index 19c1ab10f1..0e6f678a7d 100644 --- a/packages/core/src/builtin-coding-workflow-ir.ts +++ b/packages/core/src/builtin-coding-workflow-ir.ts @@ -45,7 +45,8 @@ const RAW_BUILTIN_CODING_WORKFLOW_IR: WorkflowIr = { version: "v2", name: "builtin-coding-workflow", columns: [ - { id: "triage", name: "Triage", traits: [{ trait: "intake" }] }, + // FNXC:Workflows 2026-07-05-00:00: Default-workflow intake column now displays as "Planning" while keeping the `triage` id for lifecycle/DB/type stability (FN-7599). + { id: "triage", name: "Planning", traits: [{ trait: "intake" }] }, { id: "todo", name: "Todo", diff --git a/packages/core/src/builtin-pr-workflow-ir.ts b/packages/core/src/builtin-pr-workflow-ir.ts index 136ac7b422..71552b1012 100644 --- a/packages/core/src/builtin-pr-workflow-ir.ts +++ b/packages/core/src/builtin-pr-workflow-ir.ts @@ -60,7 +60,7 @@ const RAW_BUILTIN_PR_WORKFLOW_IR: WorkflowIr = { version: "v2", name: "builtin-pr", columns: [ - { id: "triage", name: "Triage", traits: [{ trait: "intake" }] }, + { id: "triage", name: "Planning", traits: [{ trait: "intake" }] }, { id: "in-progress", name: "In progress", diff --git a/packages/core/src/builtin-stepwise-coding-workflow-ir.ts b/packages/core/src/builtin-stepwise-coding-workflow-ir.ts index 730d17eba8..2f93c37090 100644 --- a/packages/core/src/builtin-stepwise-coding-workflow-ir.ts +++ b/packages/core/src/builtin-stepwise-coding-workflow-ir.ts @@ -60,7 +60,7 @@ const RAW_BUILTIN_STEPWISE_CODING_WORKFLOW_IR: WorkflowIr = { version: "v2", name: "builtin-stepwise-coding", columns: [ - { id: "triage", name: "Triage", traits: [{ trait: "intake" }] }, + { id: "triage", name: "Planning", traits: [{ trait: "intake" }] }, { id: "todo", name: "Todo", From 5b193d2d08ed9719e5435c176d2be0b3a4b7ee2e Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 17:03:56 -0700 Subject: [PATCH 13/24] FN-7600: fix Nudge control stuck on periodic-observation copy when overseer is active Attach the transient plannerOverseerState snapshot to the single-task detail route so the Nudge control reflects live overseer observation instead of always showing the periodic-observation message. - GET /api/tasks/:id now best-effort attaches plannerOverseerState (mirrors the list route), never throwing on enrichment failure. - TaskDetailModal reads overseerSnapshot from workingTask (merged full-detail object) instead of the raw task prop, so detail refetches via fetchTaskDetail (dependency chips, Documents view, logs, post-open refetch) no longer drop the snapshot. - Added regression tests for the detail-route enrichment and the modal's Nudge-availability behavior. - Added a patch changeset documenting the fix. Files changed: .changeset/fn-7600-oversight-nudge-detail-snapshot.md | 7 ++ packages/dashboard/app/components/TaskDetailModal.tsx | 14 ++- .../TaskDetailModal.oversight-controls.test.tsx | 131 +++++++++++++++++++++ .../__tests__/tasks-planner-overseer-state.test.ts | 95 +++++++++++++++ packages/dashboard/src/routes/register-task-workflow-routes.ts | 25 +++- 5 files changed, 269 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-7600 Fusion-Task-Lineage: 500614d0-091a-461c-8e7b-329a7b791502 Co-authored-by: Fusion (runfusion.ai) --- ...fn-7600-oversight-nudge-detail-snapshot.md | 7 + .../app/components/TaskDetailModal.tsx | 14 +- ...askDetailModal.oversight-controls.test.tsx | 131 ++++++++++++++++++ .../tasks-planner-overseer-state.test.ts | 95 +++++++++++++ .../routes/register-task-workflow-routes.ts | 25 +++- 5 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-7600-oversight-nudge-detail-snapshot.md diff --git a/.changeset/fn-7600-oversight-nudge-detail-snapshot.md b/.changeset/fn-7600-oversight-nudge-detail-snapshot.md new file mode 100644 index 0000000000..2690189613 --- /dev/null +++ b/.changeset/fn-7600-oversight-nudge-detail-snapshot.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the task-detail Nudge control staying disabled when the overseer is actively watching. +category: fix +dev: GET /api/tasks/:id now attaches the transient plannerOverseerState snapshot (mirrors the list route); TaskDetailModal reads the snapshot from workingTask so detail refetches no longer drop it. diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 010c127e3b..8f2b73d279 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -3183,7 +3183,19 @@ export function TaskDetailContent({ copy-only, selected via the already-computed `overseerHumanControlSuppressed` / `overseerActive` booleans below. */ - const overseerSnapshot = task.plannerOverseerState ?? null; + /* + FNXC:PlannerOversight 2026-07-05-00:00: + FN-7600: this used to read `task.plannerOverseerState` — the transient + snapshot enrichment from `GET /api/tasks` (list) — but the modal is + frequently opened via `fetchTaskDetail` (dependency chips, Documents view, + logs, or the post-open detail refetch) where the parent `task` prop never + carries the snapshot, so `overseerActive`/`canNudgeOverseer` were almost + always false and Nudge showed the periodic-observation copy even while the + overseer was actively watching. `GET /api/tasks/:id` now attaches the same + snapshot (mirrors the list route), so read it from `workingTask` — the + full-detail-backed merged object — instead of the raw prop. + */ + const overseerSnapshot = workingTask.plannerOverseerState ?? null; const overseerActive = Boolean(overseerSnapshot); const isDoneOrArchivedColumn = task.column === "done" || task.column === "archived"; const isOverseerHumanReviewTerminal = task.column === "in-review" && !effectiveAutoMerge; diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx index 768ed722a7..18bf22f1ad 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx @@ -407,6 +407,137 @@ describe("TaskDetailModal oversight controls", () => { }); }); +/* +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7600 regression coverage: the modal previously read `overseerSnapshot` from +the raw `task` prop, which loses the snapshot whenever the modal is opened via +`fetchTaskDetail` (dependency chips, Documents view, logs) because those call +sites pass a slim `Task` (no `prompt` key) that never carries +`plannerOverseerState` — only the full-detail fetch response does. These +tests reproduce that exact path: a slim task prop with NO snapshot, plus a +mocked `fetchTaskDetail` resolving a full TaskDetail WITH an active snapshot, +and assert Nudge enables (helper absent) once the fetched detail lands — at +both the desktop inline site and the mobile overflow-menu site. +*/ +describe("TaskDetailModal oversight controls — snapshot delivered via fetched full detail (FN-7600)", () => { + const originalInnerWidth = window.innerWidth; + + beforeEach(async () => { + vi.clearAllMocks(); + mockConfirm.mockResolvedValue(true); + const api = await import("../../api"); + vi.mocked(api.fetchBoardWorkflows).mockResolvedValue({ flagEnabled: false, defaultWorkflowId: "", workflows: [], taskWorkflowIds: {} }); + vi.mocked(api.fetchWorkflowSettingValues).mockResolvedValue({ stored: {}, effective: {}, defaults: {} }); + vi.mocked(api.nudgeOverseer).mockResolvedValue({ applied: false, reason: "oversight-off" }); + vi.mocked(api.stopOverseer).mockResolvedValue({ applied: true, reason: "stopped" }); + vi.mocked(api.explainOverseer).mockResolvedValue({ snapshot: null }); + }); + + afterEach(() => { + Object.defineProperty(window, "innerWidth", { value: originalInnerWidth, configurable: true }); + }); + + function makeSlimTaskWithoutSnapshot(overrides: Record = {}) { + // Omit `prompt`/`log`/`steps` so the modal treats this as a slim `Task` + // (not a `TaskDetail`) and triggers the `fetchTaskDetail` fetch-on-open + // path instead of using the prop directly as `fullDetail`. + const { prompt: _prompt, log: _log, steps: _steps, plannerOverseerState: _snap, ...task } = makeTask({ + id: "FN-220", + column: "in-progress", + plannerOversightLevel: "autonomous", + ...overrides, + }); + return task; + } + + it("desktop: enables Nudge and hides the disabled-reason helper once the fetched full detail carries an active snapshot", async () => { + const api = await import("../../api"); + vi.mocked(api.fetchTaskDetail).mockResolvedValueOnce(makeTask({ + id: "FN-220", + column: "in-progress", + plannerOversightLevel: "autonomous", + plannerOverseerState: activeSnapshot, + })); + + render( + , + ); + + const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); + await waitFor(() => { + expect(nudgeBtn).not.toBeDisabled(); + }); + expect(screen.queryByTestId("detail-overseer-nudge-disabled-reason")).not.toBeInTheDocument(); + }); + + it("desktop: still shows the periodic-observation copy while the fetched full detail carries no snapshot", async () => { + const api = await import("../../api"); + vi.mocked(api.fetchTaskDetail).mockResolvedValueOnce(makeTask({ + id: "FN-221", + column: "in-progress", + plannerOversightLevel: "autonomous", + })); + + render( + , + ); + + const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); + expect(nudgeBtn).toBeDisabled(); + const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason"); + expect(reason).toHaveTextContent("Nudge becomes available once the overseer is observing this task's current stage"); + }); + + it("mobile: enables Nudge and hides the disabled-reason helper behind the overflow menu once the fetched full detail carries an active snapshot", async () => { + Object.defineProperty(window, "innerWidth", { value: 375, configurable: true }); + + const api = await import("../../api"); + vi.mocked(api.fetchTaskDetail).mockResolvedValueOnce(makeTask({ + id: "FN-222", + column: "in-progress", + plannerOversightLevel: "autonomous", + plannerOverseerState: activeSnapshot, + })); + + render( + , + ); + + const trigger = await screen.findByTestId("detail-oversight-menu-trigger"); + fireEvent.click(trigger); + + const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); + await waitFor(() => { + expect(nudgeBtn).not.toBeDisabled(); + }); + expect(screen.queryByTestId("detail-overseer-nudge-disabled-reason")).not.toBeInTheDocument(); + }); +}); + /* * FNXC:PlannerOversight 2026-07-04-20:30 (FN-7558): * FN-7521's original mobile suite asserted the oversight quick-controls diff --git a/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts b/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts index f401b54d33..468a590fca 100644 --- a/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts +++ b/packages/dashboard/src/routes/__tests__/tasks-planner-overseer-state.test.ts @@ -112,3 +112,98 @@ describe("GET /tasks — plannerOverseerState enrichment", () => { expect(found && "plannerOverseerState" in found).toBe(false); }); }); + +// FN-7600: GET /tasks/:id (detail route) must attach the same transient +// `plannerOverseerState` snapshot as the list route above — the Task Detail +// modal's Overseer/Nudge controls read the snapshot from the full-detail +// payload, not the list payload, so the detail route previously never +// carried it and Nudge always showed the periodic-observation disabled copy. +describe("GET /tasks/:id — plannerOverseerState enrichment", () => { + let store: TaskStore; + let rootDir: string; + let globalDir: string; + + beforeEach(async () => { + rootDir = mkdtempSync(join(tmpdir(), "planner-overseer-state-detail-root-")); + globalDir = mkdtempSync(join(tmpdir(), "planner-overseer-state-detail-global-")); + store = new TaskStore(rootDir, globalDir, { inMemoryDb: true }); + await store.init(); + }); + + afterEach(() => { + store.close(); + rmSync(rootDir, { recursive: true, force: true }); + rmSync(globalDir, { recursive: true, force: true }); + }); + + function buildApp(engine: Partial | undefined): express.Express { + const app = express(); + app.use(express.json()); + app.use("/api", createApiRoutes(store, engine ? { engine: engine as unknown as ProjectEngine } : undefined)); + return app; + } + + it("attaches plannerOverseerState when the engine snapshot accessor returns a snapshot", async () => { + const task = await store.createTask({ description: "watched task" }); + + const snapshot = { + state: "watching" as const, + oversightLevel: "autonomous" as const, + watchedStage: "executor", + signal: "progressing", + attemptCount: 0, + attemptLimit: 3, + pendingConfirmation: false, + observedAt: 1700000000000, + }; + + const engineStub: Partial = { + getTaskStore: () => store, + getPlannerOverseerRuntimeSnapshot: (taskId: string) => (taskId === task.id ? snapshot : null), + }; + + const app = buildApp(engineStub); + const res = await REQUEST(app, "GET", `/api/tasks/${task.id}`); + expect(res.status).toBe(200); + expect((res.body as Record).plannerOverseerState).toEqual(snapshot); + }); + + it("omits plannerOverseerState entirely (no key) when the accessor returns null", async () => { + const task = await store.createTask({ description: "idle task" }); + + const engineStub: Partial = { + getTaskStore: () => store, + getPlannerOverseerRuntimeSnapshot: () => null, + }; + + const app = buildApp(engineStub); + const res = await REQUEST(app, "GET", `/api/tasks/${task.id}`); + expect(res.status).toBe(200); + expect("plannerOverseerState" in (res.body as Record)).toBe(false); + }); + + it("returns 200 with the un-enriched task when the accessor throws (detail load never fails)", async () => { + const task = await store.createTask({ description: "throwing task" }); + + const engineStub: Partial = { + getTaskStore: () => store, + getPlannerOverseerRuntimeSnapshot: () => { + throw new Error("boom"); + }, + }; + + const app = buildApp(engineStub); + const res = await REQUEST(app, "GET", `/api/tasks/${task.id}`); + expect(res.status).toBe(200); + expect("plannerOverseerState" in (res.body as Record)).toBe(false); + }); + + it("returns 200 with the un-enriched task when no engine is present at all", async () => { + const task = await store.createTask({ description: "no engine task" }); + + const app = buildApp(undefined); + const res = await REQUEST(app, "GET", `/api/tasks/${task.id}`); + expect(res.status).toBe(200); + expect("plannerOverseerState" in (res.body as Record)).toBe(false); + }); +}); diff --git a/packages/dashboard/src/routes/register-task-workflow-routes.ts b/packages/dashboard/src/routes/register-task-workflow-routes.ts index 6884cd5130..e4b93c4206 100644 --- a/packages/dashboard/src/routes/register-task-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-task-workflow-routes.ts @@ -2933,11 +2933,32 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork // Get single task with prompt content router.get("/tasks/:id", async (req, res) => { try { - const { store: scopedStore } = await getProjectContext(req); + const { store: scopedStore, engine } = await getProjectContext(req); const task = await scopedStore.getTask(req.params.id, { activityLogLimit: TASK_DETAIL_ACTIVITY_LOG_LIMIT, }); - res.json(trimTaskDetailActivityLog(task)); + let enrichedTask = task; + // FNXC:PlannerOversight 2026-07-05-00:00: + // FN-7600: the Task Detail modal's Overseer/Nudge controls read + // `plannerOverseerState` from the merged full-detail object, but this + // detail route previously never attached it (only the list route did, + // per FN-7531 above) — so opening the modal via fetchTaskDetail + // (dependency chips, Documents view, logs, or the post-open detail + // refetch) always lost the snapshot and the Nudge button showed the + // periodic-observation disabled copy even when the overseer was + // actively watching. Mirror the list-route contract exactly: best- + // effort, never throws, and omits the key (not `null`) when the + // accessor returns no active observation. + try { + const plannerOverseerState = engine?.getPlannerOverseerRuntimeSnapshot(task.id); + if (plannerOverseerState) { + enrichedTask = { ...task, plannerOverseerState }; + } + } catch { + // Planner-overseer-state enrichment is best-effort and must never + // fail the task-detail load — fall through with the un-enriched task. + } + res.json(trimTaskDetailActivityLog(enrichedTask)); } catch (err: unknown) { if (err instanceof ApiError) { throw err; From 2025f9d56d04cf63f6f7441ca8971ae1b2690bd2 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 17:13:36 -0700 Subject: [PATCH 14/24] chore(release): v0.56.0 Version bump via changesets. --- .../FN-7491-triage-splitting-setting.md | 7 - .../FN-7492-task-card-plan-review-progress.md | 7 - .changeset/FN-7541-remove-chat-eye-icon.md | 7 - .../FN-7546-oversight-controls-clarity.md | 7 - ...unify-task-detail-quick-control-styling.md | 7 - .changeset/activity-menu-ios.md | 7 - ...x-anthropic-subscription-fragment-login.md | 7 - ...ropic-subscription-relogin-after-logout.md | 7 - ...tom-executor-kills-live-ephemeral-tasks.md | 7 - .../fix-windows-terminal-worktrunk-popup.md | 7 - .../fn-7468-onboarding-quick-providers.md | 7 - ...n-7469-select-created-project-directory.md | 7 - .changeset/fn-7470-git-onboarding.md | 7 - .changeset/fn-7471-desktop-update-version.md | 7 - .../fn-7472-windows-desktop-close-quits.md | 7 - ...fn-7473-desktop-anthropic-oauth-browser.md | 7 - .../fn-7474-github-onboarding-actions.md | 7 - .changeset/fn-7475-github-setup-warning.md | 7 - .changeset/fn-7476-desktop-engine-banner.md | 7 - .../fn-7477-connection-manager-clarity.md | 7 - .../fn-7478-local-server-switch-option.md | 7 - .changeset/fn-7479-right-dock-task-popup.md | 7 - .changeset/fn-7482-code-review-remediation.md | 7 - .../fn-7486-merge-recovery-noop-ownership.md | 7 - .changeset/fn-7488-source-free-completion.md | 7 - .changeset/fn-7490-push-to-remote-setting.md | 7 - .changeset/fn-7493-task-popup-layer.md | 7 - .changeset/fn-7494-keyboard-shortcuts.md | 7 - .changeset/fn-7495-settings-search.md | 7 - .../fn-7497-chat-first-event-timeout.md | 7 - .changeset/fn-7498-original-task-prompt.md | 7 - .../fn-7499-before-after-transformation.md | 7 - .changeset/fn-7500-terminal-shortcuts.md | 7 - .changeset/fn-7502-terminal-below-layout.md | 7 - .changeset/fn-7503-agent-log-timing.md | 7 - .../fn-7504-mobile-chat-composer-keyboard.md | 7 - .../fn-7505-settings-default-descriptions.md | 7 - .changeset/fn-7506-settings-reset.md | 7 - ...fn-7508-planner-oversight-level-setting.md | 7 - ...509-per-task-planner-oversight-override.md | 7 - .changeset/fn-7510-oversight-default.md | 7 - .../fn-7511-planner-overseer-monitoring.md | 7 - .../fn-7512-planner-bounded-recovery.md | 7 - .../fn-7513-planner-confirmation-gate.md | 7 - .../fn-7514-overseer-human-control-guard.md | 7 - ...-7515-planner-oversight-config-exposure.md | 7 - ...n-7518-oversight-notification-verbosity.md | 7 - .../fn-7519-planner-intervention-timeline.md | 7 - .changeset/fn-7520-planner-overseer-events.md | 7 - .changeset/fn-7523-task-revert.md | 7 - .changeset/fn-7524-ai-undo-revert.md | 7 - .changeset/fn-7525-revert-card-affordance.md | 7 - .changeset/fn-7526-plan-auto-approve.md | 7 - ...n-7527-desktop-switch-server-navigation.md | 7 - .../fn-7528-task-performance-capture.md | 7 - ...fn-7531-planner-overseer-state-exposure.md | 7 - .changeset/fn-7532-branch-group-completion.md | 7 - .../fn-7534-branch-group-archived-member.md | 7 - .../fn-7535-global-gitlab-setting-save.md | 7 - .../fn-7536-activity-dropdown-mobile.md | 7 - .../fn-7537-backup-automation-manual-run.md | 7 - .changeset/fn-7539-oversight-badge-default.md | 7 - .../fn-7542-remove-overseer-state-badge.md | 7 - ...7543-original-prompt-markdown-collapsed.md | 7 - ...44-artifact-cross-instance-live-refresh.md | 7 - .changeset/fn-7547-workspace-task-revert.md | 7 - .../fn-7548-per-sha-revert-granularity.md | 7 - .../fn-7550-terminal-shortcut-scroll.md | 7 - .../fn-7551-overseer-timeline-wiring.md | 7 - ...552-mobile-authentication-global-prefix.md | 7 - .../fn-7553-keyboard-shortcuts-section.md | 7 - .changeset/fn-7554-pr-based-revert.md | 7 - .changeset/fn-7556-ai-undo-workflow.md | 7 - .../fn-7557-plan-auto-approve-default.md | 7 - .../fn-7559-approval-gate-disambiguation.md | 7 - .changeset/fn-7560-mobile-terminal-footer.md | 7 - ...-release-gate-disclaimer-false-positive.md | 7 - .changeset/fn-7561-mobile-terminal-spacing.md | 7 - .changeset/fn-7561-plan-review-replan-loop.md | 7 - .../fn-7563-overseer-badge-explanation.md | 7 - ...pprove-plan-release-authorization-guard.md | 7 - .../fn-7565-mobile-terminal-close-corner.md | 7 - .changeset/fn-7567-mobile-terminal-spacing.md | 7 - .changeset/fn-7568-fn-cli-release-asset.md | 7 - .../fn-7569-plan-approval-idempotent.md | 7 - ...intervention-timeline-activity-dropdown.md | 7 - .../fn-7574-oauth-expiry-detection-refresh.md | 7 - .changeset/fn-7575-release-version-comment.md | 7 - ...apper-subscription-getapikey-delegation.md | 7 - .changeset/fn-7577-workspace-pr-revert.md | 7 - .../fn-7578-ai-undo-workflow-setting-ui.md | 7 - .../fn-7579-ask-user-exit-gate-nodes.md | 7 - .../fn-7579-tracking-dedup-stale-issue.md | 7 - .../fn-7582-oversight-guideline-copy.md | 7 - .changeset/fn-7584-brainstorming-builtin.md | 7 - .changeset/fn-7591-coding-ideas-intake.md | 7 - .changeset/fn-7591-intake-card-disappears.md | 7 - .changeset/fn-7593-before-after-top.md | 7 - ...597-priority-dropdown-matches-oversight.md | 7 - .changeset/fn-7599-planning-column-rename.md | 7 - ...fn-7600-oversight-nudge-detail-snapshot.md | 7 - .changeset/fn-coding-ideas-workflow.md | 7 - ...e-branch-strategy-and-parked-validation.md | 7 - ...anner-overseer-no-recover-healthy-tasks.md | 7 - CHANGELOG.md | 417 ++++++++++++++++-- package.json | 2 +- packages/cli-alias/CHANGELOG.md | 110 +++++ packages/cli-alias/package.json | 2 +- packages/cli/CHANGELOG.md | 320 ++++++++++++++ packages/cli/package.json | 2 +- packages/core/CHANGELOG.md | 2 + packages/core/package.json | 2 +- packages/dashboard/CHANGELOG.md | 17 + packages/dashboard/package.json | 2 +- packages/desktop/CHANGELOG.md | 8 + packages/desktop/package.json | 2 +- packages/droid-cli/CHANGELOG.md | 6 + packages/droid-cli/package.json | 2 +- packages/engine/CHANGELOG.md | 7 + packages/engine/package.json | 2 +- packages/i18n/CHANGELOG.md | 6 + packages/i18n/package.json | 2 +- packages/mobile/CHANGELOG.md | 2 + packages/mobile/package.json | 2 +- packages/pi-claude-cli/CHANGELOG.md | 2 + packages/pi-claude-cli/package.json | 2 +- packages/plugin-sdk/CHANGELOG.md | 6 + packages/plugin-sdk/package.json | 2 +- .../fusion-plugin-auto-label/CHANGELOG.md | 6 + .../fusion-plugin-auto-label/package.json | 2 +- .../fusion-plugin-ci-status/CHANGELOG.md | 6 + .../fusion-plugin-ci-status/package.json | 2 +- .../fusion-plugin-notification/CHANGELOG.md | 6 + .../fusion-plugin-notification/package.json | 2 +- .../fusion-plugin-settings-demo/CHANGELOG.md | 6 + .../fusion-plugin-settings-demo/package.json | 2 +- .../fusion-plugin-acp-runtime/CHANGELOG.md | 7 + .../fusion-plugin-acp-runtime/package.json | 2 +- .../fusion-plugin-agent-browser/CHANGELOG.md | 6 + .../fusion-plugin-agent-browser/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../fusion-plugin-cursor-runtime/CHANGELOG.md | 6 + .../fusion-plugin-cursor-runtime/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../fusion-plugin-droid-runtime/CHANGELOG.md | 6 + .../fusion-plugin-droid-runtime/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../fusion-plugin-hermes-runtime/CHANGELOG.md | 6 + .../fusion-plugin-hermes-runtime/package.json | 2 +- .../fusion-plugin-linear-import/CHANGELOG.md | 7 + .../fusion-plugin-linear-import/package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- plugins/fusion-plugin-reports/CHANGELOG.md | 8 + plugins/fusion-plugin-reports/package.json | 2 +- plugins/fusion-plugin-roadmap/CHANGELOG.md | 7 + plugins/fusion-plugin-roadmap/package.json | 2 +- .../fusion-plugin-whatsapp-chat/CHANGELOG.md | 6 + .../fusion-plugin-whatsapp-chat/package.json | 2 +- 166 files changed, 1009 insertions(+), 807 deletions(-) delete mode 100644 .changeset/FN-7491-triage-splitting-setting.md delete mode 100644 .changeset/FN-7492-task-card-plan-review-progress.md delete mode 100644 .changeset/FN-7541-remove-chat-eye-icon.md delete mode 100644 .changeset/FN-7546-oversight-controls-clarity.md delete mode 100644 .changeset/FN-7585-unify-task-detail-quick-control-styling.md delete mode 100644 .changeset/activity-menu-ios.md delete mode 100644 .changeset/fix-anthropic-subscription-fragment-login.md delete mode 100644 .changeset/fix-anthropic-subscription-relogin-after-logout.md delete mode 100644 .changeset/fix-phantom-executor-kills-live-ephemeral-tasks.md delete mode 100644 .changeset/fix-windows-terminal-worktrunk-popup.md delete mode 100644 .changeset/fn-7468-onboarding-quick-providers.md delete mode 100644 .changeset/fn-7469-select-created-project-directory.md delete mode 100644 .changeset/fn-7470-git-onboarding.md delete mode 100644 .changeset/fn-7471-desktop-update-version.md delete mode 100644 .changeset/fn-7472-windows-desktop-close-quits.md delete mode 100644 .changeset/fn-7473-desktop-anthropic-oauth-browser.md delete mode 100644 .changeset/fn-7474-github-onboarding-actions.md delete mode 100644 .changeset/fn-7475-github-setup-warning.md delete mode 100644 .changeset/fn-7476-desktop-engine-banner.md delete mode 100644 .changeset/fn-7477-connection-manager-clarity.md delete mode 100644 .changeset/fn-7478-local-server-switch-option.md delete mode 100644 .changeset/fn-7479-right-dock-task-popup.md delete mode 100644 .changeset/fn-7482-code-review-remediation.md delete mode 100644 .changeset/fn-7486-merge-recovery-noop-ownership.md delete mode 100644 .changeset/fn-7488-source-free-completion.md delete mode 100644 .changeset/fn-7490-push-to-remote-setting.md delete mode 100644 .changeset/fn-7493-task-popup-layer.md delete mode 100644 .changeset/fn-7494-keyboard-shortcuts.md delete mode 100644 .changeset/fn-7495-settings-search.md delete mode 100644 .changeset/fn-7497-chat-first-event-timeout.md delete mode 100644 .changeset/fn-7498-original-task-prompt.md delete mode 100644 .changeset/fn-7499-before-after-transformation.md delete mode 100644 .changeset/fn-7500-terminal-shortcuts.md delete mode 100644 .changeset/fn-7502-terminal-below-layout.md delete mode 100644 .changeset/fn-7503-agent-log-timing.md delete mode 100644 .changeset/fn-7504-mobile-chat-composer-keyboard.md delete mode 100644 .changeset/fn-7505-settings-default-descriptions.md delete mode 100644 .changeset/fn-7506-settings-reset.md delete mode 100644 .changeset/fn-7508-planner-oversight-level-setting.md delete mode 100644 .changeset/fn-7509-per-task-planner-oversight-override.md delete mode 100644 .changeset/fn-7510-oversight-default.md delete mode 100644 .changeset/fn-7511-planner-overseer-monitoring.md delete mode 100644 .changeset/fn-7512-planner-bounded-recovery.md delete mode 100644 .changeset/fn-7513-planner-confirmation-gate.md delete mode 100644 .changeset/fn-7514-overseer-human-control-guard.md delete mode 100644 .changeset/fn-7515-planner-oversight-config-exposure.md delete mode 100644 .changeset/fn-7518-oversight-notification-verbosity.md delete mode 100644 .changeset/fn-7519-planner-intervention-timeline.md delete mode 100644 .changeset/fn-7520-planner-overseer-events.md delete mode 100644 .changeset/fn-7523-task-revert.md delete mode 100644 .changeset/fn-7524-ai-undo-revert.md delete mode 100644 .changeset/fn-7525-revert-card-affordance.md delete mode 100644 .changeset/fn-7526-plan-auto-approve.md delete mode 100644 .changeset/fn-7527-desktop-switch-server-navigation.md delete mode 100644 .changeset/fn-7528-task-performance-capture.md delete mode 100644 .changeset/fn-7531-planner-overseer-state-exposure.md delete mode 100644 .changeset/fn-7532-branch-group-completion.md delete mode 100644 .changeset/fn-7534-branch-group-archived-member.md delete mode 100644 .changeset/fn-7535-global-gitlab-setting-save.md delete mode 100644 .changeset/fn-7536-activity-dropdown-mobile.md delete mode 100644 .changeset/fn-7537-backup-automation-manual-run.md delete mode 100644 .changeset/fn-7539-oversight-badge-default.md delete mode 100644 .changeset/fn-7542-remove-overseer-state-badge.md delete mode 100644 .changeset/fn-7543-original-prompt-markdown-collapsed.md delete mode 100644 .changeset/fn-7544-artifact-cross-instance-live-refresh.md delete mode 100644 .changeset/fn-7547-workspace-task-revert.md delete mode 100644 .changeset/fn-7548-per-sha-revert-granularity.md delete mode 100644 .changeset/fn-7550-terminal-shortcut-scroll.md delete mode 100644 .changeset/fn-7551-overseer-timeline-wiring.md delete mode 100644 .changeset/fn-7552-mobile-authentication-global-prefix.md delete mode 100644 .changeset/fn-7553-keyboard-shortcuts-section.md delete mode 100644 .changeset/fn-7554-pr-based-revert.md delete mode 100644 .changeset/fn-7556-ai-undo-workflow.md delete mode 100644 .changeset/fn-7557-plan-auto-approve-default.md delete mode 100644 .changeset/fn-7559-approval-gate-disambiguation.md delete mode 100644 .changeset/fn-7560-mobile-terminal-footer.md delete mode 100644 .changeset/fn-7560-release-gate-disclaimer-false-positive.md delete mode 100644 .changeset/fn-7561-mobile-terminal-spacing.md delete mode 100644 .changeset/fn-7561-plan-review-replan-loop.md delete mode 100644 .changeset/fn-7563-overseer-badge-explanation.md delete mode 100644 .changeset/fn-7564-approve-plan-release-authorization-guard.md delete mode 100644 .changeset/fn-7565-mobile-terminal-close-corner.md delete mode 100644 .changeset/fn-7567-mobile-terminal-spacing.md delete mode 100644 .changeset/fn-7568-fn-cli-release-asset.md delete mode 100644 .changeset/fn-7569-plan-approval-idempotent.md delete mode 100644 .changeset/fn-7571-intervention-timeline-activity-dropdown.md delete mode 100644 .changeset/fn-7574-oauth-expiry-detection-refresh.md delete mode 100644 .changeset/fn-7575-release-version-comment.md delete mode 100644 .changeset/fn-7576-cli-wrapper-subscription-getapikey-delegation.md delete mode 100644 .changeset/fn-7577-workspace-pr-revert.md delete mode 100644 .changeset/fn-7578-ai-undo-workflow-setting-ui.md delete mode 100644 .changeset/fn-7579-ask-user-exit-gate-nodes.md delete mode 100644 .changeset/fn-7579-tracking-dedup-stale-issue.md delete mode 100644 .changeset/fn-7582-oversight-guideline-copy.md delete mode 100644 .changeset/fn-7584-brainstorming-builtin.md delete mode 100644 .changeset/fn-7591-coding-ideas-intake.md delete mode 100644 .changeset/fn-7591-intake-card-disappears.md delete mode 100644 .changeset/fn-7593-before-after-top.md delete mode 100644 .changeset/fn-7597-priority-dropdown-matches-oversight.md delete mode 100644 .changeset/fn-7599-planning-column-rename.md delete mode 100644 .changeset/fn-7600-oversight-nudge-detail-snapshot.md delete mode 100644 .changeset/fn-coding-ideas-workflow.md delete mode 100644 .changeset/mission-triage-branch-strategy-and-parked-validation.md delete mode 100644 .changeset/planner-overseer-no-recover-healthy-tasks.md diff --git a/.changeset/FN-7491-triage-splitting-setting.md b/.changeset/FN-7491-triage-splitting-setting.md deleted file mode 100644 index a8a2494275..0000000000 --- a/.changeset/FN-7491-triage-splitting-setting.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Add a workflow setting to disable automatic large-task triage splitting. -category: feature -dev: Adds triageProactiveSubtaskSplittingEnabled while preserving explicit breakIntoSubtasks requests. diff --git a/.changeset/FN-7492-task-card-plan-review-progress.md b/.changeset/FN-7492-task-card-plan-review-progress.md deleted file mode 100644 index 9250f398c2..0000000000 --- a/.changeset/FN-7492-task-card-plan-review-progress.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Show active Plan Review progress on triage task cards. -category: fix -dev: TaskCard now renders the existing progress affordance for Triage only when unified progress has active workflow work. diff --git a/.changeset/FN-7541-remove-chat-eye-icon.md b/.changeset/FN-7541-remove-chat-eye-icon.md deleted file mode 100644 index abf2336a76..0000000000 --- a/.changeset/FN-7541-remove-chat-eye-icon.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Remove the eye icon markdown/plain toggle from chat; messages always render as Markdown. -category: breaking -dev: Removed ChatView `chat-thread-header-render-toggle` (desktop + mobile), `showAllAsPlain` state, and `chat.showRenderedMarkdown`/`chat.showPlainText` i18n keys (FN-7541). diff --git a/.changeset/FN-7546-oversight-controls-clarity.md b/.changeset/FN-7546-oversight-controls-clarity.md deleted file mode 100644 index 632089357e..0000000000 --- a/.changeset/FN-7546-oversight-controls-clarity.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Clarify task-detail oversight Nudge/Explain controls: visible label, disabled reason, always-openable Explain panel. -category: fix -dev: TaskDetailModal now renders a `detail-oversight-controls-label` group label and `detail-overseer-nudge-disabled-reason` helper text (both gated by the existing oversight-cluster visibility condition); Explain no longer disables on `!canExplainOverseer` since it is read-only. Nudge's `canNudgeOverseer` gate and Stop's confirm dialog are unchanged. diff --git a/.changeset/FN-7585-unify-task-detail-quick-control-styling.md b/.changeset/FN-7585-unify-task-detail-quick-control-styling.md deleted file mode 100644 index b536ea7b2a..0000000000 --- a/.changeset/FN-7585-unify-task-detail-quick-control-styling.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Unify border, radius, and height of the task-detail Priority/Execution/Oversight controls. -category: fix -dev: Adds a shared --detail-control-border-radius token alongside --detail-priority-control-min-height so .detail-priority-chip, .detail-execution-mode-toggle, .detail-oversight-chip, and .detail-oversight-menu-trigger all resolve the same border-width/color/radius/height. diff --git a/.changeset/activity-menu-ios.md b/.changeset/activity-menu-ios.md deleted file mode 100644 index f4a8c962c7..0000000000 --- a/.changeset/activity-menu-ios.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Keep the task-detail Activity view menu open during mobile iOS taps. -category: fix -dev: Guards the Activity views dropdown against iOS visualViewport resize/scroll echoes during menu opening. diff --git a/.changeset/fix-anthropic-subscription-fragment-login.md b/.changeset/fix-anthropic-subscription-fragment-login.md deleted file mode 100644 index 85d2c35fc6..0000000000 --- a/.changeset/fix-anthropic-subscription-fragment-login.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix Anthropic subscription login when pasted callback URLs contain fragment OAuth parameters. -category: fix -dev: Normalizes pasted OAuth callback fragments before resolving dashboard manual-code login prompts. diff --git a/.changeset/fix-anthropic-subscription-relogin-after-logout.md b/.changeset/fix-anthropic-subscription-relogin-after-logout.md deleted file mode 100644 index e6c3bb56b3..0000000000 --- a/.changeset/fix-anthropic-subscription-relogin-after-logout.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix Claude/Anthropic subscription re-login showing "Login did not complete" after logging out. -category: fix -dev: Anthropic subscription OAuth is aliased across the legacy `anthropic` row (where interactive login persists the credential) and the `anthropic-subscription` id (where the settings card's in-memory logged-out suppression and status read are keyed). Re-login wrote only `anthropic`, so `loggedOutProviders` kept suppressing `anthropic-subscription` and the card reported failure despite a valid stored credential until process restart. auth-storage's proxy now clears the logged-out state on both aliases when either is re-authenticated (new `login` trap + hardened `set` trap via `clearReauthenticatedLogoutState`; raw api_key writes stay scoped to their own card). Also surfaces background OAuth login failures on `GET /auth/status` (`loginError`) + server logs so future paste-callback failures are diagnosable instead of a generic error. diff --git a/.changeset/fix-phantom-executor-kills-live-ephemeral-tasks.md b/.changeset/fix-phantom-executor-kills-live-ephemeral-tasks.md deleted file mode 100644 index 6c3bde0dc3..0000000000 --- a/.changeset/fix-phantom-executor-kills-live-ephemeral-tasks.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Stop self-healing from killing actively-running tasks after ~30 minutes. -category: fix -dev: FN-7566. isPhantomExecutorBinding's liveness gate (heartbeat/checkout/runAudit) was blind to ephemeral executor agents, leaving only the age>graceMs*3 threshold, so any ephemeral-executor task running longer than ~30 min was reclaimed to `todo` mid-flight. Adds the in-process live-session veto (activeSessionRegistry path / executingTaskLock / isTaskActive), mirroring the isWorkspaceTaskLive/sessionDead predicate, and honors clearPhantomExecutorBinding's live-session refusal in reclaimSelfOwnedBranchConflicts. diff --git a/.changeset/fix-windows-terminal-worktrunk-popup.md b/.changeset/fix-windows-terminal-worktrunk-popup.md deleted file mode 100644 index d44499dc33..0000000000 --- a/.changeset/fix-windows-terminal-worktrunk-popup.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Stop Windows Terminal version dialogs from popping up when opening the dashboard or Settings on Windows. -category: fix -dev: Root cause was the worktrunk integration, not the embedded terminal: worktrunk's CLI is named `wt`, which collides with Windows Terminal (`wt.exe`) on PATH, so probing it with `wt --version` launched Windows Terminal. Fixed by (1) `useWorktrunkInstallStatus` only auto-fetching `/api/worktrunk/status` when the integration is enabled (user opt-in) instead of on every Settings/dashboard mount, and (2) an engine-level guard in `probeWorktrunk` that refuses to exec a resolved `wt` that is the Windows Terminal alias (under `WindowsApps` / a `WindowsTerminal` package dir), covering all resolution surfaces. diff --git a/.changeset/fn-7468-onboarding-quick-providers.md b/.changeset/fn-7468-onboarding-quick-providers.md deleted file mode 100644 index 5fec19580c..0000000000 --- a/.changeset/fn-7468-onboarding-quick-providers.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Expand first-run AI provider quick-start choices beyond Anthropic. -category: feature -dev: Moves advanced/all-provider onboarding controls under the quick-start provider section. diff --git a/.changeset/fn-7469-select-created-project-directory.md b/.changeset/fn-7469-select-created-project-directory.md deleted file mode 100644 index 2572f60643..0000000000 --- a/.changeset/fn-7469-select-created-project-directory.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Select newly created folders automatically during project setup. -category: fix -dev: Adds DirectoryPicker opt-in selection for project-registration surfaces while preserving default picker behavior. diff --git a/.changeset/fn-7470-git-onboarding.md b/.changeset/fn-7470-git-onboarding.md deleted file mode 100644 index f58fd6e383..0000000000 --- a/.changeset/fn-7470-git-onboarding.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Show Git prerequisite guidance during first-run GitHub onboarding. -category: feature -dev: Adds bounded server-host git availability to auth status and onboarding. diff --git a/.changeset/fn-7471-desktop-update-version.md b/.changeset/fn-7471-desktop-update-version.md deleted file mode 100644 index 127e445d57..0000000000 --- a/.changeset/fn-7471-desktop-update-version.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Prevent Desktop update banners from using 0.0.0 as the current version. -category: fix -dev: Dashboard update checks now resolve packaged @fusion/desktop metadata and fail closed for unresolved versions. diff --git a/.changeset/fn-7472-windows-desktop-close-quits.md b/.changeset/fn-7472-windows-desktop-close-quits.md deleted file mode 100644 index 21f08bc954..0000000000 --- a/.changeset/fn-7472-windows-desktop-close-quits.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Quit Fusion Desktop on Windows when the window is closed. -category: fix -dev: Updates Electron close lifecycle so Windows shutdown reaches embedded runtime cleanup. diff --git a/.changeset/fn-7473-desktop-anthropic-oauth-browser.md b/.changeset/fn-7473-desktop-anthropic-oauth-browser.md deleted file mode 100644 index 0ba9284e63..0000000000 --- a/.changeset/fn-7473-desktop-anthropic-oauth-browser.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Open desktop Anthropic Subscription OAuth logins in the system browser. -category: fix -dev: Adds Electron window-open policy coverage and preserves Settings auth polling completion paths. diff --git a/.changeset/fn-7474-github-onboarding-actions.md b/.changeset/fn-7474-github-onboarding-actions.md deleted file mode 100644 index effca3756f..0000000000 --- a/.changeset/fn-7474-github-onboarding-actions.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add GitHub OAuth and CLI setup actions to first-run onboarding. -category: feature -dev: GitHub onboarding now shows in-flow OAuth connect, gh auth login, and gh install guidance. diff --git a/.changeset/fn-7475-github-setup-warning.md b/.changeset/fn-7475-github-setup-warning.md deleted file mode 100644 index ddb9fae15a..0000000000 --- a/.changeset/fn-7475-github-setup-warning.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Delay GitHub setup warnings for one day and add a dashboard connect action. -category: fix -dev: Dashboard setup warnings now gate GitHub prompts per project and route the CTA to Settings → Authentication. diff --git a/.changeset/fn-7476-desktop-engine-banner.md b/.changeset/fn-7476-desktop-engine-banner.md deleted file mode 100644 index 5b29ed00c6..0000000000 --- a/.changeset/fn-7476-desktop-engine-banner.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix a false AI engine not running banner in desktop mode. -category: fix -dev: Distinguishes transient embedded desktop engine startup from true dashboard-only mode. diff --git a/.changeset/fn-7477-connection-manager-clarity.md b/.changeset/fn-7477-connection-manager-clarity.md deleted file mode 100644 index bcb18981e7..0000000000 --- a/.changeset/fn-7477-connection-manager-clarity.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Clarify the desktop Connection Manager add-remote flow. -category: fix -dev: Desktop Connection Manager now separates Local Server context from saved remote profiles and collapses the remote editor until add/edit. diff --git a/.changeset/fn-7478-local-server-switch-option.md b/.changeset/fn-7478-local-server-switch-option.md deleted file mode 100644 index 26378a00a4..0000000000 --- a/.changeset/fn-7478-local-server-switch-option.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Restore Local Server in the desktop Switch server list. -category: fix -dev: Desktop Connection Manager now lists local and saved remote destinations together. diff --git a/.changeset/fn-7479-right-dock-task-popup.md b/.changeset/fn-7479-right-dock-task-popup.md deleted file mode 100644 index fc5cbabe00..0000000000 --- a/.changeset/fn-7479-right-dock-task-popup.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Make right-dock task list clicks respect the task popup setting. -category: fix -dev: Threads openMobileTasksInPopup through the right-dock Tasks list route while preserving embedded dock detail when disabled. diff --git a/.changeset/fn-7482-code-review-remediation.md b/.changeset/fn-7482-code-review-remediation.md deleted file mode 100644 index 0b9d79d018..0000000000 --- a/.changeset/fn-7482-code-review-remediation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Auto-retry retryable Code Review remediation failures. -category: fix -dev: Prevents retryable code-review-remediation graph failures from stranding tasks in in-review. diff --git a/.changeset/fn-7486-merge-recovery-noop-ownership.md b/.changeset/fn-7486-merge-recovery-noop-ownership.md deleted file mode 100644 index eff2f7557e..0000000000 --- a/.changeset/fn-7486-merge-recovery-noop-ownership.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix no-op task branch recovery after a previously landed task. -category: fix -dev: Merge/recovery ownership classification now checks no-diff branches before foreign trailer rejection. diff --git a/.changeset/fn-7488-source-free-completion.md b/.changeset/fn-7488-source-free-completion.md deleted file mode 100644 index 7a47fa068c..0000000000 --- a/.changeset/fn-7488-source-free-completion.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Allow documented source-free task-artifact deliveries to finish without commits. -category: fix -dev: fn_task_done now recognizes explicit gitignored .fusion/tasks artifact contracts while preserving source-change no-commit guards. diff --git a/.changeset/fn-7490-push-to-remote-setting.md b/.changeset/fn-7490-push-to-remote-setting.md deleted file mode 100644 index fcbcd51212..0000000000 --- a/.changeset/fn-7490-push-to-remote-setting.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix direct merges so Push to remote after merge honors the configured remote and branch. -category: fix -dev: Resolves remote-only push targets from the merge integration branch and preserves non-fatal push errors on done tasks. diff --git a/.changeset/fn-7493-task-popup-layer.md b/.changeset/fn-7493-task-popup-layer.md deleted file mode 100644 index 8447417427..0000000000 --- a/.changeset/fn-7493-task-popup-layer.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Keep task popups on the board layer with Activity menus above them. -category: fix -dev: Task-detail FloatingWindow callers use a lower layer band, and Activity view menus reposition after popup geometry changes. diff --git a/.changeset/fn-7494-keyboard-shortcuts.md b/.changeset/fn-7494-keyboard-shortcuts.md deleted file mode 100644 index 48eefcefe7..0000000000 --- a/.changeset/fn-7494-keyboard-shortcuts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add configurable dashboard keyboard shortcuts for Quick Chat and Terminal. -category: feature -dev: Global dashboardKeyboardShortcuts settings, guarded document-level key handling, and Escape topmost-popup dismissal. diff --git a/.changeset/fn-7495-settings-search.md b/.changeset/fn-7495-settings-search.md deleted file mode 100644 index e5ff54d07d..0000000000 --- a/.changeset/fn-7495-settings-search.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add search in Settings so operators can find settings faster. -category: feature -dev: Dashboard Settings filters visible sections by setting labels and keywords. diff --git a/.changeset/fn-7497-chat-first-event-timeout.md b/.changeset/fn-7497-chat-first-event-timeout.md deleted file mode 100644 index d32b0d45cc..0000000000 --- a/.changeset/fn-7497-chat-first-event-timeout.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Keep accepted chat requests waiting instead of showing false first-event timeout failures. -category: fix -dev: Dashboard chat POST streams no longer abort accepted-but-silent responses on the client first-event timer. diff --git a/.changeset/fn-7498-original-task-prompt.md b/.changeset/fn-7498-original-task-prompt.md deleted file mode 100644 index e4a1a1b17d..0000000000 --- a/.changeset/fn-7498-original-task-prompt.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Show each task's original prompt in the Plan tab alongside the generated plan. -category: fix -dev: Adds a read-only Task Detail original-prompt section backed by task.description. diff --git a/.changeset/fn-7499-before-after-transformation.md b/.changeset/fn-7499-before-after-transformation.md deleted file mode 100644 index aa45ca63f9..0000000000 --- a/.changeset/fn-7499-before-after-transformation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Add before-to-after transformation summaries to generated task definitions. -category: feature -dev: Built-in standard and fast triage prompts now require a `## Before → After Transformation` section. diff --git a/.changeset/fn-7500-terminal-shortcuts.md b/.changeset/fn-7500-terminal-shortcuts.md deleted file mode 100644 index e296300ad5..0000000000 --- a/.changeset/fn-7500-terminal-shortcuts.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Restore terminal Ctrl/Cmd copy and paste shortcuts. -category: fix -dev: Integrated and embedded terminals now own physical clipboard paste to avoid swallowed or duplicate input. diff --git a/.changeset/fn-7502-terminal-below-layout.md b/.changeset/fn-7502-terminal-below-layout.md deleted file mode 100644 index 6c7aa6e95f..0000000000 --- a/.changeset/fn-7502-terminal-below-layout.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a pinned below-application layout option for the dashboard terminal. -category: feature -dev: Terminal display mode now supports persisted docked, floating, and below layouts, with header controls replacing the footer shell. diff --git a/.changeset/fn-7503-agent-log-timing.md b/.changeset/fn-7503-agent-log-timing.md deleted file mode 100644 index 2b3c5ff780..0000000000 --- a/.changeset/fn-7503-agent-log-timing.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Show first-token and tool processing durations in task agent logs. -category: feature -dev: Adds optional agent-log timing fields `timeToFirstTokenMs` and `durationMs`. diff --git a/.changeset/fn-7504-mobile-chat-composer-keyboard.md b/.changeset/fn-7504-mobile-chat-composer-keyboard.md deleted file mode 100644 index 2bb827bed9..0000000000 --- a/.changeset/fn-7504-mobile-chat-composer-keyboard.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix mobile Chat composer being hidden behind the keyboard accessory bar. -category: fix -dev: Adds keyboard-open bottom clearance in ChatView so the composer clears the iOS input-assistant/autofill bar without a persistent .chat-thread transform or Android reserved-gap. diff --git a/.changeset/fn-7505-settings-default-descriptions.md b/.changeset/fn-7505-settings-default-descriptions.md deleted file mode 100644 index 23bf9f718c..0000000000 --- a/.changeset/fn-7505-settings-default-descriptions.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Settings descriptions now show each setting's default value. -category: feature -dev: Appended default-value copy to settings.* i18n descriptions across Global, Runtimes, and Project Settings sections, sourced from DEFAULT_GLOBAL_SETTINGS/DEFAULT_PROJECT_SETTINGS in settings-schema.ts; added settings-default-descriptions.test.tsx guarding that every surfaced setting states a default (or explicit "inherits"/"no default \u2014 unset") and that every DEFAULT_SETTINGS key is documented or allowlisted as not surfaced. diff --git a/.changeset/fn-7506-settings-reset.md b/.changeset/fn-7506-settings-reset.md deleted file mode 100644 index 8731353926..0000000000 --- a/.changeset/fn-7506-settings-reset.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a Reset Settings button to restore a menu's or all project settings to defaults. -category: feature -dev: New tested section→keys (scope-aware) registry (packages/dashboard/app/components/settings/section-keys.ts) drives per-menu reset via updateSettings/updateGlobalSettings with null-as-delete; non-blob sections (secrets, MCP, plugins, memory, auth, prompts, CLI agents, runtimes) are excluded with a documented reason. diff --git a/.changeset/fn-7508-planner-oversight-level-setting.md b/.changeset/fn-7508-planner-oversight-level-setting.md deleted file mode 100644 index 8fb77762f5..0000000000 --- a/.changeset/fn-7508-planner-oversight-level-setting.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a per-workflow planner oversight level setting (Off, Observe, Steer, Autonomous recovery). -category: feature -dev: New workflow setting `plannerOversightLevel` declared in BUILTIN_OVERSIGHT_SETTINGS; default `autonomous`. Per-task override and engine behavior land in follow-up tasks. diff --git a/.changeset/fn-7509-per-task-planner-oversight-override.md b/.changeset/fn-7509-per-task-planner-oversight-override.md deleted file mode 100644 index 4ccbe4ee99..0000000000 --- a/.changeset/fn-7509-per-task-planner-oversight-override.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Tasks can override the workflow planner oversight level (Off, Observe, Steer, Autonomous recovery). -category: feature -dev: New nullable Task.plannerOversightLevel field (migration 137, SCHEMA_VERSION 137) mirroring executionMode; NULL inherits the workflow setting. Adds resolveEffectivePlannerOversightLevel precedence helper. Dashboard UI/API threading and engine behavior land in follow-up tasks. diff --git a/.changeset/fn-7510-oversight-default.md b/.changeset/fn-7510-oversight-default.md deleted file mode 100644 index 94cce48169..0000000000 --- a/.changeset/fn-7510-oversight-default.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Planner oversight now defaults to full steering/control for every workflow unless explicitly changed. -category: feature -dev: Confirms the `plannerOversightLevel` workflow-setting default is the highest (autonomous) level; unset workflow value and unset per-task override both resolve to full steering via `resolveEffectivePlannerOversightLevel` (task override → workflow effective value → autonomous), adding dedicated regression coverage for the "unless explicitly disabled" precedence. diff --git a/.changeset/fn-7511-planner-overseer-monitoring.md b/.changeset/fn-7511-planner-overseer-monitoring.md deleted file mode 100644 index 7037d140bb..0000000000 --- a/.changeset/fn-7511-planner-overseer-monitoring.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Planner oversight now monitors tasks across executor, reviewer, merger, pull-request, and workflow-gate stages. -category: feature -dev: Adds records-only PlannerOverseerMonitor + resolveWatchedStage + OverseerStageObservation in @fusion/engine, gated by resolveEffectivePlannerOversightLevel (off = no observation) and wired into ProjectEngine via a bounded poll. Steering/recovery and UI land in FN-7512/FN-7515+. diff --git a/.changeset/fn-7512-planner-bounded-recovery.md b/.changeset/fn-7512-planner-bounded-recovery.md deleted file mode 100644 index 76586f23d0..0000000000 --- a/.changeset/fn-7512-planner-bounded-recovery.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Planner oversight can autonomously inject guidance, retry stuck/failed steps, and request fixes within bounded limits. -category: feature -dev: Adds pure `decidePlannerRecovery` + recovery types (core) and `PlannerRecoveryController` with injected guidance/retry/targeted-fix handlers (engine), consuming the FN-7511 observation. Acts only at effective level `autonomous`, caps attempts per (task, stage) via `PLANNER_RECOVERY_MAX_ATTEMPTS`, skips user-paused tasks, and excludes merge/PR/destructive actions (deferred to FN-7513) and comprehensive human-control safeguards (FN-7514). diff --git a/.changeset/fn-7513-planner-confirmation-gate.md b/.changeset/fn-7513-planner-confirmation-gate.md deleted file mode 100644 index 69b79446a0..0000000000 --- a/.changeset/fn-7513-planner-confirmation-gate.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Planner oversight now requires confirmation before merge/PR actions and destructive/external side effects. -category: feature -dev: Adds `PlannerActionSideEffectClass` + `PlannerConfirmationRequest` and `classifyPlannerActionSideEffect`/`requiresPlannerConfirmation` (core), extends `decidePlannerRecovery` with an `await_confirmation` action, and adds `requestConfirmation`/`resolveConfirmation` gating to `PlannerRecoveryController` (engine). Merge/PR and destructive/external actions never execute without a recorded approval; bounded recovery (guidance/retry/targeted-fix) is unchanged. UX rendering, human-control safeguards, timeline, and run-audit land in follow-up tasks. diff --git a/.changeset/fn-7514-overseer-human-control-guard.md b/.changeset/fn-7514-overseer-human-control-guard.md deleted file mode 100644 index 469d0ffb18..0000000000 --- a/.changeset/fn-7514-overseer-human-control-guard.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Planner overseer now stays fully hands-off for paused tasks and auto-merge-off / human-review tasks. -category: feature -dev: Adds the pure `evaluateOverseerHumanControl` policy (packages/engine/src/overseer-human-control-policy.ts), consulted at the top of `PlannerRecoveryController.tick()` before any action classification, confirmation gating, steering, retry, or dispatch — so a user-paused or `autoMerge:false`/human-review task never even records a pending confirmation. Reuses `allowsAutoMergeProcessing` from `@fusion/core` verbatim (never re-derives the auto-merge/human-review predicate). Distinguishes explicit user pause (`task.userPaused===true`, or `task.paused===true` with no `pausedReason`) from engine/self-healing parks (which always stamp a `pausedReason`). Emits a bounded `overseer:oversight-withheld-human-control` run-audit no-action event (metadata: `{ taskId, reason, stage, oversightLevel }`), deduped per (taskId, reason) so it does not spam every poll. diff --git a/.changeset/fn-7515-planner-oversight-config-exposure.md b/.changeset/fn-7515-planner-oversight-config-exposure.md deleted file mode 100644 index 8fa1d7229b..0000000000 --- a/.changeset/fn-7515-planner-oversight-config-exposure.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Configure planner oversight level per task and per project in the workflow editor and task create/detail. -category: feature -dev: Per-task `plannerOversightLevel` override exposed via TaskForm (Inherit/off/observe/steer/autonomous), threaded through createTask/updateTask; workflow-editor Values tab gets a first-class display entry. Workflow-native setting; not a project setting. diff --git a/.changeset/fn-7518-oversight-notification-verbosity.md b/.changeset/fn-7518-oversight-notification-verbosity.md deleted file mode 100644 index 18b3979702..0000000000 --- a/.changeset/fn-7518-oversight-notification-verbosity.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a configurable planner-overseer notification verbosity level (Silent/Errors/Important/All). -category: feature -dev: New workflow-native enum setting `plannerOversightNotificationLevel` in BUILTIN_OVERSIGHT_SETTINGS; default `important`. Resolves via resolveEffectiveSettings; emission gating that reads it lands in FN-7519/FN-7520. diff --git a/.changeset/fn-7519-planner-intervention-timeline.md b/.changeset/fn-7519-planner-intervention-timeline.md deleted file mode 100644 index 1118363906..0000000000 --- a/.changeset/fn-7519-planner-intervention-timeline.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a task-detail planner-overseer intervention timeline (stage, reason, action, outcome, attempts, links). -category: feature -dev: New core `PlannerInterventionEntry` model + `recordPlannerIntervention`/`getPlannerInterventionTimeline` helpers persisting via the run-audit store under the `overseer:intervention` mutation, plus a `PlannerInterventionTimeline` component rendered in the task-detail Planner Oversight cluster. Emission call-sites land in FN-7520. diff --git a/.changeset/fn-7520-planner-overseer-events.md b/.changeset/fn-7520-planner-overseer-events.md deleted file mode 100644 index b7850edd77..0000000000 --- a/.changeset/fn-7520-planner-overseer-events.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Emit planner-overseer run-audit events for observations, steering, retries, recovery, confirmations, and escalations. -category: feature -dev: New core emitters (emitOverseerObservation/Steering/RecoveryAttempt/Retry/Confirmation/Escalation) in planner-overseer-events.ts, each mapping its decision-point to the correct intervention action/outcome and delegating to FN-7519's recordPlannerIntervention under the overseer:intervention mutation. Producer call-sites land in FN-7511/FN-7512/FN-7513. diff --git a/.changeset/fn-7523-task-revert.md b/.changeset/fn-7523-task-revert.md deleted file mode 100644 index 4912dce4c3..0000000000 --- a/.changeset/fn-7523-task-revert.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Add an intelligent git-revert engine service and POST /api/tasks/:id/revert route. -category: feature -dev: New `packages/engine/src/task-revert.ts` exports `resolveTaskRevertCommits`, `classifyTaskRevert`, and `performTaskRevert` (squash/rebase/lineage attribution precedence, dry-run classification, guaranteed-clean rollback). Route enforces done/archived-only and autoMerge-off guard rails; conflicting results are returned unresolved for sibling FN-7524 (AI-undo) to act on. Workspace tasks return `unsupported`. diff --git a/.changeset/fn-7524-ai-undo-revert.md b/.changeset/fn-7524-ai-undo-revert.md deleted file mode 100644 index 3a78674fac..0000000000 --- a/.changeset/fn-7524-ai-undo-revert.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add an AI-undo fallback task when reverting a done task via git conflicts or is unsupported. -category: feature -dev: `POST /api/tasks/:id/revert` now accepts `{ mode?: "git" | "ai" | "auto" }` (default `"auto"`). `"auto"` tries the FN-7523 git-revert path first and falls back to creating an AI-undo board task (`{ mode: "ai", createdTaskId, alreadyOpen? }`) on a conflicting or unsupported (e.g. workspace) git result; `needsHuman` (autoMerge-off) never triggers the fallback. `"ai"` always creates the AI-undo task; `"git"` keeps the FN-7523 git-only contract, which is otherwise unchanged. New engine exports: `createAiUndoTask`, `buildAiUndoTaskDescription`, `REVERT_OF_METADATA_KEY`. New core store method `TaskStore.findOpenRevertTaskForSource` backs the idempotency guard (an open undo task suppresses a duplicate; a closed one does not). diff --git a/.changeset/fn-7525-revert-card-affordance.md b/.changeset/fn-7525-revert-card-affordance.md deleted file mode 100644 index 2444d32ac2..0000000000 --- a/.changeset/fn-7525-revert-card-affordance.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a Revert action to Done/Archived task cards to undo landed changes. -category: feature -dev: Wires onRevertTask through Board/List/Detail surfaces; calls POST /tasks/:id/revert in "auto" mode with a conflict-confirm AI-undo fallback (mode: "ai"). diff --git a/.changeset/fn-7526-plan-auto-approve.md b/.changeset/fn-7526-plan-auto-approve.md deleted file mode 100644 index 7dce0c5a0b..0000000000 --- a/.changeset/fn-7526-plan-auto-approve.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Auto-approve now reliably sends specified plans to the board without a manual approval stop. -category: fix -dev: FN-7526 — investigated the reported "plans still park at awaiting-approval when auto-approve is on" symptom; resolvePlanApprovalRequired, mergeEffectiveSettings/applyWorkflowSettingsOverlay, and every finalizeApprovedTask call site (specifyTask, recoverApprovedTask, retryUnavailablePlanReview, tryFinalizeExplicitDuplicateMarker) already honored project planApprovalMode: "auto-approve-all" over a stored workflow requirePlanApproval value — no production defect reproduced. Added end-to-end regression coverage across every enumerated surface (Plan Review reviewer-outage retry, refinement routing, self-healing starved-refinement recovery) using the real mergeEffectiveSettings pipeline instead of isolated bare-settings unit calls, plus explicit assertions that the independent release-authorization and Workflow Plan Review gates remain intact under auto-approve-all, so a future bare-settings call site is caught immediately instead of silently reintroducing the reported behavior. diff --git a/.changeset/fn-7527-desktop-switch-server-navigation.md b/.changeset/fn-7527-desktop-switch-server-navigation.md deleted file mode 100644 index 19784749de..0000000000 --- a/.changeset/fn-7527-desktop-switch-server-navigation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix the in-dashboard Switch server menu not switching desktop local/remote. -category: fix -dev: The desktop shell's redirect effects in App.tsx read a dead `localServer` field that the preload never populates; extracted `resolveDesktopShellRedirectTarget` in appLifecycle.ts now derives the navigation target from the live `localRuntime`/`activeProfileId` state for both directions, and the unused `localServer` field was removed from `ShellConnectionState`. diff --git a/.changeset/fn-7528-task-performance-capture.md b/.changeset/fn-7528-task-performance-capture.md deleted file mode 100644 index c460beaa73..0000000000 --- a/.changeset/fn-7528-task-performance-capture.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Capture a structured performance snapshot when an agent task completes. -category: feature -dev: New AgentReflectionService.captureTaskPerformance persists a non-LLM post-task ReflectionMetrics record (duration, packages/files touched, verification command + scope, retry/rework count) and emits ids/counts-only `reflection:captured` run-audit telemetry; populates performanceSummary/latestReflection. diff --git a/.changeset/fn-7531-planner-overseer-state-exposure.md b/.changeset/fn-7531-planner-overseer-state-exposure.md deleted file mode 100644 index e47ffac7bc..0000000000 --- a/.changeset/fn-7531-planner-overseer-state-exposure.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Task cards can now show the planner overseer's active state (idle/watching/steering/recovering/awaiting-confirmation). -category: feature -dev: Adds a serializable `PlannerOverseerRuntimeSnapshot` + pure `derivePlannerOverseerState` (core), a read-only `ProjectEngine.getPlannerOverseerRuntimeSnapshot(taskId)` accessor assembling it from the FN-7511 monitor + FN-7512/7513 recovery controller, and a best-effort additive `plannerOverseerState` enrichment on `GET /api/tasks` (mirrors the `branchProgress` pattern; never persisted, never fails the board load). Consumed by FN-7516's TaskCard. diff --git a/.changeset/fn-7532-branch-group-completion.md b/.changeset/fn-7532-branch-group-completion.md deleted file mode 100644 index 8b18b6cb39..0000000000 --- a/.changeset/fn-7532-branch-group-completion.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix branch group completion checklists to show accurate landed/finished counts. -category: fix -dev: runAiMerge (the sole merge path since master-plan U0) never resolved branch-group routing or stamped mergeDetails.mergeTargetBranch/mergeTargetSource, so isBranchGroupMemberLanded permanently reported shared-group members as not landed. Routes through resolveBranchGroupMergeRouting (matching the legacy merger.ts pattern) and stamps the target fields on both the landed and no-op finalize paths; preserves merge-target-safety in isBranchGroupMemberLanded (a sibling/mismatched-branch member still never counts as landed). diff --git a/.changeset/fn-7534-branch-group-archived-member.md b/.changeset/fn-7534-branch-group-archived-member.md deleted file mode 100644 index d79c918c9d..0000000000 --- a/.changeset/fn-7534-branch-group-archived-member.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Branch groups no longer report complete (or become promotable) when an unlanded member is archived. -category: fix -dev: listTasksByBranchGroup membership now scans with includeArchived:true so an archived-but-unlanded member stays counted in total instead of silently dropping out; mergeDetails is now persisted on ArchivedTaskEntry so an archived member that had already landed keeps counting as landed. evaluateBranchGroupCompletion / promoteBranchGroup gate correctly; merge-target-safety in isBranchGroupMemberLanded is unchanged. diff --git a/.changeset/fn-7535-global-gitlab-setting-save.md b/.changeset/fn-7535-global-gitlab-setting-save.md deleted file mode 100644 index adf4d7d16c..0000000000 --- a/.changeset/fn-7535-global-gitlab-setting-save.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix the global GitLab integration setting not persisting when saved. -category: fix -dev: splitSettingsSave now diffs the five global GitLab keys (gitlabEnabled, gitlabInstanceUrl, gitlabApiBaseUrl, gitlabAuthToken, gitlabAuthTokenType) against scoped global initials only, never the project-effective merged initialValues, so a project override no longer suppresses a real global save. diff --git a/.changeset/fn-7536-activity-dropdown-mobile.md b/.changeset/fn-7536-activity-dropdown-mobile.md deleted file mode 100644 index 1fefce406b..0000000000 --- a/.changeset/fn-7536-activity-dropdown-mobile.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix task-detail Activity view dropdown not opening reliably on mobile. -category: fix -dev: Guards the Activity menu's window resize/orientationchange/scroll close-listener with the same opening-tap timing guard already used for visualViewport, and exempts scroll events originating in the `.detail-tabs` scroller, so a same-gesture mobile tap echo (Android/iOS, fixed modal or `.floating-window--task-detail` popup) no longer closes the menu the instant it opens. diff --git a/.changeset/fn-7537-backup-automation-manual-run.md b/.changeset/fn-7537-backup-automation-manual-run.md deleted file mode 100644 index d367f5d451..0000000000 --- a/.changeset/fn-7537-backup-automation-manual-run.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Manual "Run now" for the Database Backup automation now runs in-process like the scheduler, matching cron behavior. -category: fix -dev: The legacy single-command and command-step manual automation run path (`executeSingleCommand` in packages/dashboard/src/routes.ts) now intercepts `isInProcessBackupCommand`/`isInProcessMemoryBackupCommand` via the scoped TaskStore, mirroring `RoutineRunner.executeCommand`/`CronRunner`, instead of always shelling out via `exec()`. `formatInProcessBackupError`, `isInProcessBackupCommand`, and `isInProcessMemoryBackupCommand` are now exported from `@fusion/engine` for reuse. Existing onStep/onText live-run callbacks already stream incremental output for command/backup runs; added regression coverage confirming this holds for the new interception branch. diff --git a/.changeset/fn-7539-oversight-badge-default.md b/.changeset/fn-7539-oversight-badge-default.md deleted file mode 100644 index c9ab7eaf20..0000000000 --- a/.changeset/fn-7539-oversight-badge-default.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Task cards no longer show the "Auto-recovery" oversight badge unless oversight is explicitly configured. -category: fix -dev: `TaskCard.tsx`'s `showOversightBadge` gate now also suppresses the badge when the effective level equals `DEFAULT_PLANNER_OVERSIGHT_LEVEL` ("autonomous") and there is no explicit per-task `plannerOversightLevel` override; an explicit per-task override of "autonomous" still renders the badge. diff --git a/.changeset/fn-7542-remove-overseer-state-badge.md b/.changeset/fn-7542-remove-overseer-state-badge.md deleted file mode 100644 index 3491a6b29d..0000000000 --- a/.changeset/fn-7542-remove-overseer-state-badge.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Remove the per-card overseer-state ("Executor") badge from task cards. -category: fix -dev: Deleted the FN-7516 `card-overseer-state-badge` render, its card-local `deriveOverseerCardWatchedStage` helper/label maps, and its CSS; the sibling oversight-level badge (`card-oversight-badge`) is unaffected. diff --git a/.changeset/fn-7543-original-prompt-markdown-collapsed.md b/.changeset/fn-7543-original-prompt-markdown-collapsed.md deleted file mode 100644 index 79284d7736..0000000000 --- a/.changeset/fn-7543-original-prompt-markdown-collapsed.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Original task prompt now renders as Markdown and is collapsed by default in the task Plan tab. -category: feature -dev: Task Detail Plan/Definition tab original-prompt section reuses the existing `.detail-source-toggle`/`.detail-source-chevron--expanded` collapse pattern and the shared `ReactMarkdown` pipeline (`remarkGfm`, `sharedRehypePlugins`, `markdownLinkifyComponents`); backed by read-only `task.description`, no change to the generated `PROMPT.md` editor/revision flow. diff --git a/.changeset/fn-7544-artifact-cross-instance-live-refresh.md b/.changeset/fn-7544-artifact-cross-instance-live-refresh.md deleted file mode 100644 index f890cc0447..0000000000 --- a/.changeset/fn-7544-artifact-cross-instance-live-refresh.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix agent-created artifacts not appearing live in the dashboard artifacts view. -category: fix -dev: Root cause was cross-instance artifact-registration replication, not the route/hook/render path (all already correct). `TaskStore.registerArtifact()` never bumped `lastModified`, and `checkForChanges()` (the polling replicator that lets a second TaskStore instance on the same project — e.g. the dashboard's cached store vs. the engine's own store — mirror events it did not write itself) only ever diffed the `tasks` table, never `artifacts`. A store instance that did not perform the write could therefore never observe or re-emit `artifact:registered`, leaving an already-open Documents/task Artifacts gallery stale until a full reload. Fixed by bumping `lastModified` on artifact writes and adding a strictly-increasing `rowid`-cursor poll over the `artifacts` table in `checkForChanges()`. See `packages/core/src/__tests__/artifacts.test.ts` and `packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts` for regression coverage. diff --git a/.changeset/fn-7547-workspace-task-revert.md b/.changeset/fn-7547-workspace-task-revert.md deleted file mode 100644 index 1404ce5d4c..0000000000 --- a/.changeset/fn-7547-workspace-task-revert.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Support reverting multi-repo workspace tasks via git, all-or-nothing across sub-repos. -category: feature -dev: Extends `packages/engine/src/task-revert.ts` with `resolveWorkspaceTaskRevertCommits`/`revertWorkspaceTask` and wires `POST /api/tasks/:id/revert` to dispatch workspace tasks (`isWorkspaceTask`) to the new path; returns `{ mode: "git", clean, workspace: { repos: [...] }, conflicts? }`. Single-repo `performTaskRevert` path is unchanged. diff --git a/.changeset/fn-7548-per-sha-revert-granularity.md b/.changeset/fn-7548-per-sha-revert-granularity.md deleted file mode 100644 index 657bd6db0f..0000000000 --- a/.changeset/fn-7548-per-sha-revert-granularity.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add per-sha revert commit granularity to the task revert API and service. -category: feature -dev: `performTaskRevert` and `POST /api/tasks/:id/revert` accept an optional `granularity: "squash" | "per-sha"` (default `"squash"`, unchanged FN-7523 behavior). `"per-sha"` creates one attributed `revert(FN-xxxx)` commit per original sha (each with its own `Fusion-Task-Id` trailer and audit line), skipping no-op shas without empty commits. A mid-batch conflict in either mode rolls back the whole batch to the pre-call HEAD — no partially-landed per-sha commits. The clean result now reports `revertCommitShas: string[]` (all created commits) alongside the existing `revertCommitSha` (kept for backward compatibility). diff --git a/.changeset/fn-7550-terminal-shortcut-scroll.md b/.changeset/fn-7550-terminal-shortcut-scroll.md deleted file mode 100644 index 89119231f1..0000000000 --- a/.changeset/fn-7550-terminal-shortcut-scroll.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix the mobile terminal shortcut bar so it scrolls horizontally to reach every key. -category: fix -dev: Added `min-width: 0` to `.terminal-shortcut-panel` to defeat the flex min-width:auto trap that clipped overflow instead of engaging `overflow-x: auto`. diff --git a/.changeset/fn-7551-overseer-timeline-wiring.md b/.changeset/fn-7551-overseer-timeline-wiring.md deleted file mode 100644 index 82d4775708..0000000000 --- a/.changeset/fn-7551-overseer-timeline-wiring.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Planner-oversight intervention timeline now populates from real engine activity. -category: fix -dev: Wires PlannerOverseerMonitor/PlannerRecoveryController decision points to the FN-7520 emitOverseer* façade with the real TaskStore; observation/escalation emission deduped per (task, stage[, signal]). diff --git a/.changeset/fn-7552-mobile-authentication-global-prefix.md b/.changeset/fn-7552-mobile-authentication-global-prefix.md deleted file mode 100644 index a36edea9cb..0000000000 --- a/.changeset/fn-7552-mobile-authentication-global-prefix.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Show the "Global" prefix on the Authentication entry in the mobile Settings picker. -category: fix -dev: resolveSettingsSectionOptionLabel now derives the Global-group prefix for storage-less (scope: undefined) sections in SettingsModal.tsx (FN-7552). diff --git a/.changeset/fn-7553-keyboard-shortcuts-section.md b/.changeset/fn-7553-keyboard-shortcuts-section.md deleted file mode 100644 index 79259dd4d5..0000000000 --- a/.changeset/fn-7553-keyboard-shortcuts-section.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a dedicated Keyboard Shortcuts settings section with click-to-record capture and more configurable actions. -category: feature -dev: Relocates dashboardKeyboardShortcuts into its own settings section, adds a ShortcutCaptureInput recorder, and extends DashboardShortcutAction with openFiles/openSettings/openCommandCenter/newTask actions wired into existing App nav handlers. diff --git a/.changeset/fn-7554-pr-based-revert.md b/.changeset/fn-7554-pr-based-revert.md deleted file mode 100644 index 5b164b3be2..0000000000 --- a/.changeset/fn-7554-pr-based-revert.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Open a revert PR for done/archived tasks when autoMerge is disabled instead of refusing. -category: feature -dev: `POST /api/tasks/:id/revert` gains an additive `{ mode: "pr", clean: true, prUrl, prNumber, revertBranch, existingPr? }` result for clean single-repo reverts under `autoMerge:false`, reusing `GitHubClient.createPr`, `findPrForBranch` idempotency, and the `manual:true` PR handoff. New engine export `prepareRevertPrBranch` (packages/engine/src/task-revert.ts) prepares the dedicated `fusion/revert-` branch without ever mutating the base branch. Existing `{ mode: "git" | "ai", ... }` shapes and the `autoMerge:true` path are unchanged. diff --git a/.changeset/fn-7556-ai-undo-workflow.md b/.changeset/fn-7556-ai-undo-workflow.md deleted file mode 100644 index b087f540ba..0000000000 --- a/.changeset/fn-7556-ai-undo-workflow.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: AI-undo tasks now default to a configurable, stricter review workflow. -category: feature -dev: New project setting `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) selects the workflow for AI-undo board tasks created by `POST /api/tasks/:id/revert` (`mode: "ai"`, the `auto` conflict fallback, and the workspace conflict fallback all share the `createAiUndoResult()` closure, so all three inherit this default). A blank/unset value means the created task inherits the project default workflow (pre-FN-7556 behavior). The route validates the configured id via `getWorkflowDefinition`/`isBuiltinWorkflowId` and falls back to inherit (with a logged warning) on a blank or unknown value, so a misconfigured id never breaks AI-undo task creation. The engine's `createAiUndoTask` helper stays pure — it only forwards a `workflowId` it is given, never resolves the setting itself. The Settings Modal UI field for this setting is a deliberate follow-up task; the value is settable today only via the settings API. diff --git a/.changeset/fn-7557-plan-auto-approve-default.md b/.changeset/fn-7557-plan-auto-approve-default.md deleted file mode 100644 index f849b72911..0000000000 --- a/.changeset/fn-7557-plan-auto-approve-default.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Plan auto-approval is now the default; specified tasks skip manual approval unless you opt into workflow/require-all. -category: feature -dev: `DEFAULT_PROJECT_SETTINGS.planApprovalMode` flips `workflow` → `auto-approve-all`; existing projects with an explicit stored value are unchanged; consumed by `resolvePlanApprovalRequired` at the triage gating sites. diff --git a/.changeset/fn-7559-approval-gate-disambiguation.md b/.changeset/fn-7559-approval-gate-disambiguation.md deleted file mode 100644 index 7ac656be79..0000000000 --- a/.changeset/fn-7559-approval-gate-disambiguation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Tasks held for release authorization or Plan Review are now shown distinctly, so auto-approve no longer looks broken. -category: fix -dev: FN-7559 — auto-approve-all bypasses only the manual plan-approval gate (unchanged, FN-7526). Release-authorization holds are surfaced with a new distinct status reason (`Task.awaitingApprovalReason: "release-authorization"`) and no longer render the generic manual Approve/Reject affordance in TaskCard/TaskDetailModal; Workflow Plan Review already used distinct statuses (`needs-replan`/`plan-review-unavailable`) and is unaffected. Both gates remain independent and intact — this is UI/data disambiguation only. diff --git a/.changeset/fn-7560-mobile-terminal-footer.md b/.changeset/fn-7560-mobile-terminal-footer.md deleted file mode 100644 index 6ae6140ab1..0000000000 --- a/.changeset/fn-7560-mobile-terminal-footer.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Move mobile terminal controls into a bottom footer so they no longer crowd the header, with a scrollable shortcut bar. -category: fix -dev: On the ≤768px terminal, the `.terminal-actions` cluster now renders in a `terminal-footer-actions` bar (with `min-width:0; overflow-x:auto`) instead of the header; desktop/floating/pinned-below keep the FN-7502 header layout. Preserves the FN-7550 shortcut-panel scroll fix. diff --git a/.changeset/fn-7560-release-gate-disclaimer-false-positive.md b/.changeset/fn-7560-release-gate-disclaimer-false-positive.md deleted file mode 100644 index 6ddbecef94..0000000000 --- a/.changeset/fn-7560-release-gate-disclaimer-false-positive.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Stop the release-authorization gate from holding tasks that merely disclaim releasing. -category: fix -dev: classifyReleaseTask now strips negated release-disclaimer clauses (e.g. "this task performs no release/publish; releases are owned by scripts/release.mjs") before signal matching in packages/engine/src/triage-release-authorization.ts, so revert/undo/UI specs are no longer false-flagged as release-class. Genuine "run pnpm release"/"publish @runfusion/fusion" intent still trips the gate. diff --git a/.changeset/fn-7561-mobile-terminal-spacing.md b/.changeset/fn-7561-mobile-terminal-spacing.md deleted file mode 100644 index 020bb8c7c0..0000000000 --- a/.changeset/fn-7561-mobile-terminal-spacing.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix mobile terminal text still rendering with excess inter-character gaps after font-load settle. -category: fix -dev: Root cause: xterm's OptionsService setter is a no-op when reassigning an already-current fontFamily/fontSize, so post-settle reapply never forced CharSizeService/DomRenderer to remeasure. Added `forceTerminalFontRemeasure()` in `terminalPreferences.ts`, used by both `TerminalModal.tsx` and `SessionTerminal.tsx` at every post-`waitForTerminalFontMetrics()` settle site. diff --git a/.changeset/fn-7561-plan-review-replan-loop.md b/.changeset/fn-7561-plan-review-replan-loop.md deleted file mode 100644 index 3bae0fe9b9..0000000000 --- a/.changeset/fn-7561-plan-review-replan-loop.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Stop Plan Review from looping tasks forever and fix its "can't find the plan" reviews. -category: fix -dev: FN-7561 — Plan Review pre-merge gate hardening in packages/engine/src/executor.ts. (1) The reviewer ran readonly with cwd=worktree but the spec lives at project-root .fusion/tasks//PROMPT.md, so "Read PROMPT.md" produced "no PROMPT.md found / data is in a DB" non-verdicts; the spec text is now injected into the reviewer prompt via readTaskArtifact. (2) A malformed reviewer response now self-retries once on the primary model when no fallback is configured. (3) A malformed (advisory_failure, no verdict) plan-review result can never trigger a triage replan. (4) The unbounded plan-review replan default is capped at 15 attempts with a loud halting log entry, so a persistently-disagreeing planner/reviewer no longer burns LLM calls indefinitely (FN-7525 ran 13+ attempts overnight). diff --git a/.changeset/fn-7563-overseer-badge-explanation.md b/.changeset/fn-7563-overseer-badge-explanation.md deleted file mode 100644 index 31b019ecfe..0000000000 --- a/.changeset/fn-7563-overseer-badge-explanation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Planner-overseer task badge now shows a readable label and explains what it is waiting on. -category: fix -dev: TaskCard badge renders plannerOverseerStateLabel + plannerOverseerBadgeTooltip built from the existing PlannerOverseerRuntimeSnapshot (reason/watchedStage/signal/pendingConfirmation); presentation-only, no engine changes. diff --git a/.changeset/fn-7564-approve-plan-release-authorization-guard.md b/.changeset/fn-7564-approve-plan-release-authorization-guard.md deleted file mode 100644 index e225c8e2a9..0000000000 --- a/.changeset/fn-7564-approve-plan-release-authorization-guard.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Plan approve/reject API now blocks release-authorization holds, requiring the authorization marker first. -category: fix -dev: FN-7564 — POST /tasks/:id/approve-plan and /reject-plan now return 400 when task.awaitingApprovalReason === "release-authorization" (FN-7559 discriminator), enforcing the FN-6481 release-authorization gate at the API layer regardless of client. Manual-approval holds are unaffected. diff --git a/.changeset/fn-7565-mobile-terminal-close-corner.md b/.changeset/fn-7565-mobile-terminal-close-corner.md deleted file mode 100644 index 57f627a4b1..0000000000 --- a/.changeset/fn-7565-mobile-terminal-close-corner.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Pin the mobile terminal close (X) button to the top-right corner so it is easy to find and tap. -category: fix -dev: On the ≤768px terminal, the `terminal-close` button now carries a `terminal-close--corner` class (order:3 + margin-inline-start:auto) so it renders last in flex order and hugs the right edge next to the tab dropdown, instead of falling back to order:0 (far left). Desktop/floating/pinned-below placement inside `.terminal-actions` is unchanged. diff --git a/.changeset/fn-7567-mobile-terminal-spacing.md b/.changeset/fn-7567-mobile-terminal-spacing.md deleted file mode 100644 index 1462cca8e4..0000000000 --- a/.changeset/fn-7567-mobile-terminal-spacing.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix mobile terminal excess character spacing that survived earlier font-remeasure fixes. -category: fix -dev: `TerminalModal`/`SessionTerminal` re-bake xterm's `DomRenderer` letter-spacing compensation AFTER `fitAddon.fit()` settles the post-fit column count (not just before it), since `handleResize()` never re-bakes spacing itself. See `docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md` recurrence #4. diff --git a/.changeset/fn-7568-fn-cli-release-asset.md b/.changeset/fn-7568-fn-cli-release-asset.md deleted file mode 100644 index 3ef5b22298..0000000000 --- a/.changeset/fn-7568-fn-cli-release-asset.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Rename downloadable CLI release binaries to the fn-cli- base name. -category: internal -dev: `binaryNameForTarget` in `packages/cli/build.ts` and the `release.yml` / `test-release.yml` matrices now emit `fn-cli-`; the local dev binary stays `fn`/`fn.exe`. diff --git a/.changeset/fn-7569-plan-approval-idempotent.md b/.changeset/fn-7569-plan-approval-idempotent.md deleted file mode 100644 index 7e50bcf5b3..0000000000 --- a/.changeset/fn-7569-plan-approval-idempotent.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Manual plan approval no longer re-asks you to approve a plan you already approved when it hasn't changed. -category: fix -dev: FN-7569 — approving a plan records a fingerprint of the approved PROMPT.md (new nullable Task.approvedPlanFingerprint, migration 139). The manual plan-approval gate skips re-parking at awaiting-approval when a re-specification (replan, plan-review retry, self-healing rebound) produces the same plan; a changed plan or reject-plan still requires fresh approval. Release authorization, Workflow Plan Review, and auto-approve-all are unchanged. diff --git a/.changeset/fn-7571-intervention-timeline-activity-dropdown.md b/.changeset/fn-7571-intervention-timeline-activity-dropdown.md deleted file mode 100644 index a3af496a8b..0000000000 --- a/.changeset/fn-7571-intervention-timeline-activity-dropdown.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Move the planner intervention timeline into the task Activity view dropdown. -category: feature -dev: Removes the inline `PlannerInterventionTimeline` mount from the FN-7517 oversight cluster in `TaskDetailModal.tsx` and adds a fourth `interventions` `ActivitySegment`, shown in the Activity dropdown only when planner oversight is active for the task; falls back to Live if oversight turns off while Interventions is selected. diff --git a/.changeset/fn-7574-oauth-expiry-detection-refresh.md b/.changeset/fn-7574-oauth-expiry-detection-refresh.md deleted file mode 100644 index 1624b75079..0000000000 --- a/.changeset/fn-7574-oauth-expiry-detection-refresh.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Expired Claude subscription logins now show disconnected with a re-login prompt; tokens auto-refresh before expiry. -category: fix -dev: Unifies OAuth expiry detection between OAuthExpiryMonitor and /api/auth/status, and adds an engine-side proactive OAuth refresh scheduler wired in project-engine (guarded by skipNotifier). No token material logged. diff --git a/.changeset/fn-7575-release-version-comment.md b/.changeset/fn-7575-release-version-comment.md deleted file mode 100644 index e06e707639..0000000000 --- a/.changeset/fn-7575-release-version-comment.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Fusion self-repo issue-close comments now show current and target release versions. -category: feature -dev: GitHubIssueCommentService appends "Current version: v{current}" and "Target release: v{next-minor}" lines when the linked source issue is runfusion/fusion; other repos unchanged. Version resolved via getCliPackageVersion. diff --git a/.changeset/fn-7576-cli-wrapper-subscription-getapikey-delegation.md b/.changeset/fn-7576-cli-wrapper-subscription-getapikey-delegation.md deleted file mode 100644 index c0cd7d259c..0000000000 --- a/.changeset/fn-7576-cli-wrapper-subscription-getapikey-delegation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Anthropic subscription reads now refresh the OAuth token automatically instead of silently failing when expired. -category: fix -dev: mergeAuthStorageReads getApiKey("anthropic-subscription") now delegates to the underlying engine authStorage.getApiKey (the only refresh-token HTTP round trip) instead of a local static expiry check; regression tests drive the wrapper directly. No token material logged. diff --git a/.changeset/fn-7577-workspace-pr-revert.md b/.changeset/fn-7577-workspace-pr-revert.md deleted file mode 100644 index a231551ed5..0000000000 --- a/.changeset/fn-7577-workspace-pr-revert.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Open one revert PR per sub-repo for workspace tasks when autoMerge is disabled. -category: feature -dev: `POST /api/tasks/:id/revert` gains an additive workspace `{ mode: "pr", clean: true, workspace: { repos: [{ repo, revertBranch, prUrl, prNumber, existingPr? }] } }` result for clean multi-repo reverts under `autoMerge:false`, extending FN-7554's single-repo `mode:"pr"` path. New engine export `prepareWorkspaceRevertPrBranches` (packages/engine/src/task-revert.ts) classifies every sub-repo first and only prepares a dedicated `fusion/revert-` branch per sub-repo when all are clean/already-reverted (all-or-nothing at the branch-prep phase), never force-writing any sub-repo integration branch. The route resolves owner/repo and checks the rate limiter for every sub-repo before pushing/creating any PR, so GitHub-unconfigured/rate-limited cases degrade the whole task to `needsHuman` rather than opening a partial subset of PRs. Existing `{ mode: "git" | "ai" | "pr", ... }` shapes, the `autoMerge:true` workspace path, and FN-7554's single-repo path are unchanged. diff --git a/.changeset/fn-7578-ai-undo-workflow-setting-ui.md b/.changeset/fn-7578-ai-undo-workflow-setting-ui.md deleted file mode 100644 index a57c515d5c..0000000000 --- a/.changeset/fn-7578-ai-undo-workflow-setting-ui.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a Settings → General picker to choose the workflow used for AI-undo (revert) tasks. -category: feature -dev: Surfaces `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) in GeneralSection; empty selection means "inherit project default workflow", matching the revert route's blank-is-inherit behavior from FN-7556. diff --git a/.changeset/fn-7579-ask-user-exit-gate-nodes.md b/.changeset/fn-7579-ask-user-exit-gate-nodes.md deleted file mode 100644 index 66172c6c82..0000000000 --- a/.changeset/fn-7579-ask-user-exit-gate-nodes.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add "Ask user question" and "Exit gate" workflow nodes for mid-flow chat reach-out and early exit. -category: feature -dev: New IR node kinds `ask-user` (reuses await-input park/resume; surfaces the question in the task chat) and `exit-gate` (terminates the workflow early, optional condition). Editor palette + summaries + help updated; `prompt`+`awaitInput` remains a back-compat alias. diff --git a/.changeset/fn-7579-tracking-dedup-stale-issue.md b/.changeset/fn-7579-tracking-dedup-stale-issue.md deleted file mode 100644 index 9a3d2d26c8..0000000000 --- a/.changeset/fn-7579-tracking-dedup-stale-issue.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Stop GitHub tracking-issue creation from linking new tasks to old/closed issues. -category: fix -dev: github-tracking dedup now only reuses OPEN issues and requires a File-Scope path overlap (keyword-only matches no longer link). Prevents mis-linking a fresh task to a stale/resolved tracking issue (FN-7579). Setting `githubTrackingDedupEnabled` unchanged. diff --git a/.changeset/fn-7582-oversight-guideline-copy.md b/.changeset/fn-7582-oversight-guideline-copy.md deleted file mode 100644 index 4041f9f1c4..0000000000 --- a/.changeset/fn-7582-oversight-guideline-copy.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Clarify the oversight "Nudge unavailable" guideline so it no longer reads as an overseer fault. -category: fix -dev: TaskDetailModal oversight controls — reworded taskDetail.oversight.nudgeDisabledTitle and added taskDetail.oversight.nudgeSuppressedTitle to differentiate periodic-observation vs. manual-control states. No enablement/engine logic changed. diff --git a/.changeset/fn-7584-brainstorming-builtin.md b/.changeset/fn-7584-brainstorming-builtin.md deleted file mode 100644 index 21c73e7257..0000000000 --- a/.changeset/fn-7584-brainstorming-builtin.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a built-in "Brainstorming" workflow that talks to you before planning. -category: feature -dev: Registers `builtin:brainstorming` (non-default, default-enabled) composing FN-7579's `ask-user` → refine → `exit-gate`-on-approval phase ahead of the normal coding plan/execute/review/merge spine. Parity suite (`builtin-workflows.test.ts`) extended for the new entry. diff --git a/.changeset/fn-7591-coding-ideas-intake.md b/.changeset/fn-7591-coding-ideas-intake.md deleted file mode 100644 index 6db08f8a77..0000000000 --- a/.changeset/fn-7591-coding-ideas-intake.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: New tasks created under the Coding (Ideas) workflow now land in the Ideas column and wait for you to promote them. -category: fix -dev: Dashboard create surfaces (InlineCreateCard, QuickEntryBox, NewTaskModal, insight/todo → task) no longer hard-code column:"triage"; the store now resolves the selected/default workflow's intake column. InlineCreateCard forwards workflowId at create time instead of applying it post-create. Also fixed a glue-layer regression in `useTaskHandlers.ts` (`handleBoardQuickCreate`/`handleModalCreate`) that re-forced column:"triage" even after the UI surfaces stopped sending it. diff --git a/.changeset/fn-7591-intake-card-disappears.md b/.changeset/fn-7591-intake-card-disappears.md deleted file mode 100644 index 819f32512c..0000000000 --- a/.changeset/fn-7591-intake-card-disappears.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix tasks vanishing from the board after being added to a workflow like Coding (Ideas). -category: fix -dev: Board.tsx forces a board-workflows refetch (deferred one tick, signature-guarded) whenever a rendered task is missing from the taskWorkflowIds map, so its real workflow and intake column resolve regardless of which create surface added it; the single-workflow grouping also re-homes a task whose column its workflow no longer declares into the intake lane instead of dropping it. Fixes the FN-7591 regression where intake-column cards (column "ideas") fell back to the default workflow, which has no such column, and were filtered out until a manual reload. diff --git a/.changeset/fn-7593-before-after-top.md b/.changeset/fn-7593-before-after-top.md deleted file mode 100644 index 43186a0490..0000000000 --- a/.changeset/fn-7593-before-after-top.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Move the Before → After transformation summary to the top of generated task definitions. -category: fix -dev: Reorders the standard and fast triage `PROMPT.md` templates in packages/core/src/agent-prompts.ts so `## Before → After Transformation` is the first content section, ahead of `## Review Level` and `## Mission`, matching FN-7499's glance-verification intent. diff --git a/.changeset/fn-7597-priority-dropdown-matches-oversight.md b/.changeset/fn-7597-priority-dropdown-matches-oversight.md deleted file mode 100644 index 4d89e6363c..0000000000 --- a/.changeset/fn-7597-priority-dropdown-matches-oversight.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Task-detail Priority dropdown now matches the Oversight dropdown's size, border, and typography. -category: fix -dev: Removed the Priority-only forced select/option uppercase, added a neutral chip background scoped to `.detail-priority-chip.card-priority-badge--normal` for the untinted `normal` level, and reused the FN-7585 shared `--btn-border-width`/`--border`/`--detail-control-border-radius`/`--detail-priority-control-min-height` tokens so both dropdowns render as one control style across desktop and the mobile oversight-overflow surface. diff --git a/.changeset/fn-7599-planning-column-rename.md b/.changeset/fn-7599-planning-column-rename.md deleted file mode 100644 index 59cac03411..0000000000 --- a/.changeset/fn-7599-planning-column-rename.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Default workflow boards now label the intake column "Planning" instead of "Triage". -category: fix -dev: Renamed the `name` of the `id: "triage"` intake column to "Planning" in builtin-coding, builtin-stepwise-coding, and builtin-pr workflow IRs (column id unchanged; linear built-ins inherit via canonicalBuiltinWorkflowColumns). COLUMN_LABELS.triage was already "Planning". diff --git a/.changeset/fn-7600-oversight-nudge-detail-snapshot.md b/.changeset/fn-7600-oversight-nudge-detail-snapshot.md deleted file mode 100644 index 2690189613..0000000000 --- a/.changeset/fn-7600-oversight-nudge-detail-snapshot.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix the task-detail Nudge control staying disabled when the overseer is actively watching. -category: fix -dev: GET /api/tasks/:id now attaches the transient plannerOverseerState snapshot (mirrors the list route); TaskDetailModal reads the snapshot from workingTask so detail refetches no longer drop it. diff --git a/.changeset/fn-coding-ideas-workflow.md b/.changeset/fn-coding-ideas-workflow.md deleted file mode 100644 index 4f8fa8f9e1..0000000000 --- a/.changeset/fn-coding-ideas-workflow.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": minor ---- - -summary: Add a "Coding (Ideas)" workflow with a manual Ideas intake and a merged Todo planner column. -category: feature -dev: New `builtin:coding-ideas` clones the default stepwise pipeline with an `ideas` intake (autoTriage:false) in front of a merged `todo` planner+capacity column. createTask lands cards in the workflow's intake column; the triage service plans unplanned todo tasks in place; the scheduler skips bootstrap-prompt todo tasks; TaskCard gains a Start button and a Ready badge. diff --git a/.changeset/mission-triage-branch-strategy-and-parked-validation.md b/.changeset/mission-triage-branch-strategy-and-parked-validation.md deleted file mode 100644 index 0966318fa7..0000000000 --- a/.changeset/mission-triage-branch-strategy-and-parked-validation.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Honor mission branchStrategy when triage omits branchAssignment; skip validation for inactive missions. -category: fix -dev: resolveBranchAssignmentContext returns undefined for absent mode so triage falls back to mission.branchStrategy; processTaskOutcome gates on mission.status === "active" like recoverActiveMissions. diff --git a/.changeset/planner-overseer-no-recover-healthy-tasks.md b/.changeset/planner-overseer-no-recover-healthy-tasks.md deleted file mode 100644 index 9c96edc1ed..0000000000 --- a/.changeset/planner-overseer-no-recover-healthy-tasks.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Planner overseer no longer marks healthy in-progress tasks as "recovering" or steers them. -category: fix -dev: `decidePlannerRecovery` now returns `none` for healthy (`progressing`/`complete`) and `awaiting-human` executor/workflow-gate signals instead of falling through to `inject_guidance`; only `stuck`/`blocked`/`failed` trigger autonomous steering. Also dedupes the `PlannerOverseerMonitor` activity-feed heartbeat so an unchanged `(stage, signal, reason)` observation is logged once per change, not every poll tick. Fixes the "overseer recovering" badge appearing on every autonomous card and the needless AI-consuming guidance injections (FN-7577). diff --git a/CHANGELOG.md b/CHANGELOG.md index 18882c459c..60d1b72104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,60 +2,365 @@ User-facing release notes aggregated across all packages. This file is auto-synced from each `packages/*/CHANGELOG.md` by `scripts/release.mjs` — do not edit by hand. -## 0.55.0 +## 0.56.0 ### New -- Allow configuring permissions for ephemeral and permanent agents. -- Add a Settings override for the local Cursor CLI binary path. -- Add GitLab instance URL settings for GitLab.com and self-managed servers. -- Add GitLab access-token settings for personal, project, and group tokens. -- Add GitLab project issue, group issue, and merge request imports. -- Display linked GitLab tracking metadata and stale badges on tasks. -- Add GitLab comment and auto-close lifecycle actions for linked work items. -- Add GitLab as a Command Center Signals connector. -- Add explicit onboarding choices to use, initialize, or clone a git repository. -- Let chat update existing agents without delete/recreate. -- Add sidebar rename buttons to direct Chat conversations. -- Add a bundled Linear import plugin for creating tasks from Linear issues. -- Add a mandatory Planning Mode deepening checkpoint before final summaries. -- Add visible create buttons and recursive search to Project Files. -- Collapse mobile Chat thread controls into one compact header row. -- Add a GitLab enable toggle and collapsible Settings controls. +- Add a workflow setting to disable automatic large-task triage splitting. +- Expand first-run AI provider quick-start choices beyond Anthropic. +- Show Git prerequisite guidance during first-run GitHub onboarding. +- Add GitHub OAuth and CLI setup actions to first-run onboarding. +- Add configurable dashboard keyboard shortcuts for Quick Chat and Terminal. +- Add search in Settings so operators can find settings faster. +- Add before-to-after transformation summaries to generated task definitions. +- Add a pinned below-application layout option for the dashboard terminal. +- Show first-token and tool processing durations in task agent logs. +- Settings descriptions now show each setting's default value. +- Add a Reset Settings button to restore a menu's or all project settings to defaults. +- Add a per-workflow planner oversight level setting (Off, Observe, Steer, Autonomous recovery). +- Tasks can override the workflow planner oversight level (Off, Observe, Steer, Autonomous recovery). +- Planner oversight now defaults to full steering/control for every workflow unless explicitly changed. +- Planner oversight now monitors tasks across executor, reviewer, merger, pull-request, and workflow-gate stages. +- Planner oversight can autonomously inject guidance, retry stuck/failed steps, and request fixes within bounded limits. +- Planner oversight now requires confirmation before merge/PR actions and destructive/external side effects. +- Planner overseer now stays fully hands-off for paused tasks and auto-merge-off / human-review tasks. +- Configure planner oversight level per task and per project in the workflow editor and task create/detail. +- Add a configurable planner-overseer notification verbosity level (Silent/Errors/Important/All). +- Add a task-detail planner-overseer intervention timeline (stage, reason, action, outcome, attempts, links). +- Emit planner-overseer run-audit events for observations, steering, retries, recovery, confirmations, and escalations. +- Add an intelligent git-revert engine service and POST /api/tasks/:id/revert route. +- Add an AI-undo fallback task when reverting a done task via git conflicts or is unsupported. +- Add a Revert action to Done/Archived task cards to undo landed changes. +- Capture a structured performance snapshot when an agent task completes. +- Task cards can now show the planner overseer's active state (idle/watching/steering/recovering/awaiting-confirmation). +- Original task prompt now renders as Markdown and is collapsed by default in the task Plan tab. +- Support reverting multi-repo workspace tasks via git, all-or-nothing across sub-repos. +- Add per-sha revert commit granularity to the task revert API and service. +- Add a dedicated Keyboard Shortcuts settings section with click-to-record capture and more configurable actions. +- Open a revert PR for done/archived tasks when autoMerge is disabled instead of refusing. +- AI-undo tasks now default to a configurable, stricter review workflow. +- Plan auto-approval is now the default; specified tasks skip manual approval unless you opt into workflow/require-all. +- Move the planner intervention timeline into the task Activity view dropdown. +- Fusion self-repo issue-close comments now show current and target release versions. +- Open one revert PR per sub-repo for workspace tasks when autoMerge is disabled. +- Add a Settings → General picker to choose the workflow used for AI-undo (revert) tasks. +- Add "Ask user question" and "Exit gate" workflow nodes for mid-flow chat reach-out and early exit. +- Add a built-in "Brainstorming" workflow that talks to you before planning. +- Add a "Coding (Ideas)" workflow with a manual Ideas intake and a merged Todo planner column. ### Fixed -- Stop recurring Windows Terminal warning popups during terminal startup. -- Fix dashboard localStorage quota exhaustion from stale SWR caches and add a Clear local data escape hatch. -- Fix `fusion desktop` on Windows and published npm installs (Electron dependency, GPU/sandbox flags, dashboard reuse). -- First-run agent setup no longer errors on a duplicate CEO; desktop Switch-server button now opens the connection menu. -- Allow operators to delete archived tasks. -- Show workflow template block boundary connectors in the graph editor. -- Preserve the selected dashboard project across browser refreshes. -- Detect Cursor CLI installations that expose Windows cmd or bat shims. -- Preserve GitLab tracking metadata for CLI and extension imports. -- Keep Planning Mode Refine Further from getting stuck on duplicate generation. -- Show task status badges on Documents task groups. -- Suppress misleading Anthropic Subscription re-login banners when another Anthropic auth method is active. -- Keep Anthropic authentication cards grouped near the top in Settings. -- Stop planner model fallback loops with a clear terminal triage error. -- Recover stale task branch-group references from Task Detail after server restarts. -- Return GitHub issue import actions to the main issue list. -- Prevent Planning Mode sessions from failing when MCP resolution returns no shaped result. -- Fix Android mobile terminal spacing while the keyboard is open. -- Fix Last 30 days token usage to include every model in Command Center. -- Include supported chat interactions in Command Center token usage totals. -- Preserve workflow setting edits made while a values save is still in flight. -- Preserve migrated workflow settings when project identity is assigned later. -- Show the bundled Linear Import plugin in Plugin Manager and dashboard plugin surfaces. -- Fix the mobile Chat header so back navigation and session selection stay on one row. -- Fix iOS mobile terminal spacing when opening terminals with the keyboard already visible. -- Default fresh startup and theme reset to System mode. -- Remember task popup size and position when switching between tasks. -- Fix iPhone Safari terminal text spacing with the keyboard open. -- Count planning tasks correctly in the dashboard footer queue metric. -- Show conversation titles in the mobile Chat dropdown. -- Merges no longer fail when a task adds a dependency without updating the lockfile. +- Show active Plan Review progress on triage task cards. +- Clarify task-detail oversight Nudge/Explain controls: visible label, disabled reason, always-openable Explain panel. +- Unify border, radius, and height of the task-detail Priority/Execution/Oversight controls. +- Keep the task-detail Activity view menu open during mobile iOS taps. +- Fix Anthropic subscription login when pasted callback URLs contain fragment OAuth parameters. +- Fix Claude/Anthropic subscription re-login showing "Login did not complete" after logging out. +- Stop self-healing from killing actively-running tasks after ~30 minutes. +- Stop Windows Terminal version dialogs from popping up when opening the dashboard or Settings on Windows. +- Select newly created folders automatically during project setup. +- Prevent Desktop update banners from using 0.0.0 as the current version. +- Quit Fusion Desktop on Windows when the window is closed. +- Open desktop Anthropic Subscription OAuth logins in the system browser. +- Delay GitHub setup warnings for one day and add a dashboard connect action. +- Fix a false AI engine not running banner in desktop mode. +- Clarify the desktop Connection Manager add-remote flow. +- Restore Local Server in the desktop Switch server list. +- Make right-dock task list clicks respect the task popup setting. +- Auto-retry retryable Code Review remediation failures. +- Fix no-op task branch recovery after a previously landed task. +- Allow documented source-free task-artifact deliveries to finish without commits. +- Fix direct merges so Push to remote after merge honors the configured remote and branch. +- Keep task popups on the board layer with Activity menus above them. +- Keep accepted chat requests waiting instead of showing false first-event timeout failures. +- Show each task's original prompt in the Plan tab alongside the generated plan. +- Restore terminal Ctrl/Cmd copy and paste shortcuts. +- Fix mobile Chat composer being hidden behind the keyboard accessory bar. +- Auto-approve now reliably sends specified plans to the board without a manual approval stop. +- Fix the in-dashboard Switch server menu not switching desktop local/remote. +- Fix branch group completion checklists to show accurate landed/finished counts. +- Branch groups no longer report complete (or become promotable) when an unlanded member is archived. +- Fix the global GitLab integration setting not persisting when saved. +- Fix task-detail Activity view dropdown not opening reliably on mobile. +- Manual "Run now" for the Database Backup automation now runs in-process like the scheduler, matching cron behavior. +- Task cards no longer show the "Auto-recovery" oversight badge unless oversight is explicitly configured. +- Remove the per-card overseer-state ("Executor") badge from task cards. +- Fix agent-created artifacts not appearing live in the dashboard artifacts view. +- Fix the mobile terminal shortcut bar so it scrolls horizontally to reach every key. +- Planner-oversight intervention timeline now populates from real engine activity. +- Show the "Global" prefix on the Authentication entry in the mobile Settings picker. +- Tasks held for release authorization or Plan Review are now shown distinctly, so auto-approve no longer looks broken. +- Move mobile terminal controls into a bottom footer so they no longer crowd the header, with a scrollable shortcut bar. +- Stop the release-authorization gate from holding tasks that merely disclaim releasing. +- Fix mobile terminal text still rendering with excess inter-character gaps after font-load settle. +- Stop Plan Review from looping tasks forever and fix its "can't find the plan" reviews. +- Planner-overseer task badge now shows a readable label and explains what it is waiting on. +- Plan approve/reject API now blocks release-authorization holds, requiring the authorization marker first. +- Pin the mobile terminal close (X) button to the top-right corner so it is easy to find and tap. +- Fix mobile terminal excess character spacing that survived earlier font-remeasure fixes. +- Manual plan approval no longer re-asks you to approve a plan you already approved when it hasn't changed. +- Expired Claude subscription logins now show disconnected with a re-login prompt; tokens auto-refresh before expiry. +- Anthropic subscription reads now refresh the OAuth token automatically instead of silently failing when expired. +- Stop GitHub tracking-issue creation from linking new tasks to old/closed issues. +- Clarify the oversight "Nudge unavailable" guideline so it no longer reads as an overseer fault. +- New tasks created under the Coding (Ideas) workflow now land in the Ideas column and wait for you to promote them. +- Fix tasks vanishing from the board after being added to a workflow like Coding (Ideas). +- Move the Before → After transformation summary to the top of generated task definitions. +- Task-detail Priority dropdown now matches the Oversight dropdown's size, border, and typography. +- Default workflow boards now label the intake column "Planning" instead of "Triage". +- Fix the task-detail Nudge control staying disabled when the overseer is actively watching. +- Honor mission branchStrategy when triage omits branchAssignment; skip validation for inactive missions. +- Planner overseer no longer marks healthy in-progress tasks as "recovering" or steers them. + +### Breaking + +- Remove the eye icon markdown/plain toggle from chat; messages always render as Markdown. + +### Internal + +- Rename downloadable CLI release binaries to the fn-cli- base name. + +## 0.55.0 + +### @fusion/dashboard + +#### Patch Changes + +- @fusion/core@0.55.0 +- @fusion/engine@0.55.0 +- @fusion/i18n@0.39.19 +- @fusion-plugin-examples/cli-printing-press@0.1.36 +- @fusion-plugin-examples/compound-engineering@0.1.19 +- @fusion-plugin-examples/dependency-graph@0.1.50 +- @fusion-plugin-examples/roadmap@0.1.38 +- @fusion-plugin-examples/cursor-runtime@0.1.38 +- @fusion-plugin-examples/droid-runtime@0.1.45 +- @fusion-plugin-examples/hermes-runtime@0.2.69 +- @fusion-plugin-examples/openclaw-runtime@0.2.69 +- @fusion-plugin-examples/paperclip-runtime@0.2.69 + +### @fusion/desktop + +#### Patch Changes + +- @fusion/core@0.55.0 +- @fusion/dashboard@0.55.0 +- @fusion/engine@0.55.0 + +### @fusion/engine + +#### Patch Changes + +- @fusion/core@0.55.0 +- @fusion/pi-claude-cli@0.55.0 + +### @fusion/plugin-sdk + +#### Patch Changes + +- @fusion/core@0.55.0 + +### @runfusion/fusion + +#### Minor Changes + +- 8580910: summary: Allow configuring permissions for ephemeral and permanent agents. + category: feature + dev: Applies capability grants and runtime permission policies consistently across agent lifetimes. +- 5738766: summary: Add GitLab instance URL settings for GitLab.com and self-managed servers. + category: feature + dev: Adds typed GitLab web/API URL configuration and dashboard controls for later GitLab integration subtasks. +- fafe4c9: summary: Add GitLab access-token settings for personal, project, and group tokens. + category: feature + dev: Documents required GitLab API scopes and adds token auth resolution for later GitLab integration tasks. +- 865dec2: summary: Add GitLab project issue, group issue, and merge request imports. + category: feature + dev: Adds HTTP API GitLab import routes, dashboard affordances, CLI commands, and extension tools. +- 34be333: summary: Add GitLab comment and auto-close lifecycle actions for linked work items. + category: feature + dev: Uses GitLab REST notes and state-event APIs with configured self-managed instance URLs. +- 1f6befc: summary: Add GitLab as a Command Center Signals connector. + category: feature + dev: Adds GitLab webhook token verification and issue/MR signal normalization for Command Center incidents. +- 91737b1: summary: Add explicit onboarding choices to use, initialize, or clone a git repository. + category: feature + dev: Setup wizard now sends gitSetupMode while preserving cloneUrl-only project registration clients. +- 91fb53f: summary: Let chat update existing agents without delete/recreate. + category: feature + dev: Adds the fn_agent_update Pi extension tool for scoped AgentStore.updateAgent config edits. +- dfb6e52: summary: Add a bundled Linear import plugin for creating tasks from Linear issues. + category: feature + dev: Ships fusion-plugin-linear-import with plugin settings, routes, tools, dashboard view, and bundled-plugin packaging. +- ec0256f: summary: Add a mandatory Planning Mode deepening checkpoint before final summaries. + category: feature + dev: Planning sessions now persist pending summaries behind a "Would you like to go deeper?" checkpoint. +- 5984f32: summary: Add visible create buttons and recursive search to Project Files. + category: feature + dev: Files — Project uses the existing workspace-safe create and /files/search APIs with settings pickers left compact. +- f973334: summary: Add a GitLab enable toggle and collapsible Settings controls. + category: feature + dev: Adds gitlabEnabled gating for GitLab API operations while preserving saved configuration. + +#### Patch Changes + +- 306e516: summary: Stop recurring Windows Terminal warning popups during terminal startup. + category: fix + dev: Keeps embedded terminal bootstrap on supported shells and surfaces actionable inline errors. +- 9df13c7: summary: Fix dashboard localStorage quota exhaustion from stale SWR caches and add a Clear local data escape hatch. + category: fix + dev: Stale SWR hydration entries (per-chat-session/per-room message caches) were never garbage-collected; readCache now lazily deletes stale entries, a boot sweep prunes anything older than 24h, and Settings → General exposes a user-facing "Clear local data" button that preserves the auth token. +- c7641f9: summary: Fix `fusion desktop` on Windows and published npm installs (Electron dependency, GPU/sandbox flags, dashboard reuse). + category: fix + dev: `packages/cli/package.json` now depends on `electron` at runtime; previously the desktop launcher called `require("electron")`, which is only available inside the source checkout (via `pnpm-workspace.yaml` `onlyBuiltDependencies`) and is missing for npm consumers, causing `fusion desktop` to hang or fail silently. The launcher now applies GPU/sandbox-disabling Electron flags only on Windows (`os.platform() === "win32"`), keeps hardware acceleration and the Chromium sandbox on macOS/Linux, exports `FUSION_SERVER_PORT` so the desktop reuses the CLI-started dashboard instead of double-binding ports, and isolates desktop user-data under `~/.fusion/desktop-user-data`. Relocating the profile performs a one-time copy of the previous default Electron profile (`user-data-migration.ts`) so upgrading operators keep window geometry/session. `packages/desktop/scripts/build.ts` now fails the build if `main.js`/`preload.js`/`client/index.html` are missing from `dist/` or the staged `deploy/dist/`, preventing shipping an incomplete `app.asar`. +- da662c4: summary: First-run agent setup no longer errors on a duplicate CEO; desktop Switch-server button now opens the connection menu. + category: fix + dev: Onboarding agent creation (ModelOnboardingModal + SetupWizardModal) treats a 409 "Agent with this name already exists" as success and advances, since the default CEO can be created from more than one first-run surface. The desktop preload now bridges the `shell:open-connection-manager` IPC (sent by main when the header Switch-server button is clicked) into the `window` DOM event ShellContext listens for, so NativeShellConnectionManager (Local/Remote toggle + remote profiles) actually opens. +- 377eee6: summary: Allow operators to delete archived tasks. + category: fix + dev: Extends task deletion to archive-db snapshots while preserving soft-delete tombstones and ID reservation. +- 9ee33f9: summary: Show workflow template block boundary connectors in the graph editor. + category: fix + dev: Adds visual-only foreach/loop/optional-group template boundary edges that are filtered from persisted IR. +- 3453d16: summary: Preserve the selected dashboard project across browser refreshes. + category: fix + dev: Project selection now updates and hydrates from the existing `?project=` dashboard URL contract. +- 777f647: summary: Detect Cursor CLI installations that expose Windows cmd or bat shims. + category: fix + dev: Cursor runtime probes and model discovery now shell-spawn only on Windows and preserve spawn diagnostics. +- abb1917: summary: Add a Settings override for the local Cursor CLI binary path. + category: feature + dev: Adds global `cursorCliBinaryPath` and threads it through Cursor CLI probes, auth status, enable validation, and model discovery. +- b8e126e: summary: Display linked GitLab tracking metadata and stale badges on tasks. + category: feature + dev: Persists GitLab tracking metadata separately from GitHub tracking fields. +- c49a933: summary: Preserve GitLab tracking metadata for CLI and extension imports. + category: fix + dev: GitLab project, group, and merge-request imports now carry gitlabTracking metadata alongside sourceIssue provenance. +- 53d5bb1: summary: Keep Planning Mode Refine Further from getting stuck on duplicate generation. + category: fix + dev: Guards completed-summary refinement as a single-flight UI turn and preserves the active planning stream on same-refine in-progress responses. +- b493a1e: summary: Show task status badges on Documents task groups. + category: fix + dev: DocumentsView now renders taskColumn metadata in task document group headers and covers collapsed done/non-done states. +- e5be924: summary: Suppress misleading Anthropic Subscription re-login banners when another Anthropic auth method is active. + category: fix + dev: Keeps subscription OAuth expired in Settings while hiding only the global urgent banner entry when API key or Claude CLI auth is active. +- 326c72b: summary: Keep Anthropic authentication cards grouped near the top in Settings. + category: fix + dev: Sorts Claude CLI, Anthropic Subscription, and Anthropic API Key before other auth cards within each auth group. +- 1d6bb08: summary: Stop planner model fallback loops with a clear terminal triage error. + category: fix + dev: Bounds prompt-time/session-creation model fallback exhaustion and persists failed triage state. +- aa8f1f3: summary: Recover stale task branch-group references from Task Detail after server restarts. + category: fix + dev: Adds branch_group restart regression coverage and non-origin integration-branch diagnostics. +- 72adb52: summary: Add sidebar rename buttons to direct Chat conversations. + category: feature + dev: Reuses ChatView's existing rename dialog and useChat renameSession path. +- 25fecd7: summary: Return GitHub issue import actions to the main issue list. + category: fix + dev: Updates Import Tasks issue import/close navigation and regression coverage. +- ffcb54b: summary: Prevent Planning Mode sessions from failing when MCP resolution returns no shaped result. + category: fix + dev: Dashboard planning lanes now default malformed MCP resolver output to an empty server set while preserving MCP forwarding. +- 8d7abb8: summary: Fix Android mobile terminal spacing while the keyboard is open. + category: fix + dev: Terminal mobile sizing now tracks visualViewport width for keyboard-open xterm fits. +- 22fd0da: summary: Fix Last 30 days token usage to include every model in Command Center. + category: fix + dev: Corrects Command Center token analytics range attribution for durable multi-model task usage. +- 765218f: summary: Include supported chat interactions in Command Center token usage totals. + category: fix + dev: Records chat-session and room-responder token usage separately from task execution tokens and aggregates both sources in token analytics. +- 1d6011c: summary: Collapse mobile Chat thread controls into one compact header row. + category: feature + dev: Mobile direct-chat moves back/session controls into ViewHeader and floats the Markdown/plain toggle. +- 2050323: summary: Preserve workflow setting edits made while a values save is still in flight. + category: fix + dev: WorkflowSettingsPanel and Project Models workflow lane saves now clear only snapshot-matching pending keys. +- 3fac409: summary: Preserve migrated workflow settings when project identity is assigned later. + category: fix + dev: Backfills rootDir-keyed workflow_settings rows into the durable project identity row, keeping identity values on conflicts. +- 78ebeaf: summary: Show the bundled Linear Import plugin in Plugin Manager and dashboard plugin surfaces. + category: fix + dev: Keeps fusion-plugin-linear-import registered across the built-in Plugin Manager catalog while reusing existing registry, dashboard view, and bundled packaging paths. +- 1430a42: summary: Fix the mobile Chat header so back navigation and session selection stay on one row. + category: fix + dev: Keeps the direct-chat mobile header collapsed while preserving desktop and room-chat layouts. +- 7a4b0bf: summary: Fix iOS mobile terminal spacing when opening terminals with the keyboard already visible. + category: fix + dev: Seeds iOS keyboard-open viewport baselines for TerminalModal and SessionTerminal before xterm fit/resize. +- 00afb22: summary: Default fresh startup and theme reset to System mode. + category: fix + dev: Fresh global settings, pre-hydration scripts, and Appearance reset now keep Shadcn Ember while following OS light/dark preference. +- 9532520: summary: Remember task popup size and position when switching between tasks. + category: fix + dev: Task-detail FloatingWindow instances share the `floating-window:task-detail` geometry key. +- 7300bf5: summary: Fix iPhone Safari terminal text spacing with the keyboard open. + category: fix + dev: Disables WebKit text-size adjustment inside dashboard xterm measurement subtrees. +- 30a11ac: summary: Count planning tasks correctly in the dashboard footer queue metric. + category: fix + dev: Footer counter tests now cover queued, running, stuck, blocked, review, overlap, background AI, and Done absence. +- d9ef514: summary: Show conversation titles in the mobile Chat dropdown. + category: fix + dev: Keeps the provider logo while removing model-name text from the mobile direct-chat trigger. +- e4349ee: summary: Merges no longer fail when a task adds a dependency without updating the lockfile. + category: fix + dev: In merge-dependency-sync.ts, an inferred frozen install (pnpm/yarn/bun) that fails with an outdated-lockfile error now retries once non-frozen (pnpm gets explicit --no-frozen-lockfile) to regenerate the lockfile in the clean-room worktree, recomputing the install marker. Configured worktreeInitCommand keeps its authoritative frozen intent and still hard-fails. Surfaced via the merge:ai-deps-sync run-audit event (healed/healedCommand). + +### runfusion.ai + +#### Patch Changes + +- Updated dependencies [306e516] +- Updated dependencies [9df13c7] +- Updated dependencies [c7641f9] +- Updated dependencies [da662c4] +- Updated dependencies [377eee6] +- Updated dependencies [9ee33f9] +- Updated dependencies [8580910] +- Updated dependencies [3453d16] +- Updated dependencies [777f647] +- Updated dependencies [abb1917] +- Updated dependencies [5738766] +- Updated dependencies [fafe4c9] +- Updated dependencies [865dec2] +- Updated dependencies [b8e126e] +- Updated dependencies [34be333] +- Updated dependencies [1f6befc] +- Updated dependencies [c49a933] +- Updated dependencies [53d5bb1] +- Updated dependencies [91737b1] +- Updated dependencies [b493a1e] +- Updated dependencies [e5be924] +- Updated dependencies [326c72b] +- Updated dependencies [1d6bb08] +- Updated dependencies [aa8f1f3] +- Updated dependencies [91fb53f] +- Updated dependencies [72adb52] +- Updated dependencies [25fecd7] +- Updated dependencies [dfb6e52] +- Updated dependencies [ec0256f] +- Updated dependencies [5984f32] +- Updated dependencies [ffcb54b] +- Updated dependencies [8d7abb8] +- Updated dependencies [22fd0da] +- Updated dependencies [765218f] +- Updated dependencies [1d6011c] +- Updated dependencies [2050323] +- Updated dependencies [3fac409] +- Updated dependencies [f973334] +- Updated dependencies [78ebeaf] +- Updated dependencies [1430a42] +- Updated dependencies [7a4b0bf] +- Updated dependencies [00afb22] +- Updated dependencies [9532520] +- Updated dependencies [7300bf5] +- Updated dependencies [30a11ac] +- Updated dependencies [d9ef514] +- Updated dependencies [e4349ee] + - @runfusion/fusion@0.55.0 ## 0.54.0 @@ -12122,6 +12427,14 @@ for reference. - Updated dependencies [a2ed6d0] - @runfusion/fusion@0.1.0 +## 0.39.20 + +### @fusion/i18n + +#### Patch Changes + +- @fusion/core@0.56.0 + ## 0.39.19 ### @fusion/i18n @@ -12276,6 +12589,14 @@ for reference. - @fusion/core@0.40.0 +## 0.11.46 + +### @fusion/droid-cli + +#### Patch Changes + +- @fusion-plugin-examples/droid-runtime@0.1.46 + ## 0.11.45 ### @fusion/droid-cli diff --git a/package.json b/package.json index 35dcd38c79..dd01cdf9c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fusion-workspace", - "version": "0.55.0", + "version": "0.56.0", "private": true, "license": "MIT", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/cli-alias/CHANGELOG.md b/packages/cli-alias/CHANGELOG.md index f0deb8a7cb..cbcb5126d8 100644 --- a/packages/cli-alias/CHANGELOG.md +++ b/packages/cli-alias/CHANGELOG.md @@ -1,5 +1,115 @@ # runfusion.ai +## 0.56.0 + +### Patch Changes + +- Updated dependencies [8668a05] +- Updated dependencies [978cdda] +- Updated dependencies [635fca2] +- Updated dependencies [3d55102] +- Updated dependencies [0f1cd0a] +- Updated dependencies [b42ba9f] +- Updated dependencies [a7559b0] +- Updated dependencies [4b530a6] +- Updated dependencies [a5ac3c3] +- Updated dependencies [8912399] +- Updated dependencies [d16c8b4] +- Updated dependencies [b800f7d] +- Updated dependencies [315f3bc] +- Updated dependencies [9dc248e] +- Updated dependencies [52dbc0e] +- Updated dependencies [ced783e] +- Updated dependencies [50cdab1] +- Updated dependencies [50786f2] +- Updated dependencies [b4b1f6d] +- Updated dependencies [e8b7362] +- Updated dependencies [0900a38] +- Updated dependencies [a2b09f2] +- Updated dependencies [0f05156] +- Updated dependencies [20184ac] +- Updated dependencies [82493e0] +- Updated dependencies [5689346] +- Updated dependencies [b42be87] +- Updated dependencies [2f23d22] +- Updated dependencies [efa8105] +- Updated dependencies [61c8bdc] +- Updated dependencies [e8dc2ae] +- Updated dependencies [d2e3134] +- Updated dependencies [b0208c1] +- Updated dependencies [7d8a1b8] +- Updated dependencies [2797803] +- Updated dependencies [a2d6349] +- Updated dependencies [4baa4c4] +- Updated dependencies [87a700c] +- Updated dependencies [68f5153] +- Updated dependencies [aa757bc] +- Updated dependencies [0689250] +- Updated dependencies [12a6d1b] +- Updated dependencies [81f2053] +- Updated dependencies [2cc84b5] +- Updated dependencies [79ab367] +- Updated dependencies [c16cc9e] +- Updated dependencies [aae603b] +- Updated dependencies [d10ea9a] +- Updated dependencies [bf68839] +- Updated dependencies [53d7b7e] +- Updated dependencies [c4d81fe] +- Updated dependencies [e7cb2f1] +- Updated dependencies [4707eb5] +- Updated dependencies [3b52a4d] +- Updated dependencies [5ad8ec8] +- Updated dependencies [726cbf8] +- Updated dependencies [36bd74e] +- Updated dependencies [df0be88] +- Updated dependencies [ec9ac61] +- Updated dependencies [8d36b99] +- Updated dependencies [ad744aa] +- Updated dependencies [5c3d58a] +- Updated dependencies [b4be515] +- Updated dependencies [62ddb19] +- Updated dependencies [883c73e] +- Updated dependencies [2ed06f9] +- Updated dependencies [8c6f76c] +- Updated dependencies [d09b57f] +- Updated dependencies [3d58260] +- Updated dependencies [052a277] +- Updated dependencies [f992e6a] +- Updated dependencies [2df6c35] +- Updated dependencies [94e9d15] +- Updated dependencies [3dd227b] +- Updated dependencies [6e4c207] +- Updated dependencies [6d364fc] +- Updated dependencies [b471aec] +- Updated dependencies [9d4a45b] +- Updated dependencies [72b77bf] +- Updated dependencies [c08498e] +- Updated dependencies [24b27e8] +- Updated dependencies [fb45157] +- Updated dependencies [7c0be53] +- Updated dependencies [71dfd3a] +- Updated dependencies [9592e3a] +- Updated dependencies [c31f9ef] +- Updated dependencies [ce9df29] +- Updated dependencies [78d4db9] +- Updated dependencies [196abb5] +- Updated dependencies [7435849] +- Updated dependencies [73b38ba] +- Updated dependencies [42bbe58] +- Updated dependencies [45e5a26] +- Updated dependencies [a1a6b09] +- Updated dependencies [53fe0d7] +- Updated dependencies [cf3fe8b] +- Updated dependencies [8b4e522] +- Updated dependencies [f30d55f] +- Updated dependencies [20379e8] +- Updated dependencies [e0f3d3d] +- Updated dependencies [5b193d2] +- Updated dependencies [ecbbb29] +- Updated dependencies [546ef16] +- Updated dependencies [b173f76] + - @runfusion/fusion@0.56.0 + ## 0.55.0 ### Patch Changes diff --git a/packages/cli-alias/package.json b/packages/cli-alias/package.json index 4188db1797..214ec777c8 100644 --- a/packages/cli-alias/package.json +++ b/packages/cli-alias/package.json @@ -1,6 +1,6 @@ { "name": "runfusion.ai", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Launch Fusion with `npx runfusion.ai` — tiny alias for @runfusion/fusion.", "homepage": "https://runfusion.ai", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a6f808e15e..7f2d684887 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,325 @@ # @runfusion/fusion +## 0.56.0 + +### Minor Changes + +- d16c8b4: summary: Expand first-run AI provider quick-start choices beyond Anthropic. + category: feature + dev: Moves advanced/all-provider onboarding controls under the quick-start provider section. +- 315f3bc: summary: Show Git prerequisite guidance during first-run GitHub onboarding. + category: feature + dev: Adds bounded server-host git availability to auth status and onboarding. +- 50cdab1: summary: Add GitHub OAuth and CLI setup actions to first-run onboarding. + category: feature + dev: GitHub onboarding now shows in-flow OAuth connect, gh auth login, and gh install guidance. +- 2f23d22: summary: Add configurable dashboard keyboard shortcuts for Quick Chat and Terminal. + category: feature + dev: Global dashboardKeyboardShortcuts settings, guarded document-level key handling, and Escape topmost-popup dismissal. +- efa8105: summary: Add search in Settings so operators can find settings faster. + category: feature + dev: Dashboard Settings filters visible sections by setting labels and keywords. +- 7d8a1b8: summary: Add a pinned below-application layout option for the dashboard terminal. + category: feature + dev: Terminal display mode now supports persisted docked, floating, and below layouts, with header controls replacing the footer shell. +- 87a700c: summary: Add a Reset Settings button to restore a menu's or all project settings to defaults. + category: feature + dev: New tested section→keys (scope-aware) registry (packages/dashboard/app/components/settings/section-keys.ts) drives per-menu reset via updateSettings/updateGlobalSettings with null-as-delete; non-blob sections (secrets, MCP, plugins, memory, auth, prompts, CLI agents, runtimes) are excluded with a documented reason. +- 68f5153: summary: Add a per-workflow planner oversight level setting (Off, Observe, Steer, Autonomous recovery). + category: feature + dev: New workflow setting `plannerOversightLevel` declared in BUILTIN_OVERSIGHT_SETTINGS; default `autonomous`. Per-task override and engine behavior land in follow-up tasks. +- aa757bc: summary: Tasks can override the workflow planner oversight level (Off, Observe, Steer, Autonomous recovery). + category: feature + dev: New nullable Task.plannerOversightLevel field (migration 137, SCHEMA_VERSION 137) mirroring executionMode; NULL inherits the workflow setting. Adds resolveEffectivePlannerOversightLevel precedence helper. Dashboard UI/API threading and engine behavior land in follow-up tasks. +- 0689250: summary: Planner oversight now defaults to full steering/control for every workflow unless explicitly changed. + category: feature + dev: Confirms the `plannerOversightLevel` workflow-setting default is the highest (autonomous) level; unset workflow value and unset per-task override both resolve to full steering via `resolveEffectivePlannerOversightLevel` (task override → workflow effective value → autonomous), adding dedicated regression coverage for the "unless explicitly disabled" precedence. +- 12a6d1b: summary: Planner oversight now monitors tasks across executor, reviewer, merger, pull-request, and workflow-gate stages. + category: feature + dev: Adds records-only PlannerOverseerMonitor + resolveWatchedStage + OverseerStageObservation in @fusion/engine, gated by resolveEffectivePlannerOversightLevel (off = no observation) and wired into ProjectEngine via a bounded poll. Steering/recovery and UI land in FN-7512/FN-7515+. +- 81f2053: summary: Planner oversight can autonomously inject guidance, retry stuck/failed steps, and request fixes within bounded limits. + category: feature + dev: Adds pure `decidePlannerRecovery` + recovery types (core) and `PlannerRecoveryController` with injected guidance/retry/targeted-fix handlers (engine), consuming the FN-7511 observation. Acts only at effective level `autonomous`, caps attempts per (task, stage) via `PLANNER_RECOVERY_MAX_ATTEMPTS`, skips user-paused tasks, and excludes merge/PR/destructive actions (deferred to FN-7513) and comprehensive human-control safeguards (FN-7514). +- 2cc84b5: summary: Planner oversight now requires confirmation before merge/PR actions and destructive/external side effects. + category: feature + dev: Adds `PlannerActionSideEffectClass` + `PlannerConfirmationRequest` and `classifyPlannerActionSideEffect`/`requiresPlannerConfirmation` (core), extends `decidePlannerRecovery` with an `await_confirmation` action, and adds `requestConfirmation`/`resolveConfirmation` gating to `PlannerRecoveryController` (engine). Merge/PR and destructive/external actions never execute without a recorded approval; bounded recovery (guidance/retry/targeted-fix) is unchanged. UX rendering, human-control safeguards, timeline, and run-audit land in follow-up tasks. +- 79ab367: summary: Planner overseer now stays fully hands-off for paused tasks and auto-merge-off / human-review tasks. + category: feature + dev: Adds the pure `evaluateOverseerHumanControl` policy (packages/engine/src/overseer-human-control-policy.ts), consulted at the top of `PlannerRecoveryController.tick()` before any action classification, confirmation gating, steering, retry, or dispatch — so a user-paused or `autoMerge:false`/human-review task never even records a pending confirmation. Reuses `allowsAutoMergeProcessing` from `@fusion/core` verbatim (never re-derives the auto-merge/human-review predicate). Distinguishes explicit user pause (`task.userPaused===true`, or `task.paused===true` with no `pausedReason`) from engine/self-healing parks (which always stamp a `pausedReason`). Emits a bounded `overseer:oversight-withheld-human-control` run-audit no-action event (metadata: `{ taskId, reason, stage, oversightLevel }`), deduped per (taskId, reason) so it does not spam every poll. +- c16cc9e: summary: Configure planner oversight level per task and per project in the workflow editor and task create/detail. + category: feature + dev: Per-task `plannerOversightLevel` override exposed via TaskForm (Inherit/off/observe/steer/autonomous), threaded through createTask/updateTask; workflow-editor Values tab gets a first-class display entry. Workflow-native setting; not a project setting. +- aae603b: summary: Add a configurable planner-overseer notification verbosity level (Silent/Errors/Important/All). + category: feature + dev: New workflow-native enum setting `plannerOversightNotificationLevel` in BUILTIN_OVERSIGHT_SETTINGS; default `important`. Resolves via resolveEffectiveSettings; emission gating that reads it lands in FN-7519/FN-7520. +- d10ea9a: summary: Add a task-detail planner-overseer intervention timeline (stage, reason, action, outcome, attempts, links). + category: feature + dev: New core `PlannerInterventionEntry` model + `recordPlannerIntervention`/`getPlannerInterventionTimeline` helpers persisting via the run-audit store under the `overseer:intervention` mutation, plus a `PlannerInterventionTimeline` component rendered in the task-detail Planner Oversight cluster. Emission call-sites land in FN-7520. +- bf68839: summary: Emit planner-overseer run-audit events for observations, steering, retries, recovery, confirmations, and escalations. + category: feature + dev: New core emitters (emitOverseerObservation/Steering/RecoveryAttempt/Retry/Confirmation/Escalation) in planner-overseer-events.ts, each mapping its decision-point to the correct intervention action/outcome and delegating to FN-7519's recordPlannerIntervention under the overseer:intervention mutation. Producer call-sites land in FN-7511/FN-7512/FN-7513. +- c4d81fe: summary: Add an AI-undo fallback task when reverting a done task via git conflicts or is unsupported. + category: feature + dev: `POST /api/tasks/:id/revert` now accepts `{ mode?: "git" | "ai" | "auto" }` (default `"auto"`). `"auto"` tries the FN-7523 git-revert path first and falls back to creating an AI-undo board task (`{ mode: "ai", createdTaskId, alreadyOpen? }`) on a conflicting or unsupported (e.g. workspace) git result; `needsHuman` (autoMerge-off) never triggers the fallback. `"ai"` always creates the AI-undo task; `"git"` keeps the FN-7523 git-only contract, which is otherwise unchanged. New engine exports: `createAiUndoTask`, `buildAiUndoTaskDescription`, `REVERT_OF_METADATA_KEY`. New core store method `TaskStore.findOpenRevertTaskForSource` backs the idempotency guard (an open undo task suppresses a duplicate; a closed one does not). +- e7cb2f1: summary: Add a Revert action to Done/Archived task cards to undo landed changes. + category: feature + dev: Wires onRevertTask through Board/List/Detail surfaces; calls POST /tasks/:id/revert in "auto" mode with a conflict-confirm AI-undo fallback (mode: "ai"). +- 5ad8ec8: summary: Capture a structured performance snapshot when an agent task completes. + category: feature + dev: New AgentReflectionService.captureTaskPerformance persists a non-LLM post-task ReflectionMetrics record (duration, packages/files touched, verification command + scope, retry/rework count) and emits ids/counts-only `reflection:captured` run-audit telemetry; populates performanceSummary/latestReflection. +- 726cbf8: summary: Task cards can now show the planner overseer's active state (idle/watching/steering/recovering/awaiting-confirmation). + category: feature + dev: Adds a serializable `PlannerOverseerRuntimeSnapshot` + pure `derivePlannerOverseerState` (core), a read-only `ProjectEngine.getPlannerOverseerRuntimeSnapshot(taskId)` accessor assembling it from the FN-7511 monitor + FN-7512/7513 recovery controller, and a best-effort additive `plannerOverseerState` enrichment on `GET /api/tasks` (mirrors the `branchProgress` pattern; never persisted, never fails the board load). Consumed by FN-7516's TaskCard. +- 2ed06f9: summary: Support reverting multi-repo workspace tasks via git, all-or-nothing across sub-repos. + category: feature + dev: Extends `packages/engine/src/task-revert.ts` with `resolveWorkspaceTaskRevertCommits`/`revertWorkspaceTask` and wires `POST /api/tasks/:id/revert` to dispatch workspace tasks (`isWorkspaceTask`) to the new path; returns `{ mode: "git", clean, workspace: { repos: [...] }, conflicts? }`. Single-repo `performTaskRevert` path is unchanged. +- 8c6f76c: summary: Add per-sha revert commit granularity to the task revert API and service. + category: feature + dev: `performTaskRevert` and `POST /api/tasks/:id/revert` accept an optional `granularity: "squash" | "per-sha"` (default `"squash"`, unchanged FN-7523 behavior). `"per-sha"` creates one attributed `revert(FN-xxxx)` commit per original sha (each with its own `Fusion-Task-Id` trailer and audit line), skipping no-op shas without empty commits. A mid-batch conflict in either mode rolls back the whole batch to the pre-call HEAD — no partially-landed per-sha commits. The clean result now reports `revertCommitShas: string[]` (all created commits) alongside the existing `revertCommitSha` (kept for backward compatibility). +- f992e6a: summary: Add a dedicated Keyboard Shortcuts settings section with click-to-record capture and more configurable actions. + category: feature + dev: Relocates dashboardKeyboardShortcuts into its own settings section, adds a ShortcutCaptureInput recorder, and extends DashboardShortcutAction with openFiles/openSettings/openCommandCenter/newTask actions wired into existing App nav handlers. +- 2df6c35: summary: Open a revert PR for done/archived tasks when autoMerge is disabled instead of refusing. + category: feature + dev: `POST /api/tasks/:id/revert` gains an additive `{ mode: "pr", clean: true, prUrl, prNumber, revertBranch, existingPr? }` result for clean single-repo reverts under `autoMerge:false`, reusing `GitHubClient.createPr`, `findPrForBranch` idempotency, and the `manual:true` PR handoff. New engine export `prepareRevertPrBranch` (packages/engine/src/task-revert.ts) prepares the dedicated `fusion/revert-` branch without ever mutating the base branch. Existing `{ mode: "git" | "ai", ... }` shapes and the `autoMerge:true` path are unchanged. +- 94e9d15: summary: AI-undo tasks now default to a configurable, stricter review workflow. + category: feature + dev: New project setting `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) selects the workflow for AI-undo board tasks created by `POST /api/tasks/:id/revert` (`mode: "ai"`, the `auto` conflict fallback, and the workspace conflict fallback all share the `createAiUndoResult()` closure, so all three inherit this default). A blank/unset value means the created task inherits the project default workflow (pre-FN-7556 behavior). The route validates the configured id via `getWorkflowDefinition`/`isBuiltinWorkflowId` and falls back to inherit (with a logged warning) on a blank or unknown value, so a misconfigured id never breaks AI-undo task creation. The engine's `createAiUndoTask` helper stays pure — it only forwards a `workflowId` it is given, never resolves the setting itself. The Settings Modal UI field for this setting is a deliberate follow-up task; the value is settable today only via the settings API. +- 3dd227b: summary: Plan auto-approval is now the default; specified tasks skip manual approval unless you opt into workflow/require-all. + category: feature + dev: `DEFAULT_PROJECT_SETTINGS.planApprovalMode` flips `workflow` → `auto-approve-all`; existing projects with an explicit stored value are unchanged; consumed by `resolvePlanApprovalRequired` at the triage gating sites. +- 78d4db9: summary: Fusion self-repo issue-close comments now show current and target release versions. + category: feature + dev: GitHubIssueCommentService appends "Current version: v{current}" and "Target release: v{next-minor}" lines when the linked source issue is runfusion/fusion; other repos unchanged. Version resolved via getCliPackageVersion. +- 7435849: summary: Open one revert PR per sub-repo for workspace tasks when autoMerge is disabled. + category: feature + dev: `POST /api/tasks/:id/revert` gains an additive workspace `{ mode: "pr", clean: true, workspace: { repos: [{ repo, revertBranch, prUrl, prNumber, existingPr? }] } }` result for clean multi-repo reverts under `autoMerge:false`, extending FN-7554's single-repo `mode:"pr"` path. New engine export `prepareWorkspaceRevertPrBranches` (packages/engine/src/task-revert.ts) classifies every sub-repo first and only prepares a dedicated `fusion/revert-` branch per sub-repo when all are clean/already-reverted (all-or-nothing at the branch-prep phase), never force-writing any sub-repo integration branch. The route resolves owner/repo and checks the rate limiter for every sub-repo before pushing/creating any PR, so GitHub-unconfigured/rate-limited cases degrade the whole task to `needsHuman` rather than opening a partial subset of PRs. Existing `{ mode: "git" | "ai" | "pr", ... }` shapes, the `autoMerge:true` workspace path, and FN-7554's single-repo path are unchanged. +- 73b38ba: summary: Add a Settings → General picker to choose the workflow used for AI-undo (revert) tasks. + category: feature + dev: Surfaces `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) in GeneralSection; empty selection means "inherit project default workflow", matching the revert route's blank-is-inherit behavior from FN-7556. +- 42bbe58: summary: Add "Ask user question" and "Exit gate" workflow nodes for mid-flow chat reach-out and early exit. + category: feature + dev: New IR node kinds `ask-user` (reuses await-input park/resume; surfaces the question in the task chat) and `exit-gate` (terminates the workflow early, optional condition). Editor palette + summaries + help updated; `prompt`+`awaitInput` remains a back-compat alias. +- 53fe0d7: summary: Add a built-in "Brainstorming" workflow that talks to you before planning. + category: feature + dev: Registers `builtin:brainstorming` (non-default, default-enabled) composing FN-7579's `ask-user` → refine → `exit-gate`-on-approval phase ahead of the normal coding plan/execute/review/merge spine. Parity suite (`builtin-workflows.test.ts`) extended for the new entry. +- ecbbb29: summary: Add a "Coding (Ideas)" workflow with a manual Ideas intake and a merged Todo planner column. + category: feature + dev: New `builtin:coding-ideas` clones the default stepwise pipeline with an `ideas` intake (autoTriage:false) in front of a merged `todo` planner+capacity column. createTask lands cards in the workflow's intake column; the triage service plans unplanned todo tasks in place; the scheduler skips bootstrap-prompt todo tasks; TaskCard gains a Start button and a Ready badge. + +### Patch Changes + +- 8668a05: summary: Add a workflow setting to disable automatic large-task triage splitting. + category: feature + dev: Adds triageProactiveSubtaskSplittingEnabled while preserving explicit breakIntoSubtasks requests. +- 978cdda: summary: Show active Plan Review progress on triage task cards. + category: fix + dev: TaskCard now renders the existing progress affordance for Triage only when unified progress has active workflow work. +- 635fca2: summary: Remove the eye icon markdown/plain toggle from chat; messages always render as Markdown. + category: breaking + dev: Removed ChatView `chat-thread-header-render-toggle` (desktop + mobile), `showAllAsPlain` state, and `chat.showRenderedMarkdown`/`chat.showPlainText` i18n keys (FN-7541). +- 3d55102: summary: Clarify task-detail oversight Nudge/Explain controls: visible label, disabled reason, always-openable Explain panel. + category: fix + dev: TaskDetailModal now renders a `detail-oversight-controls-label` group label and `detail-overseer-nudge-disabled-reason` helper text (both gated by the existing oversight-cluster visibility condition); Explain no longer disables on `!canExplainOverseer` since it is read-only. Nudge's `canNudgeOverseer` gate and Stop's confirm dialog are unchanged. +- 0f1cd0a: summary: Unify border, radius, and height of the task-detail Priority/Execution/Oversight controls. + category: fix + dev: Adds a shared --detail-control-border-radius token alongside --detail-priority-control-min-height so .detail-priority-chip, .detail-execution-mode-toggle, .detail-oversight-chip, and .detail-oversight-menu-trigger all resolve the same border-width/color/radius/height. +- b42ba9f: summary: Keep the task-detail Activity view menu open during mobile iOS taps. + category: fix + dev: Guards the Activity views dropdown against iOS visualViewport resize/scroll echoes during menu opening. +- a7559b0: summary: Fix Anthropic subscription login when pasted callback URLs contain fragment OAuth parameters. + category: fix + dev: Normalizes pasted OAuth callback fragments before resolving dashboard manual-code login prompts. +- 4b530a6: summary: Fix Claude/Anthropic subscription re-login showing "Login did not complete" after logging out. + category: fix + dev: Anthropic subscription OAuth is aliased across the legacy `anthropic` row (where interactive login persists the credential) and the `anthropic-subscription` id (where the settings card's in-memory logged-out suppression and status read are keyed). Re-login wrote only `anthropic`, so `loggedOutProviders` kept suppressing `anthropic-subscription` and the card reported failure despite a valid stored credential until process restart. auth-storage's proxy now clears the logged-out state on both aliases when either is re-authenticated (new `login` trap + hardened `set` trap via `clearReauthenticatedLogoutState`; raw api_key writes stay scoped to their own card). Also surfaces background OAuth login failures on `GET /auth/status` (`loginError`) + server logs so future paste-callback failures are diagnosable instead of a generic error. +- a5ac3c3: summary: Stop self-healing from killing actively-running tasks after ~30 minutes. + category: fix + dev: FN-7566. isPhantomExecutorBinding's liveness gate (heartbeat/checkout/runAudit) was blind to ephemeral executor agents, leaving only the age>graceMs\*3 threshold, so any ephemeral-executor task running longer than ~30 min was reclaimed to `todo` mid-flight. Adds the in-process live-session veto (activeSessionRegistry path / executingTaskLock / isTaskActive), mirroring the isWorkspaceTaskLive/sessionDead predicate, and honors clearPhantomExecutorBinding's live-session refusal in reclaimSelfOwnedBranchConflicts. +- 8912399: summary: Stop Windows Terminal version dialogs from popping up when opening the dashboard or Settings on Windows. + category: fix + dev: Root cause was the worktrunk integration, not the embedded terminal: worktrunk's CLI is named `wt`, which collides with Windows Terminal (`wt.exe`) on PATH, so probing it with `wt --version` launched Windows Terminal. Fixed by (1) `useWorktrunkInstallStatus` only auto-fetching `/api/worktrunk/status` when the integration is enabled (user opt-in) instead of on every Settings/dashboard mount, and (2) an engine-level guard in `probeWorktrunk` that refuses to exec a resolved `wt` that is the Windows Terminal alias (under `WindowsApps` / a `WindowsTerminal` package dir), covering all resolution surfaces. +- b800f7d: summary: Select newly created folders automatically during project setup. + category: fix + dev: Adds DirectoryPicker opt-in selection for project-registration surfaces while preserving default picker behavior. +- 9dc248e: summary: Prevent Desktop update banners from using 0.0.0 as the current version. + category: fix + dev: Dashboard update checks now resolve packaged @fusion/desktop metadata and fail closed for unresolved versions. +- 52dbc0e: summary: Quit Fusion Desktop on Windows when the window is closed. + category: fix + dev: Updates Electron close lifecycle so Windows shutdown reaches embedded runtime cleanup. +- ced783e: summary: Open desktop Anthropic Subscription OAuth logins in the system browser. + category: fix + dev: Adds Electron window-open policy coverage and preserves Settings auth polling completion paths. +- 50786f2: summary: Delay GitHub setup warnings for one day and add a dashboard connect action. + category: fix + dev: Dashboard setup warnings now gate GitHub prompts per project and route the CTA to Settings → Authentication. +- b4b1f6d: summary: Fix a false AI engine not running banner in desktop mode. + category: fix + dev: Distinguishes transient embedded desktop engine startup from true dashboard-only mode. +- e8b7362: summary: Clarify the desktop Connection Manager add-remote flow. + category: fix + dev: Desktop Connection Manager now separates Local Server context from saved remote profiles and collapses the remote editor until add/edit. +- 0900a38: summary: Restore Local Server in the desktop Switch server list. + category: fix + dev: Desktop Connection Manager now lists local and saved remote destinations together. +- a2b09f2: summary: Make right-dock task list clicks respect the task popup setting. + category: fix + dev: Threads openMobileTasksInPopup through the right-dock Tasks list route while preserving embedded dock detail when disabled. +- 0f05156: summary: Auto-retry retryable Code Review remediation failures. + category: fix + dev: Prevents retryable code-review-remediation graph failures from stranding tasks in in-review. +- 20184ac: summary: Fix no-op task branch recovery after a previously landed task. + category: fix + dev: Merge/recovery ownership classification now checks no-diff branches before foreign trailer rejection. +- 82493e0: summary: Allow documented source-free task-artifact deliveries to finish without commits. + category: fix + dev: fn_task_done now recognizes explicit gitignored .fusion/tasks artifact contracts while preserving source-change no-commit guards. +- 5689346: summary: Fix direct merges so Push to remote after merge honors the configured remote and branch. + category: fix + dev: Resolves remote-only push targets from the merge integration branch and preserves non-fatal push errors on done tasks. +- b42be87: summary: Keep task popups on the board layer with Activity menus above them. + category: fix + dev: Task-detail FloatingWindow callers use a lower layer band, and Activity view menus reposition after popup geometry changes. +- 61c8bdc: summary: Keep accepted chat requests waiting instead of showing false first-event timeout failures. + category: fix + dev: Dashboard chat POST streams no longer abort accepted-but-silent responses on the client first-event timer. +- e8dc2ae: summary: Show each task's original prompt in the Plan tab alongside the generated plan. + category: fix + dev: Adds a read-only Task Detail original-prompt section backed by task.description. +- d2e3134: summary: Add before-to-after transformation summaries to generated task definitions. + category: feature + dev: Built-in standard and fast triage prompts now require a `## Before → After Transformation` section. +- b0208c1: summary: Restore terminal Ctrl/Cmd copy and paste shortcuts. + category: fix + dev: Integrated and embedded terminals now own physical clipboard paste to avoid swallowed or duplicate input. +- 2797803: summary: Show first-token and tool processing durations in task agent logs. + category: feature + dev: Adds optional agent-log timing fields `timeToFirstTokenMs` and `durationMs`. +- a2d6349: summary: Fix mobile Chat composer being hidden behind the keyboard accessory bar. + category: fix + dev: Adds keyboard-open bottom clearance in ChatView so the composer clears the iOS input-assistant/autofill bar without a persistent .chat-thread transform or Android reserved-gap. +- 4baa4c4: summary: Settings descriptions now show each setting's default value. + category: feature + dev: Appended default-value copy to settings.\* i18n descriptions across Global, Runtimes, and Project Settings sections, sourced from DEFAULT_GLOBAL_SETTINGS/DEFAULT_PROJECT_SETTINGS in settings-schema.ts; added settings-default-descriptions.test.tsx guarding that every surfaced setting states a default (or explicit "inherits"/"no default \u2014 unset") and that every DEFAULT_SETTINGS key is documented or allowlisted as not surfaced. +- 53d7b7e: summary: Add an intelligent git-revert engine service and POST /api/tasks/:id/revert route. + category: feature + dev: New `packages/engine/src/task-revert.ts` exports `resolveTaskRevertCommits`, `classifyTaskRevert`, and `performTaskRevert` (squash/rebase/lineage attribution precedence, dry-run classification, guaranteed-clean rollback). Route enforces done/archived-only and autoMerge-off guard rails; conflicting results are returned unresolved for sibling FN-7524 (AI-undo) to act on. Workspace tasks return `unsupported`. +- 4707eb5: summary: Auto-approve now reliably sends specified plans to the board without a manual approval stop. + category: fix + dev: FN-7526 — investigated the reported "plans still park at awaiting-approval when auto-approve is on" symptom; resolvePlanApprovalRequired, mergeEffectiveSettings/applyWorkflowSettingsOverlay, and every finalizeApprovedTask call site (specifyTask, recoverApprovedTask, retryUnavailablePlanReview, tryFinalizeExplicitDuplicateMarker) already honored project planApprovalMode: "auto-approve-all" over a stored workflow requirePlanApproval value — no production defect reproduced. Added end-to-end regression coverage across every enumerated surface (Plan Review reviewer-outage retry, refinement routing, self-healing starved-refinement recovery) using the real mergeEffectiveSettings pipeline instead of isolated bare-settings unit calls, plus explicit assertions that the independent release-authorization and Workflow Plan Review gates remain intact under auto-approve-all, so a future bare-settings call site is caught immediately instead of silently reintroducing the reported behavior. +- 3b52a4d: summary: Fix the in-dashboard Switch server menu not switching desktop local/remote. + category: fix + dev: The desktop shell's redirect effects in App.tsx read a dead `localServer` field that the preload never populates; extracted `resolveDesktopShellRedirectTarget` in appLifecycle.ts now derives the navigation target from the live `localRuntime`/`activeProfileId` state for both directions, and the unused `localServer` field was removed from `ShellConnectionState`. +- 36bd74e: summary: Fix branch group completion checklists to show accurate landed/finished counts. + category: fix + dev: runAiMerge (the sole merge path since master-plan U0) never resolved branch-group routing or stamped mergeDetails.mergeTargetBranch/mergeTargetSource, so isBranchGroupMemberLanded permanently reported shared-group members as not landed. Routes through resolveBranchGroupMergeRouting (matching the legacy merger.ts pattern) and stamps the target fields on both the landed and no-op finalize paths; preserves merge-target-safety in isBranchGroupMemberLanded (a sibling/mismatched-branch member still never counts as landed). +- df0be88: summary: Branch groups no longer report complete (or become promotable) when an unlanded member is archived. + category: fix + dev: listTasksByBranchGroup membership now scans with includeArchived:true so an archived-but-unlanded member stays counted in total instead of silently dropping out; mergeDetails is now persisted on ArchivedTaskEntry so an archived member that had already landed keeps counting as landed. evaluateBranchGroupCompletion / promoteBranchGroup gate correctly; merge-target-safety in isBranchGroupMemberLanded is unchanged. +- ec9ac61: summary: Fix the global GitLab integration setting not persisting when saved. + category: fix + dev: splitSettingsSave now diffs the five global GitLab keys (gitlabEnabled, gitlabInstanceUrl, gitlabApiBaseUrl, gitlabAuthToken, gitlabAuthTokenType) against scoped global initials only, never the project-effective merged initialValues, so a project override no longer suppresses a real global save. +- 8d36b99: summary: Fix task-detail Activity view dropdown not opening reliably on mobile. + category: fix + dev: Guards the Activity menu's window resize/orientationchange/scroll close-listener with the same opening-tap timing guard already used for visualViewport, and exempts scroll events originating in the `.detail-tabs` scroller, so a same-gesture mobile tap echo (Android/iOS, fixed modal or `.floating-window--task-detail` popup) no longer closes the menu the instant it opens. +- ad744aa: summary: Manual "Run now" for the Database Backup automation now runs in-process like the scheduler, matching cron behavior. + category: fix + dev: The legacy single-command and command-step manual automation run path (`executeSingleCommand` in packages/dashboard/src/routes.ts) now intercepts `isInProcessBackupCommand`/`isInProcessMemoryBackupCommand` via the scoped TaskStore, mirroring `RoutineRunner.executeCommand`/`CronRunner`, instead of always shelling out via `exec()`. `formatInProcessBackupError`, `isInProcessBackupCommand`, and `isInProcessMemoryBackupCommand` are now exported from `@fusion/engine` for reuse. Existing onStep/onText live-run callbacks already stream incremental output for command/backup runs; added regression coverage confirming this holds for the new interception branch. +- 5c3d58a: summary: Task cards no longer show the "Auto-recovery" oversight badge unless oversight is explicitly configured. + category: fix + dev: `TaskCard.tsx`'s `showOversightBadge` gate now also suppresses the badge when the effective level equals `DEFAULT_PLANNER_OVERSIGHT_LEVEL` ("autonomous") and there is no explicit per-task `plannerOversightLevel` override; an explicit per-task override of "autonomous" still renders the badge. +- b4be515: summary: Remove the per-card overseer-state ("Executor") badge from task cards. + category: fix + dev: Deleted the FN-7516 `card-overseer-state-badge` render, its card-local `deriveOverseerCardWatchedStage` helper/label maps, and its CSS; the sibling oversight-level badge (`card-oversight-badge`) is unaffected. +- 62ddb19: summary: Original task prompt now renders as Markdown and is collapsed by default in the task Plan tab. + category: feature + dev: Task Detail Plan/Definition tab original-prompt section reuses the existing `.detail-source-toggle`/`.detail-source-chevron--expanded` collapse pattern and the shared `ReactMarkdown` pipeline (`remarkGfm`, `sharedRehypePlugins`, `markdownLinkifyComponents`); backed by read-only `task.description`, no change to the generated `PROMPT.md` editor/revision flow. +- 883c73e: summary: Fix agent-created artifacts not appearing live in the dashboard artifacts view. + category: fix + dev: Root cause was cross-instance artifact-registration replication, not the route/hook/render path (all already correct). `TaskStore.registerArtifact()` never bumped `lastModified`, and `checkForChanges()` (the polling replicator that lets a second TaskStore instance on the same project — e.g. the dashboard's cached store vs. the engine's own store — mirror events it did not write itself) only ever diffed the `tasks` table, never `artifacts`. A store instance that did not perform the write could therefore never observe or re-emit `artifact:registered`, leaving an already-open Documents/task Artifacts gallery stale until a full reload. Fixed by bumping `lastModified` on artifact writes and adding a strictly-increasing `rowid`-cursor poll over the `artifacts` table in `checkForChanges()`. See `packages/core/src/__tests__/artifacts.test.ts` and `packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts` for regression coverage. +- d09b57f: summary: Fix the mobile terminal shortcut bar so it scrolls horizontally to reach every key. + category: fix + dev: Added `min-width: 0` to `.terminal-shortcut-panel` to defeat the flex min-width:auto trap that clipped overflow instead of engaging `overflow-x: auto`. +- 3d58260: summary: Planner-oversight intervention timeline now populates from real engine activity. + category: fix + dev: Wires PlannerOverseerMonitor/PlannerRecoveryController decision points to the FN-7520 emitOverseer\* façade with the real TaskStore; observation/escalation emission deduped per (task, stage[, signal]). +- 052a277: summary: Show the "Global" prefix on the Authentication entry in the mobile Settings picker. + category: fix + dev: resolveSettingsSectionOptionLabel now derives the Global-group prefix for storage-less (scope: undefined) sections in SettingsModal.tsx (FN-7552). +- 6e4c207: summary: Tasks held for release authorization or Plan Review are now shown distinctly, so auto-approve no longer looks broken. + category: fix + dev: FN-7559 — auto-approve-all bypasses only the manual plan-approval gate (unchanged, FN-7526). Release-authorization holds are surfaced with a new distinct status reason (`Task.awaitingApprovalReason: "release-authorization"`) and no longer render the generic manual Approve/Reject affordance in TaskCard/TaskDetailModal; Workflow Plan Review already used distinct statuses (`needs-replan`/`plan-review-unavailable`) and is unaffected. Both gates remain independent and intact — this is UI/data disambiguation only. +- 6d364fc: summary: Move mobile terminal controls into a bottom footer so they no longer crowd the header, with a scrollable shortcut bar. + category: fix + dev: On the ≤768px terminal, the `.terminal-actions` cluster now renders in a `terminal-footer-actions` bar (with `min-width:0; overflow-x:auto`) instead of the header; desktop/floating/pinned-below keep the FN-7502 header layout. Preserves the FN-7550 shortcut-panel scroll fix. +- b471aec: summary: Stop the release-authorization gate from holding tasks that merely disclaim releasing. + category: fix + dev: classifyReleaseTask now strips negated release-disclaimer clauses (e.g. "this task performs no release/publish; releases are owned by scripts/release.mjs") before signal matching in packages/engine/src/triage-release-authorization.ts, so revert/undo/UI specs are no longer false-flagged as release-class. Genuine "run pnpm release"/"publish @runfusion/fusion" intent still trips the gate. +- 9d4a45b: summary: Fix mobile terminal text still rendering with excess inter-character gaps after font-load settle. + category: fix + dev: Root cause: xterm's OptionsService setter is a no-op when reassigning an already-current fontFamily/fontSize, so post-settle reapply never forced CharSizeService/DomRenderer to remeasure. Added `forceTerminalFontRemeasure()` in `terminalPreferences.ts`, used by both `TerminalModal.tsx` and `SessionTerminal.tsx` at every post-`waitForTerminalFontMetrics()` settle site. +- 72b77bf: summary: Stop Plan Review from looping tasks forever and fix its "can't find the plan" reviews. + category: fix + dev: FN-7561 — Plan Review pre-merge gate hardening in packages/engine/src/executor.ts. (1) The reviewer ran readonly with cwd=worktree but the spec lives at project-root .fusion/tasks//PROMPT.md, so "Read PROMPT.md" produced "no PROMPT.md found / data is in a DB" non-verdicts; the spec text is now injected into the reviewer prompt via readTaskArtifact. (2) A malformed reviewer response now self-retries once on the primary model when no fallback is configured. (3) A malformed (advisory_failure, no verdict) plan-review result can never trigger a triage replan. (4) The unbounded plan-review replan default is capped at 15 attempts with a loud halting log entry, so a persistently-disagreeing planner/reviewer no longer burns LLM calls indefinitely (FN-7525 ran 13+ attempts overnight). +- c08498e: summary: Planner-overseer task badge now shows a readable label and explains what it is waiting on. + category: fix + dev: TaskCard badge renders plannerOverseerStateLabel + plannerOverseerBadgeTooltip built from the existing PlannerOverseerRuntimeSnapshot (reason/watchedStage/signal/pendingConfirmation); presentation-only, no engine changes. +- 24b27e8: summary: Plan approve/reject API now blocks release-authorization holds, requiring the authorization marker first. + category: fix + dev: FN-7564 — POST /tasks/:id/approve-plan and /reject-plan now return 400 when task.awaitingApprovalReason === "release-authorization" (FN-7559 discriminator), enforcing the FN-6481 release-authorization gate at the API layer regardless of client. Manual-approval holds are unaffected. +- fb45157: summary: Pin the mobile terminal close (X) button to the top-right corner so it is easy to find and tap. + category: fix + dev: On the ≤768px terminal, the `terminal-close` button now carries a `terminal-close--corner` class (order:3 + margin-inline-start:auto) so it renders last in flex order and hugs the right edge next to the tab dropdown, instead of falling back to order:0 (far left). Desktop/floating/pinned-below placement inside `.terminal-actions` is unchanged. +- 7c0be53: summary: Fix mobile terminal excess character spacing that survived earlier font-remeasure fixes. + category: fix + dev: `TerminalModal`/`SessionTerminal` re-bake xterm's `DomRenderer` letter-spacing compensation AFTER `fitAddon.fit()` settles the post-fit column count (not just before it), since `handleResize()` never re-bakes spacing itself. See `docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md` recurrence #4. +- 71dfd3a: summary: Rename downloadable CLI release binaries to the fn-cli- base name. + category: internal + dev: `binaryNameForTarget` in `packages/cli/build.ts` and the `release.yml` / `test-release.yml` matrices now emit `fn-cli-`; the local dev binary stays `fn`/`fn.exe`. +- 9592e3a: summary: Manual plan approval no longer re-asks you to approve a plan you already approved when it hasn't changed. + category: fix + dev: FN-7569 — approving a plan records a fingerprint of the approved PROMPT.md (new nullable Task.approvedPlanFingerprint, migration 139). The manual plan-approval gate skips re-parking at awaiting-approval when a re-specification (replan, plan-review retry, self-healing rebound) produces the same plan; a changed plan or reject-plan still requires fresh approval. Release authorization, Workflow Plan Review, and auto-approve-all are unchanged. +- c31f9ef: summary: Move the planner intervention timeline into the task Activity view dropdown. + category: feature + dev: Removes the inline `PlannerInterventionTimeline` mount from the FN-7517 oversight cluster in `TaskDetailModal.tsx` and adds a fourth `interventions` `ActivitySegment`, shown in the Activity dropdown only when planner oversight is active for the task; falls back to Live if oversight turns off while Interventions is selected. +- ce9df29: summary: Expired Claude subscription logins now show disconnected with a re-login prompt; tokens auto-refresh before expiry. + category: fix + dev: Unifies OAuth expiry detection between OAuthExpiryMonitor and /api/auth/status, and adds an engine-side proactive OAuth refresh scheduler wired in project-engine (guarded by skipNotifier). No token material logged. +- 196abb5: summary: Anthropic subscription reads now refresh the OAuth token automatically instead of silently failing when expired. + category: fix + dev: mergeAuthStorageReads getApiKey("anthropic-subscription") now delegates to the underlying engine authStorage.getApiKey (the only refresh-token HTTP round trip) instead of a local static expiry check; regression tests drive the wrapper directly. No token material logged. +- 45e5a26: summary: Stop GitHub tracking-issue creation from linking new tasks to old/closed issues. + category: fix + dev: github-tracking dedup now only reuses OPEN issues and requires a File-Scope path overlap (keyword-only matches no longer link). Prevents mis-linking a fresh task to a stale/resolved tracking issue (FN-7579). Setting `githubTrackingDedupEnabled` unchanged. +- a1a6b09: summary: Clarify the oversight "Nudge unavailable" guideline so it no longer reads as an overseer fault. + category: fix + dev: TaskDetailModal oversight controls — reworded taskDetail.oversight.nudgeDisabledTitle and added taskDetail.oversight.nudgeSuppressedTitle to differentiate periodic-observation vs. manual-control states. No enablement/engine logic changed. +- cf3fe8b: summary: New tasks created under the Coding (Ideas) workflow now land in the Ideas column and wait for you to promote them. + category: fix + dev: Dashboard create surfaces (InlineCreateCard, QuickEntryBox, NewTaskModal, insight/todo → task) no longer hard-code column:"triage"; the store now resolves the selected/default workflow's intake column. InlineCreateCard forwards workflowId at create time instead of applying it post-create. Also fixed a glue-layer regression in `useTaskHandlers.ts` (`handleBoardQuickCreate`/`handleModalCreate`) that re-forced column:"triage" even after the UI surfaces stopped sending it. +- 8b4e522: summary: Fix tasks vanishing from the board after being added to a workflow like Coding (Ideas). + category: fix + dev: Board.tsx forces a board-workflows refetch (deferred one tick, signature-guarded) whenever a rendered task is missing from the taskWorkflowIds map, so its real workflow and intake column resolve regardless of which create surface added it; the single-workflow grouping also re-homes a task whose column its workflow no longer declares into the intake lane instead of dropping it. Fixes the FN-7591 regression where intake-column cards (column "ideas") fell back to the default workflow, which has no such column, and were filtered out until a manual reload. +- f30d55f: summary: Move the Before → After transformation summary to the top of generated task definitions. + category: fix + dev: Reorders the standard and fast triage `PROMPT.md` templates in packages/core/src/agent-prompts.ts so `## Before → After Transformation` is the first content section, ahead of `## Review Level` and `## Mission`, matching FN-7499's glance-verification intent. +- 20379e8: summary: Task-detail Priority dropdown now matches the Oversight dropdown's size, border, and typography. + category: fix + dev: Removed the Priority-only forced select/option uppercase, added a neutral chip background scoped to `.detail-priority-chip.card-priority-badge--normal` for the untinted `normal` level, and reused the FN-7585 shared `--btn-border-width`/`--border`/`--detail-control-border-radius`/`--detail-priority-control-min-height` tokens so both dropdowns render as one control style across desktop and the mobile oversight-overflow surface. +- e0f3d3d: summary: Default workflow boards now label the intake column "Planning" instead of "Triage". + category: fix + dev: Renamed the `name` of the `id: "triage"` intake column to "Planning" in builtin-coding, builtin-stepwise-coding, and builtin-pr workflow IRs (column id unchanged; linear built-ins inherit via canonicalBuiltinWorkflowColumns). COLUMN_LABELS.triage was already "Planning". +- 5b193d2: summary: Fix the task-detail Nudge control staying disabled when the overseer is actively watching. + category: fix + dev: GET /api/tasks/:id now attaches the transient plannerOverseerState snapshot (mirrors the list route); TaskDetailModal reads the snapshot from workingTask so detail refetches no longer drop it. +- 546ef16: summary: Honor mission branchStrategy when triage omits branchAssignment; skip validation for inactive missions. + category: fix + dev: resolveBranchAssignmentContext returns undefined for absent mode so triage falls back to mission.branchStrategy; processTaskOutcome gates on mission.status === "active" like recoverActiveMissions. +- b173f76: summary: Planner overseer no longer marks healthy in-progress tasks as "recovering" or steers them. + category: fix + dev: `decidePlannerRecovery` now returns `none` for healthy (`progressing`/`complete`) and `awaiting-human` executor/workflow-gate signals instead of falling through to `inject_guidance`; only `stuck`/`blocked`/`failed` trigger autonomous steering. Also dedupes the `PlannerOverseerMonitor` activity-feed heartbeat so an unchanged `(stage, signal, reason)` observation is logged once per change, not every poll tick. Fixes the "overseer recovering" badge appearing on every autonomous card and the needless AI-consuming guidance injections (FN-7577). + ## 0.55.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 12b7620272..fd3058f086 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@runfusion/fusion", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 81a426217d..9dd821dbb1 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,7 @@ # @fusion/core +## 0.56.0 + ## 0.55.0 ## 0.54.0 diff --git a/packages/core/package.json b/packages/core/package.json index 92b3a6d9c8..231c17ef04 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/core", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Fusion core: task store, scheduler, settings, and shared domain types backing the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/dashboard/CHANGELOG.md b/packages/dashboard/CHANGELOG.md index e8cf58ead6..2c24c505ba 100644 --- a/packages/dashboard/CHANGELOG.md +++ b/packages/dashboard/CHANGELOG.md @@ -1,5 +1,22 @@ # @fusion/dashboard +## 0.56.0 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/engine@0.56.0 +- @fusion/i18n@0.39.20 +- @fusion-plugin-examples/cli-printing-press@0.1.37 +- @fusion-plugin-examples/compound-engineering@0.1.20 +- @fusion-plugin-examples/dependency-graph@0.1.51 +- @fusion-plugin-examples/roadmap@0.1.39 +- @fusion-plugin-examples/cursor-runtime@0.1.39 +- @fusion-plugin-examples/droid-runtime@0.1.46 +- @fusion-plugin-examples/hermes-runtime@0.2.70 +- @fusion-plugin-examples/openclaw-runtime@0.2.70 +- @fusion-plugin-examples/paperclip-runtime@0.2.70 + ## 0.55.0 ### Patch Changes diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index a7cb0917aa..fd257dd688 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/dashboard", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Fusion dashboard: React UI and HTTP API server for monitoring and controlling the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 25edae85ec..9cdc43a1f9 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -1,5 +1,13 @@ # @fusion/desktop +## 0.56.0 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/dashboard@0.56.0 +- @fusion/engine@0.56.0 + ## 0.55.0 ### Patch Changes diff --git a/packages/desktop/package.json b/packages/desktop/package.json index bcb0451adc..3ada770992 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@fusion/desktop", "productName": "Fusion", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "author": { "name": "Runfusion", diff --git a/packages/droid-cli/CHANGELOG.md b/packages/droid-cli/CHANGELOG.md index 7d0ba7fdf8..76ed864e1f 100644 --- a/packages/droid-cli/CHANGELOG.md +++ b/packages/droid-cli/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion/droid-cli +## 0.11.46 + +### Patch Changes + +- @fusion-plugin-examples/droid-runtime@0.1.46 + ## 0.11.45 ### Patch Changes diff --git a/packages/droid-cli/package.json b/packages/droid-cli/package.json index 3f4e3c0aca..b4f78e77ce 100644 --- a/packages/droid-cli/package.json +++ b/packages/droid-cli/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/droid-cli", - "version": "0.11.45", + "version": "0.11.46", "description": "First-party Fusion pi extension that routes LLM calls through the Droid CLI subprocess.", "license": "MIT", "private": true, diff --git a/packages/engine/CHANGELOG.md b/packages/engine/CHANGELOG.md index 28d72ab991..9a2a0641c7 100644 --- a/packages/engine/CHANGELOG.md +++ b/packages/engine/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion/engine +## 0.56.0 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/pi-claude-cli@0.56.0 + ## 0.55.0 ### Patch Changes diff --git a/packages/engine/package.json b/packages/engine/package.json index 576d863dc6..c0dbe437f9 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/engine", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Fusion engine: executor, merger, scheduler, and automation runtime for the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 192f39166e..6cca96c292 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion/i18n +## 0.39.20 + +### Patch Changes + +- @fusion/core@0.56.0 + ## 0.39.19 ### Patch Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 16495becdf..1f72dffbb0 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/i18n", - "version": "0.39.19", + "version": "0.39.20", "license": "MIT", "description": "Fusion i18n: authored translation catalogs and shared i18next configuration for the Fusion dashboard and terminal UI.", "type": "module", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index 049b06a582..eb259f5e2f 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -1,5 +1,7 @@ # @fusion/mobile +## 0.56.0 + ## 0.55.0 ## 0.54.0 diff --git a/packages/mobile/package.json b/packages/mobile/package.json index fe473d5575..9155721d9e 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/mobile", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Fusion mobile: Capacitor wrapper around the Fusion dashboard for iOS and Android.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/pi-claude-cli/CHANGELOG.md b/packages/pi-claude-cli/CHANGELOG.md index 3283269e5e..33da4a620d 100644 --- a/packages/pi-claude-cli/CHANGELOG.md +++ b/packages/pi-claude-cli/CHANGELOG.md @@ -1,5 +1,7 @@ # @fusion/pi-claude-cli +## 0.56.0 + ## 0.55.0 ## 0.54.0 diff --git a/packages/pi-claude-cli/package.json b/packages/pi-claude-cli/package.json index 552068bcdb..5492de6a46 100644 --- a/packages/pi-claude-cli/package.json +++ b/packages/pi-claude-cli/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/pi-claude-cli", - "version": "0.55.0", + "version": "0.56.0", "description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.", "license": "MIT", "private": true, diff --git a/packages/plugin-sdk/CHANGELOG.md b/packages/plugin-sdk/CHANGELOG.md index b0d8b656c2..7fae7b8544 100644 --- a/packages/plugin-sdk/CHANGELOG.md +++ b/packages/plugin-sdk/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion/plugin-sdk +## 0.56.0 + +### Patch Changes + +- @fusion/core@0.56.0 + ## 0.55.0 ### Patch Changes diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 6babb155ea..521758e3da 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/plugin-sdk", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "description": "Fusion plugin SDK: types and helpers for authoring third-party plugins that extend the Fusion dashboard and engine.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md b/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md index bd78d1f52f..92123aac5b 100644 --- a/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/auto-label +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-auto-label/package.json b/plugins/examples/fusion-plugin-auto-label/package.json index 436cbc4f57..ac3934a24c 100644 --- a/plugins/examples/fusion-plugin-auto-label/package.json +++ b/plugins/examples/fusion-plugin-auto-label/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/auto-label", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Automatically labels tasks based on description content", "keywords": [ diff --git a/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md b/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md index bbedf030ab..b14c0e873b 100644 --- a/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/ci-status +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-ci-status/package.json b/plugins/examples/fusion-plugin-ci-status/package.json index 60874dc4be..8ac8d2e1d2 100644 --- a/plugins/examples/fusion-plugin-ci-status/package.json +++ b/plugins/examples/fusion-plugin-ci-status/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/ci-status", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Polls CI status for branches and provides a custom API to query results", "keywords": [ diff --git a/plugins/examples/fusion-plugin-notification/CHANGELOG.md b/plugins/examples/fusion-plugin-notification/CHANGELOG.md index 9a5153f7d2..7b693db1b3 100644 --- a/plugins/examples/fusion-plugin-notification/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-notification/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/notification +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-notification/package.json b/plugins/examples/fusion-plugin-notification/package.json index de681e04cd..0bcc3dda54 100644 --- a/plugins/examples/fusion-plugin-notification/package.json +++ b/plugins/examples/fusion-plugin-notification/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/notification", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Example Fusion plugin that sends webhook notifications on task lifecycle events", "keywords": [ diff --git a/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md b/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md index b2151e7a23..bc00e600ca 100644 --- a/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/settings-demo +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-settings-demo/package.json b/plugins/examples/fusion-plugin-settings-demo/package.json index 1de0c8445f..c0fe1a7a18 100644 --- a/plugins/examples/fusion-plugin-settings-demo/package.json +++ b/plugins/examples/fusion-plugin-settings-demo/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/settings-demo", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Example Fusion plugin demonstrating settings schema and runtime configuration", "keywords": [ diff --git a/plugins/fusion-plugin-acp-runtime/CHANGELOG.md b/plugins/fusion-plugin-acp-runtime/CHANGELOG.md index 5f187fdcc1..3a345fbe25 100644 --- a/plugins/fusion-plugin-acp-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-acp-runtime/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/acp-runtime +## 0.1.20 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/fusion-plugin-acp-runtime/package.json b/plugins/fusion-plugin-acp-runtime/package.json index 40beacbe3c..74d51139c1 100644 --- a/plugins/fusion-plugin-acp-runtime/package.json +++ b/plugins/fusion-plugin-acp-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/acp-runtime", - "version": "0.1.19", + "version": "0.1.20", "type": "module", "description": "ACP (Agent Client Protocol) runtime plugin for Fusion — drives any ACP-compatible agent over JSON-RPC/stdio", "keywords": [ diff --git a/plugins/fusion-plugin-agent-browser/CHANGELOG.md b/plugins/fusion-plugin-agent-browser/CHANGELOG.md index 465de46dfd..a505b9593b 100644 --- a/plugins/fusion-plugin-agent-browser/CHANGELOG.md +++ b/plugins/fusion-plugin-agent-browser/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/agent-browser +## 0.1.40 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.1.39 ### Patch Changes diff --git a/plugins/fusion-plugin-agent-browser/package.json b/plugins/fusion-plugin-agent-browser/package.json index 09c90a3e70..ad16fb8759 100644 --- a/plugins/fusion-plugin-agent-browser/package.json +++ b/plugins/fusion-plugin-agent-browser/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/agent-browser", - "version": "0.1.39", + "version": "0.1.40", "type": "module", "description": "Agent Browser runtime and prompt/skill/workflow contributions for Fusion", "private": true, diff --git a/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md b/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md index e7e6907954..b36d1e51c8 100644 --- a/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md +++ b/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/cli-printing-press +## 0.1.37 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.36 ### Patch Changes diff --git a/plugins/fusion-plugin-cli-printing-press/package.json b/plugins/fusion-plugin-cli-printing-press/package.json index 5402523845..67fb31e856 100644 --- a/plugins/fusion-plugin-cli-printing-press/package.json +++ b/plugins/fusion-plugin-cli-printing-press/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/cli-printing-press", - "version": "0.1.36", + "version": "0.1.37", "type": "module", "description": "CLI Printing Press plugin package for Fusion", "private": true, diff --git a/plugins/fusion-plugin-compound-engineering/CHANGELOG.md b/plugins/fusion-plugin-compound-engineering/CHANGELOG.md index 314fc4a286..d18283a8d3 100644 --- a/plugins/fusion-plugin-compound-engineering/CHANGELOG.md +++ b/plugins/fusion-plugin-compound-engineering/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/compound-engineering +## 0.1.20 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.19 ### Patch Changes diff --git a/plugins/fusion-plugin-compound-engineering/package.json b/plugins/fusion-plugin-compound-engineering/package.json index db69cc969d..6d77c79dd1 100644 --- a/plugins/fusion-plugin-compound-engineering/package.json +++ b/plugins/fusion-plugin-compound-engineering/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/compound-engineering", - "version": "0.1.19", + "version": "0.1.20", "type": "module", "description": "Compound Engineering plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md b/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md index 298197aa82..f9549c047a 100644 --- a/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/cursor-runtime +## 0.1.39 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.1.38 ### Patch Changes diff --git a/plugins/fusion-plugin-cursor-runtime/package.json b/plugins/fusion-plugin-cursor-runtime/package.json index 5fb1a19cac..2e70825d3a 100644 --- a/plugins/fusion-plugin-cursor-runtime/package.json +++ b/plugins/fusion-plugin-cursor-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/cursor-runtime", - "version": "0.1.38", + "version": "0.1.39", "type": "module", "description": "Cursor CLI runtime plugin for Fusion", "keywords": [ diff --git a/plugins/fusion-plugin-dependency-graph/CHANGELOG.md b/plugins/fusion-plugin-dependency-graph/CHANGELOG.md index 8d45e99289..525a181f64 100644 --- a/plugins/fusion-plugin-dependency-graph/CHANGELOG.md +++ b/plugins/fusion-plugin-dependency-graph/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/dependency-graph +## 0.1.51 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.50 ### Patch Changes diff --git a/plugins/fusion-plugin-dependency-graph/package.json b/plugins/fusion-plugin-dependency-graph/package.json index ec099682a2..16222c5d22 100644 --- a/plugins/fusion-plugin-dependency-graph/package.json +++ b/plugins/fusion-plugin-dependency-graph/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/dependency-graph", - "version": "0.1.50", + "version": "0.1.51", "type": "module", "description": "Dependency graph dashboard view plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-droid-runtime/CHANGELOG.md b/plugins/fusion-plugin-droid-runtime/CHANGELOG.md index 153b7b0df3..1b22afb3d5 100644 --- a/plugins/fusion-plugin-droid-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-droid-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.46 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.1.45 ### Patch Changes diff --git a/plugins/fusion-plugin-droid-runtime/package.json b/plugins/fusion-plugin-droid-runtime/package.json index fecaf23c7a..012e0f4c99 100644 --- a/plugins/fusion-plugin-droid-runtime/package.json +++ b/plugins/fusion-plugin-droid-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/droid-runtime", - "version": "0.1.45", + "version": "0.1.46", "type": "module", "description": "Droid runtime plugin for Fusion", "keywords": [ diff --git a/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md b/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md index 88992c756d..79db7d7f80 100644 --- a/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md +++ b/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/even-realities-glasses +## 0.1.39 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.38 ### Patch Changes diff --git a/plugins/fusion-plugin-even-realities-glasses/package.json b/plugins/fusion-plugin-even-realities-glasses/package.json index 1d68b89255..344be51f03 100644 --- a/plugins/fusion-plugin-even-realities-glasses/package.json +++ b/plugins/fusion-plugin-even-realities-glasses/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/even-realities-glasses", - "version": "0.1.38", + "version": "0.1.39", "type": "module", "description": "Canonical Even Realities Fusion plugin with board/task cards, actions, notifications, and webhook transport", "keywords": [ diff --git a/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md b/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md index a32c1b620e..54429dbe28 100644 --- a/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/hermes-runtime +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/fusion-plugin-hermes-runtime/package.json b/plugins/fusion-plugin-hermes-runtime/package.json index d40c24993b..af876dab32 100644 --- a/plugins/fusion-plugin-hermes-runtime/package.json +++ b/plugins/fusion-plugin-hermes-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/hermes-runtime", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Hermes AI runtime plugin for Fusion - provides AI agent execution runtime", "keywords": [ diff --git a/plugins/fusion-plugin-linear-import/CHANGELOG.md b/plugins/fusion-plugin-linear-import/CHANGELOG.md index d160b64733..1638eac78f 100644 --- a/plugins/fusion-plugin-linear-import/CHANGELOG.md +++ b/plugins/fusion-plugin-linear-import/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/linear-import +## 0.1.2 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.1 ### Patch Changes diff --git a/plugins/fusion-plugin-linear-import/package.json b/plugins/fusion-plugin-linear-import/package.json index c35942c056..f118732483 100644 --- a/plugins/fusion-plugin-linear-import/package.json +++ b/plugins/fusion-plugin-linear-import/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/linear-import", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "description": "Linear issue import plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md b/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md index 71884eae8d..921a963f1f 100644 --- a/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/openclaw-runtime +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/fusion-plugin-openclaw-runtime/package.json b/plugins/fusion-plugin-openclaw-runtime/package.json index 91da95e93d..3dfa0f20d1 100644 --- a/plugins/fusion-plugin-openclaw-runtime/package.json +++ b/plugins/fusion-plugin-openclaw-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/openclaw-runtime", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Provides OpenClaw runtime for Fusion AI agents", "keywords": [ diff --git a/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md b/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md index 559e620071..d44930ff61 100644 --- a/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/paperclip-runtime +## 0.2.70 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.2.69 ### Patch Changes diff --git a/plugins/fusion-plugin-paperclip-runtime/package.json b/plugins/fusion-plugin-paperclip-runtime/package.json index acbc0c2281..cc413d9618 100644 --- a/plugins/fusion-plugin-paperclip-runtime/package.json +++ b/plugins/fusion-plugin-paperclip-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/paperclip-runtime", - "version": "0.2.69", + "version": "0.2.70", "type": "module", "description": "Paperclip runtime plugin for Fusion — provides AI agent web access capabilities", "keywords": [ diff --git a/plugins/fusion-plugin-reports/CHANGELOG.md b/plugins/fusion-plugin-reports/CHANGELOG.md index 96cf568a8f..bf9e6f7188 100644 --- a/plugins/fusion-plugin-reports/CHANGELOG.md +++ b/plugins/fusion-plugin-reports/CHANGELOG.md @@ -1,5 +1,13 @@ # @fusion-plugin-examples/reports +## 0.1.39 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/dashboard@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.38 ### Patch Changes diff --git a/plugins/fusion-plugin-reports/package.json b/plugins/fusion-plugin-reports/package.json index c4441930a7..fc187bba8c 100644 --- a/plugins/fusion-plugin-reports/package.json +++ b/plugins/fusion-plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/reports", - "version": "0.1.38", + "version": "0.1.39", "type": "module", "description": "Reports plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-roadmap/CHANGELOG.md b/plugins/fusion-plugin-roadmap/CHANGELOG.md index ba5c8679a2..f55491f4e1 100644 --- a/plugins/fusion-plugin-roadmap/CHANGELOG.md +++ b/plugins/fusion-plugin-roadmap/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/roadmap +## 0.1.39 + +### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/plugin-sdk@0.56.0 + ## 0.1.38 ### Patch Changes diff --git a/plugins/fusion-plugin-roadmap/package.json b/plugins/fusion-plugin-roadmap/package.json index ed4eb23b05..6bf65b5af6 100644 --- a/plugins/fusion-plugin-roadmap/package.json +++ b/plugins/fusion-plugin-roadmap/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/roadmap", - "version": "0.1.38", + "version": "0.1.39", "type": "module", "description": "Roadmap plugin package for Fusion", "private": true, diff --git a/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md b/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md index 41291b02cd..3afe98696f 100644 --- a/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md +++ b/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/whatsapp-chat +## 0.1.39 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.0 + ## 0.1.38 ### Patch Changes diff --git a/plugins/fusion-plugin-whatsapp-chat/package.json b/plugins/fusion-plugin-whatsapp-chat/package.json index 210ab07cf3..b66806fbb3 100644 --- a/plugins/fusion-plugin-whatsapp-chat/package.json +++ b/plugins/fusion-plugin-whatsapp-chat/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/whatsapp-chat", - "version": "0.1.38", + "version": "0.1.39", "type": "module", "description": "WhatsApp Web (Baileys) chat bridge for Fusion agents", "keywords": [ From ed823c794caba762087a716a3e759bd2b7ffe6a0 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 18:47:40 -0700 Subject: [PATCH 15/24] fix: preserve Claude OAuth scopes on token refresh so inference keeps working MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Anthropic OAuth refresh request sent `scope: user:profile`, which under RFC 6749 §6 re-issues the access token with exactly that scope — stripping `user:inference` and 403-ing every model call while the account still read as "logged in via OAuth". Stop sending `scope` on refresh (Anthropic then preserves the originally-granted scopes, matching pi-ai), and widen ANTHROPIC_DEFAULT_SCOPES to mirror pi-ai's full granted Claude Code scope set so any fallback describes a usable token. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../anthropic-oauth-refresh-preserve-scopes.md | 7 +++++++ .changeset/anthropic-oauth-refresh-scope.md | 7 +++++++ .../engine/src/__tests__/auth-storage.test.ts | 11 +++++++++-- packages/engine/src/auth-storage.ts | 18 ++++++++++++++++-- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 .changeset/anthropic-oauth-refresh-preserve-scopes.md create mode 100644 .changeset/anthropic-oauth-refresh-scope.md diff --git a/.changeset/anthropic-oauth-refresh-preserve-scopes.md b/.changeset/anthropic-oauth-refresh-preserve-scopes.md new file mode 100644 index 0000000000..796fdd5d7f --- /dev/null +++ b/.changeset/anthropic-oauth-refresh-preserve-scopes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Claude subscription login so model calls stop 403-ing after an OAuth token refresh. +category: fix +dev: `refreshAnthropicOAuthCredential` no longer sends `scope` on the refresh request (RFC 6749 §6 re-issues the token with exactly that scope, which stripped `user:inference` and narrowed refreshed tokens to `user:profile`). `ANTHROPIC_DEFAULT_SCOPES` now mirrors pi-ai's full granted Claude Code scope set so any fallback describes a usable token. diff --git a/.changeset/anthropic-oauth-refresh-scope.md b/.changeset/anthropic-oauth-refresh-scope.md new file mode 100644 index 0000000000..31c262f722 --- /dev/null +++ b/.changeset/anthropic-oauth-refresh-scope.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix Anthropic subscription showing "logged in" while all model calls fail. +category: fix +dev: OAuth token refresh in `packages/engine/src/auth-storage.ts` sent a `scope` param (defaulting to `user:profile`), which per RFC 6749 §6 re-issued the access token narrowed to that scope and stripped `user:inference` — so refreshed tokens 403'd on every model call. Refresh now omits `scope` (preserving the originally-granted scopes, matching pi-ai's own refresh), and `ANTHROPIC_DEFAULT_SCOPES` mirrors the full Claude Code scope set. Existing narrowed tokens need one re-login to obtain a fresh broad grant. diff --git a/packages/engine/src/__tests__/auth-storage.test.ts b/packages/engine/src/__tests__/auth-storage.test.ts index bbbfd3ac19..bcb56fa352 100644 --- a/packages/engine/src/__tests__/auth-storage.test.ts +++ b/packages/engine/src/__tests__/auth-storage.test.ts @@ -415,11 +415,16 @@ describe("createFusionAuthStorage", () => { // subscription id only — the raw `anthropic` slot stays empty. expect(await authStorage.getApiKey("anthropic")).toBe("refreshed-subscription-access-token"); expect(await authStorage.getApiKey("anthropic-subscription")).toBe("refreshed-subscription-access-token"); + // FNXC:ClaudeOAuth 2026-07-05-18:52: the refresh request MUST NOT send `scope`. + // Per RFC 6749 §6 an included scope re-issues the token with exactly that scope + // (never broader), which previously narrowed refreshed tokens to profile-only and + // stripped `user:inference` — leaving the account "logged in" yet 403ing on every + // model call. Omitting scope makes Anthropic preserve the originally-granted scopes. expect(fetchMock).toHaveBeenCalledWith( "https://platform.claude.com/v1/oauth/token", expect.objectContaining({ method: "POST", - body: expect.stringContaining("\"scope\":\"user:profile org:create_api_key\""), + body: expect.not.stringContaining("\"scope\""), }), ); expect(authStorage.get("anthropic-subscription")).toEqual({ @@ -739,11 +744,13 @@ describe("createFusionAuthStorage", () => { const authStorage = createFusionAuthStorage(); expect(await authStorage.getApiKey("anthropic")).toBe("refreshed-claude-access-token"); + // FNXC:ClaudeOAuth 2026-07-05-18:52: refresh must omit `scope` so Anthropic preserves + // the original grant (RFC 6749 §6); sending it previously stripped `user:inference`. expect(fetchMock).toHaveBeenCalledWith( "https://platform.claude.com/v1/oauth/token", expect.objectContaining({ method: "POST", - body: expect.stringContaining("\"scope\":\"user:profile org:create_api_key\""), + body: expect.not.stringContaining("\"scope\""), }), ); expect(authStorage.get("anthropic")).toEqual({ diff --git a/packages/engine/src/auth-storage.ts b/packages/engine/src/auth-storage.ts index da3fb15595..2a830942b1 100644 --- a/packages/engine/src/auth-storage.ts +++ b/packages/engine/src/auth-storage.ts @@ -33,7 +33,18 @@ const ANTHROPIC_PROVIDER_ID = "anthropic"; const ANTHROPIC_SUBSCRIPTION_PROVIDER_ID = "anthropic-subscription"; const ANTHROPIC_TOKEN_ENDPOINT = "https://platform.claude.com/v1/oauth/token"; const ANTHROPIC_OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"; -const ANTHROPIC_DEFAULT_SCOPES = ["user:profile"]; +/* +FNXC:ClaudeOAuth 2026-07-05-18:52: +Anthropic subscription login (delegated to pi-ai) grants the full Claude Code scope set — `user:inference` is what authorizes model calls. Earlier this constant was `["user:profile"]`, which was WRONG twice over: (1) it under-describes the token pi-ai actually obtains, and (2) it was fed into the refresh request's `scope` param, which under RFC 6749 §6 NARROWS the refreshed access token to profile-only and strips `user:inference`. The symptom: the account reads "logged in via OAuth" (token present + unexpired) yet every model call 403s with "OAuth token does not meet scope requirement any_of(user:inference, ...)". The default must mirror pi-ai's granted scopes so any fallback describes a usable token, and the refresh path (below) must NOT send it as a narrowing scope. +*/ +const ANTHROPIC_DEFAULT_SCOPES = [ + "org:create_api_key", + "user:profile", + "user:inference", + "user:sessions:claude_code", + "user:mcp_servers", + "user:file_upload", +]; const OAUTH_REFRESH_TIMEOUT_MS = 10_000; const OAUTH_REFRESH_FAILURE_COOLDOWN_MS = 30_000; @@ -212,6 +223,10 @@ async function refreshAnthropicOAuthCredential(credential: StoredCredential): Pr Fusion must renew expired Claude OAuth credentials with the stored refresh token so users are not forced through repeated manual Claude re-login when the access token expires. Persist the rotated access token in Fusion auth storage because model execution and dashboard usage resolve credentials through different runtime paths. */ + /* + FNXC:ClaudeOAuth 2026-07-05-18:52: + Do NOT send `scope` on refresh. RFC 6749 §6: a refresh request that includes `scope` re-issues the access token with EXACTLY that scope (never broader), so sending our stored/derived scope list can only strip capabilities — and did: it narrowed refreshed tokens to `user:profile` and broke inference. Omitting `scope` makes Anthropic preserve the originally-granted scopes (this is what pi-ai's own `refreshAnthropicToken` does). `scopes` is still resolved above and used only as the parseScopes fallback for the persisted credential record. + */ const response = await fetch(ANTHROPIC_TOKEN_ENDPOINT, { method: "POST", headers: { @@ -222,7 +237,6 @@ async function refreshAnthropicOAuthCredential(credential: StoredCredential): Pr grant_type: "refresh_token", refresh_token: refresh, client_id: ANTHROPIC_OAUTH_CLIENT_ID, - scope: scopes.join(" "), }), signal: controller.signal, }); From 670c41345b453f90b57e0aed1c7548f084e25b4f Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 18:50:37 -0700 Subject: [PATCH 16/24] chore: drop duplicate changeset for the OAuth refresh scope fix Both changesets in ed823c794 describe the same fix; keep the more complete one. Co-Authored-By: Claude Fable 5 --- .changeset/anthropic-oauth-refresh-preserve-scopes.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .changeset/anthropic-oauth-refresh-preserve-scopes.md diff --git a/.changeset/anthropic-oauth-refresh-preserve-scopes.md b/.changeset/anthropic-oauth-refresh-preserve-scopes.md deleted file mode 100644 index 796fdd5d7f..0000000000 --- a/.changeset/anthropic-oauth-refresh-preserve-scopes.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix Claude subscription login so model calls stop 403-ing after an OAuth token refresh. -category: fix -dev: `refreshAnthropicOAuthCredential` no longer sends `scope` on the refresh request (RFC 6749 §6 re-issues the token with exactly that scope, which stripped `user:inference` and narrowed refreshed tokens to `user:profile`). `ANTHROPIC_DEFAULT_SCOPES` now mirrors pi-ai's full granted Claude Code scope set so any fallback describes a usable token. From b9d60b3c39606a302719a08b15457aae37a028cf Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 18:56:24 -0700 Subject: [PATCH 17/24] FN-7602: fix Record/Clear button overlap in Keyboard Shortcuts rows Fixes overlapping Record and Clear buttons on the Keyboard Shortcuts settings rows by replacing the icon-only button class with a text button class and locking layout with flex-shrink. - Swap ShortcutCaptureInput Record/Clear buttons off the icon-only `btn-icon` class (which forced line-height:0 and a 36px mobile square, clipping labels) onto a text-button class - Add `.shortcut-capture` row CSS with `flex-shrink:0` on controls so the input and buttons never overlap and stack cleanly on mobile - Add regression tests covering the Keyboard Shortcuts section layout - Add changeset documenting the fix Files changed: .changeset/fn-7602-shortcut-row-layout.md | 7 ++ .../dashboard/app/components/SettingsModal.css | 17 ++++ .../settings/sections/ShortcutCaptureInput.tsx | 14 +++- .../__tests__/KeyboardShortcutsSection.test.tsx | 95 ++++++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7602 Fusion-Task-Lineage: 50cf6975-f0fb-42dd-87b0-50578977a0f4 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7602-shortcut-row-layout.md | 7 ++ .../app/components/SettingsModal.css | 17 ++++ .../sections/ShortcutCaptureInput.tsx | 14 ++- .../KeyboardShortcutsSection.test.tsx | 95 +++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 .changeset/fn-7602-shortcut-row-layout.md diff --git a/.changeset/fn-7602-shortcut-row-layout.md b/.changeset/fn-7602-shortcut-row-layout.md new file mode 100644 index 0000000000..908fbba219 --- /dev/null +++ b/.changeset/fn-7602-shortcut-row-layout.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix overlapping Record and Clear buttons in the Keyboard Shortcuts settings rows on desktop and mobile. +category: fix +dev: The shortcut-capture Record/Clear buttons no longer use the icon-only `btn-icon` class (which set `line-height:0` and a mobile 36px square, clipping/overlapping the text labels); they use a text-button class and the `.shortcut-capture` row locks buttons with `flex-shrink:0` so the input and controls never overlap, stacking cleanly on mobile. diff --git a/packages/dashboard/app/components/SettingsModal.css b/packages/dashboard/app/components/SettingsModal.css index dbc288c1b0..21c5122a70 100644 --- a/packages/dashboard/app/components/SettingsModal.css +++ b/packages/dashboard/app/components/SettingsModal.css @@ -2550,6 +2550,17 @@ FN-7553's dedicated Keyboard Shortcuts section groups every action under a categ border-bottom: 0; } +/* +FNXC:DashboardShortcuts 2026-07-05-00:00: +FN-7602 fixes an overlap bug: Record/Clear previously used the icon-only `btn-icon` class +(line-height:0 + mobile 36px square), which clipped/overlapped their text labels +("Record"/"Recording…"/"Clear") against the input and each other (IMG_1305). The input +keeps `flex: 1 1 auto; min-width: 0;` so it shrinks first, while the buttons get +`flex-shrink: 0; white-space: nowrap;` so their content-sized width (including the longer +"Recording…" label) is never crushed or allowed to overlap a neighbor on desktop. Below +768px the row stacks to a column so the buttons sit on their own row under the full-width +input, still non-overlapping. +*/ .shortcut-capture { display: flex; align-items: center; @@ -2566,6 +2577,12 @@ FN-7553's dedicated Keyboard Shortcuts section groups every action under a categ border-color: var(--color-error); } +.shortcut-capture__record, +.shortcut-capture__clear { + flex-shrink: 0; + white-space: nowrap; +} + .shortcut-capture__record--active { color: var(--color-warning); } diff --git a/packages/dashboard/app/components/settings/sections/ShortcutCaptureInput.tsx b/packages/dashboard/app/components/settings/sections/ShortcutCaptureInput.tsx index 94ea657ee5..1e77ccbb25 100644 --- a/packages/dashboard/app/components/settings/sections/ShortcutCaptureInput.tsx +++ b/packages/dashboard/app/components/settings/sections/ShortcutCaptureInput.tsx @@ -91,9 +91,19 @@ export function ShortcutCaptureInput({ id, value, defaultValue, invalid, describ }} onChange={(event) => onChange(event.target.value)} /> + {/* + FNXC:DashboardShortcuts 2026-07-05-00:00: + Record/Clear are TEXT-labeled buttons ("Record"/"Recording…"/"Clear"), not icon-only + controls. `btn-icon` sets `line-height: 0` and a mobile 36px square meant for SVG-only + buttons — applying it here clipped the label's line box and, at mobile widths, forced + "Recording…" to overflow the fixed square and overlap the Clear button/input + (reported via screenshot IMG_1305). Use `btn-sm` instead so labels render on a normal + line-height with content-sized width; `.shortcut-capture` locks these buttons with + `flex-shrink: 0` so they never collide with the input or each other. + */} - )} - {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && !canNudgeOverseer && ( - - {nudgeDisabledReason} - - )} - {(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && ( - - )} - {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && ( - - )} - - )} + )}
{overseerExplainOpen && (
@@ -6013,8 +5884,8 @@ export function TaskDetailModal({ onClose, ...props }: TaskDetailModalProps) { const overlayDismissProps = useOverlayDismiss(onClose); /* FNXC:TaskDetailSwipeBack 2026-07-05-12:30: - FN-7587 — track the mobile breakpoint locally (mirrors the OVERSIGHT_MENU_MOBILE_BREAKPOINT - resize-listener pattern above) so the list/modal/nested task-detail surface gets the same + FN-7587 — track the mobile breakpoint locally (mirrors the same resize-listener pattern + used elsewhere in this file) so the list/modal/nested task-detail surface gets the same presentation-only predictive-back slide/fade enter transition as the board main-panel (MainContent.tsx), without threading a new isMobile prop through App.tsx/AppModals.tsx. This is presentation-only: it never touches onClose/onRequestClose timing or the underlying diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx index acb0fa4882..b14129ed92 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.definition-actions.test.tsx @@ -1,3 +1,11 @@ +/* +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7604 — the footer "Actions" dropdown button name is matched EXACTLY +(`{ name: "Actions" }`) throughout this file, not via a loose `/actions/i` +regex. The now-universal Oversight overflow trigger's aria-label is +"Oversight actions", which also matches `/actions/i` and made every such +query ambiguous once the trigger stopped being a mobile-only affordance. +*/ import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -743,7 +751,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown to see Duplicate - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); expect(screen.getByRole("menuitem", { name: "Duplicate" })).toBeTruthy(); @@ -764,7 +772,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown - Duplicate should not be there - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); expect(screen.queryByRole("menuitem", { name: "Duplicate" })).toBeNull(); }); @@ -787,7 +795,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" })); @@ -820,7 +828,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" })); @@ -852,7 +860,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); const pauseItem = screen.getByRole("menuitem", { name: "Pause" }); fireEvent.pointerUp(pauseItem, { pointerType: "touch", pointerId: 1 }); @@ -884,7 +892,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" })); @@ -915,7 +923,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" })); @@ -945,7 +953,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Duplicate" })); @@ -976,7 +984,7 @@ describe("TaskDetailModal", () => { addToast={noop} />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); const item = screen.queryByRole("menuitem", { name: "Refine" }); if (shouldShow) expect(item).toBeTruthy(); else expect(item).toBeNull(); @@ -1012,7 +1020,7 @@ describe("TaskDetailModal", () => { />, ); - expect(screen.getByRole("button", { name: /actions/i })).toBeTruthy(); + expect(screen.getByRole("button", { name: "Actions" })).toBeTruthy(); }); it("renders Unpause button for a paused triage task", () => { @@ -1029,7 +1037,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); expect(screen.getByRole("menuitem", { name: "Unpause" })).toBeTruthy(); }); @@ -1051,7 +1059,7 @@ describe("TaskDetailModal", () => { />, ); - await userEvent.click(screen.getByRole("button", { name: /actions/i })); + await userEvent.click(screen.getByRole("button", { name: "Actions" })); await userEvent.click(screen.getByRole("menuitem", { name: "Unpause" })); await waitFor(() => { @@ -1084,7 +1092,7 @@ describe("TaskDetailModal", () => { expect(mockFetchAgent).toHaveBeenCalledWith("agent-1", undefined); }); - await userEvent.click(screen.getByRole("button", { name: /actions/i })); + await userEvent.click(screen.getByRole("button", { name: "Actions" })); await userEvent.click(screen.getByRole("menuitem", { name: "Unpause" })); await waitFor(() => { @@ -1115,7 +1123,7 @@ describe("TaskDetailModal", () => { expect(mockFetchAgent).toHaveBeenCalledWith("agent-1", undefined); }); - await userEvent.click(screen.getByRole("button", { name: /actions/i })); + await userEvent.click(screen.getByRole("button", { name: "Actions" })); expect(screen.getByRole("menuitem", { name: "Unpause" })).toBeTruthy(); expect(await screen.findByText("Paused by agent")).toBeTruthy(); @@ -1145,7 +1153,7 @@ describe("TaskDetailModal", () => { expect(mockFetchAgent).toHaveBeenCalledWith("agent-1", undefined); }); - await userEvent.click(screen.getByRole("button", { name: /actions/i })); + await userEvent.click(screen.getByRole("button", { name: "Actions" })); await userEvent.click(screen.getByRole("menuitem", { name: "Pause" })); await waitFor(() => { @@ -1177,7 +1185,7 @@ describe("TaskDetailModal", () => { />, ); - await userEvent.click(screen.getByRole("button", { name: /actions/i })); + await userEvent.click(screen.getByRole("button", { name: "Actions" })); expect(screen.getByRole("menuitem", { name: expectedLabel })).toBeTruthy(); }); @@ -1196,7 +1204,7 @@ describe("TaskDetailModal", () => { />, ); - await userEvent.click(screen.getByRole("button", { name: /actions/i })); + await userEvent.click(screen.getByRole("button", { name: "Actions" })); expect(screen.queryByRole("menuitem", { name: "Pause" })).toBeNull(); expect(screen.queryByRole("menuitem", { name: "Unpause" })).toBeNull(); @@ -1216,7 +1224,7 @@ describe("TaskDetailModal", () => { />, ); - expect(screen.queryByRole("button", { name: /actions/i })).toBeNull(); + expect(screen.queryByRole("button", { name: "Actions" })).toBeNull(); }); it("clicking Refine opens the refinement modal", () => { @@ -1234,7 +1242,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1258,7 +1266,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1281,7 +1289,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1309,7 +1317,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1333,7 +1341,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1364,7 +1372,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1392,7 +1400,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1443,7 +1451,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); @@ -1495,7 +1503,7 @@ describe("TaskDetailModal", () => { await screen.findByTestId("task-detail-workflow-badge"); expect(screen.getByTestId("task-detail-workflow-badge")).toHaveTextContent("Custom refinement lane"); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Refine" })); fireEvent.change(screen.getByPlaceholderText("Enter your feedback here..."), { target: { value: "Keep the same workflow lane" } }); fireEvent.click(screen.getByText("Create Refinement Task")); @@ -1531,7 +1539,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); // Click Refine from the dropdown @@ -1562,7 +1570,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); // Click Refine from the dropdown @@ -1596,7 +1604,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); // Click Refine from the dropdown @@ -1630,7 +1638,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); // Click Refine from the dropdown diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx index 18bf22f1ad..dccdea07d4 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx @@ -3,8 +3,18 @@ FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 coverage for the task-detail planner-overseer controls: the quick oversight-level-change select, the manual nudge/stop/explain buttons, and their enablement/leftover-shell rules (Surface Enumeration). + +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7604 — the desktop inline cluster was removed; ALL oversight controls +(including at the historically "desktop" 1024px-ish jsdom default width) now +render only behind the `detail-oversight-menu-trigger` overflow menu, the +same surface the mobile suite below already exercised. This describe block's +tests are retargeted to open the trigger via the shared `openOversightMenu()` +helper before querying level-select/nudge/stop/explain, mirroring the +pre-existing FN-7545/FN-7558 mobile pattern exactly — there is no longer a +separate desktop-only assertion path. */ -import { describe, it, expect, vi, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import type { PlannerOverseerRuntimeSnapshot } from "@fusion/core"; import { @@ -34,6 +44,20 @@ const activeSnapshot: PlannerOverseerRuntimeSnapshot = { lastAction: "inject_guidance", }; +/* +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7604 — shared open-the-overflow-menu helper reused across every describe +block in this file (the pattern the FN-7521/FN-7545 mobile-only describe +block already used). Since the dropdown is now the universal surface at +every viewport, every test that needs to observe the level select or the +nudge/stop/explain buttons must click the trigger first. +*/ +async function openOversightMenu() { + const trigger = await screen.findByTestId("detail-oversight-menu-trigger"); + fireEvent.click(trigger); + return trigger; +} + describe("TaskDetailModal oversight controls", () => { beforeEach(async () => { vi.clearAllMocks(); @@ -63,6 +87,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const select = await screen.findByTestId("detail-oversight-level-select"); expect((select as HTMLSelectElement).value).toBe("observe"); @@ -90,6 +115,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const select = await screen.findByTestId("detail-oversight-level-select"); fireEvent.change(select, { target: { value: "__inherit__" } }); @@ -114,6 +140,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).not.toBeDisabled(); fireEvent.click(nudgeBtn); @@ -136,6 +163,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).toBeDisabled(); }); @@ -153,6 +181,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const label = await screen.findByTestId("detail-oversight-controls-label"); expect(label).toHaveTextContent("Overseer controls"); @@ -181,6 +210,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).toBeDisabled(); @@ -203,6 +233,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).toBeDisabled(); @@ -225,6 +256,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).not.toBeDisabled(); expect(screen.queryByTestId("detail-overseer-nudge-disabled-reason")).not.toBeInTheDocument(); @@ -243,6 +275,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).toBeDisabled(); }); @@ -260,6 +293,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).toBeDisabled(); }); @@ -280,6 +314,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const stopBtn = await screen.findByTestId("detail-overseer-stop"); fireEvent.click(stopBtn); @@ -302,6 +337,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); await screen.findByTestId("detail-oversight-level-select"); expect(screen.queryByTestId("detail-overseer-stop")).not.toBeInTheDocument(); }); @@ -322,6 +358,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const explainBtn = await screen.findByTestId("detail-overseer-explain"); fireEvent.click(explainBtn); @@ -349,6 +386,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const explainBtn = await screen.findByTestId("detail-overseer-explain"); fireEvent.click(explainBtn); @@ -372,6 +410,7 @@ describe("TaskDetailModal oversight controls", () => { />, ); + await openOversightMenu(); const explainBtn = await screen.findByTestId("detail-overseer-explain"); // Read-only Explain must never be disabled purely because the overseer // isn't actively watching — that inactive state is exactly what the @@ -397,9 +436,11 @@ describe("TaskDetailModal oversight controls", () => { />, ); - // The quick level-change select still renders (it's always editable so an - // operator can opt IN to oversight), but nudge/stop/explain must not - // render an always-on empty shell for the common off+inactive default. + // The quick level-change select still renders inside the opened menu + // (it's always editable so an operator can opt IN to oversight), but + // nudge/stop/explain must not render an always-on empty shell for the + // common off+inactive default. + await openOversightMenu(); await screen.findByTestId("detail-oversight-level-select"); expect(screen.queryByTestId("detail-overseer-nudge")).not.toBeInTheDocument(); expect(screen.queryByTestId("detail-overseer-stop")).not.toBeInTheDocument(); @@ -416,8 +457,10 @@ sites pass a slim `Task` (no `prompt` key) that never carries `plannerOverseerState` — only the full-detail fetch response does. These tests reproduce that exact path: a slim task prop with NO snapshot, plus a mocked `fetchTaskDetail` resolving a full TaskDetail WITH an active snapshot, -and assert Nudge enables (helper absent) once the fetched detail lands — at -both the desktop inline site and the mobile overflow-menu site. +and assert Nudge enables (helper absent) once the fetched detail lands — +behind the (now universal, FN-7604) overflow-menu trigger at both a +"desktop" and a narrow-viewport width, exercised via the shared +`openOversightMenu()` helper. */ describe("TaskDetailModal oversight controls — snapshot delivered via fetched full detail (FN-7600)", () => { const originalInnerWidth = window.innerWidth; @@ -471,6 +514,7 @@ describe("TaskDetailModal oversight controls — snapshot delivered via fetched />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); await waitFor(() => { expect(nudgeBtn).not.toBeDisabled(); @@ -498,6 +542,7 @@ describe("TaskDetailModal oversight controls — snapshot delivered via fetched />, ); + await openOversightMenu(); const nudgeBtn = await screen.findByTestId("detail-overseer-nudge"); expect(nudgeBtn).toBeDisabled(); const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason"); @@ -545,18 +590,22 @@ describe("TaskDetailModal oversight controls — snapshot delivered via fetched * matching the pre-FN-7545 DOM (CSS-only `@media (max-width: 768px)` wrap, * no conditional mount). FN-7545 then collapsed that cluster's action * controls (level select / nudge / stop / explain) into a mobile overflow - * menu: at `window.innerWidth <= OVERSIGHT_MENU_MOBILE_BREAKPOINT` (768) the - * mount-time `updateOversightMenuMobile()` effect flips `isOversightMenuMobile` - * to true, so those controls now render INSIDE a closed `detail-oversight-menu` - * behind a `detail-oversight-menu-trigger` button instead of inline — the old - * flat queries no longer find them. This suite is corrected to drive the real - * FN-7545 mobile affordance: open the trigger, then query the menu items. It - * still asserts the same invariants FN-7521 required — select-writes-on-change, - * enabled nudge/stop/explain when the overseer is active, and no leftover - * empty-menu shell for the off+inactive default — just through the shipped - * mobile surface. The desktop branch (first `describe` above) is unchanged. + * menu behind a `detail-oversight-menu-trigger` button instead of inline — + * the old flat queries no longer find them. This suite drives the real + * FN-7545 overflow-menu affordance: open the trigger, then query the menu + * items. + * + * FNXC:PlannerOversight 2026-07-05-00:00 (FN-7604): + * The overflow menu this suite exercises is now the UNIVERSAL surface at + * every viewport, not a mobile-only branch — the desktop describe block + * above drives the exact same menu via the shared `openOversightMenu()` + * helper. Forcing `window.innerWidth = 375` here no longer selects a + * different code path; it is kept purely as a documented regression guard + * that the popover still renders/behaves correctly at a narrow width (e.g. + * `.detail-oversight-menu { right: 0 }` positioning), not because a second + * branch exists to select between. */ -describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-7545 overflow menu)", () => { +describe("TaskDetailModal oversight controls — narrow-viewport regression guard (FN-7521, FN-7545/FN-7604 universal overflow menu)", () => { const originalInnerWidth = window.innerWidth; beforeEach(async () => { @@ -568,10 +617,10 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN- vi.mocked(api.nudgeOverseer).mockResolvedValue({ applied: false, reason: "oversight-off" }); vi.mocked(api.stopOverseer).mockResolvedValue({ applied: true, reason: "stopped" }); vi.mocked(api.explainOverseer).mockResolvedValue({ snapshot: null }); - // Setting innerWidth before render is sufficient: TaskDetailModal's mount - // effect calls `updateOversightMenuMobile()` once on mount, reading - // `window.innerWidth` synchronously, which flips `isOversightMenuMobile` - // before the first paint the tests observe. + // Force a narrow viewport as a regression guard for the popover's mobile + // positioning/rendering; the overflow menu itself is the universal + // surface at every width post-FN-7604, so this no longer selects a + // separate branch. Object.defineProperty(window, "innerWidth", { value: 375, configurable: true }); }); @@ -579,13 +628,9 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN- Object.defineProperty(window, "innerWidth", { value: originalInnerWidth, configurable: true }); }); - async function openOversightMenu() { - const trigger = await screen.findByTestId("detail-oversight-menu-trigger"); - fireEvent.click(trigger); - return trigger; - } + // Reuses the shared `openOversightMenu()` helper defined at file scope. - it("still renders the quick level-change select behind the mobile overflow menu and writes on change", async () => { + it("still renders the quick level-change select behind the overflow menu and writes on change", async () => { const api = await import("../../api"); const mockUpdate = vi.fn().mockResolvedValue(makeTask({ id: "FN-201", plannerOversightLevel: "steer" })); vi.mocked(api.updateTask).mockImplementation(mockUpdate as any); @@ -613,7 +658,7 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN- }); }); - it("still renders enabled nudge/stop/explain controls behind the mobile overflow menu when the overseer is actively watching", async () => { + it("still renders enabled nudge/stop/explain controls behind the overflow menu at a narrow viewport when the overseer is actively watching", async () => { render( { + it("still shows the reworded periodic-observation copy (not the old alarming phrase) behind the overflow menu at a narrow viewport (FN-7582)", async () => { render( { + it("still renders no oversight-control leftover shell behind the overflow menu at a narrow viewport for the off+inactive default case", async () => { render( { beforeEach(async () => { @@ -726,7 +780,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)" />, ); - await screen.findByTestId("detail-overseer-nudge"); + await screen.findByTestId("detail-oversight-menu-trigger"); expect(screen.queryByTestId("planner-intervention-timeline")).not.toBeInTheDocument(); }); @@ -743,7 +797,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)" />, ); - await screen.findByTestId("detail-overseer-nudge"); + await screen.findByTestId("detail-oversight-menu-trigger"); openActivityViewMenu(); const option = screen.getByRole("menuitem", { name: "Interventions" }); fireEvent.click(option); @@ -764,7 +818,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)" />, ); - await screen.findByTestId("detail-oversight-level-select"); + await screen.findByTestId("detail-oversight-menu-trigger"); openActivityViewMenu(); expect(screen.queryByRole("menuitem", { name: "Interventions" })).not.toBeInTheDocument(); expect(screen.queryByTestId("planner-intervention-timeline")).not.toBeInTheDocument(); @@ -783,7 +837,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)" />, ); - await screen.findByTestId("detail-overseer-nudge"); + await screen.findByTestId("detail-oversight-menu-trigger"); openActivityViewMenu(); fireEvent.click(screen.getByRole("menuitem", { name: "Interventions" })); expect(await screen.findByTestId("planner-intervention-timeline")).toBeInTheDocument(); @@ -828,7 +882,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)" />, ); - await screen.findByTestId("detail-overseer-nudge"); + await screen.findByTestId("detail-oversight-menu-trigger"); openActivityViewMenu(); fireEvent.click(screen.getByRole("menuitem", { name: "Feed" })); @@ -858,7 +912,7 @@ describe("Intervention Timeline relocation into the Activity dropdown (FN-7571)" />, ); - await screen.findByTestId("detail-overseer-nudge"); + await screen.findByTestId("detail-oversight-menu-trigger"); openActivityViewMenu(); fireEvent.click(screen.getByRole("menuitem", { name: "Interventions" })); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx index ad14f00d29..bf1598e14d 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-mobile.test.tsx @@ -1,15 +1,21 @@ /* FNXC:PlannerOversight 2026-07-04-19:00: -FN-7545 coverage for the mobile collapse of the FN-7517 oversight action -controls into a single overflow menu (`detail-oversight-menu-trigger`). The -suite forces the narrow-viewport branch by setting `window.innerWidth` below -the `TaskDetailModal.tsx` `OVERSIGHT_MENU_MOBILE_BREAKPOINT` (768) BEFORE -render, since the component reads `window.innerWidth` on mount via a resize -listener (mirroring `DocumentsView`'s local `isMobile` pattern) rather than a -CSS media query. Every action inside the menu reuses the SAME handlers and -enablement gates as the desktop suite -(`TaskDetailModal.oversight-controls.test.tsx`) — this file only asserts the -collapsed-menu affordance, not new guard logic. +FN-7545 coverage for the collapse of the FN-7517 oversight action controls +into a single overflow menu (`detail-oversight-menu-trigger`). Every action +inside the menu reuses the SAME handlers and enablement gates as the desktop +suite (`TaskDetailModal.oversight-controls.test.tsx`) — this file only +asserts the collapsed-menu affordance, not new guard logic. + +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7604 — the overflow menu is now the SINGLE UNIVERSAL surface at every +viewport (desktop and mobile); it is no longer a narrow-viewport-only branch +selected by a JS `isOversightMenuMobile` resize listener (that state, the +`OVERSIGHT_MENU_MOBILE_BREAKPOINT` constant, and its effects were removed +from `TaskDetailModal.tsx`). `setViewportWidth`/`MOBILE_WIDTH`/`DESKTOP_WIDTH` +no longer select which branch mounts — both widths mount the exact same +dropdown — they are kept as a documented regression guard that the popover +still renders/positions/behaves correctly across a narrow AND a desktop +viewport, per the Surface Enumeration breakpoint requirement. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, waitFor } from "@testing-library/react"; @@ -455,7 +461,7 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => { expect(screen.getAllByRole("menu")).toHaveLength(1); }); - it("desktop inline oversight select is unaffected by the mobile auto-focus fix", async () => { + it("the overflow-menu popover renders identically at a desktop viewport (FN-7604 universal dropdown)", async () => { setViewportWidth(DESKTOP_WIDTH); render( @@ -470,12 +476,18 @@ describe("TaskDetailModal oversight controls — mobile overflow menu", () => { />, ); - // Desktop renders the inline native select directly (no overflow trigger, - // no custom popover) — confirm that surface is untouched by this fix. - expect(screen.queryByTestId("detail-oversight-menu-trigger")).not.toBeInTheDocument(); + // FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 — there is no longer a + // desktop-only inline select surface; the overflow-menu trigger is the + // single universal mount point at every viewport, including desktop. The + // popover stays closed until clicked, exactly like the mobile width. + const trigger = await screen.findByTestId("detail-oversight-menu-trigger"); + expect(screen.queryByTestId("detail-oversight-level-select")).not.toBeInTheDocument(); + expect(screen.queryByRole("menu")).not.toBeInTheDocument(); + + fireEvent.click(trigger); const select = await screen.findByTestId("detail-oversight-level-select"); expect(select).toBeInTheDocument(); - expect(screen.queryByRole("menu")).not.toBeInTheDocument(); + expect(screen.getByRole("menu")).toBeInTheDocument(); setViewportWidth(MOBILE_WIDTH); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx index e63acd3a39..0b5cdd3998 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.rendering.test.tsx @@ -1,6 +1,13 @@ /* FNXC:TaskDetailTabs 2026-06-17-08:20: FN-7306 labels the stable internal `chat` tab as Activity and keeps it as the default TaskDetailModal tab. Tests that assert Definition-only sections must opt into `initialTab="definition"` so they verify the intended surface instead of the Activity landing state. + +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7604 — the footer "Actions" dropdown button name is matched EXACTLY +(`{ name: "Actions" }`) throughout this file, not via a loose `/actions/i` +regex. The now-universal Oversight overflow trigger's aria-label is +"Oversight actions", which also matches `/actions/i` and made every such +query ambiguous once the trigger stopped being a mobile-only affordance. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, act, waitFor } from "@testing-library/react"; @@ -1538,7 +1545,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown to see Retry - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); expect(screen.getByRole("menuitem", { name: "Retry" })).toBeTruthy(); @@ -1560,7 +1567,7 @@ describe("TaskDetailModal", () => { ); // No Retry should be visible in the Actions dropdown - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); expect(screen.queryByRole("menuitem", { name: "Retry" })).toBeNull(); }); @@ -1599,7 +1606,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown and check for exactly one Retry - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); const retryButtons = screen.getAllByRole("menuitem", { name: "Retry" }); @@ -1622,7 +1629,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown and check for exactly one Retry - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); const retryButtons = screen.getAllByRole("menuitem", { name: "Retry" }); @@ -1644,7 +1651,7 @@ describe("TaskDetailModal", () => { />, ); - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); const retryButtons = screen.getAllByRole("menuitem", { name: "Retry" }); @@ -1670,7 +1677,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown and click Retry - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); await act(async () => { fireEvent.click(actionsBtn); }); @@ -1706,7 +1713,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown and click Retry - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); await act(async () => { fireEvent.click(actionsBtn); }); @@ -1746,7 +1753,7 @@ describe("TaskDetailModal", () => { ); // Open Actions dropdown and click Retry - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); await act(async () => { fireEvent.click(actionsBtn); }); @@ -1787,7 +1794,7 @@ describe("TaskDetailModal", () => { expect(screen.getByRole("menuitem", { name: "Back to In Progress" })).toBeTruthy(); expect(screen.queryByRole("menuitem", { name: "Move to Todo" })).toBeNull(); - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); expect(screen.queryByRole("menuitem", { name: "Retry" })).toBeNull(); }); @@ -1807,7 +1814,7 @@ describe("TaskDetailModal", () => { />, ); - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + const actionsBtn = screen.getByRole("button", { name: "Actions" }); await act(async () => { fireEvent.click(actionsBtn); }); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index c2ba62771d..c4342edd93 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -232,13 +232,16 @@ describe("TaskDetailModal", () => { const inlineControlsBlock = getStandaloneCssRuleBlock(css, ".detail-meta-inline-controls"); const priorityChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip"); const executionToggleBlock = getExactCssRuleBlock(css, ".detail-execution-mode-toggle"); - const oversightChipBlock = getExactCssRuleBlock(css, ".detail-oversight-chip"); const oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger"); - // The cluster declares one shared border-radius token; all four controls - // must reference it rather than four independent literal radii. + // The cluster declares one shared border-radius token; all three + // controls must reference it rather than independent literal radii. + // FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 removed the desktop-only + // `.detail-oversight-chip` wrapper (the inline branch it styled was + // deleted); the Oversight surface is now represented solely by + // `.detail-oversight-menu-trigger`, which already carried this trio. expect(inlineControlsBlock).toContain("--detail-control-border-radius: var(--radius-md);"); - for (const block of [priorityChipBlock, executionToggleBlock, oversightChipBlock, oversightTriggerBlock]) { + for (const block of [priorityChipBlock, executionToggleBlock, oversightTriggerBlock]) { expect(block).toContain("border-radius: var(--detail-control-border-radius);"); expect(block).toContain("border-width: var(--btn-border-width);"); expect(block).toContain("border-color: var(--border);"); @@ -247,26 +250,25 @@ describe("TaskDetailModal", () => { expect(block).toContain("box-sizing: border-box;"); } - // Guard against regressing back to four independent literal radius values - // (e.g. reintroducing a bare `var(--radius-pill)` on only the chips). + // Guard against regressing back to independent literal radius values + // (e.g. reintroducing a bare `var(--radius-pill)` on the priority chip). expect(priorityChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/); - expect(oversightChipBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/); + expect(oversightTriggerBlock).not.toMatch(/border-radius:\s*var\(--radius-pill\)/); }); it("renders the Priority dropdown chip like the Oversight dropdown chip, on every surface (FN-7597)", () => { const css = readDashboardStylesSource(); const priorityChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip"); - const oversightChipBlock = getExactCssRuleBlock(css, ".detail-oversight-chip"); const oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger"); const prioritySelectBlock = getExactCssRuleBlock(css, ".detail-priority-select"); const oversightSelectBlock = getExactCssRuleBlock(css, ".detail-oversight-select"); const prioritySelectOptionBlock = getExactCssRuleBlock(css, ".detail-priority-select option"); const oversightSelectOptionBlock = getExactCssRuleBlock(css, ".detail-oversight-select option"); - // Same box size AND same border source for the desktop Priority chip vs. - // BOTH oversight surfaces (desktop chip and the mobile overflow trigger). - for (const block of [priorityChipBlock, oversightChipBlock, oversightTriggerBlock]) { + // Same box size AND same border source for the Priority chip vs. the + // (now-universal, FN-7604) Oversight overflow trigger. + for (const block of [priorityChipBlock, oversightTriggerBlock]) { expect(block).toContain("min-height: var(--detail-priority-control-min-height);"); expect(block).toContain("border-width: var(--btn-border-width);"); expect(block).toContain("border-color: var(--border);"); @@ -613,8 +615,13 @@ describe("TaskDetailModal", () => { />, ); - // Actions are now in a dropdown - open it first - const actionsBtn = screen.getByRole("button", { name: /actions/i }); + // Actions are now in a dropdown - open it first. + // FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 — the footer "Actions" + // dropdown button name must be matched EXACTLY (not `/actions/i`) because + // the now-universal Oversight overflow trigger's aria-label is "Oversight + // actions", which also matches a loose /actions/i regex and made this + // query ambiguous once the trigger stopped being mobile-only. + const actionsBtn = screen.getByRole("button", { name: "Actions" }); fireEvent.click(actionsBtn); // Now the dropdown items should be visible @@ -653,7 +660,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -680,7 +687,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -707,7 +714,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -732,7 +739,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -771,7 +778,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -833,7 +840,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -870,7 +877,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -907,7 +914,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -950,7 +957,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -981,7 +988,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -1018,7 +1025,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { @@ -1052,7 +1059,7 @@ describe("TaskDetailModal", () => { />, ); - fireEvent.click(screen.getByRole("button", { name: /actions/i })); + fireEvent.click(screen.getByRole("button", { name: "Actions" })); fireEvent.click(screen.getByRole("menuitem", { name: "Delete" })); await waitFor(() => { From e347062e1f55ca4a59de556d379b00247cb1d2b5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 19:42:16 -0700 Subject: [PATCH 22/24] FN-7603: force xterm DOM-based char measurement to fix mobile terminal spacing Fixes recurrence #5 of mobile terminal inter-character spacing by unifying xterm's cell-width measurement pipeline with WidthCache's DOM-based glyph measurement, validated against real xterm instead of the jsdom mock. - Add withDomBasedTerminalCharacterMeasurement() in terminalPreferences.ts: transiently hides window.OffscreenCanvas during terminal.open() so CharSizeService's constructor throws and self-selects its own DOM-based fallback strategy, unifying dimensions.css.cell.width with WidthCache.get('W') measurement - Wire withDomBasedTerminalCharacterMeasurement() around terminal.open() calls in SessionTerminal.tsx and TerminalModal.tsx - Add FNXC:Terminal comments documenting the Canvas-vs-DOM measurement divergence root cause, grounded in the installed @xterm/xterm@5.5.0 source - Add docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md recurrence #5 section - Expand TerminalModal.test.tsx coverage for the new measurement-forcing behavior - Add changeset fn-7603-mobile-terminal-spacing.md (patch, fix) Files changed: .changeset/fn-7603-mobile-terminal-spacing.md | 7 + docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md | 101 ++++++ packages/dashboard/app/components/SessionTerminal.tsx | 16 +- packages/dashboard/app/components/TerminalModal.tsx | 16 +- packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 363 ++++++++++++++++++++- packages/dashboard/app/utils/terminalPreferences.ts | 63 ++++ 6 files changed, 554 insertions(+), 12 deletions(-) Fusion-Task-Id: FN-7603 Fusion-Task-Lineage: 6c7d980f-953e-4fa9-908e-b24125904cbe Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7603-mobile-terminal-spacing.md | 7 + ...ptions-noop-remeasure-after-font-settle.md | 101 +++++ .../app/components/SessionTerminal.tsx | 16 +- .../app/components/TerminalModal.tsx | 16 +- .../__tests__/TerminalModal.test.tsx | 363 +++++++++++++++++- .../app/utils/terminalPreferences.ts | 63 +++ 6 files changed, 554 insertions(+), 12 deletions(-) create mode 100644 .changeset/fn-7603-mobile-terminal-spacing.md diff --git a/.changeset/fn-7603-mobile-terminal-spacing.md b/.changeset/fn-7603-mobile-terminal-spacing.md new file mode 100644 index 0000000000..65ace4897f --- /dev/null +++ b/.changeset/fn-7603-mobile-terminal-spacing.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix persistent mobile terminal inter-character spacing (5th recurrence root cause). +category: fix +dev: xterm's CharSizeService picks a Canvas-based (OffscreenCanvas) or DOM-based character-measurement strategy at terminal.open() time; DomRenderer's letter-spacing bake always measures via a separate DOM-based WidthCache, so a Canvas-vs-DOM measurement mismatch survived FN-7561/FN-7567's remeasure-ordering fixes. `withDomBasedTerminalCharacterMeasurement` in terminalPreferences.ts forces CharSizeService onto the same DOM strategy for both TerminalModal and SessionTerminal. diff --git a/docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md b/docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md index 2ea9a036da..1ac2a1a29b 100644 --- a/docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md +++ b/docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md @@ -207,3 +207,104 @@ jsdom cannot exercise real xterm.js internals, so the FN-7567 regression models obtainable in this execution environment (headless coding agent, no physical device access) — this gap is recorded explicitly rather than treating jsdom/desktop WebKit as proof. See task document key="repro" on FN-7567 and `docs/ios-acceptance.md`. + +## Recurrence #5 (FN-7603): mock/real divergence — Canvas vs DOM character measurement + +FN-7561's `forceTerminalFontRemeasure()` and FN-7567's post-fit re-bake both ran correctly, and both +were validated ONLY against jsdom test doubles that never exercise real `@xterm/xterm@5.5.0`. On the +reported real mobile device, ordinary ASCII still rendered with visible gaps on the initial paint. This +is the fifth recurrence of the same defect, so the FN-7603 executor was required to read the installed +`@xterm/xterm@5.5.0`/`@xterm/addon-fit@0.10.0` source before touching production code (see task +document key="xterm-source-audit" on FN-7603). + +### The actual mechanism + +xterm's `CharSizeService` selects ONE of two measurement strategies **the moment `terminal.open()` +runs**: + +```js +// @xterm/xterm/lib/xterm.js (installed 5.5.0), CharSizeService constructor +try { this._measureStrategy = new OffscreenCanvasStrategy(optionsService) } // canvas: ctx.measureText("W") +catch { this._measureStrategy = new DomFallbackStrategy(document, container, optionsService) } // DOM: offsetWidth/32 +``` + +The Canvas strategy is chosen whenever `OffscreenCanvas` + `CanvasRenderingContext2D.measureText()` +reporting `fontBoundingBoxAscent`/`fontBoundingBoxDescent` are available — true on essentially every +real modern mobile Safari/Chrome. `dimensions.css.cell.width` (which feeds both `FitAddon.fit()`'s +column count, per the installed `addon-fit@0.10.0` `proposeDimensions()`, and +`DomRenderer._setDefaultSpacing()`'s baked letter-spacing) derives from whichever strategy +`CharSizeService` picked. + +Separately, `DomRenderer._setDefaultSpacing()` and `DomRendererRowFactory.createRow()`'s per-glyph +override BOTH measure via `WidthCache`, which is **always** DOM-based (`offsetWidth` of a hidden +32×-repeated-character span) — entirely independent of `CharSizeService`'s strategy choice. Real glyphs +are painted 100% through the DOM (`DomRenderer` never draws through canvas). Canvas 2D text measurement +and DOM/CSS text layout are two different browser rendering pipelines that can disagree — even by a +fraction of a device pixel — for the same font on the same device; this is a documented, +long-standing cross-API text-metrics inconsistency. `_setDefaultSpacing()`'s formula +(`dimensions.css.cell.width - widthCache.get('W')`) only correctly converges to zero (tight, contiguous +cells) when both operands are measured through the SAME pipeline. None of FN-7456/FN-7460/FN-7561/ +FN-7567 (or their test doubles) ever modeled this — all four assumed CharSizeService's measurement and +WidthCache's measurement were the same value. + +### Why FN-7456/FN-7460/FN-7561/FN-7567 missed this + +Every prior fix's test double (`mockHandleCharSizeChanged`) treated the measured character width as a +single shared value used for both "the cell width that drives fit" and "the width WidthCache subtracts +in `_setDefaultSpacing()`" — a faithful-looking model of xterm's DOM-only fallback strategy, but NOT of +the Canvas strategy that real xterm actually selects by default on real mobile browsers. Because jsdom +cannot run real `@xterm/xterm`, and no fix before FN-7603 cross-checked the double against the installed +source, the divergence between "what CharSizeService measures" (Canvas, in the real common case) and +"what WidthCache measures" (always DOM) went completely uncovered for four recurrences. + +### Solution + +Force `CharSizeService` to construct with its own DOM fallback strategy — unifying the cell-width +measurement with `WidthCache`'s measurement — by making `OffscreenCanvas` transiently unavailable for +the synchronous duration of `terminal.open()` (where `CharSizeService` is constructed): + +```ts +// packages/dashboard/app/utils/terminalPreferences.ts +export function withDomBasedTerminalCharacterMeasurement(fn: () => T): T { + const descriptor = Object.getOwnPropertyDescriptor(window, "OffscreenCanvas"); + delete (window as any).OffscreenCanvas; + try { + return fn(); + } finally { + if (descriptor) Object.defineProperty(window, "OffscreenCanvas", descriptor); + } +} +``` + +Both `TerminalModal.tsx` and `SessionTerminal.tsx` now wrap their `terminal.open(container)` call in +`withDomBasedTerminalCharacterMeasurement(() => terminal.open(container))`. `CharSizeService`'s +constructor try-block throws (no `OffscreenCanvas` global), so it self-selects the SAME DOM-based +strategy `WidthCache` already always uses — no hardcoded letter-spacing/cell-width compensation is +added; the fix unifies the measurement pipeline instead. + +Do not: + +- Patch `window.OffscreenCanvas` outside the narrow synchronous `open()` window — other page code + (charts, canvas-based rendering elsewhere in the dashboard) may legitimately need it. +- Assume this is scoped to mobile only — desktop with the DOM renderer (WebGL addon failed to load, or + `renderer: "canvas"` preference) has the identical divergence and benefits from the same fix. +- Treat this as a replacement for FN-7561/FN-7567 — both remain necessary; this fix addresses a + different, independent measurement-pipeline mismatch. + +### Regression coverage (Canvas-vs-DOM divergence, not CSS/call-count) + +- Extended the FN-7567 double: `mockCanvasCharWidthPx` (drives `FitAddon.fit()`'s column count, + mirroring `dimensions.css.cell.width`) can diverge from `mockDomCharWidthPx` (mirrors + `WidthCache.get('W')`) by a fixed offset, gated on `window.OffscreenCanvas` being defined at the + moment the mock's `open()` runs — exactly mirroring the real `CharSizeService` constructor's + try/catch strategy selection. +- The assertion is the same rendered-geometry invariant as FN-7567 (baked letter-spacing `== 0`), but + now fails on HEAD even with the full FN-7561/FN-7567 settle+pre/post-fit-remeasure sequence present, + because the divergence is NOT an ordering bug — it's a measurement-pipeline bug those fixes cannot + see or fix. +- See `TerminalModal.test.tsx` describe block "FN-7603 mobile inter-character spacing (Canvas vs DOM + CharSizeService measurement divergence)". +- Run: `pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/TerminalModal.test.tsx app/components/__tests__/SessionTerminal.test.tsx app/components/__tests__/SessionTerminal.mobile.test.tsx app/utils/__tests__/terminalPreferences.test.ts app/__tests__/terminal-input.test.ts --silent=passed-only --reporter=dot`. +- Real mobile Safari/Chrome sanity check remains the strongest signal; a real-device screenshot was not + obtainable in this execution environment — this gap is recorded explicitly (task document + key="repro" on FN-7603) rather than treating jsdom/desktop WebKit as proof. diff --git a/packages/dashboard/app/components/SessionTerminal.tsx b/packages/dashboard/app/components/SessionTerminal.tsx index 1b0d72cd2f..f81c358432 100644 --- a/packages/dashboard/app/components/SessionTerminal.tsx +++ b/packages/dashboard/app/components/SessionTerminal.tsx @@ -15,6 +15,7 @@ import { resolveTerminalFontFamily, resolveTerminalGlyphFontFamily, waitForTerminalFontMetrics, + withDomBasedTerminalCharacterMeasurement, } from "../utils/terminalPreferences"; /** @@ -459,7 +460,20 @@ export function SessionTerminal({ term.loadAddon(unicode11); term.unicode.activeVersion = "11"; - term.open(containerRef.current); + /* + FNXC:Terminal 2026-07-05-12:40: + FN-7603 recurrence #5: mirror TerminalModal's fix — force xterm's + CharSizeService to self-select its DOM-based measurement strategy for + the synchronous duration of open() so cell-width measurement (feeding + FitAddon.fit() and DomRenderer._setDefaultSpacing()'s baked + letter-spacing) uses the SAME pipeline as WidthCache's DOM-based + per-glyph measurement, instead of the default Canvas/OffscreenCanvas + strategy that measures through a different pipeline. See + docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md. + */ + withDomBasedTerminalCharacterMeasurement(() => { + term.open(containerRef.current!); + }); xtermRef.current = term; fitAddonRef.current = fitAddon as unknown as ITerminalAddon; diff --git a/packages/dashboard/app/components/TerminalModal.tsx b/packages/dashboard/app/components/TerminalModal.tsx index 39e4c78bf1..041fb2f2cc 100644 --- a/packages/dashboard/app/components/TerminalModal.tsx +++ b/packages/dashboard/app/components/TerminalModal.tsx @@ -46,6 +46,7 @@ import { resolveTerminalFontFamily, resolveTerminalGlyphFontFamily, waitForTerminalFontMetrics, + withDomBasedTerminalCharacterMeasurement, writeTerminalPreferences, type TerminalPreferences, type TerminalRenderer, @@ -1406,7 +1407,20 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG } // Open terminal in container - terminal.open(terminalRef.current); + /* + FNXC:Terminal 2026-07-05-12:40: + FN-7603 recurrence #5: force xterm's CharSizeService to self-select its + DOM-based measurement strategy (instead of its default Canvas/ + OffscreenCanvas strategy) for the synchronous duration of open(), so the + cell-width measurement that feeds FitAddon.fit() and + DomRenderer._setDefaultSpacing()'s baked letter-spacing uses the SAME + pipeline as WidthCache's DOM-based per-glyph measurement. See + `withDomBasedTerminalCharacterMeasurement` and + docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md. + */ + withDomBasedTerminalCharacterMeasurement(() => { + terminal.open(terminalRef.current!); + }); // Clear watchdog — imports and open() succeeded within deadline if (watchdogTimer) { diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx index 40d96c86ce..14ca8230e2 100644 --- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx @@ -83,13 +83,40 @@ CSS-property or call-count check. const MOCK_CONTAINER_WIDTH_PX = 728; const MOCK_FALLBACK_CHAR_WIDTH_PX = 9; const MOCK_SETTLED_CHAR_WIDTH_PX = 7; +/* +FNXC:Terminal 2026-07-05-12:45: +FN-7603 recurrence #5: real xterm's `CharSizeService` picks ONE of two +measurement strategies at `terminal.open()` time — Canvas/OffscreenCanvas +(`ctx.measureText("W")`, chosen whenever `OffscreenCanvas` + the required +`TextMetrics` fields are available, i.e. virtually every real mobile browser) +or a DOM fallback (`offsetWidth` of a hidden repeated-"W" span, chosen only if +the canvas strategy's constructor throws). Separately, `DomRenderer. +_setDefaultSpacing()`/`DomRendererRowFactory` ALWAYS measure via `WidthCache`, +which is ALWAYS DOM-based (`offsetWidth`), regardless of which strategy +CharSizeService picked. The FN-7567 mock above (and prior recurrences) modeled +both as the SAME shared width, hiding this divergence. Model them +independently: `mockCanvasCharWidthPx` (drives `FitAddon.fit()`'s column +count, mirroring `dimensions.css.cell.width`) can diverge from +`mockDomCharWidthPx` (mirrors `WidthCache.get('W')`) whenever +`window.OffscreenCanvas` is defined at the moment the mock's `open()` runs — +exactly mirroring the real `CharSizeService` constructor's +`try { new OffscreenCanvasStrategy } catch { new DomFallbackStrategy }` +selection. The production fix (`withDomBasedTerminalCharacterMeasurement`) +hides `window.OffscreenCanvas` for the synchronous duration of `open()`, which +this mock's `open()` observes to decide which strategy was "selected". +*/ +const MOCK_CANVAS_DOM_DIVERGENCE_PX = 0.7; let mockFontsSettledForCharSize = false; -let mockMeasuredCharWidthPx = MOCK_FALLBACK_CHAR_WIDTH_PX; +let mockDomCharWidthPx = MOCK_FALLBACK_CHAR_WIDTH_PX; +let mockCanvasCharWidthPx = MOCK_FALLBACK_CHAR_WIDTH_PX; +let mockCharSizeServiceUsesCanvasStrategy = true; let mockBakedLetterSpacingPx = 0; function resetMockTerminalGeometry(): void { mockFontsSettledForCharSize = false; - mockMeasuredCharWidthPx = MOCK_FALLBACK_CHAR_WIDTH_PX; + mockDomCharWidthPx = MOCK_FALLBACK_CHAR_WIDTH_PX; + mockCanvasCharWidthPx = MOCK_FALLBACK_CHAR_WIDTH_PX; + mockCharSizeServiceUsesCanvasStrategy = true; mockBakedLetterSpacingPx = 0; mockTerminalInstance.cols = 80; } @@ -104,27 +131,48 @@ function getMockBakedLetterSpacingPx(): number { return mockBakedLetterSpacingPx; } +/** + * Mirrors real xterm's `CharSizeService` constructor picking its measurement + * strategy the moment `terminal.open()` runs: Canvas/OffscreenCanvas when + * `window.OffscreenCanvas` is present, DOM fallback otherwise. + */ +function mockSelectCharSizeServiceStrategyAtOpen(): void { + mockCharSizeServiceUsesCanvasStrategy = + typeof (window as unknown as { OffscreenCanvas?: unknown }).OffscreenCanvas !== "undefined"; +} + // Mirrors xterm's CharSizeService.measure() -> onCharSizeChange -> // DomRenderer.handleCharSizeChanged() -> _updateDimensions() + // _setDefaultSpacing(): runs on every GENUINE fontFamily/fontSize option // transition, using the CURRENT (possibly stale, pre-fit) column count. +// +// `mockDomCharWidthPx` mirrors `WidthCache.get('W')` (always DOM-based). +// `mockCanvasCharWidthPx` mirrors `CharSizeService.width`: identical to the +// DOM value when the DOM strategy was selected at open(), but offset by a +// fixed divergence when the Canvas strategy was selected — modeling the real +// cross-pipeline (Canvas 2D vs DOM layout) measurement discrepancy that +// `_setDefaultSpacing()`'s `dimensions.css.cell.width - widthCache.get('W')` +// formula depends on both operands agreeing to correctly converge to zero. function mockHandleCharSizeChanged(): void { - mockMeasuredCharWidthPx = mockFontsSettledForCharSize + mockDomCharWidthPx = mockFontsSettledForCharSize ? MOCK_SETTLED_CHAR_WIDTH_PX : MOCK_FALLBACK_CHAR_WIDTH_PX; + mockCanvasCharWidthPx = mockCharSizeServiceUsesCanvasStrategy + ? mockDomCharWidthPx + MOCK_CANVAS_DOM_DIVERGENCE_PX + : mockDomCharWidthPx; const cols = (mockTerminalInstance.cols as number) || 1; const cellWidthPx = MOCK_CONTAINER_WIDTH_PX / cols; - mockBakedLetterSpacingPx = cellWidthPx - mockMeasuredCharWidthPx; + mockBakedLetterSpacingPx = cellWidthPx - mockDomCharWidthPx; } -// Mirrors FitAddon.fit() -> terminal.resize(cols, rows) -> -// DomRenderer.handleResize(): recomputes cols/cell-width from the CURRENT -// measured char width but deliberately does NOT touch letter-spacing (real -// xterm's handleResize() never calls _setDefaultSpacing()). +// Mirrors FitAddon.proposeDimensions(): cols = floor(availableWidth / +// renderService.dimensions.css.cell.width) — the CANVAS-strategy-derived +// value when that strategy is active, matching the installed +// @xterm/addon-fit@0.10.0 source (`t.css.cell.width`). const mockFitAddonFit = vi.fn(() => { mockTerminalInstance.cols = Math.max( 1, - Math.floor(MOCK_CONTAINER_WIDTH_PX / mockMeasuredCharWidthPx), + Math.floor(MOCK_CONTAINER_WIDTH_PX / mockCanvasCharWidthPx), ); }); @@ -183,7 +231,11 @@ function createMockTerminalOptions(): Record { const mockTerminalInstance = { loadAddon: vi.fn(), - open: vi.fn(), + // FN-7603: mirror the real CharSizeService constructor's strategy + // selection, which happens synchronously inside terminal.open(). + open: vi.fn(() => { + mockSelectCharSizeServiceStrategyAtOpen(); + }), onData: vi.fn((cb: (data: string) => void) => { terminalDataHandler = cb; return { dispose: vi.fn() }; @@ -7645,3 +7697,294 @@ describe("TerminalModal — FN-7567 mobile inter-character spacing (stale post-f }); }); }); + +/* +FNXC:Terminal 2026-07-05-12:50: +FN-7603 (recurrence #5 of mobile terminal inter-character spacing, after +FN-7456's DOM glyph-fallback fix, FN-7460's `text-size-adjust: none`, +FN-7561's `forceTerminalFontRemeasure`, and FN-7567's post-fit re-bake) root +cause, grounded against the installed `@xterm/xterm@5.5.0` source (see task +document key="xterm-source-audit" on FN-7603): xterm's `CharSizeService` +selects ONE of two independent measurement strategies the moment +`terminal.open()` runs — a Canvas/`OffscreenCanvas` strategy (chosen whenever +`OffscreenCanvas` + the required `TextMetrics` fields are available, i.e. +virtually every real modern mobile browser) or a DOM fallback strategy (only +selected if the canvas strategy's constructor throws). `dimensions.css.cell.width` +(which feeds `FitAddon.fit()`'s column count AND `DomRenderer. +_setDefaultSpacing()`'s baked letter-spacing) derives from whichever strategy +CharSizeService picked. Separately, `WidthCache` (used by both +`_setDefaultSpacing()` and `DomRendererRowFactory`'s per-glyph override) is +ALWAYS DOM-based. Real glyphs are painted 100% through the DOM, so +`_setDefaultSpacing()`'s `cell.width - widthCache.get('W')` formula only +converges to zero — i.e. tight, contiguous monospace cells — when BOTH +operands are measured through the SAME pipeline. Canvas 2D text measurement +and DOM/CSS text layout are two different browser rendering pipelines that can +disagree by a small but visible amount for the same font on the same device — +a divergence none of FN-7456/FN-7460/FN-7561/FN-7567 (or their test doubles) +ever modeled, because all four assumed a single unified character-width +measurement. This is why the reported symptom survived every prior remedy: +none of them touched WHICH measurement pipeline xterm's cell geometry is +computed from, only WHEN it recomputes. + +The fix (`withDomBasedTerminalCharacterMeasurement` in terminalPreferences.ts) +makes `window.OffscreenCanvas` transiently unavailable for the synchronous +duration of `terminal.open()`, forcing `CharSizeService`'s constructor +try-block to throw and self-select its own DOM fallback strategy — unifying +`dimensions.css.cell.width` and `WidthCache.get('W')` onto the SAME +measurement pipeline instead of adding any hardcoded letter-spacing +compensation. + +This suite extends the FN-7567 geometry-accurate mock +(`mockHandleCharSizeChanged`/`mockFitAddonFit`) to model the Canvas-vs-DOM +divergence explicitly (`mockCanvasCharWidthPx` vs `mockDomCharWidthPx`, gated +on `window.OffscreenCanvas` availability observed at the mock's `open()` call +— exactly mirroring the real CharSizeService constructor's strategy +selection), which the FN-7567 double could not represent (it modeled both +measurements as a single shared value). It fails on pre-fix code — where +`window.OffscreenCanvas` stays available throughout `open()`, so the mock +selects its "Canvas strategy" and the baked letter-spacing settles to a +persistent NONZERO value even after the full settle + pre/post-fit remeasure +sequence FN-7561/FN-7567 added — and passes once the fix hides +`OffscreenCanvas` around `open()`, converging the bake to exactly zero. +See `docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md` +recurrence-#5 section. +*/ +describe("TerminalModal — FN-7603 mobile inter-character spacing (Canvas vs DOM CharSizeService measurement divergence)", () => { + const mockOnClose = vi.fn(); + const mockSendInput = vi.fn(); + const mockResize = vi.fn(); + const mockReconnect = vi.fn(); + + const createMockTerminalState = (overrides = {}) => ({ + connectionStatus: "connected" as const, + sendInput: mockSendInput, + resize: mockResize, + onData: vi.fn(() => vi.fn()), + onExit: vi.fn(() => vi.fn()), + onConnect: vi.fn(() => vi.fn()), + onScrollback: vi.fn(() => vi.fn()), + reconnect: mockReconnect, + onSessionInvalid: vi.fn(() => vi.fn()), + ...overrides, + }); + + let previousInnerWidth: number; + let previousOntouchstart: unknown; + let previousOffscreenCanvas: unknown; + let hadOwnOffscreenCanvas: boolean; + + beforeEach(() => { + vi.clearAllMocks(); + resetFontRemeasureCount(); + resetMockTerminalGeometry(); + vi.spyOn(window, "matchMedia").mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); + previousInnerWidth = window.innerWidth; + previousOntouchstart = window.ontouchstart; + hadOwnOffscreenCanvas = Object.prototype.hasOwnProperty.call(window, "OffscreenCanvas"); + previousOffscreenCanvas = (window as unknown as { OffscreenCanvas?: unknown }).OffscreenCanvas; + // Real reported device: a narrow touch-primary mobile viewport with a + // modern engine that supports OffscreenCanvas (true on essentially every + // real mobile Safari/Chrome), so xterm's CharSizeService would pick its + // Canvas measurement strategy absent the fix. + Object.defineProperty(window, "innerWidth", { value: 390, configurable: true }); + Object.defineProperty(window, "ontouchstart", { value: null, configurable: true }); + Object.defineProperty(window, "OffscreenCanvas", { + value: class MockOffscreenCanvas {}, + writable: true, + configurable: true, + }); + window.localStorage.removeItem(TERMINAL_FONT_SIZE_KEY); + window.localStorage.removeItem(TERMINAL_PREFERENCES_KEY); + mockTerminalInstance.options.fontFamily = XTERM_FONT_FAMILY; + mockTerminalInstance.options.fontSize = 12; + mockTerminalInstance.options.cursorStyle = "block"; + mockTerminalInstance.options.cursorBlink = true; + resetFontRemeasureCount(); + resetMockTerminalGeometry(); + mockUseTerminal.mockReturnValue(createMockTerminalState()); + mockUseTerminalSessions.mockReturnValue(defaultSessionState); + mockUseWorkspaces.mockReturnValue({ + projectName: "kb", + workspaces: [], + loading: false, + error: null, + }); + mockCreateTerminalSession.mockResolvedValue({ + sessionId: "test-session-123", + shell: "/bin/bash", + cwd: "/project", + }); + }); + + afterEach(() => { + Object.defineProperty(window, "innerWidth", { value: previousInnerWidth, configurable: true }); + if (previousOntouchstart === undefined) { + delete (window as unknown as { ontouchstart?: unknown }).ontouchstart; + } else { + Object.defineProperty(window, "ontouchstart", { value: previousOntouchstart, configurable: true }); + } + if (hadOwnOffscreenCanvas) { + Object.defineProperty(window, "OffscreenCanvas", { + value: previousOffscreenCanvas, + writable: true, + configurable: true, + }); + } else { + delete (window as unknown as { OffscreenCanvas?: unknown }).OffscreenCanvas; + } + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("converges baked letter-spacing to exactly zero by forcing xterm off its Canvas character-measurement strategy during open()", async () => { + let resolveLoad: (() => void) | undefined; + let resolveReady: (() => void) | undefined; + Object.defineProperty(document, "fonts", { + value: { + load: vi.fn( + () => + new Promise((resolve) => { + resolveLoad = resolve; + }), + ), + ready: new Promise((resolve) => { + resolveReady = resolve; + }), + }, + configurable: true, + }); + + render(); + + await waitFor(() => expect(mockTerminalInstance.open).toHaveBeenCalled()); + resetFontRemeasureCount(); + + settleMockTerminalFontForCharSize(); + + await act(async () => { + resolveLoad?.(); + resolveReady?.(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(getFontRemeasureCount()).toBeGreaterThan(0); + }); + + /* + * The decisive geometry assertion for this recurrence: on pre-fix code, + * `window.OffscreenCanvas` stays defined throughout `terminal.open()`, so + * the mock's CharSizeService strategy selects "Canvas" and + * `mockCanvasCharWidthPx` diverges from `mockDomCharWidthPx` by + * `MOCK_CANVAS_DOM_DIVERGENCE_PX`. Even after the full FN-7561/FN-7567 + * settle + pre/post-fit remeasure sequence runs to completion, the baked + * letter-spacing does NOT converge to zero — it settles at a persistent, + * nonzero residual driven purely by the Canvas-vs-DOM measurement + * mismatch, exactly matching "still spaced apart even after every prior + * fix ran correctly". This assertion fails on HEAD before this task's fix + * and passes once `withDomBasedTerminalCharacterMeasurement` hides + * `OffscreenCanvas` around `open()`. + */ + await waitFor(() => { + expect(getMockBakedLetterSpacingPx()).toBeCloseTo(0, 5); + }); + }); + + it("also converges to zero with the mobile keyboard already open and a persisted 10px font", async () => { + window.localStorage.setItem(TERMINAL_FONT_SIZE_KEY, "10"); + + const mockVV = { + width: 375, + height: 300, + offsetTop: 0, + offsetLeft: 0, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }; + Object.defineProperty(window, "visualViewport", { + value: mockVV, + writable: true, + configurable: true, + }); + Object.defineProperty(window, "innerHeight", { value: 300, writable: true, configurable: true }); + + let resolveLoad: (() => void) | undefined; + let resolveReady: (() => void) | undefined; + Object.defineProperty(document, "fonts", { + value: { + load: vi.fn( + () => + new Promise((resolve) => { + resolveLoad = resolve; + }), + ), + ready: new Promise((resolve) => { + resolveReady = resolve; + }), + }, + configurable: true, + }); + + render(); + + await waitFor(() => expect(mockTerminalInstance.open).toHaveBeenCalled()); + resetFontRemeasureCount(); + settleMockTerminalFontForCharSize(); + + await act(async () => { + resolveLoad?.(); + resolveReady?.(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(getFontRemeasureCount()).toBeGreaterThan(0); + }); + + await waitFor(() => { + expect(getMockBakedLetterSpacingPx()).toBeCloseTo(0, 5); + }); + + Object.defineProperty(window, "visualViewport", { value: undefined, writable: true, configurable: true }); + }); + + it("does not regress when the real browser has no OffscreenCanvas support (xterm already self-selects the DOM strategy)", async () => { + window.localStorage.removeItem(TERMINAL_FONT_SIZE_KEY); + delete (window as unknown as { OffscreenCanvas?: unknown }).OffscreenCanvas; + settleMockTerminalFontForCharSize(); + + Object.defineProperty(document, "fonts", { + value: { + load: vi.fn(() => Promise.resolve()), + ready: Promise.resolve(), + }, + configurable: true, + }); + + render(); + await waitFor(() => expect(mockTerminalInstance.open).toHaveBeenCalled()); + + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + + await waitFor(() => { + expect(getMockBakedLetterSpacingPx()).toBeCloseTo(0, 5); + }); + }); +}); diff --git a/packages/dashboard/app/utils/terminalPreferences.ts b/packages/dashboard/app/utils/terminalPreferences.ts index a97d5a2ba8..38e2aac000 100644 --- a/packages/dashboard/app/utils/terminalPreferences.ts +++ b/packages/dashboard/app/utils/terminalPreferences.ts @@ -231,6 +231,69 @@ export function forceTerminalFontRemeasure( terminal.options.fontFamily = fontFamily; } +/* +FNXC:Terminal 2026-07-05-12:40: +FN-7603 recurrence #5 root cause (grounded in the installed `@xterm/xterm@5.5.0` +source, see task doc key="xterm-source-audit" on FN-7603): xterm's +`CharSizeService` picks its character-measurement strategy at construction time +(inside `terminal.open()`) via `try { new OffscreenCanvasStrategy(optionsService) } +catch { new DomFallbackStrategy(document, helperContainer, optionsService) }`. +Whenever `OffscreenCanvas` + `CanvasRenderingContext2D.measureText()` reporting +`fontBoundingBoxAscent`/`fontBoundingBoxDescent` are available — true on +essentially every real modern mobile Safari/Chrome — the CANVAS strategy is +chosen, and `dimensions.css.cell.width` (which feeds `FitAddon.fit()`'s column +count AND `DomRenderer._setDefaultSpacing()`'s baked letter-spacing) is measured +via Canvas 2D `ctx.measureText("W")`. Separately, `DomRenderer._setDefaultSpacing()` +subtracts `WidthCache.get('W')` — an ENTIRELY SEPARATE, DOM-based measurement +(`offsetWidth` of a hidden 32x-repeated-"W" span) — from that canvas-measured +cell width to compute the compensating letter-spacing baked onto `.xterm-rows`. +Those are two DIFFERENT browser text-rendering pipelines (Canvas 2D vs CSS/DOM +layout) queried against the same font; real glyphs are painted 100% via DOM +(`DomRenderer` never draws through canvas), so the only way for the baked +spacing to reliably converge to the DOM's own natural (tight) glyph advance is +for BOTH measurements to go through the SAME (DOM) pipeline. FN-7456/FN-7460/ +FN-7561/FN-7567 never touched which measurement strategy CharSizeService uses, +only WHEN/how often it remeasures — so this Canvas-vs-DOM divergence survived +all four prior fixes and can still bake a small-but-visible, non-zero, +systematic inter-character gap on the very first mobile layout even after every +prior remedy runs correctly. Force xterm onto the SAME (DOM) measurement +pipeline CharSizeService already ships as its own fallback strategy by making +`OffscreenCanvas` transiently unavailable for the synchronous duration of +`terminal.open()`, so `CharSizeService`'s constructor try-block throws and it +self-selects its own DOM-based `l` strategy — unifying `dimensions.css.cell.width` +and `WidthCache.get('W')` onto one measurement pipeline instead of adding any +hardcoded compensation. See `docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md` +recurrence #5 section. +*/ +export function withDomBasedTerminalCharacterMeasurement(fn: () => T): T { + if (typeof window === "undefined" || !("OffscreenCanvas" in window)) { + // Nothing to hide — CharSizeService will already fall back to its DOM + // strategy on its own (e.g. older WebKit without OffscreenCanvas support). + return fn(); + } + + const descriptor = Object.getOwnPropertyDescriptor(window, "OffscreenCanvas"); + const originalValue = (window as unknown as Record).OffscreenCanvas; + + try { + delete (window as unknown as Record).OffscreenCanvas; + } catch { + // Some environments define OffscreenCanvas as non-configurable; nothing + // we can safely do, so proceed without forcing the DOM strategy. + return fn(); + } + + try { + return fn(); + } finally { + if (descriptor) { + Object.defineProperty(window, "OffscreenCanvas", descriptor); + } else if (originalValue !== undefined) { + (window as unknown as Record).OffscreenCanvas = originalValue; + } + } +} + function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } From f4f165640aee51a9c0735fe92676ba028ab32c52 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 5 Jul 2026 19:51:35 -0700 Subject: [PATCH 23/24] FN-7607: fix manual PR flow gating to key off global auto-merge setting Fixes TaskDetailModal so manual PR affordances stay visible based on the live global auto-merge setting rather than the per-task effective override, and repairs a pre-existing test regression from the FN-7510 oversight default change. - isManualPrFlow now checks mergeStrategy === "pull-request" && !autoMergeEnabled (live global setting) instead of the per-task effective auto-merge override, fixing a regression from FN-7255 that stranded users without manual PR controls when a task's auto-merge override was true but global auto-merge was off. - Pinned plannerOversightLevel: "off" on the Chat-first default-routing test fixture so the FN-7510 autonomous-oversight default doesn't add an extra Activity-view option and break the test's actual intent (asserting Chat-first tab routing). - Added changeset documenting the fix. Files changed: .changeset/fn-7607-manual-pr-flow.md | 7 +++++++ packages/dashboard/app/components/TaskDetailModal.tsx | 14 +++++++++++++- .../TaskDetailModal.attachments-and-tabs.test.tsx | 12 +++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7607 Fusion-Task-Lineage: f0b077d4-792f-4e43-8e40-43d325920be5 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7607-manual-pr-flow.md | 7 +++++++ .../dashboard/app/components/TaskDetailModal.tsx | 14 +++++++++++++- .../TaskDetailModal.attachments-and-tabs.test.tsx | 12 +++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/fn-7607-manual-pr-flow.md diff --git a/.changeset/fn-7607-manual-pr-flow.md b/.changeset/fn-7607-manual-pr-flow.md new file mode 100644 index 0000000000..9a4af33b9c --- /dev/null +++ b/.changeset/fn-7607-manual-pr-flow.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix manual PR actions hidden when a task auto-merge override was on but global auto-merge was off. +category: fix +dev: TaskDetailModal isManualPrFlow now keys off live global autoMergeEnabled, not the per-task effective override (regression from FN-7255). diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index a1edff3916..3f6f8fc86b 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -3130,7 +3130,19 @@ export function TaskDetailContent({ const mergeStrategy = settings?.mergeStrategy ?? "direct"; const autoMergeEnabled = autoMergeEnabledProp ?? (settings?.autoMerge ?? false); const effectiveAutoMerge = resolveEffectiveAutoMerge({ autoMerge: task.autoMerge }, { autoMerge: autoMergeEnabled }); - const isManualPrFlow = mergeStrategy === "pull-request" && !effectiveAutoMerge; + /* + FNXC:TaskDetailPr 2026-07-05-19:45: + Manual PR flow visibility must follow the LIVE GLOBAL auto-merge setting + (`autoMergeEnabled`), not the per-task effective auto-merge override + (`effectiveAutoMerge`). Otherwise a per-task auto-merge override of `true` + hides manual PR affordances even when global auto-merge is off, stranding + the user with no way to manually open/manage the PR (FN-7607; regression + introduced by FN-7255 / commit 924bcb97d, which switched this from + `!autoMergeEnabled` to `!effectiveAutoMerge`). The `autoMerge` prop passed + to PrPanel stays `effectiveAutoMerge` — only this flow-gating boolean is + keyed off the live global setting. + */ + const isManualPrFlow = mergeStrategy === "pull-request" && !autoMergeEnabled; /* FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 enablement rules for the nudge/stop/explain controls. Nudge and diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx index d61fdde6b1..959a10ec4b 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx @@ -483,9 +483,19 @@ describe("TaskDetailModal", () => { describe("tab toggle", () => { it("restores planner Chat as the omitted non-done default when Chat-first is enabled", () => { + /* + FNXC:PlannerOversight 2026-07-05-19:45: + FN-7510 made DEFAULT_PLANNER_OVERSIGHT_LEVEL = "autonomous", so a task + fixture with no per-task override and no resolvable workflow now + legitimately resolves oversight-active, which surfaces an additional + "Interventions" Activity-view option. This test's intent is to assert + Chat-first default routing (the omitted-tab default lands on Chat), not + oversight gating, so pin plannerOversightLevel: "off" to keep the + three-label Activity-view assertion meaningful and honest (FN-7607). + */ const { container } = render( Date: Sun, 5 Jul 2026 19:57:09 -0700 Subject: [PATCH 24/24] chore(release): v0.56.1 Version bump via changesets. --- .changeset/anthropic-oauth-refresh-scope.md | 7 - .changeset/coding-ideas-custom-column-move.md | 7 - .changeset/fn-7602-shortcut-row-layout.md | 7 - .changeset/fn-7603-mobile-terminal-spacing.md | 7 - .changeset/fn-7607-manual-pr-flow.md | 7 - CHANGELOG.md | 598 ++++++++++++++---- package.json | 2 +- packages/cli-alias/CHANGELOG.md | 11 + packages/cli-alias/package.json | 2 +- packages/cli/CHANGELOG.md | 20 + packages/cli/package.json | 2 +- packages/core/CHANGELOG.md | 2 + packages/core/package.json | 2 +- packages/dashboard/CHANGELOG.md | 17 + packages/dashboard/package.json | 2 +- packages/desktop/CHANGELOG.md | 8 + packages/desktop/package.json | 2 +- packages/droid-cli/CHANGELOG.md | 6 + packages/droid-cli/package.json | 2 +- packages/engine/CHANGELOG.md | 7 + packages/engine/package.json | 2 +- packages/i18n/CHANGELOG.md | 6 + packages/i18n/package.json | 2 +- packages/mobile/CHANGELOG.md | 2 + packages/mobile/package.json | 2 +- packages/pi-claude-cli/CHANGELOG.md | 2 + packages/pi-claude-cli/package.json | 2 +- packages/plugin-sdk/CHANGELOG.md | 6 + packages/plugin-sdk/package.json | 2 +- .../fusion-plugin-auto-label/CHANGELOG.md | 6 + .../fusion-plugin-auto-label/package.json | 2 +- .../fusion-plugin-ci-status/CHANGELOG.md | 6 + .../fusion-plugin-ci-status/package.json | 2 +- .../fusion-plugin-notification/CHANGELOG.md | 6 + .../fusion-plugin-notification/package.json | 2 +- .../fusion-plugin-settings-demo/CHANGELOG.md | 6 + .../fusion-plugin-settings-demo/package.json | 2 +- .../fusion-plugin-acp-runtime/CHANGELOG.md | 7 + .../fusion-plugin-acp-runtime/package.json | 2 +- .../fusion-plugin-agent-browser/CHANGELOG.md | 6 + .../fusion-plugin-agent-browser/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../fusion-plugin-cursor-runtime/CHANGELOG.md | 6 + .../fusion-plugin-cursor-runtime/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../fusion-plugin-droid-runtime/CHANGELOG.md | 6 + .../fusion-plugin-droid-runtime/package.json | 2 +- .../CHANGELOG.md | 7 + .../package.json | 2 +- .../fusion-plugin-hermes-runtime/CHANGELOG.md | 6 + .../fusion-plugin-hermes-runtime/package.json | 2 +- .../fusion-plugin-linear-import/CHANGELOG.md | 7 + .../fusion-plugin-linear-import/package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- .../CHANGELOG.md | 6 + .../package.json | 2 +- plugins/fusion-plugin-reports/CHANGELOG.md | 8 + plugins/fusion-plugin-reports/package.json | 2 +- plugins/fusion-plugin-roadmap/CHANGELOG.md | 7 + plugins/fusion-plugin-roadmap/package.json | 2 +- .../fusion-plugin-whatsapp-chat/CHANGELOG.md | 6 + .../fusion-plugin-whatsapp-chat/package.json | 2 +- 67 files changed, 729 insertions(+), 176 deletions(-) delete mode 100644 .changeset/anthropic-oauth-refresh-scope.md delete mode 100644 .changeset/coding-ideas-custom-column-move.md delete mode 100644 .changeset/fn-7602-shortcut-row-layout.md delete mode 100644 .changeset/fn-7603-mobile-terminal-spacing.md delete mode 100644 .changeset/fn-7607-manual-pr-flow.md diff --git a/.changeset/anthropic-oauth-refresh-scope.md b/.changeset/anthropic-oauth-refresh-scope.md deleted file mode 100644 index 6dbb6e0632..0000000000 --- a/.changeset/anthropic-oauth-refresh-scope.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix Anthropic subscription showing "logged in" while all model calls fail. -category: fix -dev: Two-part fix. (1) OAuth token refresh in `packages/engine/src/auth-storage.ts` sent a `scope` param (defaulting to `user:profile`), which per RFC 6749 §6 re-issued the access token narrowed to that scope and stripped `user:inference` — so refreshed tokens 403'd on every model call. Refresh now omits `scope` (preserving the originally-granted scopes, matching pi-ai's own refresh), and `ANTHROPIC_DEFAULT_SCOPES` mirrors the full Claude Code scope set. (2) `/auth/status` now reports an unexpired Anthropic OAuth token that lacks an inference scope as not-connected (authenticated:false, expired:true so the re-login banner fires) with a scope-specific loginError, instead of falsely claiming a live session. Existing narrowed tokens need one re-login to obtain a fresh broad grant. diff --git a/.changeset/coding-ideas-custom-column-move.md b/.changeset/coding-ideas-custom-column-move.md deleted file mode 100644 index b0fcc920fb..0000000000 --- a/.changeset/coding-ideas-custom-column-move.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix "Invalid transition" error when moving cards out of a custom workflow column like Coding (Ideas) → Ideas. -category: fix -dev: moveTaskInternal's compat-flag legacy path validated moves against the legacy VALID_TRANSITIONS table, which is keyed only by the built-in column ids; a task in a non-legacy workflow column (e.g. "ideas") had no key and every move was rejected. The legacy branch now resolves a non-legacy source column's targets from the task's own workflow adjacency (resolveAllowedColumns) while preserving the legacy bare-Error contract for legacy columns. diff --git a/.changeset/fn-7602-shortcut-row-layout.md b/.changeset/fn-7602-shortcut-row-layout.md deleted file mode 100644 index 908fbba219..0000000000 --- a/.changeset/fn-7602-shortcut-row-layout.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix overlapping Record and Clear buttons in the Keyboard Shortcuts settings rows on desktop and mobile. -category: fix -dev: The shortcut-capture Record/Clear buttons no longer use the icon-only `btn-icon` class (which set `line-height:0` and a mobile 36px square, clipping/overlapping the text labels); they use a text-button class and the `.shortcut-capture` row locks buttons with `flex-shrink:0` so the input and controls never overlap, stacking cleanly on mobile. diff --git a/.changeset/fn-7603-mobile-terminal-spacing.md b/.changeset/fn-7603-mobile-terminal-spacing.md deleted file mode 100644 index 65ace4897f..0000000000 --- a/.changeset/fn-7603-mobile-terminal-spacing.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix persistent mobile terminal inter-character spacing (5th recurrence root cause). -category: fix -dev: xterm's CharSizeService picks a Canvas-based (OffscreenCanvas) or DOM-based character-measurement strategy at terminal.open() time; DomRenderer's letter-spacing bake always measures via a separate DOM-based WidthCache, so a Canvas-vs-DOM measurement mismatch survived FN-7561/FN-7567's remeasure-ordering fixes. `withDomBasedTerminalCharacterMeasurement` in terminalPreferences.ts forces CharSizeService onto the same DOM strategy for both TerminalModal and SessionTerminal. diff --git a/.changeset/fn-7607-manual-pr-flow.md b/.changeset/fn-7607-manual-pr-flow.md deleted file mode 100644 index 9a4af33b9c..0000000000 --- a/.changeset/fn-7607-manual-pr-flow.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@runfusion/fusion": patch ---- - -summary: Fix manual PR actions hidden when a task auto-merge override was on but global auto-merge was off. -category: fix -dev: TaskDetailModal isManualPrFlow now keys off live global autoMergeEnabled, not the per-task effective override (regression from FN-7255). diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d1b72104..833a246cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,123 +2,485 @@ User-facing release notes aggregated across all packages. This file is auto-synced from each `packages/*/CHANGELOG.md` by `scripts/release.mjs` — do not edit by hand. -## 0.56.0 - -### New - -- Add a workflow setting to disable automatic large-task triage splitting. -- Expand first-run AI provider quick-start choices beyond Anthropic. -- Show Git prerequisite guidance during first-run GitHub onboarding. -- Add GitHub OAuth and CLI setup actions to first-run onboarding. -- Add configurable dashboard keyboard shortcuts for Quick Chat and Terminal. -- Add search in Settings so operators can find settings faster. -- Add before-to-after transformation summaries to generated task definitions. -- Add a pinned below-application layout option for the dashboard terminal. -- Show first-token and tool processing durations in task agent logs. -- Settings descriptions now show each setting's default value. -- Add a Reset Settings button to restore a menu's or all project settings to defaults. -- Add a per-workflow planner oversight level setting (Off, Observe, Steer, Autonomous recovery). -- Tasks can override the workflow planner oversight level (Off, Observe, Steer, Autonomous recovery). -- Planner oversight now defaults to full steering/control for every workflow unless explicitly changed. -- Planner oversight now monitors tasks across executor, reviewer, merger, pull-request, and workflow-gate stages. -- Planner oversight can autonomously inject guidance, retry stuck/failed steps, and request fixes within bounded limits. -- Planner oversight now requires confirmation before merge/PR actions and destructive/external side effects. -- Planner overseer now stays fully hands-off for paused tasks and auto-merge-off / human-review tasks. -- Configure planner oversight level per task and per project in the workflow editor and task create/detail. -- Add a configurable planner-overseer notification verbosity level (Silent/Errors/Important/All). -- Add a task-detail planner-overseer intervention timeline (stage, reason, action, outcome, attempts, links). -- Emit planner-overseer run-audit events for observations, steering, retries, recovery, confirmations, and escalations. -- Add an intelligent git-revert engine service and POST /api/tasks/:id/revert route. -- Add an AI-undo fallback task when reverting a done task via git conflicts or is unsupported. -- Add a Revert action to Done/Archived task cards to undo landed changes. -- Capture a structured performance snapshot when an agent task completes. -- Task cards can now show the planner overseer's active state (idle/watching/steering/recovering/awaiting-confirmation). -- Original task prompt now renders as Markdown and is collapsed by default in the task Plan tab. -- Support reverting multi-repo workspace tasks via git, all-or-nothing across sub-repos. -- Add per-sha revert commit granularity to the task revert API and service. -- Add a dedicated Keyboard Shortcuts settings section with click-to-record capture and more configurable actions. -- Open a revert PR for done/archived tasks when autoMerge is disabled instead of refusing. -- AI-undo tasks now default to a configurable, stricter review workflow. -- Plan auto-approval is now the default; specified tasks skip manual approval unless you opt into workflow/require-all. -- Move the planner intervention timeline into the task Activity view dropdown. -- Fusion self-repo issue-close comments now show current and target release versions. -- Open one revert PR per sub-repo for workspace tasks when autoMerge is disabled. -- Add a Settings → General picker to choose the workflow used for AI-undo (revert) tasks. -- Add "Ask user question" and "Exit gate" workflow nodes for mid-flow chat reach-out and early exit. -- Add a built-in "Brainstorming" workflow that talks to you before planning. -- Add a "Coding (Ideas)" workflow with a manual Ideas intake and a merged Todo planner column. +## 0.56.1 ### Fixed -- Show active Plan Review progress on triage task cards. -- Clarify task-detail oversight Nudge/Explain controls: visible label, disabled reason, always-openable Explain panel. -- Unify border, radius, and height of the task-detail Priority/Execution/Oversight controls. -- Keep the task-detail Activity view menu open during mobile iOS taps. -- Fix Anthropic subscription login when pasted callback URLs contain fragment OAuth parameters. -- Fix Claude/Anthropic subscription re-login showing "Login did not complete" after logging out. -- Stop self-healing from killing actively-running tasks after ~30 minutes. -- Stop Windows Terminal version dialogs from popping up when opening the dashboard or Settings on Windows. -- Select newly created folders automatically during project setup. -- Prevent Desktop update banners from using 0.0.0 as the current version. -- Quit Fusion Desktop on Windows when the window is closed. -- Open desktop Anthropic Subscription OAuth logins in the system browser. -- Delay GitHub setup warnings for one day and add a dashboard connect action. -- Fix a false AI engine not running banner in desktop mode. -- Clarify the desktop Connection Manager add-remote flow. -- Restore Local Server in the desktop Switch server list. -- Make right-dock task list clicks respect the task popup setting. -- Auto-retry retryable Code Review remediation failures. -- Fix no-op task branch recovery after a previously landed task. -- Allow documented source-free task-artifact deliveries to finish without commits. -- Fix direct merges so Push to remote after merge honors the configured remote and branch. -- Keep task popups on the board layer with Activity menus above them. -- Keep accepted chat requests waiting instead of showing false first-event timeout failures. -- Show each task's original prompt in the Plan tab alongside the generated plan. -- Restore terminal Ctrl/Cmd copy and paste shortcuts. -- Fix mobile Chat composer being hidden behind the keyboard accessory bar. -- Auto-approve now reliably sends specified plans to the board without a manual approval stop. -- Fix the in-dashboard Switch server menu not switching desktop local/remote. -- Fix branch group completion checklists to show accurate landed/finished counts. -- Branch groups no longer report complete (or become promotable) when an unlanded member is archived. -- Fix the global GitLab integration setting not persisting when saved. -- Fix task-detail Activity view dropdown not opening reliably on mobile. -- Manual "Run now" for the Database Backup automation now runs in-process like the scheduler, matching cron behavior. -- Task cards no longer show the "Auto-recovery" oversight badge unless oversight is explicitly configured. -- Remove the per-card overseer-state ("Executor") badge from task cards. -- Fix agent-created artifacts not appearing live in the dashboard artifacts view. -- Fix the mobile terminal shortcut bar so it scrolls horizontally to reach every key. -- Planner-oversight intervention timeline now populates from real engine activity. -- Show the "Global" prefix on the Authentication entry in the mobile Settings picker. -- Tasks held for release authorization or Plan Review are now shown distinctly, so auto-approve no longer looks broken. -- Move mobile terminal controls into a bottom footer so they no longer crowd the header, with a scrollable shortcut bar. -- Stop the release-authorization gate from holding tasks that merely disclaim releasing. -- Fix mobile terminal text still rendering with excess inter-character gaps after font-load settle. -- Stop Plan Review from looping tasks forever and fix its "can't find the plan" reviews. -- Planner-overseer task badge now shows a readable label and explains what it is waiting on. -- Plan approve/reject API now blocks release-authorization holds, requiring the authorization marker first. -- Pin the mobile terminal close (X) button to the top-right corner so it is easy to find and tap. -- Fix mobile terminal excess character spacing that survived earlier font-remeasure fixes. -- Manual plan approval no longer re-asks you to approve a plan you already approved when it hasn't changed. -- Expired Claude subscription logins now show disconnected with a re-login prompt; tokens auto-refresh before expiry. -- Anthropic subscription reads now refresh the OAuth token automatically instead of silently failing when expired. -- Stop GitHub tracking-issue creation from linking new tasks to old/closed issues. -- Clarify the oversight "Nudge unavailable" guideline so it no longer reads as an overseer fault. -- New tasks created under the Coding (Ideas) workflow now land in the Ideas column and wait for you to promote them. -- Fix tasks vanishing from the board after being added to a workflow like Coding (Ideas). -- Move the Before → After transformation summary to the top of generated task definitions. -- Task-detail Priority dropdown now matches the Oversight dropdown's size, border, and typography. -- Default workflow boards now label the intake column "Planning" instead of "Triage". -- Fix the task-detail Nudge control staying disabled when the overseer is actively watching. -- Honor mission branchStrategy when triage omits branchAssignment; skip validation for inactive missions. -- Planner overseer no longer marks healthy in-progress tasks as "recovering" or steers them. +- Fix Anthropic subscription showing "logged in" while all model calls fail. +- Fix "Invalid transition" error when moving cards out of a custom workflow column like Coding (Ideas) → Ideas. +- Fix overlapping Record and Clear buttons in the Keyboard Shortcuts settings rows on desktop and mobile. +- Fix persistent mobile terminal inter-character spacing (5th recurrence root cause). +- Fix manual PR actions hidden when a task auto-merge override was on but global auto-merge was off. -### Breaking +## 0.56.0 -- Remove the eye icon markdown/plain toggle from chat; messages always render as Markdown. +### @fusion/dashboard -### Internal +#### Patch Changes -- Rename downloadable CLI release binaries to the fn-cli- base name. +- @fusion/core@0.56.0 +- @fusion/engine@0.56.0 +- @fusion/i18n@0.39.20 +- @fusion-plugin-examples/cli-printing-press@0.1.37 +- @fusion-plugin-examples/compound-engineering@0.1.20 +- @fusion-plugin-examples/dependency-graph@0.1.51 +- @fusion-plugin-examples/roadmap@0.1.39 +- @fusion-plugin-examples/cursor-runtime@0.1.39 +- @fusion-plugin-examples/droid-runtime@0.1.46 +- @fusion-plugin-examples/hermes-runtime@0.2.70 +- @fusion-plugin-examples/openclaw-runtime@0.2.70 +- @fusion-plugin-examples/paperclip-runtime@0.2.70 + +### @fusion/desktop + +#### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/dashboard@0.56.0 +- @fusion/engine@0.56.0 + +### @fusion/engine + +#### Patch Changes + +- @fusion/core@0.56.0 +- @fusion/pi-claude-cli@0.56.0 + +### @fusion/plugin-sdk + +#### Patch Changes + +- @fusion/core@0.56.0 + +### @runfusion/fusion + +#### Minor Changes + +- d16c8b4: summary: Expand first-run AI provider quick-start choices beyond Anthropic. + category: feature + dev: Moves advanced/all-provider onboarding controls under the quick-start provider section. +- 315f3bc: summary: Show Git prerequisite guidance during first-run GitHub onboarding. + category: feature + dev: Adds bounded server-host git availability to auth status and onboarding. +- 50cdab1: summary: Add GitHub OAuth and CLI setup actions to first-run onboarding. + category: feature + dev: GitHub onboarding now shows in-flow OAuth connect, gh auth login, and gh install guidance. +- 2f23d22: summary: Add configurable dashboard keyboard shortcuts for Quick Chat and Terminal. + category: feature + dev: Global dashboardKeyboardShortcuts settings, guarded document-level key handling, and Escape topmost-popup dismissal. +- efa8105: summary: Add search in Settings so operators can find settings faster. + category: feature + dev: Dashboard Settings filters visible sections by setting labels and keywords. +- 7d8a1b8: summary: Add a pinned below-application layout option for the dashboard terminal. + category: feature + dev: Terminal display mode now supports persisted docked, floating, and below layouts, with header controls replacing the footer shell. +- 87a700c: summary: Add a Reset Settings button to restore a menu's or all project settings to defaults. + category: feature + dev: New tested section→keys (scope-aware) registry (packages/dashboard/app/components/settings/section-keys.ts) drives per-menu reset via updateSettings/updateGlobalSettings with null-as-delete; non-blob sections (secrets, MCP, plugins, memory, auth, prompts, CLI agents, runtimes) are excluded with a documented reason. +- 68f5153: summary: Add a per-workflow planner oversight level setting (Off, Observe, Steer, Autonomous recovery). + category: feature + dev: New workflow setting `plannerOversightLevel` declared in BUILTIN_OVERSIGHT_SETTINGS; default `autonomous`. Per-task override and engine behavior land in follow-up tasks. +- aa757bc: summary: Tasks can override the workflow planner oversight level (Off, Observe, Steer, Autonomous recovery). + category: feature + dev: New nullable Task.plannerOversightLevel field (migration 137, SCHEMA_VERSION 137) mirroring executionMode; NULL inherits the workflow setting. Adds resolveEffectivePlannerOversightLevel precedence helper. Dashboard UI/API threading and engine behavior land in follow-up tasks. +- 0689250: summary: Planner oversight now defaults to full steering/control for every workflow unless explicitly changed. + category: feature + dev: Confirms the `plannerOversightLevel` workflow-setting default is the highest (autonomous) level; unset workflow value and unset per-task override both resolve to full steering via `resolveEffectivePlannerOversightLevel` (task override → workflow effective value → autonomous), adding dedicated regression coverage for the "unless explicitly disabled" precedence. +- 12a6d1b: summary: Planner oversight now monitors tasks across executor, reviewer, merger, pull-request, and workflow-gate stages. + category: feature + dev: Adds records-only PlannerOverseerMonitor + resolveWatchedStage + OverseerStageObservation in @fusion/engine, gated by resolveEffectivePlannerOversightLevel (off = no observation) and wired into ProjectEngine via a bounded poll. Steering/recovery and UI land in FN-7512/FN-7515+. +- 81f2053: summary: Planner oversight can autonomously inject guidance, retry stuck/failed steps, and request fixes within bounded limits. + category: feature + dev: Adds pure `decidePlannerRecovery` + recovery types (core) and `PlannerRecoveryController` with injected guidance/retry/targeted-fix handlers (engine), consuming the FN-7511 observation. Acts only at effective level `autonomous`, caps attempts per (task, stage) via `PLANNER_RECOVERY_MAX_ATTEMPTS`, skips user-paused tasks, and excludes merge/PR/destructive actions (deferred to FN-7513) and comprehensive human-control safeguards (FN-7514). +- 2cc84b5: summary: Planner oversight now requires confirmation before merge/PR actions and destructive/external side effects. + category: feature + dev: Adds `PlannerActionSideEffectClass` + `PlannerConfirmationRequest` and `classifyPlannerActionSideEffect`/`requiresPlannerConfirmation` (core), extends `decidePlannerRecovery` with an `await_confirmation` action, and adds `requestConfirmation`/`resolveConfirmation` gating to `PlannerRecoveryController` (engine). Merge/PR and destructive/external actions never execute without a recorded approval; bounded recovery (guidance/retry/targeted-fix) is unchanged. UX rendering, human-control safeguards, timeline, and run-audit land in follow-up tasks. +- 79ab367: summary: Planner overseer now stays fully hands-off for paused tasks and auto-merge-off / human-review tasks. + category: feature + dev: Adds the pure `evaluateOverseerHumanControl` policy (packages/engine/src/overseer-human-control-policy.ts), consulted at the top of `PlannerRecoveryController.tick()` before any action classification, confirmation gating, steering, retry, or dispatch — so a user-paused or `autoMerge:false`/human-review task never even records a pending confirmation. Reuses `allowsAutoMergeProcessing` from `@fusion/core` verbatim (never re-derives the auto-merge/human-review predicate). Distinguishes explicit user pause (`task.userPaused===true`, or `task.paused===true` with no `pausedReason`) from engine/self-healing parks (which always stamp a `pausedReason`). Emits a bounded `overseer:oversight-withheld-human-control` run-audit no-action event (metadata: `{ taskId, reason, stage, oversightLevel }`), deduped per (taskId, reason) so it does not spam every poll. +- c16cc9e: summary: Configure planner oversight level per task and per project in the workflow editor and task create/detail. + category: feature + dev: Per-task `plannerOversightLevel` override exposed via TaskForm (Inherit/off/observe/steer/autonomous), threaded through createTask/updateTask; workflow-editor Values tab gets a first-class display entry. Workflow-native setting; not a project setting. +- aae603b: summary: Add a configurable planner-overseer notification verbosity level (Silent/Errors/Important/All). + category: feature + dev: New workflow-native enum setting `plannerOversightNotificationLevel` in BUILTIN_OVERSIGHT_SETTINGS; default `important`. Resolves via resolveEffectiveSettings; emission gating that reads it lands in FN-7519/FN-7520. +- d10ea9a: summary: Add a task-detail planner-overseer intervention timeline (stage, reason, action, outcome, attempts, links). + category: feature + dev: New core `PlannerInterventionEntry` model + `recordPlannerIntervention`/`getPlannerInterventionTimeline` helpers persisting via the run-audit store under the `overseer:intervention` mutation, plus a `PlannerInterventionTimeline` component rendered in the task-detail Planner Oversight cluster. Emission call-sites land in FN-7520. +- bf68839: summary: Emit planner-overseer run-audit events for observations, steering, retries, recovery, confirmations, and escalations. + category: feature + dev: New core emitters (emitOverseerObservation/Steering/RecoveryAttempt/Retry/Confirmation/Escalation) in planner-overseer-events.ts, each mapping its decision-point to the correct intervention action/outcome and delegating to FN-7519's recordPlannerIntervention under the overseer:intervention mutation. Producer call-sites land in FN-7511/FN-7512/FN-7513. +- c4d81fe: summary: Add an AI-undo fallback task when reverting a done task via git conflicts or is unsupported. + category: feature + dev: `POST /api/tasks/:id/revert` now accepts `{ mode?: "git" | "ai" | "auto" }` (default `"auto"`). `"auto"` tries the FN-7523 git-revert path first and falls back to creating an AI-undo board task (`{ mode: "ai", createdTaskId, alreadyOpen? }`) on a conflicting or unsupported (e.g. workspace) git result; `needsHuman` (autoMerge-off) never triggers the fallback. `"ai"` always creates the AI-undo task; `"git"` keeps the FN-7523 git-only contract, which is otherwise unchanged. New engine exports: `createAiUndoTask`, `buildAiUndoTaskDescription`, `REVERT_OF_METADATA_KEY`. New core store method `TaskStore.findOpenRevertTaskForSource` backs the idempotency guard (an open undo task suppresses a duplicate; a closed one does not). +- e7cb2f1: summary: Add a Revert action to Done/Archived task cards to undo landed changes. + category: feature + dev: Wires onRevertTask through Board/List/Detail surfaces; calls POST /tasks/:id/revert in "auto" mode with a conflict-confirm AI-undo fallback (mode: "ai"). +- 5ad8ec8: summary: Capture a structured performance snapshot when an agent task completes. + category: feature + dev: New AgentReflectionService.captureTaskPerformance persists a non-LLM post-task ReflectionMetrics record (duration, packages/files touched, verification command + scope, retry/rework count) and emits ids/counts-only `reflection:captured` run-audit telemetry; populates performanceSummary/latestReflection. +- 726cbf8: summary: Task cards can now show the planner overseer's active state (idle/watching/steering/recovering/awaiting-confirmation). + category: feature + dev: Adds a serializable `PlannerOverseerRuntimeSnapshot` + pure `derivePlannerOverseerState` (core), a read-only `ProjectEngine.getPlannerOverseerRuntimeSnapshot(taskId)` accessor assembling it from the FN-7511 monitor + FN-7512/7513 recovery controller, and a best-effort additive `plannerOverseerState` enrichment on `GET /api/tasks` (mirrors the `branchProgress` pattern; never persisted, never fails the board load). Consumed by FN-7516's TaskCard. +- 2ed06f9: summary: Support reverting multi-repo workspace tasks via git, all-or-nothing across sub-repos. + category: feature + dev: Extends `packages/engine/src/task-revert.ts` with `resolveWorkspaceTaskRevertCommits`/`revertWorkspaceTask` and wires `POST /api/tasks/:id/revert` to dispatch workspace tasks (`isWorkspaceTask`) to the new path; returns `{ mode: "git", clean, workspace: { repos: [...] }, conflicts? }`. Single-repo `performTaskRevert` path is unchanged. +- 8c6f76c: summary: Add per-sha revert commit granularity to the task revert API and service. + category: feature + dev: `performTaskRevert` and `POST /api/tasks/:id/revert` accept an optional `granularity: "squash" | "per-sha"` (default `"squash"`, unchanged FN-7523 behavior). `"per-sha"` creates one attributed `revert(FN-xxxx)` commit per original sha (each with its own `Fusion-Task-Id` trailer and audit line), skipping no-op shas without empty commits. A mid-batch conflict in either mode rolls back the whole batch to the pre-call HEAD — no partially-landed per-sha commits. The clean result now reports `revertCommitShas: string[]` (all created commits) alongside the existing `revertCommitSha` (kept for backward compatibility). +- f992e6a: summary: Add a dedicated Keyboard Shortcuts settings section with click-to-record capture and more configurable actions. + category: feature + dev: Relocates dashboardKeyboardShortcuts into its own settings section, adds a ShortcutCaptureInput recorder, and extends DashboardShortcutAction with openFiles/openSettings/openCommandCenter/newTask actions wired into existing App nav handlers. +- 2df6c35: summary: Open a revert PR for done/archived tasks when autoMerge is disabled instead of refusing. + category: feature + dev: `POST /api/tasks/:id/revert` gains an additive `{ mode: "pr", clean: true, prUrl, prNumber, revertBranch, existingPr? }` result for clean single-repo reverts under `autoMerge:false`, reusing `GitHubClient.createPr`, `findPrForBranch` idempotency, and the `manual:true` PR handoff. New engine export `prepareRevertPrBranch` (packages/engine/src/task-revert.ts) prepares the dedicated `fusion/revert-` branch without ever mutating the base branch. Existing `{ mode: "git" | "ai", ... }` shapes and the `autoMerge:true` path are unchanged. +- 94e9d15: summary: AI-undo tasks now default to a configurable, stricter review workflow. + category: feature + dev: New project setting `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) selects the workflow for AI-undo board tasks created by `POST /api/tasks/:id/revert` (`mode: "ai"`, the `auto` conflict fallback, and the workspace conflict fallback all share the `createAiUndoResult()` closure, so all three inherit this default). A blank/unset value means the created task inherits the project default workflow (pre-FN-7556 behavior). The route validates the configured id via `getWorkflowDefinition`/`isBuiltinWorkflowId` and falls back to inherit (with a logged warning) on a blank or unknown value, so a misconfigured id never breaks AI-undo task creation. The engine's `createAiUndoTask` helper stays pure — it only forwards a `workflowId` it is given, never resolves the setting itself. The Settings Modal UI field for this setting is a deliberate follow-up task; the value is settable today only via the settings API. +- 3dd227b: summary: Plan auto-approval is now the default; specified tasks skip manual approval unless you opt into workflow/require-all. + category: feature + dev: `DEFAULT_PROJECT_SETTINGS.planApprovalMode` flips `workflow` → `auto-approve-all`; existing projects with an explicit stored value are unchanged; consumed by `resolvePlanApprovalRequired` at the triage gating sites. +- 78d4db9: summary: Fusion self-repo issue-close comments now show current and target release versions. + category: feature + dev: GitHubIssueCommentService appends "Current version: v{current}" and "Target release: v{next-minor}" lines when the linked source issue is runfusion/fusion; other repos unchanged. Version resolved via getCliPackageVersion. +- 7435849: summary: Open one revert PR per sub-repo for workspace tasks when autoMerge is disabled. + category: feature + dev: `POST /api/tasks/:id/revert` gains an additive workspace `{ mode: "pr", clean: true, workspace: { repos: [{ repo, revertBranch, prUrl, prNumber, existingPr? }] } }` result for clean multi-repo reverts under `autoMerge:false`, extending FN-7554's single-repo `mode:"pr"` path. New engine export `prepareWorkspaceRevertPrBranches` (packages/engine/src/task-revert.ts) classifies every sub-repo first and only prepares a dedicated `fusion/revert-` branch per sub-repo when all are clean/already-reverted (all-or-nothing at the branch-prep phase), never force-writing any sub-repo integration branch. The route resolves owner/repo and checks the rate limiter for every sub-repo before pushing/creating any PR, so GitHub-unconfigured/rate-limited cases degrade the whole task to `needsHuman` rather than opening a partial subset of PRs. Existing `{ mode: "git" | "ai" | "pr", ... }` shapes, the `autoMerge:true` workspace path, and FN-7554's single-repo path are unchanged. +- 73b38ba: summary: Add a Settings → General picker to choose the workflow used for AI-undo (revert) tasks. + category: feature + dev: Surfaces `aiUndoTaskWorkflowId` (default `builtin:review-heavy`) in GeneralSection; empty selection means "inherit project default workflow", matching the revert route's blank-is-inherit behavior from FN-7556. +- 42bbe58: summary: Add "Ask user question" and "Exit gate" workflow nodes for mid-flow chat reach-out and early exit. + category: feature + dev: New IR node kinds `ask-user` (reuses await-input park/resume; surfaces the question in the task chat) and `exit-gate` (terminates the workflow early, optional condition). Editor palette + summaries + help updated; `prompt`+`awaitInput` remains a back-compat alias. +- 53fe0d7: summary: Add a built-in "Brainstorming" workflow that talks to you before planning. + category: feature + dev: Registers `builtin:brainstorming` (non-default, default-enabled) composing FN-7579's `ask-user` → refine → `exit-gate`-on-approval phase ahead of the normal coding plan/execute/review/merge spine. Parity suite (`builtin-workflows.test.ts`) extended for the new entry. +- ecbbb29: summary: Add a "Coding (Ideas)" workflow with a manual Ideas intake and a merged Todo planner column. + category: feature + dev: New `builtin:coding-ideas` clones the default stepwise pipeline with an `ideas` intake (autoTriage:false) in front of a merged `todo` planner+capacity column. createTask lands cards in the workflow's intake column; the triage service plans unplanned todo tasks in place; the scheduler skips bootstrap-prompt todo tasks; TaskCard gains a Start button and a Ready badge. + +#### Patch Changes + +- 8668a05: summary: Add a workflow setting to disable automatic large-task triage splitting. + category: feature + dev: Adds triageProactiveSubtaskSplittingEnabled while preserving explicit breakIntoSubtasks requests. +- 978cdda: summary: Show active Plan Review progress on triage task cards. + category: fix + dev: TaskCard now renders the existing progress affordance for Triage only when unified progress has active workflow work. +- 635fca2: summary: Remove the eye icon markdown/plain toggle from chat; messages always render as Markdown. + category: breaking + dev: Removed ChatView `chat-thread-header-render-toggle` (desktop + mobile), `showAllAsPlain` state, and `chat.showRenderedMarkdown`/`chat.showPlainText` i18n keys (FN-7541). +- 3d55102: summary: Clarify task-detail oversight Nudge/Explain controls: visible label, disabled reason, always-openable Explain panel. + category: fix + dev: TaskDetailModal now renders a `detail-oversight-controls-label` group label and `detail-overseer-nudge-disabled-reason` helper text (both gated by the existing oversight-cluster visibility condition); Explain no longer disables on `!canExplainOverseer` since it is read-only. Nudge's `canNudgeOverseer` gate and Stop's confirm dialog are unchanged. +- 0f1cd0a: summary: Unify border, radius, and height of the task-detail Priority/Execution/Oversight controls. + category: fix + dev: Adds a shared --detail-control-border-radius token alongside --detail-priority-control-min-height so .detail-priority-chip, .detail-execution-mode-toggle, .detail-oversight-chip, and .detail-oversight-menu-trigger all resolve the same border-width/color/radius/height. +- b42ba9f: summary: Keep the task-detail Activity view menu open during mobile iOS taps. + category: fix + dev: Guards the Activity views dropdown against iOS visualViewport resize/scroll echoes during menu opening. +- a7559b0: summary: Fix Anthropic subscription login when pasted callback URLs contain fragment OAuth parameters. + category: fix + dev: Normalizes pasted OAuth callback fragments before resolving dashboard manual-code login prompts. +- 4b530a6: summary: Fix Claude/Anthropic subscription re-login showing "Login did not complete" after logging out. + category: fix + dev: Anthropic subscription OAuth is aliased across the legacy `anthropic` row (where interactive login persists the credential) and the `anthropic-subscription` id (where the settings card's in-memory logged-out suppression and status read are keyed). Re-login wrote only `anthropic`, so `loggedOutProviders` kept suppressing `anthropic-subscription` and the card reported failure despite a valid stored credential until process restart. auth-storage's proxy now clears the logged-out state on both aliases when either is re-authenticated (new `login` trap + hardened `set` trap via `clearReauthenticatedLogoutState`; raw api_key writes stay scoped to their own card). Also surfaces background OAuth login failures on `GET /auth/status` (`loginError`) + server logs so future paste-callback failures are diagnosable instead of a generic error. +- a5ac3c3: summary: Stop self-healing from killing actively-running tasks after ~30 minutes. + category: fix + dev: FN-7566. isPhantomExecutorBinding's liveness gate (heartbeat/checkout/runAudit) was blind to ephemeral executor agents, leaving only the age>graceMs\*3 threshold, so any ephemeral-executor task running longer than ~30 min was reclaimed to `todo` mid-flight. Adds the in-process live-session veto (activeSessionRegistry path / executingTaskLock / isTaskActive), mirroring the isWorkspaceTaskLive/sessionDead predicate, and honors clearPhantomExecutorBinding's live-session refusal in reclaimSelfOwnedBranchConflicts. +- 8912399: summary: Stop Windows Terminal version dialogs from popping up when opening the dashboard or Settings on Windows. + category: fix + dev: Root cause was the worktrunk integration, not the embedded terminal: worktrunk's CLI is named `wt`, which collides with Windows Terminal (`wt.exe`) on PATH, so probing it with `wt --version` launched Windows Terminal. Fixed by (1) `useWorktrunkInstallStatus` only auto-fetching `/api/worktrunk/status` when the integration is enabled (user opt-in) instead of on every Settings/dashboard mount, and (2) an engine-level guard in `probeWorktrunk` that refuses to exec a resolved `wt` that is the Windows Terminal alias (under `WindowsApps` / a `WindowsTerminal` package dir), covering all resolution surfaces. +- b800f7d: summary: Select newly created folders automatically during project setup. + category: fix + dev: Adds DirectoryPicker opt-in selection for project-registration surfaces while preserving default picker behavior. +- 9dc248e: summary: Prevent Desktop update banners from using 0.0.0 as the current version. + category: fix + dev: Dashboard update checks now resolve packaged @fusion/desktop metadata and fail closed for unresolved versions. +- 52dbc0e: summary: Quit Fusion Desktop on Windows when the window is closed. + category: fix + dev: Updates Electron close lifecycle so Windows shutdown reaches embedded runtime cleanup. +- ced783e: summary: Open desktop Anthropic Subscription OAuth logins in the system browser. + category: fix + dev: Adds Electron window-open policy coverage and preserves Settings auth polling completion paths. +- 50786f2: summary: Delay GitHub setup warnings for one day and add a dashboard connect action. + category: fix + dev: Dashboard setup warnings now gate GitHub prompts per project and route the CTA to Settings → Authentication. +- b4b1f6d: summary: Fix a false AI engine not running banner in desktop mode. + category: fix + dev: Distinguishes transient embedded desktop engine startup from true dashboard-only mode. +- e8b7362: summary: Clarify the desktop Connection Manager add-remote flow. + category: fix + dev: Desktop Connection Manager now separates Local Server context from saved remote profiles and collapses the remote editor until add/edit. +- 0900a38: summary: Restore Local Server in the desktop Switch server list. + category: fix + dev: Desktop Connection Manager now lists local and saved remote destinations together. +- a2b09f2: summary: Make right-dock task list clicks respect the task popup setting. + category: fix + dev: Threads openMobileTasksInPopup through the right-dock Tasks list route while preserving embedded dock detail when disabled. +- 0f05156: summary: Auto-retry retryable Code Review remediation failures. + category: fix + dev: Prevents retryable code-review-remediation graph failures from stranding tasks in in-review. +- 20184ac: summary: Fix no-op task branch recovery after a previously landed task. + category: fix + dev: Merge/recovery ownership classification now checks no-diff branches before foreign trailer rejection. +- 82493e0: summary: Allow documented source-free task-artifact deliveries to finish without commits. + category: fix + dev: fn_task_done now recognizes explicit gitignored .fusion/tasks artifact contracts while preserving source-change no-commit guards. +- 5689346: summary: Fix direct merges so Push to remote after merge honors the configured remote and branch. + category: fix + dev: Resolves remote-only push targets from the merge integration branch and preserves non-fatal push errors on done tasks. +- b42be87: summary: Keep task popups on the board layer with Activity menus above them. + category: fix + dev: Task-detail FloatingWindow callers use a lower layer band, and Activity view menus reposition after popup geometry changes. +- 61c8bdc: summary: Keep accepted chat requests waiting instead of showing false first-event timeout failures. + category: fix + dev: Dashboard chat POST streams no longer abort accepted-but-silent responses on the client first-event timer. +- e8dc2ae: summary: Show each task's original prompt in the Plan tab alongside the generated plan. + category: fix + dev: Adds a read-only Task Detail original-prompt section backed by task.description. +- d2e3134: summary: Add before-to-after transformation summaries to generated task definitions. + category: feature + dev: Built-in standard and fast triage prompts now require a `## Before → After Transformation` section. +- b0208c1: summary: Restore terminal Ctrl/Cmd copy and paste shortcuts. + category: fix + dev: Integrated and embedded terminals now own physical clipboard paste to avoid swallowed or duplicate input. +- 2797803: summary: Show first-token and tool processing durations in task agent logs. + category: feature + dev: Adds optional agent-log timing fields `timeToFirstTokenMs` and `durationMs`. +- a2d6349: summary: Fix mobile Chat composer being hidden behind the keyboard accessory bar. + category: fix + dev: Adds keyboard-open bottom clearance in ChatView so the composer clears the iOS input-assistant/autofill bar without a persistent .chat-thread transform or Android reserved-gap. +- 4baa4c4: summary: Settings descriptions now show each setting's default value. + category: feature + dev: Appended default-value copy to settings.\* i18n descriptions across Global, Runtimes, and Project Settings sections, sourced from DEFAULT_GLOBAL_SETTINGS/DEFAULT_PROJECT_SETTINGS in settings-schema.ts; added settings-default-descriptions.test.tsx guarding that every surfaced setting states a default (or explicit "inherits"/"no default \u2014 unset") and that every DEFAULT_SETTINGS key is documented or allowlisted as not surfaced. +- 53d7b7e: summary: Add an intelligent git-revert engine service and POST /api/tasks/:id/revert route. + category: feature + dev: New `packages/engine/src/task-revert.ts` exports `resolveTaskRevertCommits`, `classifyTaskRevert`, and `performTaskRevert` (squash/rebase/lineage attribution precedence, dry-run classification, guaranteed-clean rollback). Route enforces done/archived-only and autoMerge-off guard rails; conflicting results are returned unresolved for sibling FN-7524 (AI-undo) to act on. Workspace tasks return `unsupported`. +- 4707eb5: summary: Auto-approve now reliably sends specified plans to the board without a manual approval stop. + category: fix + dev: FN-7526 — investigated the reported "plans still park at awaiting-approval when auto-approve is on" symptom; resolvePlanApprovalRequired, mergeEffectiveSettings/applyWorkflowSettingsOverlay, and every finalizeApprovedTask call site (specifyTask, recoverApprovedTask, retryUnavailablePlanReview, tryFinalizeExplicitDuplicateMarker) already honored project planApprovalMode: "auto-approve-all" over a stored workflow requirePlanApproval value — no production defect reproduced. Added end-to-end regression coverage across every enumerated surface (Plan Review reviewer-outage retry, refinement routing, self-healing starved-refinement recovery) using the real mergeEffectiveSettings pipeline instead of isolated bare-settings unit calls, plus explicit assertions that the independent release-authorization and Workflow Plan Review gates remain intact under auto-approve-all, so a future bare-settings call site is caught immediately instead of silently reintroducing the reported behavior. +- 3b52a4d: summary: Fix the in-dashboard Switch server menu not switching desktop local/remote. + category: fix + dev: The desktop shell's redirect effects in App.tsx read a dead `localServer` field that the preload never populates; extracted `resolveDesktopShellRedirectTarget` in appLifecycle.ts now derives the navigation target from the live `localRuntime`/`activeProfileId` state for both directions, and the unused `localServer` field was removed from `ShellConnectionState`. +- 36bd74e: summary: Fix branch group completion checklists to show accurate landed/finished counts. + category: fix + dev: runAiMerge (the sole merge path since master-plan U0) never resolved branch-group routing or stamped mergeDetails.mergeTargetBranch/mergeTargetSource, so isBranchGroupMemberLanded permanently reported shared-group members as not landed. Routes through resolveBranchGroupMergeRouting (matching the legacy merger.ts pattern) and stamps the target fields on both the landed and no-op finalize paths; preserves merge-target-safety in isBranchGroupMemberLanded (a sibling/mismatched-branch member still never counts as landed). +- df0be88: summary: Branch groups no longer report complete (or become promotable) when an unlanded member is archived. + category: fix + dev: listTasksByBranchGroup membership now scans with includeArchived:true so an archived-but-unlanded member stays counted in total instead of silently dropping out; mergeDetails is now persisted on ArchivedTaskEntry so an archived member that had already landed keeps counting as landed. evaluateBranchGroupCompletion / promoteBranchGroup gate correctly; merge-target-safety in isBranchGroupMemberLanded is unchanged. +- ec9ac61: summary: Fix the global GitLab integration setting not persisting when saved. + category: fix + dev: splitSettingsSave now diffs the five global GitLab keys (gitlabEnabled, gitlabInstanceUrl, gitlabApiBaseUrl, gitlabAuthToken, gitlabAuthTokenType) against scoped global initials only, never the project-effective merged initialValues, so a project override no longer suppresses a real global save. +- 8d36b99: summary: Fix task-detail Activity view dropdown not opening reliably on mobile. + category: fix + dev: Guards the Activity menu's window resize/orientationchange/scroll close-listener with the same opening-tap timing guard already used for visualViewport, and exempts scroll events originating in the `.detail-tabs` scroller, so a same-gesture mobile tap echo (Android/iOS, fixed modal or `.floating-window--task-detail` popup) no longer closes the menu the instant it opens. +- ad744aa: summary: Manual "Run now" for the Database Backup automation now runs in-process like the scheduler, matching cron behavior. + category: fix + dev: The legacy single-command and command-step manual automation run path (`executeSingleCommand` in packages/dashboard/src/routes.ts) now intercepts `isInProcessBackupCommand`/`isInProcessMemoryBackupCommand` via the scoped TaskStore, mirroring `RoutineRunner.executeCommand`/`CronRunner`, instead of always shelling out via `exec()`. `formatInProcessBackupError`, `isInProcessBackupCommand`, and `isInProcessMemoryBackupCommand` are now exported from `@fusion/engine` for reuse. Existing onStep/onText live-run callbacks already stream incremental output for command/backup runs; added regression coverage confirming this holds for the new interception branch. +- 5c3d58a: summary: Task cards no longer show the "Auto-recovery" oversight badge unless oversight is explicitly configured. + category: fix + dev: `TaskCard.tsx`'s `showOversightBadge` gate now also suppresses the badge when the effective level equals `DEFAULT_PLANNER_OVERSIGHT_LEVEL` ("autonomous") and there is no explicit per-task `plannerOversightLevel` override; an explicit per-task override of "autonomous" still renders the badge. +- b4be515: summary: Remove the per-card overseer-state ("Executor") badge from task cards. + category: fix + dev: Deleted the FN-7516 `card-overseer-state-badge` render, its card-local `deriveOverseerCardWatchedStage` helper/label maps, and its CSS; the sibling oversight-level badge (`card-oversight-badge`) is unaffected. +- 62ddb19: summary: Original task prompt now renders as Markdown and is collapsed by default in the task Plan tab. + category: feature + dev: Task Detail Plan/Definition tab original-prompt section reuses the existing `.detail-source-toggle`/`.detail-source-chevron--expanded` collapse pattern and the shared `ReactMarkdown` pipeline (`remarkGfm`, `sharedRehypePlugins`, `markdownLinkifyComponents`); backed by read-only `task.description`, no change to the generated `PROMPT.md` editor/revision flow. +- 883c73e: summary: Fix agent-created artifacts not appearing live in the dashboard artifacts view. + category: fix + dev: Root cause was cross-instance artifact-registration replication, not the route/hook/render path (all already correct). `TaskStore.registerArtifact()` never bumped `lastModified`, and `checkForChanges()` (the polling replicator that lets a second TaskStore instance on the same project — e.g. the dashboard's cached store vs. the engine's own store — mirror events it did not write itself) only ever diffed the `tasks` table, never `artifacts`. A store instance that did not perform the write could therefore never observe or re-emit `artifact:registered`, leaving an already-open Documents/task Artifacts gallery stale until a full reload. Fixed by bumping `lastModified` on artifact writes and adding a strictly-increasing `rowid`-cursor poll over the `artifacts` table in `checkForChanges()`. See `packages/core/src/__tests__/artifacts.test.ts` and `packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts` for regression coverage. +- d09b57f: summary: Fix the mobile terminal shortcut bar so it scrolls horizontally to reach every key. + category: fix + dev: Added `min-width: 0` to `.terminal-shortcut-panel` to defeat the flex min-width:auto trap that clipped overflow instead of engaging `overflow-x: auto`. +- 3d58260: summary: Planner-oversight intervention timeline now populates from real engine activity. + category: fix + dev: Wires PlannerOverseerMonitor/PlannerRecoveryController decision points to the FN-7520 emitOverseer\* façade with the real TaskStore; observation/escalation emission deduped per (task, stage[, signal]). +- 052a277: summary: Show the "Global" prefix on the Authentication entry in the mobile Settings picker. + category: fix + dev: resolveSettingsSectionOptionLabel now derives the Global-group prefix for storage-less (scope: undefined) sections in SettingsModal.tsx (FN-7552). +- 6e4c207: summary: Tasks held for release authorization or Plan Review are now shown distinctly, so auto-approve no longer looks broken. + category: fix + dev: FN-7559 — auto-approve-all bypasses only the manual plan-approval gate (unchanged, FN-7526). Release-authorization holds are surfaced with a new distinct status reason (`Task.awaitingApprovalReason: "release-authorization"`) and no longer render the generic manual Approve/Reject affordance in TaskCard/TaskDetailModal; Workflow Plan Review already used distinct statuses (`needs-replan`/`plan-review-unavailable`) and is unaffected. Both gates remain independent and intact — this is UI/data disambiguation only. +- 6d364fc: summary: Move mobile terminal controls into a bottom footer so they no longer crowd the header, with a scrollable shortcut bar. + category: fix + dev: On the ≤768px terminal, the `.terminal-actions` cluster now renders in a `terminal-footer-actions` bar (with `min-width:0; overflow-x:auto`) instead of the header; desktop/floating/pinned-below keep the FN-7502 header layout. Preserves the FN-7550 shortcut-panel scroll fix. +- b471aec: summary: Stop the release-authorization gate from holding tasks that merely disclaim releasing. + category: fix + dev: classifyReleaseTask now strips negated release-disclaimer clauses (e.g. "this task performs no release/publish; releases are owned by scripts/release.mjs") before signal matching in packages/engine/src/triage-release-authorization.ts, so revert/undo/UI specs are no longer false-flagged as release-class. Genuine "run pnpm release"/"publish @runfusion/fusion" intent still trips the gate. +- 9d4a45b: summary: Fix mobile terminal text still rendering with excess inter-character gaps after font-load settle. + category: fix + dev: Root cause: xterm's OptionsService setter is a no-op when reassigning an already-current fontFamily/fontSize, so post-settle reapply never forced CharSizeService/DomRenderer to remeasure. Added `forceTerminalFontRemeasure()` in `terminalPreferences.ts`, used by both `TerminalModal.tsx` and `SessionTerminal.tsx` at every post-`waitForTerminalFontMetrics()` settle site. +- 72b77bf: summary: Stop Plan Review from looping tasks forever and fix its "can't find the plan" reviews. + category: fix + dev: FN-7561 — Plan Review pre-merge gate hardening in packages/engine/src/executor.ts. (1) The reviewer ran readonly with cwd=worktree but the spec lives at project-root .fusion/tasks//PROMPT.md, so "Read PROMPT.md" produced "no PROMPT.md found / data is in a DB" non-verdicts; the spec text is now injected into the reviewer prompt via readTaskArtifact. (2) A malformed reviewer response now self-retries once on the primary model when no fallback is configured. (3) A malformed (advisory_failure, no verdict) plan-review result can never trigger a triage replan. (4) The unbounded plan-review replan default is capped at 15 attempts with a loud halting log entry, so a persistently-disagreeing planner/reviewer no longer burns LLM calls indefinitely (FN-7525 ran 13+ attempts overnight). +- c08498e: summary: Planner-overseer task badge now shows a readable label and explains what it is waiting on. + category: fix + dev: TaskCard badge renders plannerOverseerStateLabel + plannerOverseerBadgeTooltip built from the existing PlannerOverseerRuntimeSnapshot (reason/watchedStage/signal/pendingConfirmation); presentation-only, no engine changes. +- 24b27e8: summary: Plan approve/reject API now blocks release-authorization holds, requiring the authorization marker first. + category: fix + dev: FN-7564 — POST /tasks/:id/approve-plan and /reject-plan now return 400 when task.awaitingApprovalReason === "release-authorization" (FN-7559 discriminator), enforcing the FN-6481 release-authorization gate at the API layer regardless of client. Manual-approval holds are unaffected. +- fb45157: summary: Pin the mobile terminal close (X) button to the top-right corner so it is easy to find and tap. + category: fix + dev: On the ≤768px terminal, the `terminal-close` button now carries a `terminal-close--corner` class (order:3 + margin-inline-start:auto) so it renders last in flex order and hugs the right edge next to the tab dropdown, instead of falling back to order:0 (far left). Desktop/floating/pinned-below placement inside `.terminal-actions` is unchanged. +- 7c0be53: summary: Fix mobile terminal excess character spacing that survived earlier font-remeasure fixes. + category: fix + dev: `TerminalModal`/`SessionTerminal` re-bake xterm's `DomRenderer` letter-spacing compensation AFTER `fitAddon.fit()` settles the post-fit column count (not just before it), since `handleResize()` never re-bakes spacing itself. See `docs/solutions/ui-bugs/xterm-options-noop-remeasure-after-font-settle.md` recurrence #4. +- 71dfd3a: summary: Rename downloadable CLI release binaries to the fn-cli- base name. + category: internal + dev: `binaryNameForTarget` in `packages/cli/build.ts` and the `release.yml` / `test-release.yml` matrices now emit `fn-cli-`; the local dev binary stays `fn`/`fn.exe`. +- 9592e3a: summary: Manual plan approval no longer re-asks you to approve a plan you already approved when it hasn't changed. + category: fix + dev: FN-7569 — approving a plan records a fingerprint of the approved PROMPT.md (new nullable Task.approvedPlanFingerprint, migration 139). The manual plan-approval gate skips re-parking at awaiting-approval when a re-specification (replan, plan-review retry, self-healing rebound) produces the same plan; a changed plan or reject-plan still requires fresh approval. Release authorization, Workflow Plan Review, and auto-approve-all are unchanged. +- c31f9ef: summary: Move the planner intervention timeline into the task Activity view dropdown. + category: feature + dev: Removes the inline `PlannerInterventionTimeline` mount from the FN-7517 oversight cluster in `TaskDetailModal.tsx` and adds a fourth `interventions` `ActivitySegment`, shown in the Activity dropdown only when planner oversight is active for the task; falls back to Live if oversight turns off while Interventions is selected. +- ce9df29: summary: Expired Claude subscription logins now show disconnected with a re-login prompt; tokens auto-refresh before expiry. + category: fix + dev: Unifies OAuth expiry detection between OAuthExpiryMonitor and /api/auth/status, and adds an engine-side proactive OAuth refresh scheduler wired in project-engine (guarded by skipNotifier). No token material logged. +- 196abb5: summary: Anthropic subscription reads now refresh the OAuth token automatically instead of silently failing when expired. + category: fix + dev: mergeAuthStorageReads getApiKey("anthropic-subscription") now delegates to the underlying engine authStorage.getApiKey (the only refresh-token HTTP round trip) instead of a local static expiry check; regression tests drive the wrapper directly. No token material logged. +- 45e5a26: summary: Stop GitHub tracking-issue creation from linking new tasks to old/closed issues. + category: fix + dev: github-tracking dedup now only reuses OPEN issues and requires a File-Scope path overlap (keyword-only matches no longer link). Prevents mis-linking a fresh task to a stale/resolved tracking issue (FN-7579). Setting `githubTrackingDedupEnabled` unchanged. +- a1a6b09: summary: Clarify the oversight "Nudge unavailable" guideline so it no longer reads as an overseer fault. + category: fix + dev: TaskDetailModal oversight controls — reworded taskDetail.oversight.nudgeDisabledTitle and added taskDetail.oversight.nudgeSuppressedTitle to differentiate periodic-observation vs. manual-control states. No enablement/engine logic changed. +- cf3fe8b: summary: New tasks created under the Coding (Ideas) workflow now land in the Ideas column and wait for you to promote them. + category: fix + dev: Dashboard create surfaces (InlineCreateCard, QuickEntryBox, NewTaskModal, insight/todo → task) no longer hard-code column:"triage"; the store now resolves the selected/default workflow's intake column. InlineCreateCard forwards workflowId at create time instead of applying it post-create. Also fixed a glue-layer regression in `useTaskHandlers.ts` (`handleBoardQuickCreate`/`handleModalCreate`) that re-forced column:"triage" even after the UI surfaces stopped sending it. +- 8b4e522: summary: Fix tasks vanishing from the board after being added to a workflow like Coding (Ideas). + category: fix + dev: Board.tsx forces a board-workflows refetch (deferred one tick, signature-guarded) whenever a rendered task is missing from the taskWorkflowIds map, so its real workflow and intake column resolve regardless of which create surface added it; the single-workflow grouping also re-homes a task whose column its workflow no longer declares into the intake lane instead of dropping it. Fixes the FN-7591 regression where intake-column cards (column "ideas") fell back to the default workflow, which has no such column, and were filtered out until a manual reload. +- f30d55f: summary: Move the Before → After transformation summary to the top of generated task definitions. + category: fix + dev: Reorders the standard and fast triage `PROMPT.md` templates in packages/core/src/agent-prompts.ts so `## Before → After Transformation` is the first content section, ahead of `## Review Level` and `## Mission`, matching FN-7499's glance-verification intent. +- 20379e8: summary: Task-detail Priority dropdown now matches the Oversight dropdown's size, border, and typography. + category: fix + dev: Removed the Priority-only forced select/option uppercase, added a neutral chip background scoped to `.detail-priority-chip.card-priority-badge--normal` for the untinted `normal` level, and reused the FN-7585 shared `--btn-border-width`/`--border`/`--detail-control-border-radius`/`--detail-priority-control-min-height` tokens so both dropdowns render as one control style across desktop and the mobile oversight-overflow surface. +- e0f3d3d: summary: Default workflow boards now label the intake column "Planning" instead of "Triage". + category: fix + dev: Renamed the `name` of the `id: "triage"` intake column to "Planning" in builtin-coding, builtin-stepwise-coding, and builtin-pr workflow IRs (column id unchanged; linear built-ins inherit via canonicalBuiltinWorkflowColumns). COLUMN_LABELS.triage was already "Planning". +- 5b193d2: summary: Fix the task-detail Nudge control staying disabled when the overseer is actively watching. + category: fix + dev: GET /api/tasks/:id now attaches the transient plannerOverseerState snapshot (mirrors the list route); TaskDetailModal reads the snapshot from workingTask so detail refetches no longer drop it. +- 546ef16: summary: Honor mission branchStrategy when triage omits branchAssignment; skip validation for inactive missions. + category: fix + dev: resolveBranchAssignmentContext returns undefined for absent mode so triage falls back to mission.branchStrategy; processTaskOutcome gates on mission.status === "active" like recoverActiveMissions. +- b173f76: summary: Planner overseer no longer marks healthy in-progress tasks as "recovering" or steers them. + category: fix + dev: `decidePlannerRecovery` now returns `none` for healthy (`progressing`/`complete`) and `awaiting-human` executor/workflow-gate signals instead of falling through to `inject_guidance`; only `stuck`/`blocked`/`failed` trigger autonomous steering. Also dedupes the `PlannerOverseerMonitor` activity-feed heartbeat so an unchanged `(stage, signal, reason)` observation is logged once per change, not every poll tick. Fixes the "overseer recovering" badge appearing on every autonomous card and the needless AI-consuming guidance injections (FN-7577). + +### runfusion.ai + +#### Patch Changes + +- Updated dependencies [8668a05] +- Updated dependencies [978cdda] +- Updated dependencies [635fca2] +- Updated dependencies [3d55102] +- Updated dependencies [0f1cd0a] +- Updated dependencies [b42ba9f] +- Updated dependencies [a7559b0] +- Updated dependencies [4b530a6] +- Updated dependencies [a5ac3c3] +- Updated dependencies [8912399] +- Updated dependencies [d16c8b4] +- Updated dependencies [b800f7d] +- Updated dependencies [315f3bc] +- Updated dependencies [9dc248e] +- Updated dependencies [52dbc0e] +- Updated dependencies [ced783e] +- Updated dependencies [50cdab1] +- Updated dependencies [50786f2] +- Updated dependencies [b4b1f6d] +- Updated dependencies [e8b7362] +- Updated dependencies [0900a38] +- Updated dependencies [a2b09f2] +- Updated dependencies [0f05156] +- Updated dependencies [20184ac] +- Updated dependencies [82493e0] +- Updated dependencies [5689346] +- Updated dependencies [b42be87] +- Updated dependencies [2f23d22] +- Updated dependencies [efa8105] +- Updated dependencies [61c8bdc] +- Updated dependencies [e8dc2ae] +- Updated dependencies [d2e3134] +- Updated dependencies [b0208c1] +- Updated dependencies [7d8a1b8] +- Updated dependencies [2797803] +- Updated dependencies [a2d6349] +- Updated dependencies [4baa4c4] +- Updated dependencies [87a700c] +- Updated dependencies [68f5153] +- Updated dependencies [aa757bc] +- Updated dependencies [0689250] +- Updated dependencies [12a6d1b] +- Updated dependencies [81f2053] +- Updated dependencies [2cc84b5] +- Updated dependencies [79ab367] +- Updated dependencies [c16cc9e] +- Updated dependencies [aae603b] +- Updated dependencies [d10ea9a] +- Updated dependencies [bf68839] +- Updated dependencies [53d7b7e] +- Updated dependencies [c4d81fe] +- Updated dependencies [e7cb2f1] +- Updated dependencies [4707eb5] +- Updated dependencies [3b52a4d] +- Updated dependencies [5ad8ec8] +- Updated dependencies [726cbf8] +- Updated dependencies [36bd74e] +- Updated dependencies [df0be88] +- Updated dependencies [ec9ac61] +- Updated dependencies [8d36b99] +- Updated dependencies [ad744aa] +- Updated dependencies [5c3d58a] +- Updated dependencies [b4be515] +- Updated dependencies [62ddb19] +- Updated dependencies [883c73e] +- Updated dependencies [2ed06f9] +- Updated dependencies [8c6f76c] +- Updated dependencies [d09b57f] +- Updated dependencies [3d58260] +- Updated dependencies [052a277] +- Updated dependencies [f992e6a] +- Updated dependencies [2df6c35] +- Updated dependencies [94e9d15] +- Updated dependencies [3dd227b] +- Updated dependencies [6e4c207] +- Updated dependencies [6d364fc] +- Updated dependencies [b471aec] +- Updated dependencies [9d4a45b] +- Updated dependencies [72b77bf] +- Updated dependencies [c08498e] +- Updated dependencies [24b27e8] +- Updated dependencies [fb45157] +- Updated dependencies [7c0be53] +- Updated dependencies [71dfd3a] +- Updated dependencies [9592e3a] +- Updated dependencies [c31f9ef] +- Updated dependencies [ce9df29] +- Updated dependencies [78d4db9] +- Updated dependencies [196abb5] +- Updated dependencies [7435849] +- Updated dependencies [73b38ba] +- Updated dependencies [42bbe58] +- Updated dependencies [45e5a26] +- Updated dependencies [a1a6b09] +- Updated dependencies [53fe0d7] +- Updated dependencies [cf3fe8b] +- Updated dependencies [8b4e522] +- Updated dependencies [f30d55f] +- Updated dependencies [20379e8] +- Updated dependencies [e0f3d3d] +- Updated dependencies [5b193d2] +- Updated dependencies [ecbbb29] +- Updated dependencies [546ef16] +- Updated dependencies [b173f76] + - @runfusion/fusion@0.56.0 ## 0.55.0 @@ -12427,6 +12789,14 @@ for reference. - Updated dependencies [a2ed6d0] - @runfusion/fusion@0.1.0 +## 0.39.21 + +### @fusion/i18n + +#### Patch Changes + +- @fusion/core@0.56.1 + ## 0.39.20 ### @fusion/i18n @@ -12589,6 +12959,14 @@ for reference. - @fusion/core@0.40.0 +## 0.11.47 + +### @fusion/droid-cli + +#### Patch Changes + +- @fusion-plugin-examples/droid-runtime@0.1.47 + ## 0.11.46 ### @fusion/droid-cli diff --git a/package.json b/package.json index dd01cdf9c9..6873ec1ab3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fusion-workspace", - "version": "0.56.0", + "version": "0.56.1", "private": true, "license": "MIT", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/cli-alias/CHANGELOG.md b/packages/cli-alias/CHANGELOG.md index cbcb5126d8..6d000e9c3c 100644 --- a/packages/cli-alias/CHANGELOG.md +++ b/packages/cli-alias/CHANGELOG.md @@ -1,5 +1,16 @@ # runfusion.ai +## 0.56.1 + +### Patch Changes + +- Updated dependencies [ed823c7] +- Updated dependencies [dc44730] +- Updated dependencies [b9d60b3] +- Updated dependencies [e347062] +- Updated dependencies [f4f1656] + - @runfusion/fusion@0.56.1 + ## 0.56.0 ### Patch Changes diff --git a/packages/cli-alias/package.json b/packages/cli-alias/package.json index 214ec777c8..f9e5024570 100644 --- a/packages/cli-alias/package.json +++ b/packages/cli-alias/package.json @@ -1,6 +1,6 @@ { "name": "runfusion.ai", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Launch Fusion with `npx runfusion.ai` — tiny alias for @runfusion/fusion.", "homepage": "https://runfusion.ai", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7f2d684887..94f2909e77 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,25 @@ # @runfusion/fusion +## 0.56.1 + +### Patch Changes + +- ed823c7: summary: Fix Anthropic subscription showing "logged in" while all model calls fail. + category: fix + dev: Two-part fix. (1) OAuth token refresh in `packages/engine/src/auth-storage.ts` sent a `scope` param (defaulting to `user:profile`), which per RFC 6749 §6 re-issued the access token narrowed to that scope and stripped `user:inference` — so refreshed tokens 403'd on every model call. Refresh now omits `scope` (preserving the originally-granted scopes, matching pi-ai's own refresh), and `ANTHROPIC_DEFAULT_SCOPES` mirrors the full Claude Code scope set. (2) `/auth/status` now reports an unexpired Anthropic OAuth token that lacks an inference scope as not-connected (authenticated:false, expired:true so the re-login banner fires) with a scope-specific loginError, instead of falsely claiming a live session. Existing narrowed tokens need one re-login to obtain a fresh broad grant. +- dc44730: summary: Fix "Invalid transition" error when moving cards out of a custom workflow column like Coding (Ideas) → Ideas. + category: fix + dev: moveTaskInternal's compat-flag legacy path validated moves against the legacy VALID_TRANSITIONS table, which is keyed only by the built-in column ids; a task in a non-legacy workflow column (e.g. "ideas") had no key and every move was rejected. The legacy branch now resolves a non-legacy source column's targets from the task's own workflow adjacency (resolveAllowedColumns) while preserving the legacy bare-Error contract for legacy columns. +- b9d60b3: summary: Fix overlapping Record and Clear buttons in the Keyboard Shortcuts settings rows on desktop and mobile. + category: fix + dev: The shortcut-capture Record/Clear buttons no longer use the icon-only `btn-icon` class (which set `line-height:0` and a mobile 36px square, clipping/overlapping the text labels); they use a text-button class and the `.shortcut-capture` row locks buttons with `flex-shrink:0` so the input and controls never overlap, stacking cleanly on mobile. +- e347062: summary: Fix persistent mobile terminal inter-character spacing (5th recurrence root cause). + category: fix + dev: xterm's CharSizeService picks a Canvas-based (OffscreenCanvas) or DOM-based character-measurement strategy at terminal.open() time; DomRenderer's letter-spacing bake always measures via a separate DOM-based WidthCache, so a Canvas-vs-DOM measurement mismatch survived FN-7561/FN-7567's remeasure-ordering fixes. `withDomBasedTerminalCharacterMeasurement` in terminalPreferences.ts forces CharSizeService onto the same DOM strategy for both TerminalModal and SessionTerminal. +- f4f1656: summary: Fix manual PR actions hidden when a task auto-merge override was on but global auto-merge was off. + category: fix + dev: TaskDetailModal isManualPrFlow now keys off live global autoMergeEnabled, not the per-task effective override (regression from FN-7255). + ## 0.56.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index fd3058f086..f8a2286eb3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@runfusion/fusion", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Fusion CLI: HTTP API server, daemon, dashboard launcher, and task tooling for the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 9dd821dbb1..c8d6eef7fc 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,7 @@ # @fusion/core +## 0.56.1 + ## 0.56.0 ## 0.55.0 diff --git a/packages/core/package.json b/packages/core/package.json index 231c17ef04..c6ad2fd87d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/core", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Fusion core: task store, scheduler, settings, and shared domain types backing the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/dashboard/CHANGELOG.md b/packages/dashboard/CHANGELOG.md index 2c24c505ba..7d9e93287d 100644 --- a/packages/dashboard/CHANGELOG.md +++ b/packages/dashboard/CHANGELOG.md @@ -1,5 +1,22 @@ # @fusion/dashboard +## 0.56.1 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/engine@0.56.1 +- @fusion/i18n@0.39.21 +- @fusion-plugin-examples/cli-printing-press@0.1.38 +- @fusion-plugin-examples/compound-engineering@0.1.21 +- @fusion-plugin-examples/dependency-graph@0.1.52 +- @fusion-plugin-examples/roadmap@0.1.40 +- @fusion-plugin-examples/cursor-runtime@0.1.40 +- @fusion-plugin-examples/droid-runtime@0.1.47 +- @fusion-plugin-examples/hermes-runtime@0.2.71 +- @fusion-plugin-examples/openclaw-runtime@0.2.71 +- @fusion-plugin-examples/paperclip-runtime@0.2.71 + ## 0.56.0 ### Patch Changes diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index fd257dd688..b11d2f8ad9 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/dashboard", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Fusion dashboard: React UI and HTTP API server for monitoring and controlling the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index 9cdc43a1f9..8aba45e5e5 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -1,5 +1,13 @@ # @fusion/desktop +## 0.56.1 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/dashboard@0.56.1 +- @fusion/engine@0.56.1 + ## 0.56.0 ### Patch Changes diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 3ada770992..db9058640c 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@fusion/desktop", "productName": "Fusion", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "author": { "name": "Runfusion", diff --git a/packages/droid-cli/CHANGELOG.md b/packages/droid-cli/CHANGELOG.md index 76ed864e1f..135712e5b7 100644 --- a/packages/droid-cli/CHANGELOG.md +++ b/packages/droid-cli/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion/droid-cli +## 0.11.47 + +### Patch Changes + +- @fusion-plugin-examples/droid-runtime@0.1.47 + ## 0.11.46 ### Patch Changes diff --git a/packages/droid-cli/package.json b/packages/droid-cli/package.json index b4f78e77ce..f925155ebd 100644 --- a/packages/droid-cli/package.json +++ b/packages/droid-cli/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/droid-cli", - "version": "0.11.46", + "version": "0.11.47", "description": "First-party Fusion pi extension that routes LLM calls through the Droid CLI subprocess.", "license": "MIT", "private": true, diff --git a/packages/engine/CHANGELOG.md b/packages/engine/CHANGELOG.md index 9a2a0641c7..f8045182da 100644 --- a/packages/engine/CHANGELOG.md +++ b/packages/engine/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion/engine +## 0.56.1 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/pi-claude-cli@0.56.1 + ## 0.56.0 ### Patch Changes diff --git a/packages/engine/package.json b/packages/engine/package.json index c0dbe437f9..e4098cb6d3 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/engine", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Fusion engine: executor, merger, scheduler, and automation runtime for the Fusion AI coding agent.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 6cca96c292..90ee12f5e0 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion/i18n +## 0.39.21 + +### Patch Changes + +- @fusion/core@0.56.1 + ## 0.39.20 ### Patch Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 1f72dffbb0..3808b2a1f6 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/i18n", - "version": "0.39.20", + "version": "0.39.21", "license": "MIT", "description": "Fusion i18n: authored translation catalogs and shared i18next configuration for the Fusion dashboard and terminal UI.", "type": "module", diff --git a/packages/mobile/CHANGELOG.md b/packages/mobile/CHANGELOG.md index eb259f5e2f..c444640557 100644 --- a/packages/mobile/CHANGELOG.md +++ b/packages/mobile/CHANGELOG.md @@ -1,5 +1,7 @@ # @fusion/mobile +## 0.56.1 + ## 0.56.0 ## 0.55.0 diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9155721d9e..f21434c96c 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/mobile", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Fusion mobile: Capacitor wrapper around the Fusion dashboard for iOS and Android.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/packages/pi-claude-cli/CHANGELOG.md b/packages/pi-claude-cli/CHANGELOG.md index 33da4a620d..74c218d378 100644 --- a/packages/pi-claude-cli/CHANGELOG.md +++ b/packages/pi-claude-cli/CHANGELOG.md @@ -1,5 +1,7 @@ # @fusion/pi-claude-cli +## 0.56.1 + ## 0.56.0 ## 0.55.0 diff --git a/packages/pi-claude-cli/package.json b/packages/pi-claude-cli/package.json index 5492de6a46..3e0677fc62 100644 --- a/packages/pi-claude-cli/package.json +++ b/packages/pi-claude-cli/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/pi-claude-cli", - "version": "0.56.0", + "version": "0.56.1", "description": "Fusion vendored fork: pi coding-agent extension that routes LLM calls through the Claude Code CLI. Forked from rchern/pi-claude-cli (MIT). See UPSTREAM.md.", "license": "MIT", "private": true, diff --git a/packages/plugin-sdk/CHANGELOG.md b/packages/plugin-sdk/CHANGELOG.md index 7fae7b8544..16eb15c790 100644 --- a/packages/plugin-sdk/CHANGELOG.md +++ b/packages/plugin-sdk/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion/plugin-sdk +## 0.56.1 + +### Patch Changes + +- @fusion/core@0.56.1 + ## 0.56.0 ### Patch Changes diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 521758e3da..3b118d1e4f 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@fusion/plugin-sdk", - "version": "0.56.0", + "version": "0.56.1", "license": "MIT", "description": "Fusion plugin SDK: types and helpers for authoring third-party plugins that extend the Fusion dashboard and engine.", "homepage": "https://github.com/Runfusion/Fusion#readme", diff --git a/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md b/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md index 92123aac5b..42c6f49400 100644 --- a/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-auto-label/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/auto-label +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-auto-label/package.json b/plugins/examples/fusion-plugin-auto-label/package.json index ac3934a24c..c1b0d827d6 100644 --- a/plugins/examples/fusion-plugin-auto-label/package.json +++ b/plugins/examples/fusion-plugin-auto-label/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/auto-label", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Automatically labels tasks based on description content", "keywords": [ diff --git a/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md b/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md index b14c0e873b..231a173dcc 100644 --- a/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-ci-status/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/ci-status +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-ci-status/package.json b/plugins/examples/fusion-plugin-ci-status/package.json index 8ac8d2e1d2..52acb61b45 100644 --- a/plugins/examples/fusion-plugin-ci-status/package.json +++ b/plugins/examples/fusion-plugin-ci-status/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/ci-status", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Polls CI status for branches and provides a custom API to query results", "keywords": [ diff --git a/plugins/examples/fusion-plugin-notification/CHANGELOG.md b/plugins/examples/fusion-plugin-notification/CHANGELOG.md index 7b693db1b3..8826215ec9 100644 --- a/plugins/examples/fusion-plugin-notification/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-notification/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/notification +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-notification/package.json b/plugins/examples/fusion-plugin-notification/package.json index 0bcc3dda54..b30599211b 100644 --- a/plugins/examples/fusion-plugin-notification/package.json +++ b/plugins/examples/fusion-plugin-notification/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/notification", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Example Fusion plugin that sends webhook notifications on task lifecycle events", "keywords": [ diff --git a/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md b/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md index bc00e600ca..7d1d0186dc 100644 --- a/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md +++ b/plugins/examples/fusion-plugin-settings-demo/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/settings-demo +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/examples/fusion-plugin-settings-demo/package.json b/plugins/examples/fusion-plugin-settings-demo/package.json index c0fe1a7a18..9cf38200b1 100644 --- a/plugins/examples/fusion-plugin-settings-demo/package.json +++ b/plugins/examples/fusion-plugin-settings-demo/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/settings-demo", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Example Fusion plugin demonstrating settings schema and runtime configuration", "keywords": [ diff --git a/plugins/fusion-plugin-acp-runtime/CHANGELOG.md b/plugins/fusion-plugin-acp-runtime/CHANGELOG.md index 3a345fbe25..7e33187842 100644 --- a/plugins/fusion-plugin-acp-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-acp-runtime/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/acp-runtime +## 0.1.21 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.20 ### Patch Changes diff --git a/plugins/fusion-plugin-acp-runtime/package.json b/plugins/fusion-plugin-acp-runtime/package.json index 74d51139c1..871ef720fb 100644 --- a/plugins/fusion-plugin-acp-runtime/package.json +++ b/plugins/fusion-plugin-acp-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/acp-runtime", - "version": "0.1.20", + "version": "0.1.21", "type": "module", "description": "ACP (Agent Client Protocol) runtime plugin for Fusion — drives any ACP-compatible agent over JSON-RPC/stdio", "keywords": [ diff --git a/plugins/fusion-plugin-agent-browser/CHANGELOG.md b/plugins/fusion-plugin-agent-browser/CHANGELOG.md index a505b9593b..8ebc9bf9f5 100644 --- a/plugins/fusion-plugin-agent-browser/CHANGELOG.md +++ b/plugins/fusion-plugin-agent-browser/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/agent-browser +## 0.1.41 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.1.40 ### Patch Changes diff --git a/plugins/fusion-plugin-agent-browser/package.json b/plugins/fusion-plugin-agent-browser/package.json index ad16fb8759..e8289c6c8d 100644 --- a/plugins/fusion-plugin-agent-browser/package.json +++ b/plugins/fusion-plugin-agent-browser/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/agent-browser", - "version": "0.1.40", + "version": "0.1.41", "type": "module", "description": "Agent Browser runtime and prompt/skill/workflow contributions for Fusion", "private": true, diff --git a/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md b/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md index b36d1e51c8..5b5a6ad18a 100644 --- a/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md +++ b/plugins/fusion-plugin-cli-printing-press/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/cli-printing-press +## 0.1.38 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.37 ### Patch Changes diff --git a/plugins/fusion-plugin-cli-printing-press/package.json b/plugins/fusion-plugin-cli-printing-press/package.json index 67fb31e856..cee6385d0e 100644 --- a/plugins/fusion-plugin-cli-printing-press/package.json +++ b/plugins/fusion-plugin-cli-printing-press/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/cli-printing-press", - "version": "0.1.37", + "version": "0.1.38", "type": "module", "description": "CLI Printing Press plugin package for Fusion", "private": true, diff --git a/plugins/fusion-plugin-compound-engineering/CHANGELOG.md b/plugins/fusion-plugin-compound-engineering/CHANGELOG.md index d18283a8d3..aadb5297ee 100644 --- a/plugins/fusion-plugin-compound-engineering/CHANGELOG.md +++ b/plugins/fusion-plugin-compound-engineering/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/compound-engineering +## 0.1.21 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.20 ### Patch Changes diff --git a/plugins/fusion-plugin-compound-engineering/package.json b/plugins/fusion-plugin-compound-engineering/package.json index 6d77c79dd1..8e81d3f700 100644 --- a/plugins/fusion-plugin-compound-engineering/package.json +++ b/plugins/fusion-plugin-compound-engineering/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/compound-engineering", - "version": "0.1.20", + "version": "0.1.21", "type": "module", "description": "Compound Engineering plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md b/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md index f9549c047a..dfc6cd1023 100644 --- a/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-cursor-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/cursor-runtime +## 0.1.40 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.1.39 ### Patch Changes diff --git a/plugins/fusion-plugin-cursor-runtime/package.json b/plugins/fusion-plugin-cursor-runtime/package.json index 2e70825d3a..6d1bda08fe 100644 --- a/plugins/fusion-plugin-cursor-runtime/package.json +++ b/plugins/fusion-plugin-cursor-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/cursor-runtime", - "version": "0.1.39", + "version": "0.1.40", "type": "module", "description": "Cursor CLI runtime plugin for Fusion", "keywords": [ diff --git a/plugins/fusion-plugin-dependency-graph/CHANGELOG.md b/plugins/fusion-plugin-dependency-graph/CHANGELOG.md index 525a181f64..d3bcb35875 100644 --- a/plugins/fusion-plugin-dependency-graph/CHANGELOG.md +++ b/plugins/fusion-plugin-dependency-graph/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/dependency-graph +## 0.1.52 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.51 ### Patch Changes diff --git a/plugins/fusion-plugin-dependency-graph/package.json b/plugins/fusion-plugin-dependency-graph/package.json index 16222c5d22..be38970e04 100644 --- a/plugins/fusion-plugin-dependency-graph/package.json +++ b/plugins/fusion-plugin-dependency-graph/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/dependency-graph", - "version": "0.1.51", + "version": "0.1.52", "type": "module", "description": "Dependency graph dashboard view plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-droid-runtime/CHANGELOG.md b/plugins/fusion-plugin-droid-runtime/CHANGELOG.md index 1b22afb3d5..b2afcd23c3 100644 --- a/plugins/fusion-plugin-droid-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-droid-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.47 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.1.46 ### Patch Changes diff --git a/plugins/fusion-plugin-droid-runtime/package.json b/plugins/fusion-plugin-droid-runtime/package.json index 012e0f4c99..142edcfb7d 100644 --- a/plugins/fusion-plugin-droid-runtime/package.json +++ b/plugins/fusion-plugin-droid-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/droid-runtime", - "version": "0.1.46", + "version": "0.1.47", "type": "module", "description": "Droid runtime plugin for Fusion", "keywords": [ diff --git a/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md b/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md index 79db7d7f80..6203767d6f 100644 --- a/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md +++ b/plugins/fusion-plugin-even-realities-glasses/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/even-realities-glasses +## 0.1.40 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.39 ### Patch Changes diff --git a/plugins/fusion-plugin-even-realities-glasses/package.json b/plugins/fusion-plugin-even-realities-glasses/package.json index 344be51f03..e8c20e82a3 100644 --- a/plugins/fusion-plugin-even-realities-glasses/package.json +++ b/plugins/fusion-plugin-even-realities-glasses/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/even-realities-glasses", - "version": "0.1.39", + "version": "0.1.40", "type": "module", "description": "Canonical Even Realities Fusion plugin with board/task cards, actions, notifications, and webhook transport", "keywords": [ diff --git a/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md b/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md index 54429dbe28..0550886d3a 100644 --- a/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-hermes-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/hermes-runtime +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/fusion-plugin-hermes-runtime/package.json b/plugins/fusion-plugin-hermes-runtime/package.json index af876dab32..f749733062 100644 --- a/plugins/fusion-plugin-hermes-runtime/package.json +++ b/plugins/fusion-plugin-hermes-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/hermes-runtime", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Hermes AI runtime plugin for Fusion - provides AI agent execution runtime", "keywords": [ diff --git a/plugins/fusion-plugin-linear-import/CHANGELOG.md b/plugins/fusion-plugin-linear-import/CHANGELOG.md index 1638eac78f..01ce0c2ac6 100644 --- a/plugins/fusion-plugin-linear-import/CHANGELOG.md +++ b/plugins/fusion-plugin-linear-import/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/linear-import +## 0.1.3 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.2 ### Patch Changes diff --git a/plugins/fusion-plugin-linear-import/package.json b/plugins/fusion-plugin-linear-import/package.json index f118732483..4919e826da 100644 --- a/plugins/fusion-plugin-linear-import/package.json +++ b/plugins/fusion-plugin-linear-import/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/linear-import", - "version": "0.1.2", + "version": "0.1.3", "type": "module", "description": "Linear issue import plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md b/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md index 921a963f1f..e8508218cd 100644 --- a/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-openclaw-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/openclaw-runtime +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/fusion-plugin-openclaw-runtime/package.json b/plugins/fusion-plugin-openclaw-runtime/package.json index 3dfa0f20d1..397aa87505 100644 --- a/plugins/fusion-plugin-openclaw-runtime/package.json +++ b/plugins/fusion-plugin-openclaw-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/openclaw-runtime", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Provides OpenClaw runtime for Fusion AI agents", "keywords": [ diff --git a/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md b/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md index d44930ff61..b01a3cd5e2 100644 --- a/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md +++ b/plugins/fusion-plugin-paperclip-runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/paperclip-runtime +## 0.2.71 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.2.70 ### Patch Changes diff --git a/plugins/fusion-plugin-paperclip-runtime/package.json b/plugins/fusion-plugin-paperclip-runtime/package.json index cc413d9618..a00b20d9ca 100644 --- a/plugins/fusion-plugin-paperclip-runtime/package.json +++ b/plugins/fusion-plugin-paperclip-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/paperclip-runtime", - "version": "0.2.70", + "version": "0.2.71", "type": "module", "description": "Paperclip runtime plugin for Fusion — provides AI agent web access capabilities", "keywords": [ diff --git a/plugins/fusion-plugin-reports/CHANGELOG.md b/plugins/fusion-plugin-reports/CHANGELOG.md index bf9e6f7188..b29957f98d 100644 --- a/plugins/fusion-plugin-reports/CHANGELOG.md +++ b/plugins/fusion-plugin-reports/CHANGELOG.md @@ -1,5 +1,13 @@ # @fusion-plugin-examples/reports +## 0.1.40 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/dashboard@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.39 ### Patch Changes diff --git a/plugins/fusion-plugin-reports/package.json b/plugins/fusion-plugin-reports/package.json index fc187bba8c..0932261506 100644 --- a/plugins/fusion-plugin-reports/package.json +++ b/plugins/fusion-plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/reports", - "version": "0.1.39", + "version": "0.1.40", "type": "module", "description": "Reports plugin for Fusion", "private": true, diff --git a/plugins/fusion-plugin-roadmap/CHANGELOG.md b/plugins/fusion-plugin-roadmap/CHANGELOG.md index f55491f4e1..b8d9ed1a60 100644 --- a/plugins/fusion-plugin-roadmap/CHANGELOG.md +++ b/plugins/fusion-plugin-roadmap/CHANGELOG.md @@ -1,5 +1,12 @@ # @fusion-plugin-examples/roadmap +## 0.1.40 + +### Patch Changes + +- @fusion/core@0.56.1 +- @fusion/plugin-sdk@0.56.1 + ## 0.1.39 ### Patch Changes diff --git a/plugins/fusion-plugin-roadmap/package.json b/plugins/fusion-plugin-roadmap/package.json index 6bf65b5af6..decd0bf70e 100644 --- a/plugins/fusion-plugin-roadmap/package.json +++ b/plugins/fusion-plugin-roadmap/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/roadmap", - "version": "0.1.39", + "version": "0.1.40", "type": "module", "description": "Roadmap plugin package for Fusion", "private": true, diff --git a/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md b/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md index 3afe98696f..99c956c295 100644 --- a/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md +++ b/plugins/fusion-plugin-whatsapp-chat/CHANGELOG.md @@ -1,5 +1,11 @@ # @fusion-plugin-examples/whatsapp-chat +## 0.1.40 + +### Patch Changes + +- @fusion/plugin-sdk@0.56.1 + ## 0.1.39 ### Patch Changes diff --git a/plugins/fusion-plugin-whatsapp-chat/package.json b/plugins/fusion-plugin-whatsapp-chat/package.json index b66806fbb3..fb5071968e 100644 --- a/plugins/fusion-plugin-whatsapp-chat/package.json +++ b/plugins/fusion-plugin-whatsapp-chat/package.json @@ -1,6 +1,6 @@ { "name": "@fusion-plugin-examples/whatsapp-chat", - "version": "0.1.39", + "version": "0.1.40", "type": "module", "description": "WhatsApp Web (Baileys) chat bridge for Fusion agents", "keywords": [