Stream active tool calls in transcript mode

This commit is contained in:
Daniel Edrisian
2025-10-07 19:48:55 -07:00
parent 10e0805d65
commit 43a8a1645e
4 changed files with 60 additions and 5 deletions

View File

@@ -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

View File

@@ -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 inprogress 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();
@@ -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);

View File

@@ -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 {
@@ -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.")
@@ -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()
@@ -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()
@@ -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;
}
self.flush_active_cell();
self.active_cell = Some(Box::new(history_cell::new_active_mcp_tool_call(
ev.call_id,
@@ -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()
@@ -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,
}
}
@@ -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,
}
}
@@ -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));

View File

@@ -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)
}