fix: resource-back companion progress notice

This commit is contained in:
Keisuke Hirata 2026-06-13 00:38:52 +09:00
parent 724b79f1c0
commit 61e6c0683c
No known key found for this signature in database
5 changed files with 137 additions and 57 deletions

1
Cargo.lock generated
View File

@ -3962,6 +3962,7 @@ dependencies = [
"crossterm 0.28.1",
"llm-worker",
"manifest",
"minijinja",
"pod-registry",
"pod-store",
"protocol",

View File

@ -22,6 +22,7 @@ pod-registry = { workspace = true }
provider = { workspace = true }
ticket = { workspace = true }
serde = { workspace = true, features = ["derive"] }
minijinja = "2.19.0"
pulldown-cmark = { version = "0.13.3", default-features = false }
llm-worker.workspace = true

View File

@ -21,6 +21,7 @@ use ratatui::layout::{Constraint, Layout, Position, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
use serde::Serialize;
use session_store::FsStore;
use ticket::config::TicketConfig;
use ticket::{LocalTicketBackend, TicketBackend, TicketIdOrSlug, TicketWorkflowState};
@ -52,6 +53,8 @@ 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 SOCKET_OP_TIMEOUT: Duration = Duration::from_secs(3);
const MULTI_POD_POLL_INTERVAL: Duration = Duration::from_millis(1_500);
const TERMINAL_EVENT_POLL_INTERVAL: Duration = Duration::from_millis(100);
@ -573,6 +576,35 @@ impl CompanionProgressNoticeResult {
}
}
#[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,
}
pub(crate) struct MultiPodApp {
pub(crate) list: PodList,
pub(crate) panel: WorkspacePanelViewModel,
@ -2426,57 +2458,44 @@ fn companion_progress_notice(
) -> Option<CompanionProgressNotice> {
let companion = panel.header.companion.as_ref()?;
let orchestrator = panel.header.orchestrator.as_ref()?;
let mut lines = vec![
"Orchestrator progress context (read-only weak notification; no auto-run).".to_string(),
"Reason: workspace Panel refreshed bounded orchestration progress for Companion explanation."
.to_string(),
format!(
"Roles: Companion {} is {}; Orchestrator {} is {}.",
companion.pod_name,
companion.status.label(),
orchestrator.pod_name,
orchestrator.status.label()
),
];
let mut ticket_lines = Vec::new();
for row in panel.rows.iter().take(COMPANION_PROGRESS_MAX_TICKETS) {
let ticket_id = row
.ticket
.as_ref()
.map(|ticket| ticket.id.as_str())
.unwrap_or("unknown-ticket");
ticket_lines.push(format!(
"- {} [{}] {} (ref: .yoi/tickets/{})",
ticket_id,
row.status,
bounded_progress_text(&row.title, COMPANION_PROGRESS_MAX_TITLE_CHARS),
ticket_id
));
}
if ticket_lines.is_empty() {
lines.push("Tickets: none visible in the current Panel snapshot.".to_string());
} else {
lines.push(format!(
"Tickets (first {} visible, bounded):",
ticket_lines.len()
));
lines.extend(ticket_lines);
if panel.rows.len() > COMPANION_PROGRESS_MAX_TICKETS {
lines.push(format!(
"- … {} more ticket(s) omitted from this bounded notice.",
panel.rows.len() - COMPANION_PROGRESS_MAX_TICKETS
));
}
}
let role_pod_lines = bounded_role_pod_lines(list, companion, orchestrator);
if !role_pod_lines.is_empty() {
lines.push("Role pod status snapshot:".to_string());
lines.extend(role_pod_lines);
}
let message = bounded_progress_text(&lines.join("\n"), COMPANION_PROGRESS_MAX_MESSAGE_CHARS);
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,
@ -2484,19 +2503,35 @@ fn companion_progress_notice(
})
}
fn bounded_role_pod_lines(
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<String> {
let mut lines = Vec::new();
) -> 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;
};
lines.push(format!("- {}: {}", entry.name, row_status_label(entry).0));
role_pods.push(CompanionProgressTemplateRolePod {
name: bounded_progress_text(&entry.name, COMPANION_PROGRESS_MAX_TITLE_CHARS),
status: row_status_label(entry).0.to_string(),
});
}
lines
role_pods
}
fn bounded_progress_text(input: &str, max_chars: usize) -> String {
@ -4987,6 +5022,37 @@ mod tests {
);
}
#[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![

View File

@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-WvMpHbTswYeRrkw5I4V4E1RnG7j13PbuQCbeas/XILs=";
cargoHash = "sha256-o47Erp9UrS2Rgwd0JNpuYPO4pZmv62DkzY9KXMQpyAM=";
depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint,

View File

@ -0,0 +1,12 @@
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 %}