fix: align Backend worker picker columns

This commit is contained in:
2026-09-05 21:19:18 +09:00
parent 724205b1df
commit 130ef1f0fe
+139 -32
View File
@@ -12,6 +12,7 @@ use ratatui::layout::{Constraint, Layout};
use ratatui::style::{Color, Modifier, Style}; use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph; use ratatui::widgets::Paragraph;
use unicode_width::UnicodeWidthStr;
use crate::backend_workspace_picker::select_backend_workspace; use crate::backend_workspace_picker::select_backend_workspace;
use crate::console; use crate::console;
@@ -235,9 +236,10 @@ fn draw(frame: &mut Frame<'_>, state: &BackendWorkerPickerState) {
layout[0], layout[0],
); );
let column_widths = WorkerColumnWidths::from_workers(&state.workers);
for (i, worker) in state.workers.iter().enumerate() { for (i, worker) in state.workers.iter().enumerate() {
frame.render_widget( frame.render_widget(
Paragraph::new(row_line(worker, i == state.selected)), Paragraph::new(row_line(worker, &column_widths, i == state.selected)),
layout[i + 1], layout[i + 1],
); );
} }
@@ -272,7 +274,28 @@ fn picker_title(target: &BackendRuntimeListTarget) -> String {
format!("backend workers workspace: {workspace} runtime: {runtime}") format!("backend workers workspace: {workspace} runtime: {runtime}")
} }
fn row_line(worker: &BackendWorkerSummary, selected: bool) -> Line<'static> { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct WorkerColumnWidths {
identity: usize,
name: usize,
state: usize,
}
impl WorkerColumnWidths {
fn from_workers(workers: &[BackendWorkerSummary]) -> Self {
workers.iter().fold(Self::default(), |widths, worker| Self {
identity: widths.identity.max(text_width(&short_worker_id(worker))),
name: widths.name.max(text_width(worker_name(worker))),
state: widths.state.max(text_width(&worker_state(worker))),
})
}
}
fn row_line(
worker: &BackendWorkerSummary,
widths: &WorkerColumnWidths,
selected: bool,
) -> Line<'static> {
let marker = if selected { "" } else { " " }; let marker = if selected { "" } else { " " };
let id_style = if selected { let id_style = if selected {
Style::default() Style::default()
@@ -281,42 +304,58 @@ fn row_line(worker: &BackendWorkerSummary, selected: bool) -> Line<'static> {
} else { } else {
Style::default().fg(Color::Cyan) Style::default().fg(Color::Cyan)
}; };
let preview_style = if selected { let name_style = if selected {
Style::default().fg(Color::White) Style::default().fg(Color::White)
} else { } else {
Style::default().fg(Color::DarkGray) Style::default().fg(Color::DarkGray)
}; };
let label = if worker.label.is_empty() {
worker.worker_id.as_str()
} else {
worker.label.as_str()
};
let profile = worker.profile.as_deref().unwrap_or("-");
Line::from(vec![ Line::from(vec![
Span::raw(marker), Span::raw(marker),
Span::styled(short_worker_id(worker), id_style),
Span::raw(" "),
Span::styled( Span::styled(
format!("[{}]", worker.state), pad_column(&short_worker_id(worker), widths.identity),
state_style(worker.state.as_str()), id_style,
), ),
Span::raw(" "), Span::raw(" "),
Span::styled(pad_column(worker_name(worker), widths.name), name_style),
Span::raw(" "),
Span::styled( Span::styled(
format!("profile:{profile}"), pad_column(&worker_state(worker), widths.state),
Style::default().fg(Color::DarkGray), state_style(worker.state.as_str()),
), ),
Span::raw(" "), Span::raw(" "),
Span::styled( Span::styled(
working_directory_text(worker), working_directory_text(worker),
Style::default().fg(Color::DarkGray), Style::default().fg(Color::DarkGray),
), ),
Span::raw(" "),
Span::styled(label.to_string(), preview_style),
]) ])
} }
fn worker_name(worker: &BackendWorkerSummary) -> &str {
if !worker.label.is_empty() {
worker.label.as_str()
} else if !worker.display_name.is_empty() {
worker.display_name.as_str()
} else {
worker.worker_id.as_str()
}
}
fn worker_state(worker: &BackendWorkerSummary) -> String {
format!("[{}]", worker.state)
}
fn text_width(value: &str) -> usize {
UnicodeWidthStr::width(value)
}
fn pad_column(value: &str, width: usize) -> String {
format!(
"{value}{}",
" ".repeat(width.saturating_sub(text_width(value)))
)
}
fn state_style(state: &str) -> Style { fn state_style(state: &str) -> Style {
match state { match state {
"running" | "idle" | "active" => Style::default() "running" | "idle" | "active" => Style::default()
@@ -347,11 +386,7 @@ fn working_directory_text(worker: &BackendWorkerSummary) -> String {
let Some(wd) = worker.working_directory.as_ref() else { let Some(wd) = worker.working_directory.as_ref() else {
return "wd:—".to_string(); return "wd:—".to_string();
}; };
let cleanliness = wd.cleanliness.as_deref().unwrap_or("unknown"); format!("wd:{}{}", wd.repository_key, wd.working_directory_id)
format!(
"wd:{}:{} {} {}",
wd.repository_key, wd.working_directory_id, wd.status, cleanliness
)
} }
#[cfg(test)] #[cfg(test)]
@@ -395,18 +430,90 @@ mod tests {
} }
} }
#[test] fn row_text(worker: &BackendWorkerSummary, widths: &WorkerColumnWidths) -> String {
fn worker_row_matches_inline_picker_shape() { row_line(worker, widths, false)
let row = row_line(&worker("runtime-a", "worker-b", Some("default")), true);
let text = row
.spans .spans
.into_iter() .into_iter()
.map(|span| span.content) .map(|span| span.content)
.collect::<String>(); .collect()
assert!(text.starts_with("▶ W-1")); }
assert!(text.contains("[running]"));
assert!(text.contains("profile:default")); fn display_column(text: &str, value: &str) -> usize {
assert!(text.contains("wd:—")); let byte_offset = text.find(value).expect("value in rendered row");
text_width(&text[..byte_offset])
}
#[test]
fn worker_row_orders_and_simplifies_columns() {
let mut worker = worker("runtime-a", "worker-b", Some("builtin:coder"));
worker.resource_key = "W-90".to_string();
worker.display_name = "Coder".to_string();
worker.label = "Coder · T-585".to_string();
worker.state = "stopped".to_string();
worker.working_directory = Some(
serde_json::from_value(serde_json::json!({
"working_directory_id": "001a06a9f0202000000",
"repository_key": "main",
"materializer_kind": "local_git_worktree",
"status": "active",
"cleanliness": "clean"
}))
.unwrap(),
);
let widths = WorkerColumnWidths::from_workers(std::slice::from_ref(&worker));
let text = row_text(&worker, &widths);
assert_eq!(
text,
" W-90 Coder · T-585 [stopped] wd:main・001a06a9f0202000000"
);
assert!(!text.contains("profile:"));
assert!(!text.contains("active clean"));
}
#[test]
fn worker_rows_align_identity_name_state_and_workdir_columns() {
let mut short = worker("runtime-a", "worker-a", None);
short.resource_key = "W-2".to_string();
short.label = "Coder".to_string();
short.display_name = short.label.clone();
short.state = "idle".to_string();
let mut long = worker("runtime-a", "worker-b", None);
long.resource_key = "W-100".to_string();
long.label = "Longer worker · T-9".to_string();
long.display_name = long.label.clone();
long.state = "stopped".to_string();
for worker in [&mut short, &mut long] {
worker.working_directory = Some(
serde_json::from_value(serde_json::json!({
"working_directory_id": "workdir-1",
"repository_key": "main",
"materializer_kind": "local_git_worktree",
"status": "active"
}))
.unwrap(),
);
}
let workers = vec![short, long];
let widths = WorkerColumnWidths::from_workers(&workers);
let first = row_text(&workers[0], &widths);
let second = row_text(&workers[1], &widths);
assert_eq!(
display_column(&first, "Coder"),
display_column(&second, "Longer")
);
assert_eq!(
display_column(&first, "[idle]"),
display_column(&second, "[stopped]")
);
assert_eq!(
display_column(&first, "wd:main"),
display_column(&second, "wd:main")
);
} }
#[test] #[test]