Skip to content

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

Draft
ralphstodomingo wants to merge 5 commits into
feat/workspace-precedencefrom
feat/workspace-install-offer
Draft

feat(workspace): offer to install the engine a bound workspace needs#1158
ralphstodomingo wants to merge 5 commits into
feat/workspace-precedencefrom
feat/workspace-install-offer

Conversation

@ralphstodomingo

@ralphstodomingoralphstodomingo commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes#1157

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

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 — attach still never installs anything on its own.

The part worth reviewing is how the offer reaches the TUI. I first registered a handler with the attach module from the plugin. It typechecked, unit-tested green, and did nothing in a real TUI — the user got the old toast, never the dialog. The plugin runtime loads plugins in a separate module realm, so the instance the plugin imports is not the one attach consults; a globalThis key failed likewise. The offer is therefore published on the event bus, which is what toasts already use, and since that event carries no payload the plugin re-derives the detail itself.

Deliberate details:

  • One select for every phase, with sentinel rows for installing/installed/failed. Swapping the top-level component between states drops focus and loses the phase signal, and sentinel rows avoid the disabled flag, which the filter drops.
  • Install now only appears when Node 20+ is 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, so a resolved write is not proof — on a box with no xclip it reported a copy that never happened.
  • Not now latches 7 days, keyed on the workspace id so a rename does not reset it, scoped by tenant and API URL.
  • Headless run prints one line to stderr instead, since a toast cannot render there and stdout is reserved for JSON events.
  • Both too-old exits route through the offer, including a reused entry below the floor.
  • The install has a real deadline, reports its result even if the dialog was dismissed while npm ran, and only one offer is raised at a time — attach re-probes each turn, and a second dialog could otherwise replace an installing one and start a concurrent global install.

How did you verify your code works?

Unit 311 pass / 0 fail on the workspace and plugin suites; full suite green apart from one failure already red on the base. Typecheck clean, lint at baseline on every file touched.

E2E against a real bound workspace with the engine absent from PATH: dialog with all three options and a live declared count; Install now installs into an isolated prefix and tools appear on the next message in the same session; Not now suppresses, and returns once the latch is aged past 7 days; no Node hides Install now; headless prints exactly one line across 3 identical runs.

Not verified: the successful clipboard path — this host has no clipboard backend, so only the "could not confirm" branch ran. Not verified: Windows — the npm shim fix follows existing precedent in this repo rather than a test, and deserves a check before release.

Round-by-round review log (five rounds, seven findings — six fixed here, one routed to the PR below; three were regressions from my own earlier fixes), the second capture, and detailed evidence: see the "Codex review log" comment on this PR.

Screenshots / recordings

Terminal UI, so this is captured pane output rather than an image. Workspace name redacted.

 ┃
┃ hi Workspace "Integrations Demo" declares esc
┃ 52 integration tools, which need the local engine
· npm i -g @altimateai/datamate@0.7.0
▣ Builder · Altimate LLM Gateway
Search
● Install now Runs npm i -g @altimateai/datamate@0.7.0
Copy command Copy the install command to your clipbo
Not now Won't ask again for this workspace for 7 day

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@ralphstodomingoralphstodomingo self-assigned this Aug 26, 2026
@coderabbitai

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
auto_review:
drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ralphstodomingo

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:45e30dece9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

