openclaw - 💡(How to fix) Fix TUI: Ctrl+V image paste with inline [imgN] tokens (macOS)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The TUI still has no native clipboard image paste in v2026.5.28. #6565 was closed as "completed" but no shipping code landed — PR #24083 (the implementation attempt) was closed without merge. This issue documents a working, minimal approach and a reference monkey-patch that landed end-to-end in ~150 LOC against the current bundle, hoping it shortens the path to a real PR.

Root Cause

The TUI still has no native clipboard image paste in v2026.5.28. #6565 was closed as "completed" but no shipping code landed — PR #24083 (the implementation attempt) was closed without merge. This issue documents a working, minimal approach and a reference monkey-patch that landed end-to-end in ~150 LOC against the current bundle, hoping it shortens the path to a real PR.

Fix Action

Fix / Workaround

The TUI still has no native clipboard image paste in v2026.5.28. #6565 was closed as "completed" but no shipping code landed — PR #24083 (the implementation attempt) was closed without merge. This issue documents a working, minimal approach and a reference monkey-patch that landed end-to-end in ~150 LOC against the current bundle, hoping it shortens the path to a real PR.

Working bash patcher + injected JS against v2026.5.28: https://gist.github.com/idanshimon/387effa4e4031b525c5f06a7d94aea32 (~280 lines of bash that wraps a Python heredoc which injects 4 patch blocks into dist/tui-*.js).

If a maintainer wants me to spin this into a PR (with proper TypeScript source, tests, and cross-platform support), happy to — just say the word. Otherwise the patcher is in active use on my machine, and the diff is small enough to lift into a proper change if anyone else wants to drive it.

Code Example

// node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts
/**
 * Insert text at the current cursor position.
 * Used for programmatic insertion (e.g., clipboard image markers).
 * This is atomic for undo - single undo restores entire pre-insert state.
 */
insertTextAtCursor(text: string): void;

---

[user types]                                 prompt buffer
Cmd+Shift+Ctrl+4region                    (empty)
Ctrl+V                                       [img1] |
Cmd+Shift+Ctrl+4 → another region            [img1] |
Ctrl+V                                       [img1] [img2] |
"compare these two screenshots"              [img1] [img2] compare these two screenshots|
Backspace on ] of [img2]                     [img1] compare these two screenshots|
Entersubmit                               (sent with media attachment for [img1] only)

---

if (matchesKey(data, Key.ctrl("v"))) {
       if (_handleClipboardImagePaste(this)) return;
       // no image in clipboard — fall through to default paste
   }

---

if (matchesKey(data, Key.backspace)) {
       const hit = _isAtImageTokenClose(this);
       if (hit) {
           const lines = this.getLines();
           const cursor = this.getCursor();
           const line = lines[cursor.line];
           const newLine = line.substring(0, cursor.col - hit.tokenLen) + line.substring(cursor.col);
           const newLines = lines.slice();
           newLines[cursor.line] = newLine;
           this.setText(newLines.join("\n"));
           _editorTokenMap(this).delete(hit.idx);
           return;
       }
   }

---

const finalMessage = (process.platform === "darwin")
       ? _expandImgTokensInMessage(params.editor, value)
       : value;
   params.sendMessage(finalMessage);
RAW_BUFFERClick to expand / collapse

TUI: Ctrl+V image paste with inline [imgN] tokens (macOS)

Summary

The TUI still has no native clipboard image paste in v2026.5.28. #6565 was closed as "completed" but no shipping code landed — PR #24083 (the implementation attempt) was closed without merge. This issue documents a working, minimal approach and a reference monkey-patch that landed end-to-end in ~150 LOC against the current bundle, hoping it shortens the path to a real PR.

Why now / why this shape

The pi-tui Editor class already exposes the exact API needed:

// node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts
/**
 * Insert text at the current cursor position.
 * Used for programmatic insertion (e.g., clipboard image markers).
 * This is atomic for undo - single undo restores entire pre-insert state.
 */
insertTextAtCursor(text: string): void;

The d.ts literally calls out "clipboard image markers" as a use case, and getCursor() / getLines() / setText() are public. Nothing consumes any of this in the TUI today.

CustomEditor extends Editor in src/tui/components/custom-editor.ts (bundled at dist/tui-*.js ~line 1287) already has a clean handleInput(data) chain of matchesKey(data, Key.ctrl("x")) branches — a Key.ctrl("v") branch slots in cleanly next to the existing ctrl("l"), ctrl("o"), ctrl("p"), ctrl("g"), ctrl("t") ones. Same for Key.backspace smart-delete of completed tokens.

So this isn't a "needs deep refactor" feature — the surface area is already designed for it.

Proposed UX

[user types]                                 prompt buffer
Cmd+Shift+Ctrl+4 → region                    (empty)
Ctrl+V                                       [img1] |
Cmd+Shift+Ctrl+4 → another region            [img1] |
Ctrl+V                                       [img1] [img2] |
"compare these two screenshots"              [img1] [img2] compare these two screenshots|
Backspace on ] of [img2]                     [img1] compare these two screenshots|
Enter → submit                               (sent with media attachment for [img1] only)

