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/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-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..833a246cfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,60 +2,727 @@ 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 - -### 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. +## 0.56.1 ### 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. +- 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. + +## 0.56.0 + +### @fusion/dashboard + +#### 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 + +### @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 + +### @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 +12789,22 @@ 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 + +#### Patch Changes + +- @fusion/core@0.56.0 + ## 0.39.19 ### @fusion/i18n @@ -12276,6 +12959,22 @@ 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 + +#### Patch Changes + +- @fusion-plugin-examples/droid-runtime@0.1.46 + ## 0.11.45 ### @fusion/droid-cli 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/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/dashboard-guide.md b/docs/dashboard-guide.md index 8992295765..7cd3333754 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -196,12 +196,16 @@ Features: - Task cards show a read-only **oversight-level badge** (`Observe`, `Steer`, or `Auto-recovery`) in the meta-badges cluster reflecting the effective planner-oversight level, but only when oversight is *meaningfully configured* — an explicit per-task override (including an explicit `autonomous` override), or a resolved workflow/effective tier of `observe`/`steer` (`data-testid="card-oversight-badge"`). A card that merely **inherits** the schema default `autonomous` tier (no per-task override, no non-default workflow tier) renders no badge and no empty `.card-meta-badges` shell. The badge is also absent when the effective level is explicitly "off", **and** while an inherited (no per-task-override) workflow tier is still being resolved (in flight or not yet fetched) — it never shows a guessed default during that window. - -- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) adds four planner-overseer controls: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes. + +- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) exposes planner-overseer controls behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) on every viewport, desktop and mobile alike. Clicking the trigger opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) containing: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes. The trigger itself is withheld entirely when none of the menu's controls would render (no empty-shell overflow button for the unresolved-default case). -- The FN-7517 controls above carry a visible, non-interactive **`"Overseer controls"` group label** (`data-testid="detail-oversight-controls-label"`) so Nudge/Stop/Explain read as an identifiable cluster rather than unlabeled chips; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders an empty shell when oversight is Off/unresolved. When **Nudge** is disabled, an always-visible helper line (`data-testid="detail-overseer-nudge-disabled-reason"`) states the reason in-DOM (mirroring the existing hover `title`) instead of relying on a mouse-hover tooltip alone. **Explain** is read-only and non-mutating, so it is never disabled purely because the overseer is inactive — clicking it always opens/closes the panel, which shows the overseer's live state when watching or an informative "not currently watching this task" message otherwise. Nudge's mutating enablement rule (`canNudgeOverseer`, including the human-control suppression cases) and Stop's confirmation dialog are unchanged. - -- On mobile (viewport ≤ 768px), the level-select/nudge/stop/explain controls above collapse behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) that opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) listing the same controls as full-width, comfortably tappable `role="menuitem"` entries with the SAME testids and enablement/visibility rules as the desktop inline controls. The trigger itself is withheld entirely when none of those controls would render (no empty-shell overflow button for the oversight-off + overseer-inactive default). Desktop (>768px) keeps the inline cluster unchanged. Opening the menu presents exactly one surface: menu-open auto-focus lands on the first actionable button menuitem, never the native level ``, so the OS option picker never auto-opens on top of the custom popover (FN-7562). - The task detail modal's **Activity** tab view dropdown (Live/Feed/Raw) gains a fourth **Interventions** option, shown only when planner oversight is active for the task (same gate as the former inline mount: `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff`). Selecting it renders the **Intervention Timeline** (`data-testid="planner-intervention-timeline"`) inside the Activity panel, listing every recorded planner-overseer intervention for the task, newest-first: watched stage, reason, action taken, outcome (with a `.status-dot` indicator using semantic outcome tokens), an attempt count/limit badge (only when both are present), and source links (agent log / review comment / failed check / merge error / PR state / generic URL). It renders a calm "No planner interventions yet" empty state rather than an empty shell when there are none. When oversight is off or unresolved, the Interventions option is absent from the dropdown entirely (no leftover empty segment), and if it was previously selected the view falls back to Live rather than leaving a blank panel. Entries are read via `GET /tasks/:id/overseer/interventions`, which assembles them from the existing run-audit store under the `overseer:intervention` mutation type (`recordPlannerIntervention`/`getPlannerInterventionTimeline` in `@fusion/core`). This is a pure read surface — FN-7520 wires the actual intervention-producing call-sites. - Task detail surfaces show the selected/effective workflow identity near the task's workflow controls so individual cards remain understandable when Board is in **All workflows** or another aggregate/mixed context. @@ -432,6 +436,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/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: 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/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/docs/workflow-steps.md b/docs/workflow-steps.md index 9e453b6d98..746a4a1c05 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. 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/package.json b/package.json index 35dcd38c79..6873ec1ab3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fusion-workspace", - "version": "0.55.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 f0deb8a7cb..6d000e9c3c 100644 --- a/packages/cli-alias/CHANGELOG.md +++ b/packages/cli-alias/CHANGELOG.md @@ -1,5 +1,126 @@ # 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 + +- 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..f9e5024570 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.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 a6f808e15e..94f2909e77 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,345 @@ # @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 + +- 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..f8a2286eb3 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.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 81a426217d..c8d6eef7fc 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,9 @@ # @fusion/core +## 0.56.1 + +## 0.56.0 + ## 0.55.0 ## 0.54.0 diff --git a/packages/core/package.json b/packages/core/package.json index 92b3a6d9c8..c6ad2fd87d 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.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/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/__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/__tests__/coding-ideas-move.test.ts b/packages/core/src/__tests__/coding-ideas-move.test.ts new file mode 100644 index 0000000000..d8232b41a6 --- /dev/null +++ b/packages/core/src/__tests__/coding-ideas-move.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { createTaskStoreTestHarness } from "./store-test-helpers.js"; + +/* +FNXC:WorkflowColumns 2026-07-05-19:10: +Regression for the disappearing move on custom workflow columns. Workflow columns graduated to +always-on (no experimental flag emitted), but moveTaskInternal still gated its workflow path on the +retired strict compatibility flag, so it fell back to the legacy VALID_TRANSITIONS table — which is +keyed only by the legacy column ids. A task in a non-legacy column (Coding (Ideas) → "ideas") could +not move: "Invalid transition: 'ideas' → 'todo'. Valid targets: none". + +Surface enumeration (invariant: a move is validated by the task's WORKFLOW adjacency, not the legacy +table, on a default project with NO experimental flag set): + - Custom intake column forward move: ideas → todo is allowed. + - Full custom-column chain onward: todo → in-progress → in-review all succeed (→ done is gated by the + workflow's own merge trait, which is orthogonal to transition adjacency and excluded here). + - Non-adjacent move still rejects with the workflow's targets (ideas → in-progress). + - Holds for both user- and engine-sourced moves. + - Default workflow (legacy column ids) is unchanged (parity), verified in move-task-characterization. +*/ +describe("Coding (Ideas) custom-column moves (workflow-columns graduation)", () => { + const harness = createTaskStoreTestHarness(); + beforeEach(harness.beforeEach); + afterEach(harness.afterEach); + + it("moves an ideas-workflow task from the ideas intake column to todo", async () => { + const store = harness.store(); + const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" }); + expect(task.column).toBe("ideas"); + + const moved = await store.moveTask(task.id, "todo", { moveSource: "user" }); + expect(moved.column).toBe("todo"); + }); + + it("advances an ideas task along the Coding (Ideas) custom-column chain", async () => { + const store = harness.store(); + const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" }); + + await store.moveTask(task.id, "todo", { moveSource: "user" }); + await store.moveTask(task.id, "in-progress", { moveSource: "user" }); + const inReview = await store.moveTask(task.id, "in-review", { moveSource: "user", allowDirectInReviewMove: true }); + expect(inReview.column).toBe("in-review"); + }); + + it("still rejects a non-adjacent move out of the ideas column", async () => { + const store = harness.store(); + const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" }); + await expect( + store.moveTask(task.id, "in-progress", { moveSource: "user" }), + ).rejects.toThrow(/Invalid transition: 'ideas' → 'in-progress'/); + }); + + it("allows the ideas → todo move from an engine source too", async () => { + const store = harness.store(); + const task = await store.createTask({ description: "idea", workflowId: "builtin:coding-ideas" }); + const moved = await store.moveTask(task.id, "todo", { moveSource: "engine" }); + expect(moved.column).toBe("todo"); + }); +}); 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..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: @@ -36,6 +37,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({ @@ -60,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/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/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", diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 12e638163b..2958360fb8 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -7676,11 +7676,22 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} !sourceIsLegacy && (COLUMNS as readonly string[]).includes(toColumn); if (!isEvacuation) { - // Legacy flag-OFF branch (useWorkflow === false): both columns are - // guaranteed legacy ids here — a non-legacy `toColumn` returns `?? []` - // and rejects below, and flag-OFF tasks never hold custom column ids. - // The `as Column` is provably safe within this branch (#1403). - const validTargets = VALID_TRANSITIONS[task.column as Column] ?? []; + /* + FNXC:WorkflowColumns 2026-07-05-19:30: + Workflow columns graduated to always-on (no experimental flag emitted), so this "flag-OFF" + branch is the DEFAULT move path for nearly every project — the strict compat flag reads false + because nothing sets it. Legacy columns (triage/todo/in-progress/in-review/done/archived) are + validated verbatim by VALID_TRANSITIONS, preserving the legacy bare-Error contract. But a task + can legitimately sit in a NON-legacy workflow column now (e.g. Coding (Ideas) → "ideas"), which + VALID_TRANSITIONS cannot key — the old code returned `?? []` and rejected EVERY move out of it + ("Invalid transition: 'ideas' → 'todo'. Valid targets: none"). Resolve a non-legacy source + column's targets from the task's own workflow adjacency instead, still throwing the same + legacy-style bare Error (not TransitionRejectionError) so the flag-OFF characterization contract + holds for legacy columns. + */ + const validTargets = sourceIsLegacy + ? (VALID_TRANSITIONS[task.column as Column] ?? []) + : resolveAllowedColumns(this.resolveTaskWorkflowIrSync(id), task.column); if (!validTargets.includes(toColumn as Column)) { throw new Error( `Invalid transition: '${task.column}' → '${toColumn}'. ` + diff --git a/packages/dashboard/CHANGELOG.md b/packages/dashboard/CHANGELOG.md index e8cf58ead6..7d9e93287d 100644 --- a/packages/dashboard/CHANGELOG.md +++ b/packages/dashboard/CHANGELOG.md @@ -1,5 +1,39 @@ # @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 + +- @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/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/__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/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/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/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/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/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/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index ebac44574b..47cdecbfe8 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -322,26 +322,23 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P /* FNXC:PlannerOversight 2026-07-04-17:00: FN-7517: the oversight-controls cluster (priority + execution-mode + the - new oversight-level/nudge/stop/explain controls) must wrap without - overflowing the mobile modal width rather than forcing horizontal scroll. + Oversight trigger) must wrap without overflowing the mobile modal width + rather than forcing horizontal scroll. - FNXC:PlannerOversight 2026-07-04-19:00: - FN-7545: the level-select/nudge/stop/explain controls now collapse behind a - single `.detail-oversight-menu-trigger` button below this breakpoint (JS - `isOversightMenuMobile` swap in TaskDetailModal.tsx, not a CSS hide), so the - cluster only ever needs to fit priority + execution-mode + the trigger on - one row; `flex-wrap: wrap` remains as a safety fallback so the cluster never - forces horizontal scroll if it still overflows at very narrow widths. + FNXC:PlannerOversight 2026-07-05-00:00: + FN-7604: the level-select/nudge/stop/explain controls collapse behind the + single `.detail-oversight-menu-trigger` button at EVERY viewport now (no + longer a mobile-only branch), so the cluster only ever needs to fit + priority + execution-mode + the trigger on one row; `flex-wrap: wrap` + remains as a safety fallback so the cluster never forces horizontal scroll + if it still overflows at very narrow widths. The `.detail-oversight-menu` + positioning (`right: 0`) is already declared once on the base rule above + and applies at every width, so no mobile-only override is needed here. */ .detail-meta-inline-controls { flex-wrap: wrap; } - .detail-oversight-menu { - right: 0; - left: auto; - } - .detail-provenance { align-items: center; } @@ -370,8 +367,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,19 +396,76 @@ 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 { 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); +} + +/* +FNXC:TaskDetail 2026-07-05-13:30: +FN-7601 — the FN-7585 base `.detail-priority-chip` rule pins `border-color: +var(--border)` for every level, so once the chip shrank to a compact 30px box +the only remaining differentiator (the ~15-18% tint backgrounds inherited +from `.card-priority-badge--{low,high,urgent}`) reads as the same washed-out +box for low vs high. Give each non-neutral level its own tinted border AND a +stronger background on top of the shared base/normal rules above (higher +specificity via the `.detail-priority-chip.card-priority-badge--X` compound +selector) so low/high/urgent are unmistakably distinct colors at a glance, +while normal keeps the FN-7597 neutral treatment and the FN-7585 shared +border-width/radius/min-height stay untouched. Scoped to `.detail-priority-chip` +so the read-only `.card-priority-badge--{low,high,urgent}` tints in +TaskCard.css (out of scope) are unaffected. +*/ +.detail-priority-chip.card-priority-badge--low { + background: color-mix(in srgb, var(--color-info) 22%, transparent); + border-color: color-mix(in srgb, var(--color-info) 65%, var(--border)); + color: var(--color-info); +} + +.detail-priority-chip.card-priority-badge--high { + background: color-mix(in srgb, var(--color-warning) 28%, transparent); + border-color: color-mix(in srgb, var(--color-warning) 65%, var(--border)); + color: var(--color-warning); +} + +.detail-priority-chip.card-priority-badge--urgent { + background: color-mix(in srgb, var(--color-error) 30%, transparent); + border-color: color-mix(in srgb, var(--color-error) 65%, var(--border)); + color: var(--color-error-dark); +} + .detail-priority-select { border: 0; background: transparent; color: inherit; font: inherit; - text-transform: uppercase; - letter-spacing: inherit; cursor: pointer; padding: 0; min-height: inherit; @@ -412,10 +481,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 { @@ -425,6 +501,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 { @@ -454,25 +539,20 @@ The task-detail modal metadata must keep priority, execution mode, provenance, P /* FNXC:PlannerOversight 2026-07-04-17:00: -FN-7517 task-detail planner-overseer controls (quick level-change chip, +FN-7517 task-detail planner-overseer controls (quick level-change select, manual nudge / stop / explain buttons, explain panel). Reuses the same `.card-oversight-badge--*` status-color modifiers FN-7516's TaskCard badge 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. +semantic source. + +FNXC:PlannerOversight 2026-07-05-00:00: +FN-7604 — the desktop-only `.detail-oversight-chip` wrapper (the inline +quick-level-change chip rendered outside the overflow menu) was removed along +with the desktop inline branch it styled; the level select now renders only +inside `.detail-oversight-menu-item--select` (see below). `.detail-oversight-select` +itself stays — the dropdown menu item reuses this class for the native +` { - void handleOversightLevelChange(event.target.value); - }} - disabled={isSavingOversightLevel} - aria-label={t("taskDetail.oversight.ariaLabel", "Planner oversight level")} - > - - {PLANNER_OVERSIGHT_LEVELS.map((levelOption) => ( - - ))} - - - )} - {/* - FNXC:PlannerOversight 2026-07-04-17:00: - FN-7517 manual nudge / stop oversight / explain current action - controls. Disabled (with an accessible aria-label reason) rather - than hidden for nudge/explain when the overseer is off/inactive - so operators understand WHY the control is inert instead of it - silently vanishing; stop is hidden once oversight is already off - (nothing left to stop) per the PROMPT's enablement rule, avoiding - an always-on empty shell for the common oversight-off default. - - FNXC:PlannerOversight 2026-07-04-20:30: - FN-7546 — operators reported these controls were confusing: - unlabeled inline chips, greyed out most of the time, with the - only "why" behind a mouse-hover `title`. Add a visible, - non-interactive `detail-oversight-controls-label` group label - (gated by the SAME condition as the buttons) so the cluster is - identifiable, and a `detail-overseer-nudge-disabled-reason` - helper line that surfaces the disabled reason in-DOM (not just - on hover) whenever Nudge is unavailable. Explain is read-only - and non-mutating, so its disabled gate is removed entirely - (see handleExplainOverseer below) — it always opens its panel, - which already renders an informative "not currently watching" - empty state. Nudge's mutating gate (`canNudgeOverseer`) and - Stop's confirm dialog are unchanged. - */} - {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && ( - - {t("taskDetail.oversight.controlsLabel", "Overseer controls")} - - )} - {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && ( - - )} - {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && !canNudgeOverseer && ( - - {nudgeDisabledReason} - - )} - {(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && ( - - )} - {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && ( - - )} - - )} + )} {overseerExplainOpen && (
@@ -5997,6 +5894,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 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 + 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 (
-
+
{ + terminal.open(terminalRef.current!); + }); // Clear watchdog — imports and open() succeeded within deadline if (watchdogTimer) { 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__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 0f48411f2d..83c87f28a4 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); @@ -6396,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/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx new file mode 100644 index 0000000000..25b98b7892 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/TaskDetail.mobile-transition.board-panel.test.tsx @@ -0,0 +1,333 @@ +/** + * FN-7587 companion suite: board main-panel (MainContent.tsx) mobile predictive-back + * transition class gating. + * + * FNXC:TaskDetailSwipeBack 2026-07-05-12:45: + * Split out from `TaskDetail.mobile-transition.test.tsx` because this surface needs the + * full App-level mock harness (mirrors `TaskDetail.swipe-back.test.tsx`'s harness: Board/ + * ListView/TaskDetailModal module mocks, real lucide-react icons via Header) which conflicts + * with the TaskDetailModal-focused `test-helpers` harness's fixed lucide-react icon allowlist + * if both are combined in one test module (vi.mock hoisting collides). + * + * Asserts only the class-gating invariant: `.task-detail-main-panel--mobile-transition` is + * present on mobile and absent on desktop. Does not re-derive dismissal-routing coverage, + * which stays the responsibility of `TaskDetail.swipe-back.test.tsx` / + * `navigation-history.test.tsx` (run unmodified per PROMPT.md). + */ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import type { Settings, Task } from "@fusion/core"; +import type { ProjectInfo } from "../../api"; + +const defaultSettings: Settings = { + maxConcurrent: 2, + maxWorktrees: 4, + pollIntervalMs: 15000, + groupOverlappingFiles: false, + autoMerge: true, + recycleWorktrees: false, + worktreeInitCommand: "", + testCommand: "", + buildCommand: "", + experimentalFeatures: { insights: true, roadmap: true, skillsView: true, agentsView: true, evalsView: true, todoView: true, leftSidebarNav: false, rightDock: false }, +}; + +const mockSubscribeSse = vi.fn((..._args: any[]) => 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/__tests__/TaskDetail.swipe-back.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetail.swipe-back.test.tsx index f6045f3b19..4071908d83 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"; @@ -789,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/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( { ); // 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 768ed722a7..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(); @@ -408,24 +449,20 @@ describe("TaskDetailModal oversight controls", () => { }); /* - * FNXC:PlannerOversight 2026-07-04-20:30 (FN-7558): - * FN-7521's original mobile suite asserted the oversight quick-controls - * cluster rendered its FLAT inline testids at a narrow `window.innerWidth`, - * 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. - */ -describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-7545 overflow menu)", () => { +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 — +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; beforeEach(async () => { @@ -437,10 +474,153 @@ 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. + }); + + 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( + , + ); + + await openOversightMenu(); + 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( + , + ); + + await openOversightMenu(); + 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 + * cluster rendered its FLAT inline testids at a narrow `window.innerWidth`, + * 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 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 — narrow-viewport regression guard (FN-7521, FN-7545/FN-7604 universal overflow menu)", () => { + 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 }); + // 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 }); }); @@ -448,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); @@ -482,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 () => { @@ -595,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(); }); @@ -612,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); @@ -633,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(); @@ -652,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(); @@ -697,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" })); @@ -727,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 5bf7f21caa..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 @@ -226,6 +226,157 @@ 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 oversightTriggerBlock = getExactCssRuleBlock(css, ".detail-oversight-menu-trigger"); + + // 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, 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 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(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 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 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);"); + 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("makes low/high/urgent visibly distinct colors on the detail Priority chip, scoped away from TaskCard (FN-7601)", () => { + const css = readDashboardStylesSource(); + + // FN-7585's shared base rule and FN-7597's neutral `normal` rule must + // survive untouched — this task only ADDS per-level overrides on top. + const baseChipBlock = getExactCssRuleBlock(css, ".detail-priority-chip"); + expect(baseChipBlock).toContain("border-width: var(--btn-border-width);"); + expect(baseChipBlock).toContain("border-color: var(--border);"); + expect(baseChipBlock).toContain("border-radius: var(--detail-control-border-radius);"); + const normalBlock = getExactCssRuleBlock(css, ".detail-priority-chip.card-priority-badge--normal"); + expect(normalBlock).toMatch(/background:\s*color-mix\(in srgb, var\(--text-muted\)/); + expect(normalBlock).toContain("color: var(--text-muted);"); + + const lowBlock = getExactCssRuleBlock(css, ".detail-priority-chip.card-priority-badge--low"); + const highBlock = getExactCssRuleBlock(css, ".detail-priority-chip.card-priority-badge--high"); + const urgentBlock = getExactCssRuleBlock(css, ".detail-priority-chip.card-priority-badge--urgent"); + + // Each non-neutral level must declare its own tinted border-color AND + // background, using the matching semantic token family. + for (const [block, token] of [ + [lowBlock, "--color-info"], + [highBlock, "--color-warning"], + [urgentBlock, "--color-error"], + ] as const) { + expect(block).not.toBe(""); + expect(block).toMatch(/border-color\s*:/); + expect(block).toMatch(/background\s*:/); + expect(block).toContain(token); + } + + // None of the per-level border-colors may resolve to the plain shared + // `var(--border)` value used by the base rule — that was the original + // bug (every level looked the same washed-out box). + const borderColorOf = (block: string): string => { + const match = block.match(/border-color\s*:\s*([^;]+);/); + return match?.[1]?.trim() ?? ""; + }; + const backgroundOf = (block: string): string => { + const match = block.match(/background\s*:\s*([^;]+);/); + return match?.[1]?.trim() ?? ""; + }; + + const lowBorder = borderColorOf(lowBlock); + const highBorder = borderColorOf(highBlock); + const urgentBorder = borderColorOf(urgentBlock); + + expect(lowBorder).not.toBe("var(--border)"); + expect(highBorder).not.toBe("var(--border)"); + expect(urgentBorder).not.toBe("var(--border)"); + + // Mutually distinct — low, high, and urgent must not collapse onto the + // same border-color or background declaration as one another. + expect(new Set([lowBorder, highBorder, urgentBorder]).size).toBe(3); + const lowBg = backgroundOf(lowBlock); + const highBg = backgroundOf(highBlock); + const urgentBg = backgroundOf(urgentBlock); + expect(new Set([lowBg, highBg, urgentBg]).size).toBe(3); + + // `normal`'s background/border must remain distinct from all three tinted + // levels (it keeps the FN-7597 neutral treatment, not a semantic tint). + expect(new Set([backgroundOf(normalBlock), lowBg, highBg, urgentBg]).size).toBe(4); + + // The read-only TaskCard badge tints referenced by TaskCard.css must be + // untouched by this task — confirm no `.detail-priority-chip` compound + // selector leaks a border-color override into the bare `.card-priority-badge--*` + // selectors (those remain single-class, background/color-only rules). + expect(css).toMatch(/\.card-priority-badge--low\s*\{\s*background:\s*color-mix\(in srgb, var\(--color-info\) 15%, transparent\);\s*color:\s*var\(--color-info\);\s*\}/); + expect(css).toMatch(/\.card-priority-badge--high\s*\{\s*background:\s*color-mix\(in srgb, var\(--color-warning\) 18%, transparent\);\s*color:\s*var\(--color-warning\);\s*\}/); + expect(css).toMatch(/\.card-priority-badge--urgent\s*\{\s*background:\s*color-mix\(in srgb, var\(--color-error\) 20%, transparent\);\s*color:\s*var\(--color-error-dark\);\s*\}/); + }); + it("keeps grouped timestamp metadata inline on desktop and mobile", () => { const css = readDashboardStylesSource(); @@ -464,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 @@ -504,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(() => { @@ -531,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(() => { @@ -558,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(() => { @@ -583,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(() => { @@ -622,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(() => { @@ -684,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(() => { @@ -721,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(() => { @@ -758,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(() => { @@ -801,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(() => { @@ -832,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(() => { @@ -869,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(() => { @@ -903,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(() => { 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/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..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 @@ -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, }; } @@ -167,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 /> @@ -269,6 +288,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)); @@ -317,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/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). + */} +
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. + */}