diff --git a/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/artifacts/.gitkeep b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/artifacts/.gitkeep similarity index 100% rename from .yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/artifacts/.gitkeep rename to .yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/artifacts/.gitkeep diff --git a/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/artifacts/delegation-intent.md b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/artifacts/delegation-intent.md similarity index 100% rename from .yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/artifacts/delegation-intent.md rename to .yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/artifacts/delegation-intent.md diff --git a/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/item.md b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/item.md similarity index 98% rename from .yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/item.md rename to .yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/item.md index ab6cd059..5e73f25a 100644 --- a/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/item.md +++ b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/item.md @@ -2,12 +2,12 @@ id: 20260606-060548-workspace-panel-layout-display-tuning slug: workspace-panel-layout-display-tuning title: Workspace panel layout and display tuning -status: open +status: closed kind: task priority: P2 labels: [tui, ticket, orchestration, panel, ux] created_at: 2026-06-06T06:05:48Z -updated_at: 2026-06-06T09:32:01Z +updated_at: 2026-06-06T21:16:52Z assignee: null legacy_ticket: null --- diff --git a/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/resolution.md b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/resolution.md new file mode 100644 index 00000000..b7d62ca0 --- /dev/null +++ b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/resolution.md @@ -0,0 +1,57 @@ +Implemented workspace panel aligned row layout. + +Final Ticket/action row schema: + +```text + +``` + +Column behavior: +- marker: width 2; +- priority: width 11; +- action: width 7; +- status: width 24; +- phase: width 12; +- slug-or-id: width 32; +- title: flexible remaining width. + +Fixed columns are padded/truncated before the title. The long Ticket title is last and truncates with `…` when needed. + +Final Pod row schema: + +```text +<marker><status> <action> <kind> <pod-name> +``` + +Column behavior: +- marker: width 2; +- status: width 18; +- action: width 8; +- kind: width 3, currently `pod`; +- pod name: flexible remaining width. + +Fixed Pod columns are padded/truncated before the Pod name. Long Pod names no longer shift status/action alignment. + +Tests added/updated: +- `panel_ticket_rows_use_aligned_columns_before_title` +- `panel_ticket_title_truncates_after_stable_columns` +- `panel_pod_rows_use_aligned_columns_before_pod_name` +- `panel_pod_name_truncates_after_status_action_and_kind` +- adjusted action-before-pod ordering test to locate Ticket rows by slug because title is no longer the leading field. + +Validation after merge: +- `cargo test -p tui panel_` +- `cargo test -p tui workspace_panel` +- `cargo test -p tui multi_pod` +- `cargo test -p yoi panel` +- `cargo check --workspace --all-targets` +- `cargo fmt --check` +- `git diff --check HEAD~1..HEAD` +- `cargo build -p yoi` +- `target/debug/yoi ticket doctor` +- `nix build .#yoi --no-link --print-out-paths` + +External review approved with no requested changes. + +Remaining UX/display tuning: +- Broader detail-pane/timeline/copy tuning remains optional follow-up; this ticket intentionally stayed focused on aligned row columns. diff --git a/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/thread.md b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/thread.md new file mode 100644 index 00000000..91306347 --- /dev/null +++ b/.yoi/tickets/closed/20260606-060548-workspace-panel-layout-display-tuning/thread.md @@ -0,0 +1,135 @@ +<!-- event: create author: yoi ticket at: 2026-06-06T06:05:48Z --> + +## Created + +Created by LocalTicketBackend create. + +--- + +<!-- event: plan author: hare at: 2026-06-06T06:06:30Z --> + +## Plan + +Created after closing the first-pass workspace orchestration panel implementation. + +The first pass deliberately prioritized end-to-end behavior over visual/layout polish. This ticket owns the next tuning pass: row labels, key hints, detail pane content, composer target visibility, concise diagnostics, and optional phase/dependency/timeline display, while preserving existing TUI conventions and the thin ViewModel/action-dispatch boundaries. + + +--- + +<!-- event: decision author: hare at: 2026-06-06T08:36:49Z --> + +## Decision + +User layout direction: + +Rows should be column-aligned instead of leading with long identifiers/titles. + +- Ticket/action rows: move the long Ticket title to the end. Put short, alignable fields first, such as priority/action/status/phase/id-or-slug, then the title. +- Pod rows: move variable-length Pod id/name to the end. Put short, alignable fields first, such as status/action/role-or-kind, then the Pod id/name. +- Ticket and Pod rows do not need to share exactly the same schema, but each row family should keep stable aligned columns so status/action can be visually scanned. + +This should be handled in the layout/display tuning ticket, not by changing backend/action semantics. + + +--- + +<!-- event: plan author: hare at: 2026-06-06T09:32:01Z --> + +## Plan + +Preflight result: `implementation-ready` as a focused layout/display pass. + +Implementation should rework panel row rendering into aligned columns with short comparable fields first and long variable text last: +- Ticket/action rows: move Ticket title to the end; align priority/action/status/phase/id-or-slug first. +- Pod rows: move Pod id/name to the end; align status/action/kind first. + +This should not change Ticket backend semantics, action dispatch, Orchestrator lifecycle, Intake handoff, no-Ticket behavior, or the thin ViewModel boundary. + +Detailed delegation intent is recorded in `artifacts/delegation-intent.md`. + + +--- + +<!-- event: review author: hare at: 2026-06-06T21:16:52Z status: approve --> + +## Review: approve + +External reviewer approved the implementation with no requested changes. + +Review summary: +- Ticket/action rows use stable fixed-width columns first and place the variable Ticket title last. +- Pod rows use stable fixed-width columns first and place the variable Pod name/id last. +- Long titles/names truncate after stable columns and do not shift status/action alignment. +- Color/style semantics are preserved closely. +- Changes are scoped to rendering/helpers/tests; no Ticket action dispatch, Intake launch, Orchestrator lifecycle, Pod open/direct-send, composer target, authority, or I/O semantics changed. +- No `--multi` route was reintroduced. +- Tests cover row rendering, alignment, and truncation. + + +--- + +<!-- event: close author: hare at: 2026-06-06T21:16:52Z status: closed --> + +## Closed + +Implemented workspace panel aligned row layout. + +Final Ticket/action row schema: + +```text +<marker><priority> <action> <status> <phase> <slug-or-id> <title> +``` + +Column behavior: +- marker: width 2; +- priority: width 11; +- action: width 7; +- status: width 24; +- phase: width 12; +- slug-or-id: width 32; +- title: flexible remaining width. + +Fixed columns are padded/truncated before the title. The long Ticket title is last and truncates with `…` when needed. + +Final Pod row schema: + +```text +<marker><status> <action> <kind> <pod-name> +``` + +Column behavior: +- marker: width 2; +- status: width 18; +- action: width 8; +- kind: width 3, currently `pod`; +- pod name: flexible remaining width. + +Fixed Pod columns are padded/truncated before the Pod name. Long Pod names no longer shift status/action alignment. + +Tests added/updated: +- `panel_ticket_rows_use_aligned_columns_before_title` +- `panel_ticket_title_truncates_after_stable_columns` +- `panel_pod_rows_use_aligned_columns_before_pod_name` +- `panel_pod_name_truncates_after_status_action_and_kind` +- adjusted action-before-pod ordering test to locate Ticket rows by slug because title is no longer the leading field. + +Validation after merge: +- `cargo test -p tui panel_` +- `cargo test -p tui workspace_panel` +- `cargo test -p tui multi_pod` +- `cargo test -p yoi panel` +- `cargo check --workspace --all-targets` +- `cargo fmt --check` +- `git diff --check HEAD~1..HEAD` +- `cargo build -p yoi` +- `target/debug/yoi ticket doctor` +- `nix build .#yoi --no-link --print-out-paths` + +External review approved with no requested changes. + +Remaining UX/display tuning: +- Broader detail-pane/timeline/copy tuning remains optional follow-up; this ticket intentionally stayed focused on aligned row columns. + + +--- diff --git a/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/thread.md b/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/thread.md deleted file mode 100644 index e3f06a53..00000000 --- a/.yoi/tickets/open/20260606-060548-workspace-panel-layout-display-tuning/thread.md +++ /dev/null @@ -1,52 +0,0 @@ -<!-- event: create author: yoi ticket at: 2026-06-06T06:05:48Z --> - -## Created - -Created by LocalTicketBackend create. - ---- - -<!-- event: plan author: hare at: 2026-06-06T06:06:30Z --> - -## Plan - -Created after closing the first-pass workspace orchestration panel implementation. - -The first pass deliberately prioritized end-to-end behavior over visual/layout polish. This ticket owns the next tuning pass: row labels, key hints, detail pane content, composer target visibility, concise diagnostics, and optional phase/dependency/timeline display, while preserving existing TUI conventions and the thin ViewModel/action-dispatch boundaries. - - ---- - -<!-- event: decision author: hare at: 2026-06-06T08:36:49Z --> - -## Decision - -User layout direction: - -Rows should be column-aligned instead of leading with long identifiers/titles. - -- Ticket/action rows: move the long Ticket title to the end. Put short, alignable fields first, such as priority/action/status/phase/id-or-slug, then the title. -- Pod rows: move variable-length Pod id/name to the end. Put short, alignable fields first, such as status/action/role-or-kind, then the Pod id/name. -- Ticket and Pod rows do not need to share exactly the same schema, but each row family should keep stable aligned columns so status/action can be visually scanned. - -This should be handled in the layout/display tuning ticket, not by changing backend/action semantics. - - ---- - -<!-- event: plan author: hare at: 2026-06-06T09:32:01Z --> - -## Plan - -Preflight result: `implementation-ready` as a focused layout/display pass. - -Implementation should rework panel row rendering into aligned columns with short comparable fields first and long variable text last: -- Ticket/action rows: move Ticket title to the end; align priority/action/status/phase/id-or-slug first. -- Pod rows: move Pod id/name to the end; align status/action/kind first. - -This should not change Ticket backend semantics, action dispatch, Orchestrator lifecycle, Intake handoff, no-Ticket behavior, or the thin ViewModel boundary. - -Detailed delegation intent is recorded in `artifacts/delegation-intent.md`. - - ---- diff --git a/crates/tui/src/multi_pod.rs b/crates/tui/src/multi_pod.rs index a8da7d4e..744a6566 100644 --- a/crates/tui/src/multi_pod.rs +++ b/crates/tui/src/multi_pod.rs @@ -1950,46 +1950,138 @@ fn panel_action_header_line(total: usize, width: u16) -> Line<'static> { )) } +const TICKET_PRIORITY_COLUMN_WIDTH: usize = 11; +const TICKET_ACTION_COLUMN_WIDTH: usize = 7; +const TICKET_STATUS_COLUMN_WIDTH: usize = 24; +const TICKET_PHASE_COLUMN_WIDTH: usize = 12; +const TICKET_ID_COLUMN_WIDTH: usize = 32; +const POD_STATUS_COLUMN_WIDTH: usize = 18; +const POD_ACTION_COLUMN_WIDTH: usize = 8; +const POD_KIND_COLUMN_WIDTH: usize = 3; + fn panel_row_line(row: &PanelRow, selected: bool, width: u16) -> Line<'static> { let marker = if selected { "▶ " } else { " " }; + let title_style = if selected { + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Magenta) + }; let action = row.next_action.map(NextUserAction::label).unwrap_or("View"); - let status_style = panel_priority_style(row.priority); - let mut text = format!( - "{marker}{} [{}] {action}: {}", - row.title, - row.priority.label(), - row.status - ); - if let Some(subtitle) = row.subtitle.as_deref() { - text.push_str(" "); - text.push_str(subtitle); - } - let truncated = truncate_with_ellipsis(&text, width as usize); - let prefix = format!("{marker}{} ", row.title); - let status_prefix = format!("{prefix}[{}]", row.priority.label()); + let phase = row + .ticket + .as_ref() + .map(|ticket| ticket.phase.label()) + .unwrap_or("-"); + let ticket_ref = panel_ticket_reference(row); let mut spans = Vec::new(); - spans.push(Span::styled( - prefix.clone(), + let mut remaining = width as usize; + + push_bounded_span( + &mut spans, + marker, if selected { Style::default() .fg(Color::Magenta) .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Magenta) + Style::default().fg(Color::DarkGray) }, - )); - spans.push(Span::styled( - format!("[{}]", row.priority.label()), - status_style, - )); - let rest = truncated.strip_prefix(&status_prefix).unwrap_or(""); - spans.push(Span::styled( - rest.to_string(), + &mut remaining, + ); + push_column_span( + &mut spans, + row.priority.label(), + TICKET_PRIORITY_COLUMN_WIDTH, + panel_priority_style(row.priority), + &mut remaining, + ); + push_column_span( + &mut spans, + action, + TICKET_ACTION_COLUMN_WIDTH, + Style::default().fg(Color::Magenta), + &mut remaining, + ); + push_column_span( + &mut spans, + &row.status, + TICKET_STATUS_COLUMN_WIDTH, Style::default().fg(Color::DarkGray), - )); + &mut remaining, + ); + push_column_span( + &mut spans, + phase, + TICKET_PHASE_COLUMN_WIDTH, + Style::default().fg(Color::DarkGray), + &mut remaining, + ); + push_column_span( + &mut spans, + &ticket_ref, + TICKET_ID_COLUMN_WIDTH, + Style::default().fg(Color::DarkGray), + &mut remaining, + ); + push_bounded_span(&mut spans, row.title.as_str(), title_style, &mut remaining); + Line::from(spans) } +fn panel_ticket_reference(row: &PanelRow) -> String { + row.ticket + .as_ref() + .map(|ticket| { + if ticket.slug.is_empty() { + ticket.id.clone() + } else { + ticket.slug.clone() + } + }) + .unwrap_or_else(|| match &row.key { + PanelRowKey::Ticket(id) => id.clone(), + PanelRowKey::Pod(name) => name.clone(), + }) +} + +fn push_column_span( + spans: &mut Vec<Span<'static>>, + value: &str, + column_width: usize, + style: Style, + remaining: &mut usize, +) { + if *remaining == 0 { + return; + } + let mut content = padded_cell(value, column_width); + content.push(' '); + push_bounded_span(spans, &content, style, remaining); +} + +fn push_bounded_span( + spans: &mut Vec<Span<'static>>, + value: &str, + style: Style, + remaining: &mut usize, +) { + if *remaining == 0 || value.is_empty() { + return; + } + let content = truncate_with_ellipsis(value, *remaining); + *remaining = remaining.saturating_sub(content.width()); + spans.push(Span::styled(content, style)); +} + +fn padded_cell(value: &str, width: usize) -> String { + let mut cell = truncate_with_ellipsis(value, width); + let padding = width.saturating_sub(cell.width()); + cell.extend(std::iter::repeat_n(' ', padding)); + cell +} + fn panel_priority_style(priority: ActionPriority) -> Style { match priority { ActionPriority::UserReply => Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), @@ -2047,24 +2139,44 @@ fn row_line(entry: &PodListEntry, selected: bool, width: u16) -> Line<'static> { } else { "disabled" }; - let mut text = format!("{marker}{} [{status}] {action}", entry.name); - if let Some(preview) = entry.summary.preview.as_deref() { - text.push_str(" "); - text.push_str(preview); - } - let truncated = truncate_with_ellipsis(&text, width as usize); let mut spans = Vec::new(); - let prefix = format!("{marker}{}", entry.name); - let visible_prefix = format!("{marker}{} ", entry.name); - spans.push(Span::styled(visible_prefix, name_style)); - spans.push(Span::styled(format!("[{status}]"), status_style)); - let rest = truncated - .strip_prefix(&format!("{prefix} [{status}]")) - .unwrap_or(""); - spans.push(Span::styled( - rest.to_string(), + let mut remaining = width as usize; + + push_bounded_span( + &mut spans, + marker, + if selected { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::DarkGray) + }, + &mut remaining, + ); + push_column_span( + &mut spans, + status, + POD_STATUS_COLUMN_WIDTH, + status_style, + &mut remaining, + ); + push_column_span( + &mut spans, + action, + POD_ACTION_COLUMN_WIDTH, Style::default().fg(Color::DarkGray), - )); + &mut remaining, + ); + push_column_span( + &mut spans, + "pod", + POD_KIND_COLUMN_WIDTH, + Style::default().fg(Color::DarkGray), + &mut remaining, + ); + push_bounded_span(&mut spans, entry.name.as_str(), name_style, &mut remaining); + Line::from(spans) } @@ -2515,7 +2627,7 @@ mod tests { .collect::<Vec<_>>(); let ticket_line = lines .iter() - .position(|line| line.contains("Needs Human Reply")) + .position(|line| line.contains("needs-human-reply")) .unwrap(); let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap(); assert!(ticket_line < pod_line); @@ -2693,6 +2805,158 @@ mod tests { } } + #[test] + fn panel_ticket_rows_use_aligned_columns_before_title() { + let review_row = panel_test_ticket_row( + "workspace-panel-composer-targets", + "Workspace panel composer targets", + ActionPriority::Decision, + NextUserAction::Review, + "implementation reported", + crate::workspace_panel::TicketPanelPhase::Reviewing, + ); + let ready_row = panel_test_ticket_row( + "ticket-slug", + "Long Ticket title that should be rendered after short columns", + ActionPriority::ReadyForGo, + NextUserAction::Go, + "ready for Go", + crate::workspace_panel::TicketPanelPhase::Preflight, + ); + + let review_line = plain_line(&panel_row_line(&review_row, true, 160)); + let ready_line = plain_line(&panel_row_line(&ready_row, false, 160)); + let action_start = 2 + TICKET_PRIORITY_COLUMN_WIDTH + 1; + let status_start = action_start + TICKET_ACTION_COLUMN_WIDTH + 1; + let phase_start = status_start + TICKET_STATUS_COLUMN_WIDTH + 1; + let id_start = phase_start + TICKET_PHASE_COLUMN_WIDTH + 1; + let title_start = id_start + TICKET_ID_COLUMN_WIDTH + 1; + + assert!(!review_line.starts_with("▶ Workspace panel composer targets")); + assert_eq!(display_column(&review_line, "Review"), action_start); + assert_eq!(display_column(&ready_line, "Go"), action_start); + assert_eq!( + display_column(&review_line, "implementation reported"), + status_start + ); + assert_eq!(display_column(&ready_line, "ready for Go"), status_start); + assert_eq!(display_column(&review_line, "review"), phase_start); + assert_eq!(display_column(&ready_line, "preflight"), phase_start); + assert_eq!( + display_column(&review_line, "workspace-panel-composer-targets"), + id_start + ); + assert_eq!(display_column(&ready_line, "ticket-slug"), id_start); + assert_eq!( + display_column(&review_line, "Workspace panel composer targets"), + title_start + ); + assert_eq!( + display_column(&ready_line, "Long Ticket title"), + title_start + ); + } + + #[test] + fn panel_ticket_title_truncates_after_stable_columns() { + let row = panel_test_ticket_row( + "ticket-slug", + "Very long Ticket title that should truncate only after the aligned short columns", + ActionPriority::ReadyForGo, + NextUserAction::Go, + "ready for Go", + crate::workspace_panel::TicketPanelPhase::Preflight, + ); + + let line = plain_line(&panel_row_line(&row, false, 112)); + let title_start = 2 + + TICKET_PRIORITY_COLUMN_WIDTH + + 1 + + TICKET_ACTION_COLUMN_WIDTH + + 1 + + TICKET_STATUS_COLUMN_WIDTH + + 1 + + TICKET_PHASE_COLUMN_WIDTH + + 1 + + TICKET_ID_COLUMN_WIDTH + + 1; + + assert_eq!(line.width(), 112); + assert_eq!( + display_column(&line, "ticket-slug"), + title_start - TICKET_ID_COLUMN_WIDTH - 1 + ); + assert_eq!(display_column(&line, "Very long Ticket"), title_start); + assert!(line.ends_with('…')); + } + + #[test] + fn panel_pod_rows_use_aligned_columns_before_pod_name() { + let app = test_app(vec![ + live_info("companion", PodStatus::Idle), + live_info("very-long-background-worker-name", PodStatus::Running), + ]); + let idle = app + .list + .entries + .iter() + .find(|entry| entry.name == "companion") + .unwrap(); + let running = app + .list + .entries + .iter() + .find(|entry| entry.name == "very-long-background-worker-name") + .unwrap(); + + let idle_line = plain_line(&row_line(idle, false, 120)); + let running_line = plain_line(&row_line(running, false, 120)); + let action_start = 2 + POD_STATUS_COLUMN_WIDTH + 1; + let kind_start = action_start + POD_ACTION_COLUMN_WIDTH + 1; + let name_start = kind_start + POD_KIND_COLUMN_WIDTH + 1; + + assert!(!running_line.starts_with(" very-long-background-worker-name")); + assert_eq!(display_column(&idle_line, "send"), action_start); + assert_eq!(display_column(&running_line, "open"), action_start); + assert_eq!(display_column(&idle_line, "pod"), kind_start); + assert_eq!(display_column(&running_line, "pod"), kind_start); + assert_eq!(display_column(&idle_line, "companion"), name_start); + assert_eq!( + display_column(&running_line, "very-long-background-worker-name"), + name_start + ); + } + + #[test] + fn panel_pod_name_truncates_after_status_action_and_kind() { + let app = test_app(vec![live_info( + "very-long-background-worker-name-that-keeps-going", + PodStatus::Running, + )]); + let entry = app.list.selected_entry().unwrap(); + + let line = plain_line(&row_line(entry, false, 58)); + let name_start = 2 + + POD_STATUS_COLUMN_WIDTH + + 1 + + POD_ACTION_COLUMN_WIDTH + + 1 + + POD_KIND_COLUMN_WIDTH + + 1; + + assert_eq!(line.width(), 58); + assert_eq!( + display_column(&line, "open"), + 2 + POD_STATUS_COLUMN_WIDTH + 1 + ); + assert_eq!( + display_column(&line, "pod"), + name_start - POD_KIND_COLUMN_WIDTH - 1 + ); + assert_eq!(display_column(&line, "very-long"), name_start); + assert!(line.ends_with('…')); + } + #[test] fn multi_running_paused_and_stopped_targets_are_direct_send_disabled() { let mut app = test_app(vec![ @@ -3286,6 +3550,45 @@ mod tests { app } + fn panel_test_ticket_row( + slug: &str, + title: &str, + priority: ActionPriority, + next_action: NextUserAction, + status: &str, + phase: crate::workspace_panel::TicketPanelPhase, + ) -> PanelRow { + let ticket = crate::workspace_panel::TicketPanelEntry { + id: format!("20260606-000000-{slug}"), + slug: slug.to_string(), + title: title.to_string(), + status: "open".to_string(), + kind: "task".to_string(), + priority: "P2".to_string(), + labels: Vec::new(), + phase, + next_action: Some(next_action), + updated_at: None, + latest_event_kind: Some("implementation_report".to_string()), + latest_event_excerpt: Some("latest event stays out of the primary row".to_string()), + blocked_reason: None, + related_pods: Vec::new(), + }; + PanelRow { + key: PanelRowKey::Ticket(ticket.id.clone()), + kind: crate::workspace_panel::PanelRowKind::Ticket, + title: title.to_string(), + subtitle: Some("slug · priority · latest event".to_string()), + status: status.to_string(), + priority, + next_action: Some(next_action), + ticket: Some(ticket), + related_pods: Vec::new(), + disabled_reason: None, + key_hint: Some("Enter".to_string()), + } + } + fn closed_list(count: usize, selected: Option<&str>) -> PodList { PodList::from_sources( PodVisibilitySource::ResumePicker, @@ -3361,6 +3664,11 @@ mod tests { .collect() } + fn display_column(text: &str, needle: &str) -> usize { + let byte_index = text.find(needle).unwrap(); + text[..byte_index].width() + } + fn input_text(app: &MultiPodApp) -> String { Segment::flatten_to_text(&app.input.submit_segments()) }