use sim_kernel::{Expr, Symbol};
use sim_lib_scene::{data_map, node, sym};
use sim_lib_stream_core::{DevCassette, StreamPacket};
use sim_value::build::uint;
pub const MISSION_CONTROL_LENS: &str = "view:agent-mission-control";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MissionControlState {
pub missions: Vec<MissionCard>,
pub lease_conflicts: Vec<LeaseConflictCard>,
pub evidence: Vec<EvidenceEvent>,
pub intents: Vec<MissionControlIntent>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MissionCard {
pub id: Symbol,
pub goal: String,
pub roles: Vec<String>,
pub recipe_pattern: String,
pub leases: Vec<LeaseClaim>,
pub validation: ValidationState,
pub human_gates: Vec<HumanGate>,
pub facets: Vec<ExplanationFacet>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LeaseClaim {
pub target_kind: String,
pub target: String,
pub mode: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LeaseConflictCard {
pub left_mission: Symbol,
pub left: LeaseClaim,
pub right_mission: Symbol,
pub right: LeaseClaim,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ValidationState {
Pending,
Running,
Passed,
Failed,
}
impl ValidationState {
pub fn token(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Running => "running",
Self::Passed => "passed",
Self::Failed => "failed",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HumanGate {
pub id: String,
pub prompt: String,
pub status: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExplanationFacet {
pub label: String,
pub evidence: String,
pub confidence: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EvidenceEvent {
pub sequence: u64,
pub kind: Symbol,
pub summary: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MissionControlIntent {
pub kind: &'static str,
pub label: &'static str,
}
pub fn mission_control_view(state: &MissionControlState) -> Expr {
node(
"stack",
vec![
("role", sym("mission-control")),
("dir", sym("column")),
(
"children",
Expr::List(vec![
mission_summary(state),
mission_list(&state.missions),
evidence_replay(&state.evidence),
lease_conflicts(&state.lease_conflicts),
command_bar(&state.intents),
]),
),
],
)
}
pub fn mission_control_replay_frames(state: &MissionControlState) -> Vec<Expr> {
(0..=state.evidence.len())
.map(|count| {
let mut frame = state.clone();
frame.evidence.truncate(count);
mission_control_view(&frame)
})
.collect()
}
pub fn evidence_from_dev_cassette(cassette: &DevCassette) -> Vec<EvidenceEvent> {
cassette
.cassette()
.envelopes()
.iter()
.enumerate()
.filter_map(|(sequence, envelope)| {
let StreamPacket::Data(packet) = envelope.packet() else {
return None;
};
Some(EvidenceEvent {
sequence: sequence as u64,
kind: packet.kind.clone(),
summary: payload_summary(&packet.payload)
.unwrap_or_else(|| packet.kind.name.to_string()),
})
})
.collect()
}
pub fn mission_control_intents() -> Vec<MissionControlIntent> {
vec![
MissionControlIntent {
kind: "approve",
label: "Approve",
},
MissionControlIntent {
kind: "reject",
label: "Reject",
},
MissionControlIntent {
kind: "ask",
label: "Ask",
},
MissionControlIntent {
kind: "split-mission",
label: "Split",
},
MissionControlIntent {
kind: "pause-agent",
label: "Pause",
},
MissionControlIntent {
kind: "rerun-validation",
label: "Rerun validation",
},
MissionControlIntent {
kind: "replay-cassette",
label: "Replay",
},
MissionControlIntent {
kind: "open-source",
label: "Open source",
},
]
}
fn mission_summary(state: &MissionControlState) -> Expr {
node(
"box",
vec![
("role", sym("mission-summary")),
(
"children",
Expr::List(vec![
text(format!("missions: {}", state.missions.len())),
text(format!("evidence events: {}", state.evidence.len())),
text(format!("lease conflicts: {}", state.lease_conflicts.len())),
]),
),
],
)
}
fn mission_list(missions: &[MissionCard]) -> Expr {
node(
"grid",
vec![
("role", sym("missions")),
(
"children",
Expr::List(missions.iter().map(mission_card).collect()),
),
],
)
}
fn mission_card(mission: &MissionCard) -> Expr {
node(
"box",
vec![
("role", sym("mission-card")),
("mission", Expr::Symbol(mission.id.clone())),
(
"children",
Expr::List(vec![
text(mission.goal.clone()),
data_line("recipe-pattern", &mission.recipe_pattern),
badge(mission.validation.token(), mission.validation.token()),
list_box("roles", mission.roles.iter().cloned()),
list_box("leases", mission.leases.iter().map(LeaseClaim::label)),
list_box(
"human-gates",
mission.human_gates.iter().map(HumanGate::label),
),
list_box(
"explanation",
mission.facets.iter().map(ExplanationFacet::label),
),
]),
),
],
)
}
fn evidence_replay(evidence: &[EvidenceEvent]) -> Expr {
let events = evidence
.iter()
.map(|event| {
data_map(vec![
("at", uint(event.sequence)),
("event", Expr::Symbol(event.kind.clone())),
("label", Expr::String(event.summary.clone())),
])
})
.collect();
node(
"box",
vec![
("role", sym("evidence-replay")),
(
"children",
Expr::List(vec![
node(
"timeline",
vec![
("lane", sym("dev-cassette")),
("events", Expr::List(events)),
],
),
node(
"slider",
vec![
("target", sym("replay-cassette")),
("value", uint(evidence.len() as u64)),
("max", uint(evidence.len() as u64)),
],
),
]),
),
],
)
}
fn lease_conflicts(conflicts: &[LeaseConflictCard]) -> Expr {
let rows = conflicts
.iter()
.map(|conflict| {
node(
"text",
vec![(
"text",
Expr::String(format!(
"{} {} conflicts with {} {}",
conflict.left_mission,
conflict.left.label(),
conflict.right_mission,
conflict.right.label()
)),
)],
)
})
.collect();
node(
"box",
vec![
("role", sym("lease-conflicts")),
("children", Expr::List(rows)),
],
)
}
fn command_bar(intents: &[MissionControlIntent]) -> Expr {
node(
"stack",
vec![
("role", sym("mission-intents")),
("dir", sym("row")),
(
"children",
Expr::List(
intents
.iter()
.map(|intent| {
node(
"button",
vec![
(
"intent",
Expr::Symbol(Symbol::qualified("intent", intent.kind)),
),
("label", Expr::String(intent.label.to_owned())),
],
)
})
.collect(),
),
),
],
)
}
fn payload_summary(expr: &Expr) -> Option<String> {
let Expr::Map(entries) = expr else {
return None;
};
entries.iter().find_map(|(key, value)| {
let Expr::Symbol(symbol) = key else {
return None;
};
if symbol.namespace.is_none() && symbol.name.as_ref() == "summary" {
match value {
Expr::String(summary) => Some(summary.clone()),
_ => None,
}
} else {
None
}
})
}
fn data_line(label: &str, value: &str) -> Expr {
text(format!("{label}: {value}"))
}
fn list_box(role: &str, items: impl Iterator<Item = String>) -> Expr {
node(
"box",
vec![
("role", sym(role)),
("children", Expr::List(items.map(text).collect())),
],
)
}
fn text(content: impl Into<String>) -> Expr {
node("text", vec![("text", Expr::String(content.into()))])
}
fn badge(status: &str, label: &str) -> Expr {
node(
"badge",
vec![
("status", sym(status)),
("label", Expr::String(label.to_owned())),
],
)
}
impl LeaseClaim {
fn label(&self) -> String {
format!("{}:{} ({})", self.target_kind, self.target, self.mode)
}
}
impl HumanGate {
fn label(&self) -> String {
format!("{} ({})", self.prompt, self.status)
}
}
impl ExplanationFacet {
fn label(&self) -> String {
format!("{}: {} ({})", self.label, self.evidence, self.confidence)
}
}