Skip to content

fix(daemon): report Deliver-to-editor launch failures instead of swallowing them#3952

Open
maxmilian wants to merge 2 commits into
nexu-io:mainfrom
maxmilian:fix/3871-handoff-launch-error
Open

fix(daemon): report Deliver-to-editor launch failures instead of swallowing them#3952
maxmilian wants to merge 2 commits into
nexu-io:mainfrom
maxmilian:fix/3871-handoff-launch-error

Conversation

@maxmilian

@maxmilian maxmilian commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #3871

Why

A user reported (Discord #中文区) that clicking Deliver to VS Code does nothing — no handoff and no error. I traced the handoff path and it matches @lefarcen's diagnosis on the issue: POST /api/projects/:id/open-in reports success for a launch the OS actually refused, so the front-end never has anything to surface.

In apps/daemon/src/routes/host-tools.ts the route spawned the editor detached, attached an error listener that swallowed the failure, and then sent { ok: true } synchronously — before the child's error event can even fire (it arrives on a later tick). The web button only shows an error on a non-OK response (openProjectInEditor throws on !resp.ok, HandoffButton renders it via setError + role="alert"), so a refused launch is silently reported as success.

What users will see

When a Deliver-to-editor launch is refused by the OS (e.g. the binary passed the PATH probe but the OS blocks it — quarantine, EACCES), the handoff menu now shows an actionable inline error (Failed to launch <Editor>: <reason>) instead of appearing to do nothing. The success path is unchanged.

No UI was added — this reuses the existing HandoffButton error surface; the fix is daemon-side.

Surface area

  • Default behavior change — the /api/projects/:id/open-in endpoint now returns a non-OK error when the OS refuses the launch (previously it always returned { ok: true }). This is the bug fix itself; the success response shape is unchanged and no packages/contracts types changed (the failure reuses the existing API error envelope).
  • UI / Keyboard shortcut / CLI / API contract shape / Extension point / i18n / New dependency

Screenshots

N/A — no new UI. The error is rendered by the existing HandoffButton alert surface; only the daemon's failure-reporting changed.

Bug fix verification

  • Red-spec test path: apps/daemon/tests/host-tools-routes.test.tshost tools launch reporting (#3871).
  • Did it go red on main and green on the branch? Yes. The fix extracts the spawn into a named helper, launchHostTool, whose invariant is "resolve { ok: false } when the OS refuses the launch." The red spec spawns a non-existent binary and asserts ok === false; the old code path had no failure-reporting (it swallowed error and always returned ok: true), so the spec fails without this change and passes with it. A companion test asserts the success path still reports ok: true.
  • Why a helper-level spec rather than an HTTP-boundary e2e: forcing a real spawn failure through the full tools-dev smoke daemon isn't deterministically arrangeable without mocking the OS spawn (an editor would have to pass the PATH probe yet fail to launch). Per AGENTS.md → Bug follow-up workflow ("let the fix read as an invariant" + the cheap-red-spec escape hatch), I encoded the invariant on the extracted helper and kept the route wiring a trivial, type-checked if (!launch.ok) return sendApiError(...).

Validation

  • pnpm guard — pass
  • pnpm typecheck — pass (clean, all workspace packages)
  • Changed-area suite apps/daemon/tests/host-tools-routes.test.ts — pass (4/4).
  • Full pnpm --filter @open-design/daemon test — pass (4237 passed). A first run showed one failure in an unrelated file that this PR does not touch; it passed on a clean re-run, i.e. a pre-existing flake, not a regression from this change.

Scope

  • Intentionally not changed: editor detection (PR fix(web): make hand-off no-editors fallback perform a real reveal #2494 already handled the zero-editor Finder/Explorer fallback) — this PR is only about reporting launch failures for an editor that is detected.
  • Intentionally not added: exit-code inspection. The fix reports the OS-level launch refusal (spawn/error), which is the reported symptom. Surfacing non-zero exit of a detached launcher (e.g. open -a failing after a successful spawn) would need a settle timer and is a separate, riskier change — left as a possible follow-up.
  • The route's !launch.ok → 500 EDITOR_LAUNCH_FAILED wiring is a trivial, type-checked mapping and is exercised through the extracted helper rather than a full-server route spec — a route-level launch-failure spec would need global fs/child_process mocks inside the smoke daemon plus a platform-stable editor, which isn't worth the harness fragility here.

Known limitation (Windows)

On win32 the spawn keeps shell: true (required so .cmd/.bat editor shims like code.cmd launch at all). Under cmd.exe a refused/missing command spawns the shell successfully and then exits non-zero rather than emitting an error event, so this fix surfaces launch failures on macOS/Linux but not Windows. A Windows-specific path (e.g. inspecting the shell exit code without hanging on long-lived GUI launchers) is a separate follow-up; the available PATH probe already guards the most common missing-binary case there.

…lowing them

The POST /api/projects/:id/open-in route spawned the editor detached,
attached an `error` listener that swallowed the failure, and returned
`{ ok: true }` synchronously — before the child's `error` event can fire on
a later tick. A launch the OS refused (quarantine, EACCES) was therefore
reported as success, and the web button only surfaces an error on a non-OK
response, so clicking "Deliver to VS Code" appeared to do nothing (nexu-io#3871).

Extract the spawn into a `launchHostTool` helper that awaits the child's
`spawn`/`error` event and resolves a discriminated `{ ok }` result. On
failure the route now returns an API error so the existing HandoffButton
error UI surfaces an actionable message. The success path is unchanged.

Fixes nexu-io#3871

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maxmilian maxmilian marked this pull request as ready for review June 9, 2026 03:02
@lefarcen lefarcen requested a review from nettee June 9, 2026 03:58
@lefarcen lefarcen added size/S PR changes 20-100 lines risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps type/bugfix Bug fix labels Jun 9, 2026

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I checked the daemon route change and the helper extraction. The launch/error handling itself looks reasonable, but I left one test-coverage comment because the new spec does not pin the HTTP behavior that actually regressed.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

});
});

