# Clavue Command Deck Interaction Contract

> **Stable product contract** — not a patch log.  
> All session key behavior is defined here. Code must match this table.  
> Last updated: 2026-08-22 (AskUserQuestion Grok choice dock)
> Authority: [`clavue-epoch-constitution-v1.md`](./clavue-epoch-constitution-v1.md) · map: [`clavue-doctrine-map.md`](./clavue-doctrine-map.md)
>
> Shell layout tiers (A/B/C): [`clavue-pager-shell-layout-doctrine.md`](./clavue-pager-shell-layout-doctrine.md).

## Design rules

1. **One ladder per gesture** — Esc, Ctrl+C, Tab have ordered steps; never silent no-ops without status.
2. **Draft before destroy** — clear input before cancel/quit when draft is non-empty.
3. **Prompt is home** — typing always returns focus to prompt; scrollback is view-only until Tab.
4. **Completions steal only when active** — `/…` or live `@token`; otherwise ↑↓ = history.
5. **Stay in session** — empty Esc never opens home; use `/home` or `/exit`.
6. **Journal visual hierarchy (Grok Edit card)** — full-line green/red bands for `+ `/`- ` diffs; dim context; bold path; noise folded.
7. **Tier-0 never hidden by compact** — FAILED, blockers, permission, Needs Input, Outcome, failed verify stay visible when compact is ON. Verbose remains available from settings/`/verbose`.
8. **Needs Input one-Enter** — empty prompt Enter (when workers need input) or Enter on Needs Input card opens `/parallel needs` panel; `y/a/n` remain session-permission only.
9. **CJK + narrow terminals** — chrome truncation uses **display cells** (wide CJK = 2); sticky/header/footer budgets scale with `term_cols` (`<60` narrow, `<40` min). Layout keeps header+prompt+footer; transcript gets `Min` space.
10. **Shell layout ≠ spacing compact** — `shell_layout` (standard/minimal/console) changes chrome visibility; `compact_mode` only shrinks padding. Default layout is **standard (B · 调度日记)**.
11. **Receipt single owner** — success/fail Outcome never double-paints under header sticky **and** above composer.
12. **API fail single receipt** — provisional `error` and final failed `outcome.turn` share `blockId=receipt:<turnId>`; Pager replaces in place. No fail ribbon, duplicate status, or route flash derived from the Outcome. Soft retries stay status-bar `api_retry`; interrupt residue is dim.
    **System contract:** [`clavue-duplex-lifecycle-contract.md`](./clavue-duplex-lifecycle-contract.md) — failSurface sole classifier; essay ≠ transport fail; dual-paint forbidden.
13. **Prompt identity** — Pager mints one monotonic `promptId` per original submission. Optimistic user row and durable `user.message` are one `user:<promptId>` block. Equal text with different ids remains two turns.
14. **Process is readable by default** — Reading shows a 2-line live Thought peek (no fenced code) then spine-only when settled. Verbose may show more Thought. Focused is spine-only. Tool steps stay individual in Reading/Verbose; Focused may group them.

## AskUserQuestion · choice dock (Grok Build 2026-08)

The picker **replaces the composer** (full-width plate). Journal stays visible above. Waiting strip: `Waiting on answers for {question}… ▾`.

| Key | Effect |
|-----|--------|
| ↑ ↓ / j k | Move radio (`N (○)` / `(●)`). Number keys **select**, they do not submit. |
| Enter | Submit highlighted option. Empty `z` row opens free-form. |
| `1`–`9` | Highlight that option (same row as label + dim description). |
| `z` | Focus **Type your answer here** (zh: 在此输入你的回答) and type. |
| `y` | Copy question + options (clipboard / last-copy). |
| Tab | Next question when the pack has more than one (`Tab:next answer`). |
| Esc | Scrollback (question stays). Tab/Enter return to the plate. |
| Shift+x | Dismiss / decline. |

Footer: `Tab:next answer \| Esc:scrollback \| Shift+x:dismiss`. Plate: `↑↓ navigate · y copy` left, `Enter:submit` right.

Tool-permission radios (`y/a/n`) are unchanged.

## Journal line roles (content display)

| Role | Marker / cue | Style |
|------|----------------|-------|
| **Add** | `+ ` / `+ N\| code` | Full-line **green band** + string highlight |
| **Del** | `- ` / `- N\| code` | Full-line **red band** + string highlight |
| **Context** | `N\| code` · `… more` | **Dim gray** (skip when scanning) |
| **Path** | `…/file.rs` alone | Bold tool color |
| **Tool head** | `◆ Edit path · −n +m` | Tool green · fail red · counts on fold |
| **Thought** | `Thought for Xs` spine; Reading live peek ≤2 prose lines | Think muted; settled / Focused spine-only |
| **Outcome fail** | Outcome failed | Highest contrast |

