aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app/input.rs109
-rw-r--r--src/app/mod.rs22
-rw-r--r--src/app/state.rs14
-rw-r--r--src/persist.rs28
-rw-r--r--src/ui.rs298
5 files changed, 402 insertions, 69 deletions
diff --git a/src/app/input.rs b/src/app/input.rs
index 81089217..6af7cf0e 100644
--- a/src/app/input.rs
+++ b/src/app/input.rs
@@ -27,8 +27,8 @@ enum WheelRouting {
const WORKSPACE_DRAG_THRESHOLD: u16 = 1;
use super::state::{
- key_matches, AppState, ContextMenuKind, ContextMenuState, DragState, DragTarget, MenuListState,
- Mode, WorkspacePressState,
+ key_matches, AgentPanelScope, AppState, ContextMenuKind, ContextMenuState, DragState,
+ DragTarget, MenuListState, Mode, WorkspacePressState,
};
use super::App;
@@ -2025,6 +2025,14 @@ impl AppState {
return None;
}
+ if self.on_agent_panel_scope_toggle(mouse.column, mouse.row) {
+ self.agent_panel_scope = match self.agent_panel_scope {
+ AgentPanelScope::CurrentWorkspace => AgentPanelScope::AllWorkspaces,
+ AgentPanelScope::AllWorkspaces => AgentPanelScope::CurrentWorkspace,
+ };
+ return None;
+ }
+
if let Some((ws_idx, tab_idx, pane_id)) = self.agent_detail_target_at(mouse.row)
{
self.switch_workspace(ws_idx);
@@ -2458,29 +2466,26 @@ impl AppState {
best.map(|(insert_idx, _)| insert_idx)
}
- fn agent_detail_target_at(&self, row: u16) -> Option<(usize, usize, crate::layout::PaneId)> {
+ fn on_agent_panel_scope_toggle(&self, col: u16, row: u16) -> bool {
if self.sidebar_collapsed {
- return None;
+ return false;
}
- let content = Rect::new(
- self.view.sidebar_rect.x,
- self.view.sidebar_rect.y,
- self.view.sidebar_rect.width.saturating_sub(1),
- self.view.sidebar_rect.height,
- );
- if content.width == 0 || content.height == 0 {
+ let (_, detail_area) = crate::ui::expanded_sidebar_sections(self.view.sidebar_rect);
+ let rect = crate::ui::agent_panel_toggle_rect(detail_area, self.agent_panel_scope);
+ rect.width > 0
+ && col >= rect.x
+ && col < rect.x + rect.width
+ && row >= rect.y
+ && row < rect.y + rect.height
+ }
+
+ fn agent_detail_target_at(&self, row: u16) -> Option<(usize, usize, crate::layout::PaneId)> {
+ if self.sidebar_collapsed {
return None;
}
- let total_h = content.height as usize;
- let ws_h = (total_h + 1) / 2;
- let detail_area = Rect::new(
- content.x,
- content.y + ws_h as u16,
- content.width,
- total_h.saturating_sub(ws_h) as u16,
- );
+ let (_, detail_area) = crate::ui::expanded_sidebar_sections(self.view.sidebar_rect);
if detail_area.height < 4
|| row < detail_area.y + 3
|| row >= detail_area.y + detail_area.height
@@ -2488,17 +2493,15 @@ impl AppState {
return None;
}
- let detail_ws_idx = self.collapsed_detail_workspace_idx()?;
- let ws = self.workspaces.get(detail_ws_idx)?;
let relative_row = row - (detail_area.y + 3);
let entry_height = 3;
if relative_row % entry_height == 2 {
return None;
}
let detail_idx = (relative_row / entry_height) as usize;
- let details = ws.pane_details();
+ let details = crate::ui::agent_panel_entries(self);
let detail = details.get(detail_idx)?;
- Some((detail_ws_idx, detail.tab_idx, detail.pane_id))
+ Some((detail.ws_idx, detail.tab_idx, detail.pane_id))
}
fn screen_rect(&self) -> Rect {
@@ -3513,6 +3516,66 @@ mod tests {
}
#[test]
+ fn clicking_agent_panel_toggle_switches_scope() {
+ let mut app = app_for_mouse_test();
+ app.state.workspaces = vec![Workspace::test_new("test")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+
+ let (_, detail_area) = crate::ui::expanded_sidebar_sections(app.state.view.sidebar_rect);
+ let toggle = crate::ui::agent_panel_toggle_rect(detail_area, app.state.agent_panel_scope);
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ toggle.x,
+ toggle.y,
+ ));
+
+ assert_eq!(app.state.agent_panel_scope, AgentPanelScope::AllWorkspaces);
+ }
+
+ #[test]
+ fn clicking_all_workspaces_agent_row_switches_to_correct_workspace() {
+ let mut app = app_for_mouse_test();
+ let mut first = Workspace::test_new("one");
+ let first_pane = first.tabs[0].root_pane;
+ first.tabs[0]
+ .panes
+ .get_mut(&first_pane)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+
+ let mut second = Workspace::test_new("two");
+ let second_pane = second.tabs[0].root_pane;
+ second.tabs[0]
+ .panes
+ .get_mut(&second_pane)
+ .unwrap()
+ .detected_agent = Some(Agent::Claude);
+
+ app.state.workspaces = vec![first, second];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+ app.state.agent_panel_scope = AgentPanelScope::AllWorkspaces;
+
+ let (_, detail_area) = crate::ui::expanded_sidebar_sections(app.state.view.sidebar_rect);
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ detail_area.x + 2,
+ detail_area.y + 6,
+ ));
+
+ assert_eq!(app.state.active, Some(1));
+ assert_eq!(app.state.selected, 1);
+ assert_eq!(app.state.workspaces[1].active_tab, 0);
+ assert_eq!(
+ app.state.workspaces[1].tabs[0].layout.focused(),
+ second_pane
+ );
+ }
+
+ #[test]
fn clicking_collapsed_agent_row_switches_to_correct_tab_and_pane() {
let mut app = app_for_mouse_test();
let mut ws = Workspace::test_new("test");
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 14274210..f06bbb19 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -157,8 +157,13 @@ impl App {
let render_dirty = Arc::new(AtomicBool::new(false));
// Try to restore previous session
- let (workspaces, active, selected) = if no_session {
- (Vec::new(), None, 0)
+ let (workspaces, active, selected, agent_panel_scope) = if no_session {
+ (
+ Vec::new(),
+ None,
+ 0,
+ state::AgentPanelScope::CurrentWorkspace,
+ )
} else if let Some(snap) = crate::persist::load() {
let ws = crate::persist::restore(
&snap,
@@ -170,15 +175,20 @@ impl App {
);
if ws.is_empty() {
info!("session file found but no workspaces restored");
- (Vec::new(), None, 0)
+ (Vec::new(), None, 0, snap.agent_panel_scope)
} else {
info!(count = ws.len(), "session restored");
let active = snap.active.filter(|&i| i < ws.len());
let selected = snap.selected.min(ws.len().saturating_sub(1));
- (ws, active, selected)
+ (ws, active, selected, snap.agent_panel_scope)
}
} else {
- (Vec::new(), None, 0)
+ (
+ Vec::new(),
+ None,
+ 0,
+ state::AgentPanelScope::CurrentWorkspace,
+ )
};
let latest_release_notes_available = crate::release_notes::load_latest().is_some();
@@ -240,6 +250,7 @@ impl App {
sidebar_width: config.ui.sidebar_width,
sidebar_width_auto: true,
sidebar_collapsed: false,
+ agent_panel_scope,
confirm_close: config.ui.confirm_close,
accent: crate::config::parse_color(&config.ui.accent),
sound: config.ui.sound.clone(),
@@ -422,6 +433,7 @@ impl App {
&self.state.workspaces,
self.state.active,
self.state.selected,
+ self.state.agent_panel_scope,
);
crate::persist::save(&snap);
}
diff --git a/src/app/state.rs b/src/app/state.rs
index 74daeeb8..60cb0511 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -356,6 +356,18 @@ pub enum Mode {
KeybindHelp,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+pub enum AgentPanelScope {
+ CurrentWorkspace,
+ AllWorkspaces,
+}
+
+impl Default for AgentPanelScope {
+ fn default() -> Self {
+ Self::CurrentWorkspace
+ }
+}
+
// ---------------------------------------------------------------------------
// Settings UI state
// ---------------------------------------------------------------------------
@@ -586,6 +598,7 @@ pub struct AppState {
pub sidebar_width: u16,
pub sidebar_width_auto: bool,
pub sidebar_collapsed: bool,
+ pub agent_panel_scope: AgentPanelScope,
pub confirm_close: bool,
#[allow(dead_code)] // kept for backward compat; palette.accent is the source of truth
pub accent: Color,
@@ -693,6 +706,7 @@ impl AppState {
sidebar_width: 26,
sidebar_width_auto: true,
sidebar_collapsed: false,
+ agent_panel_scope: AgentPanelScope::CurrentWorkspace,
confirm_close: true,
accent: Color::Cyan,
sound: SoundConfig {
diff --git a/src/persist.rs b/src/persist.rs
index 345fe9f7..72ac9bef 100644
--- a/src/persist.rs
+++ b/src/persist.rs
@@ -30,6 +30,8 @@ pub struct SessionSnapshot {
pub workspaces: Vec<WorkspaceSnapshot>,
pub active: Option<usize>,
pub selected: usize,
+ #[serde(default)]
+ pub agent_panel_scope: crate::app::state::AgentPanelScope,
}
#[derive(Serialize)]
@@ -215,12 +217,14 @@ pub fn capture(
workspaces: &[Workspace],
active: Option<usize>,
selected: usize,
+ agent_panel_scope: crate::app::state::AgentPanelScope,
) -> SessionSnapshot {
SessionSnapshot {
version: SNAPSHOT_VERSION,
workspaces: workspaces.iter().map(capture_workspace).collect(),
active,
selected,
+ agent_panel_scope,
}
}
@@ -556,6 +560,7 @@ mod tests {
workspaces: vec![],
active: None,
selected: 0,
+ agent_panel_scope: crate::app::state::AgentPanelScope::CurrentWorkspace,
};
let json = serde_json::to_string(&snap).unwrap();
let restored: SessionSnapshot = serde_json::from_str(&json).unwrap();
@@ -624,6 +629,7 @@ mod tests {
}],
active: Some(0),
selected: 0,
+ agent_panel_scope: crate::app::state::AgentPanelScope::CurrentWorkspace,
version: SNAPSHOT_VERSION,
};
@@ -642,6 +648,28 @@ mod tests {
restored.workspaces[0].tabs[0].panes[&0].cwd,
PathBuf::from("/home/can/Projects/herdr")
);
+ assert_eq!(
+ restored.agent_panel_scope,
+ crate::app::state::AgentPanelScope::CurrentWorkspace
+ );
+ }
+
+ #[test]
+ fn old_snapshot_defaults_agent_panel_scope() {
+ let json = serde_json::json!({
+ "version": SNAPSHOT_VERSION,
+ "workspaces": [],
+ "active": null,
+ "selected": 0
+ })
+ .to_string();
+
+ let restored: SessionSnapshot = serde_json::from_str(&json).unwrap();
+
+ assert_eq!(
+ restored.agent_panel_scope,
+ crate::app::state::AgentPanelScope::CurrentWorkspace
+ );
}
#[test]
diff --git a/src/ui.rs b/src/ui.rs
index a949a90d..247199b1 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -6,7 +6,7 @@ use ratatui::{
Frame,
};
-use crate::app::state::{ToastKind, ToastNotification};
+use crate::app::state::{AgentPanelScope, ToastKind, ToastNotification};
use crate::app::{AppState, Mode};
use crate::detect::AgentState;
use crate::layout::PaneInfo;
@@ -26,6 +26,183 @@ fn spinner_frame(tick: u32) -> &'static str {
use crate::app::state::Palette;
+pub(crate) struct AgentPanelEntry {
+ pub ws_idx: usize,
+ pub tab_idx: usize,
+ pub pane_id: crate::layout::PaneId,
+ pub primary_label: String,
+ pub primary_tab_label: Option<String>,
+ pub agent_label: Option<String>,
+ pub state: AgentState,
+ pub seen: bool,
+}
+
+pub(crate) fn expanded_sidebar_sections(area: Rect) -> (Rect, Rect) {
+ let content = Rect::new(area.x, area.y, area.width.saturating_sub(1), area.height);
+ if content.width == 0 || content.height == 0 {
+ return (Rect::default(), Rect::default());
+ }
+
+ let total_h = content.height as usize;
+ let ws_h = (total_h + 1) / 2;
+ let detail_h = total_h.saturating_sub(ws_h);
+
+ let ws_area = Rect::new(content.x, content.y, content.width, ws_h as u16);
+ let detail_area = Rect::new(
+ content.x,
+ content.y + ws_h as u16,
+ content.width,
+ detail_h as u16,
+ );
+ (ws_area, detail_area)
+}
+
+fn agent_panel_current_workspace_idx(app: &AppState) -> Option<usize> {
+ if matches!(
+ app.mode,
+ Mode::Navigate
+ | Mode::RenameWorkspace
+ | Mode::Resize
+ | Mode::ConfirmClose
+ | Mode::ContextMenu
+ | Mode::Settings
+ | Mode::GlobalMenu
+ | Mode::KeybindHelp
+ ) {
+ Some(app.selected)
+ } else {
+ app.active
+ }
+}
+
+fn agent_panel_toggle_label(scope: AgentPanelScope) -> &'static str {
+ match scope {
+ AgentPanelScope::CurrentWorkspace => "current",
+ AgentPanelScope::AllWorkspaces => "all",
+ }
+}
+
+pub(crate) fn agent_panel_toggle_rect(area: Rect, scope: AgentPanelScope) -> Rect {
+ if area.width == 0 || area.height < 2 {
+ return Rect::default();
+ }
+
+ let label = agent_panel_toggle_label(scope);
+ let width = label.chars().count() as u16;
+ Rect::new(
+ area.x + area.width.saturating_sub(width),
+ area.y + 1,
+ width,
+ 1,
+ )
+}
+
+pub(crate) fn agent_panel_entries(app: &AppState) -> Vec<AgentPanelEntry> {
+ match app.agent_panel_scope {
+ AgentPanelScope::CurrentWorkspace => {
+ let Some(ws_idx) = agent_panel_current_workspace_idx(app) else {
+ return Vec::new();
+ };
+ let Some(ws) = app.workspaces.get(ws_idx) else {
+ return Vec::new();
+ };
+ ws.pane_details()
+ .into_iter()
+ .map(|detail| AgentPanelEntry {
+ ws_idx,
+ tab_idx: detail.tab_idx,
+ pane_id: detail.pane_id,
+ primary_label: detail.label,
+ primary_tab_label: None,
+ agent_label: None,
+ state: detail.state,
+ seen: detail.seen,
+ })
+ .collect()
+ }
+ AgentPanelScope::AllWorkspaces => app
+ .workspaces
+ .iter()
+ .enumerate()
+ .flat_map(|(ws_idx, ws)| {
+ let multi_tab = ws.tabs.len() > 1;
+ let workspace_label = ws.display_name();
+ ws.pane_details()
+ .into_iter()
+ .map(move |detail| AgentPanelEntry {
+ ws_idx,
+ tab_idx: detail.tab_idx,
+ pane_id: detail.pane_id,
+ primary_label: workspace_label.clone(),
+ primary_tab_label: multi_tab.then_some(detail.tab_label),
+ agent_label: Some(detail.agent_label),
+ state: detail.state,
+ seen: detail.seen,
+ })
+ })
+ .collect(),
+ }
+}
+
+fn truncate_text(text: &str, max_width: usize) -> String {
+ let len = text.chars().count();
+ if len <= max_width {
+ return text.to_string();
+ }
+ if max_width == 0 {
+ return String::new();
+ }
+ if max_width == 1 {
+ return "…".to_string();
+ }
+ let prefix: String = text.chars().take(max_width.saturating_sub(1)).collect();
+ format!("{prefix}…")
+}
+
+fn format_agent_panel_primary_label(entry: &AgentPanelEntry, max_width: usize) -> String {
+ let Some(tab_label) = entry.primary_tab_label.as_deref() else {
+ return truncate_text(&entry.primary_label, max_width);
+ };
+
+ let separator = " · ";
+ let separator_width = separator.chars().count();
+ if max_width <= separator_width + 2 {
+ return truncate_text(
+ &format!("{}{}{}", entry.primary_label, separator, tab_label),
+ max_width,
+ );
+ }
+
+ let available = max_width.saturating_sub(separator_width);
+ let min_tab = 4.min(available.saturating_sub(1)).max(1);
+ let preferred_workspace = ((available * 2) / 3).max(1);
+ let mut workspace_budget = preferred_workspace
+ .min(available.saturating_sub(min_tab))
+ .max(1);
+ let mut tab_budget = available.saturating_sub(workspace_budget);
+
+ let workspace_len = entry.primary_label.chars().count();
+ let tab_len = tab_label.chars().count();
+
+ if workspace_len < workspace_budget {
+ let spare = workspace_budget - workspace_len;
+ workspace_budget = workspace_len;
+ tab_budget = (tab_budget + spare).min(available.saturating_sub(workspace_budget));
+ }
+ if tab_len < tab_budget {
+ let spare = tab_budget - tab_len;
+ tab_budget = tab_len;
+ workspace_budget = (workspace_budget + spare).min(available.saturating_sub(tab_budget));
+ }
+
+ format!(
+ "{}{}{}",
+ truncate_text(&entry.primary_label, workspace_budget),
+ separator,
+ truncate_text(tab_label, tab_budget)
+ )
+}
+
/// Compute view geometry and reconcile pane sizes.
/// Called before render to separate mutation from drawing.
pub fn compute_view(app: &mut AppState, area: Rect) {
@@ -621,37 +798,13 @@ fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) {
buf[(sep_x, y)].set_style(sep_style);
}
- let content = Rect::new(area.x, area.y, area.width.saturating_sub(1), area.height);
-
- // Determine which workspace to show in the detail panel
- let detail_ws_idx = if is_navigating {
- Some(app.selected)
- } else {
- app.active
- };
-
- // Split sidebar in half: workspaces on top, agents on bottom
- let total_h = content.height as usize;
- let ws_h = (total_h + 1) / 2; // top half (ceiling)
- let detail_h = total_h.saturating_sub(ws_h);
-
- let ws_area = Rect::new(content.x, content.y, content.width, ws_h as u16);
- let detail_area = Rect::new(
- content.x,
- content.y + ws_h as u16,
- content.width,
- detail_h as u16,
- );
+ let (ws_area, detail_area) = expanded_sidebar_sections(area);
// --- Top section: Workspaces ---
render_workspace_list(app, frame, ws_area, is_navigating);
// --- Bottom section: Agent detail ---
- if let Some(ws_idx) = detail_ws_idx {
- if let Some(ws) = app.workspaces.get(ws_idx) {
- render_agent_detail(app, frame, detail_area, ws);
- }
- }
+ render_agent_detail(app, frame, detail_area);
render_sidebar_toggle(app, frame, area, false, p);
}
@@ -848,12 +1001,7 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig
}
/// Render the agent detail panel in the bottom section of the sidebar.
-fn render_agent_detail(
- app: &AppState,
- frame: &mut Frame,
- area: Rect,
- ws: &crate::workspace::Workspace,
-) {
+fn render_agent_detail(app: &AppState, frame: &mut Frame, area: Rect) {
let p = &app.palette;
if area.height < 3 {
@@ -870,7 +1018,6 @@ fn render_agent_detail(
);
row_y += 1;
- // Section header
frame.render_widget(
Paragraph::new(Line::from(vec![Span::styled(
" agents",
@@ -878,14 +1025,23 @@ fn render_agent_detail(
)])),
Rect::new(area.x, row_y, area.width, 1),
);
+ let toggle_rect = agent_panel_toggle_rect(area, app.agent_panel_scope);
+ if toggle_rect != Rect::default() {
+ frame.render_widget(
+ Paragraph::new(Span::styled(
+ agent_panel_toggle_label(app.agent_panel_scope),
+ Style::default().fg(p.overlay0).add_modifier(Modifier::BOLD),
+ ))
+ .alignment(Alignment::Right),
+ toggle_rect,
+ );
+ }
row_y += 1;
// Blank line for breathing room
row_y += 1;
- // Per-pane agent entries, sorted by urgency
- let details = ws.pane_details();
- for detail in &details {
+ for detail in &agent_panel_entries(app) {
if row_y + 1 >= area.y + area.height {
break;
}
@@ -896,12 +1052,15 @@ fn render_agent_detail(
let name_style = Style::default().fg(p.subtext0).add_modifier(Modifier::BOLD);
let status_style = Style::default().fg(label_color).add_modifier(Modifier::DIM);
+ let agent_style = Style::default().fg(p.overlay0).add_modifier(Modifier::DIM);
+ let primary_label =
+ format_agent_panel_primary_label(detail, area.width.saturating_sub(3) as usize);
let name_line = Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(icon, icon_style),
Span::styled(" ", Style::default()),
- Span::styled(&detail.label, name_style),
+ Span::styled(primary_label, name_style),
]);
frame.render_widget(
Paragraph::new(name_line),
@@ -913,12 +1072,16 @@ fn render_agent_detail(
break;
}
- let status_line = Line::from(vec![
+ let mut status_spans = vec![
Span::styled(" ", Style::default()),
Span::styled(label, status_style),
- ]);
+ ];
+ if let Some(agent_label) = &detail.agent_label {
+ status_spans.push(Span::styled(" · ", agent_style));
+ status_spans.push(Span::styled(agent_label, agent_style));
+ }
frame.render_widget(
- Paragraph::new(status_line),
+ Paragraph::new(Line::from(status_spans)),
Rect::new(area.x, row_y, area.width, 1),
);
row_y += 1;
@@ -2818,6 +2981,59 @@ fn _build_hints(items: &[(&str, &str)], key_style: Style, dim_style: Style) -> V
#[cfg(test)]
mod tests {
use super::*;
+ use crate::{detect::Agent, workspace::Workspace};
+
+ #[test]
+ fn all_workspaces_agent_panel_entries_use_workspace_and_optional_tab_labels() {
+ let mut app = crate::app::state::AppState::test_new();
+ let mut first = Workspace::test_new("one");
+ let first_pane = first.tabs[0].root_pane;
+ first.tabs[0]
+ .panes
+ .get_mut(&first_pane)
+ .unwrap()
+ .detected_agent = Some(Agent::Pi);
+
+ let mut second = Workspace::test_new("two");
+ let second_tab = second.test_add_tab(Some("logs"));
+ let second_pane = second.tabs[second_tab].root_pane;
+ second.tabs[second_tab]
+ .panes
+ .get_mut(&second_pane)
+ .unwrap()
+ .detected_agent = Some(Agent::Claude);
+
+ app.workspaces = vec![first, second];
+ app.active = Some(0);
+ app.selected = 0;
+ app.agent_panel_scope = AgentPanelScope::AllWorkspaces;
+
+ let entries = agent_panel_entries(&app);
+ assert_eq!(entries[0].primary_label, "one");
+ assert!(entries[0].primary_tab_label.is_none());
+ assert_eq!(entries[0].agent_label.as_deref(), Some("pi"));
+ assert_eq!(entries[1].primary_label, "two");
+ assert_eq!(entries[1].primary_tab_label.as_deref(), Some("logs"));
+ assert_eq!(entries[1].agent_label.as_deref(), Some("claude"));
+ }
+
+ #[test]
+ fn all_workspaces_primary_label_truncates_workspace_and_tab() {
+ let entry = AgentPanelEntry {
+ ws_idx: 0,
+ tab_idx: 0,
+ pane_id: crate::layout::PaneId::from_raw(1),
+ primary_label: "agent-browser".into(),
+ primary_tab_label: Some("test-escalation".into()),
+ agent_label: Some("claude".into()),
+ state: AgentState::Idle,
+ seen: true,
+ };
+
+ let label = format_agent_panel_primary_label(&entry, 18);
+
+ assert_eq!(label, "agent-bro… · test…");
+ }
#[test]
fn pane_scrollbar_rect_uses_rightmost_inner_column() {