fix(host-service): stop gating preset launches on OSC 133;A marker
Summary
queueInitialCommandno longer waits onshellReadyPromise— the preset command is written to the PTY immediately. Users whose shell never emits the OSC 133;A marker (stale wrapper, exec'd shell, blocking.zshrc, broken precmd hook) no longer hit a guaranteed 15 s stall on every preset send.SHELL_READY_TIMEOUT_MSdrops from 15 s to 3 s. WithinitialCommandno longer gated, the only remaining job of the timeout is flushing the scanner's held partial-marker bytes, which doesn't need 15 s.
Why / Context
Users have been reporting that sending a preset in the v2 workspace takes ~15 s before anything happens. Traced to queueInitialCommand (packages/host-service/src/terminal/terminal.ts), which deferred pty.write(cmd) behind shellReadyPromise.then(...). That promise resolves either when the OSC 133;A "prompt start" marker arrives or when SHELL_READY_TIMEOUT_MS (15 s) fires.
The marker comes from our zsh/bash wrapper rc files via a precmd hook (apps/desktop/src/main/lib/agent-setup/shell-wrappers.ts). Several real-world cases cause it to never fire:
- Stale install —
getShellBootstrapEnvonly setsZDOTDIRto~/.superset/zshif.zshrcexists there. IfsetupAgentHookshasn't run / failed silently, zsh sources the user's plain dotfiles and ourprecmdhook is never registered. exec tmux/exec screenin user.zshrc— our hook is installed in.zlogin, which runs after.zshrc. Anexecin.zshrcreplaces the process before.zloginruns..zshrcblocking on stdin —ssh-add,fnm/asdf/miseprompts,conda initlicense,gh auth. Shell sits at the keyboard;precmdnever fires.
All three look identical from the renderer: send preset → 15 s of nothing → command finally appears.
How It Works
PTY line discipline buffers bytes written to stdin until the slave (the shell) reads them. Writing claude\n immediately at session create just puts it in that buffer; the shell consumes it once it finishes sourcing rc files and enters its read loop. v1's preset path achieves the same outcome by baking the command into shell argv (zsh -lc "<cmd>") and explicitly did not gate writes — the #3478 comment in apps/desktop/src/main/terminal-host/session.ts:870 documents that gating was previously removed because it froze workspaces.
The OSC 133;A scanner still strips the marker from output and holds partial-prefix bytes back across chunks (job preserved). Adoption still treats adopted sessions as ready (the prompt has already happened in the previous host-service lifetime).
Manual QA Checklist
Terminal Features
- Send a preset (Claude) in v2 — command runs immediately, no 15 s wait
- Dev mode launches without errors after
bun dev - Agent-setup runs and updates zsh + bash wrappers on launch
- Test on a user whose
~/.superset/zsh/.zloginis missing or pre-fix-vintage — should now launch promptly instead of stalling - Test with a heavy
.zshrc(oh-my-zsh + nvm + conda init) — preset still lands, no double-prompt
Workspace
- Re-send the same preset (host-service adoption path) — still works, no re-fire of initialCommand
Testing
bun run lint— cleanbun run typecheck(host-service) — clean- New regression test:
packages/host-service/src/terminal/terminal.adoption.node-test.ts→initialCommand runs promptly even when OSC 133;A never fires. Spawns/bin/bash(inSHELLS_WITH_READY_MARKER) with no Superset wrapper on disk so the marker never arrives. Before the change the suite took 15 257 ms (the full timeout); after, 142 ms. - Full
terminal.adoption.node-test.tssuite (13 tests) passes — including the adoption-ordering test that confirms an adopted session does not re-fireinitialCommand.
Design Decisions
- Why drop the gate instead of matching v1's argv-based preset launch. v1 passes the preset command via
zsh -lc "<cmd>"so the shell exits when the preset exits. v2 keeps an interactive shell alive after the preset for the "Run" tab and similar flows; switching to argv-based would change that behavior. Dropping the gate matches v1's write policy (#3478) without changing v2's process-lifecycle behavior. - Why 3 s for the timeout. With
initialCommandno longer dependent on it, the timeout only governs how long the scanner withholds a partial OSC 133;A prefix from output. 3 s is overkill for that and bounded enough that the worst case is invisible.
Known Limitations
- If a user's
.zshrcconsumes stdin viaread(ssh-add passphrase, fnm install prompt, conda license, gpg passphrase), the preset command bytes can be consumed by that read instead of landing at the zle prompt. This was already true under the old behavior — users in this situation saw their preset command typed into the passphrase prompt anyway, just 15 s later. The principled long-term fix is to switch to argv-based preset launch (v1's pattern); not done here to keep the change minimal. - Cosmetic: during the brief window when the PTY is in canonical-mode echo, the kernel may echo the bytes back to the output stream before zle takes over. xterm's prompt redraw typically paints over it. v1 already accepts the same window for user-typed input during init.
Follow-ups
- Add timing telemetry around
resolveShellReady(state,shellName, elapsed) so we can confirm the distribution of marker-fire vs timeout in production. - Self-heal stale wrappers in
createTerminalSessionInternal— check~/.superset/zsh/.zlogincontains__superset_prompt_markbefore each spawn; re-invokecreateZshWrapperif not.
Summary by cubic
Presets now start immediately by writing the initial command to the PTY without waiting for the OSC 133;A marker, removing the 15s stall. The shell-ready timeout drops to 3s and only flushes partial marker bytes held by the output scanner.
-
Bug Fixes
- Stop gating
queueInitialCommandonshellReadyPromiseso presets run even when wrappers are stale, shellsexec, or.zshrcblocks on stdin. - Reduce
SHELL_READY_TIMEOUT_MSfrom 15s to 3s; scanner still strips OSC 133;A and flushes partial prefixes.
- Stop gating
-
Refactors
- Simplified
queueInitialCommandby removing a redundant exit guard; relaxed the regression test timing (5s assert, 10s waitFor) to reduce CI flakiness. - Trim stale comments and docstrings around the shell-ready timeout and initial command gating.
- Simplified
Summary by CodeRabbit
-
Bug Fixes
- Initial commands are now sent immediately when a terminal session starts, improving responsiveness when shell-ready signals never arrive.
- Shell-ready detection timeout shortened (previously much longer), speeding up session startup.
-
Tests
- Added an end-to-end regression test that verifies initial commands complete and records latency even if readiness markers never appear.