syntax-highlight bash lines (#3142)
i'm not yet convinced i have the best heuristics for what to highlight, but this feels like a useful step towards something a bit easier to read, esp. when the model is producing large commands. <img width="669" height="589" alt="Screenshot 2025-09-03 at 8 21 56 PM" src="https://github.com/user-attachments/assets/b9cbcc43-80e8-4d41-93c8-daa74b84b331" /> also a fairly significant refactor of our line wrapping logic.
This commit is contained in:
@@ -2,9 +2,14 @@ use crate::diff_render::create_diff_summary;
|
||||
use crate::exec_command::relativize_to_home;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::markdown::append_markdown;
|
||||
use crate::render::line_utils::line_to_static;
|
||||
use crate::render::line_utils::prefix_lines;
|
||||
use crate::render::line_utils::push_owned_lines;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::text_formatting::format_and_truncate_tool_result;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_line;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use base64::Engine;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_common::create_config_summary_entries;
|
||||
@@ -97,8 +102,7 @@ impl HistoryCell for UserHistoryCell {
|
||||
let wrapped = textwrap::wrap(
|
||||
&self.message,
|
||||
textwrap::Options::new(wrap_width as usize)
|
||||
.wrap_algorithm(textwrap::WrapAlgorithm::FirstFit) // Match textarea wrap
|
||||
.word_splitter(textwrap::WordSplitter::NoHyphenation),
|
||||
.wrap_algorithm(textwrap::WrapAlgorithm::FirstFit), // Match textarea wrap
|
||||
);
|
||||
|
||||
for line in wrapped {
|
||||
@@ -132,28 +136,16 @@ impl AgentMessageCell {
|
||||
|
||||
impl HistoryCell for AgentMessageCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
// We want:
|
||||
// - First visual line: "> " prefix (collapse with header logic)
|
||||
// - All subsequent visual lines: two-space prefix
|
||||
let mut is_first_visual = true;
|
||||
let wrap_width = width.saturating_sub(2); // account for prefix
|
||||
for line in &self.lines {
|
||||
let wrapped =
|
||||
crate::insert_history::word_wrap_lines(std::slice::from_ref(line), wrap_width);
|
||||
for (i, piece) in wrapped.into_iter().enumerate() {
|
||||
let mut spans = Vec::with_capacity(piece.spans.len() + 1);
|
||||
spans.push(if is_first_visual && i == 0 && self.is_first_line {
|
||||
word_wrap_lines(
|
||||
&self.lines,
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(if self.is_first_line {
|
||||
"> ".into()
|
||||
} else {
|
||||
" ".into()
|
||||
});
|
||||
spans.extend(piece.spans.into_iter());
|
||||
out.push(spans.into());
|
||||
}
|
||||
is_first_visual = false;
|
||||
}
|
||||
out
|
||||
})
|
||||
.subsequent_indent(" ".into()),
|
||||
)
|
||||
}
|
||||
|
||||
fn transcript_lines(&self) -> Vec<Line<'static>> {
|
||||
@@ -278,13 +270,13 @@ impl ExecCell {
|
||||
}
|
||||
|
||||
fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
let active_start_time = self
|
||||
.calls
|
||||
.iter()
|
||||
.find(|c| c.output.is_none())
|
||||
.and_then(|c| c.start_time);
|
||||
lines.push(Line::from(vec![
|
||||
out.push(Line::from(vec![
|
||||
if self.is_active() {
|
||||
// Show an animated spinner while exploring
|
||||
spinner(active_start_time)
|
||||
@@ -299,7 +291,7 @@ impl ExecCell {
|
||||
},
|
||||
]));
|
||||
let mut calls = self.calls.clone();
|
||||
let mut first = true;
|
||||
let mut out_indented = Vec::new();
|
||||
while !calls.is_empty() {
|
||||
let mut call = calls.remove(0);
|
||||
if call
|
||||
@@ -372,39 +364,24 @@ impl ExecCell {
|
||||
lines
|
||||
};
|
||||
for (title, line) in call_lines {
|
||||
let prefix_len = 4 + title.len() + 1; // " └ " + title + " "
|
||||
let wrapped = crate::insert_history::word_wrap_lines(
|
||||
&[line.into()],
|
||||
width.saturating_sub(prefix_len as u16),
|
||||
let line = Line::from(line);
|
||||
let initial_indent = Line::from(vec![title.cyan(), " ".into()]);
|
||||
let subsequent_indent = " ".repeat(initial_indent.width()).into();
|
||||
let wrapped = word_wrap_line(
|
||||
&line,
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent(initial_indent)
|
||||
.subsequent_indent(subsequent_indent),
|
||||
);
|
||||
let mut first_sub = true;
|
||||
for mut line in wrapped {
|
||||
let mut spans = Vec::with_capacity(line.spans.len() + 1);
|
||||
spans.push(if first {
|
||||
first = false;
|
||||
" └ ".dim()
|
||||
} else {
|
||||
" ".into()
|
||||
});
|
||||
if first_sub {
|
||||
first_sub = false;
|
||||
spans.push(title.cyan());
|
||||
spans.push(" ".into());
|
||||
} else {
|
||||
spans.push(" ".repeat(title.width() + 1).into());
|
||||
}
|
||||
spans.extend(line.spans.into_iter());
|
||||
line.spans = spans;
|
||||
lines.push(line);
|
||||
}
|
||||
push_owned_lines(&wrapped, &mut out_indented);
|
||||
}
|
||||
}
|
||||
lines
|
||||
out.extend(prefix_lines(out_indented, " └ ".dim(), " ".into()));
|
||||
out
|
||||
}
|
||||
|
||||
fn command_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
use textwrap::Options as TwOptions;
|
||||
use textwrap::WordSplitter;
|
||||
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let [call] = &self.calls.as_slice() else {
|
||||
@@ -424,38 +401,28 @@ impl ExecCell {
|
||||
// "• Running " (including trailing space) as the reserved prefix width.
|
||||
// If the command contains newlines, always use the multi-line variant.
|
||||
let reserved = "• Running ".width();
|
||||
let mut branch_consumed = false;
|
||||
|
||||
if !cmd_display.contains('\n')
|
||||
&& cmd_display.width() < (width as usize).saturating_sub(reserved)
|
||||
let mut body_lines: Vec<Line<'static>> = Vec::new();
|
||||
|
||||
let highlighted_lines = crate::render::highlight::highlight_bash_to_lines(&cmd_display);
|
||||
|
||||
if highlighted_lines.len() == 1
|
||||
&& highlighted_lines[0].width() < (width as usize).saturating_sub(reserved)
|
||||
{
|
||||
lines.push(Line::from(vec![
|
||||
bullet,
|
||||
" ".into(),
|
||||
title.bold(),
|
||||
" ".into(),
|
||||
cmd_display.clone().into(),
|
||||
]));
|
||||
let mut line = Line::from(vec![bullet, " ".into(), title.bold(), " ".into()]);
|
||||
line.extend(highlighted_lines[0].clone());
|
||||
lines.push(line);
|
||||
} else {
|
||||
branch_consumed = true;
|
||||
lines.push(vec![bullet, " ".into(), title.bold()].into());
|
||||
|
||||
// Wrap the command line.
|
||||
for (i, line) in cmd_display.lines().enumerate() {
|
||||
let wrapped = textwrap::wrap(
|
||||
line,
|
||||
TwOptions::new(width as usize)
|
||||
.initial_indent(" ")
|
||||
.subsequent_indent(" ")
|
||||
.word_splitter(WordSplitter::NoHyphenation),
|
||||
);
|
||||
lines.extend(wrapped.into_iter().enumerate().map(|(j, l)| {
|
||||
if i == 0 && j == 0 {
|
||||
vec![" └ ".dim(), l[4..].to_string().into()].into()
|
||||
} else {
|
||||
l.to_string().into()
|
||||
}
|
||||
}));
|
||||
for hl_line in highlighted_lines.iter() {
|
||||
let opts = crate::wrapping::RtOptions::new((width as usize).saturating_sub(4))
|
||||
.initial_indent("".into())
|
||||
.subsequent_indent(" ".into())
|
||||
// Hyphenation likes to break words on hyphens, which is bad for bash scripts --because-of-flags.
|
||||
.word_splitter(textwrap::WordSplitter::NoHyphenation);
|
||||
let wrapped_borrowed = crate::wrapping::word_wrap_line(hl_line, opts);
|
||||
body_lines.extend(wrapped_borrowed.iter().map(|l| line_to_static(l)));
|
||||
}
|
||||
}
|
||||
if let Some(output) = call.output.as_ref()
|
||||
@@ -466,25 +433,13 @@ impl ExecCell {
|
||||
.join("\n");
|
||||
if !out.trim().is_empty() {
|
||||
// Wrap the output.
|
||||
for (i, line) in out.lines().enumerate() {
|
||||
let wrapped = textwrap::wrap(
|
||||
line,
|
||||
TwOptions::new(width as usize - 4)
|
||||
.word_splitter(WordSplitter::NoHyphenation),
|
||||
);
|
||||
lines.extend(wrapped.into_iter().map(|l| {
|
||||
Line::from(vec![
|
||||
if i == 0 && !branch_consumed {
|
||||
" └ ".dim()
|
||||
} else {
|
||||
" ".dim()
|
||||
},
|
||||
l.to_string().dim(),
|
||||
])
|
||||
}));
|
||||
for line in out.lines() {
|
||||
let wrapped = textwrap::wrap(line, TwOptions::new(width as usize - 4));
|
||||
body_lines.extend(wrapped.into_iter().map(|l| Line::from(l.to_string().dim())));
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.extend(prefix_lines(body_lines, " └ ".dim(), " ".into()));
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user