mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Better UI
This commit is contained in:
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
Reference in New Issue
Block a user