Debounce SIGWINCH bursts (50ms trailing edge), wipe the alt-screen before
Ink redraws so shrunk frames don't leave stale rows, and add a 2s dim-poll
fallback for environments that drop SIGWINCH. LogsPanel now reads stdout
rows itself so timer-driven renders always see live dimensions, and the
layoutKey includes a resizeTick so Yoga's cached layout is invalidated
even when cols×rows lands back on the same string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- test(FN-2618): harden process-manager fs/tmpdir mocks
- fix(cli): mark react-devtools-core external in bun compile
- fix(pty): switch to @homebridge/node-pty-prebuilt-multiarch fork
Bun's --compile binary previously crashed at startup because:
1. node:sqlite isn't implemented in Bun 1.3.8 (require returns
undefined; import throws "No such built-in module")
2. ink imports react-devtools-core inside its reconciler; even though
gated by isDev(), the bundled module path failed to resolve at
runtime
Fixes:
- Add packages/core/src/sqlite-adapter.ts: a thin DatabaseSync wrapper
that picks bun:sqlite under Bun and node:sqlite under Node via
createRequire (so the bundler doesn't statically pull in either).
Drop-in for the three core files that import DatabaseSync.
- Install react-devtools-core as a workspace devDependency so it
resolves at bundle time. The dev-only code path is still gated by
DEV=true, so it stays inert in production.
- Revert the prior --external react-devtools-core flag (no longer
needed and was causing a different runtime error).
- Mark node-pty external in tsup so esbuild stops choking on the
homebridge fork's conditional native require()s
(build/Release/conpty.node etc.) when bundling for the npm package.
- Update bundle-output test: the bundle now contains both
bun:sqlite and node:sqlite specifiers (loaded via createRequire).
Verified end-to-end: dist/fn dashboard -p 0 starts cleanly (no PTY,
sqlite, or devtools errors). Core tests 3038/3038, CLI tests 826/826
(up from 822/826 baseline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ink@6 has a devtools.js module that imports react-devtools-core. It is
only loaded at runtime when DEV=true, but Bun's static bundler still
tries to resolve it at compile time and fails with "Could not resolve".
Marking it external lets the compile succeed; the dynamic import path
is never taken in production.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves "Failed to load PTY module" install errors on Linux/macOS by
aliasing node-pty to the homebridge fork, which ships prebuilds for
linux x64/arm64/arm/ia32 across many Node ABIs and uses prebuild-install
for darwin/windows binaries on install.
- Aliased dep so all "node-pty" import specifiers (and vi.mock calls)
keep working unchanged.
- Removed darwin chmod postinstall hack (fork handles permissions).
- Updated cli/build.ts to dynamically resolve node-pty install root and
pick prebuilds by ABI for Linux cross-compile; warn-and-skip for
darwin/windows cross-compile (host-only there, as before).
- Added type shim because the fork's bundled typings declare module
'@homebridge/node-pty-prebuilt-multiarch', not 'node-pty'.
Verified: pnpm install clean, dashboard typecheck clean, native module
loads and spawns shell via fork, host + linux-x64 cross-compile staging
both produce dist/runtime/<plat>/pty.node.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the standalone remoteEnabled setting from CLI, core settings defaults/types, and dashboard settings APIs/UI
- Treat remote access as enabled when an active provider is selected and that provider is configured as enabled
- Update remote auth and engine lifecycle checks to gate on provider activation instead of a global flag
- Adjust tests and add a changeset documenting the remote access configuration simplification
- Add regression tests across CLI, core, dashboard, and engine for remote access auth, settings parity, and serve/TUI callback wiring
- Expand dashboard route and modal coverage for remote settings/auth flows including node environment behaviors
- Redact provider-switch failure details in tunnel process manager to avoid leaking sensitive provider diagnostics
- Update route registration and engine lifecycle tests to lock in remote-access behavior under real execution paths
- Wire remote dashboard route contracts into CLI/TUI state and controller flows
- Add remote settings interaction UX in dashboard and dashboard-tui command paths
- Expand regression coverage for remote TUI behavior in dashboard and app test suites
- Document remote TUI controls and QR behavior in the CLI reference
- Add a changeset for @runfusion/fusion patch release
- Add project-scoped remote settings and control APIs for provider activation, tunnel lifecycle, token generation, URL, and QR retrieval
- Extend Settings modal with a dedicated Remote Access section, provider forms, status/actions, and coverage in SettingsModal tests
- Wire dashboard TUI settings state to remote configuration/status and add interactive remote shortcuts for start/stop/token/url/qr actions
- Document remote-access behavior in architecture, CLI, and settings references and include a patch changeset for @runfusion/fusion
Version bump via changesets (consumed 11 changesets). Forced 0.3.0
instead of changesets' default escalation to 1.0.0 for the pre-1.0
minor bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triage with claude-sonnet-4-6 via pi-claude-cli kept looping on
fn_review_spec calls that were rejected by pi's validator with
"root: must be object". Parameterless MCP tools (schema
{type:"object", properties:{}}) emit zero input_json_delta events,
so partialJson stayed "" and the catch fell through to
finalArgs = "" — a string, which TypeBox's Type.Object({}) rightly
refuses. Default empty partialJson to {} so the call lands.
Also:
- Add a 2-step reminder loop in triage before swapping to the
fallback planning model — primary models that wrote PROMPT.md
but forgot fn_review_spec recover from a nudge, no need to pay
the cold-start tax of a new triage on a different model.
- Inject @runfusion/fusion's own pi extension into dashboard/
daemon/serve sessions and propagate the path to createFnAgent
via setHostExtensionPaths so fn_* tools register globally
without requiring `pi install npm:@runfusion/fusion`.
- Drop the "historical" qualifier from replayed tool labels —
Claude was reading "TOOL RESULT (historical Read):" as
"previous session, ignore" and looping on verification.
- Remove subprocess-lifecycle stderr debug logs that landed for
hang diagnosis — root cause is fixed, the noise can go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add generated engine-tools reference and refresh extension/capabilities docs from source definitions
- Enhance sync-fusion-skill-tools script to verify and maintain skill documentation consistency
- Expand skill-sync tests and enforce sync:fusion-skill:check in the workspace test pipeline
- Add FN-2569 changeset documenting the published @runfusion/fusion patch update
Triage system prompts read "MUST call fn_review_spec()" but Sonnet 4.6 routed
through pi-claude-cli writes PROMPT.md and silently skips the call — even with
the addendum explaining the deferred-tool protocol. Confirmed by the FN-2564
agent log: model called other MCP tools (fn_memory_search, fn_task_list) fine
but consistently never reached fn_review_spec, leaving triage looping on
"fn_review_spec was never called" and falling back to zai/glm-5.1 every time.
Rewrite bare `fn_*` (and any non-built-in custom tool name) references in the
system prompt to their `mcp__custom-tools__fn_*` form before sending. The
prompt now literally says "call mcp__custom-tools__fn_review_spec()" so the
model has no inference step, and the deferred-tool reminder Claude Code injects
matches verbatim. Word-boundary safe (won't touch fn_review_specifier) and
idempotent (won't double-prefix already-MCP-named occurrences).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pi-coding-agent 0.70's createCodingTools preset only includes read/bash/edit/write,
but pi-claude-cli's tool-mapping translates Claude's Glob→find and Grep→grep.
Triage sessions running through Claude CLI hit "Tool find not found" the moment
the model called Glob and looped on the error.
Compose the built-in set explicitly so every tool referenced by tool-mapping.ts
is registered. Read-only sessions also gain ls (was silently dropped earlier
when readonly was redefined upstream).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When users have an external pi-claude-cli (e.g. a global `npm install -g
pi-claude-cli`, or `npm:pi-claude-cli` in ~/.pi/agent/settings.json packages),
pi's extension discovery loaded the upstream copy and shadowed our fork. The
upstream has a once-and-lock MCP-config bug that throws "Extension runtime not
initialized" during early streamSimple calls and never recovers.
Adds reconcileClaudeCliPaths in @fusion/core, used by both the daemon's
extension assembly and the engine's per-session registerExtensionProviders, to
drop any path with a `pi-claude-cli` segment that isn't our vendored fork and
prepend the vendored path. Engine resolves the fork via require.resolve and
gracefully no-ops when it isn't reachable (e.g. embedded standalone usage).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Annotate the intentional NUL-byte regex in log-sink, drop two stale
eslint-disable directives whose underlying any-uses are gone, and swap
@ts-ignore for @ts-expect-error in routes.test.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TUI quit ('q'/Ctrl+C) bypassed signal handlers via process.exit(0), and
neither shutdown path closed the HTTP server, so server.close()'s
stopAllDevServers() listener never ran. In-flight agent bash commands
(spawned detached for their own pgroup) were also never aborted, so
their subprocess trees — including vitest workers — survived as orphans.
Route the TUI quit through SIGINT so the registered shutdown handler
runs, await stopAllDevServers() in both shutdown paths, and abort
in-flight bash on every active agent session at the start of the
runtime drain so killProcessTree reaches every grandchild.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MCP config was generated lazily once and locked, so engine session-scoped
tools (fn_review_spec, fn_review_step) never reached the Claude CLI subprocess
and triage/executor sessions failed with "unknown tool" errors. Now the config
is hashed per call and rewritten when the tool set changes.
Also adds scripts/sync-fusion-skill-tools.mjs to regenerate the SKILL.md
tool-categories block from extension.ts at build time, with a --check mode
wired into skill-sync tests so drift fails CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Derive task elapsed time from columnMovedAt with updatedAt/createdAt fallbacks and guard against invalid or future timestamps
- Render a clock-based timer chip on in-progress and done cards with accessible labeling and tooltip metadata
- Add TaskCard styles for the timer row/chip using design tokens, including mobile-size adjustments
- Expand TaskCard tests to cover visibility by column, invalid timestamp suppression, boundary label formatting, and 30s live refresh cadence
- Route "fn plugin add" to the plugin install handler and update CLI help/usage text to document the alias
- Add bin routing tests that cover plugin add/install parity and updated error/help messaging
- Update getting-started, settings reference, and runtime plugin READMEs to standardize plugin installation examples
- Add a patch changeset for @runfusion/fusion and apply DirectoryPicker/PluginManager design-token styling refinements
TUI: vitest memory-guard threshold and on/off toggle now persist to
global settings (vitestAutoKillEnabled / vitestKillThresholdPct), so
they survive dashboard restarts. Stats panel shows the system-memory
used percentage next to used/free. Utilities panel exposes [+/-] to
adjust the threshold in 5% steps (50–99%).
Release: scripts/release.mjs auto-syncs a root CHANGELOG.md aggregated
from every packages/*/CHANGELOG.md, grouped by version with one
sub-block per package.
Versioning: all private @fusion/* packages joined the changesets fixed
group with the public cli + cli-alias and were aligned to 0.2.5, so
every release bumps every package and produces per-package CHANGELOG
entries that the aggregator picks up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ink/react-reconciler in dev mode emits performance.mark/measure() on every
render. Without a PerformanceObserver, Node retains every entry in the User
Timing buffer forever — ~165 renders/sec in the TUI accumulate ~600MB of
PerformanceMeasure objects + diff strings ("Components ⚛", "Changed Props",
"– children", etc.) over 20-30min, which is what was OOM-crashing long dev
sessions. A 30s interval calling clearMeasures()/clearMarks() drains it.
Also teach scripts/dev-with-memory.mjs to forward --inspect /
--inspect-brk, plus auto-add --heapsnapshot-near-heap-limit=3 so any future
heap-limit OOM auto-dumps. The script now invokes node directly with tsx's
preflight + loader (resolved via require.resolve) instead of going through
the .bin/tsx wrapper, so the inspector attaches to the real app process
instead of a parent wrapper that exits before serving the websocket.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
[k] kills all running vitest processes via pgrep -f. [v] toggles a
sampler-driven guard that SIGKILLs vitest when system memory exceeds
90%, throttled to once per 30s. Auto-kill defaults ON.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add refresh-page actions alongside retry/reinitialize in TerminalModal bootstrap and xterm init error states
- Add terminal error action-group styling so multiple recovery buttons render cleanly
- Extend TerminalModal tests to cover new refresh controls and verify window reload behavior
- Remove unused TaskLogEntry import and drop the stale hooks eslint suppression in dashboard-tui app
Several layered defenses against the layout occasionally rendering
at the wrong height (header pushed off-screen, content too tall).
Still not fully resolved — flag for further debug — but this
combination materially reduces frequency, especially under tmux/ssh.
* Enter alt-screen on start (\x1b[?1049h), leave on stop. Gives the
TUI a dedicated fullscreen surface that doesn't share scrollback,
so frames taller than the viewport clip cleanly instead of
scrolling the header into shell history.
* Controller subscribes to process.stdout 'resize' and calls
inkInstance.clear() — resets log-update's previous-frame line
count so the next render isn't skewed by a stale clear region.
* App-level resize listener bumps a state counter and the root Box
uses key={`${cols}x${rows}`} so React unmounts and remounts the
whole tree on dimension change, bypassing any Yoga layout cache.
* Root Box gets explicit width={cols} + overflow="hidden" so
overflow gets clipped instead of pushing siblings out.
* StatusBar: Text children now wrap="truncate-end" with
flexShrink={0} on the outer Box. Default wrap="wrap" was letting
long hotkey+URL strings wrap to 2 rows, throwing the row budget
off by 1 — the proximate cause of the 151x46 wedge.
* MainHeader outer Box: flexShrink={0} + overflow="hidden" as
belt-and-suspenders.
* Settings + Utilities side-by-side now match heights: UtilitiesPanel
uses flexGrow={1} (was flexShrink={0}), so cross-axis stretch
applies equally.
* StatusModeGrid receives rows as a prop again (single source of
truth from App's render, instead of each child reading its own
useStdout snapshot).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* HelpOverlay used `key={key}` on each shortcut row, but several keys
legitimately repeat across contexts (`[t]` for Git view AND
Toggle engine pause; `[r]` for Refresh stats AND Refresh agent
detail). React threw "Encountered two children with the same
key" and the overlay crashed. Switch to index-based keys with a
comment explaining each row is unique by position.
* Refresh the help text: unified header tabs, Settings/Files/
Agents ←/→ pane swap, Git push/fetch shortcuts, Files hidden-
files toggle, Logs G jump-to-end.
* Main (status mode) header sometimes vanished after a tmux pane
switch and only resize fixed it. Two fixes:
- Remove the `rows < 10` auto-hide in MainHeader. tmux pane
switches can briefly report stale or zero dimensions, and the
transient `return null` orphaned the header.
- Wrap MainHeader and StatusBar in flexShrink={0} boxes inside
StatusModeGrid/StatusModeSingle (matching the prior
InteractiveMode fix) so Yoga can't squeeze them to 0 rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the separate section + interactive tab strips for a single
unified strip: [m] Main [b] Board [a] Agents [g] Settings [t] Git
[e] Explorer. Width tiers tightened to fit every terminal size:
full labels at cols ≥ 90, glyph-only at 50–89, FUSION + active
pill only below 50.
New 'm' shortcut switches to status mode (Main); 's' kept as alias.
Number keys 1–5 still jump to status sections under the hood.
Settings view: ←/→ now switch between list and detail panes,
matching Agents view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yoga's default flexShrink: 1 was letting the MiniLogo and tab pills
shrink when the header row's collective content exceeded the
terminal width — driving FUSION onto a second line. Pin every
fixed-content header child (logo, dividers, tab pills, help-hint) to
flexShrink={0}; the trailing flexGrow filler still absorbs slack.
Belt-and-suspenders: wrap="truncate-end" on FUSION so any future
constraint truncates instead of wrapping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Catch-up commit for the v0.2.1 publish — the prior release script's
git commit step failed silently (allowFail), so the version bumps
and consumed changeset were left uncommitted while the npm publish
still succeeded.
The global "switch to Settings" shortcut matched both g and G, so
uppercase G never reached the logs panel and the vim-style "jump to
end" never fired. Restrict the global handler to lowercase g; G
falls through to the active panel — including the status-mode logs
list (jump-to-newest) and any other scrollable panel that opts in.
Agents view: ← focuses the list pane, → focuses the detail pane,
matching the visual layout. Tab still cycles either direction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- BoardView columns now window their card list against the available
rows so the cursor stays visible. Cards beyond the viewport scroll
in via "↑ N more" / "↓ N more" indicators. Fixes the "can't reach
cards in the Done column" bug — selectedIndex was advancing but the
cards were clipped.
- Logs section: G (vim-style, in addition to End) jumps to newest
entry. Lowercase g stays as the global Settings shortcut.
- MainHeader gains two new collapse tiers: cols < 50 renders just
FUSION + the active tab pills (no inactive tabs, no dividers); rows
< 10 hides the header entirely so the row isn't wasted.
- ExpandedLog stretches to width="100%" so the panel keeps its width
when an entry is expanded — was collapsing to content width before.
- Lighter blue palette throughout (cyanBright / cyan); status-mode
grid widened to 5:6 so Stats panel gets ~45% of cols.
- Stats: heap limit always on its own continuation row; bytes
formatted with a space ("450 MB") so a forced wrap, if it ever
happens, breaks after the unit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch the .agents-view-title flex behavior from wrap to nowrap and apply
white-space: nowrap on the title and its h2, so the Bot icon and "Agents"
label stay on one row and the view-toggle / primary actions push to the
right edge. Drop the dedicated 32px icon-only width override that came from
the previous wrapping layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a Git interactive view (hotkey [t]/[4]): branch + ahead/behind,
recent commits with detail strip, staged/unstaged/untracked files
panel, branches list, worktrees panel, and a [P] push modal with
pre-flight commit list and capture of stdout/stderr. ←→ cycles
status → branches → worktrees → commits → changes; ↑↓ navigates
rows in the focused list. 5s poll while mounted.
- Project-scoped task stats: BoardView pushes its selected project
path into the controller; refreshTUIStats now reads from a per-
project TaskStore via a shared getProjectStore helper. Project
switch triggers an immediate refresh via onBoardScopeChange.
- Unified MainHeader used by status and interactive modes — section
tabs ([1]–[5]) and interactive view tabs ([b]/[a]/[g]/[t]) always
visible; previously the interactive header replaced the main one.
- Logs panel keeps its size when expanding a single entry.
- 'f' cycles severity filter from any panel in status mode.
- BoardView: explicit cross-view shortcuts so g/a/t always switch
views regardless of input-handler ordering.
- BoardView column overlap fix: flexShrink={0} on structural rows.
- TaskCard always shows the title with wrap="wrap"; single short-id
pill, no duplicate id-as-title fallback.
- Stats panel reorg: StatRow helper, bold section headers, narrow-
width wrap for Heap and Memory trailing fragments.
- Lighter blue palette: cyanBright fg accents / cyan active bg /
logo gradient whiteBright → white → cyanBright → cyan → blue.
- System stats sampler: RSS, heap (V8 limit-aware color), external,
CPU%, load avg, system used/free. Heap thresholds scale off
--max-old-space-size automatically.
- Test fixture updated for the new InteractiveData.git block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the Colossal "888" large-logo variant with a vertically doubled
ANSI Shadow render so the bigger splash keeps the same block-letter
aesthetic as the small variant — the prior variant looked stylistically
inconsistent next to the small block letters. 47 cols × 12 rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>