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:
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);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Reference in new issue
Block a user