Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion codex-rs/ansi-escape/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,30 @@ use ansi_to_tui::IntoText;
use ratatui::text::Line;
use ratatui::text::Text;

// Expand tabs in a best-effort way for transcript rendering.
// Tabs can interact poorly with left-gutter prefixes in our TUI and CLI
// transcript views (e.g., `nl` separates line numbers from content with a tab).
// Replacing tabs with spaces avoids odd visual artifacts without changing
// semantics for our use cases.
fn expand_tabs(s: &str) -> std::borrow::Cow<'_, str> {
if s.contains('\t') {
// Keep it simple: replace each tab with 4 spaces.
// We do not try to align to tab stops since most usages (like `nl`)
// look acceptable with a fixed substitution and this avoids stateful math
// across spans.
std::borrow::Cow::Owned(s.replace('\t', " "))
} else {
std::borrow::Cow::Borrowed(s)
}
}

/// This function should be used when the contents of `s` are expected to match
/// a single line. If multiple lines are found, a warning is logged and only the
/// first line is returned.
pub fn ansi_escape_line(s: &str) -> Line<'static> {
let text = ansi_escape(s);
// Normalize tabs to spaces to avoid odd gutter collisions in transcript mode.
let s = expand_tabs(s);
let text = ansi_escape(&s);
match text.lines.as_slice() {
[] => "".into(),
[only] => only.clone(),
Expand Down
6 changes: 2 additions & 4 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,8 @@ impl App {
kind: KeyEventKind::Press,
..
} => {
// Enter alternate screen and set viewport to full size.
let _ = tui.enter_alt_screen();
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
tui.frame_requester().schedule_frame();
// Open transcript overlay: flush explore stack, enter alt screen, and enable transcript mode.
self.open_transcript_overlay(tui);
}
// Esc primes/advances backtracking only in normal (not working) mode
// with an empty composer. In any other state, forward Esc so the
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/app_backtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ impl App {

/// Open transcript overlay (enters alternate screen and shows full transcript).
pub(crate) fn open_transcript_overlay(&mut self, tui: &mut tui::Tui) {
// Flush any in‑progress explore stack so it lands in history first.
self.chat_widget.flush_active_cell();
self.chat_widget.set_transcript_mode(true);
let _ = tui.enter_alt_screen();
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
tui.frame_requester().schedule_frame();
Expand All @@ -117,6 +120,8 @@ impl App {
/// Close transcript overlay and restore normal UI.
pub(crate) fn close_transcript_overlay(&mut self, tui: &mut tui::Tui) {
let _ = tui.leave_alt_screen();
// Leave transcript mode so future tool calls return to the explore stack.
self.chat_widget.set_transcript_mode(false);
let was_backtrack = self.backtrack.overlay_preview_active;
if !self.deferred_history_lines.is_empty() {
let lines = std::mem::take(&mut self.deferred_history_lines);
Expand Down
53 changes: 52 additions & 1 deletion codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ pub(crate) struct ChatWidget {
needs_final_message_separator: bool,

last_rendered_width: std::cell::Cell<Option<usize>>,

// When true, route tool calls directly into history (transcript mode).
transcript_mode_active: bool,
}

struct UserMessage {
Expand All @@ -285,6 +288,12 @@ fn create_initial_user_message(text: String, image_paths: Vec<PathBuf>) -> Optio
}

impl ChatWidget {
/// Enable or disable transcript mode. When enabled, tool calls are
/// recorded directly into history instead of the active explore stack.
pub(crate) fn set_transcript_mode(&mut self, active: bool) {
self.transcript_mode_active = active;
}

fn model_description_for(slug: &str) -> Option<&'static str> {
if slug.starts_with("gpt-5-codex") {
Some("Optimized for coding tasks with many tools.")
Expand Down Expand Up @@ -706,6 +715,24 @@ impl ChatWidget {
None => (vec![ev.call_id.clone()], Vec::new()),
};

// In transcript mode, avoid the active explore stack and emit a
// completed exec cell directly to history.
if self.transcript_mode_active {
let mut cell = new_active_exec_command(ev.call_id.clone(), command, parsed);
cell.complete_call(
&ev.call_id,
CommandOutput {
exit_code: ev.exit_code,
stdout: ev.stdout.clone(),
stderr: ev.stderr.clone(),
formatted_output: ev.formatted_output.clone(),
},
ev.duration,
);
self.add_boxed_history(Box::new(cell));
return;
}

let needs_new = self
.active_cell
.as_ref()
Expand Down Expand Up @@ -797,6 +824,11 @@ impl ChatWidget {
parsed_cmd: ev.parsed_cmd.clone(),
},
);
// In transcript mode, skip creating/updating the active explore stack.
if self.transcript_mode_active {
self.request_redraw();
return;
}
if let Some(cell) = self
.active_cell
.as_mut()
Expand All @@ -823,6 +855,12 @@ impl ChatWidget {

pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
self.flush_answer_stream_with_separator();
if self.transcript_mode_active {
// In transcript mode, don't create an active cell; history will be
// emitted on the corresponding end event.
self.request_redraw();
return;
}
Comment on lines 823 to +863
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Running tool calls disappear after leaving transcript mode

While transcript_mode_active is true, handle_exec_begin_now (and the analogous MCP handler) returns before creating an active_cell. When the user closes the transcript overlay, close_transcript_overlay only flips the flag back to false; it never reconstructs an active cell for any command that began during transcript mode. As a result, if a tool call starts while the transcript overlay is open and the user exits the overlay before the call finishes, the main view has no active cell showing that the tool is still running (and thus no way to monitor or cancel it) until the end event finally inserts a completed history cell. Consider either continuing to track those calls in an active cell or recreating one when transcript mode is disabled so running work remains visible in the normal UI.

Useful? React with 👍 / 👎.

self.flush_active_cell();
self.active_cell = Some(Box::new(history_cell::new_active_mcp_tool_call(
ev.call_id,
Expand All @@ -840,6 +878,17 @@ impl ChatWidget {
result,
} = ev;

// In transcript mode, bypass the active stack and emit a completed tool call.
if self.transcript_mode_active {
let mut cell = history_cell::new_active_mcp_tool_call(call_id, invocation);
let extra = cell.complete(duration, result);
self.add_boxed_history(Box::new(cell));
if let Some(extra_cell) = extra {
self.add_boxed_history(extra_cell);
}
return;
}

let extra_cell = match self
.active_cell
.as_mut()
Expand Down Expand Up @@ -938,6 +987,7 @@ impl ChatWidget {
ghost_snapshots_disabled: true,
needs_final_message_separator: false,
last_rendered_width: std::cell::Cell::new(None),
transcript_mode_active: false,
}
}

Expand Down Expand Up @@ -1001,6 +1051,7 @@ impl ChatWidget {
ghost_snapshots_disabled: true,
needs_final_message_separator: false,
last_rendered_width: std::cell::Cell::new(None),
transcript_mode_active: false,
}
}

Expand Down Expand Up @@ -1225,7 +1276,7 @@ impl ChatWidget {
}
}

fn flush_active_cell(&mut self) {
pub(crate) fn flush_active_cell(&mut self) {
if let Some(active) = self.active_cell.take() {
self.needs_final_message_separator = true;
self.app_event_tx.send(AppEvent::InsertHistoryCell(active));
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ fn make_chatwidget_manual() -> (
ghost_snapshots_disabled: false,
needs_final_message_separator: false,
last_rendered_width: std::cell::Cell::new(None),
transcript_mode_active: false,
};
(widget, rx, op_rx)
}
Expand Down
Loading