Start performs a bare column move, so "Started planning {id}" reported an
outcome the handler cannot observe: the engine still has to admit the card, and
a busy pool (per-project maxConcurrent or cross-project globalMaxConcurrent) can
defer that indefinitely. The wait was only visible in the engine log
("Plan throttled by running-agent cap|global semaphore"), so a throttled card
looked like a bug.
- Add a "Queued to plan" badge: the exact complement of "Ready" (same idle-Todo
conditions, but no steps yet, so it waits for a PLANNING slot rather than a WIP
slot). Three Todo states are now distinguishable: planning in flight, queued to
plan, and ready. Pause suppression matches Ready; the badge reuses the existing
status-badge primitives with a color-mix tint, no new tokens.
- Retitle the Start toast to "Queued {id} for planning" in both call sites
(TaskCard Start and QuickEntryBox quick-add Start).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pressing Start on a Coding (Ideas) card only writes a column move — there is no
dispatch call in that path — so planning did not begin until the triage
processor's next timer tick, up to pollIntervalMs (15s default) later. The
"Started planning" toast was optimistic and the card just sat in Todo.
- Wake planning discovery on the store's task:updated/task:created event when a
task lands in todo/triage. Binding the wake to the store event rather than the
Start button covers every move surface (board drag, context menu, task detail,
List view, CLI, agent tools, POST /tasks/:id/move) by construction. The wake is
advisory: it only advances WHEN the poll runs, so every pause, seed-prompt,
dependency, and concurrency gate still applies.
- Admit a todo task whose PROMPT.md is missing instead of dropping it through a
silent `catch {}`. The scheduler KEEPS a candidate whose prompt it cannot read,
so such a card was invisible to planning while still visible to dispatch, with
no log line in either lane. Unreadable (non-ENOENT) prompts now log.
- Route the scheduler's dispatch filter through the shared isUnplannedSeedPrompt
predicate. Its open-coded strict bootstrap compare disagreed with triage on the
refinement-seed shape, leaving hold-release as the only thing between an
executor and a prompt containing just the operator's feedback text. The
predicate also normalizes line endings/trailing whitespace, so a CRLF or
trailing-newline round-trip no longer reclassifies an unplanned card as planned.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "Open" button on a possible-duplicate warning did nothing — the modal
closed and no task appeared. The app has two task deep-link shapes and only
one had a consumer: `?task=<id>` was handled by useDeepLink, while
`#/tasks/<id>` had no `hashchange` listener anywhere in the dashboard.
Five surfaces write the hash form. InlineCreateCard and NewTaskModal write it
unconditionally, so their Open was always dead. QuickEntryBox, Column, and
ListView try an in-memory board lookup first and fall through to the dead hash,
which is why it looked intermittent: duplicate matches come from a
project-wide searchTasks, so a match that is `done` or outside the loaded
board slice misses the lookup and lands on the no-op.
Handle the hash form inside useDeepLink so the app keeps one deep-link
authority owning both shapes, rather than forking a parallel hook. The id is
resolved by fetch instead of an in-memory lookup (that lookup is the dead end
being removed), the hash is cleared via replaceState so re-Opening the same
task fires again, and an unresolvable id toasts instead of failing silently.
Regression tests assert the invariant shared by all five surfaces rather than
the single reported repro: a written hash always resolves to an open or a
toast, never a no-op. Verified against the pre-fix code — 5 of the 6 new tests
fail without this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /system/agents/restart-all constructed its AgentStore from rootDir alone,
with no AsyncDataLayer, so it fell through to the sync SQLite Database path
deleted under VAL-REMOVAL-005 and threw instead of bouncing agents. It now
builds the store against the scoped project's PostgreSQL layer via
requireAsyncLayer, failing loudly when project wiring is incomplete rather
than reading a SQLite shadow. This was the last unmigrated AgentStore call
site; the route test harness lacked getAsyncLayer, which is why nothing
caught it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLI JSON/create success lines write via result() (raw stdout) so quiet mode
cannot drop machine-readable output; capture that seam in research/update/task
tests instead of console.log. Allowlist nested voiceInput settings for the
FN-7505 default-description guard and ship locale keys for Voice Input UI.
A legacy source column (todo/in-progress/...) validated moves only against the
closed VALID_TRANSITIONS map, which cannot know about a workflow-declared
column, so Todo -> Ideas was rejected even though the board drag pre-check and
context menu both offered it. Legacy sources now union VALID_TRANSITIONS with
the task's workflow-resolved adjacency, resolved lazily only when the legacy
table alone would reject. builtin:coding adjacency is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assign context-menu-created tags to their open conversations immediately.
- Return the created chat tag from the chat hook.
- Preserve existing session tags while assigning the new tag through either creation control.
- Cover Enter, Add, existing-tag, and blank-name paths.
- Add a patch changeset for the chat-tag behavior fix.
Files changed:
.changeset/fn-8568-chat-tag-creation.md | 7 ++
packages/dashboard/app/components/ChatView.tsx | 36 ++++++++-
.../__tests__/ChatView.core-interactions.test.tsx | 90 ++++++++++++++++++++++
packages/dashboard/app/hooks/useChat.ts | 4 +-
4 files changed, 133 insertions(+), 4 deletions(-)
Fusion-Task-Id: FN-8568
Fusion-Task-Lineage: 38ee0e02-adbb-4b21-9058-486310da2190
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
- Register AuthenticationSection search entry for anthropicAuthPreference
- Map setting default description + en locale strings for the new control
- Expect executorState paused when enginePaused with zero running tasks
- Replace undefined --space-2xs in ChatView tag menu with calc(--space-xs / 2)
- Stub fetchChatTags/create/rename/delete on streaming-thread api mock for useChat mount
- Expect TaskForm model/tracking callbacks with TaskFormValueChangeMeta source
- Drive remote tunnel lifecycle via one stop then error status (no double Stop race)
Global settings live in settings.json under the resolved global dir, which
falls back to the pre-rename ~/.pi/fusion and ~/.pi/kb dirs for installs that
never migrated. The reader hardcoded ~/.fusion, so on those installs the
operator's preference was silently ignored and resolution fell back to raw-key
precedence -- the same silent fallback the preference exists to remove.
Mirror the legacy-aware lookup getModelRegistryModelsPath already does for
models.json rather than importing core's resolver, which throws under VITEST
when called without an explicit dir.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audit of the class behind the planning bug: createFnAgent forwards no model
unless both defaultProvider and defaultModelId are set, after which
pi-coding-agent picks its own built-in default (anthropic/claude-opus-4-8).
Seven lanes resolved no pair at all, so they hit that path on every call --
a permanent 401 invalid x-api-key for custom-provider and subscription
operators, and a hole in test-mode forcing:
- milestone/slice interviews (no model plumbing at all)
- subtask breakdown, triage and streaming paths
- agent generation
- text refine and goal drafting
- agent reflection (optional ctor pair no production caller supplies)
Two more resolved the halves independently, which the runtime treats as
unset: research synthesis defaults and pr-conflict-resolver's hand-rolled
copy of resolveProjectDefaultModel (which also skipped test-mode overrides).
Add lane-session-model.ts as the shared resolver and a source ratchet that
fails when a dashboard session is constructed from an inline literal with no
model decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An operator can hold a raw Anthropic API key and a Claude subscription OAuth
login at once, and the raw key always won silently. A stale or revoked saved
key therefore shadowed a working subscription and failed every direct Anthropic
call with 401 invalid x-api-key, while both Settings cards still read Active.
Add the global anthropicAuthPreference setting ("api-key" default, preserving
the historical precedence, or "subscription"), read in resolveAnthropicRuntimeApiKey
straight from ~/.fusion/settings.json so it applies without a restart and needs
no settings plumbing through createFusionAuthStorage. Neither value removes a
source: with one credential configured, resolution reaches it either way.
Settings -> Authentication now names the credential in use on the two Anthropic
cards and renders the control, but only when both are actually connected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the hidden long-press/right-click Start menu on Quick Add's Save with a
visible Start button in the action row, rendered only for workflows whose first
visible lane is a hold column (or Coding (Ideas)). Eligibility, the workflow
snapshot, the create-time column override, and the hold-first follow-up move are
unchanged; ineligible workflows render no Start chip or shell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ensureSessionAgent rebuilt the agent with an empty provider/model pair while
preserving draftThinkingLevel, so resumed turns (respond, retry, rewind, drafts
resumed after the in-memory agent was dropped) fell through to the runtime's
built-in default model (anthropic/claude-opus-4-8) and hit api.anthropic.com
with a key the operator never configured. The non-streaming start had the same
hole. Resolve the pair from the persisted draft, then the lane's settings, on
every rebuild and start.
Also route planning through createResolvedAgentSession like chat/executor/merger
so CLI and plugin runtimes can own their own auth and planning emits
session:runtime-resolved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Desktop board scrollers no longer snap: the browser's proximity-snap settle
animation was capturing wheel/trackpad pans and reading as a slow, sticky drag
toward a column center. Base `.board`, `.board-workflow-columns`, and
`.lane-columns` declare `scroll-snap-type: none`; proximity snap is re-declared
in phone-tier media blocks only, where the JS column pager owns paging.
On phones, the hook now owns post-lift motion instead of waiting it out. A
directional lift kills native inertia and animates to its target column via rAF
ease-out (~190-300ms), so the page starts moving on lift rather than after a
native fling that can coast for most of a second. Fling reach is preserved by
deriving a page count (1-3) from release velocity sampled off the board's own
scroll ticks, not from however far inertia happens to travel.
Guards: re-touch cancels the animation and hands the axis back to the finger;
reduced motion, missing rAF, and sub-2px distances fall back to the instant hard
jump; unmount mid-animation restores the frozen inline styles; a fast drag that
rests before lifting is not treated as a flick.
Tap-to-stop-during-momentum is gone as an interaction (no long coast remains to
interrupt). Its regression test is reframed around the equivalent seam: a drag
that interrupts the page animation wins over the pending page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FN-8557 excluded any touch device from mobile mode when its reported physical
screen exceeded the 480px phone threshold, at any CSS width. Large Android
phones report exactly that, so they resolved to tablet: MobileNavBar returns
null off mode "mobile", while MobileNavBar.css still displayed at
(max-width: 768px) — JS and CSS disagreeing about the same device. The board
also fell back to the horizontally-scrollable desktop layout.
No tablet uses a <=600px viewport as its primary layout, so width now
overrides the physical-screen heuristic below that floor; the carve-out
FN-8557 wanted (portrait tablets at the 768 boundary) keeps the 601-768 band.
The floor checks all three width signals the classifier already trusts —
media query, innerWidth, visualViewport — since each can lead on a different
device or update late on rotation. Regression tests cover all three; each
fails against the pre-fix classifier.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A paused engine with nothing running derived executorState "idle", so the
footer badge was indistinguishable from a healthy engine waiting for work.
That is exactly the state a pause settles into once in-flight tasks drain:
triage and planning stall while the board keeps moving, with the pause only
visible by opening the Engine Control menu. Pause state now dominates run
state; the adjacent counters still report throughput.
In the CLI TUI, the global `t` (Git view) branch returned before the Utilities
dispatch in the same key handler, so the advertised "[t] Toggle Engine Pause"
was unreachable dead UI. It now yields when Utilities owns input, and because
the shortcut can stop the board, pausing takes a second `t` within 5s while
resuming stays single-press.
Tests assert the invariant across the whole state matrix (both pause flags x
0/1/5 running), not just the zero-running repro, plus the TUI routing, the
two-press pause, single-press resume, and re-arm behavior.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two auto-save tests waited out the real 500ms debounce via waitFor (up to two
cycles each), unlike the sibling coalesces test which already uses fake timers.
Switch both to vi.advanceTimersByTimeAsync(500) to remove ~1.7s of wall-clock
dead time from the dashboard's slowest feedback-loop suite with identical
coverage (Standing Rule: prefer fake timers over real time waits).
Full Suite shard 4 timed out runTaskShow lock-exhaustion at the default 5s
budget (run 30096660913): each case re-imported the heavy task.js graph under
fake timers, so cold CI workers spent the whole budget on transform and left
unhandled store.getTask / process.exit races after the timeout.
Import task.js once per cached-store describe under real timers via a mutable
store holder, and enable fake timers only around the backoff body. Exhaustion
cases now finish in ~1ms locally while keeping the shipped entry path.
- general: FN-8453 removed the duplicate "Max Triage Concurrent" control
(assert it stays gone); #2400 replaced the per-phase moved-to-workflow note
with the editable "Project workflow model lanes" section.
- scheduling-merge: await the updateSettings call that carries
worktreeCopyFiles instead of assuming calls[0] (CI lane-load ordering).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On GitHub runners headless system Chrome dies at launch without --no-sandbox
and --disable-dev-shm-usage, cascading all five tests as "browser has been
closed" (run 30081843074). The lane had been masked by the shard-4 watchdog
kill since the file landed, so it had never actually executed on CI. Flags
apply only under CI; local launches keep the default sandbox.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nine more api-lane files get the established getProjectScopedPluginMcpServers
stub (includes shared-branch-group-entry-points, whose two current failures
were FN-8491 500s — the historical per-task-derived pair is gone). App lane:
FN-8557 made window.innerWidth<=768 a mobile signal, so leaked innerWidth=375
defineProperty stamps flipped later ListView/settings tests into mobile
layout (reset in beforeEach), and useModalResizePersist's 700px "mobile"
fixture became tablet-class (now a phone-class 375px viewport).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The core watchdog kill had been hiding everything scheduled after it in
shard 4; with the budget fixed, 15 api-lane files surfaced. Thirteen needed
the established FN-8491 recipe (mock stores expose
getProjectScopedPluginMcpServers so the binder short-circuits). Also:
mcp-lane-forwarding tracks resolveManualAiPromptMcpServers' move to
automation-step-execution and FN-8538's getSettings on planning stores;
planning-answered-question-reemit handles PR #2417's synchronous single-turn
admission with a bounded onceAdmitted retry (admission rejections are
side-effect free); routes-system tracks getProjectPluginLoader resolution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-suite run 30077108784 hit an additive insert under CI lane load (group
spliced while merge→end survived) because the toolbar pick ran before the
canvas settled its edge-target state; 5/5 green locally. Same
settle-before-interact class as 5a5796bca; recurrence goes to quarantine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
b2a7425c7 replaced the six-legacy-id whitelist with normalizeColumnId: custom
workflow column ids pass through untouched, only structurally unusable values
(non-string/empty) fall back to triage. The two normalization tests now assert
both halves of that invariant instead of the deleted whitelist behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same drift as chat-commands/mobile: sendMessage gained the {onDelivered,onFailed}
third argument; autosize and draft suites now expect it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- routes-auth/automation/git/github/tasks: FN-8491 binds the plugin-MCP
provider on every project-context resolution; mock stores now expose
getProjectScopedPluginMcpServers so routes stop 500ing.
- routes-github: stub FN-8442 durable planning-claim plumbing whose internal
getSession bypassed the namespace spy; the scoped-store routing contract
is asserted unchanged.
- api-git: POST /planning/create-task now returns the FN-8442
{task, alreadyCreated} envelope; mock matches.
- TaskDetailModal.tab-persistence: settle-then-requery before clicking the
Session tab (detached-node race under CI load).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On some systems the terminal opened blank — buffered prompt present but
never painted (renderer stalled at init: WebGL activation on a zero-sized
canvas or context-loss fallback). Every automatic recovery path fit() but
never refreshed, and fit() with unchanged cols/rows triggers no internal
xterm repaint, so only user input, a font-size change, or a new tab
repaired it. Observer/geometry-driven fits in TerminalModal and
SessionTerminal now always follow fit() with an explicit full-viewport
terminal.refresh(0, rows-1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- remote-access-routes: FN-8491 made every project-context resolution call the
plugin-MCP binder; the mock store now satisfies the runtime-owned-store guard.
- ChatView.mobile: sendMessage gained the FN-8502 {onDelivered,onFailed} arg.
- PlanningModeModal.planning-flow: settle-then-requery before clicking Proceed
(detached-node race under CI load, same class 5a5796bca fixed for Stop/Refine)
and await the create-dispatch mock signal instead of the DOM alone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CLI full-package build's nested @fusion/desktop build stages the
production closure via pnpm deploy (~1300+ packages); on cold-store macOS
release runners that exceeded runWorkspaceCommand's default 10-minute
timeout and killed every v0.73.0-beta.* bun-darwin-arm64 release leg with
exit 143. Raise the desktop sub-build budget to 30 minutes and widen the
build-binaries job timeout to 45 minutes to match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>