mirror of
https://github.com/openai/codex.git
synced 2026-03-05 21:45:28 +03:00
tui: use config snapshots for pane-resumed forks
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
use crate::app_backtrack::BacktrackState;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event::ExitMode;
|
||||
use crate::app_event::RealtimeAudioDeviceKind;
|
||||
use crate::app_event::ForkPanePlacement;
|
||||
use crate::app_event::RealtimeAudioDeviceKind;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -45,6 +45,7 @@ use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::CodexThread;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::codex::SessionSettingsUpdate;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigBuilder;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -98,6 +99,8 @@ use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -115,6 +118,7 @@ use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use tokio::task::JoinHandle;
|
||||
use toml::Value as TomlValue;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod pending_interactive_replay;
|
||||
|
||||
@@ -1425,8 +1429,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn fresh_session_config(&self) -> Config {
|
||||
let mut config = self.config.clone();
|
||||
config.service_tier = self.chat_widget.current_service_tier();
|
||||
let mut config = self.chat_widget.config_ref().clone();
|
||||
config.model = Some(self.chat_widget.current_model().to_string());
|
||||
config
|
||||
}
|
||||
|
||||
@@ -1444,16 +1448,62 @@ impl App {
|
||||
let Some(multiplexer) = terminal_info.multiplexer.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let description =
|
||||
match spawn_fork_in_new_pane(multiplexer, &forked_thread_id, placement).await {
|
||||
Ok(description) => description,
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Forked session created but failed to open a new pane: {err}"
|
||||
));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let thread_snapshot = forked_thread.config_snapshot().await;
|
||||
let current_collaboration_mode = self.chat_widget.current_collaboration_mode();
|
||||
let runtime_overrides = SessionSettingsUpdate {
|
||||
cwd: Some(thread_snapshot.cwd.clone()),
|
||||
approval_policy: Some(thread_snapshot.approval_policy),
|
||||
sandbox_policy: Some(thread_snapshot.sandbox_policy.clone()),
|
||||
windows_sandbox_level: None,
|
||||
collaboration_mode: Some(CollaborationMode {
|
||||
mode: self.chat_widget.active_collaboration_mode_kind(),
|
||||
settings: Settings {
|
||||
model: thread_snapshot.model.clone(),
|
||||
reasoning_effort: thread_snapshot.reasoning_effort,
|
||||
developer_instructions: current_collaboration_mode
|
||||
.settings
|
||||
.developer_instructions
|
||||
.clone(),
|
||||
},
|
||||
}),
|
||||
reasoning_summary: None,
|
||||
service_tier: Some(self.chat_widget.current_service_tier()),
|
||||
final_output_json_schema: None,
|
||||
personality: thread_snapshot.personality,
|
||||
app_server_client_name: None,
|
||||
};
|
||||
let runtime_overrides_path = match (|| -> io::Result<PathBuf> {
|
||||
let overrides_dir = self.config.codex_home.join("tmp").join("runtime-overrides");
|
||||
fs::create_dir_all(&overrides_dir)?;
|
||||
let path = overrides_dir.join(format!("{}.json", Uuid::new_v4()));
|
||||
let contents = serde_json::to_vec(&runtime_overrides).map_err(io::Error::other)?;
|
||||
fs::write(&path, contents)?;
|
||||
Ok(path)
|
||||
})() {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Forked session created but failed to prepare runtime overrides: {err}"
|
||||
));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let description = match spawn_fork_in_new_pane(
|
||||
multiplexer,
|
||||
&forked_thread_id,
|
||||
&runtime_overrides_path,
|
||||
placement,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(description) => description,
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Forked session created but failed to open a new pane: {err}"
|
||||
));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
self.suppressed_thread_created.insert(forked_thread_id);
|
||||
if let Err(err) = forked_thread.submit(Op::Shutdown).await {
|
||||
@@ -5036,13 +5086,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fresh_session_config_uses_current_service_tier() {
|
||||
async fn fresh_session_config_uses_live_chat_widget_state() {
|
||||
let mut app = make_test_app().await;
|
||||
app.chat_widget.set_model("gpt-test");
|
||||
app.chat_widget
|
||||
.set_service_tier(Some(codex_protocol::config_types::ServiceTier::Fast));
|
||||
|
||||
let config = app.fresh_session_config();
|
||||
|
||||
assert_eq!(config.model.as_deref(), Some("gpt-test"));
|
||||
assert_eq!(
|
||||
config.service_tier,
|
||||
Some(codex_protocol::config_types::ServiceTier::Fast)
|
||||
|
||||
@@ -3,6 +3,8 @@ use codex_core::terminal::Multiplexer;
|
||||
use codex_core::terminal::terminal_info;
|
||||
use codex_protocol::ThreadId;
|
||||
use shlex::try_join;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
@@ -14,14 +16,42 @@ pub(crate) struct MultiplexerSpawnConfig {
|
||||
}
|
||||
|
||||
fn codex_executable() -> PathBuf {
|
||||
std::env::current_exe().unwrap_or_else(|_| PathBuf::from("codex"))
|
||||
std::env::current_exe()
|
||||
.map(|path| resolve_codex_executable(&path))
|
||||
.unwrap_or_else(|_| PathBuf::from("codex"))
|
||||
}
|
||||
|
||||
fn resume_command_parts(exe: &Path, thread_id: &ThreadId) -> Vec<String> {
|
||||
fn resolve_codex_executable(current_exe: &Path) -> PathBuf {
|
||||
let Some(file_name) = current_exe.file_name().and_then(|name| name.to_str()) else {
|
||||
return PathBuf::from("codex");
|
||||
};
|
||||
if !file_name.starts_with("codex-tui") {
|
||||
return current_exe.to_path_buf();
|
||||
}
|
||||
|
||||
let sibling = if let Some((_, extension)) = file_name.rsplit_once('.') {
|
||||
current_exe.with_file_name(format!("codex.{extension}"))
|
||||
} else {
|
||||
current_exe.with_file_name("codex")
|
||||
};
|
||||
if sibling.is_file() {
|
||||
sibling
|
||||
} else {
|
||||
PathBuf::from("codex")
|
||||
}
|
||||
}
|
||||
|
||||
fn resume_command_parts(
|
||||
exe: &Path,
|
||||
thread_id: &ThreadId,
|
||||
runtime_overrides_path: &Path,
|
||||
) -> Vec<String> {
|
||||
vec![
|
||||
exe.display().to_string(),
|
||||
"resume".to_string(),
|
||||
thread_id.to_string(),
|
||||
"--runtime-overrides".to_string(),
|
||||
runtime_overrides_path.display().to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -86,9 +116,10 @@ fn fork_spawn_config(
|
||||
multiplexer: &Multiplexer,
|
||||
exe: &Path,
|
||||
thread_id: &ThreadId,
|
||||
runtime_overrides_path: &Path,
|
||||
placement: Option<ForkPanePlacement>,
|
||||
) -> MultiplexerSpawnConfig {
|
||||
let resume_command = resume_command_parts(exe, thread_id);
|
||||
let resume_command = resume_command_parts(exe, thread_id, runtime_overrides_path);
|
||||
match multiplexer {
|
||||
Multiplexer::Zellij {} => MultiplexerSpawnConfig {
|
||||
program: "zellij",
|
||||
@@ -138,21 +169,97 @@ pub(crate) fn validate_fork_placement(placement: Option<ForkPanePlacement>) -> R
|
||||
pub(crate) async fn spawn_fork_in_new_pane(
|
||||
multiplexer: &Multiplexer,
|
||||
thread_id: &ThreadId,
|
||||
runtime_overrides_path: &Path,
|
||||
placement: Option<ForkPanePlacement>,
|
||||
) -> Result<&'static str, String> {
|
||||
let exe = codex_executable();
|
||||
let config = fork_spawn_config(multiplexer, &exe, thread_id, placement);
|
||||
let config = fork_spawn_config(
|
||||
multiplexer,
|
||||
&exe,
|
||||
thread_id,
|
||||
runtime_overrides_path,
|
||||
placement,
|
||||
);
|
||||
let MultiplexerSpawnConfig {
|
||||
program,
|
||||
args,
|
||||
description,
|
||||
} = config;
|
||||
let status = tokio::task::spawn_blocking(move || Command::new(program).args(args).status())
|
||||
.await
|
||||
.map_err(|err| format!("failed to spawn {program} pane: {err}"))?;
|
||||
let status = match tokio::task::spawn_blocking(move || {
|
||||
Command::new(program).args(args).status()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(status) => status,
|
||||
Err(err) => {
|
||||
cleanup_runtime_overrides(runtime_overrides_path);
|
||||
return Err(format!("failed to spawn {program} pane: {err}"));
|
||||
}
|
||||
};
|
||||
match status {
|
||||
Ok(status) if status.success() => Ok(description),
|
||||
Ok(status) => Err(format!("{program} exited with status {status}")),
|
||||
Err(err) => Err(format!("failed to run {program}: {err}")),
|
||||
Ok(status) => {
|
||||
cleanup_runtime_overrides(runtime_overrides_path);
|
||||
Err(format!("{program} exited with status {status}"))
|
||||
}
|
||||
Err(err) => {
|
||||
cleanup_runtime_overrides(runtime_overrides_path);
|
||||
Err(format!("failed to run {program}: {err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_runtime_overrides(path: &Path) {
|
||||
if let Err(err) = fs::remove_file(path)
|
||||
&& err.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
tracing::warn!(path = %path.display(), error = %err, "failed to remove runtime overrides");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn resolve_codex_executable_rewrites_codex_tui_to_sibling_codex() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let current_exe = tmp.path().join("codex-tui");
|
||||
let sibling_codex = tmp.path().join("codex");
|
||||
std::fs::write(&sibling_codex, "").expect("write sibling codex");
|
||||
|
||||
let resolved = resolve_codex_executable(¤t_exe);
|
||||
|
||||
assert_eq!(resolved, sibling_codex);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_codex_executable_keeps_non_tui_binary() {
|
||||
let current_exe = PathBuf::from("/tmp/codex");
|
||||
|
||||
let resolved = resolve_codex_executable(¤t_exe);
|
||||
|
||||
assert_eq!(resolved, current_exe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_command_parts_include_runtime_overrides() {
|
||||
let exe = PathBuf::from("/tmp/codex");
|
||||
let thread_id = ThreadId::new();
|
||||
let runtime_overrides_path = PathBuf::from("/tmp/runtime-overrides.json");
|
||||
|
||||
let command = resume_command_parts(&exe, &thread_id, &runtime_overrides_path);
|
||||
|
||||
assert_eq!(
|
||||
command,
|
||||
vec![
|
||||
"/tmp/codex".to_string(),
|
||||
"resume".to_string(),
|
||||
thread_id.to_string(),
|
||||
"--runtime-overrides".to_string(),
|
||||
"/tmp/runtime-overrides.json".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user