Skip to content

fix(tui): prevent piped stdin from breaking UI and keyboard input - #34242

Closed
LordMikkel wants to merge 12 commits into
anomalyco:devfrom
LordMikkel:fix/tui-stdin-pipe-v2
Closed

LordMikkel wants to merge 12 commits into
anomalyco:devfrom
LordMikkel:fix/tui-stdin-pipe-v2

Conversation

@LordMikkel

@LordMikkel LordMikkel commented Jun 27, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #28538
Closes #24195
Closes #3871
Closes #6220

Replaces #28540, which was closed by the automated PR cleanup bot before maintainer review.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

When stdin is piped, the TUI renderer can read keyboard input from the pipe instead of the terminal. That breaks the UI and keyboard control for a simple command like:

echo "hello" | opencode

The run command already handles this with resolveInteractiveStdin(). This PR applies the same approach to the TUI: pass the controlling terminal (/dev/tty on Linux, CONIN$ on Windows) to the renderer while still consuming piped stdin as the prompt.

How did you verify your code works?

  • bun --cwd packages/opencode test runtime.stdin
  • bun --cwd packages/opencode typecheck
  • Manual case to validate in a real terminal: echo "hello" | opencode

Screenshots / recordings

With the fix:

image image

Without the fix:

Image image

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Jun 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@andrei-hasna

Copy link
Copy Markdown

Local merge/validation note from review queue:

Validated locally against current dev (ae53163):

  • PR head 571459c merges cleanly with no conflicts.
  • packages/opencode typecheck passes.
  • packages/tui typecheck passes.
  • packages/opencode test/cli/run/runtime.stdin.test.ts passes.
  • packages/tui test/app-lifecycle.test.tsx passes.
  • Changed-file oxlint exits 0, with existing warnings outside the added lines.

I would not merge this yet. Remaining blockers found in review:

  1. The resolved interactive stdin is passed to the TUI renderer, but TUI openEditor() call sites still omit it. External editor launches therefore inherit the original pipe/EOF after flows like echo prompt | opencode, so editor-based prompt/session export workflows can still have broken stdin.
  2. The Windows CONIN$ path needs another pass: Ctrl+C guard, processed-input disable, and input-buffer flush still operate on process.stdin, not the resolved renderer stdin.
  3. The no-controlling-terminal error currently reuses --mini requires a controlling terminal for input, which is misleading for the non-mini TUI path.

Also, the branch is currently stale relative to dev (1 ahead / 9 behind); GitHub reports mergeStateStatus=BLOCKED even though the content is MERGEABLE locally.

@andrei-hasna

Copy link
Copy Markdown

Revalidated by the merge queue on 2026-06-28 against current dev (61a7f6d) and PR head 571459c.

What passed:

  • PR still applies cleanly locally; git merge-tree origin/dev refs/remotes/pr/34242 succeeds.
  • git diff --check origin/dev...refs/remotes/pr/34242 passes.
  • bun --cwd packages/opencode test --timeout 30000 test/cli/run/runtime.stdin.test.ts test/cli/tui/thread.test.ts passes.
  • bun run --cwd packages/opencode typecheck passes.
  • bun run --cwd packages/tui typecheck passes.
  • Changed-file oxlint has 0 errors; warnings are on pre-existing lines.

Still not merging yet. The blockers from the previous review comment still apply because the head commit has not changed:

  1. TUI external editor call sites still omit the resolved stdin, so editor workflows can inherit the original pipe/EOF.
  2. Windows processed-input/flush/Ctrl+C guard paths still key off process.stdin, not the resolved interactive stdin stream.
  3. The no-controlling-terminal error text is still mini-specific for the standard TUI path.
  4. GitHub reports mergeStateStatus=BLOCKED; the branch is now 1 ahead / 11 behind current dev.

Please address those before this is merge-ready.

@andrei-hasna

Copy link
Copy Markdown

Revalidated by the merge queue on 2026-06-28 against current dev (dfeb1b505) and PR head 571459c.

What passed locally in an isolated worktree:

  • PR merges cleanly into current dev with no conflicts.
  • git diff --check passes.
  • bun --cwd packages/opencode test --timeout 30000 test/cli/run/runtime.stdin.test.ts test/cli/tui/thread.test.ts passes.
  • bun --cwd packages/tui test --timeout 30000 test/app-lifecycle.test.tsx passes.
  • bun run --cwd packages/opencode typecheck passes.
  • bun run --cwd packages/tui typecheck passes.
  • Changed-file oxlint has 0 errors; warnings are on pre-existing lines.

Still not merging. Independent review confirmed the remaining blockers:

  1. The resolved interactive stdin is only passed to the OpenTUI renderer. TUI openEditor() call sites still omit it, so editor launches can inherit the original piped stdin/EOF after flows like echo prompt | opencode.
  2. Windows terminal handling still keys off process.stdin: Ctrl+C guard, processed-input disable, and input-buffer flush do not use the resolved CONIN$ stream passed to the renderer.
  3. The no-controlling-terminal error text still says --mini requires a controlling terminal for input, which is misleading for the standard TUI path now using the same resolver.
  4. GitHub still reports mergeStateStatus=BLOCKED; branch divergence is 12 1 (dev ahead / PR ahead).

