Skip to content

fix(desktop): bundle runtime .mjs scripts into the server bundle - #208

Merged
aterrylu merged 1 commit into
mainfrom
terry/desktop-statusline-bundle
Jun 11, 2026
Merged

fix(desktop): bundle runtime .mjs scripts into the server bundle#208
aterrylu merged 1 commit into
mainfrom
terry/desktop-statusline-bundle

Conversation

@aterrylu

Copy link
Copy Markdown
Owner

Problem

Claude Code's statusline doesn't apply in the desktop app (works on hosted). Root cause: two .mjs files are loaded by path at runtime, not by import, so bun build --target=node never includes them and build-binary.ts never copied them:

  1. providers/statusline.mjs — injected into every spawned session's --settings statusLine.command. In the bundled server the path resolves inside app Resources where the file doesn't exist; CC swallows the statusline error → no statusline, zero symptoms.
  2. channel-server/dist.mjs — the per-agent MCP channel-server subprocess. Same latent bug: in the desktop, spawned agents' send/create_agent MCP tools would be dead. Worse, runtime.ts resolved it as ../channel-server/dist.mjs, which escapes the bundle dir entirely once bundled.

Same bug class as the spawn-helper fix already in build-binary.ts. This was a false-pass: embedded-mode-integration.test.ts explicitly sets statusLine: {enabled: false}, so nothing covered it.

Solution

flowchart LR
    subgraph src ["tsx from source"]
        SP1["scriptPaths.ts<br/>import.meta.dirname = src/"] --> A1["src/providers/statusline.mjs"]
        SP1 --> B1["src/channel-server/dist.mjs"]
    end
    subgraph bundle ["bun bundle (desktop)"]
        SP2["index.js (all modules concatenated)<br/>import.meta.dirname = bundle dir"] --> A2["bundle/providers/statusline.mjs"]
        SP2 --> B2["bundle/channel-server/dist.mjs"]
    end
    BB["build-binary.ts<br/>stageRuntimeScripts()"] -- "copies RUNTIME_SCRIPTS<br/>+ writes manifest" --> A2
    BB --> B2
    SMOKE["smoke-test-bundle.sh"] -- "fails if any manifest<br/>entry missing" --> bundle
Loading
  • src/scriptPaths.ts (new) — single source of truth. import.meta.dirname is src/ from source and the bundle dir when bundled (bun concatenates every module into index.js), so the same relative paths work in both contexts. All three call sites (claude-code.ts, runtime.ts, run.ts) now import from it.
  • build-binary.tsstageRuntimeScripts() copies each RUNTIME_SCRIPTS entry into the bundle preserving relative paths, aborts the build (exit 1) on a missing source, and writes runtime-scripts.manifest.
  • smoke-test-bundle.sh — loops over the manifest and fails on any missing script (or a missing/empty manifest), so the bundle-side check tracks the staging list automatically.
  • run.ts — boot-time warning when a runtime script is missing (mirrors the gemini-cli existsSync precedent; CC otherwise swallows the failure silently in the field).
  • script-paths.test.ts (new) — staging contract: RUNTIME_SCRIPTS ↔ source tree sync; exported script constants are auto-derived from the module's exports so a forgotten third constant fails the test; the --settings statusLine.command points at an existing file; statusLine.enabled: false still suppresses injection.

Testing

  • make check green: 409 server + 209 dashboard tests (4 new)
  • Bundle rebuilt; both scripts land at the resolved paths; full smoke test passes including new assertions
  • import.meta.dirname semantics in bun --target=node bundles verified empirically (survives verbatim; Node populates it with the bundle dir)
  • Staged statusline.mjs executed standalone from its bundle location — full two-line render with live hierarchy enrichment
  • Negative tests: removed staged script → smoke FAILs + server logs [startup] warning; emptied manifest → smoke FAILs; removed source script → build aborts exit 1

Risks

  • Low. Source-mode behavior is unchanged (same resolved paths, now via one module). Bundle layout mirrors the src/ tree shape.
  • pr-artifact.yml's DMG build + headless smoke test is the end-to-end gate for the desktop artifact.

Alternatives considered

  • Flat-copying statusline.mjs next to index.js and keeping per-file literals: rejected — no single literal works for runtime.ts in both contexts (../channel-server/ is correct from src/agents/ but escapes the bundle dir when bundled).
  • Deriving the smoke-test list by grepping scriptPaths.ts: rejected in favor of a build-written manifest — the manifest asserts what the build actually intended to stage and works without repo source.

🤖 Generated with Claude Code

@aterrylu
aterrylu marked this pull request as ready for review June 11, 2026 17:20

@nox-0x nox-0x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — clean fix for a real silent failure, with the right shape: one source-of-truth module for runtime script paths, build-time staging that aborts on missing source, manifest-driven smoke check, and a boot-time warning. Test coverage matches the failure modes (source-tree sync, exports↔staging coverage, settings.statusLine command path), and the bundle side is covered by the existing smoke-test extension.

The load-bearing claim — import.meta.dirname surviving bun --target=node bundling and resolving to the bundle dir at runtime — is already proven in tree by the embedded-dashboard resolution in run.ts:170-171, so the contract is internally consistent.

One minor follow-up (not blocking): the comment block in stageNodePtySpawnHelper (build-binary.ts) still says "import.meta.dirname is NOT populated in a bun --target=node bundle", which contradicts both run.ts and this PR's design. Worth a one-line cleanup so future readers don't get conflicting guidance.

The statusline renderer (providers/statusline.mjs) and the per-agent MCP
channel-server (channel-server/dist.mjs) are referenced by path at runtime,
not by import — so `bun build` never included them in the desktop server
bundle. Spawned sessions got a statusLine command pointing at a missing
file (CC swallows the error → no statusline) and an MCP config whose
subprocess script didn't exist (agent send/create_agent tools dead).

- Centralize resolution in src/scriptPaths.ts: import.meta.dirname is src/
  from source and the bundle dir when bundled (bun concatenates all modules
  into index.js), so one set of relative paths works in both contexts. This
  also fixes runtime.ts's ../channel-server/ resolution, which escaped the
  bundle dir entirely.
- stageRuntimeScripts() in build-binary.ts copies each RUNTIME_SCRIPTS entry
  into the bundle (aborting the build on a missing source) and writes a
  runtime-scripts.manifest.
- smoke-test-bundle.sh fails if any manifest entry is missing from the
  staged bundle — closes the false-pass that hid this bug.
- Server warns at boot when a runtime script is missing, mirroring the
  gemini-cli existsSync precedent (CC otherwise swallows the failure).
- script-paths.test.ts guards the staging contract: RUNTIME_SCRIPTS ↔ tree
  sync, exported constants auto-derived (a forgotten entry fails the test),
  and the --settings statusLine.command pointing at an existing file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aterrylu
aterrylu force-pushed the terry/desktop-statusline-bundle branch from 7a93df6 to cabe950 Compare June 11, 2026 17:25
@aterrylu
aterrylu enabled auto-merge (squash) June 11, 2026 17:25
@aterrylu
aterrylu merged commit 06389b0 into main Jun 11, 2026
9 checks passed
@aterrylu
aterrylu deleted the terry/desktop-statusline-bundle branch June 11, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants