Better UI

This commit is contained in:
jif-oai
2025-10-16 14:15:24 +01:00
parent 54b9436699
commit 010dfa7751
3 changed files with 117 additions and 158 deletions

View File

@@ -1,5 +1,4 @@
use std::path::Path;
use chrono::Local;
use codex_core::protocol::AgentMessageEvent;
use codex_core::protocol::EventMsg;
use codex_infty::AggregatedVerifierVerdict;
@@ -7,55 +6,82 @@ use codex_infty::DirectiveResponse;
use codex_infty::ProgressReporter;
use codex_infty::VerifierDecision;
use codex_infty::VerifierVerdict;
use crossterm::style::Stylize;
use owo_colors::OwoColorize;
use std::path::Path;
use supports_color::Stream;
pub(crate) struct TerminalProgressReporter {
color_enabled: bool,
}
#[derive(Debug, Default, Clone)]
pub(crate) struct TerminalProgressReporter;
impl TerminalProgressReporter {
pub(crate) fn with_color(color_enabled: bool) -> Self {
Self { color_enabled }
pub(crate) fn with_color(_color_enabled: bool) -> Self {
Self
}
fn format_role_label(&self, role: &str) -> String {
let lower = role.to_ascii_lowercase();
if lower == "solver" {
return "[solver]".magenta().bold().to_string();
}
if lower == "director" {
return "[director]".blue().bold().to_string();
}
if lower == "user" {
return "[user]".cyan().bold().to_string();
}
if lower.contains("verifier") {
return format!("[{role}]").green().bold().to_string();
}
format!("[{role}]").magenta().bold().to_string()
}
fn timestamp(&self) -> String {
let timestamp = Local::now().format("%H:%M:%S");
let display = format!("[{timestamp}]");
if supports_color::on(Stream::Stdout).is_some() {
format!("{}", display.dim())
} else {
display
}
}
fn print_exchange(
&self,
from_role: &str,
to_role: &str,
lines: Vec<String>,
trailing_blank_line: bool,
) {
let header = format!(
"{} ----> {}",
self.format_role_label(from_role),
self.format_role_label(to_role)
);
println!("{} {header}", self.timestamp());
for line in lines {
println!("{line}");
}
if trailing_blank_line {
println!();
}
}
fn format_decision(&self, decision: VerifierDecision) -> String {
let label = match decision {
VerifierDecision::Pass => "pass",
VerifierDecision::Fail => "fail",
};
if !self.color_enabled {
return label.to_string();
}
match decision {
VerifierDecision::Pass => format!("{}", label.green().bold()),
VerifierDecision::Fail => format!("{}", label.red().bold()),
VerifierDecision::Pass => "pass".green().bold().to_string(),
VerifierDecision::Fail => "fail".red().bold().to_string(),
}
}
}
impl Default for TerminalProgressReporter {
fn default() -> Self {
Self::with_color(supports_color::on(Stream::Stdout).is_some())
}
}
impl ProgressReporter for TerminalProgressReporter {
fn objective_posted(&self, objective: &str) {
let line = format!("→ objective sent to solver: {objective}");
if self.color_enabled {
println!("{}", line.cyan());
} else {
println!("{line}");
}
}
fn waiting_for_solver(&self) {
if self.color_enabled {
println!("{}", "Waiting for solver response...".dimmed());
} else {
println!("Waiting for solver response...");
}
let objective_line = format!(
"{}",
format!("→ objective: {objective}")
);
self.print_exchange("user", "solver", vec![objective_line], true);
}
fn solver_event(&self, event: &EventMsg) {
@@ -81,157 +107,92 @@ impl ProgressReporter for TerminalProgressReporter {
}
fn solver_agent_message(&self, agent_msg: &AgentMessageEvent) {
let prefix = if self.color_enabled {
format!("{}", "[solver]".magenta().bold())
} else {
"[solver]".to_string()
};
println!("{prefix} {}", agent_msg.message);
let mut lines: Vec<String> = agent_msg
.message
.lines()
.map(|line| line.to_string())
.collect();
if lines.is_empty() {
lines.push(String::new());
}
self.print_exchange("solver", "user", lines, true);
}
fn direction_request(&self, prompt: &str) {
let line = format!("→ solver requested direction: {prompt}");
if self.color_enabled {
println!("{}", line.yellow().bold());
} else {
println!("{line}");
}
let prompt_line = format!("{}", prompt.yellow());
self.print_exchange("solver", "director", vec![prompt_line], true);
}
fn director_response(&self, directive: &DirectiveResponse) {
match directive.rationale.as_deref() {
Some(rationale) if !rationale.is_empty() => {
let line = format!(
"[director] directive: {} (rationale: {rationale})",
directive.directive
);
if self.color_enabled {
println!("{}", line.blue());
} else {
println!("{line}");
}
}
_ => {
let line = format!("[director] directive: {}", directive.directive);
if self.color_enabled {
println!("{}", line.blue());
} else {
println!("{line}");
}
}
}
let suffix = directive
.rationale
.as_deref()
.filter(|rationale| !rationale.is_empty())
.map(|rationale| format!(" (rationale: {rationale})"))
.unwrap_or_default();
let directive_line = format!("{}{}", directive.directive, suffix);
self.print_exchange("director", "solver", vec![directive_line], true);
}
fn verification_request(&self, claim_path: &str, notes: Option<&str>) {
let line = format!("→ solver requested verification for {claim_path}");
if self.color_enabled {
println!("{}", line.yellow().bold());
} else {
println!("{line}");
}
if let Some(notes) = notes
&& !notes.is_empty()
{
let notes_line = format!(" notes: {notes}");
if self.color_enabled {
println!("{}", notes_line.dimmed());
} else {
println!("{notes_line}");
}
let mut lines = Vec::new();
let path_line = format!("→ path: {claim_path}");
lines.push(format!("{}", path_line.dim()));
if let Some(notes) = notes.filter(|notes| !notes.is_empty()) {
let note_line = format!("→ note: {notes}");
lines.push(format!("{}", note_line.dim()));
}
self.print_exchange("solver", "verifier", lines, true);
}
fn verifier_verdict(&self, role: &str, verdict: &VerifierVerdict) {
let decision = self.format_decision(verdict.verdict);
let prefix = if self.color_enabled {
format!("{}", format!("[{role}]").magenta().bold())
} else {
format!("[{role}]")
};
println!("{prefix} verdict: {decision}");
let mut lines = Vec::new();
lines.push(format!("verdict: {decision}"));
if !verdict.reasons.is_empty() {
let reasons = verdict.reasons.join("; ");
let line = format!(" reasons: {reasons}");
if self.color_enabled {
println!("{}", line.dimmed());
} else {
println!("{line}");
}
let reason_line = format!(" reasons: {reasons}");
lines.push(format!("{}", reason_line.dim()));
}
if !verdict.suggestions.is_empty() {
let suggestions = verdict.suggestions.join("; ");
let line = format!(" suggestions: {suggestions}");
if self.color_enabled {
println!("{}", line.dimmed());
} else {
println!("{line}");
}
let suggestion_line = format!(" suggestions: {suggestions}");
lines.push(format!("{}", suggestion_line.dim()));
}
self.print_exchange(role, "solver", lines, false);
}
fn verification_summary(&self, summary: &AggregatedVerifierVerdict) {
println!();
let decision = self.format_decision(summary.overall);
let heading = if self.color_enabled {
format!("{}", "Verification summary".bold())
} else {
"Verification summary".to_string()
};
println!("{heading}: {decision}");
for report in &summary.verdicts {
let report_decision = self.format_decision(report.verdict);
let line = format!(" {}{report_decision}", report.role);
println!("{line}");
if !report.reasons.is_empty() {
let reasons = report.reasons.join("; ");
let reason_line = format!(" reasons: {reasons}");
if self.color_enabled {
println!("{}", reason_line.dimmed());
} else {
println!("{reason_line}");
}
}
if !report.suggestions.is_empty() {
let suggestions = report.suggestions.join("; ");
let suggestion_line = format!(" suggestions: {suggestions}");
if self.color_enabled {
println!("{}", suggestion_line.dimmed());
} else {
println!("{suggestion_line}");
}
}
}
let heading = "Verification summary".bold();
let summary_line = format!("{heading}: {decision}");
self.print_exchange("verifier", "solver", vec![summary_line], true);
}
fn final_delivery(&self, deliverable_path: &Path, summary: Option<&str>) {
println!();
let line = format!(
"✓ solver reported final delivery at {}",
deliverable_path.display()
let delivery_line = format!(
"{}",
format!(
"✓ solver reported final delivery at {}",
deliverable_path.display()
)
.green()
.bold()
);
if self.color_enabled {
println!("{}", line.green().bold());
} else {
println!("{line}");
}
if let Some(summary) = summary
&& !summary.is_empty()
{
let hint = " (final summary will be shown below)";
if self.color_enabled {
println!("{}", hint.dimmed());
} else {
println!("{hint}");
}
let mut lines = vec![delivery_line];
if summary.is_some_and(|summary| !summary.is_empty()) {
let hint = " (final summary will be shown below)".to_string();
lines.push(format!("{}", hint.dim()));
}
self.print_exchange("solver", "verifier", lines, true);
}
fn run_interrupted(&self) {
let line = "Run interrupted by Ctrl+C. Shutting down sessions…";
if self.color_enabled {
println!("{}", line.red().bold());
} else {
println!("{line}");
}
println!(
"{}",
"Run interrupted by Ctrl+C. Shutting down sessions…"
.red()
.bold(),
);
}
}

View File

@@ -355,7 +355,6 @@ impl InftyOrchestrator {
state.waiting_for_signal = true;
if let Some(p) = self.progress_ref() {
p.objective_posted(objective);
p.waiting_for_solver();
}
}
Ok(())

View File

@@ -9,7 +9,6 @@ use crate::signals::VerifierVerdict;
pub trait ProgressReporter: Send + Sync {
fn objective_posted(&self, _objective: &str) {}
fn waiting_for_solver(&self) {}
fn solver_event(&self, _event: &EventMsg) {}
fn role_event(&self, _role: &str, _event: &EventMsg) {}
fn solver_agent_message(&self, _message: &AgentMessageEvent) {}