Render: `pager/src/render.rs` · tokens: `theme.rs` `diff_*` · body: `formatEditDiffBody`.

---

## Session · Ctrl+C ladder (Grok Build 1:1 · return to dialogue)

| State | Effect |
|-------|--------|
| Draft non-empty (or history browse) | **Clear draft** · focus **Prompt** · keep turn running if any · **do not** cancel/quit |
| Running + empty draft | **Cancel turn** · focus **Prompt** · status `cancelled · back to dialogue` · tip: Ctrl+C again to quit |
| Idle + scrollback focus + empty | Focus **Prompt** first (return to dialogue) |
| Idle + prompt + empty · first press | Tip: Ctrl+C again to quit |
| Idle + empty · second within 2s | **Quit** |

**Rule:** Ctrl+C never leaves the conversation on first press. Cancel returns you to free dialogue (prompt home).

Also: **Ctrl+U** clears draft anytime (kill line).

---

## Session · Esc ladder (Grok Build 0.2.112)

| Order | Condition | Effect |
|-------|-----------|--------|
| 0 | Esc×2 rewind picker open | Close picker |
| 1 | Running | Steal-Esc first: clear text selection → Scrollback→Prompt. Then **cancel turn**. **Empty composer + no file mutations** (typo / misclick): conversation-rewind that last send and **restore the prompt** (Grok cancelRewind — no `/rewind`). Typed follow-up drafts are kept. File-touching turns: cancel only + ~1s grace so mashed Esc cannot open the picker. |
| 2 | Completions open | Close menu |
| 3 | History browse | Restore live buffer |
| 4 | Text selection | Clear selection |
| 5 | Scrollback focus | Focus prompt (Clavue convenience; Grok does not use Esc for focus) |
| 6 | Draft non-empty | **Double-Esc within 800ms** clears draft (first press tips `Esc again to clear`) · non-`/` drafts save to ↑ history |
| 7 | Idle empty | **Double-Esc** within 800ms · first press tips. Second: **lossless last turn unsends** (prompt back in the dock). Otherwise opens the **rewind picker**. |

**Ctrl+C vs Esc (Grok):** mid-turn non-empty draft — Ctrl+C clears draft first (turn keeps running); Esc cancels immediately and keeps draft. Idle non-empty — Ctrl+C clears in one press; Esc requires two presses within 800ms.

### Esc×2 rewind picker

| Key / pointer | Effect |
|---------------|--------|
| ↑ ↓ / hover | Move selection (newest first) |
| Enter / click row | Lossless: unsend. Files: open **confirm** overlay |
| Esc / q | Close picker |

Status tip: default path **truncates** the selected turn and everything after.

### Esc×2 confirm overlay (Grok Rewind radios)

Skip confirm when lossless (no file-checkpoint delta): direct conversation rewind + fill prompt. After send with no file mutations, Esc (running) / Esc×2 (idle) unsends without opening this overlay.

Shown only when the selected turn **touched files**. Title **Rewind**. Default = **both** conversation and file changes.

| Option | Effect |
|--------|--------|
| **a Both conversation and file changes** | Engine: conversation + file checkpoints, rehydrate truncated journal, put prompt in draft |
| c Conversation only | Engine: conversation rewind only |
| f File changes only | Engine: file checkpoints only (journal stays) |

| Key / pointer | Effect |
|---------------|--------|
| ↑ ↓ / hover | Move radio |
| a / c / f (1 / 2 / 3 aliases) | Submit that option immediately |
| Click radio | Select + submit (Enter-equivalent) |
| Enter | Submit highlighted option (`session.rewind` → engine) |
| Esc / q | Back to message list |

**Forbidden:** wiping the entire a2p journal when prompt text fails exact match — fall back to picker rank (`indexFromEnd`).  
**Forbidden:** intermediate empty `journal.clear` flash that drops optimistic pre-cut history before hydrate replay.

`/rewind` and `/undo` print the same Esc×2 / 时间轴 instructions (no Ink MessageSelector).

---

## Agent Dashboard (Grok Build)

