Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
be557b2
enable opening prompt in /
sayan-oai Dec 4, 2025
a52486b
expand pending_pastes when opening editor
sayan-oai Dec 4, 2025
7ac9bce
add shortcut to footer and improve errors
sayan-oai Dec 4, 2025
8791b43
gate import to unix
sayan-oai Dec 4, 2025
acf929b
simplify errors, rm Option, disambiguate image placeholders
sayan-oai Dec 4, 2025
c8e45e9
add external editor footer
sayan-oai Dec 5, 2025
2c46e32
comment + clean up
sayan-oai Dec 5, 2025
e77f2a9
mv const to correct area
sayan-oai Dec 5, 2025
267e626
prevent shortcut while bottom pane in use
sayan-oai Dec 5, 2025
60e4892
fix leaky map; compute image placeholders on demand
sayan-oai Dec 8, 2025
db5e8ae
fix terminal flushing
sayan-oai Dec 9, 2025
85934a7
fix footer end calculation
sayan-oai Dec 9, 2025
0871623
whitespace
sayan-oai Dec 9, 2025
b8c6ce2
rework to set app state and avoid forced renders/cursor moves
sayan-oai Dec 10, 2025
a311b54
keep raw mode enabled
sayan-oai Dec 12, 2025
ea3b60f
rm cursor moving + clearing logic bc we stay in raw mode
sayan-oai Dec 12, 2025
841e8ac
clean up tests
sayan-oai Dec 12, 2025
2037863
cleanup
sayan-oai Dec 16, 2025
ce38e84
cleanup
sayan-oai Dec 17, 2025
a9aa090
cleanup + reorganize
sayan-oai Dec 19, 2025
eb7ca03
refactor terminal release + regain to single operation
sayan-oai Dec 19, 2025
52fce07
rework opening editor to release filehandle
sayan-oai Dec 19, 2025
eaabc7d
cleanup
sayan-oai Dec 20, 2025
ae6ba42
handle editors on windows
sayan-oai Dec 22, 2025
cc0cdb3
use winsplit for windows parsing
sayan-oai Dec 22, 2025
b17fbd5
Merge branch 'main' into dev/sayan/prompt-editor
sayan-oai Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ strum_macros = { workspace = true }
supports-color = { workspace = true }
tempfile = { workspace = true }
textwrap = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = [
"io-std",
"macros",
Expand All @@ -97,6 +98,14 @@ tokio-util = { workspace = true, features = ["time"] }
[target.'cfg(unix)'.dependencies]
libc = { workspace = true }

[target.'cfg(windows)'.dependencies]
which = { workspace = true }
windows-sys = { version = "0.52", features = [
"Win32_Foundation",
"Win32_System_Console",
] }
winsplit = "0.1"

# Clipboard support via `arboard` is not available on Android/Termux.
# Only include it for non-Android targets so the crate builds on Android.
[target.'cfg(not(target_os = "android"))'.dependencies]
Expand Down
92 changes: 92 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::ApprovalRequest;
use crate::chatwidget::ChatWidget;
use crate::chatwidget::ExternalEditorState;
use crate::diff_render::DiffSummary;
use crate::exec_command::strip_bash_lc_and_escape;
use crate::external_editor;
use crate::file_search::FileSearchManager;
use crate::history_cell;
use crate::history_cell::HistoryCell;
use crate::model_migration::ModelMigrationOutcome;
use crate::model_migration::migration_copy_for_models;
Expand Down Expand Up @@ -62,6 +65,8 @@ use tokio::sync::mpsc::unbounded_channel;
#[cfg(not(debug_assertions))]
use crate::history_cell::UpdateAvailableHistoryCell;

const EXTERNAL_EDITOR_HINT: &str = "Save and close external editor to continue.";

#[derive(Debug, Clone)]
pub struct AppExitInfo {
pub token_usage: TokenUsage,
Expand Down Expand Up @@ -542,6 +547,11 @@ impl App {
}
},
)?;
if self.chat_widget.external_editor_state() == ExternalEditorState::Requested {
self.chat_widget
.set_external_editor_state(ExternalEditorState::Active);
self.app_event_tx.send(AppEvent::LaunchExternalEditor);
}
}
}
}
Expand Down Expand Up @@ -796,6 +806,11 @@ impl App {
AppEvent::OpenFeedbackConsent { category } => {
self.chat_widget.open_feedback_consent(category);
}
AppEvent::LaunchExternalEditor => {
if self.chat_widget.external_editor_state() == ExternalEditorState::Active {
self.launch_external_editor(tui).await;
}
}
AppEvent::OpenWindowsSandboxEnablePrompt { preset } => {
self.chat_widget.open_windows_sandbox_enable_prompt(preset);
}
Expand Down Expand Up @@ -1148,6 +1163,68 @@ impl App {
self.config.model_reasoning_effort = effort;
}

async fn launch_external_editor(&mut self, tui: &mut tui::Tui) {
let editor_cmd = match external_editor::resolve_editor_command() {
Ok(cmd) => cmd,
Err(external_editor::EditorError::MissingEditor) => {
self.chat_widget
.add_to_history(history_cell::new_error_event(
"Cannot open external editor: set $VISUAL or $EDITOR".to_string(),
));
self.reset_external_editor_state(tui);
return;
}
Err(err) => {
self.chat_widget
.add_to_history(history_cell::new_error_event(format!(
"Failed to open editor: {err}",
)));
self.reset_external_editor_state(tui);
return;
}
};

let seed = self.chat_widget.composer_text_with_pending();
let editor_result = tui
.with_restored(tui::RestoreMode::KeepRaw, || async {
external_editor::run_editor(&seed, &editor_cmd).await
})
.await;
self.reset_external_editor_state(tui);

match editor_result {
Ok(new_text) => {
// Trim trailing whitespace
let cleaned = new_text.trim_end().to_string();
self.chat_widget.apply_external_edit(cleaned);
}
Err(err) => {
self.chat_widget
.add_to_history(history_cell::new_error_event(format!(
"Failed to open editor: {err}",
)));
}
}
tui.frame_requester().schedule_frame();
}

fn request_external_editor_launch(&mut self, tui: &mut tui::Tui) {
self.chat_widget
.set_external_editor_state(ExternalEditorState::Requested);
self.chat_widget.set_footer_hint_override(Some(vec![(
EXTERNAL_EDITOR_HINT.to_string(),
String::new(),
)]));
tui.frame_requester().schedule_frame();
}

fn reset_external_editor_state(&mut self, tui: &mut tui::Tui) {
self.chat_widget
.set_external_editor_state(ExternalEditorState::Closed);
self.chat_widget.set_footer_hint_override(None);
tui.frame_requester().schedule_frame();
}

async fn handle_key_event(&mut self, tui: &mut tui::Tui, key_event: KeyEvent) {
match key_event {
KeyEvent {
Expand All @@ -1161,6 +1238,21 @@ impl App {
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
tui.frame_requester().schedule_frame();
}
KeyEvent {
code: KeyCode::Char('g'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
// Only launch the external editor if there is no overlay and the bottom pane is not in use.
// Note that it can be launched while a task is running to enable editing while the previous turn is ongoing.
if self.overlay.is_none()
&& self.chat_widget.can_launch_external_editor()
&& self.chat_widget.external_editor_state() == ExternalEditorState::Closed
{
self.request_external_editor_launch(tui);
}
}
// Esc primes/advances backtracking only in normal (not working) mode
// with the composer focused and empty. In any other state, forward
// Esc so the active UI (e.g. status indicator, modals, popups)
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ pub(crate) enum AppEvent {
OpenFeedbackConsent {
category: FeedbackCategory,
},

/// Launch the external editor after a normal draw has completed.
LaunchExternalEditor,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
Loading
Loading