Skip to content

Input Handling

Source attribution: Implementation details traced from references/aider/ at commit b9050e1d, references/codex/ at commit 4ab44e2c5, references/opencode/ at commit 7ed449974, and references/opentui/ at commit f4712b9.

A terminal-based coding agent needs a sophisticated input system. The user types prompts, navigates history, triggers slash commands, attaches files, pastes code blocks, and occasionally sends Ctrl+C to interrupt a long-running agent turn. All of this happens in a raw terminal where the application receives byte sequences, not semantic key events.

The hard problems are:

  1. Key disambiguation: Terminals encode keys as ANSI escape sequences. Escape and Alt+a both start with \x1b. Arrow keys with modifiers vary across terminal emulators. The Kitty keyboard protocol fixes this, but only some terminals support it.
  2. Paste detection: Without bracketed paste mode, a pasted block of text arrives as a rapid sequence of individual key events. The agent must distinguish “the user typed 50 characters in 8ms” (paste) from “the user pressed keys individually” (typing). Getting this wrong causes slash command triggers, submit-on-Enter, and mode toggles during pastes.
  3. Slash commands and autocomplete: /model at the start of input should trigger a command popup, not insert a literal slash. @ should trigger file/agent autocompletion. These triggers must coexist with normal text input and handle edge cases (slash in the middle of text, @ in an email address).
  4. Multiline editing: A prompt editor needs cursor movement, word navigation, line operations, selection, undo/redo, and kill/yank — essentially a minimal text editor. The keybindings must not conflict with the application’s global shortcuts.
  5. External process handoff: When the user invokes an external editor (Vim, $EDITOR) or the agent spawns a subprocess, the TUI must release stdin, disable raw mode, and cleanly resume when the external process exits — including flushing any buffered input that leaked across the boundary.

This page covers input capture and keybinding systems. For rendering, see TUI Rendering.


Reference: references/aider/aider/io.py, aider/commands.py | Commit: b9050e1d

Aider delegates all input handling to Python’s prompt_toolkit library. It doesn’t manage raw terminal input directly — prompt_toolkit handles ANSI parsing, keybinding dispatch, completion popups, and multiline editing.

The input loop lives in InputOutput.get_input() (io.py, lines 523-734). Session configuration (lines 347-366):

session_kwargs = {
"input": self.input,
"output": self.output,
"lexer": PygmentsLexer(MarkdownLexer),
"editing_mode": self.editingmode, # EditingMode.VI or EditingMode.EMACS
}
if self.editingmode == EditingMode.VI:
session_kwargs["cursor"] = ModalCursorShapeConfig()
if self.input_history_file is not None:
session_kwargs["history"] = FileHistory(self.input_history_file)

Aider supports both Emacs and Vi editing modes. In Vi mode, ModalCursorShapeConfig() changes the cursor shape between insert and normal modes. History is persisted to a file via FileHistory.

The prompt call (lines 656-666):

line = self.prompt_session.prompt(
show,
default=default,
completer=completer_instance,
reserve_space_for_menu=4,
complete_style=CompleteStyle.MULTI_COLUMN,
key_bindings=kb,
complete_while_typing=True,
prompt_continuation=get_continuation,
)

complete_while_typing=True triggers completion popups as the user types, without requiring Tab. reserve_space_for_menu=4 keeps 4 lines at the bottom for the completion dropdown.

Key bindings are defined via prompt_toolkit’s decorator API in io.py (lines 575-635):

kb = KeyBindings()
@kb.add(Keys.ControlZ, filter=Condition(lambda: hasattr(signal, "SIGTSTP")))
def _(event):
event.app.suspend_to_background()
@kb.add("c-space")
def _(event):
event.current_buffer.insert_text(" ")

Registered bindings:

BindingAction
Ctrl+ZSuspend to background (Unix only, gated by SIGTSTP check)
Ctrl+SpaceInsert literal space (overrides system-level input method toggle)
Ctrl+Up / Ctrl+DownNavigate input history backward/forward
Ctrl+X, Ctrl+EOpen current input in $EDITOR (Emacs-style edit-and-execute)

Lines 612-634 implement context-dependent Enter behavior:

  • Multiline mode (self.multiline): Enter inserts a newline, Alt+Enter (or Meta+Enter) submits. Vi navigation mode (InputMode.NAVIGATION) overrides this — Enter submits regardless.
  • Normal mode: Enter submits, Alt+Enter inserts a newline.

