diff --git a/specs/GH9959/product.md b/specs/GH9959/product.md new file mode 100644 index 000000000..7ca405107 --- /dev/null +++ b/specs/GH9959/product.md @@ -0,0 +1,117 @@ +# Block navigation in agent mode + +Tracking: [#9959](https://github.com/warpdotdev/warp/issues/9959) + +## Summary + +Bring Warp's terminal-mode block navigation (CMD-UP / CMD-DOWN to select the most recent block, arrow keys to move between blocks, SHIFT-CMD-UP/DOWN to scroll to the edges of the selected block) to agent mode. In agent mode, navigable units are agent responses, agent-executed commands (with their output), and user prompts — and which of those count as "blocks" is configurable in Settings. + +## Problem + +In long agent conversations — multiple back-and-forths, several agent-run commands with lengthy output, large markdown responses — the only way to revisit an earlier exchange is manual scrolling. Terminal mode does not have this problem because every block is keyboard-navigable. The keyboard model that makes terminal mode pleasant simply doesn't exist on the agent side, so power users lose the same workflow exactly when conversations get long enough to need it most. + +## Goals / Non-goals + +**Goals** +- Same muscle memory as terminal mode: CMD-UP / arrow keys / SHIFT-CMD-UP-DOWN behave the same way wherever you are in Warp. +- A single configurable surface for which agent-mode element types participate in block navigation. +- Selection in agent mode is at parity with terminal mode for copy, plus useful agent-specific affordances (attach as context, rerun command, retry response, fork from prompt). + +**Non-goals** +- Redesigning terminal-mode navigation, keybindings, or selection styling. +- Navigating *within* a single agent response (heading-by-heading, code-block-by-code-block). The smallest navigable unit is the whole response. +- Rebinding CMD-UP / CMD-DOWN / arrow keys in agent input itself when the input field has focus and a typical text-editing operation is expected. + +## Figma + +Figma: links to be added by the spec author before the spec is approved. Until then, agent-mode block selection MUST visually match terminal-mode block selection (same border treatment, same theme tokens, same multi-select rendering). + +## Behavior + +### Block model + +1. In an agent-mode conversation, the navigable items in left-to-right reading order are the visible *agent blocks* in the conversation: + - **User prompts** — each message the user sent to the agent. + - **Agent responses** — each top-level response the agent produced. + - **Agent-executed commands** — each command the agent ran, *together with its output as a single block* (the command and its output are not separately navigable). +2. When agent mode is rendered inline inside a terminal block list (agent view is embedded rather than full-screen), all blocks in that block list — both terminal blocks and agent blocks — participate in the same navigation order. The user perceives one continuous block list, not two. +3. When agent mode is rendered full-screen, only agent blocks are present and only they participate in navigation. +4. A block that is still streaming (response being generated, or command still producing output) IS navigable and selectable. Its visible bounds may grow while selected; the selection remains attached to the same logical block as it grows. +5. A tool-call block that has not yet produced any visible content (zero-output command, in-flight tool call with nothing rendered yet) IS still navigable — empty does not mean absent. +6. Hidden / collapsed blocks (folded responses, collapsed command output) are skipped during arrow navigation. Expanding a previously hidden block makes it navigable again. Selection state is preserved across collapse/expand of the *selected* block. + +### Configurable block-type filter + +7. A setting under **Settings → AI → Block navigation** controls which agent-block types participate in CMD-UP / arrow navigation. The setting is a multi-select with three independent toggles: + - Agent responses (default: on) + - Agent-executed commands (default: on) + - User prompts (default: on) +8. The default ships with all three on. With all three on, every agent block is navigable. +9. Disabling a type via the setting causes navigation to **skip** blocks of that type, exactly as if they were not in the list. Disabled-type blocks remain visible in the conversation; only navigation skips them. +10. Disabling all three types disables agent-mode block navigation entirely. CMD-UP and arrow keys then fall through to whatever lower-priority binding would otherwise handle them in agent mode (typically the agent input field for arrow keys; nothing for CMD-UP). +11. Changing the setting mid-conversation takes effect on the next keypress without requiring a refresh. If the currently selected block becomes a non-navigable type because of a setting change, selection is cleared. +12. The setting is per-user (synced via the same mechanism as other Settings → AI preferences), not per-conversation. + +### Selecting a block — initial selection + +13. With no agent block currently selected, pressing **CMD-UP** while the agent view has focus selects the most recent navigable agent block (the bottom-most block in the conversation, after applying the block-type filter from §7). The selected block is scrolled into view if not already visible. +14. With no agent block currently selected, pressing **CMD-DOWN** while the agent view has focus is a no-op — there is nothing below the input. +15. CMD-UP / CMD-DOWN never move focus into an editable input; they always operate on the block list. (Contrast with arrow keys, which only operate on blocks when a block is already selected — see §17.) + +### Moving between blocks + +16. With a block selected, **UP** / **DOWN** arrow keys move the selection to the previous / next navigable agent block (per the §7 filter), scrolling the new selection into view. +17. With **no** block selected, UP / DOWN arrow keys retain their default behavior in whatever input or surface currently has focus. Arrow keys do not "wake up" block selection — only CMD-UP does. +18. Pressing UP at the top-most navigable block leaves selection on that block (no wrap-around). +19. Pressing DOWN at the bottom-most navigable block clears selection and returns focus to the agent input field, mirroring the equivalent terminal-mode behavior. +20. **CMD-UP / CMD-DOWN** with a block already selected jumps to the top-most / bottom-most navigable block in the conversation (jump to extremes), not just one step. +21. **SHIFT-UP / SHIFT-DOWN** extends the selection by one navigable block in that direction, producing a multi-block selection. **SHIFT-CMD-UP** with no selection selects from the bottom of the conversation up to the top-most navigable block. + +### SHIFT-CMD scroll + +22. **SHIFT-CMD-UP** scrolls the viewport so the top edge of the selected block (or the top-most block in a multi-selection) is visible. **SHIFT-CMD-DOWN** scrolls so the bottom edge of the selected block (or bottom-most block in a multi-selection) is visible. Selection itself is not changed. This matches terminal-mode `ScrollToTopOfSelectedBlocks` / `ScrollToBottomOfSelectedBlocks` semantics. +23. With nothing selected, SHIFT-CMD-UP / SHIFT-CMD-DOWN are no-ops in agent mode. + +### What selection does + +24. A selected block renders with the same selection border treatment as a selected terminal block — same border width, same theme tokens, same multi-block range rendering — so users perceive one consistent "selected block" UI across modes. +25. **Copy (CMD-C)** with one or more agent blocks selected copies their content to the system clipboard: + - User prompt → the prompt's plain text. + - Agent response → the response's markdown source (not the rendered HTML/styled form). + - Agent-executed command → the command line followed by its output (same shape as copying a terminal block). + Multi-block copy concatenates blocks in conversation order with a single blank line between them. +26. **Attach to next prompt as context** is exposed as an explicit action — keystroke `CMD-SHIFT-K` and a button on the selected block's hover affordance — *not* automatically on selection. Triggering it adds the selected block(s) as a context chip on the agent input. The chip is removable like any other context chip, and the user can keep navigating / selecting more blocks and attach them too. +27. **Per-block actions** are exposed via the existing block hover/keyboard affordance (the same "block actions" surface terminal blocks already use). Available actions depend on the selected block's type: + - User prompt → **Fork conversation from here** (creates a new conversation seeded with history up through but not including this prompt; the original conversation is unchanged). + - Agent response → **Retry from here** (re-runs the agent with the same prompt that produced this response, replacing this response and everything after it with the new one). + - Agent-executed command → **Rerun command** (re-executes the command in the same shell context the agent used; output is appended to the conversation as a new agent-command block). +28. Per-block actions in §27 are only available when exactly one block is selected. With a multi-block selection, only Copy and Attach-as-context are available. +29. Selection has no side effect beyond §24–§28. It does not edit conversation state, does not pause streaming, does not consume tool-call results, does not change agent focus. + +### Focus and input interactions + +30. When the agent input field has focus and a block is selected, typing any printable character (or any text-editing key like Backspace) clears the block selection and the keystroke goes to the input field. Modifier-key combinations and arrow keys are not "printable" for the purpose of this rule and do not clear selection. +31. Pressing **Escape** with a block selected clears the selection and returns focus to the agent input. Pressing Escape with no selection retains its existing agent-mode behavior. +32. Block selection is cleared whenever the user sends a new prompt to the agent. (The new exchange becomes the bottom of the list and any prior selection would be visually disorienting.) +33. Switching between agent mode and terminal mode does not preserve agent-mode selection — selection is per-mode and resets when the surface changes. +34. Block selection is not persisted across app restarts or conversation reloads. + +### Edge cases + +35. **Empty conversation** — no agent exchanges yet: CMD-UP and arrow keys are no-ops in agent mode. Nothing to select. +36. **Single-block conversation** — exactly one navigable block: CMD-UP selects it; UP/DOWN are no-ops; CMD-DOWN clears selection per §19. +37. **Streaming response selected** — if the user selects an agent response that is still streaming, navigation behavior is unchanged. Copy (§25) copies whatever is rendered at the moment of copy (no waiting for stream completion). Retry-from-here (§27) cancels the in-flight stream and starts a new generation. +38. **Long-running command selected** — equivalent to §37: copy reads currently-rendered output; rerun starts a new execution and does not interfere with the original. +39. **Block deleted while selected** (e.g. agent retracts a tool call, conversation is edited): selection moves to the next-newer navigable block. If no newer block exists, selection moves to the next-older navigable block. If no navigable block remains, selection is cleared. +40. **Filter change clears non-matching selection** — see §11. +41. **Input mode quirks** — terminal mode has an `InputMode::PinnedToTop` that inverts UP/DOWN semantics. Agent mode does NOT honor that mode; CMD-UP always means "select the most recent block" (i.e. the bottom-most), independent of any terminal-side pinned-to-top setting. +42. **AltScreen** — terminal-mode navigation is disabled when AltScreen is active. Agent mode is not affected by AltScreen state — the agent view is its own surface and its block navigation works regardless of any concurrent terminal AltScreen. + +### Accessibility + +43. Selecting a block via keyboard moves the assistive-tech focus to that block. The block's accessible name announces its type ("user prompt", "agent response", "agent command") and a short content preview (first ~80 characters of the block's text). +44. The selection border is rendered using existing theme tokens that satisfy the contrast requirements already met by terminal-mode block selection — no new tokens are introduced for agent mode. + +### Cross-surface consistency + +45. The keystroke chord, the selection visual, the multi-select extension behavior, and the "scroll to edge of selected block" behavior MUST be identical in semantics between terminal mode and agent mode. A user who learned the gesture in one mode applies it unchanged in the other. The only intentional differences between the two modes are the configurable block-type filter (§7–§12), the per-block-type action set (§27), and the absence of `PinnedToTop` inversion in agent mode (§41). diff --git a/specs/GH9959/tech.md b/specs/GH9959/tech.md new file mode 100644 index 000000000..4eea81279 --- /dev/null +++ b/specs/GH9959/tech.md @@ -0,0 +1,191 @@ +# Block navigation in agent mode — tech spec + +Tracking: [#9959](https://github.com/warpdotdev/warp/issues/9959). Behavior is defined in [`product.md`](./product.md); this spec maps it onto the codebase. + +## Context + +Terminal-mode block navigation is fully wired and the agent view *shares the same `BlockList` / `BlockListElement` surface as terminal mode* — agent exchanges are rendered as items in the same scrollable container that terminal blocks live in (see `app/src/terminal/view.rs:2736` and the agent-view rendering path at `2991-3300`). The work in this spec is therefore not "build a new navigation system", it is "extend the existing one to recognise agent-side items as navigable, and make the set of recognised item kinds configurable". + +Relevant code surfaces: + +- **Terminal nav action enum** — `TerminalAction::SelectPriorBlock` (`app/src/terminal/view/action.rs:180`) and `SelectNextBlock` (`:187`). +- **Handlers** — `SelectPriorBlock`/`SelectNextBlock` arms in the `TerminalAction` match at `app/src/terminal/view.rs:24670` and `:24694`. These delegate to `select_less_recent_block` (`:18406`) and `select_more_recent_block(is_cmd_down, is_shift_down, ctx)` (`:18456`), which mutate selection through `change_block_selections(|selected| { selected.range_select(…) }, ctx)`. +- **Selection model** — `SelectedBlocks` at `app/src/terminal/model/terminal_model.rs:729-1007`. Public API includes `range_select`, `reset`, `reset_to_single`, `reset_to_block_indices`, `is_selected`, `is_empty`, `is_singleton`, `tail`, `block_indices`, `sorted_ranges`, `cardinality`. Indexed by `BlockIndex` — i.e. terminal-block-only. +- **Selection rendering** — `SelectionBorderWidth { single: 2.0, tail_multi: 3.0, reg_multi: 1.5 }` at `app/src/terminal/block_list_element.rs:110-114`; border colour from `warp_theme.block_selection_as_context_border_color()` (`:3890`) with `accent()` fallback (`:3892`). +- **Keybindings** — `EditableBinding` registration at `app/src/terminal/view/init.rs:558-574`, mapped from `CustomAction::SelectBlockAbove`/`SelectBlockBelow` (`app/src/util/bindings.rs:81-82`) and `ScrollToTopOfSelectedBlocks`/`ScrollToBottomOfSelectedBlocks` (`:101-102`). Context predicate today: `id!("Terminal") & id!("TerminalView_NonEmptyBlockList") & !id!("AltScreen")`. +- **Agent-side data** — `AIConversation::all_exchanges() -> Vec<&AIAgentExchange>` (`app/src/ai/agent/conversation.rs:1065`) walks the conversation in display order. `AIAgentExchange { input: Vec, output_status: AIAgentOutputStatus, … }` (`app/src/ai/agent/mod.rs:2835`); inputs include `AIAgentInput::UserQuery { query, … }` (`:2398`); responses are described by `AIAgentOutputStatus::{Streaming, Finished}` (`:246`); commands the agent ran are surfaced through `AIAgentOutputMessageType` (`:1566`) inside the finished output. +- **Agent view state** — `AgentViewState::Active { conversation_id, origin, display_mode, … }` at `app/src/ai/blocklist/agent_view/controller.rs:230-276`, with `AgentViewDisplayMode::{FullScreen, Inline}` (`:44-59`) and accessors `is_inline()` / `is_fullscreen()`. +- **AI settings** — `define_settings_group!(AISettings, settings: [...])` at `app/src/settings/ai.rs:710-827`. Existing toggles (`is_active_ai_enabled_internal`, `intelligent_autosuggestions_enabled_internal`, etc.) are the pattern to mirror: `field_name: FieldTypeIdent { type: bool, default, supported_platforms, sync_to_cloud, private, toml_path, description }`. +- **AI settings UI** — `app/src/settings_view/ai_page.rs:2186` renders the page; `render_ai_setting_toggle::(label, action, …)` at `:3065` is the per-toggle helper, with section helpers like `render_prompt_suggestions_section` (`:3760`). +- **Agent shortcuts (existing)** — `app/src/ai/blocklist/agent_view/shortcuts/mod.rs:109-250`: `!`, `/`, `@`, `cmd+shift+y`, `cmd+enter`, toggle-right-panel, toggle-conversation-list, toggle-autoexecute, `ctrl+c`, `escape`. `cmd+shift+k` is unused. + +## Proposed changes + +### 1. Unified navigable-item identity + +Introduce one type that represents a navigable item across both modes. `BlockIndex` cannot be reused because agent items are not addressed by it. + +```rust +// app/src/terminal/model/navigable_item.rs (new file) +pub enum NavigableItemId { + Terminal(BlockIndex), + AgentPrompt(AIAgentExchangeId), + AgentResponse(AIAgentExchangeId), + AgentCommand { exchange: AIAgentExchangeId, command_index: usize }, +} +``` + +`AIAgentExchangeId` is a stable identifier across the lifetime of the exchange (used today for `exchange_with_id` lookup at `conversation.rs:1098`), so a streaming response remains the same logical item as it grows — satisfying invariant §4 of the product spec. + +### 2. Iterator that yields the unified display order + +Add `TerminalView::iter_navigable_items(&self, ctx) -> impl Iterator` that walks the visible items in screen order. Implementation: + +- If `agent_view_controller.display_mode().is_fullscreen()`, walk `AIConversation::all_exchanges()` and emit `AgentPrompt` / `AgentResponse` / `AgentCommand` items per exchange, applying the §7–§10 filter (see settings below). +- Otherwise (inline or pure terminal), interleave terminal `BlockIndex`es from `TerminalModel.block_list()` with agent items in their actual display position. The agent view already knows where it is anchored in the block list (`agent_view/controller.rs` rendering path at `view.rs:2991-3300`); reuse that to weave both streams into one ordered sequence. + +This iterator is the single source of truth for "what is navigable right now" — every selection mutation goes through it. Hidden / collapsed items (§6) are filtered out here. + +### 3. Selection state + +`TerminalView.selected_blocks: SelectedBlocks` continues to exist and continues to govern terminal-only operations (terminal block actions, terminal-side context-attach, etc.) for backwards compatibility. Add a sibling field: + +```rust +// TerminalView +selected_navigable_items: SelectedNavigableItems, +``` + +`SelectedNavigableItems` mirrors `SelectedBlocks`'s API surface (`range_select`, `reset`, `reset_to_single`, `is_selected`, `is_empty`, `is_singleton`, `tail`, `cardinality`, `sorted`) but is keyed by `NavigableItemId`. Internally it stores a `Vec`; ranges are computed against the iterator from §2 so "extend selection by one" means "advance one position in `iter_navigable_items`". + +When the selection contains only `NavigableItemId::Terminal(_)` entries, mirror them into `selected_blocks` so existing terminal-block consumers (copy, context attach, block actions) keep working unchanged. This avoids a sweeping refactor of every call site that reads `selected_blocks` today. + +### 4. Handler reuse — broaden the existing actions, do not duplicate them + +`TerminalAction::SelectPriorBlock` / `SelectNextBlock` already encode "move selection one block toward older / newer", which is exactly the agent-mode semantic. Modify the handlers at `view.rs:24670` and `:24694` (and the helpers `select_less_recent_block` / `select_more_recent_block`) to: + +1. Operate on `SelectedNavigableItems` via `iter_navigable_items` instead of `SelectedBlocks` directly. +2. Skip the `InputMode::PinnedToTop` direction inversion when the agent view is full-screen — satisfies product §41. +3. On reaching the bottom past the last item, clear selection and refocus the agent input when in agent mode (§19), mirroring the existing "refocus terminal input" behaviour. + +For `ScrollToTopOfSelectedBlocks` / `ScrollToBottomOfSelectedBlocks`: same treatment — the existing scroll math already targets the bounds of the selection; switching the source-of-truth to `selected_navigable_items` makes it work for agent items too (§22). + +### 5. Keybinding context predicate + +Update the predicate in `init.rs:564-565` and `:573-574` from: + +``` +id!("Terminal") & id!("TerminalView_NonEmptyBlockList") & !id!("AltScreen") +``` + +to: + +``` +(id!("Terminal") & id!("TerminalView_NonEmptyBlockList") & !id!("AltScreen")) +| id!("TerminalView_AgentMode_Navigable") +``` + +…where `TerminalView_AgentMode_Navigable` is a new context id raised by `TerminalView` whenever `agent_view_controller.is_active()` and `iter_navigable_items` yields at least one item. The disjunction guarantees agent-mode navigation works regardless of `AltScreen` (§42). + +### 6. New action `AttachSelectedBlocksAsContext` + +Add `CustomAction::AttachSelectedBlocksAsContext` to `app/src/util/bindings.rs` (default keystroke `cmd+shift+k`) and a corresponding `TerminalAction::AttachSelectedBlocksAsContext`. Handler reads `selected_navigable_items`, materialises each as a context chip via the existing context-chip system in `app/src/context_chips/`, and attaches them to the agent input. Predicate: same `TerminalView_AgentMode_Navigable` & a new `Selection_NonEmpty` context. + +### 7. Configurable block-type filter (Settings → AI) + +Add three boolean fields to `AISettings` in `app/src/settings/ai.rs:710-827` following the existing pattern: + +```rust +agent_block_nav_responses_enabled: AgentBlockNavResponsesEnabled { + type: bool, default: true, supported_platforms: ALL, + sync_to_cloud: true, private: false, + toml_path: "ai.agent_block_navigation.responses_enabled", + description: "Include agent responses in block navigation", +}, +agent_block_nav_commands_enabled: AgentBlockNavCommandsEnabled { … }, +agent_block_nav_prompts_enabled: AgentBlockNavPromptsEnabled { … }, +``` + +`iter_navigable_items` reads these and skips kinds that are off (§7–§11). When all three are off it yields nothing for agent items; combined with the `TerminalView_AgentMode_Navigable` predicate, the bindings simply don't fire (§10). + +UI: add `render_block_navigation_section(&self, app)` to `app/src/settings_view/ai_page.rs` and call it from the existing AI page render at `:2186`. Three `render_ai_setting_toggle::<…>(…)` calls — labels "Agent responses", "Agent-executed commands", "User prompts", grouped under a "Block navigation" subhead. + +### 8. Per-block actions (rerun / retry / fork) + +The existing terminal-block action affordance lives alongside `block_list_element.rs`. Mirror it for agent items by: + +- For `NavigableItemId::AgentCommand` — wire **Rerun command** to dispatch the same command through the agent's command-execution path. +- For `NavigableItemId::AgentResponse` — wire **Retry from here** to call into `AIConversation` to truncate the conversation at this exchange and restart generation. +- For `NavigableItemId::AgentPrompt` — wire **Fork conversation from here** to create a new `AIConversation` seeded with all exchanges *before* the selected prompt. + +These are gated to single-block selections (§28). The hover/keyboard affordance reuses the same UI primitives the terminal-block action menu already uses. + +### 9. Selection rendering + +`block_list_element.rs:108-134` already paints selection borders using `SelectionBorderWidth` and theme tokens. Extend the per-item rendering to ask `selected_navigable_items.is_selected(item_id)` (in addition to today's `selected_blocks.is_selected(block_index)`) so agent items pick up the identical border treatment — satisfying §24 with no new theme tokens. + +### 10. Streaming, deletion, focus + +- **Streaming (§37, §38)** — selection is by `AIAgentExchangeId`, which is stable across stream growth; no special handling needed beyond §2. +- **Block deleted while selected (§39)** — wire a hook in `AIConversation` mutation paths (truncation, retraction) to call `selected_navigable_items.invalidate(removed_id)`, which advances the tail to the next-newer then next-older item, then clears. +- **Focus rules (§30, §31, §32)** — wire into the existing agent input focus event handling: clear selection when the input takes a printable character, on Escape, and on prompt-send. + +## End-to-end flow + +```mermaid +sequenceDiagram + participant User + participant Keymap as Keymap (init.rs) + participant TV as TerminalView + participant Iter as iter_navigable_items + participant Sel as SelectedNavigableItems + participant Render as block_list_element + + User->>Keymap: CMD-UP + Keymap->>TV: dispatch TerminalAction::SelectPriorBlock + TV->>Iter: walk in display order, apply AISettings filter + Iter-->>TV: ordered NavigableItemIds + TV->>Sel: range_select(prev item) + Sel-->>TV: updated selection + TV->>Render: re-render with selected_navigable_items + Render-->>User: selection border + scroll into view +``` + +## Testing and validation + +Tests are organised per product-spec invariant. Each numbered group lists invariants → test type → location. + +**Unit (Rust, `cargo nextest`)** + +- `SelectedNavigableItems` API parity with `SelectedBlocks` — invariants §13–§21. New `selected_navigable_items_tests.rs` next to the new module. +- `iter_navigable_items` filter — invariants §7–§11 — table-driven test enumerating every combination of the three setting toggles against a fixture conversation containing at least one prompt, one response, and one agent-run command. Confirm §11 selection-clear on filter change. +- `NavigableItemId::AgentResponse(exchange_id)` selection stability across streaming chunks — invariant §4, §37. Drive an `AIAgentOutputStatus::Streaming` fixture forward and assert selection identity is preserved. +- Iterator skips collapsed items — invariant §6. + +**Integration (`crates/integration` Builder/TestStep framework)** + +- Cover invariants §13, §16, §17, §18, §19, §20, §21 with a single test that starts an agent conversation, sends three prompts (each producing a response and one agent-run command), and exercises CMD-UP, UP/DOWN, SHIFT-UP/DOWN, CMD-DOWN at the bottom edge, and Escape clearing selection. Use the agent-mode harness already present in `crates/integration` (see `warp-integration-test` skill for conventions). +- Cover invariants §22, §23 (SHIFT-CMD scroll) by selecting a long agent response and asserting viewport top/bottom positions before and after. +- Cover invariants §25, §26 (copy + attach-as-context) via clipboard inspection and agent-input chip inspection. +- Cover invariants §41, §42 by enabling `InputMode::PinnedToTop` then switching to agent mode and confirming CMD-UP still selects the bottom-most item; and by activating AltScreen on the underlying terminal then confirming agent-mode CMD-UP still works. +- Cover invariant §39 (block deleted while selected) by selecting an exchange then triggering a "retry from here" on a *different* exchange that truncates the selected one out. + +**Manual** + +- Visual parity: screenshot side-by-side of a selected terminal block and a selected agent block, confirming border width, colour, and multi-select rendering match — invariant §24. +- Settings UI: walk through every toggle combination and confirm the on-page description and grouping match the existing AI page conventions. +- Accessibility (§43, §44): VoiceOver pass on macOS confirming the announced name for each block kind and contrast on the selection border using existing theme tokens. + +**Cross-mode consistency (§45)** is verified implicitly by the unit test that asserts `TerminalAction::SelectPriorBlock` produces the same `range_select` mutation regardless of whether the current head item is `Terminal(_)` or one of the agent variants. + +## Risks and mitigations + +- **Broadening the keybinding predicate could regress terminal mode** — mitigated by keeping the existing terminal predicate clause unchanged and adding the agent clause as a disjunction. The terminal-only branch in handlers continues to operate on the legacy `selected_blocks` mirror. +- **Two selection fields can drift** — `selected_navigable_items` is the single writer; `selected_blocks` is mirrored from it for the terminal-only subset. Add a `debug_assert!` in `change_block_selections` that the two agree whenever the navigable selection contains only `Terminal(_)` items. +- **`cmd+shift+k` collision** — confirmed unused in agent shortcuts (`shortcuts/mod.rs:109-250`), but cross-platform check with the global keymap is required before merge; if a collision surfaces, fall back to `cmd+shift+m`. +- **Per-block "Retry from here" cancels in-flight stream** — product §37 makes this explicit; ensure the retry action calls into the existing agent cancellation path before issuing a new generation, or the conversation will end up in an inconsistent state. + +## Follow-ups + +- The new "block actions" affordance for agent items (§8) is parallel to terminal block actions; once stable, consider unifying the two action menus behind a `NavigableItemId`-keyed action registry. +- Internal navigation *within* a single agent response (heading-to-heading, code-block-to-code-block) is explicitly out of scope (product spec Non-goals); revisit if user feedback asks for it. +- If the inline-mode iteration in §2 turns out to be hot, memoise the unified order and invalidate on block-list / conversation mutation.