Skip to content

fix: don't timeout-flush unambiguous escape sequences split across reads#819

Merged
simonklee merged 3 commits intomainfrom
fix/csi-timeout-flush
Mar 18, 2026
Merged

fix: don't timeout-flush unambiguous escape sequences split across reads#819
simonklee merged 3 commits intomainfrom
fix/csi-timeout-flush

Conversation

@Hona
Copy link
Copy Markdown
Member

@Hona Hona commented Mar 16, 2026

Summary

  • Stop timeout-flushing escape sequences once the framing byte is seen (CSI, OSC, DCS, APC, SS3, mouse states)
  • Only esc and esc_recovery states still honor forceFlush since a lone ESC is genuinely ambiguous

Fixes #818

Copilot AI review requested due to automatic review settings March 16, 2026 00:25
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 16, 2026

@opentui/core

npm i https://pkg.pr.new/@opentui/core@1fba2e7

@opentui/react

npm i https://pkg.pr.new/@opentui/react@1fba2e7

@opentui/solid

npm i https://pkg.pr.new/@opentui/solid@1fba2e7

@opentui/core-darwin-arm64

npm i https://pkg.pr.new/@opentui/core-darwin-arm64@1fba2e7

@opentui/core-darwin-x64

npm i https://pkg.pr.new/@opentui/core-darwin-x64@1fba2e7

@opentui/core-linux-arm64

npm i https://pkg.pr.new/@opentui/core-linux-arm64@1fba2e7

@opentui/core-linux-x64

npm i https://pkg.pr.new/@opentui/core-linux-x64@1fba2e7

@opentui/core-win32-arm64

npm i https://pkg.pr.new/@opentui/core-win32-arm64@1fba2e7

@opentui/core-win32-x64

npm i https://pkg.pr.new/@opentui/core-win32-x64@1fba2e7

commit: 1fba2e7

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes issue #818 where CSI (and similar) escape sequences split across two stdin reads were being prematurely flushed as "unknown" responses due to the 10ms timeout. The timeout exists to detect lone ESC keypresses, but once a framing byte is seen (e.g., ESC[ for CSI), the sequence is unambiguous and should wait for completion.

Changes:

  • Remove forceFlush handling from ss3, csi, osc, dcs, apc, esc_less_mouse, and esc_less_x10_mouse states — these now always call markPending() and return when data runs out
  • Retain forceFlush handling in esc and esc_recovery states where a lone ESC is genuinely ambiguous
  • Add inline comments explaining why each state no longer honors timeout-flushing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/lib/stdin-parser.ts Outdated
Comment on lines +748 to +753
// ESC[ already seen — unambiguously CSI. Wait for final byte
// or interruption by a new ESC (handled below). Don't
// timeout-flush; split sequences across reads are common on
// Windows Terminal with kitty keyboard.
this.markPending()
return
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The maxPendingBytes overflow (64MB) is the hard safety net, but in practice any new input breaks it out — either a new ESC interrupts at line 759, or any byte in the final-byte range (0x40-0x7E) terminates the CSI. A secondary timeout would just reintroduce the race for a scenario that doesn't happen with real terminals.

Comment thread packages/core/src/lib/stdin-parser.ts Outdated
Comment on lines +747 to +753
if (this.cursor >= bytes.length) {
if (!this.forceFlush) {
this.markPending()
return
}

this.emitOpaqueResponse("unknown", bytes.subarray(this.unitStart, this.cursor))
this.state = { tag: "ground" }
this.consumePrefix(this.cursor)
continue
// ESC[ already seen — unambiguously CSI. Wait for final byte
// or interruption by a new ESC (handled below). Don't
// timeout-flush; split sequences across reads are common on
// Windows Terminal with kitty keyboard.
this.markPending()
return
@simonklee
Copy link
Copy Markdown
Member

I agree with the csi part, but I don't think its true for osc / dcs / apc (or ss3).

  • ESC[ already has a resync path for incomplete CSI: if a new ESC arrives, it flushes the partial CSI and restarts at the new escape. That makes it workable for split kitty sequences like ESC[118;5 + ;3u.
  • ESC], ESCP, and ESC_ do not have that recovery behavior. if we "keep waiting forever" it is unsafe because they are open-ended control strings. FOr example send \x1b]52;c;, wait past the 10ms timeout, then type abc or even \x1b[A. No key events are emitted, because the parser is still in osc and keeps treating those bytes as OSC payload until BEL / ESC\ or overflow.
  • I think this should be narrowed to csi rather than all framed escape states.

I think we can:

  1. Only disable timeout-flush in csiinside csi.
  2. Keep timeout flush for ss3, osc, dcs, apc
  • Stop the timer from looping for pending csi (bug in current pr), because a single partial sequence now creates a permanent wakeup loop until more bytes arrive

I can also try to clean this up for you tomorrow 😉

@Hona
Copy link
Copy Markdown
Member Author

Hona commented Mar 16, 2026

thanks @simonklee I've in theory actioned what you said if you can take another look
im kinda blind with a lot of this terminal stuff

@simonklee
Copy link
Copy Markdown
Member

thanks @simonklee I've in theory actioned what you said if you can take another look im kinda blind with a lot of this terminal stuff

Thanks — i'm looking at this, but it's a bit of a rabbit hole. I'll try to clean up my patch later tonight.

Hona and others added 3 commits March 17, 2026 21:40
Keep split CSI sequences resumable across timeout while flushing unsafe framed escape states. Also stop timed-out pending CSI from re-arming the parser wakeup loop until new bytes arrive.
Timed-out generic CSI prefixes are ambiguous, but reply families can be safely
deferred when their protocol windows are known to be active.

Add parser-level protocol context and explicit CSI sub-states so timeout
deferral is driven by byte state + negotiated protocol state. Keep generic CSI
conservative (flush on timeout) to prevent stale-prefix input capture, allow
deferred reassembly for kitty keyboard, SGR mouse, private capability replies,
pixel resolution responses, and explicit-width CPR only.
@simonklee simonklee force-pushed the fix/csi-timeout-flush branch from 1fba2e7 to 43ce950 Compare March 17, 2026 20:42
@simonklee
Copy link
Copy Markdown
Member

thanks @simonklee I've in theory actioned what you said if you can take another look im kinda blind with a lot of this terminal stuff

Thanks — i'm looking at this, but it's a bit of a rabbit hole. I'll try to clean up my patch later tonight.

I'm going to have another look and do some more interactive testing with different setups before merging this tomorrow.

@simonklee simonklee merged commit eda42a6 into main Mar 18, 2026
23 checks passed
nybble73 pushed a commit to nybble73/opentui that referenced this pull request Mar 19, 2026
…o#819)

Timed-out generic CSI prefixes are ambiguous, but reply families can be safely
deferred when their protocol windows are known to be active.

Add parser-level protocol context and explicit CSI sub-states so timeout
deferral is driven by byte state + negotiated protocol state. Keep generic CSI
conservative (flush on timeout) to prevent stale-prefix input capture, allow
deferred reassembly for kitty keyboard, SGR mouse, private capability replies,
pixel resolution responses, and explicit-width CPR only.


Fixes anomalyco#818

---------

Co-authored-by: Simon Klee <hello@simonklee.dk>
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.

stdin parser: CSI timeout flush drops split escape sequences

3 participants