Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions src-tauri/src/backend/app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const REQUEST_TIMEOUT: Duration = Duration::from_secs(300);

pub(crate) struct WorkspaceSession {
pub(crate) entry: WorkspaceEntry,
pub(crate) codex_args: Option<String>,
pub(crate) child: Mutex<Child>,
pub(crate) stdin: Mutex<ChildStdin>,
pub(crate) pending: Mutex<HashMap<u64, oneshot::Sender<Value>>>,
Expand Down Expand Up @@ -339,6 +340,7 @@ pub(crate) async fn spawn_workspace_session<E: EventSink>(

let session = Arc::new(WorkspaceSession {
entry: entry.clone(),
codex_args,
child: Mutex::new(child),
stdin: Mutex::new(stdin),
pending: Mutex::new(HashMap::new()),
Expand Down
26 changes: 26 additions & 0 deletions src-tauri/src/bin/codex_monitor_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,32 @@ impl DaemonState {
.await
}

async fn set_workspace_runtime_codex_args(
&self,
workspace_id: String,
codex_args: Option<String>,
client_version: String,
) -> Result<workspaces_core::WorkspaceRuntimeCodexArgsResult, String> {
workspaces_core::set_workspace_runtime_codex_args_core(
workspace_id,
codex_args,
&self.workspaces,
&self.sessions,
&self.app_settings,
move |entry, default_bin, next_args, codex_home| {
spawn_with_client(
self.event_sink.clone(),
client_version.clone(),
entry,
default_bin,
next_args,
codex_home,
)
},
)
.await
}

async fn get_app_settings(&self) -> AppSettings {
settings_core::get_app_settings_core(&self.app_settings).await
}
Expand Down
17 changes: 17 additions & 0 deletions src-tauri/src/bin/codex_monitor_daemon/rpc/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,23 @@ pub(super) async fn try_handle(
.map(|_| json!({ "ok": true })),
)
}
"set_workspace_runtime_codex_args" => {
let workspace_id = match parse_string(params, "workspaceId") {
Ok(value) => value,
Err(err) => return Some(Err(err)),
};
let codex_args = parse_optional_string(params, "codexArgs");
Some(
state
.set_workspace_runtime_codex_args(
workspace_id,
codex_args,
client_version.to_string(),
)
.await
.and_then(|value| serde_json::to_value(value).map_err(|e| e.to_string())),
)
}
"remove_workspace" => {
let id = match parse_string(params, "id") {
Ok(value) => value,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ pub fn run() {
workspaces::apply_worktree_changes,
workspaces::update_workspace_settings,
workspaces::update_workspace_codex_bin,
workspaces::set_workspace_runtime_codex_args,
codex::start_thread,
codex::send_user_message,
codex::turn_steer,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/remote_backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ fn can_retry_after_disconnect(method: &str) -> bool {
| "collaboration_mode_list"
| "connect_workspace"
| "experimental_feature_list"
| "set_workspace_runtime_codex_args"
| "file_read"
| "get_agents_settings"
| "get_config_model"
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/shared/workspaces_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod crud_persistence;
mod git_orchestration;
mod helpers;
mod io;
mod runtime_codex_args;
mod worktree;

pub(crate) use connect::connect_workspace_core;
Expand All @@ -16,6 +17,9 @@ pub(crate) use io::{
get_open_app_icon_core, list_workspace_files_core, open_workspace_in_core,
read_workspace_file_core,
};
pub(crate) use runtime_codex_args::{
set_workspace_runtime_codex_args_core, WorkspaceRuntimeCodexArgsResult,
};
pub(crate) use worktree::{
add_worktree_core, remove_worktree_core, rename_worktree_core, rename_worktree_upstream_core,
worktree_setup_mark_ran_core, worktree_setup_status_core,
Expand Down
263 changes: 263 additions & 0 deletions src-tauri/src/shared/workspaces_core/runtime_codex_args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
use std::collections::HashMap;
use std::future::Future;
use std::path::PathBuf;
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;

use crate::backend::app_server::WorkspaceSession;
use crate::codex::args::resolve_workspace_codex_args;
use crate::codex::home::resolve_workspace_codex_home;
use crate::shared::process_core::kill_child_process_tree;
use crate::types::{AppSettings, WorkspaceEntry};

use super::helpers::resolve_entry_and_parent;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WorkspaceRuntimeCodexArgsResult {
pub(crate) applied_codex_args: Option<String>,
pub(crate) respawned: bool,
}

pub(crate) async fn set_workspace_runtime_codex_args_core<F, Fut>(
workspace_id: String,
codex_args_override: Option<String>,
workspaces: &Mutex<HashMap<String, WorkspaceEntry>>,
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
app_settings: &Mutex<AppSettings>,
spawn_session: F,
) -> Result<WorkspaceRuntimeCodexArgsResult, String>
where
F: Fn(WorkspaceEntry, Option<String>, Option<String>, Option<PathBuf>) -> Fut,
Fut: Future<Output = Result<Arc<WorkspaceSession>, String>>,
{
let (entry, parent_entry) = resolve_entry_and_parent(workspaces, &workspace_id).await?;

let (default_bin, resolved_args) = {
let settings = app_settings.lock().await;
(
settings.codex_bin.clone(),
resolve_workspace_codex_args(&entry, parent_entry.as_ref(), Some(&settings)),
)
};

let target_args = codex_args_override
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.or(resolved_args);

// If we are not connected, we can't respawn. Treat this as a no-op success; callers
// should call again after connecting.
let current = sessions.lock().await.get(&entry.id).cloned();
let Some(current) = current else {
return Ok(WorkspaceRuntimeCodexArgsResult {
applied_codex_args: target_args,
respawned: false,
});
};

if current.codex_args == target_args {
return Ok(WorkspaceRuntimeCodexArgsResult {
applied_codex_args: target_args,
respawned: false,
});
}

let codex_home = resolve_workspace_codex_home(&entry, parent_entry.as_ref());
let new_session = spawn_session(entry.clone(), default_bin, target_args.clone(), codex_home).await?;
if let Some(old_session) = sessions.lock().await.insert(entry.id.clone(), new_session) {
let mut child = old_session.child.lock().await;
kill_child_process_tree(&mut child).await;
}

Ok(WorkspaceRuntimeCodexArgsResult {
applied_codex_args: target_args,
respawned: true,
})
}

#[cfg(test)]
mod tests {
use super::*;

use std::process::Stdio;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};

use tokio::process::Command;

use crate::types::{WorkspaceKind, WorkspaceSettings};

fn make_workspace_entry(id: &str) -> WorkspaceEntry {
WorkspaceEntry {
id: id.to_string(),
name: id.to_string(),
path: "/tmp".to_string(),
codex_bin: None,
kind: WorkspaceKind::Main,
parent_id: None,
worktree: None,
settings: WorkspaceSettings::default(),
}
}

fn make_session(entry: WorkspaceEntry, codex_args: Option<String>) -> WorkspaceSession {
let mut cmd = if cfg!(windows) {
let mut cmd = Command::new("cmd");
cmd.args(["/C", "more"]);
cmd
} else {
let mut cmd = Command::new("sh");
cmd.args(["-c", "cat"]);
cmd
};

cmd.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null());

let mut child = cmd.spawn().expect("spawn dummy child");
let stdin = child.stdin.take().expect("dummy child stdin");

WorkspaceSession {
entry,
codex_args,
child: Mutex::new(child),
stdin: Mutex::new(stdin),
pending: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(0),
background_thread_callbacks: Mutex::new(HashMap::new()),
}
}

#[test]
fn set_workspace_runtime_codex_args_is_noop_when_workspace_not_connected() {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let entry = make_workspace_entry("ws-1");
let workspaces = Mutex::new(HashMap::from([(entry.id.clone(), entry.clone())]));
let sessions = Mutex::new(HashMap::<String, Arc<WorkspaceSession>>::new());
let app_settings = Mutex::new(AppSettings::default());

let spawn_calls = Arc::new(AtomicUsize::new(0));
let spawn_calls_ref = spawn_calls.clone();

let result = set_workspace_runtime_codex_args_core(
entry.id.clone(),
Some(" --profile dev ".to_string()),
&workspaces,
&sessions,
&app_settings,
move |entry, _bin, args, _home| {
let spawn_calls_ref = spawn_calls_ref.clone();
async move {
spawn_calls_ref.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(make_session(entry, args)))
}
},
)
.await
.expect("core call succeeds");

assert_eq!(
result,
WorkspaceRuntimeCodexArgsResult {
applied_codex_args: Some("--profile dev".to_string()),
respawned: false
}
);
assert_eq!(spawn_calls.load(Ordering::SeqCst), 0);
});
}

#[test]
fn set_workspace_runtime_codex_args_is_noop_when_args_match() {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let entry = make_workspace_entry("ws-1");
let workspaces = Mutex::new(HashMap::from([(entry.id.clone(), entry.clone())]));
let current_session = Arc::new(make_session(entry.clone(), Some("--same".to_string())));
let sessions = Mutex::new(HashMap::from([(entry.id.clone(), current_session)]));
let app_settings = Mutex::new(AppSettings::default());

let spawn_calls = Arc::new(AtomicUsize::new(0));
let spawn_calls_ref = spawn_calls.clone();

let result = set_workspace_runtime_codex_args_core(
entry.id.clone(),
Some("--same".to_string()),
&workspaces,
&sessions,
&app_settings,
move |entry, _bin, args, _home| {
let spawn_calls_ref = spawn_calls_ref.clone();
async move {
spawn_calls_ref.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(make_session(entry, args)))
}
},
)
.await
.expect("core call succeeds");

assert_eq!(
result,
WorkspaceRuntimeCodexArgsResult {
applied_codex_args: Some("--same".to_string()),
respawned: false
}
);
assert_eq!(spawn_calls.load(Ordering::SeqCst), 0);
});
}

#[test]
fn set_workspace_runtime_codex_args_respawns_when_args_change() {
tokio::runtime::Runtime::new().unwrap().block_on(async {
let entry = make_workspace_entry("ws-1");
let workspaces = Mutex::new(HashMap::from([(entry.id.clone(), entry.clone())]));
let current_session = Arc::new(make_session(entry.clone(), Some("--old".to_string())));
let sessions = Mutex::new(HashMap::from([(entry.id.clone(), current_session)]));
let app_settings = Mutex::new(AppSettings::default());

let spawn_calls = Arc::new(AtomicUsize::new(0));
let spawn_calls_ref = spawn_calls.clone();

let result = set_workspace_runtime_codex_args_core(
entry.id.clone(),
Some("--new".to_string()),
&workspaces,
&sessions,
&app_settings,
move |entry, _bin, args, _home| {
let spawn_calls_ref = spawn_calls_ref.clone();
async move {
spawn_calls_ref.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(make_session(entry, args)))
}
},
)
.await
.expect("core call succeeds");

assert_eq!(
result,
WorkspaceRuntimeCodexArgsResult {
applied_codex_args: Some("--new".to_string()),
respawned: true
}
);
assert_eq!(spawn_calls.load(Ordering::SeqCst), 1);

let next = sessions
.lock()
.await
.get(&entry.id)
.expect("session updated")
.codex_args
.clone();
assert_eq!(next, Some("--new".to_string()));
});
}
}
Loading