| Entry | Effect |
|-------|--------|
| `/dashboard` · `/agents-dashboard` · bare `/sessions` | Open live **Agent Dashboard** (tasks pane) |
| Header `■ ›` / `■ N ›` · `:: N` | Toggle dashboard from the top-right rail (chevron `▾` while open) |
| Strip `[stop]` / `[bg]` / `[↓]` | Cancel live turn · background + open dashboard · jump to tail |
| Needs strip click | Same as empty Enter → `/parallel needs` (or dashboard if no worker ask) |
| `Ctrl+B` · `Ctrl+\` · palette · Home `d` | Toggle / open dashboard |
| Roster | **[+ Dispatch] + primary + bg + workers + goal + loop** |
| j/k · ↑/↓ | Select row · peek line under header |
| **Tab** | List focus ↔ compose focus (Grok) |
| **Compose strip** | `❯ draft → target` under peek (always visible) |
| Enter (empty) | Primary close · bg restore · dispatch tips compose · worker opens compose |
| Enter (typed) | **Peek reply / dispatch**: primary → session.prompt · bg → restore+prompt · worker → `/parallel resolve` or directed prompt · dispatch → new_session+seed |
| **Ctrl+S** | Send/reply **and attach** (close dashboard) — Grok send+open |
| x · Delete | Kill running bg · dismiss settled · cancel primary turn if running |
| `Ctrl+X` | Running bg/primary: cancel once · settled bg: **twice within 2s** dismiss · idle primary: arm `/delete` |
| `Ctrl+R` | Rename selected (primary → `/rename` · bg/worker local title) |
| `Ctrl+T` | Pin / unpin selected (dashboard steals todos toggle while open) |
| `Ctrl+/` | Search mode · live filter · prefixes `a:` `s:` `#` · Esc exits search |
| `Ctrl+G` | Toggle group by **state** ↔ **name** (dashboard steals editor while open) |
| `Esc` | Step-back: rename → search → filter → Ctrl+X arm → selection → close |
| `n` / `r` | New session · `/resume` disk picker |
| `?` | In-pane help |

Disk session list remains `/resume` · `Ctrl+S`. Subcommands: `/sessions foreign|search|…` still Node.  

**Multi-session attach (Session Host · default ON · A–E+):**  
`docs/clavue-multi-session-host-mvp-design.md` · `src/uiProtocol/sessionHost/`.  
One duplex process · N interactive children in `agent` + `backgroundTasks`.  
Dashboard open → `host_roster` refresh · header chip `{i}/{n}`.  
`[+ New Agent]` → `host_dispatch` (park, no kill) · Enter row → `host_attach`.  
**`[` / `]`** → `host_cycle` · multi-line peek · **x** → `host_stop` · idle fold `+N more`.  
Opt out: `CLAVUE_SESSION_HOST=0`. Cap: `CLAVUE_SESSION_HOST_MAX` (default 8).

## Extensions modal (Grok Build)

| Entry | Effect |
|-------|--------|
| `/extensions` · `/hooks` · `/skills` · `/plugin` · `/marketplace` | Tabbed inventory panel |
| Tab actions | Hooks · Skills · Plugins · MCP · Marketplace |
| MCP tab | Full MCP settings panel |

## Resume · journal hydrate barrier (Grok Build)

| Phase | Effect |
|-------|--------|
| `session.hydrating` start | `journal_hydrating=true` · strip **Restoring… · Enter queues only** |
| User Enter while hydrating | Toast/status only · **no** optimistic user journal row · duplex queues · **does not** cancel hydrate gate |
| Hydrate load | Disk I/O **before** paint clear |
| Hydrate paint | **Burst** clear+replay (single write) — no multi-frame empty gap |
| `session.hydrating` done | `journal_hydrating=false` · flush queue · status ready · queued prompt becomes real user turn |

**Forbidden:** empty journal UI while agent has non-empty `--resume` context.  
**Forbidden:** `sendUserPrompt` bumping `journalHydrateGate` while a resume/rewind hydrate is in flight.  
**Forbidden:** optimistic user row during hydrate (hydrate `journal.clear` would wipe it).

---

## Session · Prompt (draft editing)

