From 03ad525fccc930ebd59024bd0263b4a1e53a7a91 Mon Sep 17 00:00:00 2001 From: Hare Date: Tue, 23 Jun 2026 15:27:04 +0900 Subject: [PATCH] tui: trim dashboard redundant hints --- crates/tui/src/dashboard/mod.rs | 52 ------ crates/tui/src/dashboard/render.rs | 276 ++--------------------------- crates/tui/src/dashboard/tests.rs | 65 ++++--- 3 files changed, 60 insertions(+), 333 deletions(-) diff --git a/crates/tui/src/dashboard/mod.rs b/crates/tui/src/dashboard/mod.rs index e1e25a98..75db186d 100644 --- a/crates/tui/src/dashboard/mod.rs +++ b/crates/tui/src/dashboard/mod.rs @@ -1437,31 +1437,6 @@ impl DashboardApp { } } - pub(crate) fn selected_open_disabled_reason(&self) -> Option { - if let Some(row) = self - .selected_panel_row() - .filter(|row| row.is_ticket_action()) - { - return Some( - row.disabled_reason - .clone() - .or_else(|| row.key_hint.clone()) - .unwrap_or_else(|| { - "Enter dispatches this Ticket action after re-checking current Ticket authority." - .to_string() - }), - ); - } - if let Some(entry) = self.selected_pod_entry() { - if entry.actions.can_open { - return None; - } - return Some(open_disabled_reason(entry)); - } - self.selected_panel_row() - .and_then(|row| row.disabled_reason.clone().or_else(|| row.key_hint.clone())) - } - pub(crate) fn select_next(&mut self) { let visible = visible_panel_keys(&self.panel, &self.list); if visible.is_empty() { @@ -5037,33 +5012,6 @@ fn segments_are_blank(segments: &[Segment]) -> bool { }) } -fn open_disabled_reason(entry: &PodListEntry) -> String { - if let Some(live) = entry.live.as_ref() { - if !live.reachable { - return "Selected live Pod is unreachable.".to_string(); - } - return match live.status { - Some(PodStatus::Running) => { - "Selected Pod is running; Enter opens/attaches for inspection.".to_string() - } - Some(PodStatus::Paused) => { - "Selected Pod is paused; open it explicitly to resume or start a new turn." - .to_string() - } - Some(PodStatus::Idle) => "Selected Pod can be opened/attached.".to_string(), - None => "Selected Pod did not report a live status.".to_string(), - }; - } - if entry.stored.is_some() { - return "Selected Pod is stopped; Enter restores/opens for inspection.".to_string(); - } - entry - .actions - .disabled_reason - .clone() - .unwrap_or_else(|| "Selected Pod cannot be opened from this row.".to_string()) -} - fn selected_ticket_notice(row: Option<&PanelRow>) -> String { match row { Some(row) if row.is_ticket_action() => { diff --git a/crates/tui/src/dashboard/render.rs b/crates/tui/src/dashboard/render.rs index e631cc4f..b463e584 100644 --- a/crates/tui/src/dashboard/render.rs +++ b/crates/tui/src/dashboard/render.rs @@ -57,24 +57,14 @@ pub(super) fn input_area_height(render: &crate::input::InputRender, terminal_hei } pub(super) fn draw_title(frame: &mut Frame<'_>, app: &DashboardApp, area: Rect) { - let guidance = if app - .panel - .composer - .is_available(ComposerTarget::TicketIntake) - { - " Row selection: blank Enter opens/dispatches · text Enter uses target · Tab target" - } else if app.panel.header.ticket_configured { - " Row selection: blank Enter opens/dispatches · text Enter sends to Companion" - } else { - " Pod-centric view · Row selection: blank Enter opens · text Enter sends to Companion" - }; - let mut spans = vec![ - Span::styled( - "workspace dashboard", - Style::default().add_modifier(Modifier::BOLD), - ), - Span::styled(guidance, Style::default().fg(Color::DarkGray)), - ]; + frame.render_widget(Paragraph::new(title_line(app)), area); +} + +pub(super) fn title_line(app: &DashboardApp) -> Line<'static> { + let mut spans = vec![Span::styled( + "workspace dashboard", + Style::default().add_modifier(Modifier::BOLD), + )]; if let Some(companion) = &app.panel.header.companion { spans.push(Span::styled( " · companion ", @@ -101,7 +91,7 @@ pub(super) fn draw_title(frame: &mut Frame<'_>, app: &DashboardApp, area: Rect) orchestrator_status_style(orchestrator.status), )); } - frame.render_widget(Paragraph::new(Line::from(spans)), area); + Line::from(spans) } pub(super) fn companion_status_style(status: CompanionPanelStatus) -> Style { @@ -688,115 +678,8 @@ pub(super) fn draw_target_status(frame: &mut Frame<'_>, app: &DashboardApp, area frame.render_widget(Paragraph::new(target_status_line(app)), area); } -pub(super) fn target_status_line(app: &DashboardApp) -> Line<'static> { - if !app.composer_is_blank() { - return Line::from(vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" · draft Enter ", Style::default().fg(Color::DarkGray)), - Span::styled( - composer_enter_status_text(app), - Style::default().fg(Color::Green), - ), - Span::styled( - " · row selection waits until composer is blank", - Style::default().fg(Color::DarkGray), - ), - ]); - } - - if let Some(row) = app - .selected_panel_row() - .filter(|row| row.is_ticket_action()) - { - let action = row - .next_action - .map(|action| panel_ticket_action_label(row, action)) - .unwrap_or("View"); - let mut spans = vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" · selected Ticket ", Style::default().fg(Color::DarkGray)), - Span::styled(row.status.clone(), panel_priority_style(row.priority)), - Span::styled(" · blank Enter ", Style::default().fg(Color::DarkGray)), - Span::styled(action, Style::default().fg(Color::Magenta)), - ]; - if let Some(reason) = panel_ticket_reason(row) { - spans.push(Span::styled(" · ", Style::default().fg(Color::DarkGray))); - spans.push(Span::styled( - truncate_with_ellipsis(reason, 100), - ticket_detail_style(row), - )); - } - 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![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" · selected Pod ", Style::default().fg(Color::DarkGray)), - Span::styled(status.to_string(), status_style), - Span::styled( - " · blank Enter open/attach", - Style::default().fg(Color::DarkGray), - ), - ]) - } else { - Line::from(vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default().fg(Color::DarkGray), - ), - Span::styled( - " · no row selected · ↑/↓ selects a row", - Style::default().fg(Color::DarkGray), - ), - ]) - } +pub(super) fn target_status_line(_app: &DashboardApp) -> Line<'static> { + Line::from(Span::raw("")) } pub(super) fn draw_input(frame: &mut Frame<'_>, render: &crate::input::InputRender, area: Rect) { @@ -817,103 +700,6 @@ pub(super) fn draw_input(frame: &mut Frame<'_>, render: &crate::input::InputRend } } -pub(super) fn composer_enter_status_text(app: &DashboardApp) -> String { - match app.composer_target() { - ComposerTarget::Companion => companion_enter_status_text(app), - ComposerTarget::TicketIntake - if app.selected_ticket_action() == Some(NextUserAction::Queue) => - { - "return selected ready Ticket to planning".to_string() - } - ComposerTarget::TicketIntake => "launch Intake with composer text".to_string(), - } -} - -pub(super) fn composer_enter_actionbar_text(app: &DashboardApp) -> String { - match app.composer_target() { - ComposerTarget::Companion => companion_enter_actionbar_text(app), - ComposerTarget::TicketIntake if app.selected_ticket_action() == Some(NextUserAction::Queue) => { - "Ticket Intake target: Enter records instructions and returns selected ready Ticket to planning".to_string() - } - ComposerTarget::TicketIntake => { - "Ticket Intake target: Enter launches Intake with composer text".to_string() - } - } -} - -pub(super) fn companion_enter_status_text(app: &DashboardApp) -> String { - match companion_send_availability(app) { - CompanionSendAvailability::Ready => "send composer text to workspace Companion".to_string(), - CompanionSendAvailability::Unavailable(reason) => format!("keep draft; {reason}"), - } -} - -pub(super) fn companion_enter_actionbar_text(app: &DashboardApp) -> String { - match companion_send_availability(app) { - CompanionSendAvailability::Ready => { - "Companion target: Enter sends composer text to workspace Companion".to_string() - } - CompanionSendAvailability::Unavailable(reason) => { - format!("Companion target: Enter keeps draft; {reason}") - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum CompanionSendAvailability { - Ready, - Unavailable(String), -} - -pub(super) fn companion_send_availability(app: &DashboardApp) -> CompanionSendAvailability { - let Some(companion) = app.panel.header.companion.as_ref() else { - return CompanionSendAvailability::Unavailable( - "workspace Companion is unavailable".to_string(), - ); - }; - if matches!( - companion.status, - CompanionPanelStatus::Unavailable - | CompanionPanelStatus::Missing - | CompanionPanelStatus::Stopped - ) { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion is {}", - companion.status.label() - )); - } - let Some(entry) = app - .list - .entries - .iter() - .find(|entry| entry.name == companion.pod_name) - else { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is not in the Pod list", - companion.pod_name - )); - }; - let Some(live) = entry.live.as_ref() else { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is stopped", - companion.pod_name - )); - }; - if !live.reachable { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is unreachable", - companion.pod_name - )); - } - if live.status == Some(PodStatus::Running) { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is running", - companion.pod_name - )); - } - CompanionSendAvailability::Ready -} - pub(super) fn actionbar_left_text(app: &DashboardApp) -> String { if app.sending && app.composer_target() == ComposerTarget::TicketIntake { "launching Ticket Intake…".to_string() @@ -927,52 +713,20 @@ pub(super) fn actionbar_left_text(app: &DashboardApp) -> String { Some(notice) => format!("{notice} Refreshing workspace…"), None => "Refreshing workspace…".to_string(), } - } else if !app.composer_is_blank() { - composer_enter_actionbar_text(app) } else if let Some(notice) = app.notice.as_deref() { notice.to_string() - } else if let Some(reason) = app.selected_open_disabled_reason() { - reason } else { - match app.composer_target() { - ComposerTarget::Companion => { - "Composer target: Companion; type text to send, or use ↑/↓ then blank Enter for rows" - .to_string() - } - ComposerTarget::TicketIntake => { - if app.selected_ticket_action() == Some(NextUserAction::Queue) { - "Composer target: Ticket Intake; text + Enter returns selected ready Ticket to planning".to_string() - } else { - "Composer target: Ticket Intake; type a request, then Enter launches Intake".to_string() - } - } - } + String::new() } } pub(super) fn actionbar_right_text(app: &DashboardApp) -> &'static str { if app.panel_diagnostic_open { - "F2/Esc close details Ctrl+C quit" + "F2/Esc close details" } else if app.panel_diagnostic.is_some() { - "F2 details ↑/↓ select row Enter selected row Tab target Esc clear selection Left/Right cursor Ctrl+C quit" - } else if !app.composer_is_blank() { - if app - .panel - .composer - .is_available(ComposerTarget::TicketIntake) - { - "↑/↓ draft lines Left/Right cursor Enter composer target Tab target Esc clear selection Ctrl+C quit" - } else { - "↑/↓ draft lines Left/Right cursor Enter composer target Esc clear selection Ctrl+C quit" - } - } else if app - .panel - .composer - .is_available(ComposerTarget::TicketIntake) - { - "↑/↓ select row Enter selected row Tab target Esc clear selection Left/Right cursor Ctrl+C quit" + "F2 details" } else { - "↑/↓ select row Enter selected row Esc clear selection Left/Right cursor Ctrl+C quit" + "" } } diff --git a/crates/tui/src/dashboard/tests.rs b/crates/tui/src/dashboard/tests.rs index 9a8367f4..af3f3846 100644 --- a/crates/tui/src/dashboard/tests.rs +++ b/crates/tui/src/dashboard/tests.rs @@ -1072,7 +1072,9 @@ fn mouse_click_selects_panel_row_for_blank_enter_action() { ); assert_eq!(app.selected_panel_row().unwrap().title, "Queued"); assert_eq!(app.selected_ticket_action(), Some(NextUserAction::Wait)); - assert!(plain_line(&target_status_line(&app)).contains("blank Enter Wait")); + let selected_title = + plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]); + assert!(selected_title.starts_with("▶ queued")); assert!(matches!( app.handle_key(key(KeyCode::Enter)), DashboardAction::DispatchTicketAction(request) if request.ticket_id == "TICKET-2" @@ -1152,7 +1154,7 @@ fn mouse_click_does_not_override_existing_composer_keyboard_behavior() { } #[test] -fn selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior() { +fn selected_ticket_row_with_non_empty_composer_hides_redundant_status_hints() { let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); panel.header.companion = Some(CompanionPanelState::new( "yoi", @@ -1185,14 +1187,12 @@ fn selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior() { let actionbar_right = actionbar_right_text(&app); let target_status = plain_line(&target_status_line(&app)); - assert!(actionbar_left.contains("Companion target: Enter sends composer text")); - assert!(actionbar_right.contains("Enter composer target")); - assert!(!actionbar_left.contains("Queue")); - assert!(!actionbar_right.contains("selected row")); - assert!(target_status.contains("composer target Companion")); - assert!(target_status.contains("draft Enter send composer text to workspace Companion")); - assert!(target_status.contains("row selection waits until composer is blank")); - assert!(!target_status.contains("blank Enter Queue")); + assert_eq!(actionbar_left, ""); + assert_eq!(actionbar_right, ""); + assert_eq!(target_status, ""); + let selected_title = + plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]); + assert!(selected_title.starts_with("▶ ready")); } #[test] @@ -1529,7 +1529,6 @@ fn dashboard_idle_live_selected_target_is_open_eligible() { let app = test_app(vec![live_info("idle", PodStatus::Idle)]); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); } #[test] @@ -1557,6 +1556,35 @@ fn dashboard_status_labels_preserve_explicit_live_statuses() { } } +#[test] +fn dashboard_title_omits_redundant_key_hint_guidance() { + let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); + panel.header.ticket_configured = true; + panel.header.companion = Some(CompanionPanelState::new( + "yoi", + CompanionPanelStatus::Live, + Some("idle".to_string()), + )); + let app = app_with_panel( + PodList::from_sources( + PodVisibilitySource::ResumePicker, + vec![], + vec![live_info("yoi", PodStatus::Idle)], + None, + 10, + ), + panel, + ); + + let title = plain_line(&title_line(&app)); + + assert!(title.contains("workspace dashboard")); + assert!(title.contains("companion live")); + assert!(!title.contains("Row selection")); + assert!(!title.contains("blank Enter")); + assert!(!title.contains("Tab target")); +} + #[test] fn panel_ticket_rows_render_state_title_then_detail_line() { let row = panel_test_ticket_row( @@ -1725,7 +1753,7 @@ fn panel_ticket_intake_child_rows_render_as_indented_single_line() { } #[test] -fn selected_ticket_intake_child_status_is_not_rendered_as_generic_ticket_or_pod() { +fn selected_ticket_intake_child_keeps_row_marker_without_status_line() { let ticket_id = "00001TICKET"; let pod_name = "intake-live"; let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); @@ -1750,11 +1778,11 @@ fn selected_ticket_intake_child_status_is_not_rendered_as_generic_ticket_or_pod( 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")); + assert_eq!(status, ""); + + let title_line = plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]); + assert!(title_line.starts_with(" ▶ live")); + assert!(title_line.contains("Intake")); } #[test] @@ -1828,15 +1856,12 @@ fn dashboard_running_paused_and_stopped_targets_are_open_eligible() { app.ensure_selection_visible(); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); app.select_next(); assert_eq!(app.list.selected_entry().unwrap().name, "paused"); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); app.select_next(); assert_eq!(app.list.selected_entry().unwrap().name, "stopped"); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); } #[test]