describe('host tools launch reporting (#3871)', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This new block only exercises launchHostTool directly, so it does not actually lock in the observable regression described in the PR: POST /api/projects/:id/open-in returning a non-OK EDITOR_LAUNCH_FAILED response when the launch is refused. If the route later regressed back to swallowing !launch.ok or mistakenly mapped that branch to 200 { ok: true }, both of these tests would still stay green because the helper invariant would keep passing. Please add one route-level spec that forces the launch path to fail and asserts the HTTP status plus error code/body from /api/projects/:id/open-in, even if that means mocking launchHostTool or child_process.spawn in the daemon test harness.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 8b84778host-tools-open-in-route.test.ts now exercises the full route: registerHostToolsRoutes mounted on a real Express app with stub deps, spawn mocked at the node:child_process boundary (per your suggestion) so the refused-launch path runs deterministically on any CI platform, and fs.access mocked so the editor probe resolves without an installed editor.

Assertions lock the observable contract you described: forcing the launch to fail asserts HTTP 500 with error.code === 'EDITOR_LAUNCH_FAILED' and the message body (Failed to launch Cursor: spawn cursor ENOENT), plus the 200 { ok: true, editorId, path } counterpart.

Falsifiability check: re-introducing the swallow (if (false && !launch.ok)) turns the new test red, so a regression back to 200 { ok: true } can't stay green. Daemon typecheck + both host-tools suites pass (6 tests).

@lefarcen

lefarcen commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Hey @maxmilian 👋 — the daemon-side fix is well traced, and the PR body does a great job explaining the async/sync race at the core of the bug. @nettee reviewed and left one actionable comment.

The ask is about locking in the regression at the right boundary: the current spec exercises launchHostTool directly, which is a solid helper-invariant, but it doesn't pin the observable HTTP behavior from POST /api/projects/:id/open-in. If the route wiring later regressed to swallowing !launch.ok, both helper tests would stay green — the actual regression point would go undetected. Adding a route-level spec that asserts the non-OK HTTP status and error body from that endpoint would close that gap. Full context in @nettee's inline comment.

Once that test is in, this should be clean to move forward. 🙌

💡 To drive this PR to merge hands-free, paste this to your AI coding agent (Claude Code / Codex / opencode / Cursor …):
Take over nexu-io/open-design#3952 until it merges — read https://raw.githubusercontent.com/nexu-io/looper/main/skills/pr-takeover/SKILL.md and follow it.

Review follow-up: the helper-level tests pin launchHostTool's contract but
not the route's translation of a refused launch into an HTTP response. Mock
spawn at the node:child_process boundary, run the real route, and assert
500 + EDITOR_LAUNCH_FAILED code/message (plus the 200 ok:true counterpart)
so a regression back to swallowing !launch.ok turns the suite red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@maxmilian I reviewed the updated daemon launch handling and the added route-level regression coverage. launchHostTool now feeds /api/projects/:id/open-in a real launch-failure path instead of swallowing the async spawn error, and host-tools-open-in-route.test.ts now locks both the 500 EDITOR_LAUNCH_FAILED response and the success response at the HTTP boundary. Nice follow-through on closing the earlier test-gap cleanly.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

@lefarcen

lefarcen commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@maxmilian nice execution on the route-level spec — mocking spawn at the node:child_process boundary and the falsifiability re-introduction check are exactly right. @nettee's approved the updated coverage. ✅

CI is fully green on the new head. The PR is currently gated on an admin-side step for external PRs; the maintainer team will clear it to merge — nothing more needed on your side.

@lefarcen lefarcen added needs-validation Runtime change detected; needs human or /explore agent validation. and removed needs-validation Runtime change detected; needs human or /explore agent validation. labels Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps size/S PR changes 20-100 lines type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deliver to VS Code can appear to do nothing

3 participants