| Key | Effect |
|-----|--------|
| Printable | Insert at caret |
| Backspace / Delete | Delete before / after caret |
| ← → | Move caret by char |
| Home / End · Ctrl+A / Ctrl+E | **Logical line** start / end (`\n` delimited) |
| Ctrl+U | Clear entire draft |
| Ctrl+K | Kill caret → end |
| Ctrl+W | Kill word before caret |
| Ctrl+D | Delete forward |
| Ctrl+J / Shift+Enter / Alt+Enter | **Newline** (Grok 1:1 — never Ctrl+Enter) |
| Ctrl+M | Toggle multiline composer; Enter becomes newline, Shift/Alt+Enter sends |
| Enter while running | Queue a follow-up; **Grok-style strip** shows `#1 #2 #3` previews above the composer with real **`[Send now]` / `[cancel]`** chips |
| Click **`[Send now]`** | Flush queue head immediately (same as Ctrl+Enter mid-turn) |
| Click **`[cancel]`** | Clear the entire follow-up queue |
| Ctrl+Enter (running) | **Send now** — cancel current turn + send draft (or flush queue head if draft empty). Grok cancel-and-send. |
| Ctrl+Enter (idle) | **No-op** (tip: use Ctrl+J / Shift+Enter for newline). Matches Grok Build. |
| Ctrl+I / Alt+I | Send now fallback when terminal drops Ctrl+Enter |
| Ctrl+Backspace (empty draft) / Ctrl+Shift+X | **Cancel queue** — clear the entire follow-up queue |
| Enter | Send (or run local `/`); **empty** + Needs Input open → `/parallel needs` panel |
| ↑ ↓ | Move caret by **soft-wrap display row** when draft has >1 display row; else history (↑ from top) |
| Ctrl+Y (prompt focus) | Copy **selection** if any, else entire draft |
| Ctrl+' (prompt focus) | Reseed last success-gated Edit/Write as `@path` (does not send) |
| Mouse hold-then-drag on prompt | Left **click + hold (~300ms)** then drag selects → release copies · **keeps highlight** for Ctrl+Y. Immediate click-drag does **not** highlight. |
| Double-click prompt | Select word → copy · keep highlight |
| Triple-click prompt | Select whole draft → copy · keep highlight |
| Tab | Toggle prompt ↔ scrollback (if `/` or `@` completions open, Tab commits completion first) |
| ↑ ↓ (history) | Prompt history only when caret cannot move within draft display rows |
| Ctrl+P / ? | Searchable command palette |
| Ctrl+N | Global new-session confirmation; press twice within 1s |
| Ctrl+; | Prompt queue pane (select/send now/delete) |
| Ctrl+R | Reverse history search (match draft · again = older) |
| Ctrl+S | Resume session picker |
| Paste / drag path | Insert at caret · file/folder/doc → `@path` (multi → `@a @b `) · **image path → Grok `[Image #N]` chip** |
| Paste image *data* / Cmd+V screenshot | Save under `.clavue/attachments/` · insert **`[Image #N]`** (composer-owned) · send folds chip into **one** user row + `images[]` multimodal blocks · **never** orphan system journal chip |
| Enter on `[Image #N]` (caret on chip) | **Grok ImagePreview** — open OS viewer · **do not send** · Shift/Alt+Enter still sends |
| Journal `◇ Image` **[预览]** | Open same path via `image_path_index` · **[路径]** copies absolute path |

---

## Session · Completions (`/` or `@`)

| Key | Effect |
|-----|--------|
| ↑ ↓ Tab Shift+Tab | Move selection |
| Enter | Apply · if `/cmd` open surface |
| Esc | Close menu |
| Type / Backspace | Refine filter |

---

## Session · Scrollback

Two keyboard profiles share the same journal paint. Toggle Vim with `/vim`, Settings, or `pager.toml` `[ui] vim_mode`.

### Simple (default)

| Key | Effect |
|-----|--------|
| Tab | Toggle focus prompt ↔ scrollback |
| ↑ ↓ PageUp/Down | Select entry |
| ← → | Collapse / expand selected |
| e | Toggle expand/collapse selected |
| Shift+↑ / Shift+↓ | Jump prev/next **user** turn |
| Enter on **Needs Input** card | Open `/parallel needs [worker]` panel |
| Enter (other) | Expand if folded · else open full-text viewer (not “return to prompt”) |
| Space | **Always** return focus to prompt |
| y | If selected block has body text → **copy body** and stay; else type `y` into prompt |
| Y | If selected block has summary → **copy summary**; else type into prompt |
| Ctrl+Y | Copy full retained transcript |
| Type / Backspace | Return to prompt + edit |
| Mouse hold-then-drag | Left **click + hold (~300ms)** then drag selects → copy on release. Immediate click-drag does **not** highlight. |
| Click expand chip / double-click expandable | Expand in place · **keep Prompt focus** |
| Sticky header click | Select turn · **keep Prompt focus** · Tab for journal keys |
| Double-click user turn | Restore message into prompt for edit/re-send |
| Double-click other | Word/path copy · expandable → expand |
| Triple-click | Copy visual line |
| Alt+↑↓ | From prompt: enter scrollback (one step) |
| Shift+↑↓ (from prompt) | Enter scrollback + jump user turn |

