feat(workspace): offer to install the engine a bound workspace needs

Merged
RA
2mo ago
+1780-52

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.