Metadata
| Status | done |
|---|---|
| Assigned | agent-1156 |
| Agent identity | f51439356729d112a6c404803d88015d5b44832c6c584c62b96732b63c2b0c7e |
| Model | claude:opus |
| Created | 2026-04-29T17:25:12.121349189+00:00 |
| Started | 2026-04-29T17:30:10.796679947+00:00 |
| Completed | 2026-04-29T17:37:57.863520084+00:00 |
| Tags | priority-high,research,tui,ux,input, eval-scheduled |
| Eval score | 0.88 |
| └ blocking impact | 0.95 |
| └ completeness | 0.90 |
| └ constraint fidelity | 0.55 |
| └ coordination overhead | 0.95 |
| └ correctness | 0.95 |
| └ downstream usability | 0.85 |
| └ efficiency | 0.85 |
| └ intent fidelity | 0.90 |
| └ style adherence | 0.90 |
Description
Description
User report: in the wg TUI, keys like PgUp / PgDn / arrows are passed through to the inner PTY app (codex, claude, nex, or whatever shell-like thing is in there), so the user can't use them to navigate the OUTER scrollback. Touch scroll works (recently confirmed); mouse wheel queued (fix-mouse-wheel). But there's no keyboard-driven path to scroll, which is the right path for a serious TUI.
User quote: 'we need something like control T but some other control that's not getting in the way of typical applications. to be able to go into a mode where we can do page up and page down and arrow up and down to read things... we need a kind of research task that establishes [this]. Then that should feed naturally into a process that's going to effectively resolve the problem.'
Goal
Pick the toggle key + mode semantics for scroll mode. Hand off concrete spec to the implementation task.
What to investigate
1. Survey conventions in existing TUI tools
- tmux:
Ctrl+B [→ copy mode (PgUp/Down, arrows, vim-style nav, search). Exit withqor Esc. - screen:
Ctrl+A [(orEsc) → copy mode, similar. - less / more: not modal — already a pager; arrows + PgUp/Down work directly.
- vim: command vs insert mode — Esc enters command mode.
- htop / btop: arrows scroll the process list directly; no special mode needed because the inner content is the only thing.
The wg TUI is more like tmux: there's an inner app (codex/claude) AND an outer chrome (chat tabs, viz, status). The conflict is identical to tmux's, so the tmux solution is the obvious shape.
2. Pick the toggle key
Constraints:
- Must NOT clash with what codex CLI uses (codex --help → check its keymap; Ctrl+T is reserved per the screenshot earlier)
- Must NOT clash with what claude CLI uses (similar check)
- Must NOT clash with common shell shortcuts (Ctrl+C signal, Ctrl+D EOF, Ctrl+Z suspend, Ctrl+L clear, Ctrl+R reverse-history-search)
- Must be ergonomic on US keyboards (no awkward stretches)
- Should ideally NOT clash with tmux's
Ctrl+Bif the user is running wg TUI inside tmux — but the user can rebind tmux's prefix, so this is softer
Candidates worth evaluating with research:
Ctrl+G— sometimes used as 'cancel' in some TUIs, otherwise rareCtrl+\— quit signal in shell (SIGQUIT) — badCtrl+]— telnet escape, otherwise uncommonCtrl+_— undo in some apps; otherwise rareF2/F3— function keys usually not captured; widely availableAlt+/orAlt+S— Meta combos uncommon in CLI apps but require Alt-mapping supportCtrl+Space— sometimes used for completion; otherwise rare
Cross-check each candidate against codex / claude actual keymaps. Pick one that's free in both.
3. Mode semantics
Modal (recommended): toggle enters scroll mode, status bar shows 'SCROLL MODE — q/Esc to exit'. In scroll mode:
- PgUp / PgDn scroll the active pane's scrollback
- Arrow keys scroll line-by-line
- Home / End jump to top / bottom
- Optional:
/to search forward,?backward (vim-style);n/Nnext / prev match - Optional:
venters visual selection;yyanks (matches tmux copy mode) - Esc / q exits back to passthrough
Non-modal (rejected for first version): use a chord like Ctrl+G PgUp = single page up, key passes back to inner app immediately. Ergonomically worse for reading more than a single page.
4. Visual indicator
Status bar should always show the toggle key reminder when in passthrough mode ('Ctrl+? to scroll'), AND switch to a mode indicator when in scroll mode ('SCROLL MODE — q/Esc to exit'). First-run hint optional.
5. Per-pane vs global
Active pane only. Switching panes while in scroll mode either exits scroll mode OR moves the scroll context to the new pane (pick one; recommend exit-on-pane-switch for simplicity).
Deliverable
Design doc posted via wg log with:
- Chosen toggle key + rationale (why not the others)
- Cross-check table: codex / claude / nex / common shells — confirmation that the chosen key is free in each
- Mode semantics specified (key list with action per key)
- Visual indicator design (status bar text in each state)
- File pointers in src/tui/ where the input dispatch needs to grow the scroll-mode arm
- Smoke scenario list
Validation
- Toggle key chosen with rationale
- Cross-check: chosen key verified to NOT be captured by codex / claude / common shells (cite each tool's docs or keymap)
- Mode semantics fully specified (every key the mode handles)
- File pointers for the implementation
- Smoke scenario list (enter mode, scroll, exit, verify state restoration)
- No source modifications — research only
Depends on
Required by
Log
- 2026-04-29T17:25:12.105879775+00:00 Task paused
- 2026-04-29T17:25:41.817952471+00:00 Task published
- 2026-04-29T17:30:10.796683103+00:00 Spawned by coordinator --executor claude --model opus
- 2026-04-29T17:30:18.041734098+00:00 Starting research: TUI scroll-mode toggle key selection. Will survey codex/claude/shell keymaps, identify free key, write design doc.
- 2026-04-29T17:30:35.378450156+00:00 PTY scroll API exists (pty_pane.rs lines 374-410): scroll_up/down/to_top/to_bottom. Mouse wheel hookup in fix-mouse-wheel branch. Need to find input dispatch for keyboard.
- 2026-04-29T17:35:52.712215049+00:00 Cross-check complete. CODEX uses: Ctrl+C, Ctrl+L, Ctrl+N (new task), Ctrl+O (copy), Ctrl+V (paste image). CLAUDE uses: Ctrl+A/B/D/E/K/U/W (readline), Ctrl+C (interrupt), Ctrl+G (plan echo), Ctrl+L (clear), Ctrl+T (swap chars), Ctrl+Y (yank). Free in BOTH: Ctrl+], Ctrl+\\, Ctrl+_, Ctrl+^, F2-F12. Best pick: Ctrl+] (telnet-escape precedent, free in both, free in readline except niche character-search).
- 2026-04-29T17:37:36.360346005+00:00 # Design: TUI scroll-mode toggle (research-tui-scroll deliverable) ## Decision: Ctrl+] toggles a modal scroll mode `Ctrl+]` enters/exits SCROLL MODE on the focused chat PTY pane. Inside scroll mode, the inner PTY is frozen (no input forwarded); arrow keys, PgUp/PgDn, Home/End, vim-style g/G navigate the OUTER vt100 scrollback buffer; Esc / q / Ctrl+] exit back to passthrough. ## Why Ctrl+] ### Hard constraints (must not clash with inner PTY apps) Extracted from the actual installed binaries on this machine (`codex-cli 0.125.0` at `/usr/lib/node_modules/@openai/codex/node_modules/@openai/codex-linux-x64/vendor/x86_64-unknown-linux-musl/codex/codex`, `Claude Code 2.1.123` at `/home/erik/.local/bin/claude`): | Ctrl key | codex (binary `strings`) | claude (binary `strings`) | shell readline default | |---------:|:-------------------------|:---------------------------------|:-----------------------| | Ctrl+A | — | "Move to start of line" | beginning-of-line | | Ctrl+B | — | (only in tmux help text) | backward-char | | Ctrl+C | "Ctrl+C to exit/stop" | "Ctrl+C to cancel/stop" | SIGINT | | Ctrl+D | — | "Delete character / Exit if line empty" | delete-char/EOF | | Ctrl+E | — | "Move to end of line" | end-of-line | | Ctrl+G | — | "Ctrl+G); determines whether the plan is echoed" | abort | | Ctrl+K | — | "Delete to end of line" | kill-line | | Ctrl+L | "Ctrl+L is disabled while a task is in progress" | "Clear screen" | clear-screen | | Ctrl+N | "Ctrl+N (editing new task)" | — | next-history | | Ctrl+O | "Use /copy or press Ctrl+O to copy the latest agent response as Markdown" | — | rare | | Ctrl+P | — | — | previous-history | | Ctrl+R | — | — | reverse-search-history | | Ctrl+T | (used by codex per known UI; wg already steals this) | "Swap characters" | transpose-chars | | Ctrl+U | — | "Delete to start of line" | unix-line-discard | | Ctrl+V | "Paste an image with Ctrl+V to attach it to your next message" | — | quoted-insert | | Ctrl+W | — | "Delete word backward" | unix-word-rubout | | Ctrl+Y | — | "Ctrl+Y to paste deleted text" | yank | | Ctrl+Z | — | (mentioned re: process state) | SIGTSTP | **Used by EITHER codex or claude or readline:** A, B, C, D, E, G, J, K, L, M, N, O, P, R, T, U, V, W, Y, Z. **Free in both AND not a readline hot key:** Ctrl+], Ctrl+\\, Ctrl+_, Ctrl+^, F2–F12. ### Eliminate the rest - **Ctrl+\\** — sends SIGQUIT in cooked mode; even in raw mode some terminal drivers interpret it. Also generates a core-dump prompt on some signal setups. Reject. - **Ctrl+_** — readline `undo` (bound by default in bash/zsh inside the inner REPL's input line). Also awkward chord on US keyboards (Shift+Ctrl+`-`). Reject as primary; keep as candidate. - **Ctrl+^** — readline rarely binds it; chord requires Shift on most layouts (Shift+Ctrl+`6`). Awkward. Reject. - **F2–F12** — TRULY free in both apps. But: laptop F-rows often need an Fn modifier (chord becomes Fn+F7), some terminal multiplexers and emulators swallow F-keys (GNOME Terminal binds F10 to menu, tmux without proper TERM passthrough drops some F-keys), and they're far from the home row. Reject as primary; recommend as documented secondary for users with US-layout-hostile keyboards. - **Ctrl+]** — sends ASCII GS (0x1D). Bound by readline as `character-search`, which is so rare in practice that bash itself flags it as "obscure" in the manpage and very few users use it. Telnet's well-known "escape character" precedent (`^]`) gives the key strong "break out of the inner protocol layer" semantics that map perfectly to "leave the inner PTY, scroll the outer". One-handed reach (right pinky on `]`). Not in any codex or claude user-facing binding per binary `strings`. **PICK.** ### Why not Ctrl+B (the obvious tmux analog) - It IS free in claude/codex (the only `Ctrl+B` strings are an incidental `Ctrl+B/FSymbol()` JS token and tmux's own help text, neither user-facing) - BUT it's readline's `backward-char` — bound by default in every readline consumer including the input boxes inside codex and claude. Stealing it outside the REPL means the user can no longer move the cursor backward one character with Ctrl+B inside the REPL prompt without the wg TUI intercepting it. That's a real cost for Emacs-keymap users. - Soft conflict with a tmux-prefix user is the smaller harm; the readline conflict is the deciding one. Reject. ### Why not Alt+S / Alt+/ - Some terminal emulators (notably default macOS Terminal.app) send Alt as ESC-prefix unless "Use Option as Meta" is enabled, which produces a different keypath. Crossterm sees `Esc` then the letter, not `Alt+letter`. Cross-platform reliability issue. - Reject as primary; could be added as a future user-configurable alias. ## Mode semantics (modal, tmux-copy-mode-style) ### Entering scroll mode - `Ctrl+]` from passthrough → enters SCROLL MODE on the currently focused chat PTY pane (`active_coordinator_id`). - The inner PTY continues to run and produce output normally; the parser keeps appending to scrollback. We just don't forward keystrokes and we hold `auto_follow = false` so new output doesn't yank the viewport. ### Inside scroll mode (active key handling) | Key | Action | |--------------------------|-----------------------------------------| | `Ctrl+]` | exit scroll mode (toggle) | | `Esc` | exit scroll mode | | `q` | exit scroll mode | | `PageUp` | scroll up one page (`pane.scroll_up(viewport_height)`) | | `PageDown` | scroll down one page | | `Up` / `k` | scroll up one line | | `Down` / `j` | scroll down one line | | `Home` / `g` | jump to top of scrollback | | `End` / `G` | jump to bottom (live) | | `Ctrl+u` | half-page up | | `Ctrl+d` | half-page down | | Anything else | swallow (do NOT forward to PTY) | The single-key vim aliases (`j`/`k`/`g`/`G`) are safe inside scroll mode because the PTY is not receiving them — we're a separate input mode now. ### Optional keys (phase-2, not blocking implementation) - `/` — search forward in scrollback - `?` — search backward - `n` / `N` — next/prev match - `v` — start visual selection (matches tmux copy mode) - `y` — yank selection to system clipboard Phase-1 scope: just navigation (table above). Phase-2 search/selection is a follow-up implementation task once navigation lands. ### Pane-switch behavior **Recommendation: switching panes exits scroll mode.** Simpler invariant: SCROLL MODE is always "on the currently focused pane". If the user clicks a different chat tab, focuses the graph panel, opens a launcher, etc., scroll mode exits cleanly and the user is back in normal nav. This avoids the "phantom scroll mode on a hidden pane" trap. If we want the alternative ("scroll mode follows the new active pane") we can revisit, but for v1 simplicity wins. ## Visual indicator ### Passthrough mode (existing chrome plus a single hint) The chat PTY pane currently has a status border with the chat task title and the "↓N" scrollback indicator at the right edge (rendered by `render_pty_pane` in `src/tui/pty_pane.rs:333-345`). Add a left-edge hint: ``` ┌─ chat-0 Ctrl+] scroll ──── ↓0 ─┐ ``` When `auto_follow == true` and not in scroll mode, the hint is dimmed. ### SCROLL MODE (mode swap) Border title flips to a high-contrast SCROLL banner: ``` ┌── SCROLL MODE ↑↓ line PgUp/PgDn page g/G top/bot q/Esc/Ctrl+] exit ↓42 ─┐ ``` Color: yellow background or reversed colors (use the same accent color the codebase uses for confirm prompts — grep `vy("plan")` etc.). The `↓42` indicator already exists from `pty_pane.rs:339`; keep it on the right. ### First-run hint After the first time the user enters PTY focus, briefly toast "Press Ctrl+] to scroll the pane history" in the status line for ~3 seconds. Optional; not on the v1 critical path. ## File pointers for the implementation task The implementation task (`implement-tui-scroll`) needs to grow these arms: ### 1. Input dispatch — `src/tui/viz_viewer/event.rs` The PTY-focus key handler is at lines **497–544** (`handle_normal_key`, `vendor_pty_active` branch). Currently it intercepts only PageUp/PageDown/ Home/End at lines 513–530 and forwards everything else (line 534). Grow the pattern: - Add a new `InputMode::ScrollMode { task_id: String }` variant alongside the existing `Normal` / `Search` / `ChatInput` etc. in `src/tui/viz_viewer/state.rs:810`. - BEFORE the `vendor_pty_active` block, check `if let InputMode::ScrollMode { task_id } = &app.input_mode { ... }` and dispatch the scroll-mode keymap from the table above. Return early — never fall through to PTY forwarding while in scroll mode. - INSIDE the `vendor_pty_active` block, add a new is-toggle case for `Ctrl+]` (KeyCode::Char(']') with KeyModifiers::CONTROL): set `app.input_mode = InputMode::ScrollMode { task_id: chat_task_id }` and return (do not forward). - The `is_toggle` for Ctrl+T already exists; the new Ctrl+] is a sibling. Optionally move both to a small `match` for clarity. ### 2. Mode state — `src/tui/viz_viewer/state.rs` - Add `ScrollMode { task_id: String }` to `pub enum InputMode` at line **810**. - The chat-task-id is already computed everywhere as `workgraph::chat_id::format_chat_task_id(app.active_coordinator_id)`; reuse that helper. - No new fields on `VizApp` needed; `auto_follow` lives on the pane itself. ### 3. Pane scroll ops — `src/tui/pty_pane.rs` Already complete. The four operations the new keymap needs all exist at lines **374–410**: - `scroll_up(n: usize)` - `scroll_down(n: usize)` - `scroll_to_top()` - `scroll_to_bottom()` No changes needed in this file. ### 4. Status-bar render — `src/tui/viz_viewer/render.rs` The right-panel chat tab title is rendered via the chat-tab render path (grep for `RightPanelTab::Chat` and `pty_pane`). Add: - Passthrough hint: append `" Ctrl+] scroll"` (dimmed) to the title line when `app.input_mode == InputMode::Normal && pty_focused`. - Scroll-mode banner: when `matches!(app.input_mode, InputMode::ScrollMode { task_id } if task_id == active_chat_task_id)`, swap the title for the highlighted SCROLL MODE banner described above. The existing scroll indicator (`↓N`) emitted from `render_pty_pane` (`src/tui/pty_pane.rs:333-345`) keeps working unchanged. ### 5. Pane-switch cleanup Anywhere the focused panel or active coordinator changes — `app.focused_panel = ...`, `app.active_coordinator_id = ...`, launcher open/close, etc. — emit: ```rust if matches!(app.input_mode, InputMode::ScrollMode { .. }) { app.input_mode = InputMode::Normal; } ``` Add a small helper `app.exit_scroll_mode_if_active()` to avoid drift. Candidates: `open_launcher`, `close_launcher`, focus_panel transitions in event.rs, coordinator switching in `set_active_coordinator` / `active_chat_index_set`. ### 6. Tests (cargo test) Suggested test targets in `src/tui/viz_viewer/editor_tests.rs` or a new `scroll_mode_tests.rs`: - `enter_scroll_mode_via_ctrl_bracket_changes_input_mode` - `scroll_mode_pageup_calls_pane_scroll_up` - `scroll_mode_q_exits_back_to_normal` - `scroll_mode_swallows_letter_key_does_not_send_to_pty` (assert no bytes written to the master half of the PTY) - `pane_switch_exits_scroll_mode` ## Smoke scenario (for `tests/smoke/manifest.toml`) ```toml [[scenario]] name = "tui-scroll-mode-toggle" script = "tests/smoke/scenarios/tui-scroll-mode.sh" owners = ["implement-tui-scroll"] ``` `tests/smoke/scenarios/tui-scroll-mode.sh`: 1. Launch `wg tui` with a coordinator using `claude:haiku` (or skip with exit-77 if no claude auth). 2. Drive enough output into the chat pane to fill ~3 screens of scrollback (paste a long markdown block via the chat input, or send a `/help`-style command that produces deterministic multi-screen output). 3. Send `Ctrl+]` via the input harness (existing tests use a synthetic `crossterm::event::KeyEvent` injector; reuse it). 4. Assert: `app.input_mode` is `ScrollMode` and the title bar contains "SCROLL MODE". 5. Send `PageUp` → assert pane's `auto_follow == false` and the parser's `scrollback() > 0`. 6. Send `Down` → assert scrollback decreased by 1. 7. Send `End` → assert `scrollback() == 0` and `auto_follow == true`. 8. Send `q` → assert `app.input_mode == Normal`. 9. Send a printable char like `h` while in passthrough → assert it WAS forwarded to the PTY (control: scroll mode does not get stuck). 10. Re-enter scroll mode, send `h` → assert NOT forwarded (no new bytes written to the master). 11. Re-enter scroll mode, switch panes (e.g., focus graph), assert `app.input_mode == Normal` (pane-switch exits scroll mode). Exit codes: 0 on full pass, 77 on environment-skip (no claude/codex auth), non-zero on assertion failure. ## Validation checklist (this research task) - [x] Toggle key chosen with rationale: **Ctrl+]** - [x] Cross-check confirmed via binary `strings`: codex 0.125.0 (`Ctrl+C/L/N/O/V`) and claude 2.1.123 (`Ctrl+A/B-help/C/D/E/G/K/L/T/U/W/Y`) — Ctrl+] not bound in either - [x] Mode semantics fully specified — every key in the keymap above - [x] Visual indicator design — passthrough hint + SCROLL MODE banner + retained `↓N` offset indicator - [x] File pointers — `event.rs:497-544`, `state.rs:810`, `pty_pane.rs:374-410`, `render.rs` (chat-tab title path) - [x] Smoke scenario list — 11-step scenario above - [x] No source modifications — research only
- 2026-04-29T17:37:41.460310902+00:00 Design doc posted. Validation: (1) toggle = Ctrl+] with rationale; (2) cross-check via binary strings on codex 0.125.0 + claude 2.1.123 confirms Ctrl+] free in both; (3) full keymap specified; (4) status-bar swap design specified; (5) file pointers given (event.rs:497-544, state.rs:810, pty_pane.rs:374-410, render.rs chat-tab path); (6) 11-step smoke scenario specified; (7) no source modified — research only.
- 2026-04-29T17:37:57.863525474+00:00 Task pending eval (agent reported done; awaiting `.evaluate-*` to score)
- 2026-04-29T17:40:24.083625108+00:00 PendingEval → Done (evaluator passed; downstream unblocks)