mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
## What changed - Add summary labels for MCP and dynamic tools, computer activity, terminal interactions, file edits, images, plan updates, agent calls, and historical tool fallbacks. - Keep tool output out of summaries and give summaries disclosure identities even when their cells have no activity IDs. - Bring focused disclosure headers into view and show `expand` or `collapse` in the footer, including for search-expanded entries. ## Testing Add assertions for tool labels, a transcript snapshot verifying that image-generation output stays hidden, and coverage for expanded and collapsed footer hints. GitOrigin-RevId: 076b8d74c17f26b10aa8221c65b8b38dbb5ad5ea
275 lines
9.2 KiB
Rust
275 lines
9.2 KiB
Rust
//! Background terminal interaction and process-summary history cells.
|
|
//!
|
|
//! Polling and stdin bookkeeping stay in detailed and raw history; the normal chat view
|
|
//! relies on command output and the background-process status instead of repeating notices.
|
|
|
|
use super::*;
|
|
use crate::style::accent_color;
|
|
use crate::terminal_hyperlinks::adaptive_wrap_hyperlink_lines;
|
|
use crate::width::display_width;
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct UnifiedExecInteractionCell {
|
|
command_display: Option<String>,
|
|
stdin: String,
|
|
}
|
|
|
|
impl UnifiedExecInteractionCell {
|
|
pub(crate) fn new(command_display: Option<String>, stdin: String) -> Self {
|
|
Self {
|
|
command_display,
|
|
stdin,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HistoryCell for UnifiedExecInteractionCell {
|
|
fn tool_call_labels(&self) -> Vec<String> {
|
|
let action = if self.stdin.is_empty() {
|
|
"Waited for terminal"
|
|
} else {
|
|
"Sent terminal input"
|
|
};
|
|
vec![
|
|
self.command_display
|
|
.as_ref()
|
|
.map_or_else(|| action.to_string(), |cmd| format!("{action} · {cmd}")),
|
|
]
|
|
}
|
|
|
|
fn display_lines(&self, _width: u16) -> Vec<Line<'static>> {
|
|
Vec::new()
|
|
}
|
|
|
|
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
visible_lines(self.transcript_hyperlink_lines(width))
|
|
}
|
|
|
|
fn transcript_hyperlink_lines(&self, width: u16) -> Vec<HyperlinkLine> {
|
|
if width == 0 {
|
|
return Vec::new();
|
|
}
|
|
let wrap_width = width as usize;
|
|
let waited_only = self.stdin.is_empty();
|
|
|
|
let mut header_spans = if waited_only {
|
|
vec!["• Waited for background terminal".bold()]
|
|
} else {
|
|
vec!["↳ ".dim(), "Interacted with background terminal".bold()]
|
|
};
|
|
if let Some(command) = &self.command_display
|
|
&& !command.is_empty()
|
|
{
|
|
header_spans.push(" · ".dim());
|
|
header_spans.push(command.clone().dim());
|
|
}
|
|
let header = Line::from(header_spans);
|
|
|
|
let mut out = adaptive_wrap_hyperlink_lines(&[header.into()], RtOptions::new(wrap_width));
|
|
|
|
if waited_only {
|
|
return out;
|
|
}
|
|
|
|
let input_lines: Vec<Line<'static>> = self
|
|
.stdin
|
|
.lines()
|
|
.map(|line| Line::from(line.to_string()))
|
|
.collect();
|
|
|
|
let input_wrapped = adaptive_wrap_hyperlink_lines(
|
|
&plain_hyperlink_lines(input_lines),
|
|
RtOptions::new(wrap_width)
|
|
.initial_indent(Line::from(" └ ".dim()))
|
|
.subsequent_indent(Line::from(" ".dim())),
|
|
);
|
|
out.extend(input_wrapped);
|
|
out
|
|
}
|
|
|
|
fn raw_lines(&self) -> Vec<Line<'static>> {
|
|
let mut out = Vec::new();
|
|
if self.stdin.is_empty() {
|
|
if let Some(command) = self
|
|
.command_display
|
|
.as_ref()
|
|
.filter(|command| !command.is_empty())
|
|
{
|
|
out.push(Line::from(format!(
|
|
"Waited for background terminal: {command}"
|
|
)));
|
|
} else {
|
|
out.push(Line::from("Waited for background terminal"));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if let Some(command) = self
|
|
.command_display
|
|
.as_ref()
|
|
.filter(|command| !command.is_empty())
|
|
{
|
|
out.push(Line::from(format!(
|
|
"Interacted with background terminal: {command}"
|
|
)));
|
|
} else {
|
|
out.push(Line::from("Interacted with background terminal"));
|
|
}
|
|
out.extend(raw_lines_from_source(&self.stdin));
|
|
out
|
|
}
|
|
}
|
|
|
|
pub(crate) fn new_unified_exec_interaction(
|
|
command_display: Option<String>,
|
|
stdin: String,
|
|
) -> UnifiedExecInteractionCell {
|
|
UnifiedExecInteractionCell::new(command_display, stdin)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct UnifiedExecProcessesCell {
|
|
processes: Vec<UnifiedExecProcessDetails>,
|
|
}
|
|
|
|
impl UnifiedExecProcessesCell {
|
|
fn new(processes: Vec<UnifiedExecProcessDetails>) -> Self {
|
|
Self { processes }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct UnifiedExecProcessDetails {
|
|
pub(crate) command_display: String,
|
|
pub(crate) recent_chunks: Vec<String>,
|
|
}
|
|
|
|
impl HistoryCell for UnifiedExecProcessesCell {
|
|
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
if width == 0 {
|
|
return Vec::new();
|
|
}
|
|
|
|
let wrap_width = width as usize;
|
|
let max_processes = 16usize;
|
|
let mut out: Vec<Line<'static>> = Vec::new();
|
|
out.push(vec!["Background terminals".bold()].into());
|
|
out.push("".into());
|
|
|
|
if self.processes.is_empty() {
|
|
out.push(" • No background terminals running.".italic().into());
|
|
return out;
|
|
}
|
|
|
|
let prefix = " • ";
|
|
let prefix_width = display_width(prefix);
|
|
let truncation_suffix = " [...]";
|
|
let truncation_suffix_width = display_width(truncation_suffix);
|
|
let mut shown = 0usize;
|
|
for process in &self.processes {
|
|
if shown >= max_processes {
|
|
break;
|
|
}
|
|
let command = &process.command_display;
|
|
let (snippet, snippet_truncated) = {
|
|
let max_graphemes = 80;
|
|
let mut graphemes = command.graphemes(true);
|
|
let snippet: String = graphemes
|
|
.by_ref()
|
|
.take(max_graphemes)
|
|
.map(|grapheme| match grapheme {
|
|
"\n" | "\r\n" => "↵",
|
|
_ => grapheme,
|
|
})
|
|
.collect();
|
|
(snippet, graphemes.next().is_some())
|
|
};
|
|
if wrap_width <= prefix_width {
|
|
out.push(Line::from(prefix.dim()));
|
|
shown += 1;
|
|
continue;
|
|
}
|
|
let budget = wrap_width.saturating_sub(prefix_width);
|
|
let mut needs_suffix = snippet_truncated;
|
|
if !needs_suffix {
|
|
let (_, remainder, _) = take_prefix_by_width(&snippet, budget);
|
|
if !remainder.is_empty() {
|
|
needs_suffix = true;
|
|
}
|
|
}
|
|
if needs_suffix && budget > truncation_suffix_width {
|
|
let available = budget.saturating_sub(truncation_suffix_width);
|
|
let (truncated, _, _) = take_prefix_by_width(&snippet, available);
|
|
out.push(
|
|
vec![
|
|
prefix.dim(),
|
|
truncated.fg(accent_color()),
|
|
truncation_suffix.dim(),
|
|
]
|
|
.into(),
|
|
);
|
|
} else {
|
|
let (truncated, _, _) = take_prefix_by_width(&snippet, budget);
|
|
out.push(vec![prefix.dim(), truncated.fg(accent_color())].into());
|
|
}
|
|
|
|
let chunk_prefix_first = " ↳ ";
|
|
let chunk_prefix_next = " ";
|
|
for (idx, chunk) in process.recent_chunks.iter().enumerate() {
|
|
let chunk_prefix = if idx == 0 {
|
|
chunk_prefix_first
|
|
} else {
|
|
chunk_prefix_next
|
|
};
|
|
let chunk_prefix_width = display_width(chunk_prefix);
|
|
if wrap_width <= chunk_prefix_width {
|
|
out.push(Line::from(chunk_prefix.dim()));
|
|
continue;
|
|
}
|
|
let budget = wrap_width.saturating_sub(chunk_prefix_width);
|
|
let (truncated, remainder, _) = take_prefix_by_width(chunk, budget);
|
|
if !remainder.is_empty() && budget > truncation_suffix_width {
|
|
let available = budget.saturating_sub(truncation_suffix_width);
|
|
let (shorter, _, _) = take_prefix_by_width(chunk, available);
|
|
out.push(
|
|
vec![chunk_prefix.dim(), shorter.dim(), truncation_suffix.dim()].into(),
|
|
);
|
|
} else {
|
|
out.push(vec![chunk_prefix.dim(), truncated.dim()].into());
|
|
}
|
|
}
|
|
shown += 1;
|
|
}
|
|
|
|
let remaining = self.processes.len().saturating_sub(shown);
|
|
if remaining > 0 {
|
|
let more_text = format!("... and {remaining} more running");
|
|
if wrap_width <= prefix_width {
|
|
out.push(Line::from(prefix.dim()));
|
|
} else {
|
|
let budget = wrap_width.saturating_sub(prefix_width);
|
|
let (truncated, _, _) = take_prefix_by_width(&more_text, budget);
|
|
out.push(vec![prefix.dim(), truncated.dim()].into());
|
|
}
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn raw_lines(&self) -> Vec<Line<'static>> {
|
|
plain_lines(self.display_lines(u16::MAX))
|
|
}
|
|
|
|
fn desired_height(&self, width: u16) -> u16 {
|
|
self.display_lines(width).len() as u16
|
|
}
|
|
}
|
|
|
|
pub(crate) fn new_unified_exec_processes_output(
|
|
processes: Vec<UnifiedExecProcessDetails>,
|
|
) -> CompositeHistoryCell {
|
|
let command = PlainHistoryCell::new(vec!["/ps".magenta().into()]);
|
|
let summary = UnifiedExecProcessesCell::new(processes);
|
|
CompositeHistoryCell::new(vec![Box::new(command), Box::new(summary)])
|
|
}
|