Skip to content

feat(tui): keyboard text selection + copy in transcript - #408

Open
alecuba16 wants to merge 1 commit into
Kuberwastaken:mainfrom
alecuba16:pr387-selection
Open

feat(tui): keyboard text selection + copy in transcript#408
alecuba16 wants to merge 1 commit into
Kuberwastaken:mainfrom
alecuba16:pr387-selection

Conversation

@alecuba16

@alecuba16 alecuba16 commented Sep 3, 2026

Copy link
Copy Markdown

Summary

Split B of #387. Adds keyboard-driven text selection in the transcript pane.

This PR is the second half of the #387 split. Split A is #407 (session browser toggles + keyboard selection). No shared payload between the two.

How it works

  1. Press v on an empty prompt to enter selection mode. A cursor bar appears at the bottom of the transcript.
  2. Use / to extend the selection. PageUp/PageDown scroll by a page, Home/End jump to the top/bottom.
  3. Enter or Ctrl+C copies the selected text to the clipboard and exits selection mode.
  4. Esc cancels without copying.

Review feedback addressed

Every item from the #387 review is fixed:

# Review issue Fix
3 Bare v on empty prompt eats first char of messages starting with v, shadows vim's v v handler is gated on voice_recorder.is_none() and !matches!(vim_mode, Normal/Visual/VisualBlock), mirroring the ? help toggle pattern
4 KeyContext::Transcript bypassed — keys were hardcoded KeyCode matches All selection keys route through KeyContext::Transcript in keybindings.rs. current_key_context() returns Transcript when kb_select_mode is active
5 Up/Down scroll branches unreachable Replaced with move_kb_cursor(delta) that clamps to last_selectable_area bounds and scrolls to keep the cursor visible
6 Docs described non-existent features (hjkl, Shift-extend, Enter-yank) Docs now accurately describe v entry, arrow/PageUp/Down/Home/End navigation, Enter/Ctrl+C copy, Esc cancel
7 Session browser half never runs (dead App::run code) Fixed in #407 (split A)
8 Home scrolls to bottom because total_message_lines is always 0 total_message_lines is now written at render time in render_messages
9 last_selectable_area is the whole terminal, not the transcript render_app now sets last_selectable_area to last_msg_area (falls back to full frame on welcome screen)
10 No visible cursor on entry apply_selection_highlight shows a cursor bar when anchor == focus instead of early-returning
11 _ => return false swallows Ctrl+D Guard at the top of the hardcoded section allows Ctrl+D through; all other unbound keys are swallowed
12 Preview panel gets clipped Fixed in #407 (split A)
13 Test coverage never drives real key events 13 new tests driving handle_key_event with real KeyEvents

Files changed

  • crates/core/src/keybindings.rs — Transcript context bindings updated: selectionUp/Down/PageUp/PageDown/GoStart/GoEnd/Copy/Cancel. 2 new core resolver tests.
  • crates/tui/src/app/keys.rsv entry handler, selection mode guard, current_key_context returns Transcript, action handlers, helper methods (enter_kb_selection, exit_kb_selection, move_kb_cursor, update_kb_selection)
  • crates/tui/src/app/mod.rskb_select_mode and kb_cursor_row fields
  • crates/tui/src/render.rslast_selectable_area restricted to last_msg_area, total_message_lines written at render time, apply_selection_highlight shows cursor bar when anchor == focus
  • crates/tui/src/app/tests.rs — 11 new TUI tests (8 synthetic + 3 render pipeline driving real TestBackend/render_app/handle_key_event)
  • docs/commands.md — keyboard selection section

Tests

13 new tests total:

  • 2 core keybinding resolver tests (crates/core/src/keybindings.rs): test_transcript_context_resolves_selection_actions drives the real KeybindingResolver::new(&UserKeybindings::default()) and asserts all 8 Transcript actions (selectionUp/Down/PageUp/PageDown/GoStart/GoEnd/Copy/Cancel) resolve correctly. test_transcript_context_does_not_resolve_chat_actions verifies context isolation (up in Chat resolves to historyPrev, not selectionUp).
  • 8 TUI synthetic unit tests: enter with v, keys swallowed in selection mode, cancel with Esc, context routing, Ctrl+D not swallowed, cursor movement, clamping at bounds, copy exits mode.
  • 3 TUI render pipeline tests using real TestBackend + Terminal + render_app: test_render_restricts_selectable_area_to_transcript asserts last_selectable_area.height < terminal.height, matches last_msg_area, and total_message_lines > 0 (was always 0 before). test_render_selectable_area_falls_back_on_empty_transcript verifies welcome screen fallback. test_kb_selection_full_pipeline_enter_move_copy renders messages, then drives vupdownenter through handle_key_event exercising the real keybinding resolver.

All 13 pass. Clippy clean. Cargo check clean.

  • Pre-existing failure settings_screen::tests::all_entries_returns_expected_settings (expects ≤20 settings, gets 21) is unrelated — fails on main, this PR does not touch settings.

CRLF preservation

keybindings.rs and docs/commands.md are CRLF files. All additions use \r\n. No LF churn on untouched lines.

Add keyboard-driven text selection in the transcript pane. Press `v` on
an empty prompt to enter selection mode, navigate with arrows/PageUp/Down/
Home/End, and copy with Enter or Ctrl+C. Esc cancels.

Key changes addressing review feedback on Kuberwastaken#387:

- Dedicated entry binding: `v` on empty prompt enters selection mode
  (hardcoded handler like `?` for help, gated on no voice recorder and
  not in vim Normal/Visual mode so it doesn't shadow vim's `v`).
- All selection-mode keys route through KeyContext::Transcript in
  keybindings.rs (up/down/pageup/pagedown/home/end/enter/escape) instead
  of inline KeyCode matches. Actions: selectionUp/Down/PageUp/PageDown/
  GoStart/GoEnd/Copy/Cancel.
- Content-anchored selection: cursor moves through screen rows within
  last_selectable_area (now restricted to last_msg_area instead of the
  full terminal). Scroll branches are reachable and clamp correctly.
- total_message_lines is now written at render time (was always 0).
- Visible cursor on entry: apply_selection_highlight shows a cursor bar
  when anchor == focus instead of early-returning.
- Ctrl+D is not swallowed in selection mode (allowed to trigger exit).
- Unbound keys in selection mode are swallowed by a guard, except
  Ctrl+C (copy + exit) and Ctrl+D (exit).

Tests: 8 new (enter/exit via keybinding, context routing, cursor movement,
clamping, copy exits mode, Ctrl+D not swallowed, keys swallowed in mode).
CRLF preserved on keybindings.rs and commands.md.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant