codex-rs(tui): restore EditPrompt arm and clear merge conflict markers in chat_composer

This commit is contained in:
Rai (Michael Pokorny)
2025-06-24 18:21:53 -07:00
parent 30989812e6
commit d5668f158d
7 changed files with 65 additions and 32 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -779,6 +779,7 @@ dependencies = [
"shlex",
"strum 0.27.1",
"strum_macros 0.27.1",
"tempfile",
"tokio",
"tracing",
"tracing-appender",

View File

@@ -415,4 +415,8 @@ disable_mouse_capture = true # defaults to `false`
# The composer will expand up to this many lines; additional content will enable
# an internal scrollbar.
composer_max_rows = 10 # defaults to `10`
# Command used to launch an external editor for composing the chat prompt.
# Defaults to `$VISUAL`, then `$EDITOR`, falling back to `nvim`.
editor = "${VISUAL:-${EDITOR:-nvim}}"
```

View File

@@ -95,17 +95,27 @@ pub struct Tui {
/// an internal scrollbar.
#[serde(default = "default_composer_max_rows")]
pub composer_max_rows: usize,
/// Command used to launch the external editor for editing the chat prompt.
/// Defaults to the `VISUAL` or `EDITOR` environment variable, falling back to `nvim`.
#[serde(default = "default_editor")]
pub editor: String,
}
fn default_composer_max_rows() -> usize {
10
}
/// Default editor: `$VISUAL`, then `$EDITOR`, falling back to `nvim`.
fn default_editor() -> String {
std::env::var("VISUAL").or_else(|_| std::env::var("EDITOR")).unwrap_or_else(|_| "nvim".into())
}
impl Default for Tui {
fn default() -> Self {
Self {
disable_mouse_capture: Default::default(),
composer_max_rows: default_composer_max_rows(),
editor: default_editor(),
}
}
}

View File

@@ -39,6 +39,7 @@ serde_json = { version = "1", features = ["preserve_order"] }
shlex = "1.3.0"
strum = "0.27.1"
strum_macros = "0.27.1"
tempfile = "3"
tokio = { version = "1", features = [
"io-std",
"macros",

View File

@@ -383,6 +383,9 @@ impl<'a> App<'a> {
tracing::error!("Failed to toggle mouse mode: {e}");
}
}
SlashCommand::EditPrompt => {
// External-editor prompt handled inline by the composer; no-op here.
}
SlashCommand::Quit => {
break;
}

View File

@@ -130,30 +130,8 @@ impl ChatComposer<'_> {
}
(InputResult::None, true)
}
Input {
key: Key::Enter,
shift: false,
alt: false,
ctrl: false,
} => {
Input { key: Key::Enter, shift: false, alt: false, ctrl: false } => {
if let Some(cmd) = popup.selected_command() {
let first_line = self.textarea.lines().first().map_or("", |v| v).to_string();
let args = first_line
.strip_prefix(&format!("/{}", cmd.command()))
.unwrap_or("")
.trim();
if (*cmd == SlashCommand::MountAdd || *cmd == SlashCommand::MountRemove)
&& !args.is_empty()
{
let evt = if *cmd == SlashCommand::MountAdd {
AppEvent::InlineMountAdd(args.to_string())
} else {
AppEvent::InlineMountRemove(args.to_string())
};
self.app_event_tx.send(evt);
} else {
self.app_event_tx.send(AppEvent::DispatchCommand(*cmd));
}
self.textarea.select_all();
self.textarea.cut();
self.command_popup = None;
@@ -214,18 +192,16 @@ impl ChatComposer<'_> {
(InputResult::Submitted(text), true)
}
}
Input {
key: Key::Enter, ..
}
| Input {
key: Key::Char('j'),
ctrl: true,
alt: false,
shift: false,
} => {
Input { key: Key::Enter, .. }
| Input { key: Key::Char('j'), ctrl: true, alt: false, shift: false } => {
self.textarea.insert_newline();
(InputResult::None, true)
}
Input { key: Key::Char('e'), ctrl: true, alt: false, shift: false } => {
// Launch external editor for prompt drafting
self.open_external_editor();
(InputResult::None, true)
}
input => self.handle_input_basic(input),
}
}
@@ -236,6 +212,40 @@ impl ChatComposer<'_> {
(InputResult::None, true)
}
/// Launch an external editor on a temporary file pre-populated with the current draft,
/// then reload the edited contents back into the textarea on exit.
pub fn open_external_editor(&mut self) {
use std::io::Write;
use std::process::Command;
// Dump current draft to a temp file
let content = self.textarea.lines().join("\n");
let mut tmp = match tempfile::NamedTempFile::new() {
Ok(f) => f,
Err(e) => {
tracing::error!("failed to create temp file for editor: {e}");
return;
}
};
if let Err(e) = write!(tmp, "{}", content) {
tracing::error!("failed to write to temp file for editor: {e}");
return;
}
let path = tmp.path();
// Determine editor: VISUAL > EDITOR > nvim
let editor = std::env::var("VISUAL").or_else(|_| std::env::var("EDITOR")).unwrap_or_else(|_| "nvim".into());
// Launch editor and wait for exit
if let Err(e) = Command::new(editor).arg(path).status() {
tracing::error!("failed to launch editor: {e}");
return;
}
// Read back edited contents (fall back to original on error)
let new_text = std::fs::read_to_string(path).unwrap_or(content);
// Replace textarea contents
self.textarea.select_all();
self.textarea.cut();
let _ = self.textarea.insert_str(new_text);
}
/// Synchronize `self.command_popup` with the current text in the
/// textarea. This must be called after every modification that can change
/// the text so the popup is shown/updated/hidden as appropriate.

View File

@@ -14,6 +14,8 @@ use strum_macros::IntoStaticStr;
pub enum SlashCommand {
New,
ToggleMouseMode,
/// Launch the external editor to edit the current prompt draft.
EditPrompt,
Quit,
/// Add a dynamic mount (host path → container path).
MountAdd,
@@ -28,6 +30,8 @@ impl SlashCommand {
SlashCommand::New => "Start a new chat.",
SlashCommand::ToggleMouseMode =>
"Toggle mouse mode (enable for scrolling, disable for text selection)",
SlashCommand::EditPrompt =>
"Open external editor to edit the current prompt.",
SlashCommand::Quit => "Exit the application.",
SlashCommand::MountAdd => "Add a mount: host path → container path.",
SlashCommand::MountRemove => "Remove a mount by container path.",