Skip to content

fix: restore terminal state on exit to prevent mouse escape sequence …#21429

Open
wangliang01 wants to merge 3 commits intoanomalyco:devfrom
wangliang01:fix/ctrl-c-exit-terminal-cleanup
Open

fix: restore terminal state on exit to prevent mouse escape sequence …#21429
wangliang01 wants to merge 3 commits intoanomalyco:devfrom
wangliang01:fix/ctrl-c-exit-terminal-cleanup

Conversation

@wangliang01
Copy link
Copy Markdown

@wangliang01 wangliang01 commented Apr 8, 2026

…garbage

After the TUI exits, the terminal was left with mouse tracking enabled (\x1b[?1003l / \x1b[?1006l SGR mode), causing subsequent terminal input to print raw escape sequences like ^[<35;61;11M instead of being interpreted normally.

Root cause: renderer.destroy() relies on native destroyRenderer() to send the mouse-disable sequences, but process.exit() in index.ts fires before those writes are flushed to stdout.

Fixes:

  • exit.tsx: explicitly write mouse-disable + cursor-restore sequences to stdout before renderer.destroy(), and register a process 'exit' handler as a last-resort guarantee that fires synchronously on process exit.
  • prompt/index.tsx + session/index.tsx: add double-confirm for Ctrl+C exit (press twice within 3 s) matching the behaviour of claude code and similar TUI tools; inline hint replaces the toast for the first press.
  • win32.ts: tighten the ENABLE_PROCESSED_INPUT enforcement poll from 100 ms to 16 ms so the guard reacts faster after console-mode resets.

Issue for this PR

Closes #13276

Type of change

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

What does this PR do?

When the user exits opencode (Ctrl+C twice), the terminal was left in a broken state: mouse tracking escape sequences continued to be emitted as visible garbage text (^[<35;61;11M) because the native renderer cleanup didn't finish before process.exit() terminated the process.

This PR fixes the issue in two layers:

  1. Explicit terminal reset before renderer.destroy() (exit.tsx): writes the mouse-disable sequences (?1003l, ?1006l, ?1000l) and cursor-restore (?25h) directly to stdout synchronously, so they are guaranteed to flush regardless of whether the native cleanup runs in time.

  2. process.on('exit', ...) last-resort handler (exit.tsx): the exit event fires synchronously right before the Node/Bun process terminates, giving a second chance to write the reset sequences even if process.exit() is called from an unexpected path.

  3. Double-confirm Ctrl+C exit (prompt/index.tsx, session/index.tsx): instead of exiting immediately on the first Ctrl+C, the user now has to press it twice within 3 seconds — the same UX as Claude Code and similar tools. The first press shows an inline hint in the status bar rather than a floating toast.

  4. Faster Windows console-mode enforcement (win32.ts): the ENABLE_PROCESSED_INPUT guard poll interval is reduced from 100 ms to 16 ms so it catches and reverts console-mode resets more promptly on Windows Terminal.

How did you verify your code works?

Tested manually on Windows 11 + Windows Terminal v1.21:

  1. Started opencode, ran a prompt, then pressed Ctrl+C twice to exit. After exiting, typed freely in the terminal — previously this produced lines of ^[<35;61;11M ... garbage; after the fix the terminal is clean.

  2. Pressed Ctrl+C once with an empty prompt — confirmed the inline hint ctrl+c again to exit appeared in the status bar and auto-dismissed after ~3 s.

  3. Pressed Ctrl+C twice quickly — confirmed the app exited cleanly with no garbage output.

  4. During an active session, verified Escape still interrupts the running session and Ctrl+C still triggers the two-step exit flow (not session interrupt).

  5. Ran bun typecheck in packages/opencode — no type errors.

Screenshots / recordings

If this is a UI change, please include a screenshot or recording.

Checklist

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

If you do not follow this template your PR will be automatically rejected.

…garbage

After the TUI exits, the terminal was left with mouse tracking enabled
(\x1b[?1003l / \x1b[?1006l SGR mode), causing subsequent terminal input
to print raw escape sequences like ^[<35;61;11M instead of being
interpreted normally.

Root cause: renderer.destroy() relies on native destroyRenderer() to
send the mouse-disable sequences, but process.exit() in index.ts fires
before those writes are flushed to stdout.

Fixes:
- exit.tsx: explicitly write mouse-disable + cursor-restore sequences to
  stdout before renderer.destroy(), and register a process 'exit' handler
  as a last-resort guarantee that fires synchronously on process exit.
- prompt/index.tsx + session/index.tsx: add double-confirm for Ctrl+C
  exit (press twice within 3 s) matching the behaviour of claude code and
  similar TUI tools; inline hint replaces the toast for the first press.
- win32.ts: tighten the ENABLE_PROCESSED_INPUT enforcement poll from
  100 ms to 16 ms so the guard reacts faster after console-mode resets.

Closes anomalyco#13276
@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 Apr 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

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

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.

ctrl+c无法终止程序

1 participant