Please address those before this is merge-ready.

@andrei-hasna

Copy link
Copy Markdown

Revalidated by the merge queue on 2026-06-28 against current dev (6ee817d) and PR head 571459c.

What passed locally in an isolated worktree:

  • PR merges cleanly into current dev with no conflicts.
  • git diff --check passes.
  • bun --cwd packages/opencode test --timeout 30000 test/cli/run/runtime.stdin.test.ts test/cli/tui/thread.test.ts passes (13/13).
  • bun --cwd packages/tui test --timeout 30000 test/app-lifecycle.test.tsx passes (2/2; same pre-existing KV ENOENT logs).
  • bun run --cwd packages/opencode typecheck passes.
  • bun run --cwd packages/tui typecheck passes.
  • Changed-file oxlint reports 0 errors; warnings are on pre-existing lines.

Still not merging. Independent adversarial review confirmed the remaining blockers:

  1. TUI external editor call sites still omit the resolved interactive stdin, so editor workflows can inherit the original piped stdin/EOF after flows like echo prompt | opencode.
  2. Windows terminal handling still keys off process.stdin: Ctrl+C guard, processed-input disable, and input-buffer flush do not use the resolved CONIN$ stream.
  3. The no-controlling-terminal error text still says --mini requires a controlling terminal for input, which is misleading for the standard TUI path.
  4. GitHub still reports mergeStateStatus=BLOCKED; branch divergence is 13 behind / 1 ahead current dev.

Please address those before this is merge-ready.

@LordMikkel

LordMikkel commented Jun 28, 2026 •

Copy link
Copy Markdown
Author

@andrei-hasna Thanks for the thorough review!

I addressed the blockers:

  • Passed the resolved stdin to TUI editor calls.
  • Updated Windows terminal handling to use the resolved stdin.
  • Fixed the TUI controlling-terminal error message.
  • Updated the branch against current dev.

Your detailed feedback was really valuable!

@precelca

precelca commented Jul 2, 2026

Copy link
Copy Markdown

Tested the latest revision, and I can confirm this fixes the issue for me. I can now pipe stdin into the TUI without losing keyboard input.

@andrei-hasna

Copy link
Copy Markdown

Worker review update for the current head 86812a12 against current origin/dev b7e4f1e:

Not merging as-is. Local synthetic merge into current dev applies cleanly, and focused validation passed:

  • bun typecheck from packages/opencode
  • bun test test/cli/run/runtime.stdin.test.ts --timeout 30000 from packages/opencode
  • bun typecheck from packages/tui
  • bun test test/editor.test.ts --timeout 30000 from packages/tui
  • git diff --cached --check

Merge blockers:

  1. GitHub still reports mergeStateStatus=BLOCKED, reviewDecision empty, and no latest reviews.
  2. Branch is stale versus current dev: origin/dev...origin/pr/34242 is 80 commits behind / 4 ahead in this local fetch.
  3. Adversarial review found a Windows cleanup-order risk in the merged diff: packages/opencode/src/cli/cmd/tui.ts calls interactiveStdin?.cleanup?.() before unguard?.(), while packages/tui/src/terminal-win32.ts restores console mode using the handle captured from that stdin stream. On Windows piped-stdin TUI, restore can depend on a closed CONIN$ handle. Call the guard cleanup before destroying the alternate stdin stream.
  4. Regression coverage is still thin for the full TUI piped-stdin path, editor stdin handoff, keyboard input preservation, and Windows guard cleanup ordering.

Recommendation: update/rebase the PR, fix the cleanup ordering, add focused regression coverage, and rerun CI on the updated GitHub merge ref before re-entering the merge queue.

@LordMikkel

LordMikkel commented Jul 6, 2026 •

Copy link
Copy Markdown
Author

@andrei-hasna Thanks again for the careful review and feedback.

Fixed the cleanup ordering: the guard cleanup now runs before closing the alternate stdin stream. Also removed a accidental scratch comment.

Drop the stdin context in favor of renderer.stdin, and trim the win32
console helpers back to the original shape.
_get_osfhandle aborts the process when given a Bun-opened fd, so
echo hi | opencode would crash on Windows. Open a dedicated CONIN$
handle instead; every CONIN$ handle shares the same console input buffer.
Resolve the terminal stream before the TUI try block so both handles stay
const, name the win32 CreateFileW flags, and add a Windows-only test that
exercises the console helpers with a separately opened CONIN$ stream.
@LordMikkel

Copy link
Copy Markdown
Author

Updated against the latest dev and resolved the conflict.

I also tested it on Windows this time and found that the previous version crashed with piped stdin: _get_osfhandle doesn't recognize Bun's fds and aborts the process. It now opens its own CONIN$ handle instead and works correctly.

@thdxr

thdxr commented Oct 10, 2026

Copy link
Copy Markdown
Member

This branch has been upgraded to V2. We're no longer taking PRs for V1. If you think this is still relevant, please port it over and open it for V2.

— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants