diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupt_exec_marks_failed.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupt_exec_marks_failed.snap index a7e1f4c8..89ce848b 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupt_exec_marks_failed.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__interrupt_exec_marks_failed.snap @@ -3,4 +3,4 @@ source: tui/src/chatwidget/tests.rs expression: exec_blob --- >_ - ✗ ⌨️ sleep 1 + ✗ ⌨  sleep 1 diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 659aafd5..fdfd7041 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1207,7 +1207,7 @@ fn stream_error_is_rendered_to_history() { let cells = drain_insert_history(&mut rx); assert!(!cells.is_empty(), "expected a history cell for StreamError"); let blob = lines_to_single_string(cells.last().unwrap()); - assert!(blob.contains("⚠ ")); + assert!(blob.contains("⚠  ")); assert!(blob.contains("stream error:")); assert!(blob.contains("idle timeout waiting for SSE")); } diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 93765385..079c853b 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -230,6 +230,20 @@ fn pretty_provider_name(id: &str) -> String { title_case(id) } } +/// Return the emoji followed by a hair space (U+200A) and a normal space. +/// This creates a reasonable gap across different terminals, +/// in particular Terminal.app and iTerm, which render too tightly with just a single normal space. +/// +/// Improvements here could be to condition this behavior on terminal, +/// or possibly on emoji. +fn padded_emoji(emoji: &str) -> String { + format!("{emoji}\u{200A} ") +} + +/// Convenience function over `padded_emoji()`. +fn padded_emoji_with(emoji: &str, text: impl AsRef) -> String { + format!("{}{}", padded_emoji(emoji), text.as_ref()) +} pub(crate) fn new_session_info( config: &Config, @@ -368,22 +382,22 @@ fn new_parsed_command( for parsed in parsed_commands.iter() { let text = match parsed { - ParsedCommand::Read { name, .. } => format!("📖 {name}"), + ParsedCommand::Read { name, .. } => padded_emoji_with("📖", name), ParsedCommand::ListFiles { cmd, path } => match path { - Some(p) => format!("📂 {p}"), - None => format!("📂 {cmd}"), + Some(p) => padded_emoji_with("📂", p), + None => padded_emoji_with("📂", cmd), }, ParsedCommand::Search { query, path, cmd } => match (query, path) { - (Some(q), Some(p)) => format!("🔎 {q} in {p}"), - (Some(q), None) => format!("🔎 {q}"), - (None, Some(p)) => format!("🔎 {p}"), - (None, None) => format!("🔎 {cmd}"), + (Some(q), Some(p)) => padded_emoji_with("🔎", format!("{q} in {p}")), + (Some(q), None) => padded_emoji_with("🔎", q), + (None, Some(p)) => padded_emoji_with("🔎", p), + (None, None) => padded_emoji_with("🔎", cmd), }, - ParsedCommand::Format { .. } => "✨ Formatting".to_string(), - ParsedCommand::Test { cmd } => format!("🧪 {cmd}"), - ParsedCommand::Lint { cmd, .. } => format!("🧹 {cmd}"), - ParsedCommand::Unknown { cmd } => format!("⌨️ {cmd}"), - ParsedCommand::Noop { cmd } => format!("🔄 {cmd}"), + ParsedCommand::Format { .. } => padded_emoji_with("✨", "Formatting"), + ParsedCommand::Test { cmd } => padded_emoji_with("🧪", cmd), + ParsedCommand::Lint { cmd, .. } => padded_emoji_with("🧹", cmd), + ParsedCommand::Unknown { cmd } => padded_emoji_with("⌨", cmd), + ParsedCommand::Noop { cmd } => padded_emoji_with("🔄", cmd), }; // Prefix: two spaces, marker, space. Continuations align under the text block. for (j, line_text) in text.lines().enumerate() { @@ -469,8 +483,10 @@ pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistor } pub(crate) fn new_web_search_call(query: String) -> PlainHistoryCell { - let lines: Vec> = - vec![Line::from(""), Line::from(vec!["🌐 ".into(), query.into()])]; + let lines: Vec> = vec![ + Line::from(""), + Line::from(vec![padded_emoji("🌐").into(), query.into()]), + ]; PlainHistoryCell { lines } } @@ -614,7 +630,10 @@ pub(crate) fn new_status_output( }; // 📂 Workspace - lines.push(Line::from(vec!["📂 ".into(), "Workspace".bold()])); + lines.push(Line::from(vec![ + padded_emoji("📂").into(), + "Workspace".bold(), + ])); // 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() => { @@ -695,7 +714,10 @@ pub(crate) fn new_status_output( if let Ok(auth) = try_read_auth_json(&auth_file) && let Some(tokens) = auth.tokens.clone() { - lines.push(Line::from(vec!["👤 ".into(), "Account".bold()])); + lines.push(Line::from(vec![ + padded_emoji("👤").into(), + "Account".bold(), + ])); lines.push(Line::from(" • Signed in with ChatGPT")); let info = tokens.id_token; @@ -722,7 +744,7 @@ pub(crate) fn new_status_output( } // 🧠 Model - lines.push(Line::from(vec!["🧠 ".into(), "Model".bold()])); + lines.push(Line::from(vec![padded_emoji("🧠").into(), "Model".bold()])); lines.push(Line::from(vec![ " • Name: ".into(), config.model.clone().into(), @@ -873,13 +895,21 @@ pub(crate) fn new_mcp_tools_output( } pub(crate) fn new_error_event(message: String) -> PlainHistoryCell { - let lines: Vec> = vec!["".into(), vec!["🖐 ".red().bold(), message.into()].into()]; + // Use a hair space (U+200A) to create a subtle, near-invisible separation + // before the text. VS16 is intentionally omitted to keep spacing tighter + // in terminals like Ghostty. + let lines: Vec> = vec![ + "".into(), + vec![padded_emoji("🖐").red().bold(), message.into()].into(), + ]; PlainHistoryCell { lines } } pub(crate) fn new_stream_error_event(message: String) -> PlainHistoryCell { - let lines: Vec> = - vec![vec!["⚠ ".magenta().bold(), message.dim()].into(), "".into()]; + let lines: Vec> = vec![ + vec![padded_emoji("⚠").magenta().bold(), message.dim()].into(), + "".into(), + ]; PlainHistoryCell { lines } }