fix(host-service): keep v2 preset commands alive during shell init
Problem
In v2 workspaces, launching a terminal preset (e.g. claude) loses the command when the shell is still initializing (direnv / nvm / oh-my-zsh / powerlevel10k). This is the same class of bug #2469 fixed for the v1 terminal stack (apps/desktop/src/main/terminal-host) — but v2 workspaces run an entirely separate terminal stack in packages/host-service, which never received the fix.
Reported in Slack ("command doesn't live… thought we fixed this… v2 needs this I guess").
Root cause
During startup the shell emits terminal capability queries (DA \x1b[c, DSR/CPR \x1b[6n). The renderer's xterm answers them, and those replies arrive back as PTY stdin. The host-service stack forwarded all stdin unconditionally, so responses like ?62;4;9;22c landed on the command line ahead of the queued preset command and displaced it — the command never ran.
v1 already solved this. v2 was missing both halves of the solution:
- v1 drops
\x1b-prefixed stdin while the shell ispending(terminal-host/session.ts). - v1's headless emulator answers the shell's startup queries server-side, so dropping the renderer's duplicate is safe.
v2 had the shellReadyState machine and the OSC 133;A scanner wired for output, but never gated input, and its per-session headless xterm (modeTracker) only tracked modes — it never answered queries.
Fix
Mirror v1's two-part mechanism in the host-service stack:
- Drop query responses during init —
\x1b-prefixed stdin is dropped whileshellReadyState === "pending", in both input paths (writeInputToSessionand the WebSocket"input"handler). - Answer queries server-side during init — wire
onDataon the existing per-session headless xterm (modeTracker) and, whilepending, write its DA/DSR replies back to the PTY. Gated onpendingso the renderer stays the sole responder once the shell is interactive. This is the v2 analogue of v1's headless emulator.
The preset command itself is still written immediately (queueInitialCommand unchanged) — readiness only gates query responses, not the command. A broken/missing marker can't stall the launch, which is the failure mode earlier marker-gating attempts kept hitting (#3478 / #4963).
Files changed
| File | Change |
|---|---|
packages/host-service/src/terminal/terminal-mode-tracker.ts | Add onData to ModeTracker — surface the headless xterm's auto-replies to terminal queries |
packages/host-service/src/terminal/terminal.ts | Answer startup queries server-side during pending; drop \x1b-prefixed stdin during pending in both input paths |
Verification
- Confirmed the per-session headless xterm answers DA →
\x1b[?1;2cand CPR →\x1b[1;1RviaonDatawhen fed viawriteSync(the pathmodeTracker.feeduses). bun test src/terminal— 69 pass / 0 fail.typecheckandlintclean.
Test plan
- Open a v2 workspace whose repo has a slow shell init (
.envrc+ direnv, or oh-my-zsh/powerlevel10k) and launch a preset — command renders immediately and runs once the prompt appears, with no?62;...cgarbage on the line. - Repeat with a fast shell (no direnv) — command runs promptly.
- Re-attach to an existing (already-ready) terminal — input flows normally, escape sequences not dropped.
Summary by cubic
Keep v2 terminal preset commands alive during slow shell init by filtering escape-sequence responses and answering startup queries server-side in packages/host-service. Preset commands now run reliably once the prompt is ready, without ?62;...c noise.
- Bug Fixes
- Drop
\x1b-prefixed stdin whileshellReadyState === "pending"in both input paths (writeInputToSession, WebSocket"input"). - Add
onDatatoModeTrackerand, while pending, write its DA/DSR replies to the PTY so the host answers queries during init. - Leave command queuing unchanged; readiness gates only query responses.
- Drop
Summary by CodeRabbit
-
New Features
- Added terminal callback mechanism to register handlers for automatic reply data from terminal queries.
-
Bug Fixes
- Improved terminal session initialization by handling startup queries on the server side, preventing escape sequences from being incorrectly forwarded as user input during shell startup.