Tokens are numbered per editor instance ([img1], [img2], ...). Smart backspace on the closing ] of an [imgN] token deletes the whole token in one stroke; backspace elsewhere is unchanged.

Reference implementation

Working bash patcher + injected JS against v2026.5.28: https://gist.github.com/idanshimon/387effa4e4031b525c5f06a7d94aea32 (~280 lines of bash that wraps a Python heredoc which injects 4 patch blocks into dist/tui-*.js).

Four injection points:

  1. Imports + helpers at top of tui-submit.ts region:

    • _readClipboardImage() — shells out to pngpaste -
    • _saveClipboardImage(buf) — writes to ~/.openclaw/media/inbound/clipboard-<uuid>.png
    • _editorTokenMap(editor) — per-editor Map<number, absolutePath>
    • _nextImgIdx(editor) — max of existing [imgN] in text + map + 1
    • _handleClipboardImagePaste(editor) — orchestrates: read → save → editor.insertTextAtCursor(\[img${idx}] `)`
    • _isAtImageTokenClose(editor) — uses editor.getCursor() + editor.getLines() to detect [img\d+]$ immediately left of cursor
    • _expandImgTokensInMessage(editor, value) — appends [media attached: <abs>] lines for each [imgN] in the message and clears the consumed entries from the map
  2. Ctrl+V handler in CustomEditor.handleInput, slotted immediately after if (isKeyRelease(data)) return;:

    if (matchesKey(data, Key.ctrl("v"))) {
        if (_handleClipboardImagePaste(this)) return;
        // no image in clipboard — fall through to default paste
    }
  3. Smart backspace in the same block:

    if (matchesKey(data, Key.backspace)) {
        const hit = _isAtImageTokenClose(this);
        if (hit) {
            const lines = this.getLines();
            const cursor = this.getCursor();
            const line = lines[cursor.line];
            const newLine = line.substring(0, cursor.col - hit.tokenLen) + line.substring(cursor.col);
            const newLines = lines.slice();
            newLines[cursor.line] = newLine;
            this.setText(newLines.join("\n"));
            _editorTokenMap(this).delete(hit.idx);
            return;
        }
    }
  4. Submit-time expansion in createEditorSubmitHandler — replace the bare params.sendMessage(value); with:

    const finalMessage = (process.platform === "darwin")
        ? _expandImgTokensInMessage(params.editor, value)
        : value;
    params.sendMessage(finalMessage);

End result: message text retains the [imgN] tokens (so it stays readable in scrollback / replies); attachments are appended as [media attached: /Users/.../clipboard-<uuid>.png] lines that the runtime already understands.

Verified behavior on macOS

  • Single image: Ctrl+V[img1] inserted, send → assistant receives image ✓
  • Two images: Ctrl+V Ctrl+V[img1] [img2] , send → both images received in correct order ✓
  • Backspace on ] of [img2] after multi-paste → whole token deleted, [img1] preserved, re-paste resumes at [img2]
  • Empty clipboard / non-image clipboard → Ctrl+V falls through to pi-tui's default paste handling ✓
  • After send, the per-editor token map is cleared for consumed tokens; subsequent unrelated screenshots get the next free index ✓

Cross-platform scope

The hook points are platform-neutral; only _readClipboardImage() is macOS-specific (pngpaste). Trivially extendable:

  • Linux: xclip -selection clipboard -t image/png -o (X11) or wl-paste --type image/png (Wayland)
  • Windows: powershell -c "Get-Clipboard -Format Image | %{ $_.Save([Console]::OpenStandardOutput(), [System.Drawing.Imaging.ImageFormat]::Png) }" or any tiny native shim

Happy to file the macOS path first as one PR and let cross-platform follow.

Why not just resurrect #24083

PR #24083 took a heavier approach: a new npm dependency (@crosscopy/clipboard), a separate staged-attachment state machine in tui.ts, footer indicator coupling, ChatSendOptions.attachments plumbing through gateway-chat.ts. That's more code, a new runtime dep, and changes to the gateway protocol surface — all reasons it may have been hard to land.

The shape proposed here:

  • No new deps — uses pngpaste (already widely installed on dev Macs; can fall back to a tiny no-op or document the install)
  • No protocol changes[media attached: <abs path>] is text the runtime already routes to vision models
  • Surgical TUI changes — one extra branch in handleInput, one line change in submit handler, one helper module
  • Bracketed-paste safe — the Ctrl+V intercept fires before pi-tui's paste machinery, so the existing bracketed-paste path is untouched when clipboard has no image

If a maintainer wants me to spin this into a PR (with proper TypeScript source, tests, and cross-platform support), happy to — just say the word. Otherwise the patcher is in active use on my machine, and the diff is small enough to lift into a proper change if anyone else wants to drive it.

Environment

  • macOS 15 (arm64), Node v25
  • OpenClaw v2026.5.28
  • pngpaste 0.2.3 (brew)
  • Terminal: any (Terminal.app, iTerm2, Ghostty — all tested)

Related closed:

  • #6565 — original feature request (closed as completed, but no shipping code)
  • #24083 — implementation attempt (closed without merge)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix TUI: Ctrl+V image paste with inline [imgN] tokens (macOS)