Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
104 changes: 102 additions & 2 deletions src-tauri/src/bin/codex_monitor_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,26 @@ use backend::events::{AppServerEvent, EventSink, TerminalOutput};
use storage::{read_settings, read_workspaces, write_settings, write_workspaces};
use types::{
AppSettings, WorkspaceEntry, WorkspaceInfo, WorkspaceKind, WorkspaceSettings, WorktreeInfo,
WorktreeSetupStatus,
};

const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:4732";
const WORKTREE_SETUP_MARKERS_DIR: &str = "worktree-setup";
const WORKTREE_SETUP_MARKER_EXT: &str = "ran";

fn worktree_setup_marker_path(data_dir: &PathBuf, workspace_id: &str) -> PathBuf {
data_dir
.join(WORKTREE_SETUP_MARKERS_DIR)
.join(format!("{workspace_id}.{WORKTREE_SETUP_MARKER_EXT}"))
}

fn normalize_setup_script(script: Option<String>) -> Option<String> {
match script {
Some(value) if value.trim().is_empty() => None,
Some(value) => Some(value),
None => None,
}
}

#[derive(Clone)]
struct DaemonEventSink {
Expand Down Expand Up @@ -272,7 +289,12 @@ impl DaemonState {
worktree: Some(WorktreeInfo {
branch: branch.to_string(),
}),
settings: WorkspaceSettings::default(),
settings: WorkspaceSettings {
worktree_setup_script: normalize_setup_script(
parent_entry.settings.worktree_setup_script.clone(),
),
..WorkspaceSettings::default()
},
};

let (default_bin, codex_args) = {
Expand Down Expand Up @@ -320,6 +342,51 @@ impl DaemonState {
})
}

async fn worktree_setup_status(&self, workspace_id: String) -> Result<WorktreeSetupStatus, String> {
let entry = {
let workspaces = self.workspaces.lock().await;
workspaces
.get(&workspace_id)
.cloned()
.ok_or_else(|| "workspace not found".to_string())?
};

let script = normalize_setup_script(entry.settings.worktree_setup_script.clone());
let marker_exists = if entry.kind.is_worktree() {
worktree_setup_marker_path(&self.data_dir, &entry.id).exists()
} else {
false
};
let should_run = entry.kind.is_worktree() && script.is_some() && !marker_exists;

Ok(WorktreeSetupStatus { should_run, script })
}

async fn worktree_setup_mark_ran(&self, workspace_id: String) -> Result<(), String> {
let entry = {
let workspaces = self.workspaces.lock().await;
workspaces
.get(&workspace_id)
.cloned()
.ok_or_else(|| "workspace not found".to_string())?
};
if !entry.kind.is_worktree() {
return Err("Not a worktree workspace.".to_string());
}
let marker_path = worktree_setup_marker_path(&self.data_dir, &entry.id);
if let Some(parent) = marker_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|err| format!("Failed to prepare worktree marker directory: {err}"))?;
}
let ran_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0);
std::fs::write(&marker_path, format!("ran_at={ran_at}\n"))
.map_err(|err| format!("Failed to write worktree setup marker: {err}"))?;
Ok(())
}

async fn remove_workspace(&self, id: String) -> Result<(), String> {
let (entry, child_worktrees) = {
let workspaces = self.workspaces.lock().await;
Expand Down Expand Up @@ -688,12 +755,16 @@ impl DaemonState {
settings: WorkspaceSettings,
client_version: String,
) -> Result<WorkspaceInfo, String> {
let mut settings = settings;
settings.worktree_setup_script = normalize_setup_script(settings.worktree_setup_script);

let (
previous_entry,
entry_snapshot,
parent_entry,
previous_codex_home,
previous_codex_args,
previous_worktree_setup_script,
child_entries,
) = {
let mut workspaces = self.workspaces.lock().await;
Expand All @@ -703,6 +774,7 @@ impl DaemonState {
.ok_or_else(|| "workspace not found".to_string())?;
let previous_codex_home = previous_entry.settings.codex_home.clone();
let previous_codex_args = previous_entry.settings.codex_args.clone();
let previous_worktree_setup_script = previous_entry.settings.worktree_setup_script.clone();
let entry_snapshot = match workspaces.get_mut(&id) {
Some(entry) => {
entry.settings = settings.clone();
Expand All @@ -726,12 +798,15 @@ impl DaemonState {
parent_entry,
previous_codex_home,
previous_codex_args,
previous_worktree_setup_script,
child_entries,
)
};

let codex_home_changed = previous_codex_home != entry_snapshot.settings.codex_home;
let codex_args_changed = previous_codex_args != entry_snapshot.settings.codex_args;
let worktree_setup_script_changed =
previous_worktree_setup_script != entry_snapshot.settings.worktree_setup_script;
let connected = self.sessions.lock().await.contains_key(&id);
if connected && (codex_home_changed || codex_args_changed) {
let rollback_entry = previous_entry.clone();
Expand Down Expand Up @@ -778,7 +853,7 @@ impl DaemonState {
if codex_home_changed || codex_args_changed {
let app_settings = self.app_settings.lock().await.clone();
let default_bin = app_settings.codex_bin.clone();
for child in child_entries {
for child in &child_entries {
let connected = self.sessions.lock().await.contains_key(&child.id);
if !connected {
continue;
Expand Down Expand Up @@ -832,6 +907,21 @@ impl DaemonState {
}
}
}
if worktree_setup_script_changed && !entry_snapshot.kind.is_worktree() {
let child_ids = child_entries
.iter()
.map(|child| child.id.clone())
.collect::<Vec<_>>();
if !child_ids.is_empty() {
let mut workspaces = self.workspaces.lock().await;
for child_id in child_ids {
if let Some(child) = workspaces.get_mut(&child_id) {
child.settings.worktree_setup_script =
entry_snapshot.settings.worktree_setup_script.clone();
}
}
}
}

let list: Vec<_> = {
let workspaces = self.workspaces.lock().await;
Expand Down Expand Up @@ -1770,6 +1860,16 @@ async fn handle_rpc_request(
.await?;
serde_json::to_value(workspace).map_err(|err| err.to_string())
}
"worktree_setup_status" => {
let workspace_id = parse_string(&params, "workspaceId")?;
let status = state.worktree_setup_status(workspace_id).await?;
serde_json::to_value(status).map_err(|err| err.to_string())
}
"worktree_setup_mark_ran" => {
let workspace_id = parse_string(&params, "workspaceId")?;
state.worktree_setup_mark_ran(workspace_id).await?;
Ok(json!({ "ok": true }))
}
"connect_workspace" => {
let id = parse_string(&params, "id")?;
state.connect_workspace(id, client_version).await?;
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ pub fn run() {
workspaces::add_workspace,
workspaces::add_clone,
workspaces::add_worktree,
workspaces::worktree_setup_status,
workspaces::worktree_setup_mark_ran,
workspaces::remove_workspace,
workspaces::remove_worktree,
workspaces::rename_worktree,
Expand Down
9 changes: 9 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,15 @@ pub(crate) struct WorkspaceSettings {
pub(crate) codex_args: Option<String>,
#[serde(default, rename = "launchScript")]
pub(crate) launch_script: Option<String>,
#[serde(default, rename = "worktreeSetupScript")]
pub(crate) worktree_setup_script: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct WorktreeSetupStatus {
#[serde(rename = "shouldRun")]
pub(crate) should_run: bool,
pub(crate) script: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down
Loading