### Vim (`vim_mode = true`)

All Simple keys apply, plus:

| Key | Effect |
|-----|--------|
| j / k | Select next / prev entry (**stay** in scrollback; does not type into prompt) |
| h / l | Collapse / expand |
| e / r | Expand toggle / re-select |
| y / Y | Copy block body / summary (no type-into-prompt fallback while in Vim) |
| Space | Return to prompt |

**Not Vim:** mouse wheel `scroll_mode` (auto/wheel/trackpad) is a separate settings token — device pricing, not Simple/Vim keyboard profile.

## Session · Needs Input (parallel)

| Gesture | Effect |
|---------|--------|
| Empty prompt **Enter** while `workers_needs_input > 0` | Open `/parallel needs` panel |
| Needs Input card **Enter** | Open panel focused on first listed worker |
| Panel row `worker:…` Enter | Resolve / ack worker gate (not a permission y/a/n) |
| Permission modal **y / a / n** | Session tool permission only — never worker reply |

## Session · Structured questions

`AskUserQuestion` uses the same live permission channel but renders its options
instead of the generic y/a/n modal. `↑↓` or number keys choose, `Space` toggles
multi-select rows, `Enter` submits the current question, and `Esc` declines.
The synthetic `Other` row accepts typed or pasted free-form text. Answers from
every question are returned to the engine in `updatedInput.answers`.

MCP `elicitation` requests share this owner-bound modal. Form mode renders the
restricted primitive JSON Schema fields and returns typed `content`; URL mode
shows the validated HTTP(S) URL and opens it after acceptance. `Esc` reports
`cancel`, while the visible Decline row reports `decline`. Unsupported or
invalid schemas fail closed instead of leaving the agent waiting.

---

## Session · History search (Ctrl+R)

| Key | Effect |
|-----|--------|
| Ctrl+R | Enter reverse search · needle = draft (empty = any) · again = older match |
| Printable (in search) | Append to needle · re-search from newest |
| Backspace (in search) | Pop needle · re-search |
| Esc | Restore draft · leave search |
| Enter | Accept match as prompt (send if non-empty) |
| Ctrl+S | Resume session picker (lightweight await · not turn busy) |

---

## Session · Global chords

| Key | Effect |
|-----|--------|
| Ctrl+N | Press twice within 1s to create a fresh session |
| Ctrl+Q | Press twice within 1s to quit |
| Ctrl+D | VS Code-family fallback for double-press quit; delete-forward elsewhere |
| Ctrl+G | **E3 task switcher.** Running → detach current turn to background and open the tasks pane. Idle with background rows → open/close switcher. In switcher: `j/k` select · `Enter` restore to foreground (`ui.action` `foreground_task`) · `x` kill/dismiss · `Esc` close. Permission-gated turns must be answered before backgrounding. |
| Shift+Tab | Cycle permission mode |
| y / a / n | Permission modal |
| Ctrl+O | Toggle always-approve permission mode |
| Ctrl+. / Ctrl+X | Keyboard shortcut help |
| F2 / Ctrl+, | Settings |
| Ctrl+, | Settings panel |
| Alt+T | Thinking toggle |
| Alt+O | Fast model toggle |
| Ctrl+L | Collapse last tool group |
| Mouse hold-then-drag | Left **click + hold (~300ms)** then drag selects → copy, then clear highlight. Immediate click-drag does **not** highlight. |

Terminal-family running-turn overrides follow the Grok 0.2.101 guide: Apple Terminal uses
`Ctrl+O` for send-now; VS Code, Cursor, Windsurf, and Zed use `Ctrl+L`.
Outside a running prompt these keys retain always-approve and collapse behavior.

## Pager settings

`F2` or `Ctrl+,` opens the native settings control plane. Input preferences
(`simple`/`vim` scrollback, multiline composer, sticky user header) apply to
the live pager and persist through Clavue user settings. The theme picker
previews GrokNight, GrokDay, TokyoNight, RosePineMoon, and OscuraMidnight while
moving the selection; `Enter` persists and `Esc` restores the prior theme.

---

## Paste / drag-drop contract

