Skip to content

tui: delayed OSC 10/11 replies leak into prompt as ":ffff/ffff/ffff" #57345

Open

Description

@ZaphireLiu

Summary

On opencode 2.1.0, starting the TUI over SSH can leave the tail of the terminal's OSC 11 color reply in the prompt as :ffff/ffff/ffff. The TUI sends OSC 10/11 color probes at startup; when the reply is fragmented (here: ESC ]11;rgb arrives first, the rest arrives about 2s later), the incomplete prefix is discarded and the late tail is treated as typed input. It is cosmetic, but it pollutes the first input and can be submitted accidentally.

Environment

  • opencode version: 2.1.0
  • OS: Ubuntu 24.04.5 LTS, Linux 6.8.0-147-generic x86_64
  • Terminal: Windows Terminal (PowerShell 7) over SSH; remote TERM=xterm-256color, TERM_PROGRAM/COLORTERM/VTE_VERSION unset
  • Shell: remote /bin/bash; local PowerShell 7
  • Install/channel: release standalone binary at ~/.opencode/bin/opencode, latest channel
  • Active plugins: none found
  • Multiplexer: none ($TMUX unset)

Reproduction

Real-world:

  1. SSH into a Linux host from a terminal that delivers OSC 10/11 replies in fragments.
  2. Run opencode 2.1.0.
  3. Before typing anything, the prompt contains :ffff/ffff/ffff (sometimes the preceding ]10;rgb:...]11;rgb:... is visible too). It reproduces on every startup.

Deterministic local PTY reproduction:

  1. Start opencode 2.1.0 under a PTY.
  2. Wait until the ESC ]11;? query is written.
  3. Write ESC ]10;rgb:2e2e/3434/3434 BEL and ESC ]11;rgb (stop in the middle of the OSC 11 reply).
  4. Wait 2 seconds, then write :ffff/ffff/ffff BEL.
  5. The rendered prompt contains exactly :ffff/ffff/ffff.

Control results with the same harness:

  • Whole reply in one write: no leak.
  • Split at ESC ]11;rgb with a 50 ms gap: no leak.
  • Split at ESC ]11;rgb with a 2 s gap: leaks the exact string.

This points to the escape-sequence timeout in the stdin parser discarding the incomplete OSC prefix, after which the late tail is parsed as ordinary text.

Expected Behavior

Terminal query replies, including fragmented or delayed ones, should be consumed as terminal responses and must never appear in the prompt. Late bytes belonging to an already-abandoned reply should be drained/discarded.

Actual Behavior

The tail of the reply is rendered into the prompt. It is stable on every startup on the affected SSH terminal. It is not persisted in the session database, so it appears to be a startup input-parser race.

Additional Context

Minimal PTY reproduction script
#!/usr/bin/env python3
import os, pty, select, signal, struct, time, fcntl, termios

pid, fd = pty.fork()
if pid == 0:
    os.execvpe("opencode", ["opencode"], os.environ)

fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 120, 0, 0))
os.set_blocking(fd, False)

out = bytearray()
sent_prefix = False
sent_tail = False
start = time.time()

while time.time() < start + 6:
    r, _, _ = select.select([fd], [], [], 0.05)
    if r:
        try:
            out += os.read(fd, 65536)
        except OSError:
            break

    if not sent_prefix and b"]11;?" in out:
        sent_prefix = True
        # First fragment: complete OSC 10 reply, then only the prefix of OSC 11
        os.write(fd, b"\x1b]10;rgb:2e2e/3434/3434\x07\x1b]11;rgb")
        deadline = time.time() + 2.0

    if sent_prefix and not sent_tail and time.time() >= deadline:
        sent_tail = True
        # Late tail of the OSC 11 reply; this is what leaks into the prompt
        os.write(fd, b":ffff/ffff/ffff\x07")

time.sleep(1)
os.kill(pid, signal.SIGTERM)

The rendered output then contains the literal string :ffff/ffff/ffff.

