Skip to content

fix(desktop): unblock v1 terminal user input during shell init (#3478)#3550

Merged
Kitenite merged 1 commit into
mainfrom
fix/3478-node-version-freeze
Apr 18, 2026
Merged

fix(desktop): unblock v1 terminal user input during shell init (#3478)#3550
Kitenite merged 1 commit into
mainfrom
fix/3478-node-version-freeze

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 18, 2026

Summary

  • Fixes [bug] Workspace freezes on missing Node version prompt (no way to answer y/N) #3478 — workspace froze when a .nvmrc-pinned Node version was missing and the shell-init fnm use-on-cd hook asked "Do you want to install it? answer [y/N]:".
  • Root cause: Session.write() in the v1 terminal host queued all stdin writes (user keystrokes included) until OSC 133;A. The prompt blocks init, so the marker never fires and typed y/N sits buffered for the 15s timeout.
  • Fix: pass writes straight through to the PTY, keep only the escape-sequence drop for stale DA/DSR replies from the renderer's xterm. This matches v2 (packages/host-service/src/terminal/terminal.ts), which has always written user input directly.

Test plan

  • bun test apps/desktop/src/main/terminal-host/ — 37 pass, including a new #3478 regression test asserting writes reach the PTY while shellReadyState === "pending".
  • bun run typecheck clean.
  • bun run lint clean.
  • Manual: open a repo pinned to an uninstalled Node version, verify the fnm prompt accepts y/N immediately instead of freezing for 15s.

Summary by cubic

Fixes #3478 by letting the v1 desktop terminal send user input to the PTY during shell init, so prompts like fnm’s “install missing Node version?” accept answers immediately. We now drop only stale escape-sequence replies and otherwise pass input through, matching v2 behavior.

  • Bug Fixes
    • Pass stdin through while shellReadyState === 'pending'; removed the pre-ready stdin queue and replay on marker/exit/timeout.
    • Drop only \x1b-prefixed DA/DSR responses from the renderer during pending; after ready, forward all input (e.g., arrow keys).
    • Added a regression test for [bug] Workspace freezes on missing Node version prompt (no way to answer y/N) #3478 and updated tests across shells and marker edge cases.

Written for commit 05e2d7b. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal input responsiveness by changing how write commands are handled during shell initialization; commands now pass through immediately instead of being delayed until the shell is ready.

The v1 terminal host buffered all stdin writes — user keystrokes
included — until the shell emitted OSC 133;A. When a user's `.zshrc`
hook (e.g. fnm's `use-on-cd`) opened an interactive prompt during
init, the marker never fired and typed y/N answers sat in the queue
for the full 15s timeout, making the workspace look frozen.

Pass writes straight through instead, keeping only the escape-sequence
drop for stale DA/DSR replies from the renderer's xterm. Mirrors the
v2 host-service behavior, which has always written user input directly.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

The pull request modifies the terminal shell's write handling semantics, changing from buffering pre-ready writes until shell readiness to immediately passing non-escape-sequence writes through to the subprocess. Test assertions were updated to reflect this pass-through behavior, and the preReadyStdinQueue buffering mechanism was removed from the implementation.

Changes

Cohort / File(s) Summary
Shell Ready Behavior
apps/desktop/src/main/terminal-host/session-shell-ready.test.ts
Updated test suite from "write buffering" to "write pass-through"; replaced buffering assertions with expectations that writes pass through immediately to subprocess stdin during pending state, while escape sequences remain filtered. Removed flush-on-exit behavior checks.
Write Queue Implementation
apps/desktop/src/main/terminal-host/session.ts
Removed preReadyStdinQueue field and associated buffering/flushing logic; write() method now passes non-escape writes through immediately in pending state instead of queueing, and resolveShellReady() no longer drains queued data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hops with glee, no more delay,
Writes rush through without buffering's way,
Escape sequences still stand guard,
But user input flows fast—less hard!
Direct to subprocess, quick and true.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: unblocking v1 terminal user input during shell init, with an explicit issue reference.
Description check ✅ Passed The description is comprehensive with a clear Summary section, explicit issue fix reference, root cause explanation, fix details, and detailed test plan with specific commands and results.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3478-node-version-freeze

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/terminal-host/session.ts (1)

77-82: ⚠️ Potential issue | 🟡 Minor

Stale doc: there are no "buffered writes" to flush on timeout anymore.

With this PR, Session.write() no longer queues stdin during pending — non-escape data passes through immediately. The only thing the timeout now does is flip shellReadyState so that subsequent escape-sequence writes (e.g. arrow keys from the renderer) stop being dropped. Please update this comment so it doesn't mislead future readers into thinking there's still a pre-ready queue.

📝 Proposed doc update
 /**
  * How long to wait for the shell-ready marker before unblocking writes.
- * 15s covers heavy setups like Nix-based devenv via direnv. On timeout,
- * buffered writes flush immediately (same behavior as before this feature).
+ * 15s covers heavy setups like Nix-based devenv via direnv. On timeout,
+ * the session transitions to `timed_out` and escape-sequence writes
+ * (arrow keys, etc.) stop being filtered — non-escape writes already
+ * pass through immediately regardless of state.
  */
 const SHELL_READY_TIMEOUT_MS = 15_000;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/terminal-host/session.ts` around lines 77 - 82, Update
the stale comment for SHELL_READY_TIMEOUT_MS to remove mention of flushing
"buffered writes": explain that Session.write() now passes non-escape stdin
through immediately during pending, and the timeout only flips shellReadyState
so that subsequent escape-sequence writes (e.g., arrow keys) are no longer
dropped; reference the symbols SHELL_READY_TIMEOUT_MS, Session.write(), and
shellReadyState in the comment so future readers understand the current
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/desktop/src/main/terminal-host/session.ts`:
- Around line 77-82: Update the stale comment for SHELL_READY_TIMEOUT_MS to
remove mention of flushing "buffered writes": explain that Session.write() now
passes non-escape stdin through immediately during pending, and the timeout only
flips shellReadyState so that subsequent escape-sequence writes (e.g., arrow
keys) are no longer dropped; reference the symbols SHELL_READY_TIMEOUT_MS,
Session.write(), and shellReadyState in the comment so future readers understand
the current behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eda2d9aa-16a3-487c-a2c9-d44b659ad148

📥 Commits

Reviewing files that changed from the base of the PR and between 867ef87 and 05e2d7b.

📒 Files selected for processing (2)
  • apps/desktop/src/main/terminal-host/session-shell-ready.test.ts
  • apps/desktop/src/main/terminal-host/session.ts

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 18, 2026

Greptile Summary

This PR fixes a terminal freeze in the v1 desktop terminal host (apps/desktop) when a shell init script — such as fnm's use-on-cd hook — presents an interactive prompt (e.g. "Do you want to install it? [y/N]") before the OSC 133;A shell-ready marker fires. The root cause was that Session.write() buffered all stdin writes (including user keystrokes) until the marker arrived; because the prompt blocks init completion, the marker never fires and the typed response sat buffered until the 15 s timeout.

Changes:

  • session.tswrite() now passes all non-escape-sequence data straight through to the PTY during the pending window. Only \\x1b-prefixed bytes are still dropped, which specifically prevents stale DA/DSR terminal-query replies from the renderer's xterm from appearing as literal text at the shell prompt (the headless emulator answers those directly).
  • session-shell-ready.test.ts — New, comprehensive test file covering write pass-through while pending, marker detection across split frames, kill/exit before readiness, and per-shell coverage for both supported (zsh/bash/fish) and unsupported shells (sh/ksh/dash).

Confidence Score: 5/5

Safe to merge — targeted fix with comprehensive regression tests and no regressions to existing behavior.

The change is minimal (the write() method body is a few lines), directly matches the v2 host-service design, is backed by 37 passing tests including a named regression test for #3478, and the PR description clearly traces the root cause. The only note is that the ESC-prefix filter is slightly broader than just DA/DSR replies (it also silently drops arrow-key input during the ≤15 s init window), which is documented in comments and is an acceptable tradeoff.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/main/terminal-host/session.ts Core fix: write() now passes all non-escape-sequence data through during the pending shell-ready window instead of buffering everything; only \x1b-prefixed bytes are dropped to prevent DA/DSR replies from appearing as literal text. The change is minimal, well-commented, and consistent with the v2 terminal host design.
apps/desktop/src/main/terminal-host/session-shell-ready.test.ts New regression/unit test file covering all key cases: write pass-through during pending state (the #3478 fix), escape-sequence filtering, marker detection (including split-frame and combined-marker cases), kill/exit before readiness, and per-shell coverage for supported and unsupported shells.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Session.write(data) called"] --> B{subprocess ready?}
    B -- No --> C["throw Error: PTY not spawned"]
    B -- Yes --> D{shellReadyState == pending?}
    D -- No --> G["sendWriteToSubprocess(data)"]
    D -- Yes --> E{data.startsWith ESC?}
    E -- Yes --> F["DROP - stale DA/DSR reply from renderer xterm"]
    E -- No --> G
    G --> H["PTY receives data immediately"]

    style F fill:#f88,color:#000
    style C fill:#f88,color:#000
    style H fill:#8f8,color:#000
Loading

Comments Outside Diff (1)

  1. apps/desktop/src/main/terminal-host/session.ts, line 860-868 (link)

    P2 Escape filter is broader than DA/DSR replies

    The JSDoc states the dropped sequences are "stale DA/DSR replies from the renderer's xterm", but data.startsWith("\x1b") silently discards all escape-prefixed bytes during the pending window — including legitimate user keystrokes such as arrow keys (\x1b[A), function keys (\x1bOP), and Home/End (\x1b[H).

    In practice this is unlikely to matter (the window is ≤ 15 s and users rarely press navigation keys during shell init), but it is worth noting that the v2 terminal host (packages/host-service/src/terminal/terminal.ts:539) passes all input — including escape sequences — directly to the PTY without any filter, meaning the two implementations are not fully symmetric.

    Consider updating the JSDoc to be explicit that the filter is intentionally broader than just DA/DSR and covers all \x1b-prefixed input during init:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/terminal-host/session.ts
    Line: 860-868
    
    Comment:
    **Escape filter is broader than DA/DSR replies**
    
    The JSDoc states the dropped sequences are *"stale DA/DSR replies from the renderer's xterm"*, but `data.startsWith("\x1b")` silently discards **all** escape-prefixed bytes during the `pending` window — including legitimate user keystrokes such as arrow keys (`\x1b[A`), function keys (`\x1bOP`), and Home/End (`\x1b[H`).
    
    In practice this is unlikely to matter (the window is ≤ 15 s and users rarely press navigation keys during shell init), but it is worth noting that the v2 terminal host (`packages/host-service/src/terminal/terminal.ts:539`) passes all input — including escape sequences — directly to the PTY without any filter, meaning the two implementations are not fully symmetric.
    
    Consider updating the JSDoc to be explicit that the filter is intentionally broader than just DA/DSR and covers all `\x1b`-prefixed input during init:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/terminal-host/session.ts
Line: 860-868

Comment:
**Escape filter is broader than DA/DSR replies**

The JSDoc states the dropped sequences are *"stale DA/DSR replies from the renderer's xterm"*, but `data.startsWith("\x1b")` silently discards **all** escape-prefixed bytes during the `pending` window — including legitimate user keystrokes such as arrow keys (`\x1b[A`), function keys (`\x1bOP`), and Home/End (`\x1b[H`).

In practice this is unlikely to matter (the window is ≤ 15 s and users rarely press navigation keys during shell init), but it is worth noting that the v2 terminal host (`packages/host-service/src/terminal/terminal.ts:539`) passes all input — including escape sequences — directly to the PTY without any filter, meaning the two implementations are not fully symmetric.

Consider updating the JSDoc to be explicit that the filter is intentionally broader than just DA/DSR and covers all `\x1b`-prefixed input during init:

```suggestion
	/**
	 * Write data to the PTY's stdin.
	 *
	 * Escape-sequence-prefixed data (`\x1b`-prefixed) is dropped while the
	 * shell is still initializing. In practice these bytes are stale DA/DSR
	 * replies from the renderer's xterm to terminal queries sent by the shell
	 * during startup; if forwarded they appear as typed text like
	 * `?62;4;9;22c` at the shell prompt. The headless emulator answers those
	 * queries directly (see constructor), so dropping the renderer's duplicate
	 * is safe. As a side-effect, escape-prefixed user keystrokes (arrow keys,
	 * function keys) are also dropped during this window — acceptable given
	 * the ≤ 15 s duration.
	 *
	 * All other data — user keystrokes and preset commands alike — passes
	 * through immediately. Buffering here previously froze workspaces when
	 * shell init commands (e.g. fnm's `use-on-cd` hook) opened an interactive
	 * prompt before the OSC 133;A marker fired. See #3478.
	 */
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(desktop): unblock v1 terminal user i..." | Re-trigger Greptile

@Kitenite Kitenite merged commit c8f34d8 into main Apr 18, 2026
7 checks passed
@Kitenite Kitenite deleted the fix/3478-node-version-freeze branch April 18, 2026 07:00
@github-actions
Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

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.

[bug] Workspace freezes on missing Node version prompt (no way to answer y/N)

1 participant