The list() trap now applies a final filter against loggedOutProviders,
matching the defensive approach used in the CLI layer. While target.logout()
removes entries from underlying storage, this prevents any edge case where
a logged-out provider could appear in list() results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- get() now returns undefined for logged-out providers instead of
delegating to target.get() which could bypass the guard
- getCredential() in provider-auth returns undefined for logged-out
providers instead of falling through to authStorage.get()
- getAll() skips logged-out providers at top of loop
- list() filters modelsJsonApiKeys against loggedOutProviders
- Added remove() trap in provider-auth for clearApiKey flow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The logout flow had two bugs causing credentials to reappear immediately:
1. The codebase has two separate auth storage Proxy chains:
- createFusionAuthStorage (engine, for agents)
- mergeAuthStorageReads (CLI, for dashboard UI)
Neither had a logout trap, so supplemental credentials from
~/.claude/.credentials.json were never excluded after logout.
2. The upstream AuthStorage.hasAuth() checks environment variables
(ANTHROPIC_API_KEY), which always returns true regardless of logout.
Fix: Add loggedOutProviders tracking to both Proxy chains. All query
traps (has, hasAuth, get, getAll, list, getApiKey) return false/undefined
for logged-out providers instead of delegating to the underlying storage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the local fn/fusion CLI is on the wrong version, the banner now
says "Update the Fusion CLI", shows installed vs expected versions,
and labels the action "Update with npm" instead of the generic install
copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test clicked 'Medium' and 'Continue' synchronously without waiting
for the scope question options to fully render. Use findByText/findByRole
to wait for elements to appear before interacting, and add a timeout to
the final waitFor for the second question.
Previously the back button left the dashboard page entirely because
useNavigationHistory was only enabled for mobile viewports. Now it's
enabled for all viewports — desktop and mobile — so the browser back
button dismisses modals and reverts view changes within the SPA.
Changes:
- Enable useNavigationHistory for desktop (enabled: true instead of enabled: isMobile)
- Remove all isMobile ? historyAwareHandler : plainHandler ternaries
- Always use navigation-aware handlers (openDetailTask, openSettingsWithNav, etc.)
- Update JSDoc to reflect desktop support
Up/Down arrows now cycle the focused panel on the Main page (mirroring
Left/Right), except on the Logs panel where they keep their existing
role of navigating log entries. Expanding a log entry with Enter now
also disables xterm mouse reporting so the user can click-drag to
select the message text; closing the expanded view restores wheel
scrolling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
node-pty was previously only present transitively via the workspace
@fusion/dashboard devDependency, which is stripped at publish time. Fresh
users running `npx runfusion.ai` hit a 503 "PTY module could not be loaded"
when opening the dashboard terminal. Tightened the package-config guard so
this regression is caught next time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch process.emitWarning in the shared vitest setup to drop the
"SQLite is an experimental feature" notice; align the test mock for
getQrPayload with the canonical RemoteQrPayload signature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Raise checkForChanges slow-poll warn threshold from 100ms to 750ms so
warnings only fire when cycles approach the 1s poll interval, and route
skill-resolver info diagnostics through log() instead of warn().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the race called out in code review: when recovery samples a
stale run id and a fresh run is spawned for the same agent before
endHeartbeatRun() lands, only the sampled id is terminated — never the
freshly-spawned run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the dashboard crashes mid-heartbeat, the agentRuns row is left in
status='active' forever. HeartbeatTriggerScheduler.onTimerTick treats
any active run as "still running" and skips every subsequent tick, so
agents go silent indefinitely (observed: 6+ hours). The existing
in-memory missed-heartbeat watchdog can't help — its trackedAgents map
is wiped on process restart.
SelfHealingManager.recoverStaleHeartbeatRuns now reconciles these on
startup and during periodic maintenance: terminates active runs whose
processPid does not match the current process, has no recorded pid, or
has been active for more than 6 hours.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MissionLoop calls this.missionStore.listMissions() during startup
recovery, but the mock TaskStore's getMissionStore() return value didn't
include this method. When the runtime startup sequence raced ahead, it
would hit "listMissions is not a function" — making the test flaky.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The merge completes FN-3498 across three steps: adds ownership-aware done-task reconciliation to the merger, prevents branch-missing head SHA pollution during merge operations, and restores workspace typecheck compatibility. Core changes touch the merger (103 lines) and self-healing module (67 lines
Fusion-Task-Id: FN-3498
Merged four commits implementing comment-driven retriage: triage rules now respond to specific comment patterns (Step 1) and surface needs-replan feedback inputs in the UI (Step 2), with documentation for the new behavior and a bug fix restoring workspace typecheck defaults. Changes span the core ta
Fusion-Task-Id: FN-3502
Merges FN-3470 in two steps: first, runtime plugins now prefer source path aliases over build artifacts, with hardening tests for vitest alias resolution and plugin view registry updates; second, a new `ensure-test-artifacts.mjs` bootstrap script guarantees required test files exist before test runs
Fusion-Task-Id: FN-3470
This merge introduces an eval automation domain store with persistence schema and a plugin dashboard view registry (FN-3512/FN-3513), adds scheduled eval batch architecture documentation (FN-3388), and includes substantial test coverage for Research routes and hooks (FN-3368 steps 1-3). The branch a
Fusion-Task-Id: FN-3368
The options parameter in __setCreateFnAgent's inline arrow already
uses any to match the module-level let (which has its own suppress
comment). Add the missing eslint-disable-next-line so CI passes.
Unrelated to the spinner fix — pre-existing on main.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-existing lint error on main — the `any` cast in __setCreateFnAgent
mirrors the eslint-disable already on the adjacent variable declarations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Simulates a database where project_insight_runs exists without lifecycle
and cancelledAt columns (the state that caused the original bug), then
verifies that re-opening the database adds the columns and creating a run
succeeds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This merge adds three major features: a new **eval domain store** (`eval-store.ts`, `eval-types.ts`) for AI evaluation data with persistence schema and a `PluginDashboardViewHost` for extensibility; a **plugin dashboard view registry** with navigation integration that lets plugins register custom vi
Fusion-Task-Id: FN-3512
This merge adds three major features: an eval domain (`eval-store.ts`, `eval-types.ts`) with persistence schema for evaluation data; a plugin dashboard view registry with navigation integration for third-party dashboard extensions; and GitHub source metadata traceability that locks and enforces issu
Fusion-Task-Id: FN-3513
Merged FN-3157 to add a plugin dashboard views system, including a plugin view registry with lazy loading, navigation integration for Header and MobileNavBar, a usePluginDashboardViews hook with cache and refetch support, and tests covering the no-loader path. Also added documentation in `docs/PLUGI
Fusion-Task-Id: FN-3157
Introduced a new evaluation persistence layer in `@fusion/core` with domain contracts, SQLite-backed storage APIs, and retention/window-rollup support, wired through the core store and documented in the architecture and storage docs.
Fusion-Task-Id: FN-3387
The @keyframes spin and .animate-spin rules were incorrectly placed
inside the first :root {} block, which is invalid CSS — @keyframes
cannot be nested inside selector blocks. Browsers silently ignore
them in that position, so the spinners would still get stuck on pages
without component CSS loaded.
Moves the rules to the stylesheet top level between the two :root
blocks. Also fixes the mobile-css regression test that parses the
first :root block with a non-greedy regex.
Addresses review feedback from Greptile (P1) and CodeRabbit on PR #40.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `.animate-spin` CSS utility class and `@keyframes spin` were only
defined in TaskCard.css but used by 25+ components (~60 instances).
On pages without a TaskCard rendered, the class didn't exist in the
stylesheet, causing all spinners to appear frozen.
Moves both the keyframes and utility class to the global styles.css
so they're available from the first paint regardless of which
components are loaded.
ClosesRunfusion/Fusion#39
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merges three major changesets: ephemeral agent cleanup for FN-3481 (runtime and spawned agent teardown), a fix for planning-mode refine continuation flow (FN-3209) plus a new local startup script, and chat SSE broadcast isolation with QuickChat backend unification. Key components affected include th
Fusion-Task-Id: FN-3481
Merges fixes for the planning refine continuation flow (FN-3209) alongside a new local startup script for development environments. The changes include updates to `PlanningModeModal.tsx`, new and updated tests for the planning system, route handler improvements in `chat.ts` and `planning.ts`, and do
Fusion-Task-Id: FN-3209
Databases already at schema v59+ when the lifecycle column was added never
re-ran the v59 migration, causing "no column named lifecycle" errors on
insight generation. Adds an unconditional compatibility check following the
existing ensureRoutinesSchemaCompatibility pattern.
Fixes#42
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend
- chat.ts now routes both regular chat and QuickChat through
createResolvedAgentSession instead of branching to createFnAgent for
the no-runtime-hint case. This removes the divergent path where
pi-ai's cleanupSessionResources(sessionId) could tear down resources
the next generation depends on.
- sendMessage's finally only disposes the agent if it still owns the
activeGenerations slot. A newer generation that has pre-empted us
cleans up its own agent in its own finally — disposing here would
yank the underlying CLI process out from under it.
- __setCreateFnAgent test helper now mirrors its mock into the
createResolvedAgentSession slot so existing test setups still work
after the unification.
Frontend
- Extract createChatStreamHandlers (RAF coalescing, accumulators,
tool-call dedup, fallback handling) — useChat and useQuickChat were
duplicating ~85 LOC each. Both now compose the shared factory.
- Move shared chat types into chatTypes.ts. The hooks re-export them
for backward compatibility with existing consumers.
- Removed per-message Markdown/plain-text eye toggles. A single
thread-level toggle in the chat header now flips every assistant
bubble (including the streaming one) between rendered Markdown and
plain text.
- Model-only chats hide the per-message agent identity row entirely;
the model name is already in the thread header.
Tests
- Updated ChatView tests to reflect the new render-toggle contract
(single header toggle drives all bubbles) and the model-only avatar
suppression.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
req.on("close") was firing for healthy SSE connections in this
Node/Express setup, which cancelled the in-flight agent the client was
still listening to and made even msg #1 fail after a fresh reload.
Keep the beginGeneration (no dispose) change from the same commit —
that one's correct.
Closing the QuickChat browser tab mid-response left the backend agent
running with no listener. The `activeGenerations` slot persisted and the
next message's freshly-opened CLI SessionManager raced against the
lingering agent on the same session file, so the model produced no
output for the new prompt.
- POST /chat/sessions/:id/messages now calls cancelGeneration on
req.on("close") if the response hasn't ended yet, so disconnects stop
the agent promptly.
- beginGeneration only aborts the prior controller; it no longer
disposes the prior agent. Disposing pre-emptively could yank the CLI
process out from under the new generation's session opened on the
same file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>