show thinking in transcript (#2538)
record the full reasoning trace and show it in transcript mode
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user