Compare commits

...

2 Commits

Author SHA1 Message Date
Rakan El Khalil
24d863ba69 Open /fork in iTerm on macOS 2026-04-02 12:55:43 -07:00
Rakan El Khalil
cca170f8c1 Open /fork in iTerm on macOS 2026-04-02 12:44:51 -07:00
2 changed files with 138 additions and 5 deletions

View File

@@ -153,12 +153,15 @@ use uuid::Uuid;
mod agent_navigation;
mod app_server_adapter;
mod app_server_requests;
mod fork_terminal;
mod loaded_threads;
mod pending_interactive_replay;
use self::agent_navigation::AgentNavigationDirection;
use self::agent_navigation::AgentNavigationState;
use self::app_server_requests::PendingAppServerRequests;
#[cfg(target_os = "macos")]
use self::fork_terminal::spawn_fork_in_terminal;
use self::loaded_threads::find_loaded_subagent_threads_for_primary;
use self::pending_interactive_replay::PendingInteractiveReplayState;
@@ -4086,16 +4089,58 @@ impl App {
/*inc*/ 1,
&[("source", "slash_command")],
);
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.thread_id(),
self.chat_widget.thread_name(),
);
self.chat_widget
.add_plain_history_lines(vec!["/fork".magenta().into()]);
if let Some(thread_id) = self.chat_widget.thread_id() {
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.thread_id(),
self.chat_widget.thread_name(),
);
self.refresh_in_memory_config_from_disk_best_effort("forking the thread")
.await;
#[cfg(target_os = "macos")]
if let Some(path) = self.chat_widget.rollout_path()
&& path.exists()
{
match crate::resolve_session_thread_id(
path.as_path(),
/*id_str_if_uuid*/ None,
)
.await
{
Some(fork_thread_id) => {
match spawn_fork_in_terminal(self, tui, fork_thread_id).await {
Ok(()) => {
self.chat_widget.add_to_history(
history_cell::new_info_event(
"Opened fork in a new iTerm window.".to_string(),
Some("Current session left unchanged.".to_string()),
),
);
tui.frame_requester().schedule_frame();
return Ok(AppRunControl::Continue);
}
Err(err) => {
let path_display = path.display();
self.chat_widget.add_error_message(format!(
"Failed to open forked session in iTerm from {path_display}: {err}"
));
tui.frame_requester().schedule_frame();
return Ok(AppRunControl::Continue);
}
}
}
None => {
let path_display = path.display();
self.chat_widget.add_error_message(format!(
"Failed to read session metadata from {path_display}."
));
tui.frame_requester().schedule_frame();
return Ok(AppRunControl::Continue);
}
}
}
match app_server.fork_thread(self.config.clone(), thread_id).await {
Ok(forked) => {
self.shutdown_current_thread(app_server).await;

View File

@@ -0,0 +1,88 @@
#![cfg(target_os = "macos")]
use codex_protocol::ThreadId;
use color_eyre::eyre::Result;
use super::App;
use crate::tui;
fn append_config_override(args: &mut Vec<String>, key: &str, value: String) {
args.push("-c".to_string());
args.push(format!("{key}={value}"));
}
pub(super) async fn spawn_fork_in_terminal(
app: &App,
tui: &mut tui::Tui,
thread_id: ThreadId,
) -> Result<()> {
let program = std::env::current_exe()?.to_string_lossy().into_owned();
let mut args = vec!["fork".to_string(), thread_id.to_string()];
for (key, value) in &app.cli_kv_overrides {
append_config_override(&mut args, key, value.to_string());
}
if let Some(profile) = app.active_profile.as_ref() {
args.push("-p".to_string());
args.push(profile.clone());
}
let cwd = app.config.cwd.display().to_string();
args.push("-C".to_string());
args.push(cwd.clone());
args.push("-m".to_string());
args.push(app.chat_widget.current_model().to_string());
if let Some(effort) = app.config.model_reasoning_effort {
append_config_override(&mut args, "model_reasoning_effort", effort.to_string());
}
if let Some(policy) = app.runtime_approval_policy_override.as_ref()
&& let Ok(value) = toml::Value::try_from(*policy)
{
append_config_override(&mut args, "approval_policy", value.to_string());
}
if let Some(policy) = app.runtime_sandbox_policy_override.as_ref() {
let sandbox_mode = match policy {
codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } => "read-only",
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => "workspace-write",
codex_protocol::protocol::SandboxPolicy::DangerFullAccess
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => {
"danger-full-access"
}
};
append_config_override(&mut args, "sandbox_mode", sandbox_mode.to_string());
}
let command =
shlex::try_join(std::iter::once(program.as_str()).chain(args.iter().map(String::as_str)))
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
let shell_command = format!(
"cd {} && exec {command}",
shlex::try_join(std::iter::once(cwd.as_str()))
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?,
);
let command = shlex::try_join(["/bin/sh", "-lc", shell_command.as_str()])
.map_err(|err| color_eyre::eyre::eyre!(err.to_string()))?;
tui.with_restored(tui::RestoreMode::KeepRaw, || async move {
for app_name in ["iTerm2", "iTerm"] {
let status = tokio::process::Command::new("osascript")
.arg("-e")
.arg("on run argv")
.arg("-e")
.arg(format!(
"tell application \"{app_name}\" to create window with default profile command (item 1 of argv)"
))
.arg("-e")
.arg("end run")
.arg(&command)
.status()
.await?;
if status.success() {
return Ok(());
}
}
Err(color_eyre::eyre::eyre!("failed to open iTerm"))
})
.await
}