Repository navigation
fix(host-service): send initialCommand Enter as a separate delayed write - #6026
Conversation
Agent and preset launches intermittently ended up typed at the prompt but never executed: the workspace was created, the terminal opened, and the full launch command sat in the shell's edit buffer with no agent running. The OSC 133;A shell-ready marker fires from precmd, which runs before the prompt is drawn and before the line editor starts reading input. Writing `command\n` in that gap lets shell plugin init (vi-mode, syntax-highlighting — worst under first-create load) flush the PTY input queue mid-read: the command text survives into the editor's buffer while the trailing newline is lost. Reproduced live via CDP on a first workspace create after app boot; a single synthetic Enter keypress launched the stuck agent, confirming only the newline was missing. Strip trailing newlines from the command text and send Enter as its own write 500ms later — as `\r`, what a real Enter key sends, bound to accept-line in every keymap. Exactly one Enter is ever sent, so a double run is impossible, and if startup eats the text itself the stray `\r` is a no-op at an empty prompt — no failure mode is worse than before. Residual hole in the #4963 → #5774 → #5927 marker-gating series. Claude-Session: https://claude.ai/code/session_01CwWxQTQW2zLeN9hhRkQNxT
|
Ready to review this PR? Stage has broken it down into 3 individual chapters for you:
Chapters generated by Stage for commit 56af206 on Jul 28, 2026 10:22pm UTC. |
📝 WalkthroughWalkthroughInitial terminal commands are trimmed, written after shell readiness, and followed by a delayed carriage return. Integration coverage verifies the separate write order. ChangesInitial command dispatch
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
…al test The setup-scripts integration test asserted the old single `cmd\n` write; initialCommand now arrives as the command text plus a separate delayed `\r`. Assert both writes and their ordering. Claude-Session: https://claude.ai/code/session_01CwWxQTQW2zLeN9hhRkQNxT
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/host-service/test/integration/setup-scripts.integration.test.ts`:
- Around line 126-133: Update the waitFor predicate in the integration test to
count carriage-return writes and require exactly one "\r", with that sole Enter
occurring after the setup command write. Keep the existing diagnostic message
and command-order assertion intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ad3f95d4-7281-4e43-ad99-c9f8fe54932a
📒 Files selected for processing (1)
packages/host-service/test/integration/setup-scripts.integration.test.ts
| // The command text and its Enter arrive as separate writes (the Enter is | ||
| // delayed so shell startup can't eat it) — assert both, in order. | ||
| await waitFor( | ||
| () => writes.includes("echo setup-a && echo setup-b\n"), | ||
| () => | ||
| writes.includes("echo setup-a && echo setup-b") && | ||
| writes.indexOf("\r") > writes.indexOf("echo setup-a && echo setup-b"), | ||
| 5000, | ||
| () => `expected setup command write, got ${JSON.stringify(writes)}`, | ||
| () => `expected setup command write + Enter, got ${JSON.stringify(writes)}`, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert that exactly one Enter is written.
This predicate passes if two "\r" writes occur after the command. Count the carriage-return writes and assert that the sole one follows the command, matching the PR’s exactly-one-Enter contract.
Proposed assertion
- () =>
- writes.includes("echo setup-a && echo setup-b") &&
- writes.indexOf("\r") > writes.indexOf("echo setup-a && echo setup-b"),
+ () => {
+ const commandIndex = writes.indexOf("echo setup-a && echo setup-b");
+ return (
+ commandIndex !== -1 &&
+ writes.filter((write) => write === "\r").length === 1 &&
+ writes.indexOf("\r") > commandIndex
+ );
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // The command text and its Enter arrive as separate writes (the Enter is | |
| // delayed so shell startup can't eat it) — assert both, in order. | |
| await waitFor( | |
| () => writes.includes("echo setup-a && echo setup-b\n"), | |
| () => | |
| writes.includes("echo setup-a && echo setup-b") && | |
| writes.indexOf("\r") > writes.indexOf("echo setup-a && echo setup-b"), | |
| 5000, | |
| () => `expected setup command write, got ${JSON.stringify(writes)}`, | |
| () => `expected setup command write + Enter, got ${JSON.stringify(writes)}`, | |
| // The command text and its Enter arrive as separate writes (the Enter is | |
| // delayed so shell startup can't eat it) — assert both, in order. | |
| await waitFor( | |
| () => { | |
| const commandIndex = writes.indexOf("echo setup-a && echo setup-b"); | |
| return ( | |
| commandIndex !== -1 && | |
| writes.filter((write) => write === "\r").length === 1 && | |
| writes.indexOf("\r") > commandIndex | |
| ); | |
| }, | |
| 5000, | |
| () => `expected setup command write + Enter, got ${JSON.stringify(writes)}`, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/host-service/test/integration/setup-scripts.integration.test.ts`
around lines 126 - 133, Update the waitFor predicate in the integration test to
count carriage-return writes and require exactly one "\r", with that sole Enter
occurring after the setup command write. Keep the existing diagnostic message
and command-order assertion intact.
…o terminals send submits reliably (#6284) * fix(host-service): send follow-up Enter as a separate delayed write terminals send bundled the submit Enter into the same PTY write as the bracketed-paste text. TUI agents aggregate bytes that arrive together into one paste burst, so on a busy or laggy Claude session the Enter could be folded into the input-box draft instead of submitting it — text lands but never sends, while the CLI reports submitted: true. Write the framed text first, then the Enter as its own write 500ms later (same class of fix as INITIAL_COMMAND_ENTER_DELAY_MS for initialCommand, #6026). If the session exits during the gap, the send now reports an error instead of pretending. Fixes #6243 * fix(host-service): serialize follow-up sends per session; assert split writes in tests Review follow-ups on #6284: the delayed Enter opened a window where a concurrent send's text (including submit: false drafts) could land between another send's text and its Enter and get submitted by it — chain sends per session so the sequence is atomic. Strengthen the delay test to record pty writes (a delayed-but-bundled write no longer passes) and add an overlapping-sends test. * fix(host-service): guard indexed access in send delay test for typecheck * fix(host-service): dedupe in-flight headless adoptions so racing sends share one session Concurrent terminal.send calls right after a host-service restart could each adopt their own TerminalSession (independent followUpWriteChains, duplicate daemon subscriptions), bypassing per-session send serialization. Share the leader's adoption attempt per terminal id; followers re-resolve and run their own workspace check.
What
Agent and preset launches could end up typed at the prompt but never executed: the workspace was created, the agent terminal opened, and the full launch command (
'claude' '--dangerously-skip-permissions' '<prompt>') sat in the shell's edit buffer with the cursor blinking after it. Reported as "the new workspace page creates a workspace but doesn't start the agent" — but it's page-agnostic and hits everyinitialCommandconsumer (modal creates, automations, setup terminals).Root cause
The OSC 133;A shell-ready marker is emitted from a
precmdhook, which runs before the prompt is drawn and before the line editor starts reading input. Writingcommand\nin that gap lets shell plugin init (oh-my-zsh vi-mode, syntax-highlighting — worst under first-create load) flush the PTY input queue mid-read: the command text survives into the editor's buffer, the trailing newline is lost.Confirmed live via CDP against the dev app: on the first workspace create after boot the command was stuck unexecuted; sending one synthetic Enter keypress launched Claude immediately, which then completed the prompt's task — proving only the newline was missing. This is the residual hole in the #4963 → #5774 (first-prompt gate) → #5927 (15s bound) series.
Fix
In
queueInitialCommand:\r(what a real Enter key sends, bound to accept-line in every keymap) instead of\n(relies on the ^J binding).Exactly one Enter is ever sent, so double-execution is impossible. If startup eats the text itself, the stray
\ris a no-op at an empty prompt — every failure mode is strictly no-worse than before. Same technique VS Code shell integration uses; v1 avoided the class entirely by launching viashell -lc, which is why v1 never hit this.Verification
bun run test:e2e(host-service): 20/20 pass, including the direnv input-consumption simulation and the promptness bound (command executed in 517ms with the new delay)./new-workspaceflow twice with real input events — both creates launched Claude, which replied within ~2s. Setup terminals unaffected.tscclean.https://claude.ai/code/session_01CwWxQTQW2zLeN9hhRkQNxT
Summary by cubic
Fixes an intermittent issue where the initial launch command was typed but not executed. We now send the command text and Enter separately with a short delay for reliable execution.
packages/host-service/src/terminal/terminal.ts, strip trailing CR/LF frominitialCommandand write only the text.\r500 ms after shell-ready to avoid plugin init eating it.packages/host-service/test/integration/setup-scripts.integration.test.tsto assert split write and delayed\r; tidy assertion formatting.initialCommandflows (new workspace, modal creates, automations, setup terminals).Written for commit 56af206. Summary will update on new commits.
Summary by CodeRabbit
\n, and that the Enter (\r) write occurs afterward.