refactor(tui): remove SidebarFocus and the classic sidebar's last state

Spec step 4 (part 2) of rail-unification-task-20260802 (checklist
section 2A). The type and every field it hung off of are gone; nothing
in the tree can mutate or gate on a panel system that no longer exists.

- app.rs: SidebarFocus the enum, its from_setting/as_setting impls,
  App.sidebar_focus, App.sidebar_width_percent, set_sidebar_focus, and
  the whole classic interaction block (sidebar_resizing/_hovered,
  resize anchors, last_sidebar_area/_host_width/_handle_area,
  sidebar_resize_total_width), plus ViewportState.last_sidebar_area.
- app/init.rs: the settings reads and field initializers for all of the
  above.
- subagent_routing: agents_sidebar_surface_visible now means what it
  says in the rail world (panel == Agents && last_area);
  agents_panel_has_content (auto-mode remnant) deleted.
- sidebar.rs: the five explicit_tasks_focus gates re-point at
  work_surface.panel == RailPanel::Tasks; orphaned ratatui/theme imports
  dropped.
- mouse_ui.rs: approval wheel ownership and the context-menu on_sidebar
  gate re-point at work_surface.last_area (rail rows keep their
  right-click Run/Copy menu via the shared hover sections);
  sidebar_click_action was already section-driven and is untouched; the
  scroll-leak consume block for the never-Some legacy rect is deleted.
- settings.rs: sidebar_width_percent and sidebar_focus become load-only
  migration shims (#[serde(skip_serializing)] — read by
  migrate_sidebar_settings_to_rail, never written back);
  update_sidebar_width deleted.
- ui.rs: the per-frame reset of the deleted fields is gone (the live
  sidebar_hover_tooltip clear stays).

Tests: sidebar.rs setups (~30) and ui/app tests re-point at
work_surface.panel/placement/last_area; entering_operate_preserves_
user_rail_panel replaces the sidebar_focus variant; the approval-wheel
test drops the legacy rect (rail ownership covered by the work_surface
arm); the two loading-hover tests keep their assertions against the
generic tooltip-clear path; context-menu tests drive the rail's
work_surface.last_area; sidebar_focus from_setting coverage lives on in
the settings migration tests.

Gates: cargo fmt --check -p codewhale-tui clean;
sidebar: 129 passed; mouse: 70; hotbar: 92; footer: 104; settings: 121;
commands: 760; work_surface: 64 — all 0 failed.
This commit is contained in:
Hmbown committed 2026-08-03 06:46:02 -07:00
1 parent 0f50ad834d
commit 70759e1c14
11 files changed
+89 -254

No files matched your search

