diff --git a/AGENTS.md b/AGENTS.md index 14788cb6..968ce5b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,17 @@ See `codex-rs/tui/styles.md`. - Example: patch summary file lines - Desired: vec![" └ ".into(), "M".red(), " ".dim(), "tui/src/app.rs".dim()] +### TUI Styling (ratatui) +- Prefer Stylize helpers: use "text".dim(), .bold(), .cyan(), .italic(), .underlined() instead of manual Style where possible. +- Prefer simple conversions: use "text".into() for spans and vec![…].into() for lines; when inference is ambiguous (e.g., Paragraph::new/Cell::from), use Line::from(spans) or Span::from(text). +- Computed styles: if the Style is computed at runtime, using `Span::styled` is OK (`Span::from(text).set_style(style)` is also acceptable). +- Avoid hardcoded white: do not use `.white()`; prefer the default foreground (no color). +- Chaining: combine helpers by chaining for readability (e.g., url.cyan().underlined()). +- Single items: prefer "text".into(); use Line::from(text) or Span::from(text) only when the target type isn’t obvious from context, or when using .into() would require extra type annotations. +- Building lines: use vec![…].into() to construct a Line when the target type is obvious and no extra type annotations are needed; otherwise use Line::from(vec![…]). +- Avoid churn: don’t refactor between equivalent forms (Span::styled ↔ set_style, Line::from ↔ .into()) without a clear readability or functional gain; follow file‑local conventions and do not introduce type annotations solely to satisfy .into(). +- Compactness: prefer the form that stays on one line after rustfmt; if only one of Line::from(vec![…]) or vec![…].into() avoids wrapping, choose that. If both wrap, pick the one with fewer wrapped lines. + ## Snapshot tests This repo uses snapshot tests (via `insta`), especially in `codex-rs/tui`, to validate rendered output. When UI or text output changes intentionally, update the snapshots as follows: diff --git a/codex-rs/ansi-escape/src/lib.rs b/codex-rs/ansi-escape/src/lib.rs index 3daaf46e..68ea5e9a 100644 --- a/codex-rs/ansi-escape/src/lib.rs +++ b/codex-rs/ansi-escape/src/lib.rs @@ -9,7 +9,7 @@ use ratatui::text::Text; pub fn ansi_escape_line(s: &str) -> Line<'static> { let text = ansi_escape(s); match text.lines.as_slice() { - [] => Line::from(""), + [] => "".into(), [only] => only.clone(), [first, rest @ ..] => { tracing::warn!("ansi_escape_line: expected a single line, got {first:?} and {rest:?}"); diff --git a/codex-rs/tui/src/backtrack_helpers.rs b/codex-rs/tui/src/backtrack_helpers.rs index c275519c..b006ae38 100644 --- a/codex-rs/tui/src/backtrack_helpers.rs +++ b/codex-rs/tui/src/backtrack_helpers.rs @@ -113,10 +113,9 @@ fn highlight_range_from_header(lines: &[Line<'_>], header_idx: usize) -> (usize, #[cfg(test)] mod tests { use super::*; - use ratatui::text::Span; fn line(s: &str) -> Line<'static> { - Line::from(Span::raw(s.to_string())) + s.to_string().into() } fn transcript_with_users(count: usize) -> Vec> { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index fc44d5ef..f7767f26 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1262,9 +1262,9 @@ impl WidgetRef for ChatComposer { let key_hint_style = Style::default().fg(Color::Cyan); let mut hint = if self.ctrl_c_quit_hint { vec![ - Span::from(" "), + " ".into(), "Ctrl+C again".set_style(key_hint_style), - Span::from(" to quit"), + " to quit".into(), ] } else { let newline_hint_key = if self.use_shift_enter_hint { @@ -1273,28 +1273,28 @@ impl WidgetRef for ChatComposer { "Ctrl+J" }; vec![ - Span::from(" "), + " ".into(), "⏎".set_style(key_hint_style), - Span::from(" send "), + " send ".into(), newline_hint_key.set_style(key_hint_style), - Span::from(" newline "), + " newline ".into(), "Ctrl+T".set_style(key_hint_style), - Span::from(" transcript "), + " transcript ".into(), "Ctrl+C".set_style(key_hint_style), - Span::from(" quit"), + " quit".into(), ] }; if !self.ctrl_c_quit_hint && self.esc_backtrack_hint { - hint.push(Span::from(" ")); + hint.push(" ".into()); hint.push("Esc".set_style(key_hint_style)); - hint.push(Span::from(" edit prev")); + hint.push(" edit prev".into()); } // Append token/context usage info to the footer hints when available. if let Some(token_usage_info) = &self.token_usage_info { let token_usage = &token_usage_info.total_token_usage; - hint.push(Span::from(" ")); + hint.push(" ".into()); hint.push( Span::from(format!("{} tokens used", token_usage.blended_total())) .style(Style::default().add_modifier(Modifier::DIM)), @@ -1309,7 +1309,7 @@ impl WidgetRef for ChatComposer { } else { 100 }; - hint.push(Span::from(" ")); + hint.push(" ".into()); hint.push( Span::from(format!("{percent_remaining}% context left")) .style(Style::default().add_modifier(Modifier::DIM)), diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 74b5274f..6760b67f 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -3,8 +3,7 @@ use crossterm::event::KeyEvent; use crossterm::event::KeyModifiers; use ratatui::buffer::Buffer; use ratatui::layout::Rect; -use ratatui::style::Modifier; -use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::Paragraph; @@ -42,7 +41,7 @@ pub(crate) struct ListSelectionView { impl ListSelectionView { fn dim_prefix_span() -> Span<'static> { - Span::styled("▌ ", Style::default().add_modifier(Modifier::DIM)) + "▌ ".dim() } fn render_dim_prefix_line(area: Rect, buf: &mut Buffer) { @@ -162,13 +161,8 @@ impl BottomPaneView for ListSelectionView { height: 1, }; - let title_spans: Vec> = vec![ - Self::dim_prefix_span(), - Span::styled( - self.title.clone(), - Style::default().add_modifier(Modifier::BOLD), - ), - ]; + let title_spans: Vec> = + vec![Self::dim_prefix_span(), self.title.clone().bold()]; let title_para = Paragraph::new(Line::from(title_spans)); title_para.render(title_area, buf); @@ -180,10 +174,8 @@ impl BottomPaneView for ListSelectionView { width: area.width, height: 1, }; - let subtitle_spans: Vec> = vec![ - Self::dim_prefix_span(), - Span::styled(sub.clone(), Style::default().add_modifier(Modifier::DIM)), - ]; + let subtitle_spans: Vec> = + vec![Self::dim_prefix_span(), sub.clone().dim()]; let subtitle_para = Paragraph::new(Line::from(subtitle_spans)); subtitle_para.render(subtitle_area, buf); // Render the extra spacer line with the dimmed prefix to align with title/subtitle @@ -240,10 +232,7 @@ impl BottomPaneView for ListSelectionView { width: area.width, height: 1, }; - let footer_para = Paragraph::new(Line::from(Span::styled( - hint.clone(), - Style::default().add_modifier(Modifier::DIM), - ))); + let footer_para = Paragraph::new(hint.clone().dim()); footer_para.render(footer_area, buf); } } diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs index a83ec151..c5de9510 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -4,6 +4,7 @@ use ratatui::prelude::Constraint; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::Block; @@ -38,10 +39,9 @@ pub(crate) fn render_rows( ) { let mut rows: Vec = Vec::new(); if rows_all.is_empty() { - rows.push(Row::new(vec![Cell::from(Line::from(Span::styled( - "no matches", - Style::default().add_modifier(Modifier::ITALIC | Modifier::DIM), - )))])); + rows.push(Row::new(vec![Cell::from(Line::from( + "no matches".dim().italic(), + ))])); } else { let max_rows_from_area = area.height as usize; let visible_rows = max_results @@ -79,23 +79,20 @@ pub(crate) fn render_rows( if let Some(idxs) = match_indices.as_ref() { let mut idx_iter = idxs.iter().peekable(); for (char_idx, ch) in name.chars().enumerate() { - let mut style = Style::default(); if idx_iter.peek().is_some_and(|next| **next == char_idx) { idx_iter.next(); - style = style.add_modifier(Modifier::BOLD); + spans.push(ch.to_string().bold()); + } else { + spans.push(ch.to_string().into()); } - spans.push(Span::styled(ch.to_string(), style)); } } else { - spans.push(Span::raw(name.clone())); + spans.push(name.clone().into()); } if let Some(desc) = description.as_ref() { - spans.push(Span::raw(" ")); - spans.push(Span::styled( - desc.clone(), - Style::default().add_modifier(Modifier::DIM), - )); + spans.push(" ".into()); + spans.push(desc.clone().dim()); } let mut cell = Cell::from(Line::from(spans)); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index db4609ca..290d0b19 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -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> { let mut lines: Vec> = 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> { let mut out: Vec> = 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> { - 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, ) -> PlainHistoryCell { let mut lines: Vec> = 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: [+ cached] let mut input_line_spans: Vec> = 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> = 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, ) -> PlainHistoryCell { let mut lines: Vec> = 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 = 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)] diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index 2c35c877..1ecbd344 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -419,7 +419,7 @@ mod tests { #[test] fn line_height_counts_double_width_emoji() { - let line = Line::from("😀😀😀"); // each emoji ~ width 2 + let line = "😀😀😀".into(); // each emoji ~ width 2 assert_eq!(word_wrap_line(&line, 4).len(), 2); assert_eq!(word_wrap_line(&line, 2).len(), 3); assert_eq!(word_wrap_line(&line, 6).len(), 1); @@ -428,7 +428,7 @@ mod tests { #[test] fn word_wrap_does_not_split_words_simple_english() { let sample = "Years passed, and Willowmere thrived in peace and friendship. Mira’s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them."; - let line = Line::from(sample); + let line = sample.into(); // Force small width to exercise wrapping at spaces. let wrapped = word_wrap_lines(&[line], 40); let joined: String = wrapped diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 5da8e0d6..6010c9f0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -256,7 +256,6 @@ async fn run_ratatui_app( if let Some(latest_version) = updates::get_upgrade_version(&config) { use ratatui::style::Stylize as _; use ratatui::text::Line; - use ratatui::text::Span; let current_version = env!("CARGO_PKG_VERSION"); let exe = std::env::current_exe()?; @@ -265,35 +264,35 @@ async fn run_ratatui_app( let mut lines: Vec> = Vec::new(); lines.push(Line::from(vec![ "✨⬆️ Update available!".bold().cyan(), - Span::raw(" "), - Span::raw(format!("{current_version} -> {latest_version}.")), + " ".into(), + format!("{current_version} -> {latest_version}.").into(), ])); if managed_by_npm { let npm_cmd = "npm install -g @openai/codex@latest"; lines.push(Line::from(vec![ - Span::raw("Run "), + "Run ".into(), npm_cmd.cyan(), - Span::raw(" to update."), + " to update.".into(), ])); } else if cfg!(target_os = "macos") && (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local")) { let brew_cmd = "brew upgrade codex"; lines.push(Line::from(vec![ - Span::raw("Run "), + "Run ".into(), brew_cmd.cyan(), - Span::raw(" to update."), + " to update.".into(), ])); } else { lines.push(Line::from(vec![ - Span::raw("See "), + "See ".into(), "https://github.com/openai/codex/releases/latest".cyan(), - Span::raw(" for the latest releases and installation options."), + " for the latest releases and installation options.".into(), ])); } - lines.push(Line::from("")); + lines.push("".into()); tui.insert_history_lines(lines); } diff --git a/codex-rs/tui/src/markdown.rs b/codex-rs/tui/src/markdown.rs index fcb9e774..6aff205a 100644 --- a/codex-rs/tui/src/markdown.rs +++ b/codex-rs/tui/src/markdown.rs @@ -2,7 +2,6 @@ use crate::citation_regex::CITATION_REGEX; use codex_core::config::Config; use codex_core::config_types::UriBasedFileOpener; use ratatui::text::Line; -use ratatui::text::Span; use std::borrow::Cow; use std::path::Path; @@ -44,7 +43,7 @@ fn append_markdown_with_opener_and_cwd( } else { line }; - let owned_line: Line<'static> = Line::from(Span::raw(line.to_string())); + let owned_line: Line<'static> = line.to_string().into(); lines.push(owned_line); } } diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index 347289d2..7f350fe3 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -15,7 +15,6 @@ use ratatui::style::Modifier; use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; @@ -120,20 +119,14 @@ impl AuthModeWidget { fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) { let mut lines: Vec = vec![ Line::from(vec![ - Span::raw("> "), - Span::styled( - "Sign in with ChatGPT to use Codex as part of your paid plan", - Style::default().add_modifier(Modifier::BOLD), - ), + "> ".into(), + "Sign in with ChatGPT to use Codex as part of your paid plan".bold(), ]), Line::from(vec![ - Span::raw(" "), - Span::styled( - "or connect an API key for usage-based billing", - Style::default().add_modifier(Modifier::BOLD), - ), + " ".into(), + "or connect an API key for usage-based billing".bold(), ]), - Line::from(""), + "".into(), ]; // If the user is already authenticated but the method differs from their @@ -150,8 +143,8 @@ impl AuthModeWidget { to_label(current), to_label(self.preferred_auth_method) ); - lines.push(Line::from(msg).style(Style::default())); - lines.push(Line::from("")); + lines.push(msg.into()); + lines.push("".into()); } let create_mode_item = |idx: usize, @@ -168,7 +161,7 @@ impl AuthModeWidget { text.to_string().cyan(), ]) } else { - Line::from(format!(" {}. {text}", idx + 1)) + format!(" {}. {text}", idx + 1).into() }; let line2 = if is_selected { @@ -207,19 +200,15 @@ impl AuthModeWidget { api_key_label, "Pay for what you use", )); - lines.push(Line::from("")); + lines.push("".into()); lines.push( // AE: Following styles.md, this should probably be Cyan because it's a user input tip. // But leaving this for a future cleanup. - Line::from(" Press Enter to continue") - .style(Style::default().add_modifier(Modifier::DIM)), + " Press Enter to continue".dim().into(), ); if let Some(err) = &self.error { - lines.push(Line::from("")); - lines.push(Line::from(Span::styled( - err.as_str(), - Style::default().fg(Color::Red), - ))); + lines.push("".into()); + lines.push(err.as_str().red().into()); } Paragraph::new(lines) @@ -228,28 +217,23 @@ impl AuthModeWidget { } fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) { - let mut spans = vec![Span::from("> ")]; + let mut spans = vec!["> ".into()]; // Schedule a follow-up frame to keep the shimmer animation going. self.request_frame .schedule_frame_in(std::time::Duration::from_millis(100)); spans.extend(shimmer_spans("Finish signing in via your browser")); - let mut lines = vec![Line::from(spans), Line::from("")]; + let mut lines = vec![spans.into(), "".into()]; let sign_in_state = self.sign_in_state.read().unwrap(); if let SignInState::ChatGptContinueInBrowser(state) = &*sign_in_state && !state.auth_url.is_empty() { - lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:")); - lines.push(Line::from(vec![ - Span::raw(" "), - state.auth_url.as_str().cyan().underlined(), - ])); - lines.push(Line::from("")); + lines.push(" If the link doesn't open automatically, open the following link to authenticate:".into()); + lines.push(vec![" ".into(), state.auth_url.as_str().cyan().underlined()].into()); + lines.push("".into()); } - lines.push( - Line::from(" Press Esc to cancel").style(Style::default().add_modifier(Modifier::DIM)), - ); + lines.push(" Press Esc to cancel".dim().into()); Paragraph::new(lines) .wrap(Wrap { trim: false }) .render(area, buf); @@ -257,35 +241,28 @@ impl AuthModeWidget { fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) { let lines = vec![ - Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green), - Line::from(""), - Line::from("> Before you start:"), - Line::from(""), - Line::from(" Decide how much autonomy you want to grant Codex"), + "✓ Signed in with your ChatGPT account".fg(Color::Green).into(), + "".into(), + "> Before you start:".into(), + "".into(), + " Decide how much autonomy you want to grant Codex".into(), Line::from(vec![ - Span::raw(" For more details see the "), - Span::styled( - "\u{1b}]8;;https://github.com/openai/codex\u{7}Codex docs\u{1b}]8;;\u{7}", - Style::default().add_modifier(Modifier::UNDERLINED), - ), + " For more details see the ".into(), + "\u{1b}]8;;https://github.com/openai/codex\u{7}Codex docs\u{1b}]8;;\u{7}".underlined(), ]) - .style(Style::default().add_modifier(Modifier::DIM)), - Line::from(""), - Line::from(" Codex can make mistakes"), - Line::from(" Review the code it writes and commands it runs") - .style(Style::default().add_modifier(Modifier::DIM)), - Line::from(""), - Line::from(" Powered by your ChatGPT account"), + .dim(), + "".into(), + " Codex can make mistakes".into(), + " Review the code it writes and commands it runs".dim().into(), + "".into(), + " Powered by your ChatGPT account".into(), Line::from(vec![ - Span::raw(" Uses your plan's rate limits and "), - Span::styled( - "\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}", - Style::default().add_modifier(Modifier::UNDERLINED), - ), + " Uses your plan's rate limits and ".into(), + "\u{1b}]8;;https://chatgpt.com/#settings\u{7}training data preferences\u{1b}]8;;\u{7}".underlined(), ]) - .style(Style::default().add_modifier(Modifier::DIM)), - Line::from(""), - Line::from(" Press Enter to continue").fg(Color::Cyan), + .dim(), + "".into(), + " Press Enter to continue".fg(Color::Cyan).into(), ]; Paragraph::new(lines) @@ -294,7 +271,11 @@ impl AuthModeWidget { } fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { - let lines = vec![Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green)]; + let lines = vec![ + "✓ Signed in with your ChatGPT account" + .fg(Color::Green) + .into(), + ]; Paragraph::new(lines) .wrap(Wrap { trim: false }) @@ -302,7 +283,7 @@ impl AuthModeWidget { } fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) { - let lines = vec![Line::from("✓ Using OPENAI_API_KEY").fg(Color::Green)]; + let lines = vec!["✓ Using OPENAI_API_KEY".fg(Color::Green).into()]; Paragraph::new(lines) .wrap(Wrap { trim: false }) @@ -311,13 +292,11 @@ impl AuthModeWidget { fn render_env_var_missing(&self, area: Rect, buf: &mut Buffer) { let lines = vec![ - Line::from( - " To use Codex with the OpenAI API, set OPENAI_API_KEY in your environment", - ) - .style(Style::default().fg(Color::Cyan)), - Line::from(""), - Line::from(" Press Enter to return") - .style(Style::default().add_modifier(Modifier::DIM)), + " To use Codex with the OpenAI API, set OPENAI_API_KEY in your environment" + .fg(Color::Cyan) + .into(), + "".into(), + " Press Enter to return".dim().into(), ]; Paragraph::new(lines) diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs index e775a829..ce3f2a3c 100644 --- a/codex-rs/tui/src/onboarding/trust_directory.rs +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -9,10 +9,8 @@ use ratatui::layout::Rect; use ratatui::prelude::Widget; use ratatui::style::Color; use ratatui::style::Modifier; -use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; @@ -41,30 +39,25 @@ impl WidgetRef for &TrustDirectoryWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let mut lines: Vec = vec![ Line::from(vec![ - Span::raw("> "), - Span::styled( - "You are running Codex in ", - Style::default().add_modifier(Modifier::BOLD), - ), - Span::raw(self.cwd.to_string_lossy().to_string()), + "> ".into(), + "You are running Codex in ".bold(), + self.cwd.to_string_lossy().to_string().into(), ]), - Line::from(""), + "".into(), ]; if self.is_git_repo { - lines.push(Line::from( - " Since this folder is version controlled, you may wish to allow Codex", - )); - lines.push(Line::from( - " to work in this folder without asking for approval.", - )); + lines.push( + " Since this folder is version controlled, you may wish to allow Codex".into(), + ); + lines.push(" to work in this folder without asking for approval.".into()); } else { - lines.push(Line::from( - " Since this folder is not version controlled, we recommend requiring", - )); - lines.push(Line::from(" approval of all edits and commands.")); + lines.push( + " Since this folder is not version controlled, we recommend requiring".into(), + ); + lines.push(" approval of all edits and commands.".into()); } - lines.push(Line::from("")); + lines.push("".into()); let create_option = |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> { @@ -99,10 +92,10 @@ impl WidgetRef for &TrustDirectoryWidget { "Require approval of edits and commands", )); } - lines.push(Line::from("")); + lines.push("".into()); if let Some(error) = &self.error { lines.push(Line::from(format!(" {error}")).fg(Color::Red)); - lines.push(Line::from("")); + lines.push("".into()); } // AE: Following styles.md, this should probably be Cyan because it's a user input tip. // But leaving this for a future cleanup. diff --git a/codex-rs/tui/src/onboarding/welcome.rs b/codex-rs/tui/src/onboarding/welcome.rs index bcdc5c9a..5e6906bf 100644 --- a/codex-rs/tui/src/onboarding/welcome.rs +++ b/codex-rs/tui/src/onboarding/welcome.rs @@ -1,10 +1,8 @@ use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::prelude::Widget; -use ratatui::style::Modifier; -use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; use ratatui::widgets::WidgetRef; use crate::onboarding::onboarding_screen::StepStateProvider; @@ -18,11 +16,8 @@ pub(crate) struct WelcomeWidget { impl WidgetRef for &WelcomeWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { let line = Line::from(vec![ - Span::raw(">_ "), - Span::styled( - "Welcome to Codex, OpenAI's command-line coding agent", - Style::default().add_modifier(Modifier::BOLD), - ), + ">_ ".into(), + "Welcome to Codex, OpenAI's command-line coding agent".bold(), ]); line.render(area, buf); } diff --git a/codex-rs/tui/src/pager_overlay.rs b/codex-rs/tui/src/pager_overlay.rs index ed29b8fc..c730b2fd 100644 --- a/codex-rs/tui/src/pager_overlay.rs +++ b/codex-rs/tui/src/pager_overlay.rs @@ -143,7 +143,7 @@ impl PagerView { .dim() .render_ref(area, buf); let header = format!("/ {}", self.title); - Span::from(header).dim().render_ref(area, buf); + header.dim().render_ref(area, buf); } // Removed unused render_content_page (replaced by render_content_page_prepared) @@ -545,7 +545,7 @@ mod tests { fn static_overlay_snapshot_basic() { // Prepare a static overlay with a few lines and a title let mut overlay = StaticOverlay::with_title( - vec![Line::from("one"), Line::from("two"), Line::from("three")], + vec!["one".into(), "two".into(), "three".into()], "S T A T I C".to_string(), ); let mut term = Terminal::new(TestBackend::new(40, 10)).expect("term"); @@ -557,7 +557,7 @@ mod tests { #[test] fn pager_wrap_cache_reuses_for_same_width_and_rebuilds_on_change() { let long = "This is a long line that should wrap multiple times to ensure non-empty wrapped output."; - let mut pv = PagerView::new(vec![Line::from(long), Line::from(long)], "T".to_string(), 0); + let mut pv = PagerView::new(vec![long.into(), long.into()], "T".to_string(), 0); // Build cache at width 24 pv.ensure_wrapped(24); @@ -586,13 +586,13 @@ mod tests { #[test] fn pager_wrap_cache_invalidates_on_append() { let long = "Another long line for wrapping behavior verification."; - let mut pv = PagerView::new(vec![Line::from(long)], "T".to_string(), 0); + let mut pv = PagerView::new(vec![long.into()], "T".to_string(), 0); pv.ensure_wrapped(28); let (w1, _) = pv.cached(); let len1 = w1.len(); // Append new lines should cause ensure_wrapped to rebuild due to len change - pv.lines.extend([Line::from(long), Line::from(long)]); + pv.lines.extend([long.into(), long.into()]); pv.ensure_wrapped(28); let (w2, _) = pv.cached(); assert!( diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 200ba2c0..9ea2c0bc 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -115,10 +115,7 @@ fn to_command_display<'a>( cmd: String, last_line: Vec>, ) -> Vec> { - let command_lines: Vec = cmd - .lines() - .map(|line| Span::from(line.to_string()).style(Style::new().add_modifier(Modifier::DIM))) - .collect(); + let command_lines: Vec = cmd.lines().map(|line| line.to_string().dim()).collect(); let mut lines: Vec> = vec![]; @@ -128,7 +125,7 @@ fn to_command_display<'a>( first_line.extend(last_line); } else { for line in command_lines { - lines.push(Line::from(vec![Span::from(" "), line])); + lines.push(vec![" ".into(), line].into()); } let last_line = last_line.clone(); lines.push(Line::from(last_line)); diff --git a/codex-rs/tui/tests/suite/vt100_history.rs b/codex-rs/tui/tests/suite/vt100_history.rs index 1f01a6d3..973b0aa0 100644 --- a/codex-rs/tui/tests/suite/vt100_history.rs +++ b/codex-rs/tui/tests/suite/vt100_history.rs @@ -3,10 +3,8 @@ use ratatui::backend::TestBackend; use ratatui::layout::Rect; -use ratatui::style::Color; -use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Line; -use ratatui::text::Span; // Small helper macro to assert a collection contains an item with a clearer // failure message. @@ -80,7 +78,7 @@ fn basic_insertion_no_wrap() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); - let lines = vec![Line::from("first"), Line::from("second")]; + let lines = vec!["first".into(), "second".into()]; let buf = scenario.run_insert(lines); let rows = scenario.screen_rows_from_bytes(&buf); assert_contains!(rows, String::from("first")); @@ -102,7 +100,7 @@ fn long_token_wraps() { let mut scenario = TestScenario::new(20, 6, area); let long = "A".repeat(45); // > 2 lines at width 20 - let lines = vec![Line::from(long.clone())]; + let lines = vec![long.clone().into()]; let buf = scenario.run_insert(lines); let mut parser = vt100::Parser::new(6, 20, 0); parser.process(&buf); @@ -134,7 +132,7 @@ fn emoji_and_cjk() { let mut scenario = TestScenario::new(20, 6, area); let text = String::from("😀😀😀😀😀 你好世界"); - let lines = vec![Line::from(text.clone())]; + let lines = vec![text.clone().into()]; let buf = scenario.run_insert(lines); let rows = scenario.screen_rows_from_bytes(&buf); let reconstructed: String = rows.join("").chars().filter(|c| *c != ' ').collect(); @@ -151,10 +149,7 @@ fn mixed_ansi_spans() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); - let line = Line::from(vec![ - Span::styled("red", Style::default().fg(Color::Red)), - Span::raw("+plain"), - ]); + let line = vec!["red".red(), "+plain".into()].into(); let buf = scenario.run_insert(vec![line]); let rows = scenario.screen_rows_from_bytes(&buf); assert_contains!(rows, String::from("red+plain")); @@ -165,7 +160,7 @@ fn cursor_restoration() { let area = Rect::new(0, 5, 20, 1); let mut scenario = TestScenario::new(20, 6, area); - let lines = vec![Line::from("x")]; + let lines = vec!["x".into()]; let buf = scenario.run_insert(lines); let s = String::from_utf8_lossy(&buf); // CUP to 1;1 (ANSI: ESC[1;1H) @@ -187,7 +182,7 @@ fn word_wrap_no_mid_word_split() { let mut scenario = TestScenario::new(40, 10, area); let sample = "Years passed, and Willowmere thrived in peace and friendship. Mira’s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them."; - let buf = scenario.run_insert(vec![Line::from(sample)]); + let buf = scenario.run_insert(vec![sample.into()]); let rows = scenario.screen_rows_from_bytes(&buf); let joined = rows.join("\n"); assert!( @@ -203,7 +198,7 @@ fn em_dash_and_space_word_wrap() { let mut scenario = TestScenario::new(40, 10, area); let sample = "Mara found an old key on the shore. Curious, she opened a tarnished box half-buried in sand—and inside lay a single, glowing seed."; - let buf = scenario.run_insert(vec![Line::from(sample)]); + let buf = scenario.run_insert(vec![sample.into()]); let rows = scenario.screen_rows_from_bytes(&buf); let joined = rows.join("\n"); assert!( @@ -218,7 +213,7 @@ fn pre_scroll_region_down() { let area = Rect::new(0, 3, 20, 1); let mut scenario = TestScenario::new(20, 6, area); - let lines = vec![Line::from("first"), Line::from("second")]; + let lines = vec!["first".into(), "second".into()]; let buf = scenario.run_insert(lines); let s = String::from_utf8_lossy(&buf); // Expect we limited scroll region to [top+1 .. screen_height] => [4 .. 6] (1-based) diff --git a/codex-rs/tui/tests/suite/vt100_live_commit.rs b/codex-rs/tui/tests/suite/vt100_live_commit.rs index c0cfb321..e06f1027 100644 --- a/codex-rs/tui/tests/suite/vt100_live_commit.rs +++ b/codex-rs/tui/tests/suite/vt100_live_commit.rs @@ -24,10 +24,7 @@ fn live_001_commit_on_overflow() { // Keep the last 3 in the live ring; commit the first 2. let commit_rows = rb.drain_commit_ready(3); - let lines: Vec> = commit_rows - .into_iter() - .map(|r| Line::from(r.text)) - .collect(); + let lines: Vec> = commit_rows.into_iter().map(|r| r.text.into()).collect(); let mut buf: Vec = Vec::new(); codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines); @@ -80,10 +77,7 @@ fn live_002_pre_scroll_and_commit() { // Keep 3, commit 1. let commit_rows = rb.drain_commit_ready(3); - let lines: Vec> = commit_rows - .into_iter() - .map(|r| Line::from(r.text)) - .collect(); + let lines: Vec> = commit_rows.into_iter().map(|r| r.text.into()).collect(); let mut buf: Vec = Vec::new(); codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines); diff --git a/codex-rs/tui/tests/suite/vt100_streaming_no_dup.rs b/codex-rs/tui/tests/suite/vt100_streaming_no_dup.rs index a359e77a..79f9d2fb 100644 --- a/codex-rs/tui/tests/suite/vt100_streaming_no_dup.rs +++ b/codex-rs/tui/tests/suite/vt100_streaming_no_dup.rs @@ -2,7 +2,6 @@ use ratatui::backend::TestBackend; use ratatui::layout::Rect; -use ratatui::text::Line; fn term(viewport: Rect) -> codex_tui::custom_terminal::Terminal { let backend = TestBackend::new(20, 6); @@ -23,7 +22,7 @@ fn stream_commit_trickle_no_duplication() { codex_tui::insert_history::insert_history_lines_to_writer( &mut t, &mut out1, - vec![Line::from("one")], + vec!["one".into()], ); // Step 2: later commit next row @@ -31,7 +30,7 @@ fn stream_commit_trickle_no_duplication() { codex_tui::insert_history::insert_history_lines_to_writer( &mut t, &mut out2, - vec![Line::from("two")], + vec!["two".into()], ); let combined = [out1, out2].concat(); @@ -62,7 +61,7 @@ fn live_ring_rows_not_inserted_into_history() { codex_tui::insert_history::insert_history_lines_to_writer( &mut t, &mut buf, - vec![Line::from("one"), Line::from("two")], + vec!["one".into(), "two".into()], ); // The live ring might display tail+head rows like ["two", "three"],