The toggle is controlled by --multiline flag and the /multiline-mode command.

Commands are defined in commands.py. Detection (line 255):

def is_command(self, inp):
return inp[0] in "/!"

The ! prefix runs shell commands. / invokes named commands.

Auto-discovery (lines 276-285): Commands are discovered by reflection — any method named cmd_<name> on the Commands class becomes /name (with underscores replaced by hyphens).

Fuzzy matching (line 300): matching_commands() finds the longest prefix match. /m matches /model if unambiguous.

Tab completion for commands (lines 148-184): The AutoCompleter detects when the buffer starts with / and yields command name completions. For command-specific arguments (e.g., /model gpt-5.2-codex completes model names), each command has a completions_<name>() method.

The AutoCompleter class (io.py, lines 91-227) provides context-aware completions from multiple sources:

  1. Project file names: Tokenized from disk, cached on first use.
  2. Symbol names: Extracted via Pygments lexer tokenization of in-context files.
  3. Command names: The get_commands() list.
  4. Command-specific arguments: Delegated to per-command completers.

Minimum input threshold: 3 characters before fuzzy matching activates (line 212). Completion style is MULTI_COLUMN — multiple candidates displayed in columns.

Aider’s input handling is entirely constrained by prompt_toolkit’s capabilities. There’s no custom paste detection, no leader key concept, no popup beyond the completion menu, and no TUI-style modal dialogs. This is intentional — Aider targets a simple CLI experience, not a full TUI.


Reference: references/codex/codex-rs/tui/src/ | Commit: 4ab44e2c5

Codex builds a full ratatui-based TUI with custom input handling on top of crossterm. The input pipeline spans seven files: terminal setup, event streaming, key binding definitions, a multiline textarea editor, slash command routing, paste burst detection, and clipboard image support.

tui/src/tui.rs (lines 15-82) initializes the terminal with keyboard enhancement:

PushKeyboardEnhancementFlags(
DISAMBIGUATE_ESCAPE_CODES | REPORT_EVENT_TYPES | REPORT_ALTERNATE_KEYS
)

This enables the Kitty keyboard protocol where supported, giving reliable modifier detection on arrow keys and Enter. Bracketed paste is enabled via EnableBracketedPaste.

tui/src/tui/event_stream.rs (572 lines) defines the core pipeline:

pub enum TuiEvent {
Key(KeyEvent),
Paste(String),
Draw,
}

The EventBroker manages a shared crossterm EventStream. It supports pause/resume — when the user launches an external editor or the agent spawns a subprocess, the event stream is dropped (pause_events()). When the process exits, a new stream is created (resume_events()). This prevents stdin contention.

Event mapping (lines 238-260):

Crossterm EventTuiEventNotes
Event::Key(key)TuiEvent::Key(key)Checks for suspend key on Unix
Event::Paste(s)TuiEvent::Paste(s)Bracketed paste content
Event::ResizeTuiEvent::DrawTriggers redraw
Event::FocusGainedUpdates terminal focus flagMay trigger palette requery
Event::FocusLostUpdates terminal focus flag
Other (mouse, etc.)Filtered out

tui/src/key_hint.rs (113 lines) defines the KeyBinding type:

struct KeyBinding(KeyCode, KeyModifiers);

Helper constructors: plain(), alt(), shift(), ctrl(), ctrl_alt(). The is_press() method checks if a binding matches a KeyEvent, ignoring Release and Repeat events.

Platform-aware display: macOS shows ⌥ + for Alt, other platforms show alt +. An has_ctrl_or_alt() helper detects AltGr on Windows (where ALT|CONTROL together indicate a local character, not a shortcut).

tui/src/bottom_pane/chat_composer.rs routes key events through a popup-aware dispatcher:

pub fn handle_key_event(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
if !self.input_enabled {
return (InputResult::None, false);
}
let result = match &mut self.active_popup {
ActivePopup::Command(_) => self.handle_key_event_with_slash_popup(key_event),
ActivePopup::File(_) => self.handle_key_event_with_file_popup(key_event),
ActivePopup::Skill(_) => self.handle_key_event_with_skill_popup(key_event),
ActivePopup::None => self.handle_key_event_without_popup(key_event),
};
self.sync_popups();
result
}

