Merge branch 'main' into copy_images

This commit is contained in:
pap
2025-08-01 18:24:49 +01:00
116 changed files with 6380 additions and 2815 deletions

View File

@@ -5,17 +5,21 @@ use crate::file_search::FileSearchManager;
use crate::get_git_diff::get_git_diff;
use crate::git_warning_screen::GitWarningOutcome;
use crate::git_warning_screen::GitWarningScreen;
use crate::login_screen::LoginScreen;
use crate::scroll_event_helper::ScrollEventHelper;
use crate::slash_command::SlashCommand;
use crate::tui;
use codex_core::config::Config;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use color_eyre::eyre::Result;
use crossterm::SynchronizedUpdate;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::MouseEvent;
use crossterm::event::MouseEventKind;
use crossterm::event::KeyEventKind;
use crossterm::terminal::supports_keyboard_enhancement;
use ratatui::layout::Offset;
use ratatui::prelude::Backend;
use ratatui::text::Line;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
@@ -88,8 +92,6 @@ enum AppState<'a> {
/// `AppState`.
widget: Box<ChatWidget<'a>>,
},
/// The login screen for the OpenAI provider.
Login { screen: LoginScreen },
/// The start-up warning that recommends running codex inside a Git repo.
GitWarning { screen: GitWarningScreen },
}
@@ -107,9 +109,13 @@ pub(crate) struct App<'a> {
/// True when a redraw has been scheduled but not yet executed.
pending_redraw: Arc<AtomicBool>,
pending_history_lines: Vec<Line<'static>>,
/// Stored parameters needed to instantiate the ChatWidget later, e.g.,
/// after dismissing the Git-repo warning.
chat_args: Option<ChatWidgetArgs>,
enhanced_keys_supported: bool,
}
/// Aggregate parameters needed to create a `ChatWidget`, as creation may be
@@ -119,20 +125,21 @@ struct ChatWidgetArgs {
config: Config,
initial_prompt: Option<String>,
initial_images: Vec<PathBuf>,
enhanced_keys_supported: bool,
}
impl App<'_> {
pub(crate) fn new(
config: Config,
initial_prompt: Option<String>,
show_login_screen: bool,
show_git_warning: bool,
initial_images: Vec<std::path::PathBuf>,
) -> Self {
let (app_event_tx, app_event_rx) = channel();
let app_event_tx = AppEventSender::new(app_event_tx);
let pending_redraw = Arc::new(AtomicBool::new(false));
let scroll_event_helper = ScrollEventHelper::new(app_event_tx.clone());
let enhanced_keys_supported = supports_keyboard_enhancement().unwrap_or(false);
// Spawn a dedicated thread for reading the crossterm event loop and
// re-publishing the events as AppEvents, as appropriate.
@@ -158,18 +165,6 @@ impl App<'_> {
crossterm::event::Event::Resize(_, _) => {
app_event_tx.send(AppEvent::RequestRedraw);
}
crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollUp,
..
}) => {
scroll_event_helper.scroll_up();
}
crossterm::event::Event::Mouse(MouseEvent {
kind: MouseEventKind::ScrollDown,
..
}) => {
scroll_event_helper.scroll_down();
}
crossterm::event::Event::Paste(pasted) => {
// Many terminals convert newlines to \r when
// pasting, e.g. [iTerm2][]. But [tui-textarea
@@ -192,18 +187,7 @@ impl App<'_> {
});
}
let (app_state, chat_args) = if show_login_screen {
(
AppState::Login {
screen: LoginScreen::new(app_event_tx.clone(), config.codex_home.clone()),
},
Some(ChatWidgetArgs {
config: config.clone(),
initial_prompt,
initial_images,
}),
)
} else if show_git_warning {
let (app_state, chat_args) = if show_git_warning {
(
AppState::GitWarning {
screen: GitWarningScreen::new(),
@@ -212,6 +196,7 @@ impl App<'_> {
config: config.clone(),
initial_prompt,
initial_images,
enhanced_keys_supported,
}),
)
} else {
@@ -220,6 +205,7 @@ impl App<'_> {
app_event_tx.clone(),
initial_prompt,
initial_images,
enhanced_keys_supported,
);
(
AppState::Chat {
@@ -232,12 +218,14 @@ impl App<'_> {
let file_search = FileSearchManager::new(config.cwd.clone(), app_event_tx.clone());
Self {
app_event_tx,
pending_history_lines: Vec::new(),
app_event_rx,
app_state,
config,
file_search,
pending_redraw,
chat_args,
enhanced_keys_supported,
}
}
@@ -277,27 +265,28 @@ impl App<'_> {
while let Ok(event) = self.app_event_rx.recv() {
match event {
AppEvent::InsertHistory(lines) => {
crate::insert_history::insert_history_lines(terminal, lines);
self.pending_history_lines.extend(lines);
self.app_event_tx.send(AppEvent::RequestRedraw);
}
AppEvent::RequestRedraw => {
self.schedule_redraw();
}
AppEvent::Redraw => {
self.draw_next_frame(terminal)?;
std::io::stdout().sync_update(|_| self.draw_next_frame(terminal))??;
}
AppEvent::KeyEvent(key_event) => {
match key_event {
KeyEvent {
code: KeyCode::Char('c'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
match &mut self.app_state {
AppState::Chat { widget } => {
widget.on_ctrl_c();
}
AppState::Login { .. } | AppState::GitWarning { .. } => {
AppState::GitWarning { .. } => {
// No-op.
}
}
@@ -305,6 +294,7 @@ impl App<'_> {
KeyEvent {
code: KeyCode::Char('d'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
match &mut self.app_state {
@@ -318,19 +308,22 @@ impl App<'_> {
self.dispatch_key_event(key_event);
}
}
AppState::Login { .. } | AppState::GitWarning { .. } => {
AppState::GitWarning { .. } => {
self.app_event_tx.send(AppEvent::ExitRequest);
}
}
}
_ => {
KeyEvent {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
..
} => {
self.dispatch_key_event(key_event);
}
_ => {
// Ignore Release key events for now.
}
};
}
AppEvent::Scroll(scroll_delta) => {
self.dispatch_scroll_event(scroll_delta);
}
AppEvent::Paste(text) => {
self.dispatch_paste_event(text);
}
@@ -342,11 +335,11 @@ impl App<'_> {
}
AppEvent::CodexOp(op) => match &mut self.app_state {
AppState::Chat { widget } => widget.submit_op(op),
AppState::Login { .. } | AppState::GitWarning { .. } => {}
AppState::GitWarning { .. } => {}
},
AppEvent::LatestLog(line) => match &mut self.app_state {
AppState::Chat { widget } => widget.update_latest_log(line),
AppState::Login { .. } | AppState::GitWarning { .. } => {}
AppState::GitWarning { .. } => {}
},
AppEvent::DispatchSlashCommand(command) => match command {
SlashCommand::New => {
@@ -355,10 +348,17 @@ impl App<'_> {
self.app_event_tx.clone(),
None,
Vec::new(),
self.enhanced_keys_supported,
));
self.app_state = AppState::Chat { widget: new_widget };
self.app_event_tx.send(AppEvent::RequestRedraw);
}
SlashCommand::Compact => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.clear_token_usage();
self.app_event_tx.send(AppEvent::CodexOp(Op::Compact));
}
}
SlashCommand::Quit => {
break;
}
@@ -383,6 +383,45 @@ impl App<'_> {
widget.add_diff_output(text);
}
}
#[cfg(debug_assertions)]
SlashCommand::TestApproval => {
use std::collections::HashMap;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::FileChange;
self.app_event_tx.send(AppEvent::CodexEvent(Event {
id: "1".to_string(),
// msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
// call_id: "1".to_string(),
// command: vec!["git".into(), "apply".into()],
// cwd: self.config.cwd.clone(),
// reason: Some("test".to_string()),
// }),
msg: EventMsg::ApplyPatchApprovalRequest(
ApplyPatchApprovalRequestEvent {
call_id: "1".to_string(),
changes: HashMap::from([
(
PathBuf::from("/tmp/test.txt"),
FileChange::Add {
content: "test".to_string(),
},
),
(
PathBuf::from("/tmp/test2.txt"),
FileChange::Update {
unified_diff: "+test\n-test2".to_string(),
move_path: None,
},
),
]),
reason: None,
grant_root: Some(PathBuf::from("/tmp")),
},
),
}));
}
},
AppEvent::DispatchAtCommand(at_command) => match at_command {
crate::at_command::AtCommand::ClipboardImage => {
@@ -444,22 +483,61 @@ impl App<'_> {
pub(crate) fn token_usage(&self) -> codex_core::protocol::TokenUsage {
match &self.app_state {
AppState::Chat { widget } => widget.token_usage().clone(),
AppState::Login { .. } | AppState::GitWarning { .. } => {
codex_core::protocol::TokenUsage::default()
}
AppState::GitWarning { .. } => codex_core::protocol::TokenUsage::default(),
}
}
fn draw_next_frame(&mut self, terminal: &mut tui::Tui) -> Result<()> {
// TODO: add a throttle to avoid redrawing too often
let screen_size = terminal.size()?;
let last_known_screen_size = terminal.last_known_screen_size;
if screen_size != last_known_screen_size {
let cursor_pos = terminal.get_cursor_position()?;
let last_known_cursor_pos = terminal.last_known_cursor_pos;
if cursor_pos.y != last_known_cursor_pos.y {
// The terminal was resized. The only point of reference we have for where our viewport
// was moved is the cursor position.
// NB this assumes that the cursor was not wrapped as part of the resize.
let cursor_delta = cursor_pos.y as i32 - last_known_cursor_pos.y as i32;
let new_viewport_area = terminal.viewport_area.offset(Offset {
x: 0,
y: cursor_delta,
});
terminal.set_viewport_area(new_viewport_area);
terminal.clear()?;
}
}
let size = terminal.size()?;
let desired_height = match &self.app_state {
AppState::Chat { widget } => widget.desired_height(size.width),
AppState::GitWarning { .. } => 10,
};
let mut area = terminal.viewport_area;
area.height = desired_height.min(size.height);
area.width = size.width;
if area.bottom() > size.height {
terminal
.backend_mut()
.scroll_region_up(0..area.top(), area.bottom() - size.height)?;
area.y = size.height - area.height;
}
if area != terminal.viewport_area {
terminal.clear()?;
terminal.set_viewport_area(area);
}
if !self.pending_history_lines.is_empty() {
crate::insert_history::insert_history_lines(
terminal,
self.pending_history_lines.clone(),
);
self.pending_history_lines.clear();
}
match &mut self.app_state {
AppState::Chat { widget } => {
terminal.draw(|frame| frame.render_widget_ref(&**widget, frame.area()))?;
}
AppState::Login { screen } => {
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
}
AppState::GitWarning { screen } => {
terminal.draw(|frame| frame.render_widget_ref(&*screen, frame.area()))?;
}
@@ -474,7 +552,6 @@ impl App<'_> {
AppState::Chat { widget } => {
widget.handle_key_event(key_event);
}
AppState::Login { screen } => screen.handle_key_event(key_event),
AppState::GitWarning { screen } => match screen.handle_key_event(key_event) {
GitWarningOutcome::Continue => {
// User accepted switch to chat view.
@@ -488,6 +565,7 @@ impl App<'_> {
self.app_event_tx.clone(),
args.initial_prompt,
args.initial_images,
args.enhanced_keys_supported,
));
self.app_state = AppState::Chat { widget };
self.app_event_tx.send(AppEvent::RequestRedraw);
@@ -505,21 +583,14 @@ impl App<'_> {
fn dispatch_paste_event(&mut self, pasted: String) {
match &mut self.app_state {
AppState::Chat { widget } => widget.handle_paste(pasted),
AppState::Login { .. } | AppState::GitWarning { .. } => {}
}
}
fn dispatch_scroll_event(&mut self, scroll_delta: i32) {
match &mut self.app_state {
AppState::Chat { widget } => widget.handle_scroll_delta(scroll_delta),
AppState::Login { .. } | AppState::GitWarning { .. } => {}
AppState::GitWarning { .. } => {}
}
}
fn dispatch_codex_event(&mut self, event: Event) {
match &mut self.app_state {
AppState::Chat { widget } => widget.handle_codex_event(event),
AppState::Login { .. } | AppState::GitWarning { .. } => {}
AppState::GitWarning { .. } => {}
}
}
}