use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Paragraph, Widget};
use ratatui::Frame;
use crate::colors::{Palette, SemanticRole};
use crate::config::StatePresentations;
use crate::model::{
AgentDetailTone, AppModel, GitSummaryRow, PaneRow, ResourceUsage, WorkspaceGroup,
};
const FOOTER_BRAND: &str = "🅸 🅻 🅼 🅰 🆁 🅸 ";
const FOOTER_BRAND_WIDTH: u16 = 18;
const FOOTER_COMPACT_BREAKPOINT: u16 = 80;
const TIME_COL_WIDTH: usize = 5;
const OUTPUT_COL_WIDTH: usize = 80;
const PANE_INLINE_WIDTH: usize = 4;
const AGENT_COL_WIDTH: usize = 12;
const DETAIL_COL_WIDTH: usize = 24;
const SELF_COL_WIDTH: usize = 14;
const SUB_COL_WIDTH: usize = 14;
const STATUS_COL_WIDTH: usize = 1;
pub fn render(frame: &mut Frame, model: &AppModel, palette: &Palette, states: &StatePresentations) {
let sections =
Layout::vertical([Constraint::Min(0), Constraint::Length(1), Constraint::Length(1)])
.split(frame.area());
frame.render_widget(AppView::new(model, palette, states), sections[0]);
frame.render_widget(FooterView::new(palette), sections[2]);
}
struct AppView<'a> {
model: &'a AppModel,
palette: &'a Palette,
states: &'a StatePresentations,
}
impl<'a> AppView<'a> {
fn new(model: &'a AppModel, palette: &'a Palette, states: &'a StatePresentations) -> Self {
Self { model, palette, states }
}
}
impl Widget for AppView<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
Block::default().style(self.palette.base_style()).render(area, buf);
let status_height = u16::from(!self.model.status_line.is_empty());
let sections =
Layout::vertical([Constraint::Length(status_height), Constraint::Min(0)]).split(area);
if status_height > 0 {
Paragraph::new(self.model.status_line.as_str())
.style(self.palette.base_style())
.render(sections[0], buf);
}
Paragraph::new(workspace_lines(
self.model,
self.palette,
self.states,
sections[1].width as usize,
))
.style(self.palette.base_style())
.render(sections[1], buf);
}
}
struct FooterView<'a> {
palette: &'a Palette,
}
impl<'a> FooterView<'a> {
fn new(palette: &'a Palette) -> Self {
Self { palette }
}
}
impl Widget for FooterView<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
render_footer(area, buf, self.palette);
}
}
fn workspace_lines(
model: &AppModel,
palette: &Palette,
states: &StatePresentations,
available_width: usize,
) -> Vec<Line<'static>> {
if model.workspace_groups.is_empty() {
return vec![
Line::from(Span::styled("none", palette.style_for(SemanticRole::HeadingAccent))),
Line::from(Span::styled(
" no supported agent sessions detected",
palette.style_for(SemanticRole::MutedText),
)),
];
}
let mut lines = Vec::new();
for (index, group) in model.workspace_groups.iter().enumerate() {
if index > 0 {
lines.push(Line::from(""));
}
lines.extend(workspace_header_lines(group, palette, model.show_git, available_width));
for row in &group.rows {
lines.push(workspace_row_line_with_attention(
row,
palette,
states,
model.show_app,
model.show_attention,
model.show_detail,
model.show_time,
model.show_output,
model.show_stats,
available_width,
));
if row.subtasks_expanded {
lines.extend(subtask_lines(
row,
palette,
model.show_app,
model.show_attention,
model.show_detail,
model.show_time,
model.show_output,
model.show_stats,
));
}
}
}
lines
}
fn workspace_header_lines(
group: &WorkspaceGroup,
palette: &Palette,
show_git: bool,
available_width: usize,
) -> Vec<Line<'static>> {
let mut lines = vec![workspace_header_line(group, palette, available_width)];
if show_git && group.git_summary.is_some() {
lines.extend(workspace_git_summary_lines(group, palette, available_width));
}
lines
}
fn workspace_header_line(
group: &WorkspaceGroup,
palette: &Palette,
available_width: usize,
) -> Line<'static> {
workspace_label_line(group, palette, available_width)
}
fn workspace_label_line(
group: &WorkspaceGroup,
palette: &Palette,
available_width: usize,
) -> Line<'static> {
let label_style =
palette.style_for(SemanticRole::HeadingAccent).add_modifier(Modifier::REVERSED);
let label = fit_cell(&group.label, available_width.max(group.label.chars().count()));
Line::from(vec![Span::styled(label, label_style)])
}
fn workspace_git_summary_lines(
group: &WorkspaceGroup,
palette: &Palette,
available_width: usize,
) -> Vec<Line<'static>> {
let Some(summary) = &group.git_summary else {
return Vec::new();
};
let stats_text = format_git_stats_text(summary);
let combined_width = summary.branch_name.chars().count() + 2 + stats_text.chars().count();
if combined_width > available_width {
return vec![
Line::from(vec![Span::styled(
summary.branch_name.clone(),
palette.style_for(SemanticRole::MutedText),
)]),
workspace_git_stats_line(summary, palette),
];
}
vec![Line::from(vec![
Span::styled(summary.branch_name.clone(), palette.style_for(SemanticRole::MutedText)),
Span::raw(" "),
Span::styled(
format!("+{}", summary.insertions),
palette.style_for(SemanticRole::GitInsertions),
),
Span::raw(" "),
Span::styled(
format!("-{}", summary.deletions),
palette.style_for(SemanticRole::GitDeletions),
),
])]
}
fn workspace_git_stats_line(summary: &GitSummaryRow, palette: &Palette) -> Line<'static> {
Line::from(vec![
Span::styled(
format!("+{}", summary.insertions),
palette.style_for(SemanticRole::GitInsertions),
),
Span::raw(" "),
Span::styled(
format!("-{}", summary.deletions),
palette.style_for(SemanticRole::GitDeletions),
),
])
}
fn format_git_stats_text(summary: &GitSummaryRow) -> String {
format!("+{} -{}", summary.insertions, summary.deletions)
}
fn push_cell(
spans: &mut Vec<Span<'static>>,
current_width: &mut usize,
content: String,
style: Style,
) {
if !spans.is_empty() {
push_raw_spaces(spans, current_width, 1);
}
*current_width += content.chars().count();
spans.push(Span::styled(content, style));
}
fn push_inline_span(
spans: &mut Vec<Span<'static>>,
current_width: &mut usize,
content: String,
style: Style,
) {
*current_width += content.chars().count();
spans.push(Span::styled(content, style));
}
fn push_raw_spaces(spans: &mut Vec<Span<'static>>, current_width: &mut usize, count: usize) {
if count == 0 {
return;
}
*current_width += count;
spans.push(Span::raw(" ".repeat(count)));
}
#[allow(clippy::too_many_arguments)]
fn workspace_row_line_with_attention(
row: &PaneRow,
palette: &Palette,
states: &StatePresentations,
show_app: bool,
show_attention: bool,
show_detail: bool,
show_time: bool,
show_output: bool,
show_stats: bool,
available_width: usize,
) -> Line<'static> {
let presentation = states.for_status(row.status);
let status_style = palette.style_for_color(&presentation.color);
let pane_label_style = pane_label_style(row, palette);
let mut spans = Vec::new();
let mut current_width = 0;
if show_time {
let time_label = if row.inactive_since_label.is_empty() {
" ".repeat(TIME_COL_WIDTH)
} else {
truncate_cell(&row.inactive_since_label, TIME_COL_WIDTH)
};
push_inline_span(
&mut spans,
&mut current_width,
time_label,
palette.style_for(SemanticRole::MutedText),
);
push_raw_spaces(&mut spans, &mut current_width, 1);
}
if show_attention {
push_inline_span(
&mut spans,
&mut current_width,
if row.attention { states.attention.icon.clone() } else { " ".to_string() },
palette.style_for_color(&states.attention.color),
);
push_raw_spaces(&mut spans, &mut current_width, 1);
}
push_inline_span(
&mut spans,
&mut current_width,
truncate_cell(&presentation.icon, STATUS_COL_WIDTH),
status_style,
);
push_raw_spaces(&mut spans, &mut current_width, 1);
push_inline_span(&mut spans, &mut current_width, row.pane_id.clone(), pane_label_style);
push_raw_spaces(
&mut spans,
&mut current_width,
PANE_INLINE_WIDTH.saturating_sub(row.pane_id.chars().count()),
);
if show_app {
push_cell(
&mut spans,
&mut current_width,
truncate_cell(row.client_label, AGENT_COL_WIDTH),
palette.style_for(SemanticRole::AppLabel).add_modifier(Modifier::BOLD),
);
}
if show_detail {
let (label, style) = match &row.detail {
Some(detail) => (
truncate_cell(&detail.label, DETAIL_COL_WIDTH),
palette.style_for(detail_role(detail.tone)),
),
None => (String::new(), palette.style_for(SemanticRole::MutedText)),
};
push_cell(&mut spans, &mut current_width, label, style);
}
if show_stats {
if let Some(process_usage) = row.process_usage.as_ref() {
push_cell(
&mut spans,
&mut current_width,
truncate_cell(&format_usage_chip(process_usage.agent), SELF_COL_WIDTH),
palette.style_for(SemanticRole::HeadingAccent).add_modifier(Modifier::BOLD),
);
push_cell(
&mut spans,
&mut current_width,
truncate_cell(&format_usage_chip(process_usage.spawned), SUB_COL_WIDTH),
palette.style_for(SemanticRole::AgentDetailNeutral),
);
} else {
push_cell(
&mut spans,
&mut current_width,
String::new(),
palette.style_for(SemanticRole::MutedText),
);
push_cell(
&mut spans,
&mut current_width,
String::new(),
palette.style_for(SemanticRole::MutedText),
);
}
}
if show_output {
push_cell(
&mut spans,
&mut current_width,
truncate_cell(row.output_excerpt.as_deref().unwrap_or_default(), OUTPUT_COL_WIDTH),
palette.base_style(),
);
}
if row.is_selected {
let selected_style = selected_row_style(palette);
for span in &mut spans {
span.style = span.style.patch(selected_style);
}
let trailing_width = available_width.saturating_sub(current_width);
push_inline_span(
&mut spans,
&mut current_width,
" ".repeat(trailing_width),
selected_style,
);
return Line::from(spans).style(selected_style);
}
Line::from(spans)
}
#[cfg(test)]
#[allow(clippy::too_many_arguments)]
fn workspace_row_line(
row: &PaneRow,
palette: &Palette,
states: &StatePresentations,
show_app: bool,
show_detail: bool,
show_time: bool,
show_output: bool,
show_stats: bool,
) -> Line<'static> {
workspace_row_line_with_attention(
row,
palette,
states,
show_app,
false,
show_detail,
show_time,
show_output,
show_stats,
0,
)
}
fn pane_label_style(row: &PaneRow, palette: &Palette) -> Style {
let style = palette.style_for(SemanticRole::HeadingAccent);
if row.is_jump_match {
style.add_modifier(Modifier::BOLD)
} else {
style
}
}
#[allow(clippy::too_many_arguments)]
fn subtask_lines(
row: &PaneRow,
palette: &Palette,
show_app: bool,
show_attention: bool,
show_detail: bool,
show_time: bool,
show_output: bool,
show_stats: bool,
) -> Vec<Line<'static>> {
let Some(process_usage) = row.process_usage.as_ref() else {
return Vec::new();
};
process_usage
.subtasks
.iter()
.map(|subtask| {
let detail_cell = format!(
"{}- {} #{}",
" ".repeat(subtask.depth + 1),
subtask.command_label,
subtask.pid
);
let mut spans = Vec::new();
let mut current_width = 0;
push_raw_spaces(
&mut spans,
&mut current_width,
prefix_cluster_width(show_time, show_attention),
);
if show_app {
push_cell(
&mut spans,
&mut current_width,
String::new(),
palette.style_for(SemanticRole::MutedText),
);
}
if show_detail {
push_cell(
&mut spans,
&mut current_width,
truncate_cell(&detail_cell, DETAIL_COL_WIDTH),
palette.style_for(SemanticRole::MutedText),
);
}
if show_stats {
push_cell(
&mut spans,
&mut current_width,
String::new(),
palette.style_for(SemanticRole::MutedText),
);
push_cell(
&mut spans,
&mut current_width,
truncate_cell(&format_usage_chip(subtask.usage), SUB_COL_WIDTH),
palette.style_for(SemanticRole::MutedText),
);
}
if show_output {
push_cell(
&mut spans,
&mut current_width,
if show_detail {
String::new()
} else {
truncate_cell(&detail_cell, OUTPUT_COL_WIDTH)
},
palette.style_for(SemanticRole::MutedText),
);
}
Line::from(spans)
})
.collect()
}
fn prefix_cluster_width(show_time: bool, show_attention: bool) -> usize {
let time_width = if show_time { TIME_COL_WIDTH + 1 } else { 0 };
let attention_width = if show_attention { STATUS_COL_WIDTH + 1 } else { 0 };
time_width + attention_width + STATUS_COL_WIDTH + 1 + PANE_INLINE_WIDTH
}
fn selected_row_style(palette: &Palette) -> Style {
let highlight = palette
.style_for(SemanticRole::MutedText)
.fg
.expect("muted text always has a foreground color");
Style::default().bg(highlight).add_modifier(Modifier::BOLD)
}
fn render_footer(area: Rect, buf: &mut Buffer, palette: &Palette) {
Block::default().style(palette.base_style()).render(area, buf);
let key_style = palette.style_for(SemanticRole::HeadingAccent).add_modifier(Modifier::BOLD);
let sep_style = palette.style_for(SemanticRole::MutedText);
let footer = if area.width < FOOTER_COMPACT_BREAKPOINT {
compact_footer_line(key_style, sep_style)
} else {
footer_line(key_style, sep_style)
};
let left = Paragraph::new(footer).alignment(Alignment::Left).style(palette.base_style());
if area.width > FOOTER_BRAND_WIDTH {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(0), Constraint::Length(FOOTER_BRAND_WIDTH)])
.split(area);
left.render(chunks[0], buf);
Paragraph::new(FOOTER_BRAND)
.alignment(Alignment::Right)
.style(key_style)
.render(chunks[1], buf);
} else {
left.render(area, buf);
}
}
fn footer_line(key_style: Style, sep_style: Style) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
let push_sep = |spans: &mut Vec<Span<'static>>| {
if !spans.is_empty() {
spans.push(Span::styled(" | ", sep_style));
}
};
let push_item = |spans: &mut Vec<Span<'static>>, label: &'static str, key: &'static str| {
push_sep(spans);
spans.push(Span::styled(label, sep_style));
spans.push(Span::raw(":"));
spans.push(Span::styled(key, key_style));
};
push_item(&mut spans, "Move", "j/k");
push_item(&mut spans, "Pane", "%");
push_item(&mut spans, "App", "a");
push_item(&mut spans, "Bell", "b");
push_item(&mut spans, "Git", "g");
push_item(&mut spans, "Model", "m");
push_item(&mut spans, "Attention", "n");
push_item(&mut spans, "Time", "t");
push_item(&mut spans, "Output", "o");
push_item(&mut spans, "Stats", "s");
push_item(&mut spans, "Reset", "R");
push_item(&mut spans, "Subs", "=");
push_item(&mut spans, "Jump", "Enter");
push_item(&mut spans, "Quit", "q/Esc");
Line::from(spans)
}
fn compact_footer_line(key_style: Style, sep_style: Style) -> Line<'static> {
let mut spans: Vec<Span<'static>> = Vec::new();
let push_sep = |spans: &mut Vec<Span<'static>>| {
if !spans.is_empty() {
spans.push(Span::styled(" ", sep_style));
}
};
let push_key = |spans: &mut Vec<Span<'static>>, key: &'static str| {
push_sep(spans);
spans.push(Span::styled(key, key_style));
};
for key in ["j", "k", "%", "a", "b", "g", "m", "n", "t", "o", "s", "R", "=", "q"] {
push_key(&mut spans, key);
}
Line::from(spans)
}
fn detail_role(tone: AgentDetailTone) -> SemanticRole {
match tone {
AgentDetailTone::Neutral => SemanticRole::AgentDetailNeutral,
AgentDetailTone::AmpDeep => SemanticRole::AgentDetailAmpDeep,
AgentDetailTone::AmpSmart => SemanticRole::AgentDetailAmpSmart,
AgentDetailTone::AmpRush => SemanticRole::AgentDetailAmpRush,
}
}
fn format_cpu(usage: ResourceUsage) -> String {
format!("{}.{}%", usage.cpu_tenths_percent / 10, usage.cpu_tenths_percent % 10)
}
fn format_memory(usage: ResourceUsage) -> String {
format_memory_kib(usage.memory_kib)
}
fn format_resource_usage(usage: ResourceUsage) -> String {
format!("{}/{}", format_cpu(usage), format_memory(usage))
}
fn format_usage_chip(usage: ResourceUsage) -> String {
format!("[{}]", format_resource_usage(usage))
}
fn fit_cell(value: &str, width: usize) -> String {
let truncated = truncate_cell(value, width);
let pad = width.saturating_sub(truncated.chars().count());
format!("{truncated}{}", " ".repeat(pad))
}
fn truncate_cell(value: &str, width: usize) -> String {
let char_count = value.chars().count();
if char_count <= width {
return value.to_string();
}
if width <= 3 {
return ".".repeat(width);
}
let mut truncated = value.chars().take(width - 3).collect::<String>();
truncated.push_str("...");
truncated
}
fn format_memory_kib(memory_kib: u64) -> String {
const KIB_PER_MIB: u64 = 1024;
const KIB_PER_GIB: u64 = 1024 * 1024;
if memory_kib < KIB_PER_MIB {
return format!("{memory_kib}K");
}
if memory_kib < KIB_PER_GIB {
return format!("{}M", (memory_kib + (KIB_PER_MIB / 2)) / KIB_PER_MIB);
}
let gib_tenths = (memory_kib.saturating_mul(10) + (KIB_PER_GIB / 2)) / KIB_PER_GIB;
format!("{}.{}G", gib_tenths / 10, gib_tenths % 10)
}
#[cfg(test)]
mod tests {
use super::{
compact_footer_line, footer_line, format_usage_chip, workspace_header_line,
workspace_header_lines, workspace_row_line, workspace_row_line_with_attention,
FOOTER_BRAND, FOOTER_COMPACT_BREAKPOINT,
};
use crate::colors::{ColorSpec, Palette, SemanticRole};
use crate::config::{LoadedConfig, StatePresentation, StatePresentations};
use crate::model::{
AgentDetail, AgentDetailTone, AppModel, GitSummaryRow, PaneRow, ResourceUsage,
SessionProcessUsage, SessionStatus, SubtaskProcess, WorkspaceGroup,
};
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use ratatui::style::{Color, Modifier};
use ratatui::Terminal;
use std::fs;
use std::path::PathBuf;
use std::time::{Duration, Instant, SystemTime};
#[test]
fn workspace_row_uses_semantic_status_color_when_not_selected() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: "14:27".to_string(),
output_excerpt: Some(
"Could you clarify what you'd like me to help with?".to_string().into(),
),
client_label: "Codex",
detail: Some(
AgentDetail {
label: "gpt-5.4 xhigh fast".to_string(),
tone: AgentDetailTone::Neutral,
}
.into(),
),
process_usage: Some(
SessionProcessUsage {
agent: ResourceUsage { cpu_tenths_percent: 154, memory_kib: 64 * 1024 },
spawned: ResourceUsage { cpu_tenths_percent: 8, memory_kib: 12 * 1024 },
subtasks: vec![SubtaskProcess {
pid: 102,
depth: 0,
command_label: "tmux-mcp-rs".to_string(),
usage: ResourceUsage { cpu_tenths_percent: 8, memory_kib: 12 * 1024 },
}],
}
.into(),
),
subtasks_expanded: false,
status: SessionStatus::WaitingInput,
attention: false,
status_label: "waiting-input",
is_jump_match: false,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
true,
true,
true,
true,
true,
);
let time_cell = "14:27";
let output_cell = "Could you clarify what you'd like me to help with?";
let self_usage_label =
format_usage_chip(row.process_usage.as_ref().expect("usage should exist").agent);
let spawned_usage_label =
format_usage_chip(row.process_usage.as_ref().expect("usage should exist").spawned);
let detail_label = "gpt-5.4 xhigh fast";
let self_usage_cell = self_usage_label.as_str();
let spawned_usage_cell = spawned_usage_label.as_str();
let status_cell = "●";
let app_cell = "Codex";
let detail_span = line
.spans
.iter()
.find(|span| span.content.as_ref() == detail_label)
.expect("detail span");
let output_span = line
.spans
.iter()
.find(|span| span.content.as_ref() == output_cell)
.expect("output span");
let time_span =
line.spans.iter().find(|span| span.content.as_ref() == time_cell).expect("time span");
let self_usage_span = line
.spans
.iter()
.find(|span| span.content.as_ref() == self_usage_cell)
.expect("self usage span");
let spawned_usage_span = line
.spans
.iter()
.find(|span| span.content.as_ref() == spawned_usage_cell)
.expect("spawned usage span");
let status_span = line
.spans
.iter()
.find(|span| span.content.as_ref() == status_cell)
.expect("status span");
let app_span =
line.spans.iter().find(|span| span.content.as_ref() == app_cell).expect("app span");
let pane_span =
line.spans.iter().find(|span| span.content.as_ref() == "%7").expect("pane span");
assert_eq!(detail_span.style.fg, Some(Color::Indexed(8)));
assert_eq!(output_span.style.fg, palette.base_style().fg);
assert_eq!(time_span.style.fg, Some(Color::Indexed(8)));
assert_eq!(self_usage_span.style.fg, Some(Color::Indexed(12)));
assert!(self_usage_span.style.add_modifier.contains(Modifier::BOLD));
assert_eq!(spawned_usage_span.style.fg, Some(Color::Indexed(8)));
assert_eq!(status_span.style.fg, Some(Color::Indexed(3)));
assert_eq!(app_span.style.fg, Some(Color::Indexed(14)));
assert!(app_span.style.add_modifier.contains(Modifier::BOLD));
assert_eq!(pane_span.style.fg, Some(Color::Indexed(12)));
assert!(!pane_span.style.add_modifier.contains(Modifier::BOLD));
assert!(!line.style.add_modifier.contains(Modifier::BOLD));
assert!(!line.style.add_modifier.contains(Modifier::REVERSED));
}
#[test]
fn attention_cell_uses_the_shared_icon_and_color_before_the_lifecycle_cell() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::WaitingInput,
attention: true,
status_label: "waiting-input",
is_jump_match: false,
is_selected: false,
};
let states = StatePresentations {
attention: StatePresentation {
icon: "!".to_string(),
color: ColorSpec::parse("palette:yellow").expect("valid color"),
},
..StatePresentations::default()
};
let line = workspace_row_line_with_attention(
&row,
&Palette::default(),
&states,
false,
true,
false,
false,
false,
false,
0,
);
let attention = line
.spans
.iter()
.position(|span| span.content.as_ref() == "!")
.expect("attention cell");
let lifecycle = line
.spans
.iter()
.position(|span| span.content.as_ref() == "●")
.expect("lifecycle cell");
assert!(attention < lifecycle);
assert_eq!(line.spans[attention].style.fg, Some(Color::Indexed(3)));
let normal = PaneRow { attention: false, ..row };
let line = workspace_row_line_with_attention(
&normal,
&Palette::default(),
&states,
false,
true,
false,
false,
false,
false,
0,
);
assert!(line.spans.iter().any(|span| span.content.as_ref() == " "));
assert!(!line.spans.iter().any(|span| span.content.as_ref() == "!"));
}
#[test]
fn shared_state_presentation_controls_popup_icon_and_color() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: true,
};
let states = StatePresentations {
running: StatePresentation {
icon: "R".to_string(),
color: ColorSpec::parse("#123456").expect("valid state color"),
},
..StatePresentations::default()
};
let palette = Palette::default();
let line = workspace_row_line(&row, &palette, &states, false, false, false, false, false);
let status_span = line
.spans
.iter()
.find(|span| span.content.as_ref() == "R")
.expect("configured state icon");
assert_eq!(status_span.style.fg, Some(Color::Rgb(0x12, 0x34, 0x56)));
assert_eq!(
status_span.style.bg,
palette.style_for(SemanticRole::MutedText).fg,
"selection changes the background, not the configured state foreground"
);
}
#[test]
fn custom_shared_config_drives_popup_and_tmux_presentations() {
let path = std::env::temp_dir().join(format!(
"ilmari-ui-states-{}-{}.toml",
std::process::id(),
Instant::now().elapsed().as_nanos()
));
fs::write(
&path,
r##"
[states.running]
icon = "R"
color = "#123456"
[states.attention]
icon = "!"
color = "palette:yellow"
"##,
)
.expect("write shared state config");
let config = LoadedConfig::load_from_path(&path).expect("load shared state config").values;
fs::remove_file(&path).expect("remove shared state config");
let states = config.states.as_ref().expect("states configured");
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
};
let line = workspace_row_line(
&row,
&Palette::default(),
states,
false,
false,
false,
false,
false,
);
let popup_status = line
.spans
.iter()
.find(|span| span.content.as_ref() == "R")
.expect("popup uses configured running icon");
assert_eq!(popup_status.style.fg, Some(Color::Rgb(0x12, 0x34, 0x56)));
let tmux = crate::tmux_state::RenderSettings::from_config(
&config.palette,
&config.badges,
&config.status,
config.states.as_ref(),
config.legacy_badge_state_formats,
config.legacy_status_state_formats,
false,
);
assert_eq!(tmux.badge_running.symbol, "R");
assert_eq!(tmux.badge_running.style, "fg=#123456");
assert_eq!(tmux.badge_attention.symbol, "!");
}
#[test]
fn selected_workspace_row_uses_one_highlight_background_and_keeps_glyph_colors() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: "14:27".to_string(),
output_excerpt: Some(
"Could you clarify what you'd like me to help with?".to_string().into(),
),
client_label: "Codex",
detail: Some(
AgentDetail {
label: "gpt-5.4 xhigh fast".to_string(),
tone: AgentDetailTone::Neutral,
}
.into(),
),
process_usage: Some(
SessionProcessUsage {
agent: ResourceUsage { cpu_tenths_percent: 154, memory_kib: 64 * 1024 },
spawned: ResourceUsage { cpu_tenths_percent: 8, memory_kib: 12 * 1024 },
subtasks: vec![SubtaskProcess {
pid: 102,
depth: 0,
command_label: "tmux-mcp-rs".to_string(),
usage: ResourceUsage { cpu_tenths_percent: 8, memory_kib: 12 * 1024 },
}],
}
.into(),
),
subtasks_expanded: false,
status: SessionStatus::WaitingInput,
attention: false,
status_label: "waiting-input",
is_jump_match: true,
is_selected: true,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
true,
true,
true,
true,
true,
);
let highlight = palette
.style_for(SemanticRole::MutedText)
.fg
.expect("muted text has a foreground color");
assert!(line.style.add_modifier.contains(Modifier::BOLD));
assert_eq!(line.style.bg, Some(highlight));
assert!(!line.style.add_modifier.contains(Modifier::REVERSED));
assert!(line.spans.iter().all(|span| span.style.add_modifier.contains(Modifier::BOLD)));
assert!(line.spans.iter().all(|span| span.style.bg == Some(highlight)));
let status_span =
line.spans.iter().find(|span| span.content.as_ref() == "●").expect("status span");
assert_eq!(status_span.style.fg, Some(Color::Indexed(3)));
assert_eq!(status_span.style.bg, Some(highlight));
}
#[test]
fn selected_workspace_row_fills_the_available_viewport_width() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::WaitingInput,
attention: true,
status_label: "waiting-input",
is_jump_match: false,
is_selected: true,
};
let palette = Palette::default();
let line = workspace_row_line_with_attention(
&row,
&palette,
&StatePresentations::default(),
false,
true,
false,
false,
false,
false,
40,
);
let text = line.spans.iter().map(|span| span.content.as_ref()).collect::<String>();
let highlight = palette
.style_for(SemanticRole::MutedText)
.fg
.expect("muted text has a foreground color");
assert_eq!(text.chars().count(), 40);
assert!(line.spans.iter().all(|span| span.style.bg == Some(highlight)));
let attention =
line.spans.iter().find(|span| span.content.as_ref() == "?").expect("attention glyph");
assert_eq!(attention.style.fg, Some(Color::Indexed(3)));
}
#[test]
fn workspace_row_places_time_before_status_and_pane_when_visible() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: "14:27".to_string(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::WaitingInput,
attention: false,
status_label: "waiting-input",
is_jump_match: false,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
false,
false,
true,
false,
false,
);
let time_cell = "14:27";
let status_cell = "●";
let pane_cell = "%7";
let time_position = line
.spans
.iter()
.position(|span| span.content.as_ref() == time_cell)
.expect("time span");
let status_position = line
.spans
.iter()
.position(|span| span.content.as_ref() == status_cell)
.expect("status span");
let pane_position = line
.spans
.iter()
.position(|span| span.content.as_ref() == pane_cell)
.expect("pane span");
assert!(time_position < status_position);
assert!(status_position < pane_position);
}
#[test]
fn workspace_row_keeps_blank_time_slot_for_running_rows() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
false,
false,
true,
false,
false,
);
assert_eq!(line.spans[0].content.as_ref(), " ");
assert_eq!(line.spans[1].content.as_ref(), " ");
assert_eq!(line.spans[2].content.as_ref(), "▶");
}
#[test]
fn workspace_row_uses_detail_tone_semantics_for_amp_mode() {
let row = PaneRow {
pane_id: "%8".to_string(),
inactive_since_label: String::new(),
output_excerpt: Some(
"Could you clarify what you'd like me to help with?".to_string().into(),
),
client_label: "Amp",
detail: Some(
AgentDetail { label: "smart".to_string(), tone: AgentDetailTone::AmpSmart }.into(),
),
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
true,
true,
false,
true,
false,
);
let detail_span =
line.spans.iter().find(|span| span.content.as_ref() == "smart").expect("detail span");
assert_eq!(detail_span.style.fg, Some(Color::Indexed(10)));
}
#[test]
fn workspace_row_renders_output_after_app_detail_and_stats() {
let row = PaneRow {
pane_id: "%8".to_string(),
inactive_since_label: "09:15".to_string(),
output_excerpt: Some("Final visible output excerpt".to_string().into()),
client_label: "Amp",
detail: Some(
AgentDetail { label: "smart".to_string(), tone: AgentDetailTone::AmpSmart }.into(),
),
process_usage: Some(
SessionProcessUsage {
agent: ResourceUsage { cpu_tenths_percent: 21, memory_kib: 48 * 1024 },
spawned: ResourceUsage { cpu_tenths_percent: 0, memory_kib: 3 * 1024 },
subtasks: Vec::new(),
}
.into(),
),
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
true,
true,
true,
true,
true,
);
let app_position =
line.spans.iter().position(|span| span.content.as_ref() == "Amp").expect("app span");
let detail_position = line
.spans
.iter()
.position(|span| span.content.as_ref() == "smart")
.expect("detail span");
let stats_position = line
.spans
.iter()
.position(|span| span.content.as_ref() == "[2.1%/48M]")
.expect("stats span");
let output_position = line
.spans
.iter()
.position(|span| span.content.as_ref() == "Final visible output excerpt")
.expect("output span");
assert!(app_position < detail_position);
assert!(detail_position < stats_position);
assert!(stats_position < output_position);
}
#[test]
fn workspace_row_highlights_jump_matches() {
let row = PaneRow {
pane_id: "%19".to_string(),
inactive_since_label: "09:15".to_string(),
output_excerpt: None,
client_label: "Amp",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::WaitingInput,
attention: false,
status_label: "waiting-input",
is_jump_match: true,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
false,
false,
false,
false,
false,
);
let pane_cell = "%19";
let pane_span =
line.spans.iter().find(|span| span.content.as_ref() == pane_cell).expect("pane span");
assert_eq!(line.spans[0].content.as_ref(), "●");
assert_eq!(pane_span.style.fg, Some(Color::Indexed(12)));
assert!(pane_span.style.add_modifier.contains(Modifier::BOLD));
}
#[test]
fn workspace_header_uses_second_line_for_git_summary_with_semantic_colors() {
let group = WorkspaceGroup {
label: "api".to_string(),
git_summary: Some(GitSummaryRow {
workspace_path: PathBuf::from("/tmp/api"),
workspace_label: "api".to_string(),
branch_name: "main".to_string(),
insertions: 3,
deletions: 1,
}),
rows: Vec::new(),
};
let palette = Palette::default();
let lines = workspace_header_lines(&group, &palette, true, 120);
let buffer = Buffer::with_lines(lines.clone());
let insert_x = "main ".len() as u16;
let delete_x = insert_x + "+3 ".len() as u16;
let header_text =
lines[0].spans.iter().map(|span| span.content.as_ref()).collect::<String>();
assert_eq!(lines.len(), 2);
assert_eq!(header_text.len(), 120);
assert!(header_text.starts_with("api"));
assert_eq!(lines[0].spans[0].style.fg, Some(Color::Indexed(12)));
assert!(lines[0].spans[0].style.add_modifier.contains(Modifier::REVERSED));
assert_eq!(lines[1].spans[0].style.fg, Some(Color::Indexed(8)));
assert_eq!(lines[1].spans[2].style.fg, Some(Color::Indexed(2)));
assert_eq!(lines[1].spans[4].style.fg, Some(Color::Indexed(1)));
assert_eq!(buffer.cell((insert_x, 1)).expect("insertions cell").fg, Color::Indexed(2));
assert_eq!(buffer.cell((delete_x, 1)).expect("deletions cell").fg, Color::Indexed(1));
}
#[test]
fn workspace_header_hides_git_summary_when_disabled() {
let group = WorkspaceGroup {
label: "api".to_string(),
git_summary: Some(GitSummaryRow {
workspace_path: PathBuf::from("/tmp/api"),
workspace_label: "api".to_string(),
branch_name: "main".to_string(),
insertions: 3,
deletions: 1,
}),
rows: Vec::new(),
};
let palette = Palette::default();
let line = workspace_header_line(&group, &palette, 10);
let text = line.spans.iter().map(|span| span.content.as_ref()).collect::<String>();
assert_eq!(text.trim_end(), "api");
assert_eq!(text.len(), 10);
}
#[test]
fn workspace_header_moves_git_stats_to_new_line_when_branch_is_long() {
let group = WorkspaceGroup {
label: "api".to_string(),
git_summary: Some(GitSummaryRow {
workspace_path: PathBuf::from("/tmp/api"),
workspace_label: "api".to_string(),
branch_name: "feature/very-long-branch".to_string(),
insertions: 3,
deletions: 1,
}),
rows: Vec::new(),
};
let palette = Palette::default();
let lines = workspace_header_lines(&group, &palette, true, 10);
let buffer = Buffer::with_lines(lines.clone());
assert_eq!(lines.len(), 3);
let header_text =
lines[0].spans.iter().map(|span| span.content.as_ref()).collect::<String>();
assert_eq!(header_text.len(), 10);
assert_eq!(header_text.trim_end(), "api");
assert!(lines[1]
.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>()
.contains("feature/very-long-branch"));
assert_eq!(
lines[2].spans.iter().map(|span| span.content.as_ref()).collect::<String>(),
"+3 -1"
);
assert_eq!(lines[2].spans[0].style.fg, Some(Color::Indexed(2)));
assert_eq!(lines[2].spans[2].style.fg, Some(Color::Indexed(1)));
assert_eq!(buffer.cell((0, 2)).expect("insertions cell").fg, Color::Indexed(2));
assert_eq!(buffer.cell((3, 2)).expect("deletions cell").fg, Color::Indexed(1));
}
#[test]
fn footer_line_matches_raymon_style_shortcut_layout() {
let palette = Palette::default();
let key_style = palette.style_for(SemanticRole::HeadingAccent).add_modifier(Modifier::BOLD);
let sep_style = palette.style_for(SemanticRole::MutedText);
let line = footer_line(key_style, sep_style);
let text = line.spans.iter().map(|span| span.content.as_ref()).collect::<String>();
assert_eq!(
text,
"Move:j/k | Pane:% | App:a | Bell:b | Git:g | Model:m | Attention:n | Time:t | Output:o | Stats:s | Reset:R | Subs:= | Jump:Enter | Quit:q/Esc"
);
assert_eq!(line.spans[0].style.fg, Some(Color::Indexed(8)));
assert_eq!(line.spans[2].style.fg, Some(Color::Indexed(12)));
assert_eq!(FOOTER_BRAND, "🅸 🅻 🅼 🅰 🆁 🅸 ");
}
#[test]
fn compact_footer_line_lists_action_keys_only() {
let palette = Palette::default();
let key_style = palette.style_for(SemanticRole::HeadingAccent).add_modifier(Modifier::BOLD);
let sep_style = palette.style_for(SemanticRole::MutedText);
let line = compact_footer_line(key_style, sep_style);
let text = line.spans.iter().map(|span| span.content.as_ref()).collect::<String>();
assert_eq!(text, "j k % a b g m n t o s R = q");
assert_eq!(line.spans[0].style.fg, Some(Color::Indexed(12)));
}
#[test]
fn workspace_lines_render_expanded_subtasks_under_parent_row() {
let palette = Palette::default();
let lines = super::workspace_lines(
&AppModel {
title: "ilmari".to_string(),
status_line: String::new(),
show_app: false,
show_attention: false,
show_git: true,
show_detail: true,
show_time: true,
show_output: true,
show_stats: true,
workspace_groups: vec![WorkspaceGroup {
label: "api".to_string(),
git_summary: None,
rows: vec![PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: Some(
SessionProcessUsage {
agent: ResourceUsage {
cpu_tenths_percent: 154,
memory_kib: 64 * 1024,
},
spawned: ResourceUsage {
cpu_tenths_percent: 8,
memory_kib: 12 * 1024,
},
subtasks: vec![
SubtaskProcess {
pid: 102,
depth: 0,
command_label: "tmux-mcp-rs".to_string(),
usage: ResourceUsage {
cpu_tenths_percent: 8,
memory_kib: 12 * 1024,
},
},
SubtaskProcess {
pid: 103,
depth: 1,
command_label: "helper".to_string(),
usage: ResourceUsage {
cpu_tenths_percent: 2,
memory_kib: 1024,
},
},
],
}
.into(),
),
subtasks_expanded: true,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: true,
}],
}],
refresh_interval: Duration::from_secs(5),
last_refresh: Instant::now(),
last_refresh_wallclock: SystemTime::now(),
},
&palette,
&StatePresentations::default(),
120,
);
let text = lines
.iter()
.map(|line| line.spans.iter().map(|span| span.content.as_ref()).collect::<String>())
.collect::<Vec<_>>();
assert!(text[1].contains("[15.4%/64M]"));
assert!(text[2].contains("tmux-mcp-rs #102"));
assert!(text[3].contains("helper #103"));
}
#[test]
fn workspace_lines_keep_subtask_stats_before_labels_when_detail_is_hidden() {
let palette = Palette::default();
let lines = super::workspace_lines(
&AppModel {
title: "ilmari".to_string(),
status_line: String::new(),
show_app: false,
show_attention: false,
show_git: true,
show_detail: false,
show_time: true,
show_output: true,
show_stats: true,
workspace_groups: vec![WorkspaceGroup {
label: "api".to_string(),
git_summary: None,
rows: vec![PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: Some("working".to_string().into()),
client_label: "Codex",
detail: None,
process_usage: Some(
SessionProcessUsage {
agent: ResourceUsage {
cpu_tenths_percent: 154,
memory_kib: 64 * 1024,
},
spawned: ResourceUsage {
cpu_tenths_percent: 8,
memory_kib: 12 * 1024,
},
subtasks: vec![SubtaskProcess {
pid: 102,
depth: 0,
command_label: "tmux-mcp-rs".to_string(),
usage: ResourceUsage {
cpu_tenths_percent: 8,
memory_kib: 12 * 1024,
},
}],
}
.into(),
),
subtasks_expanded: true,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
}],
}],
refresh_interval: Duration::from_secs(5),
last_refresh: Instant::now(),
last_refresh_wallclock: SystemTime::now(),
},
&palette,
&StatePresentations::default(),
120,
);
let subtask_text =
lines[2].spans.iter().map(|span| span.content.as_ref()).collect::<String>();
let stats_position = subtask_text.find("[0.8%/12M]").expect("subtask stats");
let label_position = subtask_text.find("tmux-mcp-rs #102").expect("subtask label");
assert!(stats_position < label_position);
}
#[test]
fn footer_renders_with_empty_row_before_footer() {
let model = AppModel {
title: "ilmari".to_string(),
status_line: String::new(),
show_app: false,
show_attention: false,
show_git: true,
show_detail: true,
show_time: true,
show_output: true,
show_stats: false,
workspace_groups: vec![WorkspaceGroup {
label: "api".to_string(),
git_summary: Some(GitSummaryRow {
workspace_path: PathBuf::from("/tmp/api"),
workspace_label: "api".to_string(),
branch_name: "main".to_string(),
insertions: 3,
deletions: 1,
}),
rows: vec![
PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: true,
},
PaneRow {
pane_id: "%8".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
},
PaneRow {
pane_id: "%9".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
},
PaneRow {
pane_id: "%10".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
},
],
}],
refresh_interval: Duration::from_secs(5),
last_refresh: Instant::now(),
last_refresh_wallclock: SystemTime::now(),
};
let palette = Palette::default();
let backend = TestBackend::new(FOOTER_COMPACT_BREAKPOINT - 1, 8);
let mut terminal = Terminal::new(backend).expect("terminal should initialize");
terminal
.draw(|frame| super::render(frame, &model, &palette, &StatePresentations::default()))
.expect("frame should render");
let buffer = terminal.backend().buffer();
let footer_row = (0..(FOOTER_COMPACT_BREAKPOINT - 1))
.map(|x| buffer.cell((x, 7)).expect("footer cell").symbol())
.collect::<String>();
let last_main_row = (0..(FOOTER_COMPACT_BREAKPOINT - 1))
.map(|x| buffer.cell((x, 5)).expect("main cell").symbol())
.collect::<String>();
assert!(footer_row.starts_with("j k % a b g m n t o s R = q"));
assert!(footer_row.trim_end().ends_with("🅸 🅻 🅼 🅰 🆁 🅸"));
assert!(last_main_row.contains("%10"));
assert_eq!(buffer.cell((0, 6)).expect("row above footer").symbol(), " ");
}
#[test]
fn empty_status_line_does_not_reserve_leading_rows() {
let model = AppModel {
title: "ilmari".to_string(),
status_line: String::new(),
show_app: false,
show_attention: false,
show_git: true,
show_detail: false,
show_time: true,
show_output: true,
show_stats: false,
workspace_groups: vec![WorkspaceGroup {
label: "api".to_string(),
git_summary: Some(GitSummaryRow {
workspace_path: PathBuf::from("/tmp/api"),
workspace_label: "api".to_string(),
branch_name: "main".to_string(),
insertions: 3,
deletions: 1,
}),
rows: vec![PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: None,
client_label: "Codex",
detail: None,
process_usage: None,
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: true,
}],
}],
refresh_interval: Duration::from_secs(5),
last_refresh: Instant::now(),
last_refresh_wallclock: SystemTime::now(),
};
let palette = Palette::default();
let backend = TestBackend::new(40, 8);
let mut terminal = Terminal::new(backend).expect("terminal should initialize");
terminal
.draw(|frame| super::render(frame, &model, &palette, &StatePresentations::default()))
.expect("frame should render");
let buffer = terminal.backend().buffer();
assert_eq!(buffer.cell((0, 0)).expect("workspace label cell").symbol(), "a");
assert_eq!(buffer.cell((0, 1)).expect("git summary cell").symbol(), "m");
}
#[test]
fn workspace_row_hides_model_and_stats_when_disabled() {
let row = PaneRow {
pane_id: "%7".to_string(),
inactive_since_label: String::new(),
output_excerpt: Some(
"Could you clarify what you'd like me to help with?".to_string().into(),
),
client_label: "Codex",
detail: Some(
AgentDetail {
label: "gpt-5.4 xhigh fast".to_string(),
tone: AgentDetailTone::Neutral,
}
.into(),
),
process_usage: Some(
SessionProcessUsage {
agent: ResourceUsage { cpu_tenths_percent: 154, memory_kib: 64 * 1024 },
spawned: ResourceUsage { cpu_tenths_percent: 8, memory_kib: 12 * 1024 },
subtasks: vec![SubtaskProcess {
pid: 102,
depth: 0,
command_label: "tmux-mcp-rs".to_string(),
usage: ResourceUsage { cpu_tenths_percent: 8, memory_kib: 12 * 1024 },
}],
}
.into(),
),
subtasks_expanded: false,
status: SessionStatus::Running,
attention: false,
status_label: "running",
is_jump_match: false,
is_selected: false,
};
let palette = Palette::default();
let line = workspace_row_line(
&row,
&palette,
&StatePresentations::default(),
false,
false,
false,
false,
false,
);
let text = line.spans.iter().map(|span| span.content.as_ref()).collect::<String>();
assert!(!text.contains("gpt-5.4 xhigh fast"));
assert!(!text.contains("Could you clarify what you'd like me to help with?"));
assert!(!text.contains("Codex"));
assert!(!text.contains("[15.4%/64M]"));
assert!(!text.contains("[0.8%/12M]"));
assert!(text.contains("▶"));
}
}