At most one popup is active at a time. After each key event, sync_popups() checks if the buffer state warrants showing, hiding, or switching popups.

Input result types:

pub enum InputResult {
Submitted { text: String, text_elements: Vec<TextElement> },
Queued { text: String, text_elements: Vec<TextElement> },
Command(SlashCommand),
CommandWithArgs(SlashCommand, String, Vec<TextElement>),
None,
}

Queued is for messages submitted while a task is running — they wait until the current turn completes.

Top-level composer shortcuts:

KeyAction
EnterSubmit message
Shift+EnterInsert newline
TabQueue message (during task) or submit (idle)
Ctrl+CClear input + show exit hint
?Toggle shortcut overlay
EscDismiss popup

Double-press quit: Ctrl+C twice within QUIT_SHORTCUT_TIMEOUT exits the application.

tui/src/bottom_pane/textarea.rs (700+ lines) implements a full multiline editor with Emacs-style keybindings.

Cursor movement:

KeyAction
Arrow keysCharacter/line movement
Ctrl+B/F/P/NLeft/right/up/down (Emacs)
Alt+Left/Right or Ctrl+Left/RightWord navigation
Home / Ctrl+ABeginning of line
End / Ctrl+EEnd of line

Editing operations:

KeyAction
Backspace / Ctrl+HDelete backward
Delete / Ctrl+DDelete forward
Alt+Backspace / Ctrl+WDelete word backward
Alt+DeleteDelete word forward
Ctrl+UKill to beginning of line (saved to kill buffer)
Ctrl+KKill to end of line (saved to kill buffer)
Ctrl+YYank from kill buffer

The kill buffer implements Emacs-style cut/yank semantics. Ctrl+K and Ctrl+U save to the kill buffer; Ctrl+Y pastes it back. This is separate from the system clipboard.

Control code fallbacks: Some terminals send C0 control characters without the CONTROL modifier flag. The textarea handles \u{0002} (^B), \u{0006} (^F), \u{0010} (^P), \u{000E} (^N) as direct movement commands.

Text elements: The textarea tracks annotations (file references, mentions) as byte ranges within the buffer. Editing operations that modify text update these ranges accordingly.

tui/src/slash_command.rs (177 lines) defines 50+ built-in commands as a SlashCommand enum:

pub enum SlashCommand {
Model, Approvals, Permissions, Review, Plan, Diff,
New, Resume, Fork, Quit, Logout, // ...
}

Methods on the enum:

  • supports_inline_args(): Returns true for commands like /review, /plan, /rename that accept arguments after the command name.
  • available_during_task(): Gates which commands work while the agent is mid-turn.

Command popup (tui/src/bottom_pane/command_popup.rs): Triggered by typing / in the composer.

KeyAction
Up / Ctrl+PNavigate up
Down / Ctrl+NNavigate down
TabAuto-complete command name
EnterSelect command
EscDismiss
1, 2, 3Numeric shortcut selection

tui/src/bottom_pane/paste_burst.rs (572 lines) solves the problem of detecting pastes in terminals that don’t support bracketed paste.

The problem: On Windows terminals, pasted text arrives as a rapid sequence of individual KeyCode::Char events — identical to fast typing. Without detection, a pasted ? would toggle the shortcut overlay, a pasted newline would submit the prompt, and a pasted / would open the command popup.

The solution: A PasteBurst state machine with platform-specific timing thresholds:

PlatformChar intervalActive timeoutMin chars
Non-Windows8ms8ms3
Windows30ms60ms3

If 3+ characters arrive within the char interval window, the state machine transitions to “burst mode” and treats all subsequent rapid characters as paste content.

Edge cases handled:

  • pending_first_char: The first ASCII character is held briefly to check if a burst follows, preventing flicker from transient mode toggles.
  • retro_chars: Characters already inserted before burst detection retroactively get pulled into the paste buffer.
  • Non-ASCII and IME input: No hold applied (to avoid dropped-input feeling with CJK input methods).

tui/src/clipboard_paste.rs (550 lines) handles image paste from the system clipboard via the arboard crate:

  • paste_image_as_png(): Reads image data from clipboard, encodes to PNG with dimension capture.
  • Supports file paths (e.g., from macOS Finder).
  • WSL fallback: Calls powershell.exe or pwsh to read Windows clipboard when running under WSL.
  • Handles Windows path normalization for UNC and C:\ paths.

