mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
## Why `codex-rs/tui/src/history_cell.rs` had become the dumping ground for transcript rendering: the shared trait, common helpers, and the concrete cells for messages, plans, MCP/search, notices, patches, approvals, session chrome, and separators all lived together. That made small transcript changes require reopening a very large file and made ownership less obvious. ## What changed - Replaced the monolithic `history_cell.rs` with a `history_cell/` module tree organized by concern. - Kept the existing `crate::history_cell::*` surface stable through re-exports in `history_cell/mod.rs`. - Moved the existing render coverage into `history_cell/tests.rs`. ## Reviewer notes - This PR is intentionally mechanical in mature — existing code and tests moving into files that match their concern. - The snapshot files under `codex-rs/tui/src/history_cell/snapshots/` moved with the extracted test module. `insta` resolves these unnamed snapshots relative to the source file that declares them, so this is path churn only; snapshot contents were not updated. - The small non-mechanical seam edits are limited to split fallout: sibling-module visibility for shared cell containers, moving approval-specific exec-snippet helpers beside approvals, fixing the separator module path, and keeping a couple of existing test helpers reachable after extraction.
243 lines
8.3 KiB
Rust
243 lines
8.3 KiB
Rust
//! Background terminal interaction and process-summary history cells.
|
|
|
|
use super::*;
|
|
|
|
#[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 display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
|
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: Vec<Line<'static>> = Vec::new();
|
|
let header_wrapped = adaptive_wrap_line(&header, RtOptions::new(wrap_width));
|
|
push_owned_lines(&header_wrapped, &mut out);
|
|
|
|
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_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 = UnicodeWidthStr::width(prefix);
|
|
let truncation_suffix = " [...]";
|
|
let truncation_suffix_width = UnicodeWidthStr::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 (first_line, has_more_lines) = match command.split_once('\n') {
|
|
Some((first, _)) => (first, true),
|
|
None => (command.as_str(), false),
|
|
};
|
|
let max_graphemes = 80;
|
|
let mut graphemes = first_line.grapheme_indices(true);
|
|
if let Some((byte_index, _)) = graphemes.nth(max_graphemes) {
|
|
(first_line[..byte_index].to_string(), true)
|
|
} else {
|
|
(first_line.to_string(), has_more_lines)
|
|
}
|
|
};
|
|
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.cyan(), truncation_suffix.dim()].into());
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
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)])
|
|
}
|