mirror of
https://github.com/openai/codex.git
synced 2026-10-19 08:16:48 +00:00
Render ANSI styles in TUI hook system messages (#50359)
## Why Hook system messages were rendered as plain text, leaving ANSI formatting uninterpreted. ## What changed Parse hook system messages into styled spans so colors and text styles render across lines. Preserve hook prefixes, continuation indentation, and trailing blank lines. ## Testing Add a snapshot test for multiline ANSI styling and style resets, and extend multiline coverage to include a combining character and a trailing newline. GitOrigin-RevId: 0a5a88ec26fac114fc66efa656bacf6fa15a1adf
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
//! 4. Completed runs only persist when they have user-facing output or a non-success status.
|
||||
use super::HistoryCell;
|
||||
use super::plain_lines;
|
||||
use codex_ansi_escape::ansi_escape;
|
||||
use codex_app_server_protocol::HookOutputEntry;
|
||||
use codex_app_server_protocol::HookOutputEntryKind;
|
||||
use codex_app_server_protocol::HookRunStatus;
|
||||
@@ -280,11 +281,18 @@ impl HookCell {
|
||||
let system_message = entries
|
||||
.iter()
|
||||
.find(|entry| entry.kind == HookOutputEntryKind::Warning);
|
||||
let mut system_message_lines = system_message.map(|entry| entry.text.split('\n'));
|
||||
let mut system_message_lines = system_message.map(|entry| {
|
||||
let mut lines = ansi_escape(&entry.text).lines;
|
||||
if entry.text.ends_with('\n') {
|
||||
lines.push("".into());
|
||||
}
|
||||
lines.into_iter()
|
||||
});
|
||||
if *status == HookRunStatus::Completed
|
||||
&& let Some(first_line) = system_message_lines.as_mut().and_then(Iterator::next)
|
||||
&& let Some(mut first_line) = system_message_lines.as_mut().and_then(Iterator::next)
|
||||
{
|
||||
lines.push(vec!["↳ Hook · ".dim(), first_line.to_string().into()].into());
|
||||
first_line.spans.insert(/*index*/ 0, "↳ Hook · ".dim());
|
||||
lines.push(first_line);
|
||||
} else {
|
||||
let header_text = match status {
|
||||
HookRunStatus::Completed => "Hook completed",
|
||||
@@ -301,16 +309,22 @@ impl HookCell {
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
if let Some(first_line) = system_message_lines.as_mut().and_then(Iterator::next) {
|
||||
lines.push(format!("{HOOK_OUTPUT_INDENT}└ {first_line}").into());
|
||||
if let Some(mut first_line) = system_message_lines.as_mut().and_then(Iterator::next)
|
||||
{
|
||||
first_line
|
||||
.spans
|
||||
.insert(/*index*/ 0, format!("{HOOK_OUTPUT_INDENT}└ ").into());
|
||||
lines.push(first_line);
|
||||
}
|
||||
}
|
||||
if let Some(system_message_lines) = system_message_lines {
|
||||
for line in system_message_lines {
|
||||
if line.is_empty() {
|
||||
for mut line in system_message_lines {
|
||||
if line.spans.is_empty() {
|
||||
lines.push("".into());
|
||||
} else {
|
||||
lines.push(format!("{HOOK_OUTPUT_BODY_INDENT}{line}").into());
|
||||
line.spans
|
||||
.insert(/*index*/ 0, HOOK_OUTPUT_BODY_INDENT.into());
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,6 +567,29 @@ mod tests {
|
||||
assert_eq!(lines[0].spans[1].style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_hook_system_message_renders_ansi_styles() {
|
||||
let cell = completed_hook_cell(
|
||||
HookEventName::UserPromptSubmit,
|
||||
HookRunStatus::Completed,
|
||||
vec![HookOutputEntry {
|
||||
kind: HookOutputEntryKind::Warning,
|
||||
text: "\x1b[1;92mStyled output\nacross lines\x1b[0m plain".to_string(),
|
||||
}],
|
||||
);
|
||||
|
||||
let lines = cell.display_lines(/*width*/ 80);
|
||||
|
||||
assert_eq!(
|
||||
line_texts(&cell.raw_lines()),
|
||||
vec![
|
||||
"↳ Hook · Styled output".to_string(),
|
||||
" across lines plain".to_string(),
|
||||
],
|
||||
);
|
||||
insta::assert_debug_snapshot!("completed_hook_system_message_ansi_styles", lines);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_hook_with_only_context_is_quiet_on_every_tui_surface() {
|
||||
let cell = completed_hook_cell(
|
||||
@@ -684,7 +721,7 @@ mod tests {
|
||||
HookRunStatus::Completed,
|
||||
vec![HookOutputEntry {
|
||||
kind: HookOutputEntryKind::Warning,
|
||||
text: "Heads up\nReview generated files".to_string(),
|
||||
text: "Heads up\n\u{301}\nReview generated files\n".to_string(),
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -692,7 +729,9 @@ mod tests {
|
||||
line_texts(&cell.display_lines(/*width*/ 80)),
|
||||
vec![
|
||||
"↳ Hook · Heads up".to_string(),
|
||||
" \u{301}".to_string(),
|
||||
" Review generated files".to_string(),
|
||||
"".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
---
|
||||
source: tui/src/history_cell/hook_cell.rs
|
||||
expression: lines
|
||||
---
|
||||
[
|
||||
Line::from_iter([
|
||||
Span::from("↳ Hook · ").dim(),
|
||||
Span::from("Styled output").light_green().bold(),
|
||||
]),
|
||||
Line::from_iter([
|
||||
Span::from(" "),
|
||||
Span::from("across lines").light_green().bold(),
|
||||
Span::from(" plain").fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).not_bold().not_dim().not_italic().not_underlined().not_slow_blink().not_rapid_blink().not_reversed().not_hidden().not_crossed_out(),
|
||||
]),
|
||||
]
|
||||
Reference in New Issue
Block a user