+7 -8
View File
@@ -387,9 +387,14 @@ pub struct Settings {
/// Default mode: "agent" (Act), "plan", or "operate". Legacy permission
/// shorthands are accepted for migration but never advertised as modes.
pub default_mode: String,
/// Sidebar width as percentage of terminal width
/// Legacy sidebar width as percentage of terminal width. Load-only
/// migration shim (0.9.4 rail unification): read by
/// `migrate_sidebar_settings_to_rail`, never written back.
#[serde(skip_serializing)]
pub sidebar_width_percent: u16,
/// Sidebar focus mode: pinned, auto, tasks, agents, context, hidden
/// Legacy sidebar focus mode: pinned, auto, tasks, agents, context,
/// hidden. Load-only migration shim, never written back.
#[serde(skip_serializing)]
pub sidebar_focus: String,
/// Enable the session-context panel (#504). Shows working set, tokens,
/// cost, MCP/LSP status, cycle count, and memory info.
@@ -1128,12 +1133,6 @@ impl Settings {
atomically_replace_settings_file(path, body.as_bytes())
}
/// Update and persist sidebar width percentage (10-50) — used by the
/// drag-to-resize handle in the TUI.
pub fn update_sidebar_width(&mut self, percent: u16) {
self.sidebar_width_percent = percent.clamp(10, 50);
}
/// Set a single setting by key
pub fn set(&mut self, key: &str, value: &str) -> Result<()> {
match key {
-76
View File
@@ -292,21 +292,6 @@ pub struct TurnCacheRecord {
pub recorded_at: Instant,
}
/// Sidebar content focus mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SidebarFocus {
Auto,
Pinned,
Tasks,
Agents,
Context,
/// Dedicated Sessions browser panel (#2934). Renders the persistent
/// sessions rail as the sole sidebar content, regardless of whether the
/// `sessions_rail` setting is enabled. Focuses session navigation.
Sessions,
Hidden,
}
/// Browsing context captured when the `/model` picker is dismissed (#4109).
/// Plain data so `App` does not depend on the picker's internal view enum.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -482,36 +467,6 @@ impl Default for LspRepairState {
}
}
impl SidebarFocus {
#[must_use]
pub fn from_setting(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"pinned" | "visible" | "show" | "on" | "work" | "plan" | "todos" => Self::Pinned,
// Persist/compat key remains "tasks"; user-facing panel is Activity (#4147/#4135).
"tasks" | "activity" | "live" | "running" => Self::Tasks,
"agents" | "subagents" | "sub-agents" => Self::Agents,
"context" => Self::Context,
"sessions" | "sessions_rail" | "session_history" => Self::Sessions,
"hidden" | "hide" | "closed" | "off" | "none" => Self::Hidden,
_ => Self::Auto,
}
}
#[must_use]
#[allow(dead_code)]
pub fn as_setting(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Pinned => "pinned",
Self::Tasks => "tasks",
Self::Agents => "agents",
Self::Context => "context",
Self::Sessions => "sessions",
Self::Hidden => "hidden",
}
}
}
/// Pre-session launch menu state for the underwater shell.
///
/// This is deliberately separate from onboarding and from the post-launch
@@ -685,10 +640,6 @@ pub struct ViewportState {
/// Painted band occupied by the active inline approval. Stored so wheel
/// routing can prefer the visible card over side surfaces underneath it.
pub last_approval_area: Option<Rect>,
/// Outer rect of the right-hand sidebar (when visible), stored at render
/// time so mouse hit-testing can keep scroll events over the sidebar from
/// leaking into the transcript viewport.
pub last_sidebar_area: Option<Rect>,
/// WorkflowPanel rect above the composer (#4121), for mouse toggle/cancel.
pub last_workflow_panel_area: Option<Rect>,
pub last_workflow_cancel_area: Option<Rect>,
@@ -721,7 +672,6 @@ impl Default for ViewportState {
last_transcript_area: None,
last_composer_area: None,
last_approval_area: None,
last_sidebar_area: None,
last_workflow_panel_area: None,
last_workflow_cancel_area: None,
last_transcript_top: 0,
@@ -1391,8 +1341,6 @@ pub struct App {
/// Toggled by `/voice-control`.
pub voice_control_enabled: bool,
pub transcript_spacing: TranscriptSpacing,
pub sidebar_width_percent: u16,
pub sidebar_focus: SidebarFocus,
/// Sidebar hover state for mouse tooltip support.
pub sidebar_hover: SidebarHoverState,
/// Current hover tooltip text, if any.
@@ -1408,23 +1356,6 @@ pub struct App {
pub provider_picker_memory: Option<ProviderPickerMemory>,
/// Last known mouse position for tooltip placement.
pub last_mouse_pos: Option<(u16, u16)>,
/// Whether the user is currently dragging the sidebar resize handle.
pub sidebar_resizing: bool,
/// Whether the pointer is over the classic sidebar resize handle.
pub sidebar_resize_hovered: bool,
/// Mouse column at the start of a sidebar-resize drag.
pub sidebar_resize_anchor_x: u16,
/// Sidebar width in columns at the start of a sidebar-resize drag.
pub sidebar_resize_anchor_width: u16,
/// Last sidebar area rendered (for mouse hit-testing the resize handle).
pub last_sidebar_area: Option<Rect>,
/// Last total chat/sidebar width considered for sidebar rendering.
pub last_sidebar_host_width: Option<u16>,
/// Handle rect painted on the left edge of the sidebar (1 col).
pub last_sidebar_handle_area: Option<Rect>,
/// Total horizontal space (chat + sidebar) used to compute the percentage
/// during sidebar resize drag.
pub sidebar_resize_total_width: u16,
/// Whether the session-context panel is enabled (#504).
pub context_panel: bool,
/// Whether the persistent Sessions rail is enabled (#2934). Opt-in.
@@ -4097,13 +4028,6 @@ impl App {
}
}
pub fn set_sidebar_focus(&mut self, focus: SidebarFocus) {
if self.sidebar_focus != focus {
self.sidebar_focus = focus;
}
self.needs_redraw = true;
}
pub fn close_slash_menu(&mut self) {
self.slash_menu_hidden = true;
self.needs_redraw = true;
-12
View File
@@ -241,8 +241,6 @@ impl App {
.trim()
.eq_ignore_ascii_case("vim");
let transcript_spacing = TranscriptSpacing::from_setting(&settings.transcript_spacing);
let sidebar_width_percent = settings.sidebar_width_percent;
let sidebar_focus = SidebarFocus::from_setting(&settings.sidebar_focus);
let max_input_history = settings.max_input_history;
let use_paste_burst_detection = settings.paste_burst_detection;
// Resolve the named theme from settings; unknown values were already
@@ -714,22 +712,12 @@ impl App {
voice_send_enabled: false,
voice_control_enabled: false,
transcript_spacing,
sidebar_width_percent,
sidebar_focus,
sidebar_hover: SidebarHoverState::default(),
sidebar_hover_tooltip: None,
cached_work_summary: None,
model_picker_memory: None,
provider_picker_memory: None,
last_mouse_pos: None,
sidebar_resizing: false,
sidebar_resize_hovered: false,
sidebar_resize_anchor_x: 0,
sidebar_resize_anchor_width: 0,
last_sidebar_area: None,
last_sidebar_host_width: None,
last_sidebar_handle_area: None,
sidebar_resize_total_width: 0,
context_panel: settings.context_panel,
sessions_rail: settings.sessions_rail,
sessions_rail_cache: None,
+6 -32
View File
@@ -1682,35 +1682,6 @@ fn cny_cache_savings_falls_back_to_usd_for_usd_only_models() {
assert!((savings - 0.79).abs() < 1e-9, "got {savings}");
}
#[test]
fn sidebar_focus_accepts_pinned_and_maps_legacy_trackers_to_pinned() {
assert_eq!(SidebarFocus::from_setting("auto"), SidebarFocus::Auto);
assert_eq!(SidebarFocus::from_setting("pinned"), SidebarFocus::Pinned);
assert_eq!(SidebarFocus::from_setting("work"), SidebarFocus::Pinned);
assert_eq!(SidebarFocus::from_setting("plan"), SidebarFocus::Pinned);
assert_eq!(SidebarFocus::from_setting("todos"), SidebarFocus::Pinned);
assert_eq!(SidebarFocus::from_setting("tasks"), SidebarFocus::Tasks);
assert_eq!(SidebarFocus::from_setting("activity"), SidebarFocus::Tasks);
assert_eq!(SidebarFocus::from_setting("live"), SidebarFocus::Tasks);
assert_eq!(SidebarFocus::from_setting("running"), SidebarFocus::Tasks);
assert_eq!(SidebarFocus::from_setting("agents"), SidebarFocus::Agents);
assert_eq!(SidebarFocus::from_setting("context"), SidebarFocus::Context);
assert_eq!(SidebarFocus::from_setting("sessions"), SidebarFocus::Sessions);
assert_eq!(
SidebarFocus::from_setting("sessions_rail"),
SidebarFocus::Sessions
);
assert_eq!(
SidebarFocus::from_setting("session_history"),
SidebarFocus::Sessions
);
assert_eq!(SidebarFocus::from_setting("hidden"), SidebarFocus::Hidden);
assert_eq!(SidebarFocus::from_setting("off"), SidebarFocus::Hidden);
assert_eq!(SidebarFocus::Pinned.as_setting(), "pinned");
assert_eq!(SidebarFocus::Hidden.as_setting(), "hidden");
assert_eq!(SidebarFocus::Sessions.as_setting(), "sessions");
}
#[test]
fn slash_command_classifier_treats_absolute_path_as_message() {
assert!(looks_like_slash_command_input("/"));
@@ -2873,12 +2844,15 @@ fn clear_todos_is_atomic_and_invalidates_cached_work_summary() {
}
#[test]
fn entering_operate_preserves_user_sidebar_focus() {
fn entering_operate_preserves_user_rail_panel() {
let mut app = App::new(test_options(false), &Config::default());
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Agents;
assert!(app.set_mode(AppMode::Operate));
assert_eq!(app.sidebar_focus, SidebarFocus::Tasks);
assert_eq!(
app.work_surface.panel,
crate::tui::work_surface::RailPanel::Agents
);
}
#[test]
+4 -3
View File
@@ -383,10 +383,11 @@ mod tests {
let options = TuiOptions {
..crate::test_support::test_tui_options(PathBuf::from("."))
};
// Pin sidebar so dogfood machines with Agents-visible settings.toml
// do not hide the footer agents chip this test asserts.
// Pin the rail panel so dogfood machines whose settings.toml migrates
// a legacy Agents sidebar focus do not hide the footer agents chip
// this test asserts.
let mut app = App::new(options, &Config::default());
app.sidebar_focus = crate::tui::app::SidebarFocus::Hidden;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app
}
+1 -1
View File
@@ -8,7 +8,7 @@ use crate::commands::{self, CommandInfo, CommandResult};
use crate::config::{ApiProvider, Config};
use crate::localization::{Locale, MessageId, tr};
use crate::provider_lake::all_catalog_models_for_provider;
use crate::tui::app::{App, AppAction, AppMode, SidebarFocus};
use crate::tui::app::{App, AppAction, AppMode};
use crate::tui::command_palette::{
CommandPaletteView, build_entries as build_command_palette_entries,
};
+8 -26
View File
@@ -44,7 +44,6 @@ pub(crate) fn should_drop_loading_mouse_motion(app: &App, mouse: MouseEvent) ->
// these events wedges the resize state mid-drag (#3063).
!app.viewport.transcript_selection.dragging
&& !app.viewport.transcript_scrollbar_dragging
&& !app.sidebar_resizing
&& !app.work_surface.is_resizing()
}
_ => false,
@@ -298,8 +297,7 @@ pub(crate) fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEv
// Other modals still own their wheel input exclusively (#4371).
if app.view_stack.top_kind() == Some(ModalKind::Approval) {
let over_approval = mouse_hits_rect(mouse, app.viewport.last_approval_area);
let over_side_surface = mouse_hits_rect(mouse, app.viewport.last_sidebar_area)
|| mouse_hits_rect(mouse, app.work_surface.last_area);
let over_side_surface = mouse_hits_rect(mouse, app.work_surface.last_area);
match mouse.kind {
MouseEventKind::ScrollUp => {
if over_approval || !over_side_surface {
@@ -385,22 +383,6 @@ pub(crate) fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEv
return Vec::new();
}
// Scroll events while the cursor is over the right-hand sidebar must not
// drive the transcript scroll. The sidebar is a fixed dashboard with no
// scroll state of its own, so consume the wheel event instead of leaking
// it into the transcript viewport behind it.
if matches!(
mouse.kind,
MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
) && app.viewport.last_sidebar_area.is_some_and(|area| {
mouse.column >= area.x
&& mouse.column < area.x.saturating_add(area.width)
&& mouse.row >= area.y
&& mouse.row < area.y.saturating_add(area.height)
}) {
return Vec::new();
}
match mouse.kind {
MouseEventKind::Moved => {
// Update last mouse position for tooltip rendering + hover layer.
@@ -1107,7 +1089,7 @@ pub(crate) fn open_context_menu(app: &mut App, mouse: MouseEvent) {
pub(crate) fn build_context_menu_entries(app: &App, mouse: MouseEvent) -> Vec<ContextMenuEntry> {
let mut entries = Vec::new();
let mut git_path = None;
let on_sidebar = mouse_hits_rect(mouse, app.viewport.last_sidebar_area);
let on_sidebar = mouse_hits_rect(mouse, app.work_surface.last_area);
if on_sidebar {
if let Some(command) = sidebar_click_action(app, mouse)
@@ -1680,7 +1662,7 @@ mod tests {
#[test]
fn context_menu_keeps_paste_first_outside_sidebar() {
let mut app = create_test_app();
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6));
app.work_surface.last_area = Some(Rect::new(60, 4, 20, 6));
let entries = build_context_menu_entries(&app, right_click(10, 4));
@@ -1693,7 +1675,7 @@ mod tests {
#[test]
fn sidebar_context_menu_omits_paste_without_row_action() {
let mut app = create_test_app();
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6));
app.work_surface.last_area = Some(Rect::new(60, 4, 20, 6));
app.sidebar_hover.sections.push(SidebarHoverSection {
content_area: Rect::new(60, 4, 20, 6),
lines: vec!["header".to_string()],
@@ -1713,7 +1695,7 @@ mod tests {
#[test]
fn sidebar_context_menu_runs_clickable_row_action() {
let mut app = create_test_app();
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6));
app.work_surface.last_area = Some(Rect::new(60, 4, 20, 6));
app.sidebar_hover.sections.push(SidebarHoverSection {
content_area: Rect::new(60, 4, 20, 6),
lines: vec!["job row".to_string()],
@@ -1793,7 +1775,7 @@ mod tests {
#[test]
fn sidebar_click_routes_inline_stop_zone_before_row_action() {
let mut app = create_test_app();
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 4));
app.work_surface.last_area = Some(Rect::new(60, 4, 20, 4));
app.sidebar_hover.sections.push(SidebarHoverSection {
content_area: Rect::new(60, 4, 20, 4),
lines: vec!["job row [x]".to_string()],
@@ -1819,7 +1801,7 @@ mod tests {
#[test]
fn sidebar_click_routes_agent_inline_stop_zone_before_peek_action() {
let mut app = create_test_app();
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 24, 4));
app.work_surface.last_area = Some(Rect::new(60, 4, 24, 4));
app.sidebar_hover.sections.push(SidebarHoverSection {
content_area: Rect::new(60, 4, 24, 4),
lines: vec!["[~] worker Agent 1 [x]".to_string()],
@@ -1854,7 +1836,7 @@ mod tests {
#[test]
fn sidebar_context_menu_offers_copy_of_hovered_row() {
let mut app = create_test_app();
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6));
app.work_surface.last_area = Some(Rect::new(60, 4, 20, 6));
app.sidebar_hover.sections.push(SidebarHoverSection {
content_area: Rect::new(60, 4, 20, 6),
lines: vec!["agent row".to_string()],
+48 -55
View File
@@ -13,28 +13,22 @@ use crate::localization::Locale;
use crate::tui::app::HuntVerdict;
use ratatui::{
Frame,
layout::Rect,
prelude::Widget,
style::Style,
text::{Line, Span},
widgets::{Block, Paragraph, Wrap},
};
use crate::deepseek_theme::Theme;
use crate::palette;
use crate::tools::subagent::{AgentWorkerStatus, SubAgentStatus, localized_whale_display_names};
use crate::tools::todo::TodoStatus;
use super::app::{
AgentCurrentActivity, AgentCurrentActivityStatus, App, SidebarFocus, SidebarHoverRow,
SidebarHoverSection, SidebarRowAction, TaskPanelEntry, TaskPanelEntryKind,
AgentCurrentActivity, AgentCurrentActivityStatus, App, SidebarHoverRow, SidebarRowAction,
TaskPanelEntry, TaskPanelEntryKind,
};
use super::history::{GenericToolCell, HistoryCell, ToolCell, ToolStatus, summarize_tool_output};
use super::motion::MotionPolicy;
use super::spinner::{LIVE_MARKER_DELAY_MS, braille_spinner_frame_for_elapsed_ms};
use super::subagent_routing::active_fanout_counts;
use super::ui::FILE_TREE_MIN_HOST_WIDTH;
use super::ui_text::{concise_shell_command_label, truncate_line_to_width};
/// Tolerance for floating-point cost comparison in the sidebar breakdown.
@@ -811,7 +805,7 @@ struct TaskPanelRowSets {
}
fn task_panel_row_sets(app: &App) -> TaskPanelRowSets {
let explicit_tasks_focus = app.sidebar_focus == SidebarFocus::Tasks;
let explicit_tasks_focus = app.work_surface.panel == crate::tui::work_surface::RailPanel::Tasks;
let active = active_tool_rows(app);
// Auto/Pinned mode deliberately skips live-tool dedup (passes an empty
// slice), matching the previous per-consumer call sites.
@@ -845,7 +839,7 @@ fn task_panel_rows(
let theme = &app.ui_theme;
let mut lines: Vec<Line<'static>> = Vec::with_capacity(max_rows.max(4));
let mut actions: Vec<Option<SidebarRowAction>> = Vec::with_capacity(max_rows.max(4));
let explicit_tasks_focus = app.sidebar_focus == SidebarFocus::Tasks;
let explicit_tasks_focus = app.work_surface.panel == crate::tui::work_surface::RailPanel::Tasks;
if explicit_tasks_focus && app.runtime_turn_id.is_some() {
let status = app
@@ -986,7 +980,7 @@ fn task_panel_rows(
// Yank hint: surface the keyboard path for copying the focused task/turn ID.
if lines.len() + 1 < max_rows
&& app.runtime_turn_id.is_some()
&& app.sidebar_focus == SidebarFocus::Tasks
&& app.work_surface.panel == crate::tui::work_surface::RailPanel::Tasks
{
lines.push(Line::from(Span::styled(
"y → copy turn id · Y → copy full status",
@@ -1016,7 +1010,7 @@ fn task_panel_rows(
fn task_panel_hover_texts(app: &App, row_sets: &TaskPanelRowSets, max_rows: usize) -> Vec<String> {
let mut texts = Vec::with_capacity(max_rows.max(4));
let explicit_tasks_focus = app.sidebar_focus == SidebarFocus::Tasks;
let explicit_tasks_focus = app.work_surface.panel == crate::tui::work_surface::RailPanel::Tasks;
if explicit_tasks_focus && let Some(turn_id) = app.runtime_turn_id.as_ref() {
let status = app.runtime_turn_status.as_deref().unwrap_or("unknown");
@@ -1094,7 +1088,7 @@ fn task_panel_hover_texts(app: &App, row_sets: &TaskPanelRowSets, max_rows: usiz
if texts.len() + 1 < max_rows
&& app.runtime_turn_id.is_some()
&& app.sidebar_focus == SidebarFocus::Tasks
&& app.work_surface.panel == crate::tui::work_surface::RailPanel::Tasks
{
texts.push("y -> copy turn id · Y -> copy full status".to_string());
}
@@ -2854,16 +2848,15 @@ fn agent_stop_action_for_click(action: &SidebarRowAction) -> Option<SidebarRowAc
mod tests {
use super::{
ACTIVE_TOOL_COMPLETED_ROW_TTL, ACTIVE_TOOL_STALE_RUNNING_ROW_TTL, HotbarSlotState,
SidebarAgentRow, SidebarFocus, SidebarHoverRow, SidebarHoverSection,
SidebarSubagentSummary, SidebarToolRow, SidebarWorkChecklistItem, SidebarWorkSummary,
ToolRowOrder, agent_row_hover_text, background_task_spinner_prefix,
cached_agent_activity_is_live, context_panel_cost_line, editorial_tool_rows,
hotbar_panel_enabled, hotbar_panel_hover_texts, hotbar_panel_lines, hotbar_panel_slots,
normalize_activity_text, sidebar_agent_rows, sidebar_hover_rows, sidebar_work_summary,
sort_sidebar_agent_rows_as_tree, subagent_output_handle, subagent_panel_hover_texts,
subagent_panel_lines, subagent_panel_rows, task_panel_hover_texts, task_panel_lines,
task_panel_row_sets, task_panel_rows, work_panel_empty_hint, work_panel_hover_texts,
work_panel_lines,
SidebarAgentRow, SidebarHoverRow, SidebarSubagentSummary, SidebarToolRow,
SidebarWorkChecklistItem, SidebarWorkSummary, ToolRowOrder, agent_row_hover_text,
background_task_spinner_prefix, cached_agent_activity_is_live, context_panel_cost_line,
editorial_tool_rows, hotbar_panel_enabled, hotbar_panel_hover_texts, hotbar_panel_lines,
hotbar_panel_slots, normalize_activity_text, sidebar_agent_rows, sidebar_hover_rows,
sidebar_work_summary, sort_sidebar_agent_rows_as_tree, subagent_output_handle,
subagent_panel_hover_texts, subagent_panel_lines, subagent_panel_rows,
task_panel_hover_texts, task_panel_lines, task_panel_row_sets, task_panel_rows,
work_panel_empty_hint, work_panel_hover_texts, work_panel_lines,
};
use crate::config::Config;
use crate::localization::Locale;
@@ -2873,8 +2866,8 @@ mod tests {
use crate::tui::active_cell::ActiveCell;
use crate::tui::app::{
AgentCurrentActivity, AgentCurrentActivityStatus, AgentProgressMeta, App, AppMode,
HuntVerdict, SidebarHoverState, SidebarRowAction, TaskPanelEntry, TaskPanelEntryKind,
TuiOptions,
HuntVerdict, SidebarHoverSection, SidebarHoverState, SidebarRowAction, TaskPanelEntry,
TaskPanelEntryKind, TuiOptions,
};
use crate::tui::history::{
ExecCell, ExecSource, GenericToolCell, HistoryCell, ToolCell, ToolStatus,
@@ -3161,7 +3154,7 @@ mod tests {
// `hotbar_panel_slots_resolve_configured_bindings_and_active_state`.
let mut app = create_test_app();
app.mode = AppMode::Agent;
app.sidebar_focus = SidebarFocus::Pinned;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Pinned;
assert!(
!hotbar_panel_enabled(&app, &Config::default()),
@@ -3190,7 +3183,7 @@ mod tests {
fn hotbar_panel_slots_resolve_configured_bindings_and_active_state() {
let mut app = create_test_app();
app.mode = AppMode::Agent;
app.sidebar_focus = SidebarFocus::Pinned;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Pinned;
let config = Config {
hotbar: Some(
codewhale_config::default_hotbar_bindings()
@@ -3285,7 +3278,7 @@ mod tests {
fn hotbar_panel_lines_keep_two_fixed_rows_and_hover_status() {
let mut app = create_test_app();
app.mode = AppMode::Agent;
app.sidebar_focus = SidebarFocus::Pinned;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Pinned;
let config = Config {
hotbar: Some(
codewhale_config::default_hotbar_bindings()
@@ -3799,7 +3792,7 @@ mod tests {
assert!(!joined.contains("Strategy") && !joined.contains("route "));
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let activity = lines_to_text(&task_panel_lines(&app, 48, 4));
assert!(
activity
@@ -3816,7 +3809,7 @@ mod tests {
#[test]
fn tasks_panel_renders_active_tool_rows_before_background_empty_state() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
active.push_tool(
"tool-1",
@@ -3856,7 +3849,7 @@ mod tests {
#[test]
fn tasks_panel_renders_recent_completed_tool_rows() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.history
.push(HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "read_file".to_string(),
@@ -3884,7 +3877,7 @@ mod tests {
#[test]
fn tasks_panel_expires_completed_active_tool_rows() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
active.push_tool(
"tool-1",
@@ -3924,7 +3917,7 @@ mod tests {
#[test]
fn tasks_panel_lingers_fresh_completed_active_tool_rows() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
active.push_tool(
"tool-1",
@@ -3983,7 +3976,7 @@ mod tests {
files_touched: 0,
});
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let focused = task_panel_row_sets(&app);
assert!(
focused.background.is_empty(),
@@ -3991,7 +3984,7 @@ mod tests {
focused.background
);
app.sidebar_focus = SidebarFocus::Auto;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Pinned;
let auto = task_panel_row_sets(&app);
assert_eq!(
auto.background.len(),
@@ -4003,7 +3996,7 @@ mod tests {
#[test]
fn task_panel_rows_and_hover_share_one_snapshot() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.runtime_turn_id = Some("turn_abcdef123456".to_string());
app.runtime_turn_status = Some("in_progress".to_string());
app.turn_counter = 3;
@@ -4051,7 +4044,7 @@ mod tests {
#[test]
fn tasks_panel_collapses_stale_running_tool_rows() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
for (idx, command) in ["long one", "long two"].into_iter().enumerate() {
active.push_tool(
@@ -4090,7 +4083,7 @@ mod tests {
#[test]
fn tasks_panel_does_not_double_count_running_shell_job_as_live_and_background() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
active.push_tool(
"shell-1",
@@ -4358,10 +4351,10 @@ mod tests {
}
#[test]
fn tasks_panel_auto_mode_shows_only_live_background_jobs() {
fn tasks_panel_non_tasks_mode_shows_only_live_background_jobs() {
let mut app = create_test_app();
app.low_motion = false;
app.sidebar_focus = SidebarFocus::Auto;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Pinned;
app.runtime_turn_id = Some("turn_abcdef123456".to_string());
app.runtime_turn_status = Some("in_progress".to_string());
let mut active = ActiveCell::new();
@@ -4435,7 +4428,7 @@ mod tests {
#[test]
fn tasks_panel_keeps_model_reasoning_in_transcript_only() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.runtime_turn_id = Some("turn_reasoning".to_string());
app.runtime_turn_status = Some("in_progress".to_string());
app.history.push(HistoryCell::Thinking {
@@ -4459,7 +4452,7 @@ mod tests {
#[test]
fn task_panel_actions_make_single_background_job_clickable() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.task_panel.push(TaskPanelEntry {
id: "shell_only".to_string(),
status: "running".to_string(),
@@ -4502,7 +4495,7 @@ mod tests {
#[test]
fn stale_background_job_row_shows_no_output_warning_and_cancel_hint() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.task_panel.push(TaskPanelEntry {
id: "shell_stale".to_string(),
status: "running".to_string(),
@@ -4548,7 +4541,7 @@ mod tests {
#[test]
fn task_panel_actions_route_each_job_to_its_own_id() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.task_panel.push(TaskPanelEntry {
id: "shell_aaa".to_string(),
status: "running".to_string(),
@@ -4632,7 +4625,7 @@ mod tests {
#[test]
fn task_panel_finished_job_detail_row_shows_instead_of_cancels() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.task_panel.push(TaskPanelEntry {
id: "shell_done".to_string(),
status: "completed".to_string(),
@@ -4669,7 +4662,7 @@ mod tests {
#[test]
fn task_panel_actions_align_with_lines_when_live_tools_present() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.runtime_turn_id = Some("0196f0a3-aaaa-bbbb-cccc-ddddeeee0000".to_string());
let mut active = ActiveCell::new();
active.push_tool(
@@ -5133,7 +5126,7 @@ mod tests {
#[test]
fn tasks_panel_collapses_repeated_low_value_recent_tools_after_failures() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
for path in ["src/a.rs", "src/b.rs", "src/c.rs"] {
app.history
.push(HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
@@ -5200,7 +5193,7 @@ mod tests {
#[test]
fn tasks_panel_collapses_repeated_pending_ci_polls() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
for _ in 0..3 {
app.history.push(HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: "cd /tmp/repo && sleep 15 && gh pr checks 1616 --repo Hmbown/CodeWhale"
@@ -5248,7 +5241,7 @@ mod tests {
#[test]
fn tasks_panel_failed_shell_rows_point_to_activity_details() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.history.push(HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: "cargo test -p codewhale-tui".to_string(),
status: ToolStatus::Failed,
@@ -5282,7 +5275,7 @@ mod tests {
#[test]
fn tasks_panel_keeps_duration_and_status_on_recent_shell_rows() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.history.push(HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: "cargo check".to_string(),
status: ToolStatus::Success,
@@ -5314,7 +5307,7 @@ mod tests {
#[test]
fn tasks_panel_uses_plain_names_for_shell_background_helpers() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
active.push_tool(
"shell-wait",
@@ -5346,7 +5339,7 @@ mod tests {
#[test]
fn tasks_panel_collapses_repeated_shell_waits_for_same_job() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
for id in ["shell-wait-1", "shell-wait-2"] {
active.push_tool(
@@ -5383,7 +5376,7 @@ mod tests {
#[test]
fn tasks_panel_collapses_repeated_shell_waits_without_task_marker() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
let mut active = ActiveCell::new();
for (id, summary) in [
("shell-wait-1", "Background task running (no new output)."),
@@ -6418,7 +6411,7 @@ mod tests {
#[test]
fn tasks_panel_shows_stable_turn_label_not_uuid() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.runtime_turn_id = Some("0196f0a3-1111-2222-3333-444455556666".to_string());
app.runtime_turn_status = Some("in_progress".to_string());
app.turn_counter = 3;
@@ -6443,7 +6436,7 @@ mod tests {
#[test]
fn tasks_panel_turn_label_falls_back_before_first_counted_turn() {
let mut app = create_test_app();
app.sidebar_focus = SidebarFocus::Tasks;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
app.runtime_turn_id = Some("0196f0a3-1111-2222-3333-444455556666".to_string());
app.runtime_turn_status = Some("in_progress".to_string());
app.turn_counter = 0;
+5 -15
View File
@@ -9,7 +9,7 @@ use crate::tools::subagent::{
};
use crate::tui::app::{
AgentCurrentActivity, AgentCurrentActivityStatus, AgentProgressMeta, AgentRecentAction, App,
AppMode, MAX_AGENT_RECENT_ACTIONS, SidebarFocus, TaskPanelEntry, TaskPanelEntryKind,
AppMode, MAX_AGENT_RECENT_ACTIONS, TaskPanelEntry, TaskPanelEntryKind,
bound_agent_activity_text,
};
use crate::tui::history::{HistoryCell, SubAgentCell, summarize_tool_output};
@@ -23,13 +23,6 @@ use crate::tui::workspace_context;
const SUBAGENT_TERMINAL_CARD_TTL: Duration = Duration::from_secs(5 * 60);
const SUBAGENT_TERMINAL_CARD_MAX_RETAINED: usize = 24;
fn agents_panel_has_content(app: &App) -> bool {
!app.subagent_cache.is_empty()
|| !app.agent_progress.is_empty()
|| active_fanout_counts(app).is_some()
|| foreground_rlm_running(app)
}
fn foreground_rlm_running(app: &App) -> bool {
use crate::tui::history::{HistoryCell, ToolCell, ToolStatus};
app.active_cell.as_ref().is_some_and(|active| {
@@ -46,14 +39,11 @@ fn foreground_rlm_running(app: &App) -> bool {
})
}
/// True when the Agents sidebar panel is on-screen and already owns fanout summary.
/// True when the rail's Agents panel is on-screen and already owns fanout
/// summary, so the footer chip would be redundant.
pub(super) fn agents_sidebar_surface_visible(app: &App) -> bool {
match app.sidebar_focus {
SidebarFocus::Hidden => false,
SidebarFocus::Agents => true,
SidebarFocus::Auto => agents_panel_has_content(app),
_ => false,
}
app.work_surface.panel == crate::tui::work_surface::RailPanel::Agents
&& app.work_surface.last_area.is_some()
}
pub(super) fn running_agent_count(app: &App) -> usize {
+4 -13
View File
@@ -138,10 +138,10 @@ use super::key_actions;
use super::app::{
ActiveTurnMetadata, AgentCurrentActivity, AgentCurrentActivityStatus, App, AppAction, AppMode,
ComposerSubmitAction, ComposerSubmitChord, EffectiveReasoningEffort, HuntVerdict,
OnboardingState, PendingProviderSwitch, QueuedMessage, ReasoningEffort, SidebarFocus,
StatusToast, StatusToastLevel, SubmitDisposition, TaskPanelEntry, TaskPanelEntryKind,
ToolEvidence, TuiOptions, bound_agent_activity_text, is_stop_word,
looks_like_slash_command_input, shell_command_from_bang_input,
OnboardingState, PendingProviderSwitch, QueuedMessage, ReasoningEffort, StatusToast,
StatusToastLevel, SubmitDisposition, TaskPanelEntry, TaskPanelEntryKind, ToolEvidence,
TuiOptions, bound_agent_activity_text, is_stop_word, looks_like_slash_command_input,
shell_command_from_bang_input,
};
use super::approval::{
ApprovalMode, ApprovalRequest, ApprovalView, ElevationRequest, ElevationView, ReviewDecision,
@@ -13830,15 +13830,6 @@ fn render(f: &mut Frame, app: &mut App, _config: &Config) {
app.file_tree_visible = false;
work_chat_area
};
app.last_sidebar_host_width = Some(chat_area.width);
// The legacy classic-shell sidebar no longer exists: no sidebar area
// is ever produced, so clear the interaction state its former mouse
// handlers read.
app.viewport.last_sidebar_area = None;
app.last_sidebar_area = None;
app.last_sidebar_handle_area = None;
app.sidebar_resizing = false;
app.sidebar_resize_hovered = false;
app.sidebar_hover_tooltip = None;
let chat_widget = ChatWidget::new(app, chat_area).with_ocean_viewport(size);
+6 -13
View File
@@ -812,7 +812,7 @@ fn approval_mouse_wheel_reviews_transcript_without_closing_card() {
}
#[test]
fn approval_wheel_preserves_sidebar_and_work_surface_ownership() {
fn approval_wheel_preserves_work_surface_ownership() {
let mut app = create_test_app();
app.view_stack.push(ApprovalView::new(ApprovalRequest::new(
"approval-scroll",
@@ -821,11 +821,10 @@ fn approval_wheel_preserves_sidebar_and_work_surface_ownership() {
&serde_json::json!({"command": "git status"}),
"approval-scroll-key",
)));
app.viewport.last_sidebar_area = Some(Rect::new(60, 0, 20, 20));
app.work_surface.last_area = Some(Rect::new(0, 0, 30, 20));
app.viewport.last_approval_area = Some(Rect::new(0, 12, 80, 8));
for (column, row) in [(65, 4), (10, 4)] {
for (column, row) in [(10, 4), (20, 4)] {
let events = handle_mouse_event(
&mut app,
MouseEvent {
@@ -1980,12 +1979,7 @@ fn loading_mouse_filter_keeps_hover_and_active_drags() {
app.viewport.transcript_scrollbar_dragging = true;
assert!(!should_drop_loading_mouse_motion(&app, drag));
// Sidebar drag-to-resize must also survive the loading filter (#3063).
app.viewport.transcript_scrollbar_dragging = false;
app.sidebar_resizing = true;
assert!(!should_drop_loading_mouse_motion(&app, drag));
app.sidebar_resizing = false;
app.work_surface.last_area = Some(Rect::new(0, 0, 80, 3));
let started = crate::tui::work_surface::handle_mouse(
&mut app,
@@ -2007,7 +2001,6 @@ fn loading_mouse_filter_keeps_hover_and_active_drags() {
fn loading_mouse_filter_allows_sidebar_hover_to_clear() {
let mut app = create_test_app();
app.is_loading = true;
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6));
app.sidebar_hover_tooltip = Some("Stale sidebar tooltip".to_string());
let moved = MouseEvent {
kind: MouseEventKind::Moved,
@@ -2027,7 +2020,6 @@ fn loading_mouse_filter_allows_sidebar_hover_to_clear() {
fn loading_mouse_filter_allows_sidebar_exit_to_clear_highlight() {
let mut app = create_test_app();
app.is_loading = true;
app.viewport.last_sidebar_area = Some(Rect::new(60, 4, 20, 6));
app.last_mouse_pos = Some((60, 5));
let exit_left = MouseEvent {
@@ -8785,10 +8777,10 @@ fn hotbar_alt_digit_fires_from_composer_and_sidebar_states() {
app.input = " ".to_string();
assert_eq!(hotbar_slot_from_key(&app, &alt_four), Some(4));
app.sidebar_focus = SidebarFocus::Hidden;
app.work_surface.placement = crate::tui::work_surface::WorkSurfacePlacement::Off;
assert_eq!(hotbar_slot_from_key(&app, &alt_four), Some(4));
app.sidebar_focus = SidebarFocus::Agents;
app.work_surface.panel = crate::tui::work_surface::RailPanel::Agents;
assert_eq!(hotbar_slot_from_key(&app, &alt_four), Some(4));
}
@@ -9147,7 +9139,8 @@ fn ctrl_x_jobs_prefill_only_catches_running_shell_jobs_in_tasks_sidebar() {
#[test]
fn ctrl_x_jobs_prefill_falls_through_outside_tasks_sidebar_shell_jobs() {
let mut non_shell = create_test_app();
non_shell.sidebar_focus = SidebarFocus::Tasks;
non_shell.work_surface.panel = crate::tui::work_surface::RailPanel::Tasks;
non_shell.work_surface.last_area = Some(ratatui::layout::Rect::new(0, 0, 100, 3));
non_shell.input = "draft".to_string();
non_shell.cursor_position = non_shell.input.len();
non_shell.task_panel.push(TaskPanelEntry {