Skip to content

tui: clipboard-pasted text lands with multi-second delay in question dialog answer field #44857

Closed

Description

@JavaGT

Summary

When dictating into a question dialog's free-text field (the "type your own answer" option of the multiple-choice question prompt), text inserted via clipboard paste takes several seconds to appear. The same dictation method into the main message composer lands instantly, and manual keyboard typing into the question dialog field is fully responsive — so the delay appears specific to how the question dialog's text input handles pasted input.

Environment

  • opencode version: 0.0.0-beta-18138
  • OS: macOS (Darwin 25.6.0, arm64)
  • Terminal: cmux on the client Mac, connected over SSH to the host running opencode inside a tmux session (TERM=xterm-256color, COLORTERM=truecolor)
  • Shell: /bin/zsh
  • Install/channel: beta
  • Active plugins: none configured

Reproduction

  1. Run the opencode TUI (v0.0.0-beta-18138) in tmux over SSH.
  2. Use Handy (voice-to-text) configured with the clipboard paste insertion method — it copies the transcript to the clipboard and simulates a paste keystroke.
  3. Trigger an agent question dialog (e.g. a tool asking a multiple-choice question) and select the free-text/custom answer option.
  4. Dictate a response so Handy pastes it into the custom-answer field.
  5. Compare with dictating into the main message composer.

Expected Behavior

Pasted/dictated text should appear in the question dialog's custom-answer field as promptly as it does in the main message composer.

Actual Behavior

The text appears only after a noticeable fixed lag of roughly a few seconds, regardless of dictated length. Observed behavior:

  • Main composer + same Handy clipboard-paste method: instant.
  • Question dialog custom-answer field + manual keyboard typing: instant/responsive.
  • Question dialog custom-answer field + Handy clipboard-paste: consistent multi-second delay before the text shows up.

No missing or duplicated characters were observed — the text arrives complete once the delay passes. Reproduces consistently.

Additional Context

Since manual typing is fast and the identical paste path is instant in the main composer, the culprit seems to be in the question dialog's input handling of pasted text (e.g. how it consumes bracketed-paste input from the terminal vs. the main editor). Happy to help narrow it down further if useful.