tui/src/tui/job_control.rs handles Ctrl+Z:

  1. Detect SUSPEND_KEY in the event stream.
  2. Drop alt-screen, disable raw mode.
  3. Send SIGSTOP to self.
  4. On resume: re-enable raw mode, restore alt-screen.
  5. Flush stdin via tcflush(STDIN_FILENO, TCIFLUSH) to clear buffered input.

On Windows, FlushConsoleInputBuffer serves the same purpose after external editor launches.


Reference: references/opencode/packages/opencode/src/, references/opentui/ | Commit: 7ed449974 (OpenCode), f4712b9 (OpenTUI)

OpenCode’s TUI is built on OpenTUI (a Zig-based terminal rendering engine with a TypeScript binding layer). Input flows from raw terminal bytes through OpenTUI’s key parser, through a priority-based event dispatch system, and into OpenCode’s React-like component tree where keybindings are matched against a configurable schema.

opentui/packages/core/src/lib/KeyHandler.ts (lines 1-140) captures raw terminal input:

class InternalKeyHandler {
processInput(data: string): void {
// Parse ANSI sequences via parseKeypress()
// Emit standardized KeyEvent objects
}
}

KeyEvent properties:

  • name: Key name ("escape", "return", "a")
  • ctrl, meta, shift, super: Modifier flags
  • sequence: Raw ANSI sequence
  • source: "raw" or "kitty" (keyboard protocol)
  • repeated: Boolean for key repeat events
  • preventDefault() / stopPropagation(): Event control methods

Terminal capability negotiation (opentui/packages/core/src/zig/terminal.zig, lines 78-85):

OpenTUI supports the Kitty keyboard protocol with progressive enhancement:

  • Bit 0: Disambiguate escape codes
  • Bit 1: Report event types (press/repeat/release)
  • Bit 2: Report alternate keys
  • Bit 3: Report all keys as escape codes
  • Bit 4: Report associated text

It also manages mouse tracking (5 levels: none, basic, drag, motion, pixels), bracketed paste, focus tracking, and ModifyOtherKeys.

KeyHandler.ts (lines 152-200) dispatches events in priority order:

  1. Global handlers (registered via useKeyboard()) execute first.
  2. If not stopPropagation()’d, renderable/component handlers execute.
  3. Each handler can call preventDefault() to block default behavior.

This allows app-level shortcuts (Ctrl+C for copy/exit) to take precedence over component-level input handling.

OpenCode’s keybinding system is fully user-configurable. The schema is defined in config/config.ts (lines 763-915) with 100+ bindings:

// Selection of defaults:
leader: "ctrl+x" // Leader key prefix
app_exit: "ctrl+c,ctrl+d,<leader>q" // Multiple bindings per action
editor_open: "<leader>e" // Leader key sequences
command_list: "ctrl+p" // Command palette
input_submit: "return" // Submit prompt
input_newline: "shift+return,ctrl+return,alt+return,ctrl+j"
input_paste: "ctrl+v"
input_clear: "ctrl+c"

Leader key system (cli/cmd/tui/context/keybind.tsx, lines 28-65): The leader key (Ctrl+X by default) enters a two-key sequence mode with a 2-second timeout. While active, the next keypress completes the binding (e.g., Ctrl+X then e triggers “open editor”). Focus is temporarily blurred during leader mode and restored on timeout or completion.

util/keybind.ts provides the core matching logic:

  • Keybind.parse(key: string): Info[]: Parses config strings like "ctrl+x,alt+y" into arrays of Info objects. Handles the <leader> syntax.
  • Keybind.match(a: Info, b: Info): boolean: Deep equality on name, ctrl, meta, shift, super, leader fields.
  • Keybind.toString(info: Info): Produces display strings like "ctrl+shift+a".
  • Keybind.fromParsedKey(key: ParsedKey, leader?): Converts OpenTUI’s ParsedKey to OpenCode’s Info type.

The main prompt component (cli/cmd/tui/component/prompt/index.tsx, 1154 lines) handles the highest-level input routing.

