use std::{
collections::BTreeMap,
io,
sync::mpsc::{Receiver, SyncSender, TryRecvError, sync_channel},
time::Duration,
};
use anyhow::Result;
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind, KeyModifiers},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use proofborne_core::{
Criterion, CriterionState, EventEnvelope, ProofTermination, RunOutcome, TaskContract,
};
use proofborne_runtime::{ApprovalHandler, ElicitationHandler, PolicyRequest};
use proofborne_runtime::{EventSink, RunControl, RunResult};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap},
};
use serde::Serialize;
use serde_json::Value;
enum ContractScreen {
Goal,
Criterion,
Confirm,
}
pub fn capture_task_contract() -> Result<Option<TaskContract>> {
let mut terminal = TerminalGuard::start()?;
let mut goal = String::new();
let mut criterion = String::new();
let mut screen = ContractScreen::Goal;
let result = loop {
terminal.draw(|frame| draw_contract(frame, &goal, &criterion, &screen))?;
if !event::poll(Duration::from_millis(250))? {
continue;
}
let Event::Key(key) = event::read()? else {
continue;
};
if key.kind != KeyEventKind::Press {
continue;
}
match (&screen, key.code) {
(_, KeyCode::Esc) => break None,
(ContractScreen::Goal, KeyCode::Enter) if !goal.trim().is_empty() => {
screen = ContractScreen::Criterion;
}
(ContractScreen::Goal, KeyCode::Backspace) => {
goal.pop();
}
(ContractScreen::Goal, KeyCode::Char(character)) => goal.push(character),
(ContractScreen::Criterion, KeyCode::Enter) if !criterion.trim().is_empty() => {
screen = ContractScreen::Confirm;
}
(ContractScreen::Criterion, KeyCode::Backspace) => {
criterion.pop();
}
(ContractScreen::Criterion, KeyCode::Char(character)) => criterion.push(character),
(ContractScreen::Confirm, KeyCode::Char('y' | 'Y')) => {
break Some(task_contract(&goal, &criterion));
}
(ContractScreen::Confirm, KeyCode::Char('n' | 'N') | KeyCode::Backspace) => {
screen = ContractScreen::Criterion;
}
_ => {}
}
};
Ok(result)
}
pub fn live_event_channel() -> (LiveEventSink, Receiver<EventEnvelope>) {
let (sender, receiver) = sync_channel(256);
(LiveEventSink { sender }, receiver)
}
#[derive(Clone)]
pub struct LiveEventSink {
sender: SyncSender<EventEnvelope>,
}
pub enum LiveInteraction {
Approval {
reason: String,
reply: tokio::sync::oneshot::Sender<bool>,
},
Elicitation {
question: String,
reply: tokio::sync::oneshot::Sender<Option<String>>,
},
}
pub fn live_interaction_handlers() -> (
std::sync::Arc<dyn ApprovalHandler>,
std::sync::Arc<dyn ElicitationHandler>,
Receiver<LiveInteraction>,
) {
let (sender, receiver) = std::sync::mpsc::channel();
(
std::sync::Arc::new(LiveApproval {
sender: sender.clone(),
}),
std::sync::Arc::new(LiveElicitation { sender }),
receiver,
)
}
struct LiveApproval {
sender: std::sync::mpsc::Sender<LiveInteraction>,
}
#[async_trait::async_trait]
impl ApprovalHandler for LiveApproval {
async fn approve(&self, _request: &PolicyRequest, reason: &str) -> bool {
let (reply, response) = tokio::sync::oneshot::channel();
self.sender
.send(LiveInteraction::Approval {
reason: reason.to_owned(),
reply,
})
.is_ok()
&& response.await.unwrap_or(false)
}
}
struct LiveElicitation {
sender: std::sync::mpsc::Sender<LiveInteraction>,
}
#[async_trait::async_trait]
impl ElicitationHandler for LiveElicitation {
async fn ask(&self, question: &str) -> Option<String> {
let (reply, response) = tokio::sync::oneshot::channel();
self.sender
.send(LiveInteraction::Elicitation {
question: question.to_owned(),
reply,
})
.is_ok()
.then_some(())?;
response.await.ok().flatten()
}
}
impl EventSink for LiveEventSink {
fn emit(&self, event: &EventEnvelope) -> std::result::Result<(), String> {
self.sender
.send(event.clone())
.map_err(|_| "live TUI event receiver disconnected".to_owned())
}
}
pub fn show_live_result(
contract: &TaskContract,
events: Receiver<EventEnvelope>,
interactions: Receiver<LiveInteraction>,
completion: Receiver<Result<RunResult>>,
control: RunControl,
) -> Result<(RunResult, Vec<EventEnvelope>)> {
let mut terminal = TerminalGuard::start()?;
let mut state = LiveState::new(contract);
let result = loop {
match completion.try_recv() {
Ok(result) => {
state.finished = true;
drain_live_events(&events, &mut state)?;
let result = result?;
loop {
terminal.draw(|frame| {
draw_result(
frame,
&result,
&state.events,
&SurfaceSnapshot::from_result(&result, &state.events),
state.log_back,
);
})?;
if event::poll(Duration::from_millis(100))?
&& let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press
{
let _ = apply_log_key(
key.code,
key.modifiers,
&mut state.log_back,
state.events.len(),
);
if matches!(key.code, KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q')) {
break;
}
}
}
break result;
}
Err(TryRecvError::Disconnected) => {
anyhow::bail!("live engine task ended without a result");
}
Err(TryRecvError::Empty) => {}
}
drain_live_events(&events, &mut state)?;
drain_interactions(&interactions, &mut state);
terminal.draw(|frame| draw_live(frame, &state))?;
if event::poll(Duration::from_millis(50))?
&& let Event::Key(key) = event::read()?
&& key.kind == KeyEventKind::Press
{
if state.handle_interaction_key(key.code, key.modifiers, &control) {
continue;
}
if cancellation_key(key.code, key.modifiers) && !state.cancellation_requested {
request_cooperative_cancellation(&control, &mut state);
} else {
apply_log_key(
key.code,
key.modifiers,
&mut state.log_back,
state.events.len(),
);
}
}
};
Ok((result, state.events))
}
fn drain_interactions(interactions: &Receiver<LiveInteraction>, state: &mut LiveState) {
if state.interaction.is_none()
&& let Ok(interaction) = interactions.try_recv()
{
state.interaction = Some(interaction);
}
}
fn drain_live_events(events: &Receiver<EventEnvelope>, state: &mut LiveState) -> Result<()> {
loop {
match events.try_recv() {
Ok(event) => state.apply(event),
Err(TryRecvError::Empty) => return Ok(()),
Err(TryRecvError::Disconnected) if state.finished => return Ok(()),
Err(TryRecvError::Disconnected) => {
anyhow::bail!("live event stream ended before engine completion")
}
}
}
}
fn cancellation_key(code: KeyCode, modifiers: KeyModifiers) -> bool {
code == KeyCode::Esc
|| (code == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL))
}
fn request_cooperative_cancellation(control: &RunControl, state: &mut LiveState) {
control.cancel();
state.cancellation_requested = true;
}
fn apply_log_key(
code: KeyCode,
modifiers: KeyModifiers,
log_back: &mut usize,
event_count: usize,
) -> bool {
let previous = *log_back;
match code {
KeyCode::Up => *log_back = log_back.saturating_add(1),
KeyCode::Down => *log_back = log_back.saturating_sub(1),
KeyCode::PageUp => *log_back = log_back.saturating_add(10),
KeyCode::PageDown => *log_back = log_back.saturating_sub(10),
KeyCode::Home if modifiers.is_empty() => *log_back = event_count,
KeyCode::End if modifiers.is_empty() => *log_back = 0,
_ => return false,
}
*log_back = (*log_back).min(event_count);
*log_back != previous
}
fn task_contract(goal: &str, criterion: &str) -> TaskContract {
let mut contract = TaskContract::new(
goal.trim(),
vec![Criterion::required("task_acceptance", criterion.trim())],
);
contract.confirmed = true;
contract
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(super) struct SurfaceSnapshot {
pub outcome: RunOutcome,
pub claim_scope: proofborne_core::ClaimScope,
pub assurance_level: proofborne_core::AssuranceLevel,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub criteria: Vec<CriterionSnapshot>,
pub actions: Vec<ActionSnapshot>,
pub evidence_count: usize,
pub approval_count: usize,
pub event_count: usize,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(super) struct CriterionSnapshot {
pub id: String,
pub description: String,
pub state: CriterionState,
pub evidence_count: usize,
pub unmet_requirements: Vec<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(super) struct ActionSnapshot {
pub tool_call_id: String,
pub title: String,
pub status: String,
}
impl SurfaceSnapshot {
pub(super) fn from_result(result: &RunResult, events: &[EventEnvelope]) -> Self {
let event_state = EventState::reduce(events);
let criteria = result
.evaluation
.criteria
.iter()
.map(|evaluation| {
let criterion = result
.contract
.criteria
.iter()
.find(|criterion| criterion.id == evaluation.criterion_id);
CriterionSnapshot {
id: evaluation.criterion_id.clone(),
description: criterion
.map_or_else(String::new, |item| item.description.clone()),
state: evaluation.state,
evidence_count: evaluation.qualifying_evidence_ids.len(),
unmet_requirements: evaluation.unmet_requirements.clone(),
}
})
.collect();
Self {
outcome: result.outcome,
claim_scope: result.evaluation.claim_scope,
assurance_level: result.evaluation.assurance_level,
reason: outcome_reason(result),
criteria,
actions: event_state.actions.into_values().collect(),
evidence_count: result.proof.evidence.len(),
approval_count: event_state.approval_count,
event_count: events.len(),
}
}
}
#[derive(Default, Debug, Clone)]
struct EventState {
actions: BTreeMap<String, ActionSnapshot>,
approval_count: usize,
}
impl EventState {
fn reduce(events: &[EventEnvelope]) -> Self {
let mut state = Self::default();
for event in events {
state.apply(event);
}
state
}
fn apply(&mut self, event: &EventEnvelope) {
match event.kind.as_str() {
"tool.requested" => {
if let Some(id) = string_at(&event.payload, &["id"]) {
self.actions.insert(
id.clone(),
ActionSnapshot {
tool_call_id: id,
title: string_at(&event.payload, &["name"])
.unwrap_or_else(|| "Proofborne tool".to_owned()),
status: "pending".to_owned(),
},
);
}
}
"verification.started" => {
let call = event.payload.get("call").unwrap_or(&Value::Null);
if let Some(id) = string_at(call, &["id"]) {
self.actions.insert(
id.clone(),
ActionSnapshot {
tool_call_id: id,
title: "Verify acceptance criterion".to_owned(),
status: "in_progress".to_owned(),
},
);
}
}
"action.started" => {
if let Some(id) = string_at(&event.payload, &["toolCallId"]) {
let action = self.actions.entry(id.clone()).or_insert(ActionSnapshot {
tool_call_id: id,
title: string_at(&event.payload, &["tool"])
.unwrap_or_else(|| "Proofborne action".to_owned()),
status: String::new(),
});
"in_progress".clone_into(&mut action.status);
}
}
"tool.completed" | "verification.completed" => {
let payload = event.payload.get("result").unwrap_or(&event.payload);
if let Some(id) = string_at(payload, &["callId"]) {
let success = payload
.get("success")
.and_then(Value::as_bool)
.unwrap_or(true);
set_action_status(&mut self.actions, &id, success);
}
}
"tool.failed" | "policy.denied" => {
let payload = event.payload.get("result").unwrap_or(&event.payload);
if let Some(id) = string_at(payload, &["callId"]) {
set_action_status(&mut self.actions, &id, false);
}
}
"policy.approval.requested" => {
self.approval_count = self.approval_count.saturating_add(1);
}
_ => {}
}
}
}
struct LiveState {
goal: String,
criteria: Vec<CriterionSnapshot>,
event_state: EventState,
events: Vec<EventEnvelope>,
evidence_count: usize,
provider_segment: Option<String>,
isolation: Option<String>,
cancellation_requested: bool,
finished: bool,
interaction: Option<LiveInteraction>,
interaction_text: String,
log_back: usize,
}
impl LiveState {
fn new(contract: &TaskContract) -> Self {
Self {
goal: contract.goal.clone(),
criteria: contract
.criteria
.iter()
.map(|criterion| CriterionSnapshot {
id: criterion.id.clone(),
description: criterion.description.clone(),
state: criterion.state,
evidence_count: criterion.evidence_ids.len(),
unmet_requirements: Vec::new(),
})
.collect(),
event_state: EventState::default(),
events: Vec::new(),
evidence_count: 0,
provider_segment: None,
isolation: None,
cancellation_requested: false,
finished: false,
interaction: None,
interaction_text: String::new(),
log_back: 0,
}
}
fn apply(&mut self, event: EventEnvelope) {
self.event_state.apply(&event);
match event.kind.as_str() {
"session.created" => {
let provider = string_at(&event.payload, &["provider"])
.unwrap_or_else(|| "unknown".to_owned());
let model =
string_at(&event.payload, &["model"]).unwrap_or_else(|| "unknown".to_owned());
self.provider_segment = Some(format!("{provider} / {model}"));
let host = string_at(&event.payload, &["hostExecution"])
.unwrap_or_else(|| "unknown host execution".to_owned());
let kind = string_at(&event.payload, &["isolation", "kind"])
.unwrap_or_else(|| "unknown isolation".to_owned());
self.isolation = Some(format!("{kind} · {host}"));
}
"provider.segment.started" => {
let provider = string_at(&event.payload, &["provider"])
.unwrap_or_else(|| "unknown".to_owned());
let model =
string_at(&event.payload, &["model"]).unwrap_or_else(|| "unknown".to_owned());
self.provider_segment = Some(format!("{provider} / {model} · active"));
}
"provider.segment.failed" => {
let provider = string_at(&event.payload, &["provider"])
.unwrap_or_else(|| "unknown".to_owned());
self.provider_segment = Some(format!("{provider} · failed"));
}
"evidence.recorded" => self.evidence_count = self.evidence_count.saturating_add(1),
"run.cancelled" => self.cancellation_requested = true,
_ => {}
}
self.events.push(event);
}
fn handle_interaction_key(
&mut self,
code: KeyCode,
modifiers: KeyModifiers,
control: &RunControl,
) -> bool {
let Some(interaction) = self.interaction.take() else {
return false;
};
match interaction {
LiveInteraction::Approval { reason, reply } => match code {
KeyCode::Char('y' | 'Y') => {
let _ = reply.send(true);
}
KeyCode::Char('n' | 'N') => {
let _ = reply.send(false);
}
_ if cancellation_key(code, modifiers) => {
control.cancel();
let _ = reply.send(false);
self.cancellation_requested = true;
}
_ => self.interaction = Some(LiveInteraction::Approval { reason, reply }),
},
LiveInteraction::Elicitation { question, reply } => match code {
KeyCode::Enter => {
let answer = std::mem::take(&mut self.interaction_text);
let _ = reply.send((!answer.trim().is_empty()).then_some(answer));
}
KeyCode::Backspace => {
self.interaction_text.pop();
self.interaction = Some(LiveInteraction::Elicitation { question, reply });
}
KeyCode::Char(character)
if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT =>
{
self.interaction_text.push(character);
self.interaction = Some(LiveInteraction::Elicitation { question, reply });
}
_ if cancellation_key(code, modifiers) => {
control.cancel();
let _ = reply.send(None);
self.cancellation_requested = true;
}
_ => self.interaction = Some(LiveInteraction::Elicitation { question, reply }),
},
}
true
}
}
fn set_action_status(
actions: &mut BTreeMap<String, ActionSnapshot>,
tool_call_id: &str,
success: bool,
) {
let action = actions
.entry(tool_call_id.to_owned())
.or_insert(ActionSnapshot {
tool_call_id: tool_call_id.to_owned(),
title: "Proofborne action".to_owned(),
status: String::new(),
});
if success { "completed" } else { "failed" }.clone_into(&mut action.status);
}
fn string_at(value: &Value, path: &[&str]) -> Option<String> {
let mut current = value;
for segment in path {
current = current.get(*segment)?;
}
current.as_str().map(ToOwned::to_owned)
}
fn outcome_reason(result: &RunResult) -> Option<String> {
match &result.proof.termination {
Some(ProofTermination::Blocked { reason } | ProofTermination::Cancelled { reason }) => {
Some(reason.clone())
}
None if result.outcome == RunOutcome::VerifiedWithWaivers => {
let waived = result
.contract
.criteria
.iter()
.filter_map(|criterion| {
criterion
.waiver
.as_ref()
.map(|waiver| format!("{}: {}", criterion.id, waiver.reason))
})
.collect::<Vec<_>>();
(!waived.is_empty()).then(|| format!("Waived: {}", waived.join("; ")))
}
None if result.outcome == RunOutcome::Failed => {
let unmet = result
.evaluation
.criteria
.iter()
.flat_map(|criterion| criterion.unmet_requirements.iter().cloned())
.collect::<Vec<_>>();
(!unmet.is_empty()).then(|| unmet.join("; "))
}
None => None,
}
}
fn draw_contract(frame: &mut Frame<'_>, goal: &str, criterion: &str, screen: &ContractScreen) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(10),
Constraint::Length(3),
])
.split(frame.area());
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
" PROOFBORNE ",
Style::default()
.fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" The coding agent that proves its work"),
]))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::BOTTOM)),
chunks[0],
);
match screen {
ContractScreen::Goal => {
render_editor(frame, chunks[1], " Task goal ", goal, Color::Cyan);
frame.render_widget(
Paragraph::new("Type the goal · Enter acceptance criterion · Esc cancel")
.alignment(Alignment::Center),
chunks[2],
);
}
ContractScreen::Criterion => {
render_editor(
frame,
chunks[1],
" Required, machine-checkable acceptance criterion ",
criterion,
Color::Yellow,
);
frame.render_widget(
Paragraph::new("Describe what runtime evidence must establish · Enter review")
.alignment(Alignment::Center),
chunks[2],
);
}
ContractScreen::Confirm => {
let contract = task_contract(goal, criterion);
let requirement = &contract.criteria[0].evidence_requirement;
let view = format!(
"GOAL\n{}\n\nCLAIM SCOPE\ntask\n\nREQUIRED CRITERION\n{} — {}\n\nEVIDENCE REQUIREMENTS\nkinds: {:?}\nminimum observations: {}\nminimum assurance: {:?}\nfreshness: {:?}\n\nBOUNDARY\nOnly runtime-observed evidence satisfying these requirements can pass the criterion.",
contract.goal,
contract.criteria[0].id,
contract.criteria[0].description,
requirement.allowed_kinds,
requirement.minimum_observations,
requirement.minimum_assurance,
requirement.freshness,
);
frame.render_widget(Clear, chunks[1]);
frame.render_widget(
Paragraph::new(view).wrap(Wrap { trim: false }).block(
Block::default()
.title(" Confirm Task Contract before any mutation ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow)),
),
chunks[1],
);
frame.render_widget(
Paragraph::new("Y confirm · N edit criterion · Esc cancel")
.alignment(Alignment::Center),
chunks[2],
);
}
}
}
fn render_editor(frame: &mut Frame<'_>, area: Rect, title: &str, text: &str, color: Color) {
frame.render_widget(
Paragraph::new(text).wrap(Wrap { trim: false }).block(
Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(Style::default().fg(color)),
),
area,
);
}
fn draw_result(
frame: &mut Frame<'_>,
result: &RunResult,
events: &[EventEnvelope],
snapshot: &SurfaceSnapshot,
log_back: usize,
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Percentage(30),
Constraint::Percentage(35),
Constraint::Percentage(35),
Constraint::Length(2),
])
.split(frame.area());
let color = outcome_color(snapshot.outcome);
let reason = snapshot
.reason
.as_deref()
.map_or_else(String::new, |reason| format!("\nReason: {reason}"));
frame.render_widget(
Paragraph::new(format!(
"{:?} · claimScope={:?} · assurance={:?}\nSession {} · {} evidence nodes{}",
snapshot.outcome,
snapshot.claim_scope,
snapshot.assurance_level,
result.session_id,
snapshot.evidence_count,
reason,
))
.alignment(Alignment::Center)
.style(Style::default().fg(color).add_modifier(Modifier::BOLD))
.block(Block::default().borders(Borders::ALL).title(" Proof gate ")),
chunks[0],
);
frame.render_widget(
Paragraph::new(result.output_text.as_str())
.wrap(Wrap { trim: false })
.block(
Block::default()
.borders(Borders::ALL)
.title(" Agent output "),
),
chunks[1],
);
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(55), Constraint::Percentage(45)])
.split(chunks[2]);
let criterion_rows = snapshot.criteria.iter().map(|criterion| {
let unmet = if criterion.unmet_requirements.is_empty() {
String::new()
} else {
format!(" — {}", criterion.unmet_requirements.join("; "))
};
ListItem::new(format!(
"{:?} {} ({} evidence){}",
criterion.state, criterion.id, criterion.evidence_count, unmet
))
});
frame.render_widget(
List::new(criterion_rows).block(
Block::default()
.borders(Borders::ALL)
.title(" Acceptance criteria "),
),
columns[0],
);
let action_rows = snapshot.actions.iter().map(|action| {
ListItem::new(format!(
"{} {} {}",
action.status, action.tool_call_id, action.title
))
});
frame.render_widget(
List::new(action_rows).block(
Block::default()
.borders(Borders::ALL)
.title(format!(" Actions · approvals {} ", snapshot.approval_count)),
),
columns[1],
);
let visible = chunks[3].height.saturating_sub(2) as usize;
let end = events.len().saturating_sub(log_back.min(events.len()));
let start = end.saturating_sub(visible);
let rows = events[start..end].iter().map(|event| {
ListItem::new(format!(
"{:04} {} {}",
event.seq,
event.kind,
&event.hash[..12]
))
});
frame.render_widget(
List::new(rows).block(Block::default().borders(Borders::ALL).title(format!(
" Evidence event chain · {} events · ↑/↓ scroll ",
events.len()
))),
chunks[3],
);
frame.render_widget(
Paragraph::new("Enter / Q / Esc to close").alignment(Alignment::Center),
chunks[4],
);
}
fn draw_live(frame: &mut Frame<'_>, state: &LiveState) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(5),
Constraint::Percentage(40),
Constraint::Percentage(60),
Constraint::Length(2),
])
.split(frame.area());
let status = if state.cancellation_requested {
"Cancellation requested — waiting for runtime finalization"
} else {
"Running — proof facts shown only after persistence"
};
frame.render_widget(
Paragraph::new(format!(
"{}\n{}\n{} evidence observations · {} actions · {} approvals",
state.goal,
status,
state.evidence_count,
state.event_state.actions.len(),
state.event_state.approval_count,
))
.wrap(Wrap { trim: false })
.block(
Block::default()
.borders(Borders::ALL)
.title(" Live task contract "),
),
chunks[0],
);
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(58), Constraint::Percentage(42)])
.split(chunks[1]);
let criterion_rows = state.criteria.iter().map(|criterion| {
ListItem::new(format!(
"{:?} {}\n{}",
criterion.state, criterion.id, criterion.description
))
});
frame.render_widget(
List::new(criterion_rows).block(
Block::default()
.borders(Borders::ALL)
.title(" Required acceptance criteria "),
),
columns[0],
);
let action_rows = state.event_state.actions.values().map(|action| {
ListItem::new(format!(
"{} {} {}",
action.status, action.tool_call_id, action.title
))
});
frame.render_widget(
List::new(action_rows).block(Block::default().borders(Borders::ALL).title(format!(
" Actions · approvals {} ",
state.event_state.approval_count
))),
columns[1],
);
let visible = chunks[2].height.saturating_sub(2) as usize;
let end = state
.events
.len()
.saturating_sub(state.log_back.min(state.events.len()));
let start = end.saturating_sub(visible);
let log_rows = state.events[start..end].iter().map(|event| {
ListItem::new(format!(
"{:04} {} {}",
event.seq,
event.kind,
&event.hash[..12]
))
});
let provider = state
.provider_segment
.as_deref()
.unwrap_or("waiting for session");
let isolation = state
.isolation
.as_deref()
.unwrap_or("waiting for runtime resolution");
frame.render_widget(
List::new(log_rows).block(
Block::default().borders(Borders::ALL).title(format!(
" Live persisted event chain · provider: {provider} · isolation: {isolation} · ↑/↓ PgUp/PgDn scroll "
)),
),
chunks[2],
);
frame.render_widget(
Paragraph::new(
"Ctrl-C / Esc request cooperative cancellation · facts are finalized by the runtime",
)
.alignment(Alignment::Center),
chunks[3],
);
if let Some(interaction) = &state.interaction {
let prompt = match interaction {
LiveInteraction::Approval { reason, .. } => {
format!(
"POLICY APPROVAL REQUIRED\n{reason}\n\nY approve once · N deny · Esc cancel run"
)
}
LiveInteraction::Elicitation { question, .. } => format!(
"RUNTIME QUESTION\n{question}\n\n{}\n\nEnter submit · Esc cancel run",
state.interaction_text
),
};
let area = centered_rect(76, 40, frame.area());
frame.render_widget(Clear, area);
frame.render_widget(
Paragraph::new(prompt).wrap(Wrap { trim: false }).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.title(" Runtime interaction "),
),
area,
);
}
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(area);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(vertical[1])[1]
}
fn outcome_color(outcome: RunOutcome) -> Color {
match outcome {
RunOutcome::Verified | RunOutcome::VerifiedWithWaivers => Color::Green,
RunOutcome::Failed => Color::Red,
RunOutcome::Blocked => Color::Yellow,
RunOutcome::Cancelled => Color::DarkGray,
}
}
struct TerminalGuard {
terminal: Terminal<CrosstermBackend<io::Stdout>>,
}
impl TerminalGuard {
fn start() -> Result<Self> {
enable_raw_mode()?;
let mut stdout = io::stdout();
if let Err(error) = execute!(stdout, EnterAlternateScreen) {
let _ = disable_raw_mode();
return Err(error.into());
}
match Terminal::new(CrosstermBackend::new(stdout)) {
Ok(terminal) => Ok(Self { terminal }),
Err(error) => {
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen);
Err(error.into())
}
}
}
fn draw<F>(&mut self, draw: F) -> Result<()>
where
F: FnOnce(&mut Frame<'_>),
{
let _ = self.terminal.draw(draw)?;
Ok(())
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = execute!(self.terminal.backend_mut(), LeaveAlternateScreen);
let _ = self.terminal.show_cursor();
}
}
#[cfg(test)]
mod tests {
use proofborne_core::{EventEnvelope, ProofGraph};
use uuid::Uuid;
use super::*;
#[test]
fn interactive_contract_is_task_scoped_and_confirmed() {
let contract = task_contract("repair it", "all tests pass");
assert_eq!(contract.claim_scope, proofborne_core::ClaimScope::Task);
assert!(contract.confirmed);
assert_eq!(contract.criteria[0].id, "task_acceptance");
assert!(
!contract.criteria[0]
.evidence_requirement
.allowed_kinds
.is_empty()
);
}
#[test]
fn event_state_reduces_action_and_approval_lifecycle() {
let session_id = Uuid::now_v7();
let events = vec![
EventEnvelope::new(
session_id,
0,
"tool.requested",
serde_json::json!({"id": "call-1", "name": "patch_file"}),
None,
),
EventEnvelope::new(
session_id,
1,
"action.started",
serde_json::json!({"toolCallId": "call-1", "tool": "patch_file"}),
None,
),
EventEnvelope::new(
session_id,
2,
"policy.approval.requested",
serde_json::json!({}),
None,
),
EventEnvelope::new(
session_id,
3,
"tool.completed",
serde_json::json!({"callId": "call-1", "success": true}),
None,
),
];
let state = EventState::reduce(&events);
assert_eq!(state.approval_count, 1);
assert_eq!(state.actions["call-1"].status, "completed");
}
#[test]
fn live_channel_keeps_exact_envelopes_in_order() {
let session_id = Uuid::now_v7();
let (sink, receiver) = live_event_channel();
let first = EventEnvelope::new(
session_id,
0,
"session.created",
serde_json::json!({}),
None,
);
let second = EventEnvelope::new(
session_id,
1,
"evidence.recorded",
serde_json::json!({}),
Some(first.hash.clone()),
);
sink.emit(&first).unwrap();
sink.emit(&second).unwrap();
let received_first = receiver.recv().unwrap();
let received_second = receiver.recv().unwrap();
assert_eq!(received_first, first);
assert_eq!(received_second, second);
}
#[test]
fn live_reducer_tracks_provider_isolation_evidence_and_actions() {
let contract = task_contract("repair it", "tests pass");
let session_id = Uuid::now_v7();
let mut state = LiveState::new(&contract);
state.apply(EventEnvelope::new(
session_id,
0,
"session.created",
serde_json::json!({
"provider": "mock", "model": "proof-model",
"hostExecution": "policy-enforced, not sandboxed",
"isolation": {"kind": "host"}
}),
None,
));
state.apply(EventEnvelope::new(
session_id,
1,
"action.started",
serde_json::json!({"toolCallId": "call-1", "tool": "patch_file"}),
None,
));
state.apply(EventEnvelope::new(
session_id,
2,
"evidence.recorded",
serde_json::json!({"kind": "tool"}),
None,
));
assert_eq!(state.evidence_count, 1);
assert!(
state
.provider_segment
.as_deref()
.unwrap()
.contains("mock / proof-model")
);
assert!(state.isolation.as_deref().unwrap().contains("host"));
assert_eq!(state.event_state.actions["call-1"].status, "in_progress");
}
#[test]
fn log_scrolling_is_deterministic_and_clamped() {
let mut log_back = 0;
assert!(apply_log_key(
KeyCode::PageUp,
KeyModifiers::NONE,
&mut log_back,
3
));
assert_eq!(log_back, 3);
assert!(apply_log_key(
KeyCode::Down,
KeyModifiers::NONE,
&mut log_back,
3
));
assert_eq!(log_back, 2);
assert!(apply_log_key(
KeyCode::End,
KeyModifiers::NONE,
&mut log_back,
3
));
assert_eq!(log_back, 0);
}
#[test]
fn live_widget_overlay_layout_is_bounded_and_centered() {
let overlay = centered_rect(76, 40, Rect::new(0, 0, 100, 50));
assert_eq!(overlay.width, 76);
assert_eq!(overlay.height, 20);
assert_eq!(overlay.x, 12);
assert_eq!(overlay.y, 15);
}
#[test]
fn cancellation_key_requests_runtime_owned_control() {
let contract = task_contract("repair it", "tests pass");
let mut state = LiveState::new(&contract);
let control = RunControl::default();
assert!(cancellation_key(KeyCode::Esc, KeyModifiers::NONE));
assert!(cancellation_key(KeyCode::Char('c'), KeyModifiers::CONTROL));
request_cooperative_cancellation(&control, &mut state);
assert!(control.is_cancelled());
assert!(state.cancellation_requested);
}
#[tokio::test]
async fn approval_cancellation_marks_control_before_releasing_the_reply() {
let contract = task_contract("repair it", "tests pass");
let mut state = LiveState::new(&contract);
let control = RunControl::default();
let (reply, response) = tokio::sync::oneshot::channel();
state.interaction = Some(LiveInteraction::Approval {
reason: "approval is pending".to_owned(),
reply,
});
assert!(state.handle_interaction_key(KeyCode::Esc, KeyModifiers::NONE, &control));
assert!(control.is_cancelled());
assert!(state.cancellation_requested);
assert!(!response.await.unwrap());
}
#[tokio::test]
async fn elicitation_cancellation_marks_control_before_releasing_the_reply() {
let contract = task_contract("repair it", "tests pass");
let mut state = LiveState::new(&contract);
let control = RunControl::default();
let (reply, response) = tokio::sync::oneshot::channel();
state.interaction = Some(LiveInteraction::Elicitation {
question: "continue?".to_owned(),
reply,
});
assert!(state.handle_interaction_key(KeyCode::Esc, KeyModifiers::NONE, &control));
assert!(control.is_cancelled());
assert!(state.cancellation_requested);
assert_eq!(response.await.unwrap(), None);
}
#[test]
fn final_surface_snapshot_matches_runtime_result() {
let contract = TaskContract::automatic("do something");
let mut proof = ProofGraph::new(contract.id);
proof.block("contract_insufficient: task evidence is missing");
let mut evaluated_contract = contract.clone();
let evaluation = proof.evaluate_detailed(&mut evaluated_contract).unwrap();
let result = RunResult {
session_id: Uuid::now_v7(),
outcome: evaluation.outcome,
evaluation,
contract: evaluated_contract,
proof,
output_text: String::new(),
};
let snapshot = SurfaceSnapshot::from_result(&result, &[]);
assert_eq!(snapshot.claim_scope, proofborne_core::ClaimScope::Runtime);
assert_eq!(
snapshot.assurance_level,
proofborne_core::AssuranceLevel::Recorded
);
assert!(snapshot.reason.unwrap().contains("contract_insufficient"));
assert_eq!(snapshot.outcome, result.outcome);
assert_eq!(snapshot.claim_scope, result.evaluation.claim_scope);
assert_eq!(snapshot.assurance_level, result.evaluation.assurance_level);
assert_eq!(snapshot.evidence_count, result.proof.evidence.len());
}
}