fix: clarify panel ticket row hierarchy

This commit is contained in:
Keisuke Hirata 2026-06-15 16:02:31 +09:00
parent 79dda10da3
commit f3b435e724
No known key found for this signature in database
2 changed files with 208 additions and 11 deletions

View File

@ -5221,7 +5221,7 @@ const POD_STATUS_COLUMN_WIDTH: usize = 18;
fn panel_row_lines(row: &PanelRow, selected: bool, width: u16) -> Vec<Line<'static>> {
if row.kind == PanelRowKind::TicketIntakePod {
vec![panel_row_title_line(row, selected, width)]
vec![panel_intake_child_line(row, selected, width)]
} else {
vec![
panel_row_title_line(row, selected, width),
@ -5241,7 +5241,7 @@ fn panel_row_title_line(row: &PanelRow, selected: bool, width: u16) -> Line<'sta
let mut spans = Vec::new();
let mut remaining = width as usize;
push_ticket_marker_span(&mut spans, selected, &mut remaining);
push_ticket_primary_marker_span(&mut spans, selected, &mut remaining);
push_column_span(
&mut spans,
&row.status,
@ -5254,11 +5254,41 @@ fn panel_row_title_line(row: &PanelRow, selected: bool, width: u16) -> Line<'sta
Line::from(spans)
}
fn panel_intake_child_line(row: &PanelRow, selected: bool, width: u16) -> Line<'static> {
let title_style = if selected {
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Cyan)
};
let mut spans = Vec::new();
let mut remaining = width as usize;
push_intake_child_marker_span(&mut spans, selected, &mut remaining);
push_column_span(
&mut spans,
&row.status,
TICKET_STATE_COLUMN_WIDTH,
intake_status_style(&row.status),
&mut remaining,
);
push_bounded_span(&mut spans, row.title.as_str(), title_style, &mut remaining);
Line::from(spans)
}
fn panel_row_detail_line(row: &PanelRow, selected: bool, width: u16) -> Line<'static> {
let mut spans = Vec::new();
let mut remaining = width as usize;
push_ticket_marker_span(&mut spans, selected, &mut remaining);
push_ticket_detail_marker_span(&mut spans, selected, &mut remaining);
push_bounded_span(
&mut spans,
"meta ",
Style::default().fg(Color::DarkGray),
&mut remaining,
);
push_bounded_span(
&mut spans,
&panel_ticket_detail(row),
@ -5269,10 +5299,14 @@ fn panel_row_detail_line(row: &PanelRow, selected: bool, width: u16) -> Line<'st
Line::from(spans)
}
fn push_ticket_marker_span(spans: &mut Vec<Span<'static>>, selected: bool, remaining: &mut usize) {
fn push_ticket_primary_marker_span(
spans: &mut Vec<Span<'static>>,
selected: bool,
remaining: &mut usize,
) {
let (marker, style) = if selected {
(
"| ",
" ",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
@ -5283,6 +5317,42 @@ fn push_ticket_marker_span(spans: &mut Vec<Span<'static>>, selected: bool, remai
push_bounded_span(spans, marker, style, remaining);
}
fn push_ticket_detail_marker_span(
spans: &mut Vec<Span<'static>>,
selected: bool,
remaining: &mut usize,
) {
let (marker, style) = if selected {
(
"",
Style::default()
.fg(Color::Magenta)
.add_modifier(Modifier::BOLD),
)
} else {
(" ", Style::default().fg(Color::DarkGray))
};
push_bounded_span(spans, marker, style, remaining);
}
fn push_intake_child_marker_span(
spans: &mut Vec<Span<'static>>,
selected: bool,
remaining: &mut usize,
) {
let (marker, style) = if selected {
(
"",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
} else {
("", Style::default().fg(Color::DarkGray))
};
push_bounded_span(spans, marker, style, remaining);
}
fn panel_ticket_detail(row: &PanelRow) -> String {
if row.kind == PanelRowKind::InvalidTicket {
let mut parts = vec![panel_ticket_reference(row), "Gate: unavailable".to_string()];
@ -5420,6 +5490,15 @@ fn panel_priority_style(priority: ActionPriority) -> Style {
}
}
fn intake_status_style(status: &str) -> Style {
match status {
"live" => Style::default().fg(Color::Green),
"restorable" => Style::default().fg(Color::Yellow),
"stale" => Style::default().fg(Color::DarkGray),
_ => Style::default().fg(Color::Cyan),
}
}
fn section_rows(
list: &PodList,
section: &MultiPodSection,
@ -5553,6 +5632,34 @@ fn target_status_line(app: &MultiPodApp) -> Line<'static> {
));
}
Line::from(spans)
} else if let Some(row) = app
.selected_panel_row()
.filter(|row| row.kind == PanelRowKind::TicketIntakePod)
{
let ticket_id = panel_ticket_reference(row);
let action = if row.next_action == Some(NextUserAction::OpenPod) {
"open/attach"
} else {
"unavailable"
};
Line::from(vec![
Span::styled("composer target ", Style::default().fg(Color::DarkGray)),
Span::styled(
app.composer_target().label(),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" · selected Intake Pod ",
Style::default().fg(Color::DarkGray),
),
Span::styled(row.status.clone(), intake_status_style(&row.status)),
Span::styled(
format!(" · Ticket {ticket_id} · blank Enter {action}"),
Style::default().fg(Color::DarkGray),
),
])
} else if let Some(entry) = app.selected_pod_entry() {
let (status, status_style) = row_status_label(entry);
Line::from(vec![
@ -7398,9 +7505,8 @@ branch = "orchestration/custom-panel"
let title_start = state_start + TICKET_STATE_COLUMN_WIDTH + 1;
let row_id = row.ticket.as_ref().unwrap().id.as_str();
assert!(title_line.starts_with("| "));
assert!(detail_line.starts_with("| "));
assert!(!title_line.starts_with(""));
assert!(title_line.starts_with(""));
assert!(detail_line.starts_with("│ meta "));
assert!(!title_line.contains(row_id));
assert_eq!(display_column(&title_line, "inprogress"), state_start);
assert_eq!(
@ -7430,7 +7536,7 @@ branch = "orchestration/custom-panel"
let title_start = state_start + TICKET_STATE_COLUMN_WIDTH + 1;
assert!(title_line.starts_with(" ready"));
assert!(detail_line.starts_with(" 00001KTTB479X"));
assert!(detail_line.starts_with(" meta 00001KTTB479X"));
assert_eq!(display_column(&title_line, "ready"), state_start);
assert_eq!(
display_column(&title_line, "Long Ticket title"),
@ -7458,7 +7564,7 @@ branch = "orchestration/custom-panel"
assert_eq!(display_column(&title_line, "Very long Ticket"), title_start);
assert!(title_line.ends_with('…'));
assert_eq!(detail_line.width(), 42);
assert!(detail_line.starts_with(" 00001KTTB479X · Gate: clear"));
assert!(detail_line.starts_with(" meta 00001KTTB479X · Gate: clear"));
assert!(detail_line.ends_with('…'));
}
@ -7483,6 +7589,67 @@ branch = "orchestration/custom-panel"
assert!(detail_line.contains("Reason: Queue disabled: waiting for BLOCKER-1"));
}
#[test]
fn panel_ticket_intake_child_rows_render_as_indented_single_line() {
let row = panel_test_intake_child_row(
"00001TICKET",
"intake-live",
TicketLocalClaimStatus::Live,
Some(NextUserAction::OpenPod),
);
let lines = panel_row_lines(&row, false, 160);
assert_eq!(lines.len(), 1);
let line = plain_line(&lines[0]);
let status_start = 4;
let title_start = status_start + TICKET_STATE_COLUMN_WIDTH + 1;
assert!(line.starts_with(" └ live"));
assert_eq!(display_column(&line, "live"), status_start);
assert_eq!(
display_column(&line, "Intake Pod: intake-live"),
title_start
);
assert!(!line.starts_with(" live"));
let selected_line = plain_line(&panel_row_lines(&row, true, 160)[0]);
assert!(selected_line.starts_with(" ▶ live"));
assert!(selected_line.contains("Intake Pod: intake-live"));
}
#[test]
fn selected_ticket_intake_child_status_is_not_rendered_as_generic_ticket_or_pod() {
let ticket_id = "00001TICKET";
let pod_name = "intake-live";
let mut panel = WorkspacePanelViewModel::empty(Path::new("test"));
panel.rows.push(panel_test_intake_child_row(
ticket_id,
pod_name,
TicketLocalClaimStatus::Live,
Some(NextUserAction::OpenPod),
));
let list = PodList::from_sources(
PodVisibilitySource::ResumePicker,
vec![],
vec![live_info(pod_name, PodStatus::Idle)],
None,
10,
);
let mut app = app_with_panel(list, panel);
app.select_panel_key(PanelRowKey::TicketIntakePod {
ticket_id: ticket_id.to_string(),
pod_name: pod_name.to_string(),
});
let status = plain_line(&target_status_line(&app));
assert!(status.contains("selected Intake Pod live"));
assert!(status.contains("Ticket 00001TICKET"));
assert!(status.contains("blank Enter open/attach"));
assert!(!status.contains("selected Ticket"));
assert!(!status.contains("selected Pod live"));
}
#[test]
fn panel_pod_rows_use_aligned_columns_before_pod_name() {
let app = test_app(vec![
@ -8750,6 +8917,36 @@ branch = "orchestration/custom-panel"
}
}
fn panel_test_intake_child_row(
ticket_id: &str,
pod_name: &str,
status: TicketLocalClaimStatus,
next_action: Option<NextUserAction>,
) -> PanelRow {
PanelRow {
key: PanelRowKey::TicketIntakePod {
ticket_id: ticket_id.to_string(),
pod_name: pod_name.to_string(),
},
kind: PanelRowKind::TicketIntakePod,
title: format!("Intake Pod: {pod_name}"),
subtitle: Some(format!("Intake claim for Ticket {ticket_id}")),
status: status.label().to_string(),
priority: match status {
TicketLocalClaimStatus::Live | TicketLocalClaimStatus::Restorable => {
ActionPriority::ActiveWork
}
TicketLocalClaimStatus::Stale => ActionPriority::Background,
},
next_action,
ticket: None,
related_pods: vec![pod_name.to_string()],
disabled_reason: (status == TicketLocalClaimStatus::Stale)
.then(|| "claim metadata is stale".to_string()),
key_hint: Some(format!("Ticket {ticket_id} Intake Pod {pod_name}")),
}
}
fn closed_list(count: usize, selected: Option<&str>) -> PodList {
PodList::from_sources(
PodVisibilitySource::ResumePicker,

View File

@ -1039,7 +1039,7 @@ fn ticket_intake_pod_row(intake: &TicketAssociatedIntakeEntry) -> PanelRow {
pod_name: intake.pod_name.clone(),
},
kind: PanelRowKind::TicketIntakePod,
title: format!("Intake Pod: {}", intake.pod_name),
title: format!("Intake Pod: {}", intake.pod_name),
subtitle: Some(format!(
"Ticket {} · {} · {}",
intake.ticket_id,