onKeyDown handler (line 832):

  1. Clipboard paste (line 841): Intercepts ctrl+v, checks for images before falling back to text paste.
  2. Clear input (line 854): Matches input_clear keybind, clears text and extmarks.
  3. Exit app (line 864): Matches app_exit when prompt is empty.
  4. Shell mode toggle (line 872): ! at position 0 enters shell mode.
  5. Shell mode exit (line 878): Backspace at position 0 or Escape exits shell mode.
  6. History navigation (line 888): history_previous at cursor offset 0 cycles backward; history_next at cursor end cycles forward.
  7. Autocomplete delegation (line 885): Passes event to autocomplete when visible.

onPaste handler (line 912): Handles bracketed paste with file detection:

  • If the pasted text is a file path and the file is an image: encodes as base64 and attaches.
  • If the pasted text is 3+ lines or >150 chars: creates a summarized paste reference (collapsed in UI).
  • Normalizes CRLF line endings.

onSubmit handler (line 526): Routes by mode:

  • "exit", "quit", ":q" → exit application.
  • Shell mode → sdk.client.session.shell().
  • Starts with / and matches registered command → sdk.client.session.command().
  • Otherwise → sdk.client.session.prompt() with expanded parts.

cli/cmd/tui/component/prompt/autocomplete.tsx (668 lines) implements trigger-based autocomplete.

Trigger characters:

  • @ — Agent and file references. Must have whitespace or line start before it.
  • / — Slash commands. Must be at position 0 with no spaces between / and cursor.

Navigation within autocomplete popup:

KeyAction
Up / Ctrl+PMove selection up (wraps)
Down / Ctrl+NMove selection down (wraps)
EnterSelect current item
TabExpand directory (for @) or select file
EscapeDismiss autocomplete

Sources:

  1. Files: Full-text file search via SDK.
  2. Agents: Available subagents for @ mention.
  3. Slash commands: Registered commands from config and SDK.

cli/cmd/tui/component/dialog-command.tsx (149 lines) implements a Ctrl+P command palette:

export type CommandOption = DialogSelectOption<string> & {
keybind?: keyof KeybindsConfig,
suggested?: boolean,
slash?: Slash,
hidden?: boolean,
enabled?: boolean,
}

The palette listens globally for the command_list keybind, iterates registered commands, and executes option.onSelect() on match. It also checks all registered command keybinds on every keypress (lines 61-72) — so commands are accessible both via the palette and via their direct keybind.

cli/cmd/tui/component/textarea-keybindings.ts maps OpenCode’s config keybind names to OpenTUI’s textarea action names:

const TEXTAREA_ACTIONS = [
"submit", "newline", "move-left", "move-right", "move-up", "move-down",
"select-left", "select-right", "select-up", "select-down",
"line-home", "line-end", "buffer-home", "buffer-end",
"delete-line", "delete-to-line-end", "delete-to-line-start",
"backspace", "delete", "undo", "redo",
"word-forward", "word-backward", "delete-word-forward", "delete-word-backward",
// ... 30+ actions total
]

The mapTextareaKeybindings() function converts config key names like input_move_left to OpenTUI action names like "move-left", producing KeyBinding[] arrays for the textarea component.

App-level handlers in cli/cmd/tui/app.tsx (lines 214-241):

  • Selection copy (Ctrl+C): If text is selected in the output area, copies to clipboard and clears selection instead of triggering exit.
  • Escape: Clears selection without copying.
  • Other keys: Clear selection and pass through.
  • Error screen exit (lines 775-779): Ctrl+C in error state triggers cleanup and exit.

OpenCode has a prompt stash — save the current input, do something else, then restore:

  • prompt.stash: Save current prompt text.
  • prompt.stash.pop: Restore last stashed prompt.
  • prompt.stash.list: Show all stashed prompts.

Codex’s paste burst detector uses 8ms thresholds on Unix and 30ms on Windows. These numbers come from empirical testing — Windows terminal emulators deliver pasted characters more slowly than Unix ones. Too tight a threshold misses pastes; too loose interferes with fast typing. There’s no universal correct value.

Every reference implementation overloads Ctrl+C: it means “copy” when text is selected (OpenCode), “clear input” when the prompt has text (Codex, OpenCode), “exit” when the prompt is empty (all three), and “interrupt agent” during a running turn (OpenCode). Getting the priority order wrong creates surprising behavior.

