mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Keep TUI exploration grouped across reasoning and nonzero exits (#46487)
## Why Reasoning summaries and nonzero command exits split adjacent exploration into separate groups, while compact history omitted exit codes. Replayed commands also failed to retain the same grouping as live commands. ## What changed - Keep adjacent read, list, and search commands grouped across reasoning summaries and nonzero exits in both live and replayed history. - Preserve reasoning in chronological order in the expanded transcript while omitting it from compact and raw history. - Show nonzero exit codes in compact exploration entries and keep unsuccessful reads separate from successful read summaries. Render search exit code `1` without red failure styling, and label compound-command outcomes as `command exit`. ## Testing Add regression coverage for live/replay rendering parity, reasoning order, grouping boundaries, and nonzero exit labels and colors. Update the overlapping-command test to verify that exploration stays grouped after a failure. GitOrigin-RevId: 19c6ffec44c62c185fc6f79282c38e66468f7b80
This commit is contained in:
@@ -431,7 +431,6 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
ExecEndTarget::NewCell => {
|
||||
self.flush_active_cell();
|
||||
let mut cell = new_active_exec_command(
|
||||
id.clone(),
|
||||
command,
|
||||
@@ -442,12 +441,28 @@ impl ChatWidget {
|
||||
);
|
||||
let completed = cell.complete_call(&id, output, duration);
|
||||
debug_assert!(completed, "new exec cell should contain {id}");
|
||||
if cell.should_flush() {
|
||||
self.add_to_history(cell);
|
||||
} else {
|
||||
self.transcript.active_cell = Some(Box::new(cell));
|
||||
if let Some(active) = self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_mut()
|
||||
.and_then(|cell| cell.as_any_mut().downcast_mut::<ExecCell>())
|
||||
&& !active.is_active()
|
||||
&& active.is_exploring_cell()
|
||||
&& cell.is_exploring_cell()
|
||||
{
|
||||
// Replayed commands have completion events without matching starts.
|
||||
active.calls.extend(cell.calls);
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
} else {
|
||||
self.flush_active_cell();
|
||||
if cell.should_flush() {
|
||||
self.add_to_history(cell);
|
||||
} else {
|
||||
self.transcript.active_cell = Some(Box::new(cell));
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,7 +350,20 @@ impl ChatWidget {
|
||||
if !self.reasoning_summary_parts.is_empty() {
|
||||
let reasoning_parts = std::mem::take(&mut self.reasoning_summary_parts);
|
||||
let cell = history_cell::new_reasoning_summary_block(reasoning_parts, &self.config.cwd);
|
||||
self.add_boxed_history(cell);
|
||||
if let Some(exec) = self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_mut()
|
||||
.and_then(|cell| cell.as_any_mut().downcast_mut::<ExecCell>())
|
||||
&& exec.is_exploring_cell()
|
||||
&& !exec.is_active()
|
||||
{
|
||||
// Keep adjacent exploration grouped while retaining reasoning in transcript order.
|
||||
exec.reasoning.push((exec.calls.len(), cell));
|
||||
self.bump_active_cell_revision();
|
||||
} else {
|
||||
self.add_boxed_history(cell);
|
||||
}
|
||||
}
|
||||
self.reasoning_buffer.clear();
|
||||
// Keep the last useful summary through tools and later empty items.
|
||||
|
||||
@@ -141,12 +141,10 @@ async fn failed_exploration_keeps_overlapping_commands_active_until_all_finish()
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
end_exec(&mut chat, followup, "followup\n", "", /*exit_code*/ 0);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1);
|
||||
let history = lines_to_single_string(&cells[0]);
|
||||
insta::assert_snapshot!(history, @r"
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
insta::assert_snapshot!(active_blob(&chat), @r"
|
||||
• Explored
|
||||
└ List missing
|
||||
└ List missing (exit 1)
|
||||
Read foo.txt, bar.txt
|
||||
");
|
||||
|
||||
@@ -154,10 +152,153 @@ async fn failed_exploration_keeps_overlapping_commands_active_until_all_finish()
|
||||
end_exec(&mut chat, later, "later\n", "", /*exit_code*/ 0);
|
||||
insta::assert_snapshot!(active_blob(&chat), @r"
|
||||
• Explored
|
||||
└ Read later.txt
|
||||
└ List missing (exit 1)
|
||||
Read foo.txt, bar.txt, later.txt
|
||||
");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exploration_nonzero_exits_remain_visible_beside_successful_reads() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
for (id, command, exit_code) in [
|
||||
("read", "cat first.txt", 0),
|
||||
("missing", "cat missing.txt", 1),
|
||||
("later", "cat later.txt", 0),
|
||||
("search", "rg absent .", 1),
|
||||
("error", "rg text missing.txt", 2),
|
||||
("compound", "cat existing.txt && rg absent .", 1),
|
||||
] {
|
||||
let item = begin_exec(&mut chat, id, command);
|
||||
end_exec(&mut chat, item, "", "", exit_code);
|
||||
}
|
||||
insta::assert_snapshot!(active_blob(&chat));
|
||||
let statuses = chat
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.display_lines(/*width*/ 80)
|
||||
.into_iter()
|
||||
.flat_map(|line| line.spans)
|
||||
.filter(|span| {
|
||||
span.content.starts_with(" (exit") || span.content.starts_with(" (command exit")
|
||||
})
|
||||
.map(|span| (span.content.into_owned(), span.style.fg))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
statuses,
|
||||
vec![
|
||||
(" (exit 1)".to_string(), Some(ratatui::style::Color::Red)),
|
||||
(" (exit 1)".to_string(), None),
|
||||
(" (exit 2)".to_string(), Some(ratatui::style::Color::Red)),
|
||||
(" (command exit 1)".to_string(), None),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn adjacent_exploration_groups_across_reasoning_live_and_replayed() {
|
||||
let mut renders = Vec::new();
|
||||
for replay in [false, true] {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.on_task_started();
|
||||
for (id, script, exit_code) in [
|
||||
("list", "ls missing", 1),
|
||||
("read", "cat index.html", 0),
|
||||
("run", "echo boundary", 0),
|
||||
("later", "cat later.txt", 0),
|
||||
("after-message", "cat final.txt", 0),
|
||||
] {
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), script.to_string()];
|
||||
let mut item = AppServerThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
id: id.to_string(),
|
||||
command: codex_shell_command::parse_command::shlex_join(&command),
|
||||
cwd: chat.config.cwd.clone().into(),
|
||||
process_id: None,
|
||||
plugin_id: None,
|
||||
script_path: None,
|
||||
source: ExecCommandSource::UnifiedExecStartup,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
command_actions: codex_shell_command::parse_command::parse_command(&command)
|
||||
.into_iter()
|
||||
.map(|parsed| {
|
||||
AppServerCommandAction::from_core_with_cwd(parsed, &chat.config.cwd)
|
||||
})
|
||||
.collect(),
|
||||
aggregated_output: Some(format!("{id} output\n")),
|
||||
exit_code: Some(exit_code),
|
||||
duration_ms: Some(5),
|
||||
};
|
||||
if !replay {
|
||||
handle_exec_begin(&mut chat, item.clone());
|
||||
}
|
||||
if let AppServerThreadItem::CommandExecution { status, .. } = &mut item {
|
||||
*status = if exit_code == 0 {
|
||||
AppServerCommandExecutionStatus::Completed
|
||||
} else {
|
||||
AppServerCommandExecutionStatus::Failed
|
||||
};
|
||||
}
|
||||
if replay {
|
||||
chat.replay_thread_item(item, "turn-1".to_string(), ReplayKind::ThreadSnapshot);
|
||||
} else {
|
||||
handle_exec_end(&mut chat, item);
|
||||
}
|
||||
if id == "list" {
|
||||
for summary in ["Inspecting the page", "Checking its contents"] {
|
||||
if replay {
|
||||
chat.replay_thread_item(
|
||||
AppServerThreadItem::Reasoning {
|
||||
id: summary.to_string(),
|
||||
summary: vec![summary.to_string()],
|
||||
content: Vec::new(),
|
||||
},
|
||||
"turn-1".to_string(),
|
||||
ReplayKind::ThreadSnapshot,
|
||||
);
|
||||
} else {
|
||||
chat.on_agent_reasoning_delta(summary.to_string());
|
||||
chat.on_agent_reasoning_final();
|
||||
}
|
||||
}
|
||||
}
|
||||
if id == "later" {
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"message",
|
||||
"Checking one more file.",
|
||||
Some(MessagePhase::Commentary),
|
||||
);
|
||||
}
|
||||
}
|
||||
chat.flush_active_cell();
|
||||
let cells = std::iter::from_fn(|| rx.try_recv().ok())
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => Some(cell),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(cells.len(), 5);
|
||||
let mut render = String::new();
|
||||
for mode in ["Compact", "Expanded", "Raw"] {
|
||||
render.push_str(&format!("{mode}:\n"));
|
||||
for cell in &cells {
|
||||
let lines = match mode {
|
||||
"Compact" => cell.display_lines(/*width*/ 80),
|
||||
"Expanded" => cell.transcript_lines(/*width*/ 80),
|
||||
_ => cell.raw_lines(),
|
||||
};
|
||||
render.push_str(&lines_to_single_string(&lines));
|
||||
render.push('\n');
|
||||
}
|
||||
}
|
||||
renders.push(render);
|
||||
}
|
||||
assert_eq!(renders[0], renders[1]);
|
||||
insta::assert_snapshot!(renders[0]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn replayed_commands_preserve_individual_output_and_failure_status() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: "renders[0]"
|
||||
---
|
||||
Compact:
|
||||
• Explored
|
||||
└ List missing (exit 1)
|
||||
Read index.html
|
||||
|
||||
• Ran echo boundary
|
||||
└ run output
|
||||
|
||||
• Explored
|
||||
└ Read later.txt
|
||||
|
||||
• Checking one more file.
|
||||
|
||||
• Explored
|
||||
└ Read final.txt
|
||||
|
||||
Expanded:
|
||||
$ ls missing
|
||||
list output
|
||||
✗ (1) • 5ms
|
||||
|
||||
• Inspecting the page
|
||||
|
||||
• Checking its contents
|
||||
|
||||
$ cat index.html
|
||||
read output
|
||||
✓ • 5ms
|
||||
|
||||
$ echo boundary
|
||||
run output
|
||||
✓ • 5ms
|
||||
|
||||
$ cat later.txt
|
||||
later output
|
||||
✓ • 5ms
|
||||
|
||||
• Checking one more file.
|
||||
|
||||
$ cat final.txt
|
||||
after-message output
|
||||
✓ • 5ms
|
||||
|
||||
Raw:
|
||||
$ ls missing
|
||||
list output
|
||||
✗ (1) • 5ms
|
||||
|
||||
$ cat index.html
|
||||
read output
|
||||
✓ • 5ms
|
||||
|
||||
$ echo boundary
|
||||
run output
|
||||
✓ • 5ms
|
||||
|
||||
$ cat later.txt
|
||||
later output
|
||||
✓ • 5ms
|
||||
|
||||
Checking one more file.
|
||||
|
||||
$ cat final.txt
|
||||
after-message output
|
||||
✓ • 5ms
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: active_blob(&chat)
|
||||
---
|
||||
• Explored
|
||||
└ Read first.txt
|
||||
Read missing.txt (exit 1)
|
||||
Read later.txt
|
||||
Search absent in . (exit 1)
|
||||
Search text in missing.txt (exit 2)
|
||||
Read existing.txt
|
||||
Search absent in . (command exit 1)
|
||||
@@ -1,6 +1,7 @@
|
||||
mod live_output;
|
||||
mod model;
|
||||
mod render;
|
||||
mod transcript;
|
||||
|
||||
pub(crate) use model::CommandOutput;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
//! list/search commands. The chat widget relies on stable `call_id` matching to route progress and
|
||||
//! end events into the right cell, and it treats "call id not found" as a real signal (for
|
||||
//! example, an orphan end that should render as a separate history entry).
|
||||
//! Transcript-only reasoning stays inside completed exploration groups so it does not split their
|
||||
//! compact display, while the expanded transcript retains its position between commands.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::live_output::LiveCommandOutput;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use itertools::Either;
|
||||
@@ -75,6 +78,8 @@ pub(crate) struct ExecCall {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecCell {
|
||||
pub(crate) calls: Vec<ExecCall>,
|
||||
/// Transcript-only reasoning, paired with the number of calls preceding it.
|
||||
pub(crate) reasoning: Vec<(usize, Box<dyn HistoryCell>)>,
|
||||
animations_enabled: bool,
|
||||
}
|
||||
|
||||
@@ -82,6 +87,7 @@ impl ExecCell {
|
||||
pub(crate) fn new(call: ExecCall, animations_enabled: bool) -> Self {
|
||||
Self {
|
||||
calls: vec![call],
|
||||
reasoning: Vec::new(),
|
||||
animations_enabled,
|
||||
}
|
||||
}
|
||||
@@ -133,14 +139,7 @@ impl ExecCell {
|
||||
}
|
||||
|
||||
pub(crate) fn should_flush(&self) -> bool {
|
||||
if self.calls.iter().any(|call| {
|
||||
call.output
|
||||
.as_ref()
|
||||
.is_some_and(|output| output.exit_code != 0)
|
||||
}) {
|
||||
return !self.is_active();
|
||||
}
|
||||
|
||||
// Exploration stays open for adjacent calls, including after a failed read/list/search.
|
||||
!self.is_exploring_cell() && self.calls.iter().all(|c| c.duration.is_some())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use super::model::ExecCall;
|
||||
use super::model::ExecCell;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::HistoryRenderMode;
|
||||
use crate::history_cell::plain_lines;
|
||||
use crate::motion::MotionMode;
|
||||
use crate::motion::ReducedMotionIndicator;
|
||||
@@ -15,12 +16,10 @@ use crate::render::line_utils::push_owned_lines;
|
||||
use crate::ui_consts::TRANSCRIPT_HINT;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::adaptive_wrap_line;
|
||||
use crate::wrapping::adaptive_wrap_lines;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_shell_command::bash::extract_bash_command;
|
||||
use codex_utils_elapsed::format_duration;
|
||||
use itertools::Itertools;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Modifier;
|
||||
@@ -193,53 +192,11 @@ impl HistoryCell for ExecCell {
|
||||
}
|
||||
|
||||
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = vec![];
|
||||
for (i, call) in self.iter_calls().enumerate() {
|
||||
if i > 0 {
|
||||
lines.push("".into());
|
||||
}
|
||||
let script = strip_bash_lc_and_escape(&call.command);
|
||||
let highlighted_script = highlight_bash_to_lines(&script);
|
||||
let cmd_display = adaptive_wrap_lines(
|
||||
&highlighted_script,
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent("$ ".magenta().into())
|
||||
.subsequent_indent(" ".into()),
|
||||
);
|
||||
lines.extend(cmd_display);
|
||||
|
||||
if let Some(output) = call.output.as_ref() {
|
||||
if !call.is_unified_exec_interaction() {
|
||||
let wrap_width = width.max(1) as usize;
|
||||
let wrap_opts = RtOptions::new(wrap_width);
|
||||
for unwrapped in output
|
||||
.transcript_lines()
|
||||
.map(|line| ansi_escape_line(line.as_ref()))
|
||||
{
|
||||
let wrapped = adaptive_wrap_line(&unwrapped, wrap_opts.clone());
|
||||
push_owned_lines(&wrapped, &mut lines);
|
||||
}
|
||||
}
|
||||
if let Some(duration) = call.duration {
|
||||
let duration = format_duration(duration);
|
||||
let mut result: Line = if output.exit_code == 0 {
|
||||
Line::from("✓".green().bold())
|
||||
} else {
|
||||
Line::from(vec![
|
||||
"✗".red().bold(),
|
||||
format!(" ({})", output.exit_code).into(),
|
||||
])
|
||||
};
|
||||
result.push_span(format!(" • {duration}").dim());
|
||||
lines.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines
|
||||
self.detailed_lines(width, HistoryRenderMode::Rich)
|
||||
}
|
||||
|
||||
fn raw_lines(&self) -> Vec<Line<'static>> {
|
||||
plain_lines(self.transcript_lines(u16::MAX))
|
||||
plain_lines(self.detailed_lines(u16::MAX, HistoryRenderMode::Raw))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,18 +227,27 @@ impl ExecCell {
|
||||
|
||||
let mut calls = self.calls.as_slice();
|
||||
let mut out_indented = Vec::new();
|
||||
let nonzero_exit = |call: &ExecCall| {
|
||||
call.duration
|
||||
.and(call.output.as_ref())
|
||||
.map(|output| output.exit_code)
|
||||
.filter(|code| *code != 0)
|
||||
};
|
||||
while let Some((call, remaining)) = calls.split_first() {
|
||||
let exit_code = nonzero_exit(call);
|
||||
let reads_only = call
|
||||
.parsed
|
||||
.iter()
|
||||
.all(|parsed| matches!(parsed, ParsedCommand::Read { .. }));
|
||||
let group_len = if reads_only {
|
||||
let group_len = if reads_only && exit_code.is_none() {
|
||||
1 + remaining
|
||||
.iter()
|
||||
.take_while(|next| {
|
||||
next.parsed
|
||||
.iter()
|
||||
.all(|parsed| matches!(parsed, ParsedCommand::Read { .. }))
|
||||
nonzero_exit(next).is_none()
|
||||
&& next
|
||||
.parsed
|
||||
.iter()
|
||||
.all(|parsed| matches!(parsed, ParsedCommand::Read { .. }))
|
||||
})
|
||||
.count()
|
||||
} else {
|
||||
@@ -331,7 +297,31 @@ impl ExecCell {
|
||||
lines
|
||||
};
|
||||
|
||||
for (title, line) in call_lines {
|
||||
let line_count = call_lines.len();
|
||||
for (index, (title, mut line)) in call_lines.into_iter().enumerate() {
|
||||
if let Some(code) = exit_code
|
||||
&& index + 1 == line_count
|
||||
{
|
||||
// A compound command has one exit code, not an outcome for each parsed action.
|
||||
let status = if call.parsed.len() > 1 {
|
||||
format!(" (command exit {code})")
|
||||
} else {
|
||||
format!(" (exit {code})")
|
||||
};
|
||||
// Search exit 1 can mean no matches; report the code without calling it a failure.
|
||||
line.push(
|
||||
if code == 1
|
||||
&& call
|
||||
.parsed
|
||||
.iter()
|
||||
.any(|p| matches!(p, ParsedCommand::Search { .. }))
|
||||
{
|
||||
status.dim()
|
||||
} else {
|
||||
status.red()
|
||||
},
|
||||
);
|
||||
}
|
||||
let line = Line::from(line);
|
||||
let initial_indent = Line::from(vec![title.cyan(), " ".into()]);
|
||||
let subsequent_indent = " ".repeat(initial_indent.width()).into();
|
||||
|
||||
76
codex-rs/tui/src/exec_cell/transcript.rs
Normal file
76
codex-rs/tui/src/exec_cell/transcript.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! Expanded command history, preserving reasoning between grouped exploration calls.
|
||||
//!
|
||||
//! Compact history groups adjacent exploration; expanded history retains chronological details,
|
||||
//! and raw history continues to omit transcript-only reasoning.
|
||||
|
||||
use super::model::ExecCell;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::history_cell::HistoryRenderMode;
|
||||
use crate::render::highlight::highlight_bash_to_lines;
|
||||
use crate::render::line_utils::push_owned_lines;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::adaptive_wrap_line;
|
||||
use crate::wrapping::adaptive_wrap_lines;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_utils_elapsed::format_duration;
|
||||
use ratatui::prelude::*;
|
||||
|
||||
impl ExecCell {
|
||||
pub(super) fn detailed_lines(&self, width: u16, mode: HistoryRenderMode) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = vec![];
|
||||
let mut reasoning = self.reasoning.iter().peekable();
|
||||
for (i, call) in self.iter_calls().enumerate() {
|
||||
if i > 0 {
|
||||
lines.push("".into());
|
||||
}
|
||||
let script = strip_bash_lc_and_escape(&call.command);
|
||||
let highlighted_script = highlight_bash_to_lines(&script);
|
||||
let cmd_display = adaptive_wrap_lines(
|
||||
&highlighted_script,
|
||||
RtOptions::new(width as usize)
|
||||
.initial_indent("$ ".magenta().into())
|
||||
.subsequent_indent(" ".into()),
|
||||
);
|
||||
lines.extend(cmd_display);
|
||||
|
||||
if let Some(output) = call.output.as_ref() {
|
||||
if !call.is_unified_exec_interaction() {
|
||||
let wrap_width = width.max(1) as usize;
|
||||
let wrap_opts = RtOptions::new(wrap_width);
|
||||
for unwrapped in output
|
||||
.transcript_lines()
|
||||
.map(|line| ansi_escape_line(line.as_ref()))
|
||||
{
|
||||
let wrapped = adaptive_wrap_line(&unwrapped, wrap_opts.clone());
|
||||
push_owned_lines(&wrapped, &mut lines);
|
||||
}
|
||||
}
|
||||
if let Some(duration) = call.duration {
|
||||
let duration = format_duration(duration);
|
||||
let mut result: Line = if output.exit_code == 0 {
|
||||
Line::from("✓".green().bold())
|
||||
} else {
|
||||
Line::from(vec![
|
||||
"✗".red().bold(),
|
||||
format!(" ({})", output.exit_code).into(),
|
||||
])
|
||||
};
|
||||
result.push_span(format!(" • {duration}").dim());
|
||||
lines.push(result);
|
||||
}
|
||||
}
|
||||
if mode == HistoryRenderMode::Rich {
|
||||
while let Some((_, cell)) =
|
||||
reasoning.next_if(|(after_calls, _)| *after_calls == i + 1)
|
||||
{
|
||||
let reasoning_lines = cell.transcript_lines(width);
|
||||
if !reasoning_lines.is_empty() {
|
||||
lines.push("".into());
|
||||
lines.extend(reasoning_lines);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user