mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
feat: add output to /ps (#10154)
<img width="599" height="238" alt="Screenshot 2026-01-29 at 13 24 57" src="https://github.com/user-attachments/assets/1e9a5af2-f649-476c-b310-ae4938814538" />
This commit is contained in:
@@ -225,7 +225,9 @@ struct RunningCommand {
|
||||
|
||||
struct UnifiedExecProcessSummary {
|
||||
key: String,
|
||||
call_id: String,
|
||||
command_display: String,
|
||||
recent_chunks: Vec<String>,
|
||||
}
|
||||
|
||||
struct UnifiedExecWaitState {
|
||||
@@ -1426,6 +1428,8 @@ impl ChatWidget {
|
||||
}
|
||||
|
||||
fn on_exec_command_output_delta(&mut self, ev: ExecCommandOutputDeltaEvent) {
|
||||
self.track_unified_exec_output_chunk(&ev.call_id, &ev.chunk);
|
||||
|
||||
let Some(cell) = self
|
||||
.active_cell
|
||||
.as_mut()
|
||||
@@ -1545,11 +1549,15 @@ impl ChatWidget {
|
||||
.iter_mut()
|
||||
.find(|process| process.key == key)
|
||||
{
|
||||
existing.call_id = ev.call_id.clone();
|
||||
existing.command_display = command_display;
|
||||
existing.recent_chunks.clear();
|
||||
} else {
|
||||
self.unified_exec_processes.push(UnifiedExecProcessSummary {
|
||||
key,
|
||||
call_id: ev.call_id.clone(),
|
||||
command_display,
|
||||
recent_chunks: Vec::new(),
|
||||
});
|
||||
}
|
||||
self.sync_unified_exec_footer();
|
||||
@@ -1574,6 +1582,32 @@ impl ChatWidget {
|
||||
self.bottom_pane.set_unified_exec_processes(processes);
|
||||
}
|
||||
|
||||
/// Record recent stdout/stderr lines for the unified exec footer.
|
||||
fn track_unified_exec_output_chunk(&mut self, call_id: &str, chunk: &[u8]) {
|
||||
let Some(process) = self
|
||||
.unified_exec_processes
|
||||
.iter_mut()
|
||||
.find(|process| process.call_id == call_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let text = String::from_utf8_lossy(chunk);
|
||||
for line in text
|
||||
.lines()
|
||||
.map(str::trim_end)
|
||||
.filter(|line| !line.is_empty())
|
||||
{
|
||||
process.recent_chunks.push(line.to_string());
|
||||
}
|
||||
|
||||
const MAX_RECENT_CHUNKS: usize = 3;
|
||||
if process.recent_chunks.len() > MAX_RECENT_CHUNKS {
|
||||
let drop_count = process.recent_chunks.len() - MAX_RECENT_CHUNKS;
|
||||
process.recent_chunks.drain(0..drop_count);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_unified_exec_processes(&mut self) {
|
||||
if self.unified_exec_processes.is_empty() {
|
||||
return;
|
||||
@@ -3448,7 +3482,10 @@ impl ChatWidget {
|
||||
let processes = self
|
||||
.unified_exec_processes
|
||||
.iter()
|
||||
.map(|process| process.command_display.clone())
|
||||
.map(|process| history_cell::UnifiedExecProcessDetails {
|
||||
command_display: process.command_display.clone(),
|
||||
recent_chunks: process.recent_chunks.clone(),
|
||||
})
|
||||
.collect();
|
||||
self.add_to_history(history_cell::new_unified_exec_processes_output(processes));
|
||||
}
|
||||
|
||||
@@ -2025,7 +2025,9 @@ async fn unified_exec_wait_status_header_updates_on_late_command_display() {
|
||||
chat.on_task_started();
|
||||
chat.unified_exec_processes.push(UnifiedExecProcessSummary {
|
||||
key: "proc-1".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
command_display: "sleep 5".to_string(),
|
||||
recent_chunks: Vec::new(),
|
||||
});
|
||||
|
||||
chat.on_terminal_interaction(TerminalInteractionEvent {
|
||||
|
||||
@@ -558,15 +558,21 @@ pub(crate) fn new_unified_exec_interaction(
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UnifiedExecProcessesCell {
|
||||
processes: Vec<String>,
|
||||
processes: Vec<UnifiedExecProcessDetails>,
|
||||
}
|
||||
|
||||
impl UnifiedExecProcessesCell {
|
||||
fn new(processes: Vec<String>) -> Self {
|
||||
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 {
|
||||
@@ -589,10 +595,11 @@ impl HistoryCell for UnifiedExecProcessesCell {
|
||||
let truncation_suffix = " [...]";
|
||||
let truncation_suffix_width = UnicodeWidthStr::width(truncation_suffix);
|
||||
let mut shown = 0usize;
|
||||
for command in &self.processes {
|
||||
for process in &self.processes {
|
||||
if shown >= max_processes {
|
||||
break;
|
||||
}
|
||||
let command = &process.command_display;
|
||||
let (snippet, snippet_truncated) = {
|
||||
let (first_line, has_more_lines) = match command.split_once('\n') {
|
||||
Some((first, _)) => (first, true),
|
||||
@@ -627,6 +634,32 @@ impl HistoryCell for UnifiedExecProcessesCell {
|
||||
let (truncated, _, _) = take_prefix_by_width(&snippet, budget);
|
||||
out.push(vec![prefix.dim(), truncated.cyan()].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 = UnicodeWidthStr::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;
|
||||
}
|
||||
|
||||
@@ -650,7 +683,9 @@ impl HistoryCell for UnifiedExecProcessesCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_unified_exec_processes_output(processes: Vec<String>) -> CompositeHistoryCell {
|
||||
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)])
|
||||
@@ -2022,8 +2057,14 @@ mod tests {
|
||||
#[test]
|
||||
fn ps_output_multiline_snapshot() {
|
||||
let cell = new_unified_exec_processes_output(vec![
|
||||
"echo hello\nand then some extra text".to_string(),
|
||||
"rg \"foo\" src".to_string(),
|
||||
UnifiedExecProcessDetails {
|
||||
command_display: "echo hello\nand then some extra text".to_string(),
|
||||
recent_chunks: vec!["hello".to_string(), "done".to_string()],
|
||||
},
|
||||
UnifiedExecProcessDetails {
|
||||
command_display: "rg \"foo\" src".to_string(),
|
||||
recent_chunks: vec!["src/main.rs:12:foo".to_string()],
|
||||
},
|
||||
]);
|
||||
let rendered = render_lines(&cell.display_lines(40)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
@@ -2031,9 +2072,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ps_output_long_command_snapshot() {
|
||||
let cell = new_unified_exec_processes_output(vec![String::from(
|
||||
"rg \"foo\" src --glob '**/*.rs' --max-count 1000 --no-ignore --hidden --follow --glob '!target/**'",
|
||||
)]);
|
||||
let cell = new_unified_exec_processes_output(vec![UnifiedExecProcessDetails {
|
||||
command_display: String::from(
|
||||
"rg \"foo\" src --glob '**/*.rs' --max-count 1000 --no-ignore --hidden --follow --glob '!target/**'",
|
||||
),
|
||||
recent_chunks: vec!["searching...".to_string()],
|
||||
}]);
|
||||
let rendered = render_lines(&cell.display_lines(36)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
@@ -2041,12 +2085,30 @@ mod tests {
|
||||
#[test]
|
||||
fn ps_output_many_sessions_snapshot() {
|
||||
let cell = new_unified_exec_processes_output(
|
||||
(0..20).map(|idx| format!("command {idx}")).collect(),
|
||||
(0..20)
|
||||
.map(|idx| UnifiedExecProcessDetails {
|
||||
command_display: format!("command {idx}"),
|
||||
recent_chunks: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let rendered = render_lines(&cell.display_lines(32)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ps_output_chunk_leading_whitespace_snapshot() {
|
||||
let cell = new_unified_exec_processes_output(vec![UnifiedExecProcessDetails {
|
||||
command_display: "just fix".to_string(),
|
||||
recent_chunks: vec![
|
||||
" indented first".to_string(),
|
||||
" more indented".to_string(),
|
||||
],
|
||||
}]);
|
||||
let rendered = render_lines(&cell.display_lines(60)).join("\n");
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mcp_tools_output_masks_sensitive_values() {
|
||||
let mut config = test_config().await;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
expression: rendered
|
||||
---
|
||||
/ps
|
||||
|
||||
Background terminals
|
||||
|
||||
• just fix
|
||||
↳ indented first
|
||||
more indented
|
||||
@@ -7,3 +7,4 @@ expression: rendered
|
||||
Background terminals
|
||||
|
||||
• rg "foo" src --glob '**/*. [...]
|
||||
↳ searching...
|
||||
|
||||
@@ -7,4 +7,7 @@ expression: rendered
|
||||
Background terminals
|
||||
|
||||
• echo hello [...]
|
||||
↳ hello
|
||||
done
|
||||
• rg "foo" src
|
||||
↳ src/main.rs:12:foo
|
||||
|
||||
Reference in New Issue
Block a user