Windows sends ALT|CONTROL together for AltGr characters (common in European keyboard layouts). A naive “is Alt or Ctrl pressed?” check incorrectly treats AltGr+@ (typed on a German keyboard) as a keyboard shortcut. Codex’s has_ctrl_or_alt() helper explicitly checks for the AltGr combination and ignores it.

When spawning $EDITOR, the TUI must: (1) drop the alt screen, (2) disable raw mode, (3) release stdin, (4) wait for the editor to exit, (5) flush any leaked stdin bytes (tcflush on Unix, FlushConsoleInputBuffer on Windows), (6) re-enable raw mode, (7) restore the alt screen. Missing step 5 causes phantom key events after the editor closes.

OpenCode’s leader key mode temporarily blurs focus from the input field. If the timeout fires while the user is mid-sequence, focus must be restored to exactly where it was — not to a default widget. Codex avoids this complexity by not implementing leader keys.

When the user types /m, should the command popup show or should it wait for more input? Aider uses longest-prefix matching (accepts /m if only /model matches). Codex shows a popup immediately on / and lets the user navigate. OpenCode waits for the autocomplete trigger. Each approach has tradeoffs: eager matching is faster for experienced users, popup-based is more discoverable.

CJK input methods (IME) produce multi-byte characters through a composition sequence. Paste burst detection must not hold characters during IME composition — Codex explicitly skips the hold for non-ASCII characters to avoid the “dropped character” feeling.


Use crossterm for terminal event capture with Kitty keyboard enhancement. Define a three-stage pipeline:

// Stage 1: Raw terminal events
enum RawEvent {
Key(crossterm::event::KeyEvent),
Paste(String),
Resize(u16, u16),
FocusChange(bool),
}
// Stage 2: Semantic events (after paste burst detection)
enum InputEvent {
KeyPress(KeyBinding),
Paste(String),
Resize(u16, u16),
}
// Stage 3: Application actions
enum Action {
Submit(String),
Command(SlashCommand, Option<String>),
InsertChar(char),
CursorMove(Direction),
// ...
}

The paste burst detector sits between stages 1 and 2, coalescing rapid key events into Paste events on terminals without bracketed paste support.

pub struct KeyMap {
bindings: HashMap<ActionId, Vec<KeySequence>>,
}
pub enum KeySequence {
Single(KeyBinding),
Leader(KeyBinding), // Requires leader prefix
}
impl KeyMap {
pub fn from_config(config: &Config) -> Self;
pub fn matches(&self, event: &KeyEvent, leader_active: bool) -> Option<ActionId>;
}

Store bindings in a HashMap<ActionId, Vec<KeySequence>> for O(1) lookup. Support multi-key leader sequences with a timeout-based state machine. Make all bindings user-configurable via TOML config, with Emacs defaults and an optional Vi mode.

  • openoxide-tui-input: KeyMap, KeyBinding, PasteBurstDetector, LeaderState. Pure logic, no rendering dependency.
  • openoxide-tui-textarea: Multiline text editor widget. Emacs and Vi keybinding presets. Kill buffer, word navigation, undo/redo.
  • openoxide-tui-commands: SlashCommand enum, command popup widget, autocomplete trigger logic.
  • openoxide-tui: Integrates all input components into the ratatui application. Manages the EventBroker for pause/resume, clipboard integration via arboard, external editor handoff.
  1. Emacs default, Vi optional: Follow Codex’s approach — Emacs keybindings as default with a --vi flag for Vi mode. Don’t build a custom Vi engine; map Vi-style normal/insert mode concepts to the same underlying action system.
  2. Configurable keybindings: Follow OpenCode’s approach — full keybinding configuration in a [keybinds] TOML section. Parse "ctrl+x,alt+y" syntax. Support leader key sequences.
  3. Platform-aware paste detection: Follow Codex’s PasteBurst state machine with platform-specific thresholds. Always enable bracketed paste, but fall back to burst detection when the terminal doesn’t support it.
  4. Slash commands as an enum: Follow Codex — define all commands as variants of a SlashCommand enum. This gives exhaustive match checking and prevents typos. Use strum for string conversion and iteration.
  5. Autocomplete triggers: Follow OpenCode’s @ for files and / for commands. Trigger on character, not on Tab. Show a popup immediately with filtered results.
  6. Clipboard via arboard: Follow Codex — use the arboard crate for cross-platform clipboard access. Handle WSL via PowerShell fallback. Support image paste to PNG.