Activity

  1. JavaGT commented on Aug 25, 2026

    @JavaGT
    Author

    Follow-up: root cause narrowed — TUI paste ingestion exonerated; destructive submit-on-newline reproduced

    Original reporter here. We instrumented the whole chain end-to-end. Two headline results:

    1. Paste ingestion is fast everywhere it can be measured

    All on v0.0.0-beta-18138, macOS arm64, tmux over SSH:

    Test Latency
    @opentui/core StdinParser emitting a paste event split across SSH-style chunks ~5 ms
    Isolated harness: question-dialog-style textarea, default handlePaste path ~1–2 ms
    Isolated harness: composer-style onPaste handler ~0.2–0.9 ms
    Real TUI in tmux, genuine paste into main composer (buffer paste) 88 ms
    Real TUI in tmux, genuine paste into the question dialog's custom-answer textarea 27 ms

    Terminal modes (mouse tracking, alt-screen) are identical between composer and dialog states, so there is no mode-related divergence either. The multi-second lag in the original report does not originate anywhere inside opencode/opentui.

    2. Actual failure mode: unwrapped paste + Return-bound submit corrupts multi-line answers

    Environment detail discovered during diagnosis: the reporter's terminal client (cmux, over SSH) sends pasted text as plain characters without bracketed-paste wrapping — verified by pasting into cat -v inside tmux and seeing no ESC[200~/ESC[201~ markers.

    Consequence: every newline in a pasted or dictated block arrives as a genuine Return keypress. In the question dialog's edit state (QUESTION_MODE, "type your own answer"), Return is bound to Submit answer edit, so any multi-line input self-submits on its first line break and the remaining lines scatter into option navigation.

    Deterministic repro (verified interactively): focus the custom-answer textarea, then inject first line submits + Return + second line wanders. Result: the answer submits on the first line; the second line lands outside the field.

    This explains the original symptoms: dictated answers (Handy, clipboard-paste method) appeared "late/broken/never", while manual typing was fine and single-segment input worked. The main composer is unaffected because it overrides onPaste and normalizes multi-line content itself.

    Suggestions

    • Harden question edit mode against unwrapped-paste bursts — e.g. treat Return as newline-insert when the textarea received a high-rate character burst within the last few hundred ms, or require an explicit confirmation for submit when editing began < N seconds ago.
    • Alternatively, document that reliable paste handling assumes bracketed paste from the terminal.
    • Separately, the missing bracketed-paste support belongs on the terminal-client side (cmux) — filing there too.

    Happy to test patches.


    Correction (same day): the reporter's primary symptom involves single-line pastes with no line breaks, which still fail to appear — so the Return-submits mechanism above is a real, reproducible adjacent defect but does not explain the main report. Diagnosis continues; measurements in the table above remain valid.

  2. JavaGT commented on Aug 25, 2026

    @JavaGT
    Author

    Final follow-up: mechanism confirmed — in the question menu, printable keys are live commands, which destroys unwrapped paste input

    Reporter here again. With the reporter watching live, injecting the plain string 12abc into the terminal while a question dialog sat on its option list (custom-answer box not opened) caused the form to react to the text — selecting/submitting an option — instead of displaying it.

    Complete causal chain for the original report

    1. The reporter's terminal client (cmux over SSH) sends pasted text as plain characters without bracketed-paste wrapping — verified by pasting into cat -v: no ESC[200~…ESC[201~ markers. Pasted input is therefore byte-identical to very fast typing.
    2. In the question dialog's default state, bare printable keys are bound as UI commands: 1–9 select answers (instantly submitting single-choice questions), h/j/k/l/arrows navigate, Return submits, Escape dismisses (packages/tui/src/routes/session/question.tsx binding sets).
    3. Any dictated/pasted burst arriving while the custom-answer textarea is not yet focused is consumed as UI actions: the text vanishes, forms auto-submit, selections jump. Whether a given paste "works" reduces to millisecond-level focus timing — matching the originally reported intermittent lag/broken-input behavior.
    4. The main composer is immune to the identical byte stream because plain printable keys are never commands there.

    What was ruled out

    Paste ingestion speed inside opencode/opentui: 1–88 ms end-to-end everywhere we could measure (table in the earlier comment), including a genuine paste landing in the question dialog's textarea itself in 27 ms. Ingest performance was never the issue — input routing semantics in the modal state are.

    Suggested fixes (any one would do)

    • Don't bind bare digit/letter keys in QUESTION_MODE (require a modifier for numeric selection), or
    • Suppress character-command bindings for a short window after dialog open / tab switch, or
    • Detect a rapid character run hitting command bindings and surface it as recoverable pasted text.

    Note for other reporters: terminals that send proper bracketed paste (most desktop terminals) deliver pastes as PasteEvents routed to the focused widget, so they won't trigger this. It takes an unwrapped-paste client to expose the command-key hazard — which also means the client side (cmux) is worth a feature request for bracketed-paste support.

  3. JavaGT commented on Aug 25, 2026

    @JavaGT
    Author

    Root cause confirmed via byte-level capture: paste events are silently discarded when the question dialog has no focused editor

    Original reporter here. We attached a transparent PTY logger (timestamps + hex dump of everything reaching the TUI's stdin) and captured a real failing dictation inside the question dialog. Final story:

    What actually arrives (healthy)

    +0ms    ESC[200~                                  (bracketed-paste start)
    +20ms   "Testing, testing, one, two, three" ESC[201~
    

    Properly wrapped bracketed paste, delivered instantly and completely. Transport, encoding, timing: all clean. (Earlier conclusion that the reporter's terminal sends unwrapped text was wrong — tmux strips/adds markers per-pane depending on each pane's own mode, so a cat -v probe cannot reveal markers. Mea culpa.)

    Why the text still vanished

    The captured keystroke stream shows the reporter pressed ↓ ×3 to highlight "Type your own answer", then began dictating — without pressing Enter/clicking to open the editor. During the multi-second transcription window the dialog stayed in selection mode (store.editing === false, provable from the arrow keys still navigating). When the paste finally arrived:

    • PasteEvents are dispatched to the focused renderable's paste handler only;
    • in selection mode no TextareaRenderable is mounted/focused, and no global listener handles paste;
    • result: the event has zero listeners and is dropped without any feedback.

    So the failure condition is: any dictation/paste that completes while the custom-answer editor is not open. Human typing always works because by definition you only type once the box is open. The main composer always works because its textarea is permanently mounted and focused. Everything else (intermittency, "fixed lag", text "ending up elsewhere" after retries) follows from this.

    Suggested fixes (opencode side)

    1. Catch-all paste routing: when a PasteEvent reaches the question dialog in selection mode, auto-enter editing on the custom answer (when enabled) and insert the text — i.e., treat paste as intent-to-answer.
    2. Minimally: surface undelivered paste ("paste ignored — open a field first") instead of dropping silently.
    3. Docs/workaround meanwhile: press Enter on "Type your own answer" before finishing dictation.

    Happy to test patches.

  4. JavaGT commented on Aug 25, 2026

    @JavaGT
    Author

    Root cause (final, confirmed live): pasted text inserts into the question dialog's textarea but the UI never repaints until the next keypress

    Original reporter here. After byte-level capture plus a controlled replay against the live binary, the actual mechanism is now confirmed — and it supersedes the earlier theories in this thread (transport, parser speed, and routing were all red herrings; measurements showing healthy ingestion were accurate).

    The experiment

    With the question dialog open and the custom-answer edit box focused (focus proven by hand-typing a character), two bracketed pastes were replayed into the terminal with byte-exact control:

    1. ESC[200~Alpha one ESC[201~ — delivered as a single chunk
    2. ESC[200~ … 17ms … Bravo two ESC[201~ — delivered split across chunks, matching a real dictation capture

    Result reported live by the user: neither string appeared. Then the user pressed ↑, and both strings immediately appeared in the edit box.

    Mechanism

    The pasted text is inserted into the textarea's buffer — but the question dialog's <textarea> (packages/tui/src/routes/session/question.tsx, ~line 427) declares none of the reactive callbacks the main composer's textarea declares (onContentChange / onCursorChange, see packages/tui/src/component/prompt/index.tsx ~1373-1383, which push updates into Solid stores on every mutation). With no state change and no dirty-frame trigger, the renderer never repaints the inserted content while the UI is otherwise idle. The next key event (any key) forces a frame and the previously "missing" text pops into view.

    This explains the full history of symptoms:

    • dictation "not appearing at all" in forms (UI idle → no frame until user gives up),
    • text appearing "seconds later" or "after moving away" (some unrelated event flushed a frame),
    • hand-typed characters always visible (each keystroke drives its own rendering),
    • main composer always fine (its callbacks force re-renders on every insertion),
    • all transport/parser/widget-ingest measurements being fast and clean (insertion genuinely worked every time).

    Suggested fix

    Wire the same callbacks the composer uses onto the question dialog's textarea — e.g.

    onContentChange={() => { /* touch a signal to trigger re-render */ }}

    (or address the underlying issue in @opentui/core: programmatic insertText() from a paste event should mark the renderable dirty / schedule a frame even when no reactive subscribers are attached).

    Workaround for users until patched

    After dictating/pasting into a question form field, press any harmless key (e.g. an arrow key, or space+backspace) — the text will instantly appear.

  5. JavaGT commented on Aug 25, 2026

    @JavaGT
    Author

    Proposed fix + validation results

    Following up on the root-cause comment: we validated fix candidates against @opentui/core 0.4.5 with a painted-frame harness (captureCharFrame() after pasteBracketedText, rather than reading the buffer — buffer reads always pass and mask this bug).

    Results (0.4.5, bun harness)

    Variant Buffer Painted frame
    A) bare <textarea> (current dialog code) ✓ ✓
    B) <textarea onContentChange={() => renderer.requestRender()} /> ✓ ✓
    C) composer-style onPaste self-insert ✓ ✓

    Notable: on 0.4.5 even variant A paints — meaning the missing-repaint behavior regressed somewhere between the published 0.4.5 and whatever @opentui/core build is bundled into the running binary (0.0.0-beta-18138; its bundle is minified so we couldn't diff statically — worth auditing what opentui revision the release embeds vs. the catalog: pin).

    Recommended fix (works regardless of the embedded opentui revision)

    In packages/tui/src/routes/session/question.tsx, give the custom-answer textarea the same reactive wiring the composer relies on — the renderer reference is already in scope:

    <textarea
      ref={(val: TextareaRenderable) => {
        textarea = val
        val.traits = { status: "ANSWER" }
        queueMicrotask(() => { val.focus(); val.gotoLineEnd() })
      }}
      initialValue={input()}
      placeholder="Type your own answer"
      placeholderColor={theme.textMuted}
      minHeight={1}
      maxHeight={6}
      textColor={theme.text}
      focusedTextColor={theme.text}
      cursorColor={theme.primary}
      cursorStyle={tuiConfig.cursor}
      onContentChange={() => renderer.requestRender()}
    />

    This guarantees a frame on every buffer mutation (paste included) even if the embedded core stops self-scheduling renders on programmatic inserts. Optionally also file upstream on @opentui/core: EditBufferRenderable.insertText() triggered via a paste event should mark the renderable dirty / wake the render loop when idle.

    Happy to test a patched build — the reporter can reproduce on demand within seconds using their dictation setup.

  6. JavaGT commented on Aug 25, 2026

    @JavaGT
    Author

    Root cause refined: shipped form-dialog writes paste into the Solid store but never into the mounted editor buffer

    Final correction after auditing the actual bundle embedded in 0.0.0-beta-18138 (recovered via byte-offset extraction from the installed executable).

    What ships

    The reporter's binary contains a form dialog implementation that does not match current main's packages/tui/src/routes/session/question.tsx (the shipped one replies via t.session.form.reply, supports string/number/integer/multiselect questions, dynamic placeholders like name@example.com). Its paste handler, de-minified:

    intercept("paste", (xe) => {
      if (mode.current() !== FORM_MODE) return
      const text = normalize(decodePasteBytes(xe.bytes))
      if (!X(text)) return        // X(): select custom option + setStore("custom", key, existing + text)
                                  //     + setStore("editing", true)   ← STORE WRITE ONLY
      xe.preventDefault()         // ← suppresses the widget's own paint-capable insert
    })

    The mounted <textarea> only reads its value at mount (initialValue). So when the edit box is already open:

    1. paste arrives → handler writes text into the Solid store,
    2. preventDefault() stops the widget's built-in handlePaste (which would have inserted + requestRender()ed),
    3. nothing tells the live editor buffer about the new text → invisible,
    4. navigating selection away & back closes/reopens the box → remount picks up initialValue from the store → text suddenly appears.

    Exactly matches the reporter's observed behavior, including the "text pops in after pressing ↑/↓" confirmation.

    Fix

    In that paste path, when the editor is already mounted (T.editing === true), route the text into the live buffer instead of (or in addition to) the store:

    if (T.editing) { D?.insertText(text); /* plus store sync via onContentChange */ }

    and/or make the textarea controlled (effect syncing store.custom[key] → D.setText(...) while editing), plus a requestRender() safety net.

    Version note

    Current public main's packages/tui/src/routes/session/question.tsx (last touched 2026-08-07) is a different implementation relying on the default widget paste path — which repaints correctly. If this forms dialog still exists on a preview branch, apply the fix there; users on affected builds can work around it by pressing any arrow key after dictating/pasting to force the pending frame.

  7. neriousy commented on Oct 20, 2026

    @neriousy
    Member

    V2 now inserts pasted text directly into the focused form editor instead of updating only the stored initial value, so it no longer needs an editor remount to appear. Pasting on a custom-choice question also opens its answer editor, and regression coverage checks pasting into an already active editor. If you still see this on v2, please open a new issue with the details.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions