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
@@ -175,12 +175,9 @@ pub fn compute_layout_with_dispatch(
|
||||
} else {
|
||||
1
|
||||
};
|
||||
// Standalone peek rect retired. Peek now renders
|
||||
// INSIDE the dispatch rect (which grows when `peek_visible`,
|
||||
// computed above). Kept as a zero-height field for ABI compat
|
||||
// with the existing call sites that still destructure
|
||||
// `layout.peek`; the field can be removed in a follow-up
|
||||
// cleanup.
|
||||
// Peek renders INSIDE the dispatch rect (which grows when
|
||||
// `peek_visible`, computed above); this field stays zero-height so
|
||||
// call sites that still destructure `layout.peek` keep compiling.
|
||||
let peek_h: u16 = 0;
|
||||
let remaining = area.height.saturating_sub(
|
||||
top_margin_h
|
||||
@@ -235,7 +232,7 @@ pub fn compute_layout_with_dispatch(
|
||||
};
|
||||
y += header_gap_h;
|
||||
|
||||
// Polish — inset the list by LIST_OUTER_HPAD on each side so the
|
||||
// Inset the list by LIST_OUTER_HPAD on each side so the
|
||||
// row content and group header rules have side breathing room.
|
||||
// The outer columns stay painted bg_base by the area-wide fill in
|
||||
// render_dashboard. Mirrors the dispatch inset pattern but with a
|
||||
@@ -470,7 +467,7 @@ mod tests {
|
||||
assert_eq!(layout.list.width, area.width - LIST_OUTER_HPAD * 2);
|
||||
}
|
||||
|
||||
/// Polish — the list rect is inset by LIST_OUTER_HPAD cols on each
|
||||
/// The list rect is inset by LIST_OUTER_HPAD cols on each
|
||||
/// side so row content (markers, rules, text) has breathing room
|
||||
/// and doesn't touch the terminal edges. The outer columns remain
|
||||
/// bg_base (painted by the top-level area fill).
|
||||
@@ -674,9 +671,8 @@ mod tests {
|
||||
assert_eq!(layout.peek.height, 0);
|
||||
}
|
||||
|
||||
/// The standalone peek rect was retired (peek now
|
||||
/// renders INSIDE the dispatch box). The peek rect is always
|
||||
/// zero-height; what changes when `peek_visible == true` is
|
||||
/// Peek renders INSIDE the dispatch box, so the peek rect is
|
||||
/// always zero-height; what changes when `peek_visible == true` is
|
||||
/// the dispatch rect, which grows from 3 to 5 rows to host
|
||||
/// the peek's status + reply input.
|
||||
#[test]
|
||||
|
||||
@@ -1182,10 +1182,6 @@ pub fn extract_first_user_message(agent: &AgentView) -> Option<String> {
|
||||
/// message had MORE than `count` non-empty lines — so the renderer can
|
||||
/// show a `…` continuation marker only when there's genuinely more.
|
||||
/// Returns `(vec![], false)` when the agent hasn't streamed any text.
|
||||
///
|
||||
/// This is the multi-line successor to the old single-line
|
||||
/// `extract_last_agent_message`: the peek panel now surfaces up to 3
|
||||
/// lines of the last response instead of just the first.
|
||||
pub fn extract_last_agent_lines(agent: &AgentView, count: usize) -> (Vec<String>, bool) {
|
||||
use crate::scrollback::block::RenderBlock;
|
||||
use crate::views::session_title::sanitize_display_text;
|
||||
@@ -1232,10 +1228,9 @@ pub fn extract_last_agent_lines(agent: &AgentView, count: usize) -> (Vec<String>
|
||||
/// Extract the last `count` short text descriptions from the given
|
||||
/// agent view's scrollback.
|
||||
///
|
||||
/// No more `format!("{:?}", entry.block)`, which leaked
|
||||
/// Rust Debug output (variant tags, struct field names, escaped
|
||||
/// strings — and worst of all the head of bash commands containing
|
||||
/// credentials).
|
||||
/// Never project via `format!("{:?}", entry.block)`: that leaks Rust
|
||||
/// Debug output (variant tags, struct field names, escaped strings —
|
||||
/// and worst of all the head of bash commands containing credentials).
|
||||
///
|
||||
/// Every projected string is run through
|
||||
/// `strip_ansi_escapes::strip_str` so embedded `\x1b[...]` sequences
|
||||
@@ -2047,7 +2042,8 @@ mod tests {
|
||||
reject_option: None,
|
||||
},
|
||||
);
|
||||
panel.selected_option = Some(1); // highlight the 2nd option
|
||||
// Highlight the 2nd option.
|
||||
panel.selected_option = Some(1);
|
||||
let mut reply = test_reply();
|
||||
let _ = render_peek_panel(
|
||||
&mut buf,
|
||||
@@ -2098,7 +2094,8 @@ mod tests {
|
||||
},
|
||||
);
|
||||
panel.selected_option = Some(1);
|
||||
panel.focused = false; // Tab → row nav
|
||||
// Tab → row nav.
|
||||
panel.focused = false;
|
||||
let mut reply = test_reply();
|
||||
let _ = render_peek_panel(
|
||||
&mut buf,
|
||||
@@ -2152,7 +2149,8 @@ mod tests {
|
||||
reject_option: Some(1),
|
||||
},
|
||||
);
|
||||
panel.selected_option = Some(1); // highlight the reject option
|
||||
// Highlight the reject option.
|
||||
panel.selected_option = Some(1);
|
||||
let mut reply = test_reply();
|
||||
reply.set_text("do it differently");
|
||||
let res = render_peek_panel(
|
||||
@@ -2214,7 +2212,8 @@ mod tests {
|
||||
reject_option: Some(1),
|
||||
},
|
||||
);
|
||||
panel.selected_option = Some(1); // highlight the "Other" row
|
||||
// Highlight the "Other" row.
|
||||
panel.selected_option = Some(1);
|
||||
let mut reply = test_reply();
|
||||
let _ = render_peek_panel(
|
||||
&mut buf,
|
||||
|
||||
@@ -73,8 +73,6 @@ pub fn render_dashboard(
|
||||
// empty body reads "Loading sessions…" instead of the "no agents
|
||||
// yet" hint so a fresh open doesn't flash an empty-looking screen.
|
||||
dashboard_sessions_loading: bool,
|
||||
// `_compact` removed in this version. Hide-chrome / shortened
|
||||
// activity strings are a Phase 5 polish item.
|
||||
) -> Option<(u16, u16)> {
|
||||
// Re-anchor selection BEFORE we build the rows so that the
|
||||
// visible set drives selection clamping.
|
||||
@@ -468,14 +466,13 @@ fn rename_cursor_pos(state: &DashboardState, rows: &[DashboardRow]) -> Option<(u
|
||||
}
|
||||
|
||||
/// Render the compact dashboard "banner" used when an agent is
|
||||
/// attached as a popup. Replaces the previous
|
||||
/// stacked-input-bars layout (where the dashboard's dispatch +
|
||||
/// footer rendered visibly BELOW the popup). The banner is a
|
||||
/// bordered panel containing the row list summary; the popup
|
||||
/// renders directly below it carrying the focused agent's full
|
||||
/// view (scrollback + prompt + shortcuts), so the user sees a
|
||||
/// single coherent surface with the agent's prompt as the only
|
||||
/// input bar.
|
||||
/// attached as a popup, instead of the dashboard's normal dispatch +
|
||||
/// footer (which would otherwise render visibly BELOW the popup as a
|
||||
/// second stacked input bar). The banner is a bordered panel
|
||||
/// containing the row list summary; the popup renders directly below
|
||||
/// it carrying the focused agent's full view (scrollback + prompt +
|
||||
/// shortcuts), so the user sees a single coherent surface with the
|
||||
/// agent's prompt as the only input bar.
|
||||
///
|
||||
/// Visual:
|
||||
///
|
||||
@@ -595,7 +592,7 @@ fn render_dashboard_banner(
|
||||
/// inflate the tallies if counted directly. Chips with zero count are
|
||||
/// suppressed.
|
||||
///
|
||||
/// Reintroduces a `[+ New Agent]` button on the right edge of
|
||||
/// A `[+ New Agent]` button sits on the right edge of
|
||||
/// the header. The button is the default cursor target whenever
|
||||
/// no row is selected — Up-arrow from the first row, Esc deselect,
|
||||
/// and dashboard-open-without-prior-agent all land here. While
|
||||
@@ -799,8 +796,7 @@ fn render_header(
|
||||
// Paint the current location — git branch + cwd (with worktree
|
||||
// label) — on the left, mirroring the session surfaces (welcome
|
||||
// top bar / agent status bar) so the dashboard shows WHERE a
|
||||
// dispatched session will run. Replaces the old bare "Agents"
|
||||
// label.
|
||||
// dispatched session will run.
|
||||
//
|
||||
// Width budget: from `area.x` up to the leftmost chip's leading
|
||||
// ` │ ` separator (3 cells, painted by `AgentStatusBar::render`
|
||||
@@ -964,7 +960,8 @@ fn render_location_picker(
|
||||
} else {
|
||||
"[worktree:off]"
|
||||
};
|
||||
let wt_w = wt_text.len() as u16; // ASCII → byte len == display width
|
||||
// ASCII → byte len == display width.
|
||||
let wt_w = wt_text.len() as u16;
|
||||
const WT_GAP: u16 = 1;
|
||||
const MIN_PATH_W: u16 = 16;
|
||||
let (path_w, wt_rect) = if show_worktree && content_area.width >= wt_w + WT_GAP + MIN_PATH_W
|
||||
@@ -1127,11 +1124,11 @@ fn render_location_picker(
|
||||
/// One line in the dashboard's vertical stack — either a state-group
|
||||
/// header or a content row.
|
||||
///
|
||||
/// Reintroduces explicit state group headers (`──
|
||||
/// Needs input (2) ──`). The per-row dot + state colour alone didn't
|
||||
/// communicate group boundaries clearly enough; users couldn't tell at
|
||||
/// a glance how many sessions were awaiting input vs working vs idle
|
||||
/// vs done. Headers are emitted on every top-level state transition
|
||||
/// Explicit state group headers (`──
|
||||
/// Needs input (2) ──`) exist because the per-row dot + state colour
|
||||
/// alone doesn't communicate group boundaries clearly enough; users
|
||||
/// can't tell at a glance how many sessions are awaiting input vs
|
||||
/// working vs idle vs done. Headers are emitted on every top-level state transition
|
||||
/// (subagent rows inherit their parent's group and never trigger a
|
||||
/// header) when `grouping == Grouping::State` and the filter isn't
|
||||
/// already pinned to a single state.
|
||||
@@ -2260,8 +2257,8 @@ fn render_narrow_rows(
|
||||
let viewport_h = area.height as usize;
|
||||
// The clamp follows whichever cursor is active — a row OR a section
|
||||
// header — so navigating onto a section title scrolls it into view,
|
||||
// matching the wide layout. Previously only a selected row was
|
||||
// tracked, so a selected header could stay off-screen.
|
||||
// matching the wide layout. Tracking only a selected row would leave
|
||||
// a selected header stranded off-screen.
|
||||
let selected_line_idx = lines.iter().position(|l| match l {
|
||||
DashboardLine::Row(r) => state.selected.as_ref().is_some_and(|s| r.id == *s),
|
||||
DashboardLine::PinnedHeader { .. } => state.selected_section == Some(SectionKey::Pinned),
|
||||
@@ -2476,23 +2473,6 @@ fn render_empty_state(buf: &mut Buffer, area: Rect, theme: &Theme, loading: bool
|
||||
);
|
||||
}
|
||||
|
||||
/// Paint a rounded-box
|
||||
/// chrome around the dispatch input so it reads as a real input
|
||||
/// field. On a 3-row rect the layout is:
|
||||
///
|
||||
/// ```text
|
||||
/// ╭──────────────────────────────────────────────────────────╮
|
||||
/// │ ❯ Dispatch a new agent │
|
||||
/// ╰──────────────────────────────────────────────────────────╯
|
||||
/// ```
|
||||
///
|
||||
/// On a 1-row rect (very short terminals) we fall back to the
|
||||
/// bare `❯ {text}` line so the input stays usable.
|
||||
///
|
||||
/// `reply_label` flips the placeholder between `Dispatch a new
|
||||
/// agent` (`None`) and `Reply to {label}` (`Some`) so the
|
||||
/// chrome reflects what Enter will do: dispatch a new session vs.
|
||||
/// enqueue / send a prompt to the currently-selected agent.
|
||||
/// Paint a short right-aligned feedback badge onto the dispatch box's
|
||||
/// **top border** (e.g. `✗ Session no longer exists`, `✓ Theme: Kigi
|
||||
/// Day`), in a neutral accent colour. The message is painted VERBATIM:
|
||||
@@ -2500,7 +2480,7 @@ fn render_empty_state(buf: &mut Buffer, area: Rect, theme: &Theme, loading: bool
|
||||
/// [`DashboardState::set_error_toast`] (`✗`), while successes / info
|
||||
/// arrive from the `show_toast` builders (`✓` / `⚠`). The badge
|
||||
/// therefore neither prepends a glyph nor forces a colour; doing so
|
||||
/// previously produced a doubled `✗ ✓ …` and painted non-errors (like
|
||||
/// would produce a doubled `✗ ✓ …` and paint non-errors (like
|
||||
/// "Session closed") red. The glyph, not the colour, conveys severity —
|
||||
/// mirroring the per-agent toast in [`crate::app::agent_view`]. The
|
||||
/// badge ends one column before the right corner (`╮`) and is truncated
|
||||
@@ -2609,6 +2589,17 @@ fn paint_dispatch_config_badge(
|
||||
.render_info_line(buf, info_rect, &info, theme.bg_base, theme, input_focused);
|
||||
}
|
||||
|
||||
/// Paint a rounded-box chrome around the dispatch input so it reads as a
|
||||
/// real input field. On a 3-row rect the layout is:
|
||||
///
|
||||
/// ```text
|
||||
/// ╭──────────────────────────────────────────────────────────╮
|
||||
/// │ ❯ Dispatch a new agent │
|
||||
/// ╰──────────────────────────────────────────────────────────╯
|
||||
/// ```
|
||||
///
|
||||
/// On a 1-row rect (very short terminals) we fall back to the
|
||||
/// bare `❯ {text}` line so the input stays usable.
|
||||
fn render_dispatch(
|
||||
buf: &mut Buffer,
|
||||
area: Rect,
|
||||
@@ -3015,11 +3006,9 @@ fn render_file_search_dropdown_for(
|
||||
|
||||
/// Render the dashboard's footer / shortcuts hint row.
|
||||
///
|
||||
/// Switched to the shared `ShortcutsBar` widget so
|
||||
/// the dashboard's shortcut bar uses the same visual vocabulary as
|
||||
/// the agent view's bottom bar: `Key:label` with bold keys + dim
|
||||
/// ` │ ` separators on `bg_base`, instead of the previous custom
|
||||
/// `key label · key label` gray-only string. When a
|
||||
/// Built on the shared `ShortcutsBar` widget so the dashboard's shortcut
|
||||
/// bar uses the same visual vocabulary as the agent view's bottom bar:
|
||||
/// `Key:label` with bold keys + dim ` │ ` separators on `bg_base`. When a
|
||||
/// stop-confirm is armed, `ShortcutsBar::with_pending` paints the
|
||||
/// `press again to {label}` hint in place of the regular list,
|
||||
/// matching the agent view's identical mechanism.
|
||||
@@ -3233,8 +3222,8 @@ fn render_footer(
|
||||
registry.find(id).map(|d| d.default_key).unwrap_or(fallback)
|
||||
};
|
||||
let enter = key!(Enter);
|
||||
// "Send + open" is `Ctrl+S` (was `Shift+Enter`, which now inserts a
|
||||
// newline). Hardcoded in the dispatch / peek key handlers, not a
|
||||
// "Send + open" is fixed at `Ctrl+S` — Shift+Enter inserts a newline
|
||||
// instead. Hardcoded in the dispatch / peek key handlers, not a
|
||||
// registry action, so the chip is built directly.
|
||||
let send_open = key!('s', CONTROL);
|
||||
// Multiline: bare Enter inserts a newline; Shift+Enter (or Alt+Enter
|
||||
@@ -3528,38 +3517,17 @@ pub(crate) fn cached_home() -> Option<&'static str> {
|
||||
|
||||
static HOME: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Popup overlay (banner-style)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compute the rect for the attached-agent popup overlay.
|
||||
///
|
||||
/// The popup now takes the FULL bottom portion of
|
||||
/// the screen (no horizontal inset, no bottom inset) with only a
|
||||
/// dynamic top inset reserved for the dashboard banner.
|
||||
///
|
||||
/// The previous ~1/6-inset-on-all-sides design left the dashboard's
|
||||
/// dispatch input + footer visible BELOW the popup, producing two
|
||||
/// stacked input bars. The banner above the popup carries the live
|
||||
/// row list in a bordered panel.
|
||||
///
|
||||
/// (Historical legacy comment kept for context — the old layout
|
||||
/// description is no longer accurate; see the body of this function
|
||||
/// for the current banner-style layout.)
|
||||
/// terminal yields a ~164×48 popup with a ~36×12 dashboard frame
|
||||
/// visible around it.
|
||||
/// The popup takes the FULL bottom portion of the screen (no
|
||||
/// horizontal inset, no bottom inset) with only a dynamic top inset
|
||||
/// reserved for the dashboard banner, which carries the live row list
|
||||
/// in a bordered panel above the popup.
|
||||
///
|
||||
/// On terminals too small to honour the minimum inset, falls through
|
||||
/// to a 0-inset takeover (no escape: any non-zero inset would clip
|
||||
/// the agent's prompt below readability).
|
||||
pub fn popup_rect(view: Rect) -> Rect {
|
||||
// Popup takes the FULL bottom area (no
|
||||
// horizontal inset, no bottom inset) with only a small TOP
|
||||
// inset reserved for the dashboard banner that shows the in-flight
|
||||
// rows. The previous ~1/6-inset-on-all-sides design left the
|
||||
// dashboard's own dispatch input + footer visible BELOW the
|
||||
// popup, producing two visible input bars stacked vertically.
|
||||
//
|
||||
// The banner height is dynamic: ~30% of the screen up to a
|
||||
// BANNER_MAX_HEIGHT cap, with a BANNER_MIN_HEIGHT floor on tall
|
||||
// terminals so a 1-row banner doesn't crowd the rows out. Very
|
||||
@@ -4505,7 +4473,8 @@ mod tests {
|
||||
"x",
|
||||
Some((1, 2)),
|
||||
false,
|
||||
true, // hover_next
|
||||
// hover_next
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.expect("overlay must paint");
|
||||
@@ -4886,8 +4855,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── snap_offset_to_line_boundary unit tests ──────────────────────
|
||||
|
||||
/// An offset already on a boundary is returned unchanged.
|
||||
#[test]
|
||||
fn snap_offset_already_on_boundary_returns_input() {
|
||||
@@ -4948,10 +4915,7 @@ mod tests {
|
||||
|
||||
/// `popup_rect` takes the FULL bottom area
|
||||
/// (no horizontal inset, no bottom inset) with only a top inset
|
||||
/// reserved for the dashboard banner. Replaces the previous
|
||||
/// centred-inset design which left the dashboard's own dispatch
|
||||
/// input + footer visible below the popup, producing two
|
||||
/// stacked input bars.
|
||||
/// reserved for the dashboard banner.
|
||||
#[test]
|
||||
fn popup_rect_takes_full_bottom_area_with_top_banner() {
|
||||
let view = Rect::new(0, 0, 200, 80);
|
||||
@@ -5015,8 +4979,7 @@ mod tests {
|
||||
assert_eq!(popup.y, view.y);
|
||||
}
|
||||
|
||||
/// Replacing the home-rolled chrome
|
||||
/// with `picker::render_bordered_frame` means the divider sits
|
||||
/// `picker::render_bordered_frame` places the divider
|
||||
/// ABOVE the returned content rect. This test paints the chrome
|
||||
/// plus a "fake agent" pattern in the inner rect and verifies
|
||||
/// the divider's `─` glyph survives the inner paint.
|
||||
@@ -5209,10 +5172,9 @@ mod tests {
|
||||
|
||||
/// Pressing the help key returns the
|
||||
/// `DashboardOpenShortcutsHelp` action so the dispatcher can
|
||||
/// build the modal state. No `error_toast` is set (the
|
||||
/// an earlier polish iteration surfaced a hint via the dispatch
|
||||
/// input placeholder, which the user explicitly rejected
|
||||
/// because it conflicted with their typing slot).
|
||||
/// build the modal state. No `error_toast` is set — the modal
|
||||
/// itself carries the help; surfacing it via the dispatch input
|
||||
/// placeholder would conflict with the input's typing slot.
|
||||
#[test]
|
||||
fn dashboard_shortcuts_help_action_opens_modal() {
|
||||
use super::super::state::DashboardState;
|
||||
@@ -6047,7 +6009,8 @@ mod tests {
|
||||
fn render_rows_groups_off_uses_divider_not_pinned_header() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 30));
|
||||
let mut state = DashboardState::new();
|
||||
state.grouping = Grouping::Directory; // groups off (Ctrl+G)
|
||||
// groups off (Ctrl+G)
|
||||
state.grouping = Grouping::Directory;
|
||||
let mut pinned = header_test_row(1, RowState::Idle, "pinned row");
|
||||
pinned.pinned = true;
|
||||
let rows = vec![pinned, header_test_row(2, RowState::Working, "working row")];
|
||||
@@ -6085,11 +6048,9 @@ mod tests {
|
||||
/// Idle → Completed → Failed order (matching
|
||||
/// `RowState::group_priority`).
|
||||
///
|
||||
/// Header chrome now uses Option A
|
||||
/// (` ● Label (N)`): a 2-col indent, a state-coloured dot, then
|
||||
/// the label + count in `gray_dim`. The previous full-row
|
||||
/// `── Label (N) ────────────────` chrome was dropped (the
|
||||
/// trailing dashes felt visually obnoxious — user complaint).
|
||||
/// Regression guard: header chrome is `Label N ──…` (bold label,
|
||||
/// dim count, trailing rule), NOT the parenthesised
|
||||
/// `Awaiting (1)` form.
|
||||
#[test]
|
||||
fn render_rows_emits_group_headers_in_state_order() {
|
||||
// Rows are 3 cells tall, headers 2 cells; 5 of each
|
||||
@@ -6231,7 +6192,8 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 100, 2));
|
||||
let theme = Theme::current();
|
||||
let mut state = DashboardState::new();
|
||||
state.spinner_tick = 8; // → dot_spinner_frames()[2] = `⸬`.
|
||||
// → dot_spinner_frames()[2] = `⸬`.
|
||||
state.spinner_tick = 8;
|
||||
let row = DashboardRow {
|
||||
id: DashboardRowId::TopLevel(crate::app::agent::AgentId(1)),
|
||||
label: "who are you?".to_string(),
|
||||
@@ -6564,10 +6526,6 @@ mod tests {
|
||||
|
||||
/// `Grouping::Directory` keeps cwd as the
|
||||
/// grouping primitive, so state headers are suppressed.
|
||||
///
|
||||
/// Header chrome marker updated to match Option
|
||||
/// A. The `(count)` parenthesis pattern is the new specific
|
||||
/// fingerprint for a state header.
|
||||
#[test]
|
||||
fn render_rows_skips_headers_when_grouping_is_directory() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 10));
|
||||
@@ -6597,9 +6555,6 @@ mod tests {
|
||||
|
||||
/// `Filter::State(_)` collapses the view to a
|
||||
/// single state, so the header would be redundant chrome.
|
||||
///
|
||||
/// Header chrome marker updated to match Option
|
||||
/// A (look for `Working (` instead of `── Working`).
|
||||
#[test]
|
||||
fn render_rows_skips_headers_when_filter_is_state() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 80, 10));
|
||||
@@ -6833,10 +6788,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Header redesign tests
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Basename of the test process's cwd — the one deterministic
|
||||
/// fragment of the header's location label. The full label depends
|
||||
/// on global git caches (`git_info::*`) that parallel tests may
|
||||
@@ -6962,11 +6913,12 @@ mod tests {
|
||||
fn underline_location_on_hover_excludes_branch_icon() {
|
||||
let icon = "\u{e0a0}";
|
||||
let plain = Style::default();
|
||||
// leading inset, git (icon + branch), git↔path separator, path.
|
||||
let spans = vec![
|
||||
Span::styled(" ".to_string(), plain), // leading inset
|
||||
Span::styled(format!("{icon} main"), plain), // git: icon + branch
|
||||
Span::styled(" ".to_string(), plain), // git↔path separator
|
||||
Span::styled("/home/me/repo".to_string(), plain), // path
|
||||
Span::styled(" ".to_string(), plain),
|
||||
Span::styled(format!("{icon} main"), plain),
|
||||
Span::styled(" ".to_string(), plain),
|
||||
Span::styled("/home/me/repo".to_string(), plain),
|
||||
];
|
||||
let out = underline_location_on_hover(spans, icon);
|
||||
|
||||
@@ -7495,7 +7447,8 @@ mod tests {
|
||||
&state,
|
||||
®istry,
|
||||
None,
|
||||
true, // peek_active
|
||||
// peek_active
|
||||
true,
|
||||
None,
|
||||
);
|
||||
let content = buf_to_text(&buf);
|
||||
@@ -7553,7 +7506,8 @@ mod tests {
|
||||
&state,
|
||||
®istry,
|
||||
None,
|
||||
true, // peek_active
|
||||
// peek_active
|
||||
true,
|
||||
None,
|
||||
);
|
||||
let content = buf_to_text(&buf);
|
||||
@@ -7570,7 +7524,8 @@ mod tests {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 200, 1));
|
||||
let theme = Theme::current();
|
||||
let mut state = DashboardState::new();
|
||||
state.list_focused = true; // used to steal the footer before the peek fix
|
||||
// Regression guard: this used to steal the footer before the peek fix.
|
||||
state.list_focused = true;
|
||||
state.peek = Some(crate::views::dashboard::peek::PeekPanelState::new(
|
||||
DashboardRowId::TopLevel(crate::app::agent::AgentId(0)),
|
||||
crate::views::dashboard::peek::PeekFields {
|
||||
@@ -7706,7 +7661,8 @@ mod tests {
|
||||
state,
|
||||
®istry,
|
||||
None,
|
||||
true, // peek_active
|
||||
// peek_active
|
||||
true,
|
||||
None,
|
||||
);
|
||||
buf_to_text(&buf)
|
||||
@@ -7792,10 +7748,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// When a row (NeedsInput or otherwise) is selected
|
||||
/// with an empty prompt, the footer shows `Enter:open`.
|
||||
/// The previous `see details` label is folded into the
|
||||
/// unified "row selected → open" semantics — every row's
|
||||
/// detail view is the answer surface for any user-input
|
||||
/// with an empty prompt, the footer shows `Enter:open` — every
|
||||
/// row's detail view is the answer surface for any user-input
|
||||
/// state, including `NeedsInput`.
|
||||
#[test]
|
||||
fn render_footer_row_selected_empty_prompt_shows_enter_open() {
|
||||
|
||||
@@ -1258,10 +1258,9 @@ mod tests {
|
||||
assert_eq!(rows[1].state, RowState::Working);
|
||||
assert_eq!(rows[2].state, RowState::Idle);
|
||||
}
|
||||
/// Renamed from `sort_deterministic_with_equal_keys`.
|
||||
/// The original name implied a tiebreak guarantee that
|
||||
/// `sort_cluster_key` does NOT provide; documents the actual
|
||||
/// behavioural contract: idempotent on identical inputs.
|
||||
/// `sort_cluster_key` does not guarantee a full tiebreak by itself;
|
||||
/// this asserts the weaker contract it does provide: idempotent on
|
||||
/// identical inputs.
|
||||
#[test]
|
||||
fn sort_is_idempotent_for_identical_inputs() {
|
||||
let now = SystemTime::now();
|
||||
@@ -1737,9 +1736,8 @@ mod tests {
|
||||
AgentView::new(session, ScrollbackState::new())
|
||||
}
|
||||
/// An idle local agent with no last message has a BLANK second line —
|
||||
/// the model is no longer used as a fallback there (it now shows in
|
||||
/// the peek panel's bottom-border badge for the selected row, keeping
|
||||
/// the list uncluttered).
|
||||
/// the model appears in the peek panel's bottom-border badge for the
|
||||
/// selected row instead, keeping the list uncluttered.
|
||||
#[test]
|
||||
fn idle_local_agent_without_message_has_blank_secondary() {
|
||||
let agent = make_idle_agent_with_model(Some("kigi-4.5"));
|
||||
|
||||
@@ -1498,27 +1498,18 @@ impl DashboardState {
|
||||
/// agent/subagent no longer exists is silently dropped. Avoids edge
|
||||
/// case 20 (a pinned row whose agent was deleted).
|
||||
///
|
||||
/// Also clears an in-flight `rename` whose row
|
||||
/// disappeared (parent closed, subagent finished, etc.), so a
|
||||
/// Commit-Enter doesn't silently drop the draft on a phantom row.
|
||||
///
|
||||
/// Extends the gc to `peek`, `hovered_row`, and
|
||||
/// `last_click`. The previous version only cleared `pinned`,
|
||||
/// `reorder`, `rename`, and `selected`. A stale `peek` would
|
||||
/// render cached content for a dead row; a stale `last_click`
|
||||
/// could trigger a double-click "attach" against whatever new
|
||||
/// row took the cell.
|
||||
///
|
||||
/// Drop the "Row no longer exists; rename
|
||||
/// cancelled" toast on stale rename. The toast fired on every
|
||||
/// dashboard open if a row was closed externally, surprising the
|
||||
/// user (they hadn't done anything since). The clear itself is
|
||||
/// preserved — silently clearing the rename matches the silent
|
||||
/// gc on `pinned` / `reorder` / `selected` / `peek` /
|
||||
/// `hovered_row` / `last_click`. the earlier invariant ("Commit
|
||||
/// Enter can't dispatch against a phantom row") is preserved
|
||||
/// because the renamed row can't render the overlay if it's
|
||||
/// gone, so Enter can't reach the commit path.
|
||||
/// Also silently clears `rename`, `peek`, `hovered_row`, and
|
||||
/// `last_click` when their row disappeared (parent closed, subagent
|
||||
/// finished, etc.):
|
||||
/// - A stale `rename` is cleared without a "Row no longer exists;
|
||||
/// rename cancelled" toast, so a row closed elsewhere doesn't
|
||||
/// surprise the user with a toast on the next dashboard open.
|
||||
/// Commit-Enter still can't dispatch against a phantom row, because
|
||||
/// the renamed row can't render the overlay once it's gone.
|
||||
/// - A stale `peek` would otherwise render cached content for a dead
|
||||
/// row.
|
||||
/// - A stale `last_click` could trigger a double-click "attach"
|
||||
/// against whatever new row took the cell.
|
||||
pub fn gc_stale_refs(&mut self, alive: &dyn Fn(&DashboardRowId) -> bool) {
|
||||
self.pinned.retain(|id| alive(id));
|
||||
self.reorder.retain(|id| alive(id));
|
||||
@@ -2516,7 +2507,7 @@ impl DashboardState {
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.reject_option == selected);
|
||||
match (key.code, selected) {
|
||||
// ── No option selected → navigate agents / open ──
|
||||
// No option selected: navigate agents / open.
|
||||
(KeyCode::Up, None) => {
|
||||
return Some(InputOutcome::Action(Action::DashboardSelectPrev));
|
||||
}
|
||||
@@ -2542,7 +2533,7 @@ impl DashboardState {
|
||||
.unwrap_or(InputOutcome::Unchanged),
|
||||
);
|
||||
}
|
||||
// ── Option selected → move within options (spill at edges) ──
|
||||
// Option selected: move within options (spill at edges).
|
||||
(KeyCode::Up, Some(0)) => {
|
||||
return Some(InputOutcome::Action(Action::DashboardSelectPrev));
|
||||
}
|
||||
@@ -2882,11 +2873,11 @@ impl DashboardState {
|
||||
// (the early return at the top of this function), so by the time
|
||||
// execution reaches here the peek panel is guaranteed closed.
|
||||
|
||||
// ── Ctrl+V / Cmd+V paste ────────────────────────────────────────
|
||||
// Read the pbpaste text once and route through the shared deferred
|
||||
// paste pipeline: a file path wins synchronously, else the clipboard
|
||||
// image/file-url probe defers off the event loop. Mirrors `AgentView`
|
||||
// — without this, Ctrl+V on the dashboard did nothing useful.
|
||||
// Ctrl+V / Cmd+V paste: read the pbpaste text once and route through
|
||||
// the shared deferred paste pipeline: a file path wins synchronously,
|
||||
// else the clipboard image/file-url probe defers off the event loop.
|
||||
// Mirrors `AgentView` — without this, Ctrl+V on the dashboard did
|
||||
// nothing useful.
|
||||
if crate::input::key::is_paste_key(key) {
|
||||
let clipboard_text = crate::app::actions::ClipboardTextRead::from_result(
|
||||
crate::clipboard::system_clipboard_read_text(),
|
||||
@@ -2894,11 +2885,10 @@ impl DashboardState {
|
||||
return self.handle_paste_key_deferred(clipboard_text, /* peek */ false);
|
||||
}
|
||||
|
||||
// ── @-file-search intercept ─────────────────────────────────────
|
||||
// The dispatch input offers a session-less `@` context picker
|
||||
// rooted at the pager's launch cwd. While its dropdown is
|
||||
// visible, the prompt widget owns Up/Down/Tab/Enter/Esc — route
|
||||
// the key there BEFORE the dashboard's row-nav / Enter / Esc
|
||||
// @-file-search intercept: the dispatch input offers a session-less
|
||||
// `@` context picker rooted at the pager's launch cwd. While its
|
||||
// dropdown is visible, the prompt widget owns Up/Down/Tab/Enter/Esc
|
||||
// — route the key there BEFORE the dashboard's row-nav / Enter / Esc
|
||||
// handlers, mirroring `agent_view::handle_prompt_key`.
|
||||
if self.dispatch.file_search_visible() {
|
||||
match self.dispatch.handle_key(key) {
|
||||
@@ -4112,9 +4102,8 @@ impl DashboardState {
|
||||
if let Some(sel) = self.selected.as_ref()
|
||||
&& !selectable.iter().any(|r| r.id == *sel)
|
||||
{
|
||||
// The previously selected row was filtered out / closed
|
||||
// / lost its parent. Drop the cursor — re-selecting is
|
||||
// the user's job.
|
||||
// The selected row was filtered out / closed / lost its
|
||||
// parent. Drop the cursor — re-selecting is the user's job.
|
||||
self.selected = None;
|
||||
}
|
||||
}
|
||||
@@ -4292,10 +4281,6 @@ fn handle_rename_key(draft: &mut RenameDraft, key: &KeyEvent) -> InputOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter parser (edge case 11)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Parse a filter expression from the dispatch input.
|
||||
///
|
||||
/// Rules per edge case 11:
|
||||
@@ -4367,10 +4352,6 @@ pub fn parse_row_state_token(s: &str) -> Option<RowState> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persistence I/O
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Read the persisted `[dashboard].enabled` flag (defaults to `true`).
|
||||
///
|
||||
/// Lenient: any error or unparseable value returns `None`, which the
|
||||
@@ -4583,10 +4564,6 @@ fn parse_persist_key_list(item: &toml_edit::Item) -> Vec<PersistedRowId> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: relative path display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Compact a `Path` for display against `$HOME`, returning a `String`.
|
||||
///
|
||||
/// Used by the row renderer + filter substring search to keep cwd
|
||||
@@ -5333,10 +5310,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// handle_key tests (Esc cascade, Enter routing).
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
fn make_state_with_selection() -> DashboardState {
|
||||
let mut s = DashboardState::new();
|
||||
s.selected = Some(DashboardRowId::TopLevel(AgentId(0)));
|
||||
@@ -5724,7 +5697,8 @@ mod tests {
|
||||
p.options = vec![("a".into(), "A".into()), ("other".into(), "Other".into())];
|
||||
p.reject_option = Some(1);
|
||||
p.selected_option = Some(1);
|
||||
p.request_id = None; // Ask tool (not a permission)
|
||||
// Ask tool (not a permission).
|
||||
p.request_id = None;
|
||||
}
|
||||
let reg = crate::actions::ActionRegistry::defaults();
|
||||
let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
|
||||
@@ -5910,7 +5884,8 @@ mod tests {
|
||||
let mut state = state_with_open_peek();
|
||||
let reg = crate::actions::ActionRegistry::defaults();
|
||||
state.peek_reply.set_text("a draft");
|
||||
state.peek.as_mut().unwrap().focused = false; // Tab → row nav
|
||||
// Tab → row nav.
|
||||
state.peek.as_mut().unwrap().focused = false;
|
||||
assert!(matches!(
|
||||
state.handle_key(&KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), ®),
|
||||
InputOutcome::Action(Action::DashboardSelectNext)
|
||||
@@ -6388,7 +6363,8 @@ mod tests {
|
||||
("__other__".into(), "Other".into()),
|
||||
];
|
||||
f.reject_option = Some(2);
|
||||
f.request_id = None; // ← marks it as an ask question, not a permission
|
||||
// Marks it as an ask question, not a permission.
|
||||
f.request_id = None;
|
||||
state.peek = Some(super::super::peek::PeekPanelState::new(
|
||||
DashboardRowId::TopLevel(AgentId(0)),
|
||||
f,
|
||||
@@ -7010,12 +6986,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Reconciled dispatch-input features: slash commands, Alt+Enter
|
||||
// multiline, vim-gated j/k, and paste — layered on top of the
|
||||
// reply-mode / search-mode base.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// A `/command` Enter routes through the session-less slash
|
||||
/// dispatcher instead of becoming a new session's prompt.
|
||||
#[test]
|
||||
@@ -7421,7 +7391,8 @@ mod tests {
|
||||
let mut state = DashboardState::new();
|
||||
assert!(state.new_agent_button_focused);
|
||||
state.dispatch.set_text("fix the bug");
|
||||
state.list_focused = true; // e.g. after an Esc blur
|
||||
// e.g. after an Esc blur.
|
||||
state.list_focused = true;
|
||||
match state.handle_key(&key, ®) {
|
||||
InputOutcome::Action(Action::DashboardDispatch { text, attach }) => {
|
||||
assert_eq!(text, "fix the bug");
|
||||
@@ -7808,7 +7779,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// The clipboard raster/file-url probe (osascript) + image decode +
|
||||
// session persist run OFF the event loop. A paste that would probe
|
||||
// enqueues a `ProbeClipboardAttachment` effect and returns without an
|
||||
@@ -7816,7 +7786,6 @@ mod tests {
|
||||
// attaches later via `complete_clipboard_attachment_paste`. Snapshot /
|
||||
// support are faked via the test-only seam; plain text with no
|
||||
// raster stays fully synchronous (no defer).
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
fn probe_image_data() -> crate::clipboard::ImageData {
|
||||
crate::clipboard::ImageData {
|
||||
@@ -8252,7 +8221,8 @@ mod tests {
|
||||
/// instead of inserting into the now-hidden reply buffer.
|
||||
#[test]
|
||||
fn completion_peek_dropped_when_panel_closed() {
|
||||
let mut state = DashboardState::new(); // no peek open
|
||||
// No peek open.
|
||||
let mut state = DashboardState::new();
|
||||
state.paste_probe_in_flight = 1;
|
||||
let completion = state.complete_clipboard_attachment_paste(
|
||||
completion_ctx(None, true),
|
||||
@@ -8279,7 +8249,8 @@ mod tests {
|
||||
/// instead of replying to the newly peeked agent.
|
||||
#[test]
|
||||
fn completion_peek_dropped_when_row_changed() {
|
||||
let mut state = state_with_open_peek(); // peeks TopLevel(AgentId(0))
|
||||
// Peeks TopLevel(AgentId(0)).
|
||||
let mut state = state_with_open_peek();
|
||||
state.paste_probe_in_flight = 1;
|
||||
state.deferred_peek_send = Some(DeferredPeekSend {
|
||||
row: DashboardRowId::TopLevel(AgentId(0)),
|
||||
@@ -8418,7 +8389,8 @@ mod tests {
|
||||
/// draft stays in the widget.
|
||||
#[test]
|
||||
fn stashed_peek_reply_dropped_when_question_active() {
|
||||
let mut state = state_with_open_peek(); // peeks TopLevel(AgentId(0))
|
||||
// Peeks TopLevel(AgentId(0)).
|
||||
let mut state = state_with_open_peek();
|
||||
state.peek_reply.set_text("please look");
|
||||
state.deferred_peek_send = Some(DeferredPeekSend {
|
||||
row: DashboardRowId::TopLevel(AgentId(0)),
|
||||
@@ -8444,12 +8416,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// `/` literal + `Ctrl+/` search mode (replaces the old `/`→filter
|
||||
// behaviour that silently swallowed prompts starting with a
|
||||
// filter prefix).
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// `/` types a literal slash into the prompt — it no longer
|
||||
/// enters a filter mode. (Filtering moved to `Ctrl+/`.)
|
||||
#[test]
|
||||
@@ -8690,7 +8656,8 @@ mod tests {
|
||||
let click = MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 10,
|
||||
row: 5, // inside BOTH dropdown and row_rects
|
||||
// Inside BOTH dropdown and row_rects.
|
||||
row: 5,
|
||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||
};
|
||||
let outcome = state.handle_mouse(&click);
|
||||
@@ -9171,7 +9138,8 @@ mod tests {
|
||||
]),
|
||||
mode: crate::views::shortcuts_help::ShortcutsHelpMode::Browse,
|
||||
});
|
||||
modal.state.selected = 1; // first registry-backed hint
|
||||
// First registry-backed hint.
|
||||
modal.state.selected = 1;
|
||||
state.shortcuts_modal = Some(modal);
|
||||
|
||||
let snapshot = |s: &DashboardState| {
|
||||
@@ -9765,10 +9733,6 @@ mod tests {
|
||||
assert_eq!(s.viewport_offset, 5);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Mouse wheel decoupled from selection
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// `handle_scroll` flags `manual_scroll_active` so the next
|
||||
/// `clamp_viewport` knows to skip the snap-to-selection
|
||||
/// pull-back.
|
||||
@@ -9886,8 +9850,6 @@ mod tests {
|
||||
unsafe { std::env::remove_var("KIGI_AGENT_DASHBOARD") };
|
||||
}
|
||||
|
||||
// ── Location picker ─────────────────────────────────────────────
|
||||
|
||||
fn location_candidate(path: &str, label: &str) -> LocationCandidate {
|
||||
LocationCandidate {
|
||||
path: PathBuf::from(path),
|
||||
@@ -10046,8 +10008,10 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn location_path_completion_tags_symlinked_worktree() {
|
||||
let real = tempfile::tempdir().unwrap(); // the real worktree target
|
||||
let parent = tempfile::tempdir().unwrap(); // the dir we list
|
||||
// The real worktree target.
|
||||
let real = tempfile::tempdir().unwrap();
|
||||
// The dir we list.
|
||||
let parent = tempfile::tempdir().unwrap();
|
||||
std::os::unix::fs::symlink(real.path(), parent.path().join("link")).unwrap();
|
||||
|
||||
// Index keyed by the real (canonical) path, as the worktree DB is.
|
||||
|
||||
Reference in new issue
Block a user