From 7ff2f8e3e83781ff5f39c6902d7572a686251c8e Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 13 Jun 2026 11:51:22 +0900 Subject: [PATCH] fix: remove panel companion progress feed --- crates/tui/src/multi_pod.rs | 428 ------------------ .../panel/companion_progress_notice.md | 12 - 2 files changed, 440 deletions(-) delete mode 100644 resources/prompts/panel/companion_progress_notice.md diff --git a/crates/tui/src/multi_pod.rs b/crates/tui/src/multi_pod.rs index d96adc09..11f991ab 100644 --- a/crates/tui/src/multi_pod.rs +++ b/crates/tui/src/multi_pod.rs @@ -51,11 +51,6 @@ use crate::workspace_panel::{ const MAX_ENTRIES: usize = 50; const CLOSED_VISIBLE_ROWS: usize = 3; -const COMPANION_PROGRESS_MAX_TICKETS: usize = 5; -const COMPANION_PROGRESS_MAX_TITLE_CHARS: usize = 80; -const COMPANION_PROGRESS_MAX_MESSAGE_CHARS: usize = 1_800; -const COMPANION_PROGRESS_NOTICE_TEMPLATE: &str = - include_str!("../../../resources/prompts/panel/companion_progress_notice.md"); const ORCHESTRATOR_IDLE_QUEUE_NOTICE_TEMPLATE: &str = include_str!("../../../resources/prompts/panel/orchestrator_idle_queue_notice.md"); const ORCHESTRATOR_QUEUE_ATTENTION_MAX_TICKETS: usize = 6; @@ -142,10 +137,6 @@ pub(crate) async fn run( let result = dispatch_orchestrator_queue_attention_notice(request).await; app.finish_orchestrator_queue_attention_notice(result); } - if let Some(request) = app.prepare_companion_progress_notice() { - let result = dispatch_companion_progress_notice(request).await; - app.finish_companion_progress_notice(result); - } } terminal.draw(|f| draw(f, app))?; @@ -542,79 +533,6 @@ struct PanelDiagnostic { details: String, } -#[derive(Debug, Clone, PartialEq, Eq)] -struct CompanionProgressFreshness { - fingerprint: String, - updated_at: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct CompanionProgressNotice { - message: String, - fingerprint: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct CompanionProgressNoticeRequest { - pod_name: String, - socket_path: PathBuf, - notice: CompanionProgressNotice, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct CompanionProgressNoticeResult { - fingerprint: String, - updated_at: String, - error: Option, -} - -impl CompanionProgressNoticeResult { - fn sent(fingerprint: String, updated_at: String) -> Self { - Self { - fingerprint, - updated_at, - error: None, - } - } - - fn failed(fingerprint: String, error: impl Into) -> Self { - Self { - fingerprint, - updated_at: String::new(), - error: Some(error.into()), - } - } -} - -#[derive(Debug, Serialize)] -struct CompanionProgressTemplateContext { - companion: CompanionProgressTemplateRole, - orchestrator: CompanionProgressTemplateRole, - tickets: Vec, - omitted_ticket_count: usize, - role_pods: Vec, -} - -#[derive(Debug, Serialize)] -struct CompanionProgressTemplateRole { - pod_name: String, - status: String, -} - -#[derive(Debug, Serialize)] -struct CompanionProgressTemplateTicket { - id: String, - state: String, - title: String, - reference: String, -} - -#[derive(Debug, Serialize)] -struct CompanionProgressTemplateRolePod { - name: String, - status: String, -} - #[derive(Debug, Clone, PartialEq, Eq, Default)] struct OrchestratorWorkSet { active_inprogress: Vec, @@ -748,7 +666,6 @@ pub(crate) struct MultiPodApp { runtime_command: PodRuntimeCommand, last_companion_lifecycle_failure: Option, last_orchestrator_lifecycle_failure: Option, - companion_progress: Option, orchestrator_work_set: OrchestratorWorkSet, orchestrator_queue_attention: Option, } @@ -784,7 +701,6 @@ impl MultiPodApp { runtime_command, last_companion_lifecycle_failure: None, last_orchestrator_lifecycle_failure: None, - companion_progress: None, orchestrator_work_set: OrchestratorWorkSet::default(), orchestrator_queue_attention: None, } @@ -839,7 +755,6 @@ impl MultiPodApp { self.ensure_composer_target_available(); self.refresh_orchestrator_work_set(); self.apply_orchestrator_work_set_detail(); - self.apply_companion_progress_freshness(); } fn prepare_orchestrator_queue_attention_notice( @@ -895,49 +810,6 @@ impl MultiPodApp { apply_orchestrator_detail(&mut self.panel, detail); } - fn prepare_companion_progress_notice(&mut self) -> Option { - let target = companion_progress_notice_target(&self.panel, &self.list)?; - let notice = companion_progress_notice(&self.panel, &self.list)?; - if self - .companion_progress - .as_ref() - .is_some_and(|freshness| freshness.fingerprint == notice.fingerprint) - { - self.apply_companion_progress_freshness(); - return None; - } - Some(CompanionProgressNoticeRequest { - pod_name: target.pod_name, - socket_path: target.socket_path, - notice, - }) - } - - fn finish_companion_progress_notice(&mut self, result: CompanionProgressNoticeResult) { - if let Some(error) = result.error { - self.notice = Some(format!("Companion progress notice not delivered: {error}")); - return; - } - self.companion_progress = Some(CompanionProgressFreshness { - fingerprint: result.fingerprint, - updated_at: result.updated_at, - }); - self.apply_companion_progress_freshness(); - } - - fn apply_companion_progress_freshness(&mut self) { - let Some(freshness) = self.companion_progress.as_ref() else { - return; - }; - let Some(companion) = self.panel.header.companion.as_mut() else { - return; - }; - companion.detail = Some(format!( - "progress context updated at {} (weak notify)", - freshness.updated_at - )); - } - fn apply_companion_lifecycle_memory(&mut self, panel: &mut WorkspacePanelViewModel) { let Some(state) = panel.header.companion.as_ref() else { self.last_companion_lifecycle_failure = None; @@ -2910,123 +2782,6 @@ fn apply_orchestrator_detail(panel: &mut WorkspacePanelViewModel, detail: Option } } -#[derive(Debug, Clone, PartialEq, Eq)] -struct CompanionProgressNoticeTarget { - pod_name: String, - socket_path: PathBuf, -} - -fn companion_progress_notice_target( - panel: &WorkspacePanelViewModel, - list: &PodList, -) -> Option { - let companion = panel.header.companion.as_ref()?; - if !companion_status_is_peer_reachable(companion.status) { - return None; - } - let entry = list - .entries - .iter() - .find(|entry| entry.name == companion.pod_name)?; - let live = entry.live.as_ref()?; - if !live.reachable { - return None; - } - Some(CompanionProgressNoticeTarget { - pod_name: companion.pod_name.clone(), - socket_path: live.socket_path.clone(), - }) -} - -fn companion_status_is_peer_reachable(status: CompanionPanelStatus) -> bool { - matches!( - status, - CompanionPanelStatus::Live | CompanionPanelStatus::Restored | CompanionPanelStatus::Spawned - ) -} - -fn companion_progress_notice( - panel: &WorkspacePanelViewModel, - list: &PodList, -) -> Option { - let companion = panel.header.companion.as_ref()?; - let orchestrator = panel.header.orchestrator.as_ref()?; - let ticket_rows = panel - .rows - .iter() - .filter_map(|row| row.ticket.as_ref().map(|ticket| (row, ticket))) - .collect::>(); - let tickets = ticket_rows - .iter() - .take(COMPANION_PROGRESS_MAX_TICKETS) - .map(|(row, ticket)| CompanionProgressTemplateTicket { - id: bounded_progress_text(&ticket.id, COMPANION_PROGRESS_MAX_TITLE_CHARS), - state: bounded_progress_text(&row.status, COMPANION_PROGRESS_MAX_TITLE_CHARS), - title: bounded_progress_text(&row.title, COMPANION_PROGRESS_MAX_TITLE_CHARS), - reference: format!(".yoi/tickets/{}", ticket.id), - }) - .collect::>(); - let context = CompanionProgressTemplateContext { - companion: CompanionProgressTemplateRole { - pod_name: bounded_progress_text( - &companion.pod_name, - COMPANION_PROGRESS_MAX_TITLE_CHARS, - ), - status: companion.status.label().to_string(), - }, - orchestrator: CompanionProgressTemplateRole { - pod_name: bounded_progress_text( - &orchestrator.pod_name, - COMPANION_PROGRESS_MAX_TITLE_CHARS, - ), - status: orchestrator.status.label().to_string(), - }, - tickets, - omitted_ticket_count: ticket_rows - .len() - .saturating_sub(COMPANION_PROGRESS_MAX_TICKETS), - role_pods: bounded_role_pod_values(list, companion, orchestrator), - }; - let rendered = render_companion_progress_notice_template(&context).ok()?; - let message = bounded_progress_text(&rendered, COMPANION_PROGRESS_MAX_MESSAGE_CHARS); - let fingerprint = message.clone(); - Some(CompanionProgressNotice { - message, - fingerprint, - }) -} - -fn render_companion_progress_notice_template( - context: &CompanionProgressTemplateContext, -) -> Result { - let mut env = minijinja::Environment::new(); - env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict); - env.add_template( - "companion_progress_notice", - COMPANION_PROGRESS_NOTICE_TEMPLATE, - )?; - env.get_template("companion_progress_notice")? - .render(context) -} - -fn bounded_role_pod_values( - list: &PodList, - companion: &CompanionPanelState, - orchestrator: &OrchestratorPanelState, -) -> Vec { - let mut role_pods = Vec::new(); - for name in [&companion.pod_name, &orchestrator.pod_name] { - let Some(entry) = list.entries.iter().find(|entry| entry.name == *name) else { - continue; - }; - role_pods.push(CompanionProgressTemplateRolePod { - name: bounded_progress_text(&entry.name, COMPANION_PROGRESS_MAX_TITLE_CHARS), - status: row_status_label(entry).0.to_string(), - }); - } - role_pods -} - fn bounded_progress_text(input: &str, max_chars: usize) -> String { let mut output = String::new(); for (idx, ch) in input.chars().enumerate() { @@ -3051,19 +2806,6 @@ fn progress_notice_timestamp() -> String { } } -async fn dispatch_companion_progress_notice( - request: CompanionProgressNoticeRequest, -) -> CompanionProgressNoticeResult { - let fingerprint = request.notice.fingerprint.clone(); - match send_notify_only(&request.socket_path, request.notice.message, false).await { - Ok(()) => CompanionProgressNoticeResult::sent(fingerprint, progress_notice_timestamp()), - Err(err) => CompanionProgressNoticeResult::failed( - fingerprint, - format!("{}: {}", request.pod_name, err), - ), - } -} - async fn dispatch_orchestrator_queue_attention_notice( request: OrchestratorQueueAttentionNoticeRequest, ) -> OrchestratorQueueAttentionNoticeResult { @@ -5492,175 +5234,6 @@ mod tests { )); } - #[test] - fn companion_progress_notice_target_skips_missing_stopped_and_unreachable_without_spawn_restore() - { - let missing_app = ticket_enabled_app(vec![live_info("test-orchestrator", PodStatus::Idle)]); - assert!(companion_progress_notice_target(&missing_app.panel, &missing_app.list).is_none()); - - let mut stopped_panel = WorkspacePanelViewModel::empty(Path::new("test")); - stopped_panel.header.companion = Some(CompanionPanelState::new( - "yoi", - CompanionPanelStatus::Stopped, - None, - )); - stopped_panel.header.orchestrator = Some(OrchestratorPanelState::new( - "test-orchestrator", - OrchestratorPanelStatus::Live, - None, - )); - let stopped_list = PodList::from_sources( - PodVisibilitySource::ResumePicker, - vec![stopped_info("yoi")], - vec![live_info("test-orchestrator", PodStatus::Idle)], - None, - 10, - ); - assert!(companion_progress_notice_target(&stopped_panel, &stopped_list).is_none()); - - let mut unreachable = live_info("yoi", PodStatus::Idle); - unreachable.reachable = false; - let unreachable_app = ticket_enabled_app(vec![ - unreachable, - live_info("test-orchestrator", PodStatus::Idle), - ]); - assert!( - companion_progress_notice_target(&unreachable_app.panel, &unreachable_app.list) - .is_none() - ); - } - - #[test] - fn companion_progress_notice_uses_prompt_resource_template() { - let first_resource_line = COMPANION_PROGRESS_NOTICE_TEMPLATE.lines().next().unwrap(); - let context = CompanionProgressTemplateContext { - companion: CompanionProgressTemplateRole { - pod_name: "yoi".to_string(), - status: "Live".to_string(), - }, - orchestrator: CompanionProgressTemplateRole { - pod_name: "test-orchestrator".to_string(), - status: "Live".to_string(), - }, - tickets: vec![CompanionProgressTemplateTicket { - id: "RESOURCE-TICKET".to_string(), - state: "inprogress".to_string(), - title: "Rendered from runtime values".to_string(), - reference: ".yoi/tickets/RESOURCE-TICKET".to_string(), - }], - omitted_ticket_count: 0, - role_pods: vec![CompanionProgressTemplateRolePod { - name: "yoi".to_string(), - status: "idle".to_string(), - }], - }; - - let rendered = render_companion_progress_notice_template(&context).unwrap(); - assert!(rendered.contains(first_resource_line)); - assert!(rendered.contains("RESOURCE-TICKET")); - assert!(rendered.contains("Rendered from runtime values")); - } - - #[test] - fn companion_progress_notice_is_bounded_and_excludes_sensitive_unbounded_fields() { - let mut app = ticket_enabled_app(vec![ - live_info("yoi", PodStatus::Idle), - live_info("test-orchestrator", PodStatus::Running), - ]); - app.panel.rows = (0..12) - .map(|index| { - let mut row = panel_test_ticket_row( - &format!("TICKET-{index}"), - &format!("Visible title {index} {}", "x".repeat(140)), - ActionPriority::Background, - NextUserAction::Wait, - "inprogress", - ); - if let Some(ticket) = row.ticket.as_mut() { - ticket.latest_event_excerpt = Some( - "SECRET_PROVIDER_ERROR_TOKEN should never be copied into progress notices" - .to_string(), - ); - } - row.subtitle = Some("private thread excerpt should stay out".to_string()); - row - }) - .collect(); - app.panel - .header - .diagnostics - .push("diagnostic with SECRET_PROVIDER_ERROR_TOKEN should stay out".to_string()); - - let notice = companion_progress_notice(&app.panel, &app.list).unwrap(); - assert!(notice.message.contains("TICKET-0")); - assert!(notice.message.contains("ref: .yoi/tickets/TICKET-0")); - assert!(notice.message.contains("more ticket(s) omitted")); - assert!(notice.message.chars().count() <= COMPANION_PROGRESS_MAX_MESSAGE_CHARS + 1); - assert!(!notice.message.contains("SECRET_PROVIDER_ERROR_TOKEN")); - assert!(!notice.message.contains("private thread excerpt")); - assert_eq!(notice.fingerprint, notice.message); - } - - #[test] - fn companion_progress_notice_success_sets_panel_freshness_without_persisting_snapshot() { - let mut app = ticket_enabled_app(vec![ - live_info("yoi", PodStatus::Idle), - live_info("test-orchestrator", PodStatus::Idle), - ]); - app.panel.rows.push(panel_test_ticket_row( - "TICKET-1", - "Implement progress notices", - ActionPriority::Background, - NextUserAction::Wait, - "inprogress", - )); - - let request = app.prepare_companion_progress_notice().unwrap(); - assert_eq!(request.pod_name, "yoi"); - app.finish_companion_progress_notice(CompanionProgressNoticeResult::sent( - request.notice.fingerprint, - "unix:42".to_string(), - )); - - let detail = app - .panel - .header - .companion - .as_ref() - .and_then(|companion| companion.detail.as_deref()) - .unwrap(); - assert!(detail.contains("unix:42")); - assert!(detail.contains("weak notify")); - assert!(app.prepare_companion_progress_notice().is_none()); - } - - #[test] - fn companion_progress_notice_target_accepts_live_running_companion() { - let app = ticket_enabled_app(vec![ - live_info("yoi", PodStatus::Running), - live_info("test-orchestrator", PodStatus::Running), - ]); - let target = companion_progress_notice_target(&app.panel, &app.list).unwrap(); - assert_eq!(target.pod_name, "yoi"); - assert_eq!(target.socket_path, PathBuf::from("/tmp/yoi.sock")); - } - - #[test] - fn companion_progress_failure_is_best_effort_and_does_not_mark_freshness() { - let mut app = ticket_enabled_app(vec![ - live_info("yoi", PodStatus::Idle), - live_info("test-orchestrator", PodStatus::Idle), - ]); - let request = app.prepare_companion_progress_notice().unwrap(); - app.finish_companion_progress_notice(CompanionProgressNoticeResult::failed( - request.notice.fingerprint, - "socket closed", - )); - - assert!(app.companion_progress.is_none()); - assert!(app.notice.as_deref().unwrap().contains("not delivered")); - } - #[test] fn no_ticket_selection_keeps_enter_pod_centric() { let mut app = test_app(vec![live_info("alpha", PodStatus::Idle)]); @@ -7254,7 +6827,6 @@ mod tests { runtime_command: PodRuntimeCommand::for_executable("/tmp/yoi"), last_companion_lifecycle_failure, last_orchestrator_lifecycle_failure, - companion_progress: None, orchestrator_work_set: OrchestratorWorkSet::default(), orchestrator_queue_attention: None, }; diff --git a/resources/prompts/panel/companion_progress_notice.md b/resources/prompts/panel/companion_progress_notice.md deleted file mode 100644 index 7c52ee05..00000000 --- a/resources/prompts/panel/companion_progress_notice.md +++ /dev/null @@ -1,12 +0,0 @@ -Orchestrator progress context (read-only weak notification; no auto-run). -Reason: workspace Panel refreshed bounded orchestration progress for Companion explanation. -Roles: Companion {{ companion.pod_name }} is {{ companion.status }}; Orchestrator {{ orchestrator.pod_name }} is {{ orchestrator.status }}. - -{% if tickets %}Tickets (first {{ tickets | length }} visible, bounded): -{% for ticket in tickets %}- {{ ticket.id }} [{{ ticket.state }}] {{ ticket.title }} (ref: {{ ticket.reference }}) -{% endfor %}{% if omitted_ticket_count > 0 %}- … {{ omitted_ticket_count }} more ticket(s) omitted from this bounded notice. -{% endif %}{% else %}Tickets: none visible in the current Panel snapshot. -{% endif %}{% if role_pods %} -Role pod status snapshot: -{% for role_pod in role_pods %}- {{ role_pod.name }}: {{ role_pod.status }} -{% endfor %}{% endif %}