show thinking in transcript (#2538)

record the full reasoning trace and show it in transcript mode
This commit is contained in:
Jeremy Rose
2025-08-20 17:09:46 -07:00
committed by GitHub
parent e95cad1946
commit 9193eb6b53
8 changed files with 145 additions and 46 deletions

View File

@@ -140,10 +140,17 @@ impl App {
fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<bool> { fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<bool> {
match event { match event {
AppEvent::InsertHistory(lines) => { AppEvent::InsertHistoryLines(lines) => {
self.transcript_lines.extend(lines.clone()); self.transcript_lines.extend(lines.clone());
tui.insert_history_lines(lines); tui.insert_history_lines(lines);
} }
AppEvent::InsertHistoryCell(cell) => {
self.transcript_lines.extend(cell.transcript_lines());
let display = cell.display_lines();
if !display.is_empty() {
tui.insert_history_lines(display);
}
}
AppEvent::StartCommitAnimation => { AppEvent::StartCommitAnimation => {
if self if self
.commit_anim_running .commit_anim_running

View File

@@ -2,6 +2,8 @@ use codex_core::protocol::Event;
use codex_file_search::FileMatch; use codex_file_search::FileMatch;
use ratatui::text::Line; use ratatui::text::Line;
use crate::history_cell::HistoryCell;
use crate::slash_command::SlashCommand; use crate::slash_command::SlashCommand;
use codex_core::protocol::AskForApproval; use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SandboxPolicy;
@@ -39,7 +41,8 @@ pub(crate) enum AppEvent {
/// Result of computing a `/diff` command. /// Result of computing a `/diff` command.
DiffResult(String), DiffResult(String),
InsertHistory(Vec<Line<'static>>), InsertHistoryLines(Vec<Line<'static>>),
InsertHistoryCell(Box<dyn HistoryCell>),
StartCommitAnimation, StartCommitAnimation,
StopCommitAnimation, StopCommitAnimation,

View File

@@ -98,6 +98,8 @@ pub(crate) struct ChatWidget {
needs_redraw: bool, needs_redraw: bool,
// Accumulates the current reasoning block text to extract a header // Accumulates the current reasoning block text to extract a header
reasoning_buffer: String, reasoning_buffer: String,
// Accumulates full reasoning content for transcript-only recording
full_reasoning_buffer: String,
session_id: Option<Uuid>, session_id: Option<Uuid>,
frame_requester: FrameRequester, frame_requester: FrameRequester,
} }
@@ -138,7 +140,7 @@ impl ChatWidget {
self.bottom_pane self.bottom_pane
.set_history_metadata(event.history_log_id, event.history_entry_count); .set_history_metadata(event.history_log_id, event.history_entry_count);
self.session_id = Some(event.session_id); self.session_id = Some(event.session_id);
self.add_to_history(&history_cell::new_session_info(&self.config, event, true)); self.add_to_history(history_cell::new_session_info(&self.config, event, true));
if let Some(user_message) = self.initial_user_message.take() { if let Some(user_message) = self.initial_user_message.take() {
self.submit_user_message(user_message); self.submit_user_message(user_message);
} }
@@ -172,13 +174,23 @@ impl ChatWidget {
} }
fn on_agent_reasoning_final(&mut self) { fn on_agent_reasoning_final(&mut self) {
// Clear the reasoning buffer at the end of a reasoning block. // At the end of a reasoning block, record transcript-only content.
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
if !self.full_reasoning_buffer.is_empty() {
self.add_to_history(history_cell::new_reasoning_block(
self.full_reasoning_buffer.clone(),
&self.config,
));
}
self.reasoning_buffer.clear(); self.reasoning_buffer.clear();
self.full_reasoning_buffer.clear();
self.mark_needs_redraw(); self.mark_needs_redraw();
} }
fn on_reasoning_section_break(&mut self) { fn on_reasoning_section_break(&mut self) {
// Start a new reasoning block for header extraction. // Start a new reasoning block for header extraction and accumulate transcript.
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
self.full_reasoning_buffer.push_str("\n\n");
self.reasoning_buffer.clear(); self.reasoning_buffer.clear();
} }
@@ -188,6 +200,7 @@ impl ChatWidget {
self.bottom_pane.clear_ctrl_c_quit_hint(); self.bottom_pane.clear_ctrl_c_quit_hint();
self.bottom_pane.set_task_running(true); self.bottom_pane.set_task_running(true);
self.stream.reset_headers_for_new_turn(); self.stream.reset_headers_for_new_turn();
self.full_reasoning_buffer.clear();
self.reasoning_buffer.clear(); self.reasoning_buffer.clear();
self.mark_needs_redraw(); self.mark_needs_redraw();
} }
@@ -216,7 +229,7 @@ impl ChatWidget {
} }
fn on_error(&mut self, message: String) { fn on_error(&mut self, message: String) {
self.add_to_history(&history_cell::new_error_event(message)); self.add_to_history(history_cell::new_error_event(message));
self.bottom_pane.set_task_running(false); self.bottom_pane.set_task_running(false);
self.running_commands.clear(); self.running_commands.clear();
self.stream.clear_all(); self.stream.clear_all();
@@ -224,7 +237,7 @@ impl ChatWidget {
} }
fn on_plan_update(&mut self, update: codex_core::plan_tool::UpdatePlanArgs) { fn on_plan_update(&mut self, update: codex_core::plan_tool::UpdatePlanArgs) {
self.add_to_history(&history_cell::new_plan_update(update)); self.add_to_history(history_cell::new_plan_update(update));
} }
fn on_exec_approval_request(&mut self, id: String, ev: ExecApprovalRequestEvent) { fn on_exec_approval_request(&mut self, id: String, ev: ExecApprovalRequestEvent) {
@@ -259,7 +272,7 @@ impl ChatWidget {
} }
fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) { fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) {
self.add_to_history(&history_cell::new_patch_event( self.add_to_history(history_cell::new_patch_event(
PatchEventType::ApplyBegin { PatchEventType::ApplyBegin {
auto_approved: event.auto_approved, auto_approved: event.auto_approved,
}, },
@@ -386,7 +399,7 @@ impl ChatWidget {
self.active_exec_cell = None; self.active_exec_cell = None;
let pending = std::mem::take(&mut self.pending_exec_completions); let pending = std::mem::take(&mut self.pending_exec_completions);
for (command, parsed, output) in pending { for (command, parsed, output) in pending {
self.add_to_history(&history_cell::new_completed_exec_command( self.add_to_history(history_cell::new_completed_exec_command(
command, parsed, output, command, parsed, output,
)); ));
} }
@@ -398,9 +411,9 @@ impl ChatWidget {
event: codex_core::protocol::PatchApplyEndEvent, event: codex_core::protocol::PatchApplyEndEvent,
) { ) {
if event.success { if event.success {
self.add_to_history(&history_cell::new_patch_apply_success(event.stdout)); self.add_to_history(history_cell::new_patch_apply_success(event.stdout));
} else { } else {
self.add_to_history(&history_cell::new_patch_apply_failure(event.stderr)); self.add_to_history(history_cell::new_patch_apply_failure(event.stderr));
} }
} }
@@ -422,7 +435,7 @@ impl ChatWidget {
ev: ApplyPatchApprovalRequestEvent, ev: ApplyPatchApprovalRequestEvent,
) { ) {
self.flush_answer_stream_with_separator(); self.flush_answer_stream_with_separator();
self.add_to_history(&history_cell::new_patch_event( self.add_to_history(history_cell::new_patch_event(
PatchEventType::ApprovalRequest, PatchEventType::ApprovalRequest,
ev.changes.clone(), ev.changes.clone(),
)); ));
@@ -464,11 +477,11 @@ impl ChatWidget {
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) { pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
self.flush_answer_stream_with_separator(); self.flush_answer_stream_with_separator();
self.add_to_history(&history_cell::new_active_mcp_tool_call(ev.invocation)); self.add_to_history(history_cell::new_active_mcp_tool_call(ev.invocation));
} }
pub(crate) fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) { pub(crate) fn handle_mcp_end_now(&mut self, ev: McpToolCallEndEvent) {
self.flush_answer_stream_with_separator(); self.flush_answer_stream_with_separator();
self.add_to_history(&*history_cell::new_completed_mcp_tool_call( self.add_boxed_history(history_cell::new_completed_mcp_tool_call(
80, 80,
ev.invocation, ev.invocation,
ev.duration, ev.duration,
@@ -541,6 +554,7 @@ impl ChatWidget {
interrupts: InterruptManager::new(), interrupts: InterruptManager::new(),
needs_redraw: false, needs_redraw: false,
reasoning_buffer: String::new(), reasoning_buffer: String::new(),
full_reasoning_buffer: String::new(),
session_id: None, session_id: None,
} }
} }
@@ -573,14 +587,19 @@ impl ChatWidget {
fn flush_active_exec_cell(&mut self) { fn flush_active_exec_cell(&mut self) {
if let Some(active) = self.active_exec_cell.take() { if let Some(active) = self.active_exec_cell.take() {
self.app_event_tx self.app_event_tx
.send(AppEvent::InsertHistory(active.display_lines())); .send(AppEvent::InsertHistoryCell(Box::new(active)));
} }
} }
fn add_to_history(&mut self, cell: &dyn HistoryCell) { fn add_to_history(&mut self, cell: impl HistoryCell + 'static) {
self.flush_active_exec_cell(); self.flush_active_exec_cell();
self.app_event_tx self.app_event_tx
.send(AppEvent::InsertHistory(cell.display_lines())); .send(AppEvent::InsertHistoryCell(Box::new(cell)));
}
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>) {
self.flush_active_exec_cell();
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
} }
fn submit_user_message(&mut self, user_message: UserMessage) { fn submit_user_message(&mut self, user_message: UserMessage) {
@@ -616,7 +635,7 @@ impl ChatWidget {
// Only show the text portion in conversation history. // Only show the text portion in conversation history.
if !text.is_empty() { if !text.is_empty() {
self.add_to_history(&history_cell::new_user_prompt(text.clone())); self.add_to_history(history_cell::new_user_prompt(text.clone()));
} }
} }
@@ -692,12 +711,12 @@ impl ChatWidget {
pub(crate) fn add_diff_output(&mut self, diff_output: String) { pub(crate) fn add_diff_output(&mut self, diff_output: String) {
self.bottom_pane.set_task_running(false); self.bottom_pane.set_task_running(false);
self.add_to_history(&history_cell::new_diff_output(diff_output)); self.add_to_history(history_cell::new_diff_output(diff_output));
self.mark_needs_redraw(); self.mark_needs_redraw();
} }
pub(crate) fn add_status_output(&mut self) { pub(crate) fn add_status_output(&mut self) {
self.add_to_history(&history_cell::new_status_output( self.add_to_history(history_cell::new_status_output(
&self.config, &self.config,
&self.total_token_usage, &self.total_token_usage,
&self.session_id, &self.session_id,
@@ -808,7 +827,7 @@ impl ChatWidget {
pub(crate) fn add_mcp_output(&mut self) { pub(crate) fn add_mcp_output(&mut self) {
if self.config.mcp_servers.is_empty() { if self.config.mcp_servers.is_empty() {
self.add_to_history(&history_cell::empty_mcp_output()); self.add_to_history(history_cell::empty_mcp_output());
} else { } else {
self.submit_op(Op::ListMcpTools); self.submit_op(Op::ListMcpTools);
} }
@@ -856,7 +875,7 @@ impl ChatWidget {
} }
fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) { fn on_list_mcp_tools(&mut self, ev: McpListToolsResponseEvent) {
self.add_to_history(&history_cell::new_mcp_tools_output(&self.config, ev.tools)); self.add_to_history(history_cell::new_mcp_tools_output(&self.config, ev.tools));
} }
/// Programmatically submit a user text message as if typed in the /// Programmatically submit a user text message as if typed in the

View File

@@ -150,6 +150,7 @@ fn make_chatwidget_manual() -> (
interrupts: InterruptManager::new(), interrupts: InterruptManager::new(),
needs_redraw: false, needs_redraw: false,
reasoning_buffer: String::new(), reasoning_buffer: String::new(),
full_reasoning_buffer: String::new(),
session_id: None, session_id: None,
frame_requester: crate::tui::FrameRequester::test_dummy(), frame_requester: crate::tui::FrameRequester::test_dummy(),
}; };
@@ -161,8 +162,10 @@ fn drain_insert_history(
) -> Vec<Vec<ratatui::text::Line<'static>>> { ) -> Vec<Vec<ratatui::text::Line<'static>>> {
let mut out = Vec::new(); let mut out = Vec::new();
while let Ok(ev) = rx.try_recv() { while let Ok(ev) = rx.try_recv() {
if let AppEvent::InsertHistory(lines) = ev { match ev {
out.push(lines); AppEvent::InsertHistoryLines(lines) => out.push(lines),
AppEvent::InsertHistoryCell(cell) => out.push(cell.display_lines()),
_ => {}
} }
} }
out out
@@ -336,7 +339,8 @@ async fn binary_size_transcript_matches_ideal_fixture() {
let ev: Event = serde_json::from_value(payload.clone()).expect("parse"); let ev: Event = serde_json::from_value(payload.clone()).expect("parse");
chat.handle_codex_event(ev); chat.handle_codex_event(ev);
while let Ok(app_ev) = rx.try_recv() { while let Ok(app_ev) = rx.try_recv() {
if let AppEvent::InsertHistory(lines) = app_ev { match app_ev {
AppEvent::InsertHistoryLines(lines) => {
transcript.push_str(&lines_to_single_string(&lines)); transcript.push_str(&lines_to_single_string(&lines));
crate::insert_history::insert_history_lines_to_writer( crate::insert_history::insert_history_lines_to_writer(
&mut terminal, &mut terminal,
@@ -344,6 +348,17 @@ async fn binary_size_transcript_matches_ideal_fixture() {
lines, lines,
); );
} }
AppEvent::InsertHistoryCell(cell) => {
let lines = cell.display_lines();
transcript.push_str(&lines_to_single_string(&lines));
crate::insert_history::insert_history_lines_to_writer(
&mut terminal,
&mut ansi,
lines,
);
}
_ => {}
}
} }
} }
} }
@@ -353,7 +368,8 @@ async fn binary_size_transcript_matches_ideal_fixture() {
{ {
chat.on_commit_tick(); chat.on_commit_tick();
while let Ok(app_ev) = rx.try_recv() { while let Ok(app_ev) = rx.try_recv() {
if let AppEvent::InsertHistory(lines) = app_ev { match app_ev {
AppEvent::InsertHistoryLines(lines) => {
transcript.push_str(&lines_to_single_string(&lines)); transcript.push_str(&lines_to_single_string(&lines));
crate::insert_history::insert_history_lines_to_writer( crate::insert_history::insert_history_lines_to_writer(
&mut terminal, &mut terminal,
@@ -361,6 +377,17 @@ async fn binary_size_transcript_matches_ideal_fixture() {
lines, lines,
); );
} }
AppEvent::InsertHistoryCell(cell) => {
let lines = cell.display_lines();
transcript.push_str(&lines_to_single_string(&lines));
crate::insert_history::insert_history_lines_to_writer(
&mut terminal,
&mut ansi,
lines,
);
}
_ => {}
}
} }
} }
} }
@@ -809,7 +836,7 @@ fn headers_emitted_on_stream_begin_for_answer_and_not_for_reasoning() {
}); });
let mut saw_codex_pre = false; let mut saw_codex_pre = false;
while let Ok(ev) = rx.try_recv() { while let Ok(ev) = rx.try_recv() {
if let AppEvent::InsertHistory(lines) = ev { if let AppEvent::InsertHistoryLines(lines) = ev {
let s = lines let s = lines
.iter() .iter()
.flat_map(|l| l.spans.iter()) .flat_map(|l| l.spans.iter())
@@ -837,7 +864,7 @@ fn headers_emitted_on_stream_begin_for_answer_and_not_for_reasoning() {
chat.on_commit_tick(); chat.on_commit_tick();
let mut saw_codex_post = false; let mut saw_codex_post = false;
while let Ok(ev) = rx.try_recv() { while let Ok(ev) = rx.try_recv() {
if let AppEvent::InsertHistory(lines) = ev { if let AppEvent::InsertHistoryLines(lines) = ev {
let s = lines let s = lines
.iter() .iter()
.flat_map(|l| l.spans.iter()) .flat_map(|l| l.spans.iter())
@@ -865,7 +892,7 @@ fn headers_emitted_on_stream_begin_for_answer_and_not_for_reasoning() {
}); });
let mut saw_thinking = false; let mut saw_thinking = false;
while let Ok(ev) = rx2.try_recv() { while let Ok(ev) = rx2.try_recv() {
if let AppEvent::InsertHistory(lines) = ev { if let AppEvent::InsertHistoryLines(lines) = ev {
let s = lines let s = lines
.iter() .iter()
.flat_map(|l| l.spans.iter()) .flat_map(|l| l.spans.iter())

View File

@@ -1,6 +1,7 @@
use crate::diff_render::create_diff_summary; use crate::diff_render::create_diff_summary;
use crate::exec_command::relativize_to_home; use crate::exec_command::relativize_to_home;
use crate::exec_command::strip_bash_lc_and_escape; use crate::exec_command::strip_bash_lc_and_escape;
use crate::markdown::append_markdown;
use crate::slash_command::SlashCommand; use crate::slash_command::SlashCommand;
use crate::text_formatting::format_and_truncate_tool_result; use crate::text_formatting::format_and_truncate_tool_result;
use base64::Engine; use base64::Engine;
@@ -39,7 +40,7 @@ use std::time::Instant;
use tracing::error; use tracing::error;
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone)] #[derive(Clone, Debug)]
pub(crate) struct CommandOutput { pub(crate) struct CommandOutput {
pub(crate) exit_code: i32, pub(crate) exit_code: i32,
pub(crate) stdout: String, pub(crate) stdout: String,
@@ -54,9 +55,13 @@ pub(crate) enum PatchEventType {
/// Represents an event to display in the conversation history. Returns its /// Represents an event to display in the conversation history. Returns its
/// `Vec<Line<'static>>` representation to make it easier to display in a /// `Vec<Line<'static>>` representation to make it easier to display in a
/// scrollable list. /// scrollable list.
pub(crate) trait HistoryCell { pub(crate) trait HistoryCell: std::fmt::Debug + Send + Sync {
fn display_lines(&self) -> Vec<Line<'static>>; fn display_lines(&self) -> Vec<Line<'static>>;
fn transcript_lines(&self) -> Vec<Line<'static>> {
self.display_lines()
}
fn desired_height(&self, width: u16) -> u16 { fn desired_height(&self, width: u16) -> u16 {
Paragraph::new(Text::from(self.display_lines())) Paragraph::new(Text::from(self.display_lines()))
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
@@ -66,6 +71,7 @@ pub(crate) trait HistoryCell {
} }
} }
#[derive(Debug)]
pub(crate) struct PlainHistoryCell { pub(crate) struct PlainHistoryCell {
lines: Vec<Line<'static>>, lines: Vec<Line<'static>>,
} }
@@ -76,6 +82,22 @@ impl HistoryCell for PlainHistoryCell {
} }
} }
#[derive(Debug)]
pub(crate) struct TranscriptOnlyHistoryCell {
lines: Vec<Line<'static>>,
}
impl HistoryCell for TranscriptOnlyHistoryCell {
fn display_lines(&self) -> Vec<Line<'static>> {
Vec::new()
}
fn transcript_lines(&self) -> Vec<Line<'static>> {
self.lines.clone()
}
}
#[derive(Debug)]
pub(crate) struct ExecCell { pub(crate) struct ExecCell {
pub(crate) command: Vec<String>, pub(crate) command: Vec<String>,
pub(crate) parsed: Vec<ParsedCommand>, pub(crate) parsed: Vec<ParsedCommand>,
@@ -101,6 +123,7 @@ impl WidgetRef for &ExecCell {
} }
} }
#[derive(Debug)]
struct CompletedMcpToolCallWithImageOutput { struct CompletedMcpToolCallWithImageOutput {
_image: DynamicImage, _image: DynamicImage,
} }
@@ -930,6 +953,17 @@ pub(crate) fn new_patch_apply_success(stdout: String) -> PlainHistoryCell {
PlainHistoryCell { lines } PlainHistoryCell { lines }
} }
pub(crate) fn new_reasoning_block(
full_reasoning_buffer: String,
config: &Config,
) -> TranscriptOnlyHistoryCell {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from("thinking".magenta().italic()));
append_markdown(&full_reasoning_buffer, &mut lines, config);
lines.push(Line::from(""));
TranscriptOnlyHistoryCell { lines }
}
fn output_lines( fn output_lines(
output: Option<&CommandOutput>, output: Option<&CommandOutput>,
only_err: bool, only_err: bool,

View File

@@ -143,7 +143,7 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) {
LOGGER.write_json_line(value); LOGGER.write_json_line(value);
} }
// Internal UI events; still log for fidelity, but avoid heavy payloads. // Internal UI events; still log for fidelity, but avoid heavy payloads.
AppEvent::InsertHistory(lines) => { AppEvent::InsertHistoryLines(lines) => {
let value = json!({ let value = json!({
"ts": now_ts(), "ts": now_ts(),
"dir": "to_tui", "dir": "to_tui",
@@ -152,6 +152,15 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) {
}); });
LOGGER.write_json_line(value); LOGGER.write_json_line(value);
} }
AppEvent::InsertHistoryCell(cell) => {
let value = json!({
"ts": now_ts(),
"dir": "to_tui",
"kind": "insert_history_cell",
"lines": cell.transcript_lines().len(),
});
LOGGER.write_json_line(value);
}
AppEvent::StartFileSearch(query) => { AppEvent::StartFileSearch(query) => {
let value = json!({ let value = json!({
"ts": now_ts(), "ts": now_ts(),

View File

@@ -17,7 +17,7 @@ pub(crate) struct AppEventHistorySink(pub(crate) crate::app_event_sender::AppEve
impl HistorySink for AppEventHistorySink { impl HistorySink for AppEventHistorySink {
fn insert_history(&self, lines: Vec<Line<'static>>) { fn insert_history(&self, lines: Vec<Line<'static>>) {
self.0 self.0
.send(crate::app_event::AppEvent::InsertHistory(lines)) .send(crate::app_event::AppEvent::InsertHistoryLines(lines))
} }
fn start_commit_animation(&self) { fn start_commit_animation(&self) {
self.0 self.0

View File

@@ -328,7 +328,7 @@ impl UserApprovalWidget {
} }
} }
lines.push(Line::from("")); lines.push(Line::from(""));
self.app_event_tx.send(AppEvent::InsertHistory(lines)); self.app_event_tx.send(AppEvent::InsertHistoryLines(lines));
let op = match &self.approval_request { let op = match &self.approval_request {
ApprovalRequest::Exec { id, .. } => Op::ExecApproval { ApprovalRequest::Exec { id, .. } => Op::ExecApproval {