fix: resource-back companion progress notice
This commit is contained in:
parent
724b79f1c0
commit
61e6c0683c
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3962,6 +3962,7 @@ dependencies = [
|
|||
"crossterm 0.28.1",
|
||||
"llm-worker",
|
||||
"manifest",
|
||||
"minijinja",
|
||||
"pod-registry",
|
||||
"pod-store",
|
||||
"protocol",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
12
resources/prompts/panel/companion_progress_notice.md
Normal file
12
resources/prompts/panel/companion_progress_notice.md
Normal 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 %}
|
||||
Loading…
Reference in New Issue
Block a user