mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
ctrl-t opens a 'transcript' in less
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -924,6 +924,7 @@ dependencies = [
|
||||
"strum 0.27.2",
|
||||
"strum_macros 0.27.2",
|
||||
"supports-color",
|
||||
"tempfile",
|
||||
"textwrap 0.16.2",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
||||
@@ -66,6 +66,7 @@ tokio = { version = "1", features = [
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
tempfile = "3"
|
||||
tracing = { version = "0.1.41", features = ["log"] }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::app_event_sender::AppEventSender;
|
||||
use crate::chatwidget::ChatWidget;
|
||||
use crate::file_search::FileSearchManager;
|
||||
use crate::get_git_diff::get_git_diff;
|
||||
use crate::insert_history::write_lines;
|
||||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||
use crate::onboarding::onboarding_screen::OnboardingScreen;
|
||||
use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
|
||||
@@ -22,7 +23,9 @@ use crossterm::terminal::supports_keyboard_enhancement;
|
||||
use ratatui::layout::Offset;
|
||||
use ratatui::prelude::Backend;
|
||||
use ratatui::text::Line;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -31,6 +34,7 @@ use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Time window for debouncing redraw requests.
|
||||
const REDRAW_DEBOUNCE: Duration = Duration::from_millis(1);
|
||||
@@ -61,6 +65,7 @@ pub(crate) struct App<'a> {
|
||||
file_search: FileSearchManager,
|
||||
|
||||
pending_history_lines: Vec<Line<'static>>,
|
||||
transcript: Vec<Line<'static>>,
|
||||
|
||||
enhanced_keys_supported: bool,
|
||||
|
||||
@@ -70,6 +75,7 @@ pub(crate) struct App<'a> {
|
||||
/// Channel to schedule one-shot animation frames; coalesced by a single
|
||||
/// scheduler thread.
|
||||
frame_schedule_tx: std::sync::mpsc::Sender<Instant>,
|
||||
event_reader_enabled: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
|
||||
@@ -93,6 +99,7 @@ impl App<'_> {
|
||||
|
||||
let (app_event_tx, app_event_rx) = channel();
|
||||
let app_event_tx = AppEventSender::new(app_event_tx);
|
||||
let event_reader_enabled = Arc::new(AtomicBool::new(true));
|
||||
|
||||
let enhanced_keys_supported = supports_keyboard_enhancement().unwrap_or(false);
|
||||
|
||||
@@ -100,8 +107,13 @@ impl App<'_> {
|
||||
// re-publishing the events as AppEvents, as appropriate.
|
||||
{
|
||||
let app_event_tx = app_event_tx.clone();
|
||||
let events_enabled = event_reader_enabled.clone();
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
if !events_enabled.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
continue;
|
||||
}
|
||||
// This timeout is necessary to avoid holding the event lock
|
||||
// that crossterm::event::read() acquires. In particular,
|
||||
// reading the cursor position (crossterm::cursor::position())
|
||||
@@ -215,6 +227,7 @@ impl App<'_> {
|
||||
server: conversation_manager,
|
||||
app_event_tx,
|
||||
pending_history_lines: Vec::new(),
|
||||
transcript: Vec::new(),
|
||||
app_event_rx,
|
||||
app_state,
|
||||
config,
|
||||
@@ -222,6 +235,7 @@ impl App<'_> {
|
||||
enhanced_keys_supported,
|
||||
commit_anim_running: Arc::new(AtomicBool::new(false)),
|
||||
frame_schedule_tx: frame_tx,
|
||||
event_reader_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,10 +249,16 @@ impl App<'_> {
|
||||
|
||||
while let Ok(event) = self.app_event_rx.recv() {
|
||||
match event {
|
||||
AppEvent::InsertHistory(lines) => {
|
||||
AppEvent::InsertHistoryLines(lines) => {
|
||||
self.transcript.extend(lines.clone());
|
||||
self.pending_history_lines.extend(lines);
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
self.transcript.extend(cell.transcript_lines());
|
||||
self.pending_history_lines.extend(cell.display_lines());
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
AppEvent::RequestRedraw => {
|
||||
self.schedule_frame_in(REDRAW_DEBOUNCE);
|
||||
}
|
||||
@@ -299,6 +319,28 @@ impl App<'_> {
|
||||
}
|
||||
// No-op on non-Unix platforms.
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('t'),
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} => {
|
||||
self.event_reader_enabled.store(false, Ordering::Relaxed);
|
||||
let mut tmp = NamedTempFile::new()?;
|
||||
write_lines(tmp.as_file_mut(), self.transcript.clone());
|
||||
tmp.flush().ok();
|
||||
let path = tmp.into_temp_path();
|
||||
tui::restore_modes()?;
|
||||
let _ = Command::new("less")
|
||||
.arg("-R")
|
||||
.arg("+G")
|
||||
.arg(path.as_os_str())
|
||||
.status();
|
||||
let _ = path.close();
|
||||
tui::set_modes()?;
|
||||
self.event_reader_enabled.store(true, Ordering::Relaxed);
|
||||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('d'),
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
@@ -499,7 +541,7 @@ impl App<'_> {
|
||||
|
||||
#[cfg(unix)]
|
||||
fn suspend(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
tui::restore()?;
|
||||
tui::restore_modes()?;
|
||||
// SAFETY: Unix-only code path. We intentionally send SIGTSTP to the
|
||||
// current process group (pid 0) to trigger standard job-control
|
||||
// suspension semantics. This FFI does not involve any raw pointers,
|
||||
|
||||
@@ -5,6 +5,7 @@ use ratatui::text::Line;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::app::ChatWidgetArgs;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::slash_command::SlashCommand;
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -51,7 +52,8 @@ pub(crate) enum AppEvent {
|
||||
matches: Vec<FileMatch>,
|
||||
},
|
||||
|
||||
InsertHistory(Vec<Line<'static>>),
|
||||
InsertHistoryLines(Vec<Line<'static>>),
|
||||
InsertHistoryCell(Box<dyn HistoryCell>),
|
||||
|
||||
StartCommitAnimation,
|
||||
StopCommitAnimation,
|
||||
|
||||
@@ -86,6 +86,7 @@ pub(crate) struct ChatWidget<'a> {
|
||||
needs_redraw: bool,
|
||||
// Accumulates the current reasoning block text to extract a header
|
||||
reasoning_buffer: String,
|
||||
full_reasoning_buffer: String,
|
||||
session_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
@@ -125,7 +126,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);
|
||||
}
|
||||
@@ -160,12 +161,22 @@ impl ChatWidget<'_> {
|
||||
|
||||
fn on_agent_reasoning_final(&mut self) {
|
||||
// Clear the reasoning buffer at the end of a reasoning block.
|
||||
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.
|
||||
self.full_reasoning_buffer.push_str(&self.reasoning_buffer);
|
||||
self.full_reasoning_buffer.push_str("\n\n");
|
||||
self.reasoning_buffer.clear();
|
||||
}
|
||||
|
||||
@@ -175,6 +186,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();
|
||||
}
|
||||
@@ -203,7 +215,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();
|
||||
@@ -211,7 +223,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) {
|
||||
@@ -246,7 +258,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,
|
||||
},
|
||||
@@ -373,7 +385,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,
|
||||
));
|
||||
}
|
||||
@@ -385,9 +397,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +421,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(),
|
||||
));
|
||||
@@ -451,20 +463,26 @@ 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(
|
||||
80,
|
||||
ev.invocation,
|
||||
ev.duration,
|
||||
ev.result
|
||||
.as_ref()
|
||||
.map(|r| !r.is_error.unwrap_or(false))
|
||||
.unwrap_or(false),
|
||||
ev.result,
|
||||
));
|
||||
if let Some(cell) =
|
||||
history_cell::try_new_completed_mcp_tool_call_with_image_output(&ev.result)
|
||||
{
|
||||
self.add_to_history(cell);
|
||||
} else {
|
||||
self.add_to_history(history_cell::new_completed_mcp_tool_call(
|
||||
80,
|
||||
ev.invocation,
|
||||
ev.duration,
|
||||
ev.result
|
||||
.as_ref()
|
||||
.map(|r| !r.is_error.unwrap_or(false))
|
||||
.unwrap_or(false),
|
||||
ev.result,
|
||||
));
|
||||
}
|
||||
}
|
||||
fn interrupt_running_task(&mut self) {
|
||||
if self.bottom_pane.is_task_running() {
|
||||
@@ -522,6 +540,7 @@ impl ChatWidget<'_> {
|
||||
interrupts: InterruptManager::new(),
|
||||
needs_redraw: false,
|
||||
reasoning_buffer: String::new(),
|
||||
full_reasoning_buffer: String::new(),
|
||||
session_id: None,
|
||||
}
|
||||
}
|
||||
@@ -554,14 +573,14 @@ 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 submit_user_message(&mut self, user_message: UserMessage) {
|
||||
@@ -597,7 +616,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,11 +682,11 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn add_diff_output(&mut self, diff_output: String) {
|
||||
self.add_to_history(&history_cell::new_diff_output(diff_output.clone()));
|
||||
self.add_to_history(history_cell::new_diff_output(diff_output.clone()));
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -675,7 +694,7 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn add_prompts_output(&mut self) {
|
||||
self.add_to_history(&history_cell::new_prompts_output());
|
||||
self.add_to_history(history_cell::new_prompts_output());
|
||||
}
|
||||
|
||||
/// Forward file-search results to the bottom pane.
|
||||
|
||||
@@ -2,8 +2,11 @@ use crate::colors::LIGHT_BLUE;
|
||||
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 crate::user_approval_widget::ApprovalRequest;
|
||||
use crate::user_approval_widget::to_command_display;
|
||||
use base64::Engine;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_common::create_config_summary_entries;
|
||||
@@ -15,6 +18,7 @@ use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::plan_tool::UpdatePlanArgs;
|
||||
use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::McpInvocation;
|
||||
use codex_core::protocol::ReviewDecision;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
@@ -39,7 +43,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 +58,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 +74,7 @@ pub(crate) trait HistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PlainHistoryCell {
|
||||
lines: Vec<Line<'static>>,
|
||||
}
|
||||
@@ -76,6 +85,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![]
|
||||
}
|
||||
|
||||
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,7 +126,8 @@ impl WidgetRef for &ExecCell {
|
||||
}
|
||||
}
|
||||
|
||||
struct CompletedMcpToolCallWithImageOutput {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CompletedMcpToolCallWithImageOutput {
|
||||
_image: DynamicImage,
|
||||
}
|
||||
impl HistoryCell for CompletedMcpToolCallWithImageOutput {
|
||||
@@ -343,7 +369,7 @@ pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> PlainHistor
|
||||
|
||||
/// If the first content is an image, return a new cell with the image.
|
||||
/// TODO(rgwood-dd): Handle images properly even if they're not the first result.
|
||||
fn try_new_completed_mcp_tool_call_with_image_output(
|
||||
pub fn try_new_completed_mcp_tool_call_with_image_output(
|
||||
result: &Result<mcp_types::CallToolResult, String>,
|
||||
) -> Option<CompletedMcpToolCallWithImageOutput> {
|
||||
match result {
|
||||
@@ -387,11 +413,7 @@ pub(crate) fn new_completed_mcp_tool_call(
|
||||
duration: Duration,
|
||||
success: bool,
|
||||
result: Result<mcp_types::CallToolResult, String>,
|
||||
) -> Box<dyn HistoryCell> {
|
||||
if let Some(cell) = try_new_completed_mcp_tool_call_with_image_output(&result) {
|
||||
return Box::new(cell);
|
||||
}
|
||||
|
||||
) -> PlainHistoryCell {
|
||||
let duration = format_duration(duration);
|
||||
let status_str = if success { "success" } else { "failed" };
|
||||
let title_line = Line::from(vec![
|
||||
@@ -459,7 +481,7 @@ pub(crate) fn new_completed_mcp_tool_call(
|
||||
}
|
||||
};
|
||||
|
||||
Box::new(PlainHistoryCell { lines })
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_diff_output(message: String) -> PlainHistoryCell {
|
||||
@@ -910,3 +932,91 @@ mod tests {
|
||||
assert_eq!(lines[2].spans[0].content, " ");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_exec_approval_decision(
|
||||
approval_request: &ApprovalRequest,
|
||||
decision: codex_core::protocol::ReviewDecision,
|
||||
feedback: String,
|
||||
) -> PlainHistoryCell {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
match approval_request {
|
||||
ApprovalRequest::Exec { command, .. } => {
|
||||
let cmd = strip_bash_lc_and_escape(command);
|
||||
let mut cmd_span: Span = cmd.clone().into();
|
||||
cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM);
|
||||
|
||||
// Result line based on decision.
|
||||
match decision {
|
||||
ReviewDecision::Approved => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✔ ".fg(Color::Green),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![" this time".bold()],
|
||||
));
|
||||
}
|
||||
ReviewDecision::ApprovedForSession => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✔ ".fg(Color::Green),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
"codex to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![" every time this session".bold()],
|
||||
));
|
||||
}
|
||||
ReviewDecision::Denied => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✗ ".fg(Color::Red),
|
||||
"You ".into(),
|
||||
"did not approve".bold(),
|
||||
" codex to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![],
|
||||
));
|
||||
}
|
||||
ReviewDecision::Abort => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✗ ".fg(Color::Red),
|
||||
"You ".into(),
|
||||
"canceled".bold(),
|
||||
" the request to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![],
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ApprovalRequest::ApplyPatch { .. } => {
|
||||
lines.push(Line::from(format!("patch approval decision: {decision:?}")));
|
||||
}
|
||||
}
|
||||
if !feedback.trim().is_empty() {
|
||||
lines.push(Line::from("feedback:"));
|
||||
for l in feedback.lines() {
|
||||
lines.push(Line::from(l.to_string()));
|
||||
}
|
||||
}
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -109,6 +109,13 @@ pub fn insert_history_lines_to_writer<B, W>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_lines(writer: &mut impl Write, lines: Vec<Line>) {
|
||||
for line in lines {
|
||||
queue!(writer, Print("\r\n")).ok();
|
||||
write_spans(writer, line.iter()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetScrollRegion(pub std::ops::Range<u16>);
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ fn run_ratatui_app(
|
||||
reason = "TUI should no longer be displayed, so we can write to stderr."
|
||||
)]
|
||||
fn restore() {
|
||||
if let Err(err) = tui::restore() {
|
||||
if let Err(err) = tui::restore_modes() {
|
||||
eprintln!(
|
||||
"failed to restore terminal. Run `reset` or restart your terminal to recover: {err}"
|
||||
);
|
||||
|
||||
@@ -160,12 +160,12 @@ 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::InsertHistoryCell(cell) => {
|
||||
let value = json!({
|
||||
"ts": now_ts(),
|
||||
"dir": "to_tui",
|
||||
"kind": "insert_history",
|
||||
"lines": lines.len(),
|
||||
"lines": cell.transcript_lines().len(),
|
||||
});
|
||||
LOGGER.write_json_line(value);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,23 +23,7 @@ pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Initialize the terminal (inline viewport; history stays in normal scrollback)
|
||||
pub fn init(_config: &Config) -> Result<Tui> {
|
||||
execute!(stdout(), EnableBracketedPaste)?;
|
||||
|
||||
enable_raw_mode()?;
|
||||
// Enable keyboard enhancement flags so modifiers for keys like Enter are disambiguated.
|
||||
// chat_composer.rs is using a keyboard event listener to enter for any modified keys
|
||||
// to create a new line that require this.
|
||||
// Some terminals (notably legacy Windows consoles) do not support
|
||||
// keyboard enhancement flags. Attempt to enable them, but continue
|
||||
// gracefully if unsupported.
|
||||
let _ = execute!(
|
||||
stdout(),
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
);
|
||||
set_modes()?;
|
||||
set_panic_hook();
|
||||
|
||||
// Clear screen and move cursor to top-left before drawing UI
|
||||
@@ -53,13 +37,27 @@ pub fn init(_config: &Config) -> Result<Tui> {
|
||||
fn set_panic_hook() {
|
||||
let hook = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
let _ = restore(); // ignore any errors as we are already failing
|
||||
let _ = restore_modes(); // ignore any errors as we are already failing
|
||||
hook(panic_info);
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn set_modes() -> Result<()> {
|
||||
execute!(stdout(), EnableBracketedPaste)?;
|
||||
enable_raw_mode()?;
|
||||
let _ = execute!(
|
||||
stdout(),
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restore the terminal to its original state
|
||||
pub fn restore() -> Result<()> {
|
||||
pub fn restore_modes() -> Result<()> {
|
||||
// Pop may fail on platforms that didn't support the push; ignore errors.
|
||||
let _ = execute!(stdout(), PopKeyboardEnhancementFlags);
|
||||
execute!(stdout(), DisableBracketedPaste)?;
|
||||
|
||||
@@ -29,6 +29,7 @@ use ratatui::widgets::Wrap;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::history_cell;
|
||||
|
||||
/// Request coming from the agent that needs user approval.
|
||||
pub(crate) enum ApprovalRequest {
|
||||
@@ -109,7 +110,7 @@ pub(crate) struct UserApprovalWidget<'a> {
|
||||
done: bool,
|
||||
}
|
||||
|
||||
fn to_command_display<'a>(
|
||||
pub fn to_command_display<'a>(
|
||||
first_line: Vec<Span<'a>>,
|
||||
cmd: String,
|
||||
last_line: Vec<Span<'a>>,
|
||||
@@ -258,77 +259,9 @@ impl UserApprovalWidget<'_> {
|
||||
}
|
||||
|
||||
fn send_decision_with_feedback(&mut self, decision: ReviewDecision, feedback: String) {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
match &self.approval_request {
|
||||
ApprovalRequest::Exec { command, .. } => {
|
||||
let cmd = strip_bash_lc_and_escape(command);
|
||||
let mut cmd_span: Span = cmd.clone().into();
|
||||
cmd_span.style = cmd_span.style.add_modifier(Modifier::DIM);
|
||||
|
||||
// Result line based on decision.
|
||||
match decision {
|
||||
ReviewDecision::Approved => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✔ ".fg(Color::Green),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![" this time".bold()],
|
||||
));
|
||||
}
|
||||
ReviewDecision::ApprovedForSession => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✔ ".fg(Color::Green),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
"codex to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![" every time this session".bold()],
|
||||
));
|
||||
}
|
||||
ReviewDecision::Denied => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✗ ".fg(Color::Red),
|
||||
"You ".into(),
|
||||
"did not approve".bold(),
|
||||
" codex to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![],
|
||||
));
|
||||
}
|
||||
ReviewDecision::Abort => {
|
||||
lines.extend(to_command_display(
|
||||
vec![
|
||||
"✗ ".fg(Color::Red),
|
||||
"You ".into(),
|
||||
"canceled".bold(),
|
||||
" the request to run ".into(),
|
||||
],
|
||||
cmd,
|
||||
vec![],
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ApprovalRequest::ApplyPatch { .. } => {
|
||||
lines.push(Line::from(format!("patch approval decision: {decision:?}")));
|
||||
}
|
||||
}
|
||||
if !feedback.trim().is_empty() {
|
||||
lines.push(Line::from("feedback:"));
|
||||
for l in feedback.lines() {
|
||||
lines.push(Line::from(l.to_string()));
|
||||
}
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
self.app_event_tx.send(AppEvent::InsertHistory(lines));
|
||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
history_cell::new_exec_approval_decision(&self.approval_request, decision, feedback),
|
||||
)));
|
||||
|
||||
let op = match &self.approval_request {
|
||||
ApprovalRequest::Exec { id, .. } => Op::ExecApproval {
|
||||
|
||||
Reference in New Issue
Block a user