Repository navigation
Description
Summary
A keystroke that arrives in the same terminal input batch as a bracketed paste is inserted before the pasted text. With CJK IMEs this makes the punctuation pressed to commit a multi-character candidate land before the candidate (,你好 instead of 你好,). Single-character candidates are unaffected.
Environment
- opencode version: 2.0.22 (Arch package
opencode 2.0.22-1) - OS: Arch Linux,
Linux 7.2.8-arch1-2 x86_64 - Terminal: Alacritty 0.17.0 on niri 26.04 (Wayland),
TERM=alacritty,COLORTERM=truecolor - Shell:
/usr/bin/bash - Install/channel: release package, latest channel
- Active plugins: none (no
opencode.json/opencode.jsonc, no.opencode/plugin)
Reproduction
Real-world (fcitx5 5.1.23 + fcitx5-chinese-addons 5.1.15, Pinyin, Alacritty on Wayland):
- Open a session prompt with the Chinese IME active.
- Type
nihaoso你好is the first candidate. - With the candidate list visible, press
,(this commits你好and then,). - Prompt shows
,你好instead of你好,. A single-character candidate (ni→你, then,) is ordered correctly.
Deterministic, no IME required — verified against the real 2.0.22 binary:
Write \x1b[200~AAA\x1b[201~B to the TUI's pty in a single write:
- Result: prompt renders
BAAA. - Control (paste in one write, then
Bin a later write): prompt rendersAAAB. - Control (
ABin one write): prompt rendersAB.
Minimal pty repro script (python3)
#!/usr/bin/env python3
"""Usage: python3 repro.py [same-chunk|separate-chunks]"""
import fcntl, os, pty, re, select, signal, struct, subprocess, sys, termios, time
mode = sys.argv[1] if len(sys.argv) > 1 else "same-chunk"
os.makedirs("/tmp/oc-repro", exist_ok=True)
master, slave = pty.openpty()
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 100, 0, 0))
env = dict(os.environ, TERM="xterm-256color",
XDG_DATA_HOME="/tmp/oc-repro-data", XDG_STATE_HOME="/tmp/oc-repro-state")
proc = subprocess.Popen(
["opencode", "--standalone", "--log-level", "error", "/tmp/oc-repro"],
stdin=slave, stdout=slave, stderr=slave, env=env, cwd="/tmp/oc-repro",
start_new_session=True)
os.close(slave)
out = bytearray()
def drain(seconds):
end = time.time() + seconds
while time.time() < end:
if select.select([master], [], [], 0.1)[0]:
try:
chunk = os.read(master, 65536)
except OSError:
return
if not chunk:
return
out.extend(chunk)
try:
drain(12) # wait for the TUI
if mode == "same-chunk":
os.write(master, b"\x1b[200~AAA\x1b[201~B")
else:
os.write(master, b"\x1b[200~AAA\x1b[201~")
drain(0.8) # let the paste settle
os.write(master, b"B")
drain(1.0)
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 101, 0, 0))
drain(0.5)
fcntl.ioctl(master, termios.TIOCSWINSZ, struct.pack("HHHH", 30, 100, 0, 0))
drain(1.5)
finally:
proc.send_signal(signal.SIGKILL)
proc.wait()
os.close(master)
def screen_rows(data, rows=30, cols=110):
grid = [[" "] * cols for _ in range(rows)]
i, row, col = 0, 1, 1
while i < len(data):
b = data[i]
if b == 0x1B:
m = re.match(rb"\x1b\[([0-9;?]*)([A-Za-z])", data[i:])
if m:
ps = [int(p) for p in m.group(1).replace(b"?", b"").split(b";") if p.isdigit()]
f = m.group(2)
if f in (b"H", b"f"):
row = ps[0] if len(ps) > 0 and ps[0] else 1
col = ps[1] if len(ps) > 1 and ps[1] else 1
elif f == b"G":
col = ps[0] if ps else 1
elif f == b"A":
row = max(1, row - (ps[0] if ps else 1))
elif f == b"B":
row += ps[0] if ps else 1
elif f == b"C":
col += ps[0] if ps else 1
elif f == b"D":
col = max(1, col - (ps[0] if ps else 1))
elif f == b"K":
m0 = ps[0] if ps else 0
a, z = (col - 1, cols) if m0 == 0 else (0, col) if m0 == 1 else (0, cols)
for c in range(a, z):
grid[row - 1][c] = " "
i += m.end()
continue
i += 1
elif b in (0x0A, 0x0D, 0x08):
if b == 0x0A: row += 1
elif b == 0x0D: col = 1
else: col = max(1, col - 1)
i += 1
elif 0x20 <= b < 0x7F:
if 1 <= row <= rows and 1 <= col <= cols:
grid[row - 1][col - 1] = chr(b)
col += 1
i += 1
else:
i += 1
return ["".join(r).rstrip() for r in grid]
print(f"mode={mode}")
for n, line in enumerate(screen_rows(bytes(out)), 1):
if "AAA" in line or "BAA" in line:
print(f" {n:2}| {line}")Observed:
$ python3 repro.py same-chunk
mode=same-chunk
17| BAAA
$ python3 repro.py separate-chunks
mode=separate-chunks
17| AAAB
Expected Behavior
Input is applied in the order it was received: the pasted text first, then the following keystroke (AAAB, or 你好, with an IME).
Actual Behavior
The following keystroke is inserted first (BAAA / ,你好).
Additional Context
Root cause is the prompt's paste handling being asynchronous while key input is synchronous:
packages/tui/src/component/prompt/index.tsxonPaste(L1778–1801) callsevent.preventDefault()and thenvoid enqueuePaste((changed) => pasteInputText(normalizedText, changed))(L1800).enqueuePaste(L377–396) chains the work ontopasteQueue = pasteQueue.then(...), so insertion runs in a later microtask.pasteInputText(L1441) awaitsresolvePastedAttachments(...)(async filesystem probing) beforeinput.insertText(normalizedText)(L1472).- Regular key input is inserted synchronously by the textarea, so any key dispatched in the same input batch is applied first.
Why this shows up with IMEs: terminals intentionally send multi-character IME commits as bracketed paste (alacritty/alacritty#7092), and Alacritty treats that as intentional (alacritty/alacritty#8934 was closed; alacritty/alacritty#8764 is the same class of failure in another TUI). Single-character commits are sent as raw text, which is why only multi-character candidates break.
Possible fix directions:
- Insert plain-text bracketed pastes synchronously when they cannot resolve to a local attachment (e.g. a cheap synchronous existence check before choosing the async path); or
- Process subsequent input events after the paste queue drains.
Workaround: commit the candidate with Space first, then type the punctuation separately (separate input batches, so the order is preserved).
Activity
github-actions commentedon Oct 4, 2026
This issue might be a duplicate of an existing issue. Please check:
opencodejr commentedon Oct 27, 2026
Thanks for the detailed report and the repro script. I can confirm this on 2.0.22, and it still happens on the latest
dev(c250c272).Sending a bracketed paste followed by a keystroke in a single terminal write (
\x1b[200~AAA\x1b[201~B) leavesBAAAin the prompt. If the paste and theBarrive in separate writes, you get the correctAAAB. Your pty script gives the same result on both versions.The recording below shows the reproduction on 2.0.22, the screenshot shows the same result on
dev, and the script below reproduces it. Your analysis matches what I see in the code: the prompt's paste handler inserts the text asynchronously, throughenqueuePaste/pasteInputText, while ordinary keys are inserted right away. A key in the same input batch therefore lands first.Until this is fixed, your workaround still works: commit the candidate with Space, then type the punctuation separately.
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.