prefer ratatui Stylized for constructing lines/spans (#3068)

no functional change, just simplifying ratatui styling and adding
guidance in AGENTS.md for future.
This commit is contained in:
Jeremy Rose
2025-09-02 16:19:54 -07:00
committed by GitHub
parent 0d5ffb000e
commit 578ff09e17
18 changed files with 203 additions and 311 deletions

View File

@@ -27,7 +27,6 @@ use itertools::Itertools;
use mcp_types::EmbeddedResourceResource;
use mcp_types::ResourceLink;
use ratatui::prelude::*;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::style::Styled;
@@ -101,15 +100,15 @@ impl HistoryCell for UserHistoryCell {
);
for line in wrapped {
lines.push(Line::from(vec!["".cyan().dim(), line.to_string().dim()]));
lines.push(vec!["".cyan().dim(), line.to_string().dim()].into());
}
lines
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from("user".cyan().bold()));
lines.extend(self.message.lines().map(|l| Line::from(l.to_string())));
lines.push("user".cyan().bold().into());
lines.extend(self.message.lines().map(|l| l.to_string().into()));
lines
}
}
@@ -148,7 +147,7 @@ impl HistoryCell for AgentMessageCell {
" ".into()
});
spans.extend(piece.spans.into_iter());
out.push(Line::from(spans));
out.push(spans.into());
}
is_first_visual = false;
}
@@ -158,7 +157,7 @@ impl HistoryCell for AgentMessageCell {
fn transcript_lines(&self) -> Vec<Line<'static>> {
let mut out: Vec<Line<'static>> = Vec::new();
if self.is_first_line {
out.push(Line::from("codex".magenta().bold()));
out.push("codex".magenta().bold().into());
}
out.extend(self.lines.clone());
out
@@ -242,9 +241,9 @@ impl HistoryCell for ExecCell {
let cmd_display = strip_bash_lc_and_escape(&call.command);
for (i, part) in cmd_display.lines().enumerate() {
if i == 0 {
lines.push(Line::from(vec!["$ ".magenta(), part.to_string().into()]));
lines.push(vec!["$ ".magenta(), part.to_string().into()].into());
} else {
lines.push(Line::from(vec![" ".into(), part.to_string().into()]));
lines.push(vec![" ".into(), part.to_string().into()].into());
}
}
@@ -254,7 +253,7 @@ impl HistoryCell for ExecCell {
.duration
.map(format_duration)
.unwrap_or_else(|| "unknown".to_string());
let mut result = if output.exit_code == 0 {
let mut result: Line = if output.exit_code == 0 {
Line::from("".green().bold())
} else {
Line::from(vec![
@@ -373,7 +372,7 @@ impl ExecCell {
for (title, line) in call_lines {
let prefix_len = 4 + title.len() + 1; // " └ " + title + " "
let wrapped = crate::insert_history::word_wrap_lines(
&[Line::from(line)],
&[line.into()],
width.saturating_sub(prefix_len as u16),
);
let mut first_sub = true;
@@ -437,7 +436,7 @@ impl ExecCell {
]));
} else {
branch_consumed = true;
lines.push(Line::from(vec![bullet, " ".into(), title.bold()]));
lines.push(vec![bullet, " ".into(), title.bold()].into());
// Wrap the command line.
for (i, line) in cmd_display.lines().enumerate() {
@@ -450,9 +449,9 @@ impl ExecCell {
);
lines.extend(wrapped.into_iter().enumerate().map(|(j, l)| {
if i == 0 && j == 0 {
Line::from(vec!["".dim(), l[4..].to_string().into()])
vec!["".dim(), l[4..].to_string().into()].into()
} else {
Line::from(l.to_string())
l.to_string().into()
}
}));
}
@@ -604,7 +603,7 @@ struct CompletedMcpToolCallWithImageOutput {
}
impl HistoryCell for CompletedMcpToolCallWithImageOutput {
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
vec![Line::from("tool result (image output omitted)")]
vec!["tool result (image output omitted)".into()]
}
}
@@ -696,9 +695,9 @@ pub(crate) fn new_session_info(
PlainHistoryCell { lines: Vec::new() }
} else {
let lines = vec![
Line::from("model changed:".magenta().bold()),
Line::from(format!("requested: {}", config.model)),
Line::from(format!("used: {model}")),
"model changed:".magenta().bold().into(),
format!("requested: {}", config.model).into(),
format!("used: {model}").into(),
];
PlainHistoryCell { lines }
}
@@ -854,13 +853,7 @@ pub(crate) fn new_completed_mcp_tool_call(
}
}
Err(e) => {
lines.push(Line::from(vec![
Span::styled(
"Error: ",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
Span::raw(e),
]));
lines.push(vec!["Error: ".red().bold(), e.into()].into());
}
};
@@ -873,7 +866,7 @@ pub(crate) fn new_status_output(
session_id: &Option<Uuid>,
) -> PlainHistoryCell {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from("/status".magenta()));
lines.push("/status".magenta().into());
let config_entries = create_config_summary_entries(config);
let lookup = |k: &str| -> String {
@@ -885,10 +878,7 @@ pub(crate) fn new_status_output(
};
// 📂 Workspace
lines.push(Line::from(vec![
padded_emoji("📂").into(),
"Workspace".bold(),
]));
lines.push(vec![padded_emoji("📂").into(), "Workspace".bold()].into());
// Path (home-relative, e.g., ~/code/project)
let cwd_str = match relativize_to_home(&config.cwd) {
Some(rel) if !rel.as_os_str().is_empty() => {
@@ -898,22 +888,16 @@ pub(crate) fn new_status_output(
Some(_) => "~".to_string(),
None => config.cwd.display().to_string(),
};
lines.push(Line::from(vec![" • Path: ".into(), cwd_str.into()]));
lines.push(vec![" • Path: ".into(), cwd_str.into()].into());
// Approval mode (as-is)
lines.push(Line::from(vec![
" • Approval Mode: ".into(),
lookup("approval").into(),
]));
lines.push(vec![" • Approval Mode: ".into(), lookup("approval").into()].into());
// Sandbox (simplified name only)
let sandbox_name = match &config.sandbox_policy {
SandboxPolicy::DangerFullAccess => "danger-full-access",
SandboxPolicy::ReadOnly => "read-only",
SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
};
lines.push(Line::from(vec![
" • Sandbox: ".into(),
sandbox_name.into(),
]));
lines.push(vec![" • Sandbox: ".into(), sandbox_name.into()].into());
// AGENTS.md files discovered via core's project_doc logic
let agents_list = {
@@ -956,85 +940,62 @@ pub(crate) fn new_status_output(
}
};
if agents_list.is_empty() {
lines.push(Line::from(" • AGENTS files: (none)"));
lines.push(" • AGENTS files: (none)".into());
} else {
lines.push(Line::from(vec![
" • AGENTS files: ".into(),
agents_list.join(", ").into(),
]));
lines.push(vec![" • AGENTS files: ".into(), agents_list.join(", ").into()].into());
}
lines.push(Line::from(""));
lines.push("".into());
// 👤 Account (only if ChatGPT tokens exist), shown under the first block
let auth_file = get_auth_file(&config.codex_home);
if let Ok(auth) = try_read_auth_json(&auth_file)
&& let Some(tokens) = auth.tokens.clone()
{
lines.push(Line::from(vec![
padded_emoji("👤").into(),
"Account".bold(),
]));
lines.push(Line::from(" • Signed in with ChatGPT"));
lines.push(vec![padded_emoji("👤").into(), "Account".bold()].into());
lines.push(" • Signed in with ChatGPT".into());
let info = tokens.id_token;
if let Some(email) = &info.email {
lines.push(Line::from(vec![" • Login: ".into(), email.clone().into()]));
lines.push(vec![" • Login: ".into(), email.clone().into()].into());
}
match auth.openai_api_key.as_deref() {
Some(key) if !key.is_empty() => {
lines.push(Line::from(
" • Using API key. Run codex login to use ChatGPT plan",
));
lines.push(" • Using API key. Run codex login to use ChatGPT plan".into());
}
_ => {
let plan_text = info
.get_chatgpt_plan_type()
.map(|s| title_case(&s))
.unwrap_or_else(|| "Unknown".to_string());
lines.push(Line::from(vec![" • Plan: ".into(), plan_text.into()]));
lines.push(vec![" • Plan: ".into(), plan_text.into()].into());
}
}
lines.push(Line::from(""));
lines.push("".into());
}
// 🧠 Model
lines.push(Line::from(vec![padded_emoji("🧠").into(), "Model".bold()]));
lines.push(Line::from(vec![
" • Name: ".into(),
config.model.clone().into(),
]));
lines.push(vec![padded_emoji("🧠").into(), "Model".bold()].into());
lines.push(vec![" • Name: ".into(), config.model.clone().into()].into());
let provider_disp = pretty_provider_name(&config.model_provider_id);
lines.push(Line::from(vec![
" • Provider: ".into(),
provider_disp.into(),
]));
lines.push(vec![" • Provider: ".into(), provider_disp.into()].into());
// Only show Reasoning fields if present in config summary
let reff = lookup("reasoning effort");
if !reff.is_empty() {
lines.push(Line::from(vec![
" • Reasoning Effort: ".into(),
title_case(&reff).into(),
]));
lines.push(vec![" • Reasoning Effort: ".into(), title_case(&reff).into()].into());
}
let rsum = lookup("reasoning summaries");
if !rsum.is_empty() {
lines.push(Line::from(vec![
" • Reasoning Summaries: ".into(),
title_case(&rsum).into(),
]));
lines.push(vec![" • Reasoning Summaries: ".into(), title_case(&rsum).into()].into());
}
lines.push(Line::from(""));
lines.push("".into());
// 📊 Token Usage
lines.push(Line::from(vec!["📊 ".into(), "Token Usage".bold()]));
lines.push(vec!["📊 ".into(), "Token Usage".bold()].into());
if let Some(session_id) = session_id {
lines.push(Line::from(vec![
" • Session ID: ".into(),
session_id.to_string().into(),
]));
lines.push(vec![" • Session ID: ".into(), session_id.to_string().into()].into());
}
// Input: <input> [+ <cached> cached]
let mut input_line_spans: Vec<Span<'static>> = vec![
@@ -1064,17 +1025,14 @@ pub(crate) fn new_status_output(
/// Render a summary of configured MCP servers from the current `Config`.
pub(crate) fn empty_mcp_output() -> PlainHistoryCell {
let lines: Vec<Line<'static>> = vec![
Line::from("/mcp".magenta()),
Line::from(""),
Line::from(vec!["🔌 ".into(), "MCP Tools".bold()]),
Line::from(""),
Line::from(" • No MCP servers configured.".italic()),
"/mcp".magenta().into(),
"".into(),
vec!["🔌 ".into(), "MCP Tools".bold()].into(),
"".into(),
" • No MCP servers configured.".italic().into(),
Line::from(vec![
" See the ".into(),
Span::styled(
"\u{1b}]8;;https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers\u{7}MCP docs\u{1b}]8;;\u{7}",
Style::default().add_modifier(Modifier::UNDERLINED),
),
"\u{1b}]8;;https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers\u{7}MCP docs\u{1b}]8;;\u{7}".underlined(),
" to configure them.".into(),
])
.style(Style::default().add_modifier(Modifier::DIM)),
@@ -1089,15 +1047,15 @@ pub(crate) fn new_mcp_tools_output(
tools: std::collections::HashMap<String, mcp_types::Tool>,
) -> PlainHistoryCell {
let mut lines: Vec<Line<'static>> = vec![
Line::from("/mcp".magenta()),
Line::from(""),
Line::from(vec!["🔌 ".into(), "MCP Tools".bold()]),
Line::from(""),
"/mcp".magenta().into(),
"".into(),
vec!["🔌 ".into(), "MCP Tools".bold()].into(),
"".into(),
];
if tools.is_empty() {
lines.push(Line::from(" • No MCP tools available.".italic()));
lines.push(Line::from(""));
lines.push(" • No MCP tools available.".italic().into());
lines.push("".into());
return PlainHistoryCell { lines };
}
@@ -1110,18 +1068,12 @@ pub(crate) fn new_mcp_tools_output(
.collect();
names.sort();
lines.push(Line::from(vec![
" • Server: ".into(),
server.clone().into(),
]));
lines.push(vec![" • Server: ".into(), server.clone().into()].into());
if !cfg.command.is_empty() {
let cmd_display = format!("{} {}", cfg.command, cfg.args.join(" "));
lines.push(Line::from(vec![
" • Command: ".into(),
cmd_display.into(),
]));
lines.push(vec![" • Command: ".into(), cmd_display.into()].into());
}
if let Some(env) = cfg.env.as_ref()
@@ -1129,19 +1081,13 @@ pub(crate) fn new_mcp_tools_output(
{
let mut env_pairs: Vec<String> = env.iter().map(|(k, v)| format!("{k}={v}")).collect();
env_pairs.sort();
lines.push(Line::from(vec![
" • Env: ".into(),
env_pairs.join(" ").into(),
]));
lines.push(vec![" • Env: ".into(), env_pairs.join(" ").into()].into());
}
if names.is_empty() {
lines.push(Line::from(" • Tools: (none)"));
lines.push(" • Tools: (none)".into());
} else {
lines.push(Line::from(vec![
" • Tools: ".into(),
names.join(", ").into(),
]));
lines.push(vec![" • Tools: ".into(), names.join(", ").into()].into());
}
lines.push(Line::from(""));
}
@@ -1346,7 +1292,7 @@ fn output_lines(
let show_ellipsis = total > 2 * limit;
if show_ellipsis {
let omitted = total - 2 * limit;
out.push(Line::from(format!("… +{omitted} lines")));
out.push(format!("… +{omitted} lines").into());
}
let tail_start = if show_ellipsis {
@@ -1379,14 +1325,14 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
.unwrap_or_default();
let invocation_spans = vec![
Span::styled(invocation.server.clone(), Style::default().fg(Color::Cyan)),
Span::raw("."),
Span::styled(invocation.tool.clone(), Style::default().fg(Color::Cyan)),
Span::raw("("),
Span::styled(args_str, Style::default().add_modifier(Modifier::DIM)),
Span::raw(")"),
invocation.server.clone().cyan(),
".".into(),
invocation.tool.clone().cyan(),
"(".into(),
args_str.dim(),
")".into(),
];
Line::from(invocation_spans)
invocation_spans.into()
}
#[cfg(test)]