Description
Summary
When a terminal escape sequence arrives at the TUI in two fragments (the leading ESC in one read, the rest in the next), the leading ESC is treated as a standalone Escape key press and the remainder is inserted into the prompt as literal text. This shows up as garbage in the input box such as [5~ (PageUp), [<51;103;19M (mouse motion with Ctrl) or :ffff/ffff/ffff:eded/ecec/eeee (an OSC 10/11 colour reply).
Environment
- opencode version: 2.2.1 (also reproduced on 2.2.0)
- OS: Linux 7.0.0-38-generic x86_64 (server); client is Windows 11 10.0.26200.9711
- Terminal: Windows Terminal 1.25.2733.0 → pwsh 7.6.8 → ssh (OpenSSH_10.5p1) → Linux.
TERM=xterm-256color,COLORTERMempty on the client,TERM_PROGRAMunset. Also reproduced inside tmux 3.6 (TERM=tmux-256color). - Shell: /bin/bash
- Install/channel: latest
- Active plugins: none
Transport detail: the client terminal is Windows Terminal, which runs the shell through ConPTY. The same setup does not reproduce in mintty (Git Bash), which does not use ConPTY.
Reproduction
Deterministic reproduction with TUI raw input (no terminal client needed — I drove opencode through a pty):
- Start
opencodeattached to a pty. - Wait for the UI to settle (~5 s).
- Write
ESC(0x1b) to stdin. - Wait ~60 ms.
- Write
[5~(the rest of PageUp).
Result: [5~ is rendered into the prompt as literal text.
If ESC[5~ is written in a single write, it is consumed correctly and nothing appears.
The same holds for other sequences: splitting ESC from [<35;58;10M or from ]11;rgb:ffff/ffff/ffff produces the corresponding tail as text. With the PageUp case, a 10 ms gap was still tolerated and a 60 ms gap leaked every time.
Real-world reproduction:
- Use opencode over Windows Terminal → ssh → Linux (ConPTY transport).
- Enable mouse (default) and move the mouse, or press PageUp / use the wheel.
- Intermittently, fragments like
[5~,<35;58;10Mor:ffff/ffff/ffff:eded/ecec/eeeeappear in the prompt input.
Expected Behavior
Incomplete escape sequences should be buffered until they are complete, or until a short timeout expires, instead of the leading ESC being committed as an Escape key press and the remainder being treated as typed text.
Actual Behavior
The remainder of the sequence is inserted into the prompt as literal characters, e.g.:
[6~5~5~5~[5~6~5~[5~5~~[6[13;5u
<51;103;19M
:ffff/ffff/ffff:eded/ecec/eeee
Additional Context
Captured raw stdin with OTUI_STDIN_LOG=/tmp/oc-stdin.log in a session where the garbage appeared: one capture held 67 SGR mouse reports, all of them with the ESC byte intact, e.g. \x1b[<51;102;19M, yet the UI still printed <51;103;19M as text. So at least some cases are not caused by fragmentation upstream; the parser drops the ESC and keeps the tail. Verified with od -c that no byte was lost between tmux and opencode's stdin.
Notes on the leaked mouse reports: the ones observed leaking had cb = 51, i.e. 32 (motion) + 16 (Ctrl) + 3 (no button). Plain motion (cb = 35) was not observed leaking. Injecting a single intact \x1b[<51;102;19M into a fresh TUI does not reproduce it, so the mouse case is intermittent (observed while a response was streaming/rendering).
Workarounds that help:
"mouse": falseincli.json— removes the mouse source entirely (wheel becomes PageUp/PageDown).- Running inside tmux, which reassembles fragments for most sequences.
"animations": false— unrelated to the leak, but makes the TUI less busy.
Not related: the terminal emulator version (Windows Terminal 1.25 stable and 1.26 Preview behave the same), the ssh client (both Windows OpenSSH 9.5p2 and Git's 10.5p1 reproduce).
Activity
opencodejr commentedon Oct 29, 2026
Thanks for the very precise report. I could reproduce this on 2.2.1, and it still happens on the latest
dev(87a35fa) run from source.Steps: run the TUI under a PTY (
TERM=xterm-256color), wait for it to settle, write a loneESC, wait 60 ms, then write the rest of the sequence.[5~(PageUp):[5~shows up in the prompt.]11;rgb:ffff/ffff/ffff+ BEL:]11;rgb:ffff/ffff/ffffshows up in the prompt.ESC+[<35;58;10M): no leak in my runs.ESC[<51;…Mleaking while a response streams.The script below reproduces it (
python3 repro.py 60 pageup). There's no video, because our TUI automation tool can't send raw, split terminal bytes.It looks like the bundled OpenTUI stdin parser treats a lone
ESCas the Escape key after about 20 ms. After that it can only recover split mouse reports; other CSI tails ([5~) and OSC tails (]…) are typed as text. This overlaps with #57345 (split OSC 10/11 replies), but it also affects normal keys such as PageUp. #57517 (OSC 4 replies) is related too.Reproduction script (repro.py)
Run it:
python3 repro.pySiarhei-Yarkavy commentedon Oct 30, 2026
Some measurements on where the split actually gets absorbed — might help narrow this down to the parser rather than the transport.
Setup: opencode 2.2.3, tmux 3.8 (static build),
escape-time 100, terminal client attached through a pty. Raw bytes were written to the tmux client, i.e. exactly the path a real terminal/ConPTY split would take.Method: write
ESC, waitgap, then write the rest of the sequence; capture the pane afterwards and look for the tail rendered as text.[<65;88;19M[5~]11;rgb:ffff/ffff/ffff BEL[ASo tmux does reassemble client-side splits (at least up to 400 ms here), even with
escape-time 100. With tmux in the path, the transport-level split does not reach OpenTUI.The same inputs written directly to the pane pty (bypassing tmux, as in the repro in the issue) still leak:
ESC+ 60 ms +[5~puts[5~in the prompt every time. That is consistent with the parser committing a loneESCafter ~20 ms.Also tried the experimental parser — no change:
OTUI_NATIVE_INPUT_PARSER=1/=true:[5~still leaks.experimental_nativeInput: trueincli.json:[5~still leaks.So there seem to be two layers:
ESC(timeout) and/or droppingESCfrom an otherwise intact sequence while the TUI is busy — thecb=51case noted in the report. tmux cannot help there.Observed in the wild inside tmux 3.8 as well: wheel reports showing up as
;88;19M<65;88;19M, plus strayA(tail ofESC[A) and[5~in the composer. Rare, but it does happen with tmux in the path.Siarhei-Yarkavy commentedon Oct 30, 2026
One more concrete case, because it explains the stray
A/Bcharacters people see in the session-search input.Those are not random letters — they are the final byte of the Up/Down arrow sequences:
If the sequence is split after the 2-byte prefix (
ESC[orESC O), the parser consumes the partial CSI/SS3, times out, and the remaining byte is typed as text.Deterministic repro (raw bytes written straight to the pane pty, 60 ms gap):
ESC+[A[AESC[+AAESC O+AAESC[+BBESC O+BBThis is why the session picker shows single
A/Bcharacters: arrows navigate the list while the search field keeps focus, so the leaked final byte lands in the query.Same root cause as the
[5~and;88;19Mcases, just a different split point.TOMUIV commentedon Nov 4, 2026
Confirming on a Windows server (the report above is Linux server + Windows client), and adding two
things that I think point at a much cheaper fix than the parser rework.
1. Still present on every channel
latest 2.2.5,0.0.0-dev-578,0.0.0-beta-19507— all three still build the stdin parser withtimeoutMs: 20:So this is not fixed anywhere as of today.
2. What it looks like here
Crop from a live Windows-server session, pointer only moved over the window, nothing clicked:
Those are tails of SGR mouse reports with the
ESC[<head missing, plus strayMterminators from theprevious report. Earlier in the same session we also collected
5;35;25M, i.e. a report whosecb = 35(plain motion, no button) — so the
cb = 35case is worth keeping in scope next tocb = 51, even thougha single split
ESC+[<35;...Mdid not reproduce in the maintainer's PTY run. On this transport themouse reports leak continuously, not intermittently.
3. The three-state mouse mode already exists natively — it just is not wired up
The native terminal session accepts mouse and mouse-motion as separate flags, and validates
exactly three modes:
But nothing in the app ever sets
enableMouseMovement:and the config schema only allows a boolean:
Net effect: startup always emits all four modes and therefore enables all-motion reporting —
?1003his what turns every pointer movement into anESC[<b;x;yMwrite, i.e. it is the thing thatsupplies this bug with a constant stream of sequences. Without it there is nothing to split.
So #60942/#60943 could land as plumbing rather than new machinery: accept
mouse: "disabled" | "drag" | "motion"(the native side already understands those three literals)and pass
mouseMovement: mode === "motion".dragsends?1002honly: click, drag, wheel and keys allkeep working, hover highlighting goes away, and the motion stream — the bulk of what leaks — disappears.
That does not fix the parser, but it removes the practical impact for mouse-forced users without turning
mouse off entirely, which is the only option the boolean gives us today.
4. On the parser side
Whatever the timeout is, the tail of an uncommitted sequence should never reach the editor. A truncated
CSI/OSCis never a legitimate keystroke, so dropping the partial sequence (or buffering until thefinal byte
0x40-0x7E) is safe, while emitting its tail is guaranteed garbage. This also covers thecb = 51case from the report above where theESCitself was present in stdin.Current workaround
A stdin relay between the pty and the TUI that re-assembles control sequences and drops reports with
(b & 32) && (b & 3) == 3before the app reads them. Clicks, drag, wheel and keys pass throughuntouched; the prompt stays clean. If a short-term mitigation is wanted, that is ~150 lines and I am
happy to upstream it as a patch — but the two changes above are the real fix.
zxyqq commentedon Nov 11, 2026
Control: the leak only reproduces without tmux (direct ConPTY → SSH PTY)
Environment:
2.2.7(TUI + background service)3.2abetween SSH and opencode, none of the cases below reproduceRepro A — fresh start (no tmux):
Running
opencodedirectly in the SSH shell shows:eeee/eeee/eeeein the prompt as soon as the TUI draws.Repro B — resume after suspend (no tmux):
opencodedirectly in the SSH shell.Ctrl+Z(SIGTSTP), thenfg(SIGCONT).:eeee/eeee/eeeeis typed into the prompt again.Repro C — resuming a specific session (no tmux):
opencode -s <session-id>renders:ffff/ffff/ffff:eeee/eeee/eeeein the prompt.Control:
Running the same commands inside tmux
3.2aon the same host/terminal does not leak. So the split only happens on the direct ConPTY → SSH pty path; tmux sitting in between appears to normalize/absorb the replies.In all failing cases the leading part is consumed: the visible text starts at
:/eeee, i.e. the replies to theESC ]10;?(foreground) andESC ]11;?(background) queries are split after the leadingESC, the parser swallows the prefix, and the remainder is typed as text.ffff/ffff/ffffis the terminal foreground (white) andeeee/eeee/eeeethe background (light gray), so this is the OSC 10 + OSC 11 reply pair concatenated.This matches the lone-ESC / 20 ms timeout described above.
SIGCONT/redraw is worth noting as an additional trigger: the TUI re-issues the color queries when it resumes, so everyfgcan produce a fresh leak when running outside tmux.Siarhei-Yarkavy commentedon Nov 13, 2026
In Tmux it is also reproducible but not always. I didn't press A, I pressed up/down/up/down... keys in random order and sometimes it prints codes A and B.
Siarhei-Yarkavy commentedon Nov 13, 2026
again some mouse code when I use opencode inside tmux
