From 010dfa77511f509ef79ae0c2b8f4afd040de9dad Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 16 Oct 2025 14:15:24 +0100 Subject: [PATCH] Better UI --- codex-rs/cli/src/infty/progress.rs | 273 ++++++++++------------- codex-rs/codex-infty/src/orchestrator.rs | 1 - codex-rs/codex-infty/src/progress.rs | 1 - 3 files changed, 117 insertions(+), 158 deletions(-) diff --git a/codex-rs/cli/src/infty/progress.rs b/codex-rs/cli/src/infty/progress.rs index 1c5f0af341..ae7a31f961 100644 --- a/codex-rs/cli/src/infty/progress.rs +++ b/codex-rs/cli/src/infty/progress.rs @@ -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, + 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 = 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(), + ); } } diff --git a/codex-rs/codex-infty/src/orchestrator.rs b/codex-rs/codex-infty/src/orchestrator.rs index 9551347e6e..8aeeb479d6 100644 --- a/codex-rs/codex-infty/src/orchestrator.rs +++ b/codex-rs/codex-infty/src/orchestrator.rs @@ -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(()) diff --git a/codex-rs/codex-infty/src/progress.rs b/codex-rs/codex-infty/src/progress.rs index 143a1ead98..51928fd7d9 100644 --- a/codex-rs/codex-infty/src/progress.rs +++ b/codex-rs/codex-infty/src/progress.rs @@ -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) {}