Terminals deliver **bracketed paste** (not true HTML drag events). Finder/Explorer
drag into the terminal usually becomes a paste of path(s) or `file://` URIs.

### Paths & files

| Input | Result in **draft** (scannable) | On **send** (engine) |
|-------|----------------------------------|----------------------|
| Image / file / folder path | Short form: `@./rel/…`, `@~/…`, or `@name.ext` alias | Alias expanded to absolute `@/full/path` |
| Multi-line / tab / **space-separated** paths | `@a @b @c ` (each shortened; status tags `file`/`folder`/`image`) | Same expansion |
| Quoted path with spaces | Single `@token` (not split) | Same expansion |
| `data:image…` / empty paste + OS clipboard image | `.clavue/attachments/<id>.<ext>` + **`[Image #N]`** (Grok) | Duplex `images[]` → base64 content blocks; journal = single user turn |

Path shortening order: **cwd-relative** → **home `~/`** → **absolute if short** → **session alias** (`@photo.png` / `@paste-1.md`). Soft max ≈ 40 display cells. Status toast shows `token → …/parent/file`.

### Plain text / long content (tiered)

| Size | Policy |
|------|--------|
| **Short** (≲4k chars and ≲60 lines) | Insert into draft at caret |
| **Medium** (above short, below file floor) | Insert into draft; dock shows last lines + collapse when unfocused |
| **Long** (≥12k chars **or** ≥200 lines **or** draft+paste >24k) | Write `<cwd>/.clavue/pastes/p*.{txt,md}` → short `@paste-…` · agent **Read** after expand |
| File save fails | Inline first 4k with truncation notice |

Why file spill: keeps prompt scannable, avoids TUI jank, matches agent tools (`@path` → Read).

Constants: `PASTE_INLINE_MAX_*` · `PASTE_FILE_MIN_*` · `PROMPT_TOTAL_MAX_CHARS` · `AT_PATH_SOFT_MAX_CELLS` in `pager/src/main.rs`.

Every paste event inserts exactly once at the current character caret. Live and
resumed user turns use the same `> ` summary style; CJK/emoji summaries truncate
by display cells rather than UTF-8 byte offsets.

## Execution telemetry

The pager enters running state only after the bridge accepts the prompt on the
agent input stream. A closed stream triggers one agent restart/retry; if that
also fails, the request remains in the authoritative prompt queue and the pager
returns to idle with an explicit error. `usage` is treated as per-turn token
data, while `total_usage` is differenced from the process baseline. Starting a
new turn clears the previous turn's token and duration chrome.

Implementation: `handle_paste` · `should_spill_paste` · `spill_paste_to_file` · `extract_path_tokens`.

## One product shell · one engine

| Term | Meaning |
|------|---------|
| **Native pager** | **Only** product interactive UI (Rust/ratatui). Insert-mode line edit. Catalog kinds · local / → agent. |
| **Ink** | **Legacy** React shell. **Not** a product launch path: `--ui=ink` / `CLAVUE_UI=ink` map to pager. Residual `createRoot` only for special sessions (teleport/remote/connect) or `CLAVUE_ALLOW_INK_SHELL=1` until migration finishes. Plan: `docs/ink-to-pager-migration-plan.md`. |
| **Vim** | Native opt-in scrollback navigation (`j/k`, `h/l`, `e`, `r`); prompt editor remains a normal composer. |
| **◇ deep wizards** | `/hooks` · `/skills` · `/agents` multi-step UX may still be incomplete on pager; surfaces show honest deep-link / list, not a second Ink shell. |

Shell paints; engine executes. There is no second product interactive TUI.

## Out of scope (honest)

| Item | Path |
|------|------|
| Multi-cursor / non-contiguous selection in prompt | Not supported (single-range drag select + Ctrl+Y **is** supported) |

Native clipboard image probing: **implemented** via `try_attach_os_clipboard_image` (empty / binary paste + Ctrl/Cmd+V). Data URI and path paste also work.

---

## Implementation map

| Concern | File |
|---------|------|
| Key ladders + chords | `pager/src/main.rs` `handle_key` |
| Line buffer + caret | `pager/src/model.rs` `input` / `input_cursor` / `insert_*` / `kill_*` |
| History | `history_prev` / `history_next` / `history_search_*` / `wants_prompt_history_keys` |
| Footer tips | `grok_footer_spans` |
| Local slash wait | `awaiting_slash` · `begin_awaiting_slash` (not `running`) |

When adding a key: **update this doc first**, then code, then `/keys` help text.
