Merge branch 'develop' into work/remove-tui-ticket-commands
This commit is contained in:
commit
ffd81de815
|
|
@ -2,12 +2,12 @@
|
||||||
id: 20260606-060548-workspace-panel-layout-display-tuning
|
id: 20260606-060548-workspace-panel-layout-display-tuning
|
||||||
slug: workspace-panel-layout-display-tuning
|
slug: workspace-panel-layout-display-tuning
|
||||||
title: Workspace panel layout and display tuning
|
title: Workspace panel layout and display tuning
|
||||||
status: open
|
status: closed
|
||||||
kind: task
|
kind: task
|
||||||
priority: P2
|
priority: P2
|
||||||
labels: [tui, ticket, orchestration, panel, ux]
|
labels: [tui, ticket, orchestration, panel, ux]
|
||||||
created_at: 2026-06-06T06:05:48Z
|
created_at: 2026-06-06T06:05:48Z
|
||||||
updated_at: 2026-06-06T09:32:01Z
|
updated_at: 2026-06-06T21:16:52Z
|
||||||
assignee: null
|
assignee: null
|
||||||
legacy_ticket: null
|
legacy_ticket: null
|
||||||
---
|
---
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
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.
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -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`.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
@ -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> {
|
fn panel_row_line(row: &PanelRow, selected: bool, width: u16) -> Line<'static> {
|
||||||
let marker = if selected { "▶ " } else { " " };
|
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 action = row.next_action.map(NextUserAction::label).unwrap_or("View");
|
||||||
let status_style = panel_priority_style(row.priority);
|
let phase = row
|
||||||
let mut text = format!(
|
.ticket
|
||||||
"{marker}{} [{}] {action}: {}",
|
.as_ref()
|
||||||
row.title,
|
.map(|ticket| ticket.phase.label())
|
||||||
row.priority.label(),
|
.unwrap_or("-");
|
||||||
row.status
|
let ticket_ref = panel_ticket_reference(row);
|
||||||
);
|
|
||||||
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 mut spans = Vec::new();
|
let mut spans = Vec::new();
|
||||||
spans.push(Span::styled(
|
let mut remaining = width as usize;
|
||||||
prefix.clone(),
|
|
||||||
|
push_bounded_span(
|
||||||
|
&mut spans,
|
||||||
|
marker,
|
||||||
if selected {
|
if selected {
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Magenta)
|
.fg(Color::Magenta)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Magenta)
|
Style::default().fg(Color::DarkGray)
|
||||||
},
|
},
|
||||||
));
|
&mut remaining,
|
||||||
spans.push(Span::styled(
|
);
|
||||||
format!("[{}]", row.priority.label()),
|
push_column_span(
|
||||||
status_style,
|
&mut spans,
|
||||||
));
|
row.priority.label(),
|
||||||
let rest = truncated.strip_prefix(&status_prefix).unwrap_or("");
|
TICKET_PRIORITY_COLUMN_WIDTH,
|
||||||
spans.push(Span::styled(
|
panel_priority_style(row.priority),
|
||||||
rest.to_string(),
|
&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),
|
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)
|
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 {
|
fn panel_priority_style(priority: ActionPriority) -> Style {
|
||||||
match priority {
|
match priority {
|
||||||
ActionPriority::UserReply => Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
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 {
|
} else {
|
||||||
"disabled"
|
"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 mut spans = Vec::new();
|
||||||
let prefix = format!("{marker}{}", entry.name);
|
let mut remaining = width as usize;
|
||||||
let visible_prefix = format!("{marker}{} ", entry.name);
|
|
||||||
spans.push(Span::styled(visible_prefix, name_style));
|
push_bounded_span(
|
||||||
spans.push(Span::styled(format!("[{status}]"), status_style));
|
&mut spans,
|
||||||
let rest = truncated
|
marker,
|
||||||
.strip_prefix(&format!("{prefix} [{status}]"))
|
if selected {
|
||||||
.unwrap_or("");
|
Style::default()
|
||||||
spans.push(Span::styled(
|
.fg(Color::Cyan)
|
||||||
rest.to_string(),
|
.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),
|
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)
|
Line::from(spans)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2515,7 +2627,7 @@ mod tests {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let ticket_line = lines
|
let ticket_line = lines
|
||||||
.iter()
|
.iter()
|
||||||
.position(|line| line.contains("Needs Human Reply"))
|
.position(|line| line.contains("needs-human-reply"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap();
|
let pod_line = lines.iter().position(|line| line.contains("idle")).unwrap();
|
||||||
assert!(ticket_line < pod_line);
|
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]
|
#[test]
|
||||||
fn multi_running_paused_and_stopped_targets_are_direct_send_disabled() {
|
fn multi_running_paused_and_stopped_targets_are_direct_send_disabled() {
|
||||||
let mut app = test_app(vec![
|
let mut app = test_app(vec![
|
||||||
|
|
@ -3286,6 +3550,45 @@ mod tests {
|
||||||
app
|
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 {
|
fn closed_list(count: usize, selected: Option<&str>) -> PodList {
|
||||||
PodList::from_sources(
|
PodList::from_sources(
|
||||||
PodVisibilitySource::ResumePicker,
|
PodVisibilitySource::ResumePicker,
|
||||||
|
|
@ -3361,6 +3664,11 @@ mod tests {
|
||||||
.collect()
|
.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 {
|
fn input_text(app: &MultiPodApp) -> String {
|
||||||
Segment::flatten_to_text(&app.input.submit_segments())
|
Segment::flatten_to_text(&app.input.submit_segments())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user