docs(comments): rewrite comments across all crates to the guidelines

Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
This commit is contained in:
ZacharyZhang-NY committed 2026-07-23 16:55:39 -04:00
1 parent ff0fb56c67
commit a02b555e66
1458 files changed
+10729 -21750

No files matched your search

@@ -56,7 +56,7 @@ impl AgentView {
/// (newest-response-wins).
///
/// Monotonic accept-the-newer: a never-seen `response_id` is strictly newer
/// than any previously accepted one, so it supersedes the shown chips; a
/// than any earlier-accepted one, so it supersedes the shown chips; a
/// re-delivery of an already-accepted (hence older) response is ignored, so
/// a buffer-replay or duplicate cannot clobber the newest chips on any
/// turn-boundary path, with no reliance on a clear being wired there and no
@@ -225,7 +225,7 @@ impl AgentView {
/// Two modes:
/// - **Navigation**: j/k move cursor, Space toggles, Enter advances or
/// edits freeform, h/l/[/] cycle questions, 1-9/a-f jump+toggle,
/// n next, s skip, Shift-X kill (only explicit way to dismiss).
/// Shift-X kill (only explicit way to dismiss).
/// - **InputMode**: all keys go to the prompt widget; Esc exits input mode.
pub(super) fn handle_question_key(&mut self, key: &KeyEvent) -> InputOutcome {
use crate::views::question_view::{QuestionFocus, QuestionSelection};
@@ -837,7 +837,6 @@ impl AgentView {
_ => InputOutcome::Changed,
}
}
/// Apply a scroll delta to the question view options.
pub(super) fn apply_question_scroll(&mut self, delta: i32) {
let Some(ref mut qv) = self.question_view else {
return;
@@ -1006,8 +1005,7 @@ impl AgentView {
)
.filter(|&idx| !qv.no_freeform || idx < question.options.len())
}
/// Save the current prompt text into `per_question_freeform[active_tab]`
/// and load the text for the new `active_tab` into the prompt widget.
/// Save the current prompt text into `per_question_freeform[active_tab]`.
/// Call this BEFORE changing `active_tab`.
fn swap_question_freeform(&mut self) {
let Some(ref mut qv) = self.question_view else {
@@ -1035,7 +1033,7 @@ impl AgentView {
///
/// Restores the original prompt text that was stashed when the question
/// view opened, so typed "additional context" doesn't leak into the
/// main prompt. Also clears any stashed (tab-hidden) question view.
/// main prompt.
fn dismiss_question_view(&mut self) {
if let Some(qv) = self.question_view.take() {
self.turn_paused_duration += qv.opened_at.elapsed();
@@ -1130,9 +1128,6 @@ impl AgentView {
InputOutcome::Changed
}
/// Map a screen position to a permission option index.
///
/// Uses the prompt area and permission chrome height to determine which
/// option row the mouse is over. Returns `None` if outside the options.
pub(super) fn permission_item_at(&self, _col: u16, row: u16) -> Option<usize> {
let perm = self.permission_queue.front()?;
let prompt_area = self.pane_areas.prompt;
@@ -1156,8 +1151,6 @@ impl AgentView {
None
}
}
/// Clean up question-related visual state after the question view is
/// dismissed (submit, cancel, or replacement).
fn cleanup_question_state(&mut self) {
self.hovered_question_item = None;
self.question_scrollbar_dragging = false;
@@ -56,7 +56,6 @@ impl AgentView {
}
});
}
/// Return the URL of the currently highlighted link, if any.
pub fn highlighted_link_url(&self) -> Option<&str> {
self.highlighted_link_idx
.and_then(|idx| self.visible_link_map.links().get(idx))
@@ -208,7 +207,6 @@ mod link_click_tests {
agent.pane_areas.scrollback = area;
agent.active_pane = AgentPane::Scrollback;
}
/// Add a link to the visible_link_map covering (col_start..col_end, row).
fn add_visible_link(agent: &mut AgentView, row: u16, col_start: u16, col_end: u16, url: &str) {
let mut overlay = LinkOverlay::new();
overlay.push(OverlayLink {
@@ -936,7 +934,7 @@ mod link_click_tests {
assert!(agent.inline_edit.is_some(), "Enter must start inline edit");
}
/// Bash prompts are not inline-editable: Enter falls through to the
/// registry (OpenBlockViewer) exactly as before.
/// registry (OpenBlockViewer).
#[test]
fn enter_on_selected_bash_prompt_falls_through() {
let mut agent = make_agent();
@@ -954,8 +952,6 @@ mod link_click_tests {
"expected fall-through to OpenBlockViewer, got {outcome:?}"
);
}
/// Double-click on a user prompt enters inline edit mode (replacing the
/// old fold-toggle for editable prompts).
#[test]
fn double_click_on_user_prompt_enters_inline_edit() {
let mut agent = make_agent();
@@ -12,9 +12,6 @@ use ratatui::layout::Rect;
use ratatui::style::Style;
impl AgentView {
// -- Image viewer input --------------------------------------------------
/// Handle a key event in the image viewer modal.
pub(super) fn handle_image_viewer_key(&mut self, key: &KeyEvent) -> InputOutcome {
use crossterm::event::KeyCode;
@@ -24,9 +21,7 @@ impl AgentView {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
// Clear the Kitty image before closing.
// Old code bypassed STDERR_OUTPUT_LOCK which could interleave
// mid-frame. Safe to revert: content is valid escapes, not raw text.
// Kitty images outlive the dropped viewer state; clear before close.
kigi_shell::util::with_locked_stderr(|stderr| {
let clear = PostFlush::from(overlay::clear_kitty());
let _ = clear.write_to(stderr);
@@ -43,8 +38,6 @@ impl AgentView {
InputOutcome::Changed
}
// -- Inline media rendering -----------------------------------------------
/// Build Kitty/iTerm2 escape sequences for an inline media placement.
pub(super) fn build_inline_media_escapes(
&mut self,
@@ -85,7 +78,6 @@ impl AgentView {
let mut transmit_esc = String::new();
if needs_transmit {
// Load bytes from disk (or use cached bytes if available).
if !self.inline_media_cache.contains_key(path) {
let bytes = if placement.info.is_video {
let (frame_bytes, _, _) = crate::prompt_images::extract_poster_frame(path)?;
@@ -184,8 +176,6 @@ impl AgentView {
screen_rect: rect,
source,
} = aff;
// The transient `rendering…` hint shows only while an on-click render
// for this diagram is in flight.
let rendering = self.diagram_is_rendering(&source);
let row = affordance_row(rendering);
// A segment is drawn only if it fits wholly within the row width
@@ -194,7 +184,6 @@ impl AgentView {
let fits =
|col: u16, label: &str| col + UnicodeWidthStr::width(label) as u16 <= rect.width;
// Leading dim, non-clickable `◇ mermaid` label.
let (label_col, label_text) = row.label;
if fits(label_col, label_text) {
buf.set_string_safe(
@@ -245,7 +234,6 @@ impl AgentView {
}
}
// Trailing dim `rendering…` hint after the buttons (not clickable).
if let Some((col, status)) = row.status
&& fits(col, status)
{
@@ -259,13 +247,10 @@ impl AgentView {
}
}
/// Whether the diagram with `source` has an on-click render in flight (drives
/// the affordance row's transient `rendering…` hint).
fn diagram_is_rendering(&self, source: &str) -> bool {
self.mermaid_is_rendering(source)
}
/// Get or allocate a Kitty image ID for the given media path.
fn get_or_alloc_media_id(&mut self, path: &std::path::Path) -> u32 {
if let Some(&id) = self.inline_media_ids.get(path) {
return id;
@@ -401,7 +386,6 @@ impl AgentView {
/// path, restarts from the beginning. Frames are extracted via ffmpeg in
/// a background thread so the UI never blocks.
pub(crate) fn start_inline_video_playback(&mut self, path: &std::path::Path) {
// If already loaded for this path, just restart.
if let Some(ref mut video) = self.inline_video
&& video.path == path
{
@@ -410,7 +394,6 @@ impl AgentView {
video.last_frame_time = std::time::Instant::now();
return;
}
// Extract frames in a background thread to avoid blocking the UI.
let path_owned = path.to_path_buf();
let (tx, rx) = std::sync::mpsc::channel();
self.video_load_rx = Some(rx);
@@ -431,8 +414,6 @@ impl AgentView {
});
}
// -- Inline media click handling -----------------------------------------
/// Handle a click on inline media buttons. Returns `Some(InputOutcome)` if
/// the click was consumed, `None` to fall through to normal handling.
pub(in crate::app) fn handle_inline_media_click(
@@ -456,7 +437,6 @@ impl AgentView {
return Some(InputOutcome::Changed);
}
// [Play] button or video poster → start/restart inline playback.
let play_target = self
.inline_media_hits
.play_buttons
@@ -469,7 +449,6 @@ impl AgentView {
return Some(InputOutcome::Changed);
}
// [Copy] button → copy image to clipboard (async).
if let Some((_, path)) = self
.inline_media_hits
.copy_image_buttons
@@ -486,7 +465,6 @@ impl AgentView {
return Some(InputOutcome::Changed);
}
// Click on filepath line → copy path to clipboard.
if let Some((_, path)) = self
.inline_media_hits
.filepath_areas
@@ -498,9 +476,8 @@ impl AgentView {
return Some(InputOutcome::Changed);
}
// Mermaid affordance row → render-on-click (Open/Copy path) or copy
// source. Resolve the kind + source index first so the `mermaid_buttons`
// borrow ends before the `&mut self` dispatch below.
// Resolve the kind + source index first so the `mermaid_buttons` borrow
// ends before the `&mut self` dispatch below.
let mermaid_hit = self
.inline_media_hits
.mermaid_buttons
@@ -552,9 +529,6 @@ impl AgentView {
}
}
// -- Video viewer input --------------------------------------------------
/// Handle a key event in the video viewer modal.
pub(super) fn handle_video_viewer_key(&mut self, key: &KeyEvent) -> InputOutcome {
use crossterm::event::KeyCode;
@@ -564,7 +538,7 @@ impl AgentView {
match key.code {
KeyCode::Esc | KeyCode::Char('q') => {
// Clear the Kitty image before closing.
// Kitty images outlive the dropped viewer state; clear before close.
kigi_shell::util::with_locked_stderr(|stderr| {
let clear = PostFlush::from(overlay::clear_kitty());
let _ = clear.write_to(stderr);
@@ -588,17 +562,13 @@ impl AgentView {
InputOutcome::Changed
}
// -- /gboom easter egg input ------------------------------------------------
/// Handle a key event in the `/gboom` game modal.
pub(super) fn handle_gboom_key(&mut self, key: &KeyEvent) -> InputOutcome {
let Some(ref mut gboom) = self.gboom else {
return InputOutcome::Unchanged;
};
match gboom.handle_key(key) {
crate::gboom::GboomKeyOutcome::Close => {
// Clear the kitty image before closing (same as the video
// viewer) so no stale frame lingers in the cell grid.
// Kitty images outlive the dropped game state; clear before close.
kigi_shell::util::with_locked_stderr(|stderr| {
let clear = PostFlush::from(overlay::clear_kitty());
let _ = clear.write_to(stderr);
@@ -183,10 +183,6 @@ pub(super) fn active_contexts_for_pane(pane: ActivePane) -> Vec<crate::actions::
///
/// This will grow as we add more panes (tasks, review files, etc.).
pub type AgentPane = ActivePane;
/// Per-agent view-model.
///
/// Owns both business state (session, entries) and UI state (scroll,
/// selection, pane focus). See module docs for future split plans.
/// MCP server initialization progress, received from the shell.
#[derive(Debug, Clone)]
pub struct McpInitProgress {
@@ -254,11 +250,9 @@ impl HitArea {
self.hovered = new;
changed
}
/// Check if a position is inside the rect.
pub fn contains(&self, col: u16, row: u16) -> bool {
self.rect.is_some_and(|r| r.contains((col, row).into()))
}
/// Set the rect (called during render).
pub fn set(&mut self, rect: Option<Rect>) {
self.rect = rect;
}
@@ -266,7 +260,6 @@ impl HitArea {
pub fn set_unless_dropdown(&mut self, rect: Option<Rect>, dropdown_open: bool) {
self.set(if dropdown_open { None } else { rect });
}
/// Clear rect and hover.
pub fn clear(&mut self) {
self.rect = None;
self.hovered = false;
@@ -641,7 +634,7 @@ pub struct AgentView {
/// [`Self::self_originated_prompt_ids`] in the ACP gate / turn-start shim:
/// a client that has driven a turn can still go on to VIEW a turn another
/// client drives (e.g. a `/loop` cron, or a plain prompt typed in another
/// pane), so this flag is no longer a one-way latch.
/// pane), so this flag is not a one-way latch.
pub attached_as_viewer: bool,
/// Prompt ids of turns THIS client originated (sent to the agent as the
/// turn driver). The ACP gate consults this to keep `attached_as_viewer`
@@ -694,7 +687,6 @@ pub struct AgentView {
pub active_pane: AgentPane,
/// Current mode of the prompt widget (normal vs editing a queued prompt).
pub prompt_mode: PromptMode,
/// Current special prompt input mode (Normal/Bash/Feedback/Remember).
pub prompt_input_mode: PromptInputMode,
/// Multiline input mode: swap Enter (insert newline) and Shift+Enter (send).
/// Toggled by `Ctrl+M` or `/multiline`. Not persisted across sessions.
@@ -992,9 +984,8 @@ pub struct AgentView {
/// so switching between images is a cheap re-place (~80 bytes) instead
/// of a full re-transmit. ID 1 is reserved for modal overlays.
pub(crate) inline_media_ids: std::collections::HashMap<std::path::PathBuf, u32>,
/// Paths whose iTerm2 inline data has already been emitted this placement
/// cycle. Avoids re-sending full base64 image data every TUI frame.
/// Last iTerm2 placement per path — re-emit when `screen_rect` changes.
/// Last iTerm2 placement rect per path. Avoids re-sending full base64 image
/// data every TUI frame; re-emitted only when a path's `screen_rect` changes.
pub(crate) inline_media_iterm_emitted:
std::collections::HashMap<std::path::PathBuf, ratatui::layout::Rect>,
/// Counter for allocating the next Kitty image ID.
@@ -1176,8 +1167,8 @@ pub struct AgentView {
/// cancel falls back to that UI/config field, then the prompt panel.
pub(crate) cancel_subagents_preference: Option<bool>,
/// What gesture triggered the pending turn-cancel (Ctrl+C / mouse; Esc
/// only via the cancel-retry path while TurnCancelling — a bare Esc no
/// longer starts a cancel).
/// only via the cancel-retry path while TurnCancelling — a bare Esc does
/// not start a cancel).
/// Set by the key/mouse handler, consumed by `do_cancel_turn` / the
/// cancel-retry path so `session/cancel` carries `_meta.cancelTrigger`.
pub(crate) cancel_trigger_hint: Option<crate::app::actions::CancelTrigger>,
@@ -1364,7 +1355,7 @@ pub struct AgentView {
pub(crate) follow_up_seen: HashMap<String, u64>,
/// Monotonic generation assigned to the next newly-accepted `response_id`.
/// The ordering key for newest-wins: a fresh id takes the next value (the
/// new high-water), so every previously-seen id is strictly lower.
/// new high-water), so every already-seen id is strictly lower.
pub(crate) follow_up_next_gen: u64,
/// Stamped `kigi/follow_ups` that arrived for a turn that is NOT yet the
/// currently-adopted one, keyed by `promptId`. Ext notifications and
@@ -10,8 +10,6 @@ use crate::views::file_search::line_viewer::LineViewerState;
use crossterm::event::{KeyCode, KeyEvent};
impl AgentView {
// -- Agents modal input handling --
pub(super) fn handle_agents_modal_key(
&mut self,
key: &crossterm::event::KeyEvent,
@@ -102,8 +100,6 @@ impl AgentView {
}
}
// -- Persona detail modal input handling --
pub(super) fn handle_persona_detail_key(
&mut self,
key: &crossterm::event::KeyEvent,
@@ -156,8 +152,6 @@ impl AgentView {
}
}
// -- Hooks/plugins modal input handling --
pub(super) fn handle_extensions_modal_key(
&mut self,
key: &crossterm::event::KeyEvent,
@@ -218,7 +212,6 @@ impl AgentView {
};
}
// If in input mode, route to input handler.
if self
.extensions_modal
.as_ref()
@@ -366,7 +359,6 @@ impl AgentView {
}
}
// Delegate navigation/search/tab/filter/action to handle_picker_input.
let Some(state) = self.extensions_modal.as_mut() else {
return InputOutcome::Changed;
};
@@ -439,8 +431,6 @@ impl AgentView {
&config,
);
// Search state now lives directly in picker_state (no sync needed).
match outcome {
crate::views::picker::PickerOutcome::Closed => {
self.extensions_modal = None;
@@ -472,7 +462,6 @@ impl AgentView {
}
_ => {}
}
// Reset selection after filter change.
state.picker_state.selected = 0;
state.picker_state.scroll_offset = None;
state.picker_state.tabs_focused = false;
@@ -634,7 +623,8 @@ impl AgentView {
None
}
}
_ => None, // Unhandled — fall through to picker
// Unhandled — fall through to picker
_ => None,
}
};
// Dispatch shortcut click (if any) now that the &mut borrow is released.
@@ -709,7 +699,8 @@ impl AgentView {
crate::views::extensions_modal::ExtensionsTab::McpServers => state.mcps_filter,
_ => crate::views::extensions_modal::StatusFilter::All,
};
let action_keys: Vec<(char, &str)> = vec![]; // No action keys for mouse
// No action keys for mouse
let action_keys: Vec<(char, &str)> = vec![];
let entry_count = state.entry_data_indices.len();
let non_selectable_owned = Self::extensions_modal_non_selectable_mask(state, entry_count);
let non_selectable = &non_selectable_owned;
@@ -904,13 +895,11 @@ impl AgentView {
if let Some(gk) = group_key {
let is_expanded = state.is_group_expanded(sel, &gk);
// `set_collapsed`'s third arg is the NEW collapsed state.
// When currently expanded → new state is collapsed (true);
// when currently collapsed → new state is expanded (false).
// That value equals `is_expanded` directly. Using `!is_expanded`
// (the previous code) made `e`/Enter/Space/click into a no-op
// for every collapsible header (MCP servers and hooks
// groups).
// `set_collapsed`'s third arg is the NEW collapsed state:
// currently expanded → collapse (true); currently collapsed →
// expand (false). That value equals `is_expanded` directly.
// Passing `!is_expanded` makes `e`/Enter/Space/click a no-op for
// every collapsible header (MCP servers and hooks groups).
self.extensions_modal_set_collapsed(sel, &gk, is_expanded);
} else {
// Leaf item: toggle detail fields.
@@ -921,7 +910,6 @@ impl AgentView {
}
}
/// Set the collapsed state for a group key in the extensions modal.
fn extensions_modal_set_collapsed(
&mut self,
sel: usize,
@@ -1149,7 +1137,6 @@ impl AgentView {
}
}
ButtonAction::RemoveSelectedHook => {
// Remove the hook source_dir of the currently selected hook.
if let Some(ref state) = self.extensions_modal {
use crate::views::extensions_modal::TabDataState;
if let TabDataState::Loaded(ref data) = state.hooks_data
@@ -258,7 +258,8 @@ impl AgentView {
return true;
}
*remaining = remaining.saturating_sub(1);
return true; // redraw to advance fade
// redraw to advance fade
return true;
}
false
}
@@ -8,10 +8,6 @@ use crate::key;
use crate::scrollback::ScrollbackSearchState;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
impl AgentView {
/// Scrollback-focused key handling.
///
/// When the block viewer is open, routes keys to the viewer.
/// Otherwise, uses ActionRegistry for keybinding lookup.
pub(super) fn handle_scrollback_key(
&mut self,
key: &KeyEvent,
@@ -248,7 +244,6 @@ impl AgentView {
}
changed
}
/// Scroll the current search match into view via `reveal_entry_line`.
fn reveal_current_search_match(&mut self) {
let target = self
.scrollback_search
@@ -261,10 +256,6 @@ impl AgentView {
self.scrollback.reveal_entry_line(idx, line);
}
}
/// Todo-pane-focused key handling.
///
/// Routes structural keys through the shared overlay handler, then
/// content keys through `TodoPane::handle_key`.
pub(super) fn handle_todo_key(
&mut self,
key: &KeyEvent,
@@ -300,7 +291,6 @@ impl AgentView {
InputOutcome::Unchanged
}
}
/// Bg-task-pane-focused key handling.
pub(super) fn handle_bg_tasks_key(
&mut self,
key: &KeyEvent,
@@ -431,7 +421,6 @@ impl AgentView {
InputOutcome::Unchanged
}
}
/// Subagent-pane-focused key handling.
pub(super) fn handle_catalog_key(
&mut self,
key: &KeyEvent,
@@ -263,10 +263,6 @@ impl AgentView {
crate::wrap_clipboard_image::WrapImagePaste::NoImage => InputOutcome::Unchanged,
})
}
/// Parse a paste payload as one or more drop-style file paths and
/// route each entry: image paths become `[Image #N]` chips, non-image
/// paths get inserted as decoded absolute path text.
///
/// Route a popup pane's `Event::Paste(text)` through the drop
/// classifier and fall back to a plain text paste into the shared
/// prompt buffer. Used by the plan-feedback, permission-followup,
@@ -297,16 +293,9 @@ impl AgentView {
/// `"file://{png} file://{txt}"` → `[Image #N] {canon_txt} `;
/// `"file://{txt} file://{png}"` → `{canon_txt} [Image #N] `.
///
/// **Size guard**: payloads ≥ `DROP_CLASSIFIER_MAX_BYTES`
/// short-circuit to `None`. The early-return lives inside this
/// function (not at each call site) so every paste arm — the
/// main Prompt bracketed-paste arm, the four popup `Event::Paste`
/// arms (plan-feedback, permission-followup, plan-approval,
/// question-view), and the Cmd+V `handle_paste_key_deferred` path
/// (clipboard-text, plus deferred file-urls on completion) — gets the
/// guard uniformly. Real drag-and-drop payloads (one or more
/// `file://` URLs) are at most a few KB; anything ≥ 10 MB is a
/// log/code paste and not worth iterating line-by-line.
/// **Size guard**: payloads ≥ `DROP_CLASSIFIER_MAX_BYTES` short-circuit
/// to `None`. The early-return lives here, not at each call site, so every
/// paste arm gets the guard uniformly.
pub(super) fn try_handle_dropped_paths_paste(
&mut self,
text: &str,
@@ -1544,10 +1533,9 @@ pub(super) mod paste_key_tests {
assert!(agent.ephemeral_tip.is_active());
assert_eq!(counts.get("t_seen"), Some(&1));
}
/// `note_terminal_size` keeps the draw-path semantics it replaced:
/// Kitty IDs are invalidated only on an actual size change, the
/// `(0, 0)` pre-first-draw state never counts as a resize, and every
/// re-measure clears the resize-event staleness flag.
/// `note_terminal_size` invariants: Kitty IDs are invalidated only on an
/// actual size change, the `(0, 0)` pre-first-draw state never counts as a
/// resize, and every re-measure clears the resize-event staleness flag.
#[test]
fn note_terminal_size_invalidates_kitty_ids_only_on_change() {
let mut agent = make_agent();
@@ -1881,8 +1869,8 @@ pub(super) mod paste_key_tests {
);
assert_eq!(embedded.items.width, prompt.width);
}
/// The tool-media inline-image path (`build_inline_media_escapes`, still live
/// for tool calls) transmits the bytes (`a=t`) before placing them (`a=p`) on
/// The tool-media inline-image path (`build_inline_media_escapes`, used for
/// tool calls) transmits the bytes (`a=t`) before placing them (`a=p`) on
/// the first paint, then places only (no re-transmit) on a later frame for
/// the same path — a place-without-transmit would render blank.
#[test]
@@ -1978,7 +1966,6 @@ pub(super) mod paste_key_tests {
assert!(agent.inline_video.is_none());
assert!(agent.take_inline_media_clear_escapes().is_none());
}
/// An agent with no placements has nothing to clear.
#[test]
fn take_inline_media_clear_escapes_none_when_no_placements() {
let mut agent = make_agent();
@@ -1,5 +1,5 @@
//! Plan surfaces: plan chip/preview, plan approval + feedback, and casual
//! plan commenting (incl. the casual-commenting test fixture).
//! plan commenting.
use super::AgentView;
#[cfg(test)]
use super::{ActivePane, InputMode, test_fixtures};
@@ -15,7 +15,6 @@ use crate::views::prompt_widget::{EnterOutcome, PromptEvent};
use crossterm::event::KeyModifiers;
use crossterm::event::{KeyCode, KeyEvent};
impl AgentView {
/// Resolve the absolute path to the plan file for this session.
fn plan_file_path(&self) -> Option<std::path::PathBuf> {
let session_id = self.session.session_id.as_ref()?;
let cwd_str = self.session.cwd.to_string_lossy().into_owned();
@@ -28,38 +27,31 @@ impl AgentView {
.join("plan.md"),
)
}
/// Whether the current line viewer is showing a plan preview.
pub(super) fn is_plan_viewer(&self) -> bool {
self.line_viewer.as_ref().is_some_and(|v| {
v.kind == crate::views::file_search::line_viewer::LineViewerKind::PlanPreview
})
}
/// Whether the user is currently composing a comment via the prompt
/// input inside the *casual* plan preview (the modal opened with no
/// `plan_approval_view`). Mirrors the `pav.focus == Commenting`
/// check used by the plan-approval path so the prompt/footer
/// behaves identically across both modes.
/// True while the user composes a comment in the *casual* plan preview
/// (the modal opened with no `plan_approval_view`). Mirrors the
/// `pav.focus == Commenting` check of the plan-approval path so the
/// prompt/footer behave identically across both modes.
pub(super) fn is_casual_commenting(&self) -> bool {
self.plan_approval_view.is_none()
&& self.is_plan_viewer()
&& self.casual_commenting_range.is_some()
}
/// Whether the prompt "auto" (LLM classifier mode) flag should render.
/// Extracted for unit testing the precedence: auto shows only when the
/// session is in auto mode and neither yolo (always-approve wins) nor plan
/// is active.
/// Whether the prompt "auto" (LLM classifier mode) flag should render:
/// only when the session is in auto mode and neither yolo (always-approve
/// wins) nor plan is active.
pub(super) fn auto_flag_visible(&self, effective_plan: bool) -> bool {
self.session.is_auto() && !self.session.is_yolo() && !effective_plan
}
/// Whether plan content is available for preview.
fn plan_preview_available(&self) -> bool {
self.plan_body_for_preview().is_some()
}
/// Whether the "plan" status-bar chip should be rendered.
///
/// Visible while plan mode is active, or always when the user has set
/// `show_plan_chip = true` in `pager.toml`. Hidden by default once the
/// user exits plan mode.
/// Visible while plan mode is active, or always when `show_plan_chip` is
/// set in `pager.toml`; hidden by default once the user exits plan mode.
pub(super) fn should_show_plan_chip(
&self,
appearance: &crate::appearance::AppearanceConfig,
@@ -73,11 +65,9 @@ impl AgentView {
.and_then(|p| p.plan_content.as_deref())
.filter(|s| !s.trim().is_empty())
}
/// Resolve the plan body for the line-viewer preview.
///
/// Prefers content carried on the approval request (inline plan-creation or
/// the shell-read file body), then falls back to the on-disk plan file.
/// Request body first keeps file-backed previews working when the path
/// Request body first keeps file-backed previews working when path
/// resolution fails or the file disappears between intercept and open.
fn plan_body_for_preview(&self) -> Option<String> {
if let Some(content) = self
@@ -106,8 +96,6 @@ impl AgentView {
self.show_plan_preview();
}
}
/// Show the plan in the line viewer overlay or a "no plan" toast.
///
/// When plan approval is parked without a body, opens a placeholder
/// preview so the user always sees a decision surface (a/s/q) instead of
/// a dead "Waiting on plan approval" line with a no-op Tab:plan.
@@ -154,14 +142,10 @@ impl AgentView {
}
self.line_viewer = Some(viewer);
}
/// Test fixture: drive the agent into casual-commenting state
/// (line viewer open in plan-preview mode + `casual_commenting_range`
/// armed) so the `Event::Paste` plan-feedback arm at ~1539 is
/// reachable from a unit test without spawning the real
/// keystroke pipeline. Consolidates three field mutations into
/// one helper so a future refactor of casual-commenting state
/// only has to update this fixture rather than every test that
/// reaches into the fields by name.
/// Drive the agent into casual-commenting state (line viewer open in
/// plan-preview mode + `casual_commenting_range` armed) so the paste
/// plan-feedback arm is reachable from a unit test without the real
/// keystroke pipeline.
#[cfg(test)]
pub(crate) fn enter_casual_commenting_for_test(&mut self) {
let mut viewer =
@@ -266,10 +250,8 @@ impl AgentView {
viewer.plan_mut().feedback_active = true;
}
}
/// Discard an in-progress comment draft: clear the prompt text and
/// drop the selected line range + pending edit + stashed feedback.
/// Used whenever focus leaves the prompt without an explicit save
/// or cancel (e.g. Tab back to Preview, click into the modal).
/// Called whenever focus leaves the prompt without an explicit save or
/// cancel (e.g. Tab back to Preview, click into the modal).
fn discard_in_progress_comment(&mut self) {
if let Some(ref mut pav) = self.plan_approval_view {
pav.commenting_range = None;
@@ -484,11 +466,8 @@ impl AgentView {
}
InputOutcome::Changed
}
/// Enter casual commenting mode from the plan preview.
///
/// If the cursor is on a comment line, enter edit mode for that comment.
/// If the cursor is on a source line, capture the line range and enter
/// new-comment mode.
/// If the cursor is on a comment line, edit that comment; if on a source
/// line, capture the line range and start a new comment.
pub(super) fn enter_casual_plan_commenting(&mut self) -> InputOutcome {
let viewer = match self.line_viewer.as_mut() {
Some(v) => v,
@@ -535,7 +514,6 @@ impl AgentView {
self.prompt.set_text("");
InputOutcome::Changed
}
/// Save the current casual comment (new or edited) and rebuild the viewer.
pub(super) fn save_casual_plan_comment(&mut self) -> InputOutcome {
let text = self.prompt.text().to_owned();
if text.trim().is_empty() {
@@ -570,7 +548,6 @@ impl AgentView {
}
InputOutcome::Changed
}
/// Cancel casual plan commenting without saving.
pub(super) fn cancel_casual_plan_commenting(&mut self) -> InputOutcome {
self.casual_commenting_range = None;
self.casual_editing_comment_id = None;
@@ -581,11 +558,9 @@ impl AgentView {
}
InputOutcome::Changed
}
/// Key handler used while the user is composing a casual plan
/// comment via the prompt input. Mirrors `handle_plan_feedback_key`
/// (which serves the plan-approval Commenting focus) so the UX is
/// identical: Enter saves, Esc cancels, Tab cancels back to the
/// modal, and everything else routes to the prompt textarea.
/// Mirrors `handle_plan_feedback_key` (the plan-approval Commenting focus)
/// so the casual-commenting UX is identical: Enter saves, Esc cancels, Tab
/// cancels back to the modal, everything else routes to the prompt textarea.
pub(super) fn handle_casual_plan_feedback_key(&mut self, key: &KeyEvent) -> InputOutcome {
if key.code == KeyCode::Esc {
if self.prompt.file_search_visible() {
@@ -612,7 +587,6 @@ impl AgentView {
PromptEvent::Ignored => InputOutcome::Changed,
}
}
/// Delete the casual comment under the cursor in the plan preview.
pub(super) fn delete_casual_plan_comment_at_cursor(&mut self) -> InputOutcome {
let viewer = match self.line_viewer.as_ref() {
Some(v) => v,
@@ -658,8 +632,6 @@ impl AgentView {
#[cfg(test)]
mod prompt_flag_tests {
use super::test_fixtures::make_agent;
/// The prompt "auto" (classifier) mode flag shows only when the session is
/// in Auto and neither yolo (always-approve wins) nor plan is active.
#[test]
fn auto_flag_visible_precedence() {
let mut agent = make_agent();
@@ -60,15 +60,6 @@ impl AgentView {
history
}
/// Prompt-focused key handling.
///
/// Routes through the action registry FIRST for mapped actions (SendPrompt,
/// FocusScrollback, etc.), then falls through to the widget for text editing.
/// Agent-level and global actions are handled by the caller after this
/// returns Unchanged.
///
/// **Exception**: when the file search dropdown is visible, the widget gets
/// first shot at Tab/Enter/Esc/arrows (for navigation and acceptance).
/// Test-only wrapper around the private `handle_prompt_key` using a
/// **non–VS Code** pinned registry so host `TERM_PROGRAM` cannot change
/// InterjectPrompt / OpenExtensions chords under test.
@@ -89,6 +80,15 @@ impl AgentView {
self.handle_prompt_key(key, registry, false)
}
/// Prompt-focused key handling.
///
/// Routes through the action registry FIRST for mapped actions (SendPrompt,
/// FocusScrollback, etc.), then falls through to the widget for text editing.
/// Agent-level and global actions are handled by the caller after this
/// returns Unchanged.
///
/// **Exception**: when the file search dropdown is visible, the widget gets
/// first shot at Tab/Enter/Esc/arrows (for navigation and acceptance).
// `pub(super)`: also called by `AppView::minimal_key_intercept` to route
// Apple Terminal's Ctrl+O interject chord straight to the prompt path —
// minimal's prompt is conceptually always focused, but `active_pane` can be
@@ -110,7 +110,7 @@ impl AgentView {
// focus back from the /btw panel (its scroll keys are consumed earlier).
self.btw_focused = false;
// ── History panel intercept (modal) ─────────────────────────────
// History panel intercept (modal).
// Must run before the file-search / slash intercepts: a populated
// entry can end on an `@` token (or start with `/`), and the
// dropdown state derived from that text would otherwise steal the
@@ -119,7 +119,7 @@ impl AgentView {
return self.handle_history_search_key(key);
}
// ── File search intercept ───────────────────────────────────────
// File search intercept.
// When the @-completion dropdown is visible, the widget handles
// Tab (accept), Enter (accept), Esc (dismiss), and arrow keys
// BEFORE the action registry gets them. Otherwise Tab would
@@ -127,7 +127,6 @@ impl AgentView {
if self.prompt.file_search_visible() {
match self.prompt.handle_key(key) {
PromptEvent::Edited => {
// Check if the prompt wants to open a line viewer.
if let Some(req) = self.prompt.pending_viewer_request.take() {
self.open_line_viewer(&req.path, req.initial_range);
}
@@ -137,11 +136,12 @@ impl AgentView {
}
return InputOutcome::Changed;
}
PromptEvent::Ignored => {} // fall through to normal routing
// Fall through to normal routing.
PromptEvent::Ignored => {}
}
}
// ── Slash dropdown intercept ────────────────────────────────────
// Slash dropdown intercept.
// When the slash completion dropdown is open, intercept navigation
// and accept keys BEFORE the action registry. Completion changes
// text only — it does NOT execute commands.
@@ -165,13 +165,11 @@ impl AgentView {
return InputOutcome::Changed;
}
match key.code {
// Up / Ctrl-P: move selection up.
KeyCode::Up => {
self.prompt.slash_move_selection(-1);
self.prompt.slash_preview_current_selection();
return InputOutcome::Changed;
}
// Down / Ctrl-N: move selection down.
KeyCode::Down => {
self.prompt.slash_move_selection(1);
self.prompt.slash_preview_current_selection();
@@ -187,13 +185,11 @@ impl AgentView {
self.prompt.slash_preview_current_selection();
return InputOutcome::Changed;
}
// Tab: accept completion (text only, no execute).
KeyCode::Tab => {
self.prompt.slash_commit_preview();
self.prompt.accept_slash_completion(&self.session.models);
return InputOutcome::Changed;
}
// Esc: close dropdown, revert any live preview.
KeyCode::Esc => {
self.prompt.slash_cancel_preview();
self.prompt.slash_close();
@@ -253,7 +249,7 @@ impl AgentView {
}
}
// ── Completion dropdown intercept ────────────────────────────────
// Completion dropdown intercept.
// Priority 4-5 in the Tab chain: when the completion dropdown is
// open, handle navigation/accept/dismiss. When closed, Tab in bash
// mode is terminal-like completion (always on, no env gate).
@@ -348,7 +344,7 @@ impl AgentView {
}
}
// ── Predicted-next-prompt ghost (tab autocomplete) ──────────────
// Predicted-next-prompt ghost (tab autocomplete).
// Tab or Right arrow accepts the suggestion (the ghost only renders
// with the cursor at end-of-text, where Right is otherwise a no-op —
// same convention as fish/zsh autosuggestions); Esc on an empty
@@ -867,9 +863,8 @@ impl AgentView {
.map(str::to_owned)
{
self.prompt.history_search.deactivate();
// Detect `! ` prefix to restore bash mode. Refined: only reset to Normal
// if currently in Bash (preserve Remember if active). The ! prefix
// restore only applies when not in Remember.
// A `! ` entry restores Bash mode, except in Remember mode,
// which is preserved. A non-`!` entry in Bash resets to Normal.
if self.prompt_input_mode != PromptInputMode::Remember
&& let Some(cmd) = text.strip_prefix("! ")
{
@@ -881,7 +876,6 @@ impl AgentView {
} else {
self.prompt.set_text(&text);
}
// Move cursor to end of text.
let len = self.prompt.textarea.text().len();
self.prompt.textarea.set_cursor(len);
// Drop the recomputed `@`-completion context (same
@@ -1122,7 +1116,7 @@ mod combined_prompt_history_tests {
}
/// THIS SESSION's prompts (scrollback blocks) outrank the fetched
/// fetched history: the fetch races the shell-side append of a
/// history: the fetch races the shell-side append of a
/// fresh session's first prompts, so scrollback is the authoritative
/// "newest" source and a just-sent prompt is always recalled first.
#[test]
@@ -1142,9 +1136,12 @@ mod combined_prompt_history_tests {
assert_eq!(
texts(&agent),
[
"just sent", // this session, newest first
"ten", // this session (fetched dup ignored)
"seventeen", // fetched history follows
// this session, newest first
"just sent",
// this session (fetched dup ignored)
"ten",
// fetched history follows
"seventeen",
"sixteen",
"fifteen",
]
@@ -1169,7 +1166,8 @@ mod combined_prompt_history_tests {
texts(&agent),
[
"scrollback_only_new",
" shared ", // trim-keyed dedup: scrollback variant wins
// trim-keyed dedup: scrollback variant wins
" shared ",
"scrollback_only_old",
"fetched_only",
]
@@ -254,7 +254,6 @@ impl AgentView {
}
}
/// The transcript tail is a user-authored prompt row.
fn tail_is_user_prompt(&self) -> bool {
matches!(
self.scrollback.last().map(|entry| &entry.block),
@@ -973,8 +972,8 @@ mod queue_edit_routing_tests {
}
/// Keyboard-deleting the last *local* row while a server row remains keeps
/// the pane open and focused (regression: it previously force-hid the pane
/// and stranded the server rows).
/// the pane open and focused (regression guard: force-hiding it here would
/// strand the server rows).
#[test]
fn delete_last_local_row_keeps_pane_open_when_server_remains() {
let mut agent = make_running_agent();
@@ -117,8 +117,7 @@ impl AgentView {
///
/// Known transient: when a subagent is fullscreen (`active_subagent.is_some()`),
/// draw returns early and the child renders its own bar; Current on the parent
/// still reflects parent context (documented limitation, pre-existing before
/// this change).
/// still reflects parent context (documented limitation).
pub fn current_shortcut_hints(&self, registry: &ActionRegistry) -> Vec<HintItem> {
use crate::views::shortcuts_bar::HintItem;
if let Some(ref viewer) = self.block_viewer {
@@ -221,8 +220,7 @@ impl AgentView {
self.normal_pane_hints(registry)
}
}
/// Shared "normal pane" hints: flag computation + `build_hints` + queue hint.
/// Single source of truth for the two former duplicated blocks in
/// Shared "normal pane" hints. Single source of truth for
/// `current_shortcut_hints` and `draw`.
fn normal_pane_hints(&self, registry: &ActionRegistry) -> Vec<HintItem> {
let fold_label = self.selected_fold_label();
@@ -366,13 +364,6 @@ impl AgentView {
}
hints
}
/// Render the agent view into the given area.
///
/// Thin orchestrator: computes layout, then calls shared widgets and
/// agent-specific overlay helpers in sequence. Each component takes
/// only the state it needs — no arg threading.
///
/// Returns cursor position if the prompt is focused (for terminal cursor).
/// Render a fullscreen subagent view — replaces the ENTIRE parent view.
///
/// Draws:
@@ -137,10 +137,6 @@ impl AgentView {
}
}
/// Mouse handler for the rewind overlay. `Moved` moves the cursor
/// (`selected` for picker, `active_idx` for radio phases) and syncs
/// the scrollback preview on the picker. `Down(Left)` either
/// dispatches a synthesized key (radio) or `PickerSelect` (picker).
/// Mouse handler for the rewind overlay. `Moved` moves the cursor
/// to the row under the pointer; `Down(Left)` moves the cursor then
/// activates that row (Enter-equivalent). Geometry comes from
/// `rewind_row_at`, which mirrors `render_rewind_overlay`'s layout.
@@ -165,7 +165,6 @@ impl AgentView {
return false;
};
// Scroll the active scrollback.
let scrollback = if let Some(ref child_id) = self.active_subagent {
if let Some(child) = self.subagent_views.get_mut(child_id) {
&mut child.scrollback
@@ -826,7 +825,6 @@ impl AgentView {
}
}
// Find the content width from the resolved model's visible block geometry.
let content_width = self
.last_scrollback_selection_model
.visible_block_content_width(idx)
@@ -998,7 +996,7 @@ impl AgentView {
}
}
2 if is_prompt => {
// Edit in place; bash/cron keep the old fold behavior.
// Edit in place; bash/cron fall back to fold + scroll-to-top.
if !self.enter_inline_edit(idx) {
if foldable {
self.scrollback.toggle_fold_selected();
@@ -1679,9 +1677,7 @@ mod tests {
);
}
// -----------------------------------------------------------------------
// reclamp_drag_head_post_render tests
// -----------------------------------------------------------------------
fn mouse_down(col: u16, row: u16) -> MouseEvent {
MouseEvent {
@@ -1882,9 +1878,7 @@ mod tests {
assert_eq!(drag.head.block_line_idx, 3, "btw rebuild moves the head");
}
// -----------------------------------------------------------------------
// anchor_content_width snapshot tests
// -----------------------------------------------------------------------
/// The linear copy resolves the anchor entry's lines with the drag-start
/// width snapshot when the block is gone from `visible_blocks` (scrolled
@@ -1967,7 +1961,7 @@ mod tests {
/// Resolver miss at promotion (the anchor's range vanished from the
/// frame between press and threshold): the head collapses to the
/// anchor — the live successor of the deleted clamp-to-anchor helper.
/// anchor.
#[test]
fn promotion_miss_collapses_head_to_anchor() {
let mut agent = make_agent();
@@ -1987,8 +1981,7 @@ mod tests {
}
/// Resolver miss mid-drag (the range scrolled fully out): the head
/// keeps its previous position instead of jumping — the live successor
/// of the deleted keep-previous-head helper.
/// keeps its previous position instead of jumping.
#[test]
fn active_drag_motion_miss_keeps_previous_head() {
let mut agent = make_agent();
@@ -2131,9 +2124,7 @@ mod tests {
assert!(agent.reconstruct_drag_copy(&no_snapshot).is_none());
}
// -----------------------------------------------------------------------
// deferred text-press (anchor on entry into text) tests
// -----------------------------------------------------------------------
fn mouse_up(col: u16, row: u16) -> MouseEvent {
MouseEvent {
@@ -2686,9 +2677,7 @@ mod tests {
assert!(agent2.deferred_text_press.is_none());
}
// -----------------------------------------------------------------------
// drag-autoscroll bounce tests (tick + reclamp interplay)
// -----------------------------------------------------------------------
/// Agent over real scrollback content taller than its viewport (30
/// one-line messages through the real layout; pane rows 0-9, prompt at
@@ -53,9 +53,6 @@ impl AgentView {
.iter()
.any(|p| p == prompt_id)
}
/// Create a new agent view with default UI state.
///
/// The prompt widget is initialized with the session's working directory.
pub fn new(session: AgentSession, scrollback: ScrollbackState) -> Self {
let prompt = PromptWidget::new_with_cwd(&session.cwd);
let mut view = Self {
@@ -537,8 +534,8 @@ impl AgentView {
}
/// Effective turn elapsed time, excluding time spent in question views.
///
/// Subtracts both the accumulated `turn_paused_duration` (from previously
/// closed question views) and the time elapsed since the current question
/// Subtracts both the accumulated `turn_paused_duration` (from question
/// views closed earlier in the turn) and the time elapsed since the current question
/// view opened (if one is active).
pub fn turn_elapsed(&self) -> Option<std::time::Duration> {
let raw = self.turn_started_at?.elapsed();
@@ -863,8 +860,6 @@ mod resolve_turn_activity_tests {
Some(TurnActivity::AutoCompacting)
);
}
/// When waiting on task output, the spinner subject is the bg task's
/// description (preferred over the raw command).
#[test]
fn task_output_wait_uses_bg_task_description() {
use crate::acp::meta::NotificationMeta;
@@ -937,7 +932,6 @@ mod resolve_turn_activity_tests {
};
assert_eq!(reason.label(), "run release tests…");
}
/// Without a description, a short command is used as the subject.
#[test]
fn task_output_wait_falls_back_to_short_command() {
use crate::acp::meta::NotificationMeta;
@@ -991,7 +985,6 @@ mod resolve_turn_activity_tests {
};
assert_eq!(reason.label(), "sleep 30…");
}
/// Multi-id waits use full task_ids.len() for "+ N more", not just resolved count.
#[test]
fn task_output_wait_multi_id_uses_full_task_count() {
use crate::acp::meta::NotificationMeta;
@@ -1053,7 +1046,6 @@ mod resolve_turn_activity_tests {
"N more is based on full task_ids length, not resolved count"
);
}
/// Long first subjects still keep the multi-task suffix after clamping.
#[test]
fn task_output_wait_multi_id_preserves_suffix_when_first_is_long() {
use crate::acp::meta::NotificationMeta;
@@ -1198,7 +1190,6 @@ mod resolve_turn_activity_tests {
};
assert_eq!(reason.label(), "explore the auth module…");
}
/// Long bare commands are not used as subjects — keep the original label.
#[test]
fn task_output_wait_long_command_keeps_generic_label() {
use crate::acp::meta::NotificationMeta;
@@ -333,8 +333,6 @@ mod shell_suggestion_key_tests {
);
}
// -- always-on Tab fetch (no KIGI_SUGGESTIONS) --------------------------
/// Tab in bash mode with no fetched candidates fires a deterministic
/// fetch — no env flag, no AI, dropdown-scale limit.
#[test]
@@ -432,8 +430,6 @@ mod shell_suggestion_key_tests {
);
}
// -- terminal-like Tab (single-candidate accept / common-prefix fill) --
/// Exactly one token candidate: Tab accepts it immediately — no
/// dropdown flash — and the accept re-fetch keeps the pipeline alive.
#[test]
@@ -652,9 +648,9 @@ mod shell_suggestion_key_tests {
.count()
}
/// BugBot: a Fill whose range clips a paste chip used to no-op the
/// write and STILL kick a refetch — every Tab spun fill+refetch with no
/// draft change. The declined fill now degrades to opening the
/// BugBot: a Fill whose range clips a paste chip no-ops the write yet
/// would still kick a refetch — every Tab would spin fill+refetch with
/// no draft change. The declined fill degrades to opening the
/// dropdown: candidates visible, nothing fetched, chip intact, and the
/// second Tab rides the normal open-dropdown handling.
#[test]
@@ -705,11 +701,11 @@ mod shell_suggestion_key_tests {
}
/// BugBot sibling hole: the OPEN-dropdown accept (Tab/Enter/mouse all
/// share the helper) used to consume the candidates and close before
/// the write path declined the chip-clipping splice — leaving nothing.
/// The probe now makes it an honest no-op: nothing consumed, dropdown
/// up, chip/draft/generation untouched, no kick — and Enter must not
/// fall through to send.
/// share the helper) would consume the candidates and close before the
/// write path declined the chip-clipping splice — leaving nothing. The
/// probe makes it an honest no-op: nothing consumed, dropdown up,
/// chip/draft/generation untouched, no kick — and Enter must not fall
/// through to send.
#[test]
fn dropdown_accept_clipping_paste_chip_keeps_candidates() {
let (mut agent, text) = chip_agent(vec![
@@ -796,8 +792,6 @@ mod shell_suggestion_key_tests {
);
}
// -- Bash-mode gating of the as-you-type pipeline ------------------------
/// Typing in the normal (chat) prompt never fires the suggest pipeline;
/// the same keystroke in bash mode debounces a request.
#[test]
@@ -17,22 +17,18 @@ use ratatui::layout::Rect;
use ratatui::style::Style;
impl AgentView {
// ── Line viewer methods ────────────────────────────────────────────
/// Open the line viewer for a file path with optional initial line range.
pub(in crate::app) fn open_line_viewer(
&mut self,
path: &std::path::Path,
initial_range: Option<std::ops::Range<usize>>,
) {
// Resolve path relative to cwd.
let full_path = if path.is_relative() {
self.session.cwd.join(path)
} else {
path.to_path_buf()
};
// Get the element ID of the last file ref element (just created).
// The last file-ref element is the one the caller just created.
let element_id = self
.prompt
.textarea
@@ -43,7 +39,6 @@ impl AgentView {
.map(|e| e.id);
if let Some(mut viewer) = LineViewerState::open(&full_path, element_id) {
// If we have an initial line range, scroll to it and select.
if let Some(range) = initial_range {
viewer.set_initial_selection(range);
}
@@ -54,7 +49,6 @@ impl AgentView {
}
}
/// Handle a key event while the line viewer is open.
pub(super) fn handle_line_viewer_key(&mut self, key: &KeyEvent) -> InputOutcome {
let in_plan_approval = self.plan_approval_view.is_some();
@@ -110,7 +104,6 @@ impl AgentView {
return InputOutcome::Changed;
}
// Ctrl+F: toggle fullscreen.
if key.code == KeyCode::Char('f') && key.modifiers.contains(KeyModifiers::CONTROL) {
if let Some(ref mut viewer) = self.line_viewer {
viewer.fullscreen = !viewer.fullscreen;
@@ -185,7 +178,6 @@ impl AgentView {
self.confirm_line_viewer(false);
return InputOutcome::Changed;
}
// y: copy selected line(s) to system clipboard.
if key!('y').matches(key) {
if let Some(ref viewer) = self.line_viewer {
let text = if viewer.list_state.visual_mode {
@@ -219,7 +211,6 @@ impl AgentView {
}
return InputOutcome::Changed;
}
// Y: copy filename to clipboard.
if key!('Y').matches(key) {
if let Some(ref viewer) = self.line_viewer {
let name = viewer
@@ -258,7 +249,6 @@ impl AgentView {
self.cancel_line_viewer();
return InputOutcome::Changed;
}
// All other keys (including Ctrl-D/U for page nav): forward to ListPaneState.
if let Some(ref mut viewer) = self.line_viewer {
viewer.list_state.handle_key_event(key, &viewer.lines);
}
@@ -303,13 +293,11 @@ impl AgentView {
);
}
}
// Close the undo group.
self.prompt.textarea.insert_str(" ");
self.prompt.textarea.end_undo_group();
}
}
/// Cancel line viewer: revert all changes.
pub(crate) fn cancel_line_viewer(&mut self) {
self.line_viewer = None;
self.prompt.textarea.cancel_undo_group();
@@ -364,7 +352,6 @@ impl AgentView {
}
}
/// Handle mouse events while the line viewer is open.
pub(super) fn handle_line_viewer_mouse(
&mut self,
mouse: &crossterm::event::MouseEvent,
@@ -398,14 +385,12 @@ impl AgentView {
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
// Click on close button -> cancel.
if close_area.is_some_and(|a| a.contains((mouse.column, mouse.row).into())) {
if self.plan_approval_view.is_none() {
self.cancel_line_viewer();
}
return InputOutcome::Changed;
}
// Click on fullscreen button -> toggle fullscreen.
if fs_area.is_some_and(|a| a.contains((mouse.column, mouse.row).into())) {
if let Some(ref mut v) = self.line_viewer {
v.fullscreen = !v.fullscreen;
@@ -624,7 +609,6 @@ impl AgentView {
_ => return InputOutcome::Changed,
}
// Forward to ListPaneState if inside the popup area.
let mut should_enter_commenting = false;
let mut should_enter_plan_commenting = false;
if let Some(area) = popup_area
@@ -696,8 +680,6 @@ impl AgentView {
InputOutcome::Changed
}
// -- Scrollback selection box buttons -------------------------------------
/// Render ⧉ (copy) and ↗ (view) buttons on the scrollback selection box.
///
/// Two modes:
@@ -745,7 +727,6 @@ impl AgentView {
}
// Determine inline vs corner mode.
// Inline: entry is collapsed AND part of a group (group_range > 1).
let split_mode = self
.scrollback
.appearance()
@@ -763,7 +744,6 @@ impl AgentView {
let btn_base = Style::default().fg(theme.selection_border);
let btn_hover = Style::default().fg(theme.text_primary);
// Build button array based on capabilities.
if has_copy && has_view {
let (btn_right_x, y) = if inline {
// Inline: buttons on the selected entry's content row.
@@ -844,8 +824,6 @@ impl AgentView {
}
}
// -- Block viewer input handling ------------------------------------------
/// Handle a key event when the block viewer is open.
///
/// Returns `Changed` if consumed, `Unchanged` if the key should bubble up.
@@ -854,18 +832,15 @@ impl AgentView {
return InputOutcome::Unchanged;
};
// Check for close signals first (Esc/q/Ctrl-F)
if viewer.is_close_key(key) {
self.block_viewer = None;
return InputOutcome::Changed;
}
// Route to viewer — returns whether the key was consumed
if !viewer.handle_key(key) {
return InputOutcome::Unchanged;
}
// Handle raw toggle: capture old source map, toggle, rebuild with stability
if viewer.raw_toggle_pending {
viewer.raw_toggle_pending = false;
// Record scroll anchor BEFORE toggle so the selected line stays
@@ -883,7 +858,6 @@ impl AgentView {
)
})
});
// Toggle raw mode on the entry
if let Some(entry) = self.scrollback.get_by_id_mut(viewer.entry_id) {
entry.toggle_raw();
}
@@ -894,7 +868,6 @@ impl AgentView {
}
}
// Process pending copy actions (logic lives in BlockViewerPane)
let entry_id = viewer.entry_id;
if let Some(entry) = self.scrollback.get_by_id(entry_id)
&& let Some(text) = viewer.process_pending_copy(entry)
@@ -905,7 +878,6 @@ impl AgentView {
InputOutcome::Changed
}
/// Handle a mouse event when the block viewer modal is open.
pub(in crate::app) fn handle_block_viewer_mouse(
&mut self,
mouse: &crossterm::event::MouseEvent,
@@ -917,7 +889,6 @@ impl AgentView {
return InputOutcome::Changed;
};
// Route to modal chrome first (close button, click-outside).
let modal_outcome =
handle_modal_mouse(&mut viewer.modal, mouse.kind, mouse.column, mouse.row);
match modal_outcome {
@@ -929,7 +900,6 @@ impl AgentView {
_ => {}
}
// Content interaction (scroll, click, drag).
match mouse.kind {
MouseEventKind::ScrollDown => viewer.handle_scroll(3),
MouseEventKind::ScrollUp => viewer.handle_scroll(-3),
@@ -939,7 +909,6 @@ impl AgentView {
viewer.handle_mouse(mouse.kind, mouse.column, mouse.row);
}
MouseEventKind::Moved => {
// Update hover state for content area.
viewer.handle_mouse(mouse.kind, mouse.column, mouse.row);
}
_ => {}