feat(workspace): offer to install the engine a bound workspace needs
Issue for this PR
Closes #1157
Type of change
- Bug fix
- New feature
- Refactor / code improvement
- Documentation
What does this PR do?
Stacked on #1168 (which stacks on #1167) — this PR is the commit on top. It is the install offer from #1158 restacked onto the overlay attach; the dialog and its rules are unchanged, the refusal it substitutes is now the overlay's.
A bound workspace whose declared tools need the local engine used to report a missing engine as a 10-second toast with a command in it. This replaces that with an offer: Install now / Copy command / Not now. The install only ever runs from an explicit choice — the attach never installs anything on its own.
How the offer reaches the TUI: the plugin runtime loads plugins in a separate module realm, so nothing in-process can hand it the offer. It is published on the event bus, which is what toasts already use, and since that event carries no payload the plugin re-derives the detail from the binding on disk and the engine on PATH (describeOffer, state-free by design). After a successful install the plugin does not reconcile anything itself — it cannot reach the server's module — and does not need to: the turn boundary looks for a missing engine on PATH again every turn, so the engine attaches on the next message.
Deliberate details, carried over from the original:
- One select for every phase, with sentinel rows for installing/installed/failed; swapping the top-level component would drop focus.
- Install now only appears when Node 20+ and npm are on PATH; otherwise the command is shown with the reason it cannot be run for you.
- Copy command reads the clipboard back before claiming success; the host's writer swallows backend failures.
- Not now latches 7 days, keyed on the workspace id, scoped by tenant and API URL.
- Headless run prints one stderr line instead; stdout stays reserved for JSON events. An attached run renders the notice in its own event loop.
- Both too-old exits route through the offer. The install has a real deadline via an abort signal, reports its result even if the dialog was dismissed while npm ran, and only one offer is raised at a time.
- A zero exit from npm is not trusted: the installed engine must be discoverable on PATH and clear the version floor.
- The headless marker is stripped from the environment of nested entrypoints started by the bash tool, so a child with a TUI still gets the dialog.
How did you verify your code works?
bun run typecheck clean. New offer suite (routing to surface / bus / toast / headless line, too-old with the found version, healthy engine never offered, describeOffer re-derivation, stderr-not-stdout, install deadline via abort signal, install success verified not assumed) plus the plugin suite; 357 tests pass across the workspace, plugin, precedence and MCP lifecycle suites on this stack.
End-to-end rows from #1158 (dialog with all three options and a live declared count; Install now into an isolated prefix with tools on the next message in the same session; Not now suppresses and returns after the latch ages; no Node hides Install now; headless prints exactly one line) are re-run on this stack and recorded in the review-log comment below before this leaves draft.
Not verified here: the successful clipboard path (this host has no clipboard backend); Windows (npm.cmd follows existing precedent in this repo).
Screenshots / recordings
Terminal UI; captured pane output will accompany the E2E rows on the review-log comment.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
Summary by cubic
Replaces the toast that printed an install command for a workspace with a missing or too-old engine with an interactive offer: Install now / Copy command / Not now. The install runs only from an explicit choice, and the installed engine attaches on the next message without a restart.
Offer behavior
- Install now appears only when Node 20+ and npm are on PATH; otherwise the command shows with the reason it cannot run.
- Copy command reads the clipboard back before claiming success.
- Not now latches 7 days per workspace, scoped by tenant and API URL; past the latch the offer is re-raised hourly, and a clock step backward re-raises too.
- The dialog waits for the TUI's persisted settings to load (held up to six minutes) before checking the latch, so a restart can't re-offer inside a still-active silence.
- Headless runs print one stderr line per verdict per process, keyed by workspace id so same-named workspaces don't suppress each other; stdout stays reserved for JSON events, and an attached run prints only the offer raised for its own session.
- The tool count is optional: when the lookup fails the text drops the number instead of printing 0, and a count that arrives on a later turn doesn't re-raise the offer.
- npm's zero exit is not trusted — the installed engine must be on PATH and clear the version floor; a binary that reports no version is flagged for a manual check, and only a real older one still first on PATH is named as shadowing the new one.
- The install runs with a hard deadline over npm's whole process tree (SIGTERM, then SIGKILL, which stays armed after npm exits), and a descendant a lifecycle script leaves behind is reaped the same way after a successful exit without holding the result. No new offer is raised while an install is in flight, so a second explicit install can't start concurrently.
- A too-old engine is re-probed as soon as the file on PATH changes identity (inode, size, mtime, ctime), so an in-place update that keeps the length and restores the timestamp still attaches on the next message.
- The offer dialog hides the filter box, so keystrokes during the install reach the prompt instead of collecting in the dialog.
- The bash tool strips the headless marker from nested entrypoints so children with a TUI still render the dialog.
Summary by CodeRabbit
-
New Features
- Added automatic detection of missing or outdated workspace engines.
- Added installation offers with install, copy-command, and “Not now” options.
- Offers appear in the interactive interface, notifications, and session-specific headless output.
- Added installation verification and clearer failure reporting.
- Added controls for hiding dialog filters when appropriate.
-
Bug Fixes
- Workspace engine changes are detected sooner, including in-place binary updates.
- Duplicate installation notices are suppressed, with seven-day skip controls and improved retry timing.
- Unknown tool counts no longer display as zero, with corrected singular and plural wording.