Activity

  1. opencode-agent commented on Oct 20, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report and the PTY script. I was able to reproduce this.

    On @opencode/[email protected] I ran the TUI under a PTY and replied to the startup ESC ]11;? query in two parts:

    1. The full OSC 10 reply plus ESC ]11;rgb.
    2. Two seconds later, :ffff/ffff/ffff BEL.

    The late tail showed up in the prompt every time. With a 50 ms gap nothing leaked. The same thing happens on the latest dev (d30e990) run from source. The screenshot below shows the dev TUI with :ffff/ffff/ffff in the prompt, and the script below reproduces it (python3 repro.py 2; pass 0.05 for the control run).

    There is no video because our TUI automation tool can't send raw terminal reply bytes, so I used a real PTY instead.

    The cause looks like what you describe: the stdin parser's escape timeout drops the unfinished OSC 11 prefix, and the rest is then treated as typed text. Related reports, but not the same problem: #55641 and #54500 (probe replies leaking after exit), #40035 and #16547 (probe replies printed under tmux).

    Screenshot

    Reproduction script (repro.py)
    #!/usr/bin/env python3
    """Reproduce anomalyco/opencode#57345: a delayed OSC 11 reply tail leaks into the TUI prompt.
    Usage: python3 repro.py [GAP_SECONDS] [-- command...]   (default command: opencode)
    Writes raw PTY output to out-<gap>.bin and reports whether ':ffff/ffff/ffff' was rendered."""
    import os, pty, select, signal, struct, sys, time, fcntl, termios, tempfile
    gap = float(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1] != "--" else 2.0
    cmd = sys.argv[sys.argv.index("--")+1:] if "--" in sys.argv else ["opencode"]
    proj = tempfile.mkdtemp(prefix="oc57345-")
    pid, fd = pty.fork()
    if pid == 0:
        os.chdir(proj)
        env = dict(os.environ, TERM="xterm-256color")
        os.execvpe(cmd[0], cmd, env)
    fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 120, 0, 0))
    os.set_blocking(fd, False)
    out = bytearray(); mark = 0
    sent_prefix = sent_tail = False; deadline = None
    start = time.time()
    while time.time() < start + 12 + gap:
        r, _, _ = select.select([fd], [], [], 0.02)
        if r:
            try: out += os.read(fd, 65536)
            except OSError: break
        if not sent_prefix and b"]11;?" in out:
            sent_prefix = True
            os.write(fd, b"\x1b]10;rgb:2e2e/3434/3434\x07\x1b]11;rgb")   # OSC 10 whole, OSC 11 cut short
            deadline = time.time() + gap
        if sent_prefix and not sent_tail and time.time() >= deadline:
            sent_tail = True; mark = len(out)
            os.write(fd, b":ffff/ffff/ffff\x07")                         # late tail of the OSC 11 reply
    os.kill(pid, signal.SIGTERM)
    open(f"out-{gap}.bin", "wb").write(out)
    after = bytes(out[mark:])
    print(f"gap={gap}s prefix_sent={sent_prefix} tail_sent={sent_tail} leaked={b'ffff/ffff/ffff' in after}")

    Run it: python3 repro.py

  2. syhhyl commented on Oct 29, 2026

    @syhhyl

    Still reproduces on 2.2.0 (Linux, standalone binary). Same bug, but the fragmentation point is different from the report: here it splits right after the lone ESC, not in the middle of the payload.

    Environment

    • opencode 2.2.0 (~/.opencode/bin/opencode, latest channel)
    • Terminal: Ghostty (TERM=xterm-ghostty); inside tmux 3.4 it also reproduces (TERM=xterm-256color, tmux ccolour enabled)
    • Reproduces both with and without tmux
    • Terminal default foreground is #eeeeee

    Symptom

    The prompt is pre-filled with ]10;rgb:eeee/eeee/eeee (usually seen as :eeee/eeee/eeee, since the leading ]10;rgb is easy to miss). Cosmetic, but one accidental Enter sends it. Nothing was ever submitted here: ~/.local/state/opencode/prompt-history.jsonl has no such entry.

    Deterministic reproduction (2.2.0)

    1. start opencode under a PTY (or in a tmux pane)
    2. once the UI is up, write a single ESC byte
    3. wait 50 ms
    4. write ]10;rgb:eeee/eeee/eeee

    Rendered prompt afterwards:

    ┃  ]10;rgb:eeee/eeee/eeee
    ┃
    ┃  Build auto · deepseek-flash bosc · high
    

    Control results with the same harness:

    • whole reply in one write, BEL or ESC \ terminated: consumed correctly, no leak
    • ESC + rest with a 10 ms gap: no leak
    • ESC + rest with a 30 ms gap: leaks

    Cause

    In the bundled StdinParser, a pending lone ESC is flushed as the Escape key after timeoutMs = 21 and sets justFlushedEsc. The ground-state recovery then only re-syncs on [ (0x5B):

    if (this.justFlushedEsc) {
      if (t === 91) { /* '[' */ this.cursor += 1; this.state = { tag: "esc_recovery" }; continue }
      this.justFlushedEsc = false
    }
    // anything else falls through:
    this.emitKeyOrResponse("unknown", ...)   // -> typed into the prompt

    So any terminal reply whose continuation arrives after a >20 ms pause and starts with ] (OSC), P (DCS), _ or ^ (APC/PM) is typed as keys instead of being consumed. The prefix is dropped because the incomplete escape sequence was already discarded, which is why only the tail ...:eeee/eeee/eeee is visible.

    Status of the fix

    anomalyco/opentui#1793 ("native: hold split terminal replies while a DA1 sentinel is out", merged 2026-10-26) and anomalyco/opentui#1816 ("core: end terminal reply waits at their DA1 reply", merged 2026-10-27) describe exactly this failure mode and fix it. The 2.2.0 binary does not contain them — OT_INPUT_EXPECT_SENTINEL is absent from the bundled parser. So this should go away with the next OpenTUI bump; can this stay open until then?

  3. flexiphp commented on Oct 29, 2026

    @flexiphp

    Possibly related: I see similar stray escape-sequence text in the prompt on Windows, with a different string.

    Environment

    • OpenCode: v2.2.0 (opencode --version)
    • OS: Windows [version, e.g. Windows 11 23H2]
    • Terminal host: Orca 1.4.243 (terminal tabs inside the app)

    What I see

    • A fragment of a truecolor SGR sequence appears as typed text in the prompt input, e.g. 38;2;91;73;124m (the leading ESC[ is missing).
    • The text changes (different RGB values) every time I switch to another tab and come back.
    • It disappears by itself when I type or press the left/right arrow keys, so it looks cosmetic. I clear it with Ctrl+U before pressing Enter.
    • It shows up in sessions started by the automated dispatch above; I have not yet checked whether it also happens when OpenCode is started by hand in a plain Windows terminal.

    Why I think it may be related

    • Same class as the OSC 10/11 leak described here: an incomplete escape sequence from the terminal seems to be treated as typed input. Here the leaked text is an SGR color sequence instead of an OSC reply, so it may be a different path, or the same parser issue.
    • Version matches the affected versions in this issue (2.1.0 and 2.2.0).

    I have not confirmed whether the OpenTUI PRs mentioned above (#1793, #1816) change this behavior; I will retest once a release containing them is available. Happy to provide more details or test a build.

  4. niven823 commented on Oct 30, 2026

    @niven823

    I'm seeing the same issue on v2.2.2. In my case, the text that appears in the prompt is :eeee/eeee/eeee.

  5. Anthony14FR commented on Nov 12, 2026

    @Anthony14FR

    I'd like to help with this, together with #57517 and #60268, which look like the same root cause: the stdin parser's escape timeout drops an unfinished sequence and the tail is typed into the prompt. That parser lives in @opentui/core, not in this repo. Would you rather have the fix in OpenTUI (keep a pending OSC/CSI sequence open longer when it arrives split), or a change on the OpenCode side? I'll follow whichever you prefer.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions