fix: remove panel companion progress feed

This commit is contained in:
Keisuke Hirata 2026-06-13 11:51:22 +09:00
parent 2a7c96909c
commit 7ff2f8e3e8
No known key found for this signature in database
2 changed files with 0 additions and 440 deletions

View File

@ -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<String>,
}
impl CompanionProgressNoticeResult {
fn sent(fingerprint: String, updated_at: String) -> Self {
Self {
fingerprint,
updated_at,
error: None,
}
}
fn failed(fingerprint: String, error: impl Into<String>) -> Self {
Self {
fingerprint,
updated_at: String::new(),
error: Some(error.into()),
}
}
}
#[derive(Debug, Serialize)]
struct CompanionProgressTemplateContext {
companion: CompanionProgressTemplateRole,
orchestrator: CompanionProgressTemplateRole,
tickets: Vec<CompanionProgressTemplateTicket>,
omitted_ticket_count: usize,
role_pods: Vec<CompanionProgressTemplateRolePod>,
}
#[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<OrchestratorActiveWorkItem>,
@ -748,7 +666,6 @@ pub(crate) struct MultiPodApp {
runtime_command: PodRuntimeCommand,
last_companion_lifecycle_failure: Option<CompanionPanelState>,
last_orchestrator_lifecycle_failure: Option<OrchestratorPanelState>,
companion_progress: Option<CompanionProgressFreshness>,
orchestrator_work_set: OrchestratorWorkSet,
orchestrator_queue_attention: Option<OrchestratorQueueAttentionFreshness>,
}
@ -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<CompanionProgressNoticeRequest> {
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<CompanionProgressNoticeTarget> {
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<CompanionProgressNotice> {
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::<Vec<_>>();
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::<Vec<_>>();
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<String, minijinja::Error> {
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<CompanionProgressTemplateRolePod> {
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,
};

View File

@ -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 %}