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> {
match event {
AppEvent::InsertHistory(lines) => {
AppEvent::InsertHistoryLines(lines) => {
self.transcript_lines.extend(lines.clone());
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 => {
if self
.commit_anim_running

View File

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

View File

@@ -98,6 +98,8 @@ pub(crate) struct ChatWidget {
needs_redraw: bool,
// Accumulates the current reasoning block text to extract a header
reasoning_buffer: String,
// Accumulates full reasoning content for transcript-only recording
full_reasoning_buffer: String,
session_id: Option<Uuid>,
frame_requester: FrameRequester,
}
@@ -138,7 +140,7 @@ impl ChatWidget {
self.bottom_pane
.set_history_metadata(event.history_log_id, event.history_entry_count);
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() {
self.submit_user_message(user_message);
}
@@ -172,13 +174,23 @@ impl ChatWidget {
}
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.full_reasoning_buffer.clear();
self.mark_needs_redraw();
}
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();
}
@@ -188,6 +200,7 @@ impl ChatWidget {
self.bottom_pane.clear_ctrl_c_quit_hint();
self.bottom_pane.set_task_running(true);
self.stream.reset_headers_for_new_turn();
self.full_reasoning_buffer.clear();
self.reasoning_buffer.clear();
self.mark_needs_redraw();
}
@@ -216,7 +229,7 @@ impl ChatWidget {
}
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.running_commands.clear();
self.stream.clear_all();
@@ -224,7 +237,7 @@ impl ChatWidget {
}
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) {
@@ -259,7 +272,7 @@ impl ChatWidget {
}
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 {
auto_approved: event.auto_approved,
},
@@ -386,7 +399,7 @@ impl ChatWidget {
self.active_exec_cell = None;
let pending = std::mem::take(&mut self.pending_exec_completions);
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,
));
}
@@ -398,9 +411,9 @@ impl ChatWidget {
event: codex_core::protocol::PatchApplyEndEvent,
) {
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 {
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,
) {
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,
ev.changes.clone(),
));
@@ -464,11 +477,11 @@ impl ChatWidget {
pub(crate) fn handle_mcp_begin_now(&mut self, ev: McpToolCallBeginEvent) {
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) {
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,
ev.invocation,
ev.duration,
@@ -541,6 +554,7 @@ impl ChatWidget {
interrupts: InterruptManager::new(),
needs_redraw: false,
reasoning_buffer: String::new(),
full_reasoning_buffer: String::new(),
session_id: None,
}
}
@@ -573,14 +587,19 @@ impl ChatWidget {
fn flush_active_exec_cell(&mut self) {
if let Some(active) = self.active_exec_cell.take() {
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.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) {
@@ -616,7 +635,7 @@ impl ChatWidget {
// Only show the text portion in conversation history.
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) {
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();
}
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.total_token_usage,
&self.session_id,
@@ -808,7 +827,7 @@ impl ChatWidget {
pub(crate) fn add_mcp_output(&mut self) {
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 {
self.submit_op(Op::ListMcpTools);
}
@@ -856,7 +875,7 @@ impl ChatWidget {
}
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

View File

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

View File

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

View File

@@ -143,7 +143,7 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) {
LOGGER.write_json_line(value);
}
// Internal UI events; still log for fidelity, but avoid heavy payloads.
AppEvent::InsertHistory(lines) => {
AppEvent::InsertHistoryLines(lines) => {
let value = json!({
"ts": now_ts(),
"dir": "to_tui",
@@ -152,6 +152,15 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) {
});
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) => {
let value = json!({
"ts": now_ts(),

View File

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

View File

@@ -328,7 +328,7 @@ impl UserApprovalWidget {
}
}
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 {
ApprovalRequest::Exec { id, .. } => Op::ExecApproval {