function printLine(line: string): void {
if (syncInternals.printLine) return syncInternals.printLine(line)
try {
process.stdout.write(line + "\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve JSON framing for headless engine notices

When run --format json is used in a bound workspace with a missing or outdated engine, this writes a human-readable line directly to stdout, while run.ts promises raw JSON events and serializes its other stdout records through emit(). The resulting stream contains a non-JSON line, breaking line-oriented JSON consumers and CI automation; emit a structured JSON record in JSON mode or send this notice to stderr.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 0d942e6.

Reproduced before changing anything: run --format json in a bound workspace with no engine on PATH produced 5 stdout lines, of which line 1 was the plain notice and the other 4 were valid JSON. So the framing break is real, not theoretical.

Fixed by sending the notice to stderr rather than emitting a JSON record. It is a status notice rather than run output, which is the same reason run already writes its own > builder · <model> status line to stderr — so stderr is correct in both formats, not just a JSON-mode workaround. JSON stays parseable, and a terminal user still sees the notice in either format.

Verified after: JSON mode is 4/4 valid JSON on stdout with the notice on stderr once; text mode still prints it exactly once. Added a regression test that drives the real printer (not the seam) and asserts the line reaches stderr and never stdout.

@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-precedence branch from 808282a to 83d0636CompareAugust 26, 2026 18:23
@ralphstodomingo

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:0d942e6b34

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const spec = installSpec()
if (syncInternals.install) return syncInternals.install(spec)
return new Promise((resolve) => {
execFile("npm", ["i", "-g", spec], { timeout: 300_000 }, (err, _stdout, stderr) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invoke npm through a Windows-compatible launcher

On Windows, a normal Node installation exposes npm as a command shim rather than an npm executable, so execFile("npm", ...) fails with ENOENT when the user selects Install now, despite the Node-version gate succeeding. The existing install path in packages/opencode/src/lsp/server.ts:213-215 already handles npm as platform-specific; use a Windows-compatible invocation such as cmd.exe /c npm.cmd here as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in 1babc37.

The repo already agrees with you: lsp/server.ts:213 does process.platform === "win32" ? "npm.cmd" : "npm" for exactly this reason. execFile spawns no shell, so the bare name would ENOENT on the one platform where the Node gate had just told the user they were good to go.

Fixed via the house Process.run helper with the same platform split rather than cmd.exe /c. Two reasons: it matches the existing precedent in this codebase, and Process.run takes an argv array, so an install spec containing spaces — the local tarball path E2E uses — needs no quoting. (npm.cmd through cmd.exe /c would have needed care there.)

Re-ran the install E2E on Linux after the change since the mechanism moved: dialog to "Install now" to engine 0.7.0 installed into an isolated npm prefix, success toast, tools on the next message. Windows itself is unverified — I have no Windows host — so this rests on matching the existing precedent rather than on a test.

Comment on lines +106 to +112
// altimate_change — carries the fail-open notice when the target could not be
// attributed to the workspace; a no-op otherwise.
return Precedence.annotate(precedence, {
title: `SQL: ${args.query.slice(0, 60)}${args.query.length > 60 ? "..." : ""}`,
metadata: { rowCount: result.row_count, truncated: result.truncated },
output,
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve precedence notices on failed local calls

When precedence returns an undetermined fail-open verdict—such as a default dbt adapter whose type cannot be identified—this annotation is applied only to the successful result. If local execution then throws, the catch path returns an unannotated error, losing both the user-facing reason that routing was skipped and the precedence telemetry marker. The same omission exists in the failure paths of sql-explain.ts and schema-inspect.ts; annotate failure results too so the promised non-silent fail-open behavior survives execution errors.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed as a real gap, but this one is not mine to fix and I am routing it rather than touching it.

sql-execute.ts, sql-explain.ts and schema-inspect.ts are precedence code from the PR below this one in the stack (#1156), not the install offer. This PR changes engine-sync.ts, the workspace TUI plugin, and one env marker in run.ts; it adds no annotation and no precedence path. Fixing it here would put a precedence change in an install-offer PR and split ownership of that code across two PRs.

I have passed it to the session that owns #1156 with your reasoning intact: the undetermined fail-open verdict is annotated only on the success path, so a throw from local execution returns an unannotated error and loses both the user-facing reason routing was skipped and the precedence telemetry marker — and the same omission is in the failure paths of the other two tools.

Flagging for whoever reads this thread: if #1156 lands the fix, it arrives here through the stack rather than as a commit on this branch.

@gitguardian

gitguardianBot commented Aug 26, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@github-actions

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-precedence branch from 83d0636 to e772cb7CompareAugust 26, 2026 18:47
@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-install-offer branch 2 times, most recently from 6ab3731 to 3caa27fCompareAugust 26, 2026 19:01
@ralphstodomingo

ralphstodomingo commented Aug 26, 2026

Copy link
Copy Markdown
ContributorAuthor

Codex review log

Kept out of the PR description so the body stays inside the repo's 5,000-char limit. Edited in place as rounds land.

Round 1 — 1 finding

FindingSeverityVerdictAction
Headless notice written to stdout breaks run --format json's raw-JSON streamP1ConfirmedFixed

Reproduced first: run --format json with no engine on PATH gave 5 stdout lines, line 1 being the plain notice and the other 4 valid JSON. Fixed by sending the notice to stderr rather than emitting a JSON record — it is a status notice, which is why run already writes its own status line there, so stderr is right in both formats rather than a JSON-only workaround. After: 4/4 valid JSON with the notice on stderr; text mode unchanged. Regression test drives the real printer, not the seam.

Round 2 — 2 findings

FindingSeverityVerdictAction
execFile("npm", …) ENOENTs on Windows, where npm is a .cmd shim and nothing spawns a shellP2ConfirmedFixed
Fail-open precedence verdict annotated only on the success path; a throw loses the reason and the telemetry markerP2Confirmed, not this PRRouted to the precedence PR

The npm fix follows the split already in this repo rather than the suggested cmd.exe /c: it matches existing precedent, and the helper takes an argv array so an install spec containing spaces needs no quoting. Windows itself is unverified — no Windows host here, so it rests on precedent, not a test.

The second finding is precedence code from the PR below this one in the stack. Codex reviews the whole branch diff, so those files are in its view; fixing them here would have split ownership of that code across two PRs. It was routed, confirmed real, and found to be wider than reported — six unannotated exits rather than three — and is fixed on that branch.

Round 3 — 2 findings, both mine

FindingSeverityVerdictAction
Process.run's timeout does not enforce a deadline without an abort signal; a stalled install hangs the dialogP2Confirmed — a regression from round 2Fixed
The headless marker leaks into bash-tool child environments, so a nested entrypoint prints the notice instead of showing the dialogP2ConfirmedFixed

The first was mine to introduce: the original used a call whose timeout kills the child, and moving to the shared helper for the Windows fix silently lost that, because the helper consults its timeout only inside its abort handler as the grace before SIGKILL. Measured rather than reasoned about, same helper and an 8s sleep — timeout alone ran 8004ms, an abort signal killed at 502ms. A stalled install now reports that it did not finish, and the install E2E was re-run afterwards to confirm a normal install is unaffected.

The second has an exact precedent: the bash tool already strips the non-interactive marker from child environments for the same reason, and the headless marker was not being stripped alongside it.

Review audit

Re-audited rather than assumed closed, since a verdict can arrive as inline comments, a review body, or a reaction, and inline comments can land after the body. On this PR: 5 inline findings across 3 rounds, every review body boilerplate with no findings inside, no reaction-only verdict, and 5 replies — one per finding. 4 fixed here, 1 routed to the precedence PR and fixed there.

A known divergence, stated deliberately

describeOffer() describes current state rather than echoing the decision that raised the offer, and the two can differ: the attach layer's reuse branch reports "too old" using a reused entry's version, and that branch only fires when PATH has nothing better — so this function, probing PATH alone, may describe the same situation as "missing". That is deliberate and documented in the source. The user's actionable state is "no usable engine, run this command", and the command is identical either way; describing live state also stays correct when upstream conditions change, which is where describing code otherwise keeps asserting what used to be true.

Round 4 — 1 finding

FindingSeverityVerdictAction
Install result is never reported if the dialog is dismissed mid-install; success also clears the dialog stack unconditionallyP2ConfirmedFixed

Escape or a click outside dismisses the offer while npm keeps running. The failure path set signals on an unmounted component, so an npm error — or the five-minute timeout added in round 3 — reached nobody. That is the worse half: the timeout exists to say the install gave up, and it was silent in exactly the case where the user had stopped watching. Success also cleared the dialog stack unconditionally, which would close a dialog the user had opened since.

Fixed by tracking mount state: completion reports through a toast when the dialog is gone, carrying the error and the command to run by hand, and the dialog is cleared only while this offer still owns it. Verified in a real TUI — Install now, Escape while npm ran, no dialog rows left, install completed and the result still surfaced. That path produced nothing at all before.

On reading the verdict. The reaction on the summon comment was 👀, not 👍 — "looking", not "nothing found" — and the finding arrived about three minutes after it. Reading the reaction as a verdict would have produced a "round 4: no findings" report with a real P2 sitting unaddressed. Surfaces were compared by identifier before and after the summon rather than by count, and watched past the first signal.

Round 5 — 1 finding

FindingSeverityVerdictAction
The single-offer guard is check-then-act: the latch is read, three awaits follow, and only then is it setP2ConfirmedFixed

Two dispatches arriving inside that window both pass, and the failure mode is worse than the bug the guard was written for — a second dialog replacing an installing one can start a concurrent global npm install. The slot is now reserved before the first await and released on the suppressed and failed paths, and each raise carries an ownership token so a superseded dialog tearing down cannot free a slot the newer one holds.

No unit test for the interleaving itself: driving two concurrent dispatches through the plugin surface would have been less convincing than the structural change, so that is stated rather than implied.

A bug found by testing rather than by review

Between rounds 4 and 5, the install → next-message row stopped completing on this base. It looked like an input-delivery flake, and had it been reported that way it would have been wrong. Capturing the pane instead showed a second offer dialog, in its idle phase, sitting over the session after the install finished — attach re-probes a repairable failure every turn, so the offer was being raised again mid-install, replacing the "Installing…" dialog and swallowing keystrokes into its own filter. That produced the single-offer latch, which round 5 then correctly identified as racy.

Worth recording because the earlier bases passed this row: the retry behaviour that makes the re-raise likely arrived underneath this PR, so evidence gathered before it proved nothing about after it.

Second UI capture — no Node on PATH

Install now is absent and the reason is shown:

 ┃
┃ hi Workspace "Integrations Demo" declares esc
┃ 52 integration tools, which need the local engine
· npm i -g @altimateai/datamate@0.7.0 · (needs
▣ Builder · Altimate LLM Gateway Node 20+ to install — Node was not found on PATH)
Search
● Copy command Copy the install command to your clipbo
Not now Won't ask again for this workspace for 7 day

Verification detail

Unit 262 pass / 0 fail on the workspace + plugin suites; full test/altimate/ green apart from two failures already red on the base. Typecheck clean, lint at baseline on every file touched.

E2E against a real bound workspace with the engine absent from PATH, headless runs with stdin closed: dialog with all three options and the live declared count; Install now installs into an isolated prefix and the tools appear on the next message in the same session; Not now suppresses and returns after the latch is aged 8 days; no Node hides Install now; headless prints exactly one line, identical across 3 runs.

Verification on the merged base

Re-run of the install row on 3caa27f54 (this branch on the precedence head), because the base moved several times and evidence from an earlier base proves nothing about this one. Isolated scratch project with its own state dir; the engine genuinely absent from PATH; stdin closed on headless runs.

  • Turn 1 → dialog with all three options, Install now selected, command shown.
  • Install now → "Installing… this can take a minute." sentinel row renders, install completes, engine reports 0.7.0 in the isolated prefix.
  • Turn 2, same session → 12 datamate_* tools listed, ~9s after submit. No restart needed.

That last point is why the post-install message says tools attach on your next message rather than your next session: the attach re-probes a repairable failure each turn, so the engine installed here is picked up without restarting.

Separately, the attach behaviour underneath this was verified on a clean build of the attach branch with none of this PR's changes applied — engine absent at turn 1, installed by hand mid-session, tools present at turn 2 — so the repair is the attach layer's, not something this dialog papers over.

Re-verified on the final base

The base moved once more (precedence round 5). Rather than carry the previous result forward, the install row was re-run on this head: dialog with all three options, Install now completes, engine reports 0.7.0 in an isolated prefix, and datamate_* tools are present in the same session on the next message.

The number of tool names visible differed between runs (12 earlier, 5 here). That is a screen capture of a reply still streaming, not a measurement — the assertion is that engine tools are present in the same session after installing, which held on both. Nothing in this PR affects how many tools a workspace serves.

This PR adds no permission surface: it registers no model-facing tool, redirects no call, and changes no ruleset. Its only outputs are a dialog, a toast, a stderr line, and an npm invocation made on an explicit choice — so the redirect-across-a-permission-boundary trap that shaped the precedence work does not apply here.

@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-install-offer branch from 3caa27f to 881cb9eCompareAugust 26, 2026 19:18
@ralphstodomingo

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:881cb9ed3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if (syncInternals.install) return syncInternals.install(spec)
const npm = process.platform === "win32" ? "npm.cmd" : "npm"
try {
const result = await Process.run([npm, "i", "-g", spec], { timeout: 300_000, nothrow: true })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass an abort signal to enforce the install timeout

When npm stops making progress, this timeout does not terminate it: Process.spawn only uses that option as the grace period after its abort() handler is invoked, while this call supplies no abort signal. Consequently installEngine() can remain pending indefinitely and leave the dialog stuck on “Installing…” rather than failing after five minutes; provide a timed abort signal or implement an actual execution deadline.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in ce6cb79 — and this was a regression I introduced, not a pre-existing gap.

The original used execFile, whose timeout does kill the child. I moved to Process.run for the Windows shim fix in the previous round and silently lost the deadline, because Process.spawn consults timeout only inside its abort handler, as the grace period before escalating to SIGKILL. With no signal supplied that handler never runs.

Measured it rather than reasoning about it — same helper, same options, an 8 second sleep:

  • { timeout: 500 } alone: 8004ms (ran to completion, no deadline)
  • { abort: AbortSignal.timeout(500) }: 502ms

Fixed with a real abort signal, and a stalled install now reports "npm did not finish within 5 minutes" rather than leaving the dialog on "Installing…". Re-ran the install E2E afterwards to confirm the abort signal does not disturb a normal install: dialog to Install now to engine 0.7.0 in an isolated prefix, unchanged. Regression test added with the measured numbers in the comment so the next person does not repeat the swap.

// a dialog or a toast, so the workspace engine offer degrades to a single
// printed line. An env var because it must be readable from every module
// realm (the TUI plugin runtime loads plugins in its own).
process.env["ALTIMATE_CODE_HEADLESS"] = "1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip the headless marker from tool child environments

When a run session launches another altimate-code entrypoint such as a TUI or serve through the bash tool, that child inherits this process-wide marker because bash.ts strips ALTIMATE_NON_INTERACTIVE but not ALTIMATE_CODE_HEADLESS. The nested server then makes isHeadless() true, so offerOrNotify() bypasses TuiEvent.CommandExecute and writes the engine notice to stderr instead of showing the interactive install dialog; remove this marker from tool child environments just like the existing non-interactive marker.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in ce6cb79.

Verified the precedent you point at: bash.ts:178 already does delete mergedEnv["ALTIMATE_NON_INTERACTIVE"], with a comment explaining that a child spawned by the bash tool may itself be a server-mode entrypoint that needs the path the parent disabled. ALTIMATE_CODE_HEADLESS is exactly the same class of marker and was not being stripped, so a nested entrypoint launched from a run session would inherit "headless" and print the engine notice to stderr instead of showing the install dialog.

Fixed alongside the existing delete, with a comment tying the two together so they stay in step. Good catch — the marker only exists because the offer needs to know it has no TUI to render into, and the one case where that inference is wrong is precisely a child process that does have one.

@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-install-offer branch 2 times, most recently from ce6cb79 to 03f1218CompareAugust 26, 2026 19:47
@ralphstodomingo

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:03f121823b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1283 to +1285
if (!result.ok) {
setFailure(result.error)
setPhase("failed")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report install results after the dialog is dismissed

When a user selects Install now and then dismisses the dialog with Escape or an outside click while npm is still running, the install continues but this failure path only updates signals belonging to the now-unmounted component, so an npm error or five-minute timeout is never shown. A successful completion can also call dialog.clear() after another dialog has opened. Keep the operation's completion handling outside the dismissible component, or emit result toasts and clear only when this offer still owns the dialog.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in the head above. Both halves were real.

Failure path: setFailure / setPhase after dismissal update signals on an unmounted component, so an npm error — or the five-minute timeout added in the previous round — reached nobody. That is the worse half, because the timeout exists precisely to tell the user the install gave up, and it was silent in exactly the case where they had stopped watching the dialog.

Success path: dialog.clear() ran unconditionally, so if the user had opened another dialog while npm ran, we would have closed theirs.

Fixed by tracking mount state with onCleanup: completion reports through a toast when the dialog is gone (carrying the error and the command to run by hand), and the dialog is cleared only while this offer still owns it.

Verified in a real TUI rather than by reading: Install now, Escape while npm was still running, dialog gone with no rows left — the install completed and the result still surfaced, and the engine landed in the isolated prefix. Before this change that path produced nothing at all.

@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-precedence branch from 7fef85c to 19cb5e5CompareAugust 26, 2026 20:07
@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-install-offer branch 2 times, most recently from 6cc2b33 to ff9b5b6CompareAugust 26, 2026 20:30
@ralphstodomingo

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:ff9b5b6ef2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1407 to +1408
if (engineOfferVisible) return
const offer = await describeOffer(api.state.path.directory)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve the offer slot before awaiting discovery

When two sessions or repair retries dispatch this command close together, both handlers can pass this guard while the first is awaiting describeOffer, credential lookup, or Node detection. The later handler can then call dialog.replace() after the user has started installing from the first dialog, replacing the installing state with a fresh idle offer and allowing a second concurrent global npm install; cleanup of the first dialog can also reset engineOfferVisible while the second remains mounted. Set the in-flight latch before the first await and release it when discovery is suppressed or fails.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed in the head above. You are right, and the guard I added a commit earlier was worse than I thought.

Check-then-act: the latch was read, then describeOffer, the credential lookup and Node detection all awaited, and only then was it set. Two dispatches arriving inside that window both pass. And as you note, the failure mode is worse than the one the guard was written for — a second dialog replacing an installing one can start a concurrent global npm install, where the original bug merely swallowed keystrokes.

Fixed by reserving the slot before the first await and releasing it on the suppressed and failed paths. The teardown race you flagged is handled with an ownership token rather than a bare boolean: each raise takes a generation number, and a dialog releases the latch on cleanup only if it still owns it, so a superseded dialog tearing down cannot free a slot the newer one holds.

Verified end to end after the change on an isolated project with the engine absent from PATH: exactly one offer on screen, install completes, no dialog rows left afterwards, the next message reaches the prompt, and engine tools attach in the same session. No unit test for the interleaving itself — I did not find a way to drive two concurrent dispatches through the plugin surface that would be more convincing than the structural change, so I am flagging that rather than implying coverage I do not have.

@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-precedence branch from 19cb5e5 to 8740567CompareAugust 26, 2026 20:40
Replaces the transient toast for a missing or too-old engine with Install
now / Copy command / Not now. The offer reaches the TUI on the event bus,
since plugins load in a separate realm; the plugin re-derives detail via
describeOffer(). Headless run prints one line to stderr so --format json
stays parseable. Install runs only from an explicit choice.
ralphstodomingo added 4 commits August 27, 2026 04:41
Process.spawn consults `timeout` only inside its abort handler, as the grace
before SIGKILL, so with no signal there was no deadline and a stalled npm left
the dialog on "Installing..." indefinitely. Measured: an 8s sleep ran 8004ms
under `timeout`, 502ms under an abort signal.
The bash tool now also strips ALTIMATE_CODE_HEADLESS from child environments,
as it already does for ALTIMATE_NON_INTERACTIVE.
Escape or a click outside dismisses the offer while npm keeps running. The
failure path only set signals on the unmounted component, so a failed install
or the five-minute timeout was completely silent; success also cleared the
dialog stack unconditionally, which would close whatever had opened in its
place. Completion now reports through a toast when the dialog is gone, and
only clears a dialog this offer still owns.
Attach re-probes a repairable failure every turn, so the offer could be raised
again while one was still up. Mid-install that replaced the "Installing..."
dialog with a fresh idle one, which swallowed keystrokes into its own filter —
observed end to end: after a successful install, typing never reached the
prompt. The offer is now latched while one is on screen.
The single-offer guard checked the latch and then awaited three times before
setting it, so two dispatches arriving close together could both pass — worse
than the bug it fixed, since the second dialog can replace an installing one
and start a concurrent global npm install. The slot is now reserved before the
first await and released if discovery is suppressed or fails, and only the
raise that owns the latch may free it.
@ralphstodomingo
ralphstodomingoforce-pushed the feat/workspace-install-offer branch from 40308bc to 8992318CompareAugust 26, 2026 20:43
@ralphstodomingo

Copy link
Copy Markdown
ContributorAuthor

@codex review

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:89923187e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Release the single-offer latch however this dialog goes away — chosen,
// dismissed, or replaced — but only if this dialog still owns it. A
// superseded dialog tearing down must not free a slot the newer one holds.
if (engineOfferGeneration === props.generation) engineOfferVisible = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the install latch held after dialog dismissal

When the user selects Install now, dismisses the dialog while npm is still running, and then sends another message, this cleanup releases the only global offer latch even though runInstall() continues in the background. The repair retry can consequently open a fresh offer and allow a second npm i -g against the same global prefix. Fresh evidence beyond the previously fixed concurrent-offer race is this dismissal path explicitly setting engineOfferVisible to false before the install promise settles; keep a separate install-in-flight latch until that promise completes.

Useful? React with 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ralphstodomingo