Compare commits

...

1 Commits

Author SHA1 Message Date
Dylan Hurd
fbe77ef531 Remove exit reasons from TUI exit flow 2026-01-06 21:20:01 -08:00
2 changed files with 126 additions and 0 deletions

View File

@@ -31,6 +31,7 @@ use std::path::PathBuf;
use supports_color::Stream;
mod mcp_cmd;
mod terminal_history;
#[cfg(not(windows))]
mod wsl_paths;
@@ -309,9 +310,20 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
lines
}
fn update_terminal_history(exit_info: &AppExitInfo) {
let Some(conversation_id) = exit_info.conversation_id else {
return;
};
let resume_cmd = format!("codex resume {conversation_id}");
if let Err(err) = terminal_history::replace_last_codex_command(&resume_cmd) {
tracing::debug!("failed to update terminal history: {err}");
}
}
/// Handle the app exit and print the results. Optionally run the update action.
fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {
let update_action = exit_info.update_action;
update_terminal_history(&exit_info);
let color_enabled = supports_color::on(Stream::Stdout).is_some();
for line in format_exit_messages(exit_info, color_enabled) {
println!("{line}");

View File

@@ -0,0 +1,114 @@
use std::env;
use std::fs;
use std::io;
use std::path::PathBuf;
pub(crate) fn replace_last_codex_command(new_command: &str) -> io::Result<()> {
let Some(history_path) = resolve_history_path() else {
return Ok(());
};
let contents = match fs::read_to_string(&history_path) {
Ok(contents) => contents,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err),
};
let Some(updated) = update_history_contents(&contents, new_command) else {
return Ok(());
};
fs::write(history_path, updated)
}
fn resolve_history_path() -> Option<PathBuf> {
if let Some(history_file) = env::var_os("HISTFILE")
&& !history_file.is_empty()
{
return Some(PathBuf::from(history_file));
}
let home = PathBuf::from(env::var_os("HOME")?);
let shell = env::var("SHELL").unwrap_or_default();
if shell.contains("zsh") {
return Some(home.join(".zsh_history"));
}
if shell.contains("fish") {
let data_home = env::var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".local").join("share"));
return Some(data_home.join("fish").join("fish_history"));
}
if shell.contains("bash") {
return Some(home.join(".bash_history"));
}
Some(home.join(".bash_history"))
}
fn update_history_contents(contents: &str, new_command: &str) -> Option<String> {
let mut lines: Vec<String> = contents.split('\n').map(str::to_string).collect();
if replace_last_codex_line(&mut lines, new_command) {
Some(lines.join("\n"))
} else {
None
}
}
fn replace_last_codex_line(lines: &mut [String], new_command: &str) -> bool {
for line in lines.iter_mut().rev() {
if let Some(updated) = replace_zsh_history_line(line, new_command)
.or_else(|| replace_fish_history_line(line, new_command))
.or_else(|| replace_plain_history_line(line, new_command))
{
*line = updated;
return true;
}
}
false
}
fn replace_zsh_history_line(line: &str, new_command: &str) -> Option<String> {
if !line.starts_with(": ") {
return None;
}
let semicolon_index = line.find(';')?;
let (prefix, command) = line.split_at(semicolon_index + 1);
if !is_codex_command(command) {
return None;
}
let leading_len = command.len() - command.trim_start().len();
let leading = &command[..leading_len];
Some(format!("{prefix}{leading}{new_command}"))
}
fn replace_fish_history_line(line: &str, new_command: &str) -> Option<String> {
let trimmed = line.trim_start();
let prefix_len = line.len() - trimmed.len();
let prefix = &line[..prefix_len];
let (cmd_prefix, rest) = if let Some(rest) = trimmed.strip_prefix("- cmd: ") {
("- cmd: ", rest)
} else if let Some(rest) = trimmed.strip_prefix("cmd: ") {
("cmd: ", rest)
} else {
return None;
};
let rest = rest.trim_start();
let is_quoted = rest.starts_with('"');
let rest = rest.trim_start_matches('"');
if !is_codex_command(rest) {
return None;
}
let quote = if is_quoted { "\"" } else { "" };
Some(format!("{prefix}{cmd_prefix}{quote}{new_command}{quote}"))
}
fn replace_plain_history_line(line: &str, new_command: &str) -> Option<String> {
let trimmed = line.trim_start();
if !is_codex_command(trimmed) {
return None;
}
let prefix_len = line.len() - trimmed.len();
let prefix = &line[..prefix_len];
Some(format!("{prefix}{new_command}"))
}
fn is_codex_command(command: &str) -> bool {
let trimmed = command.trim_start();
trimmed == "codex" || trimmed.starts_with("codex ")
}