From 3e168d143be8367a4870c2d13634c3f0985d5963 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Fri, 16 Jan 2026 07:26:02 +0100 Subject: [PATCH 1/2] fix: move worktrees to app data --- README.md | 3 +- src-tauri/src/backend/app_server.rs | 6 +++- src-tauri/src/codex.rs | 11 ++++++- src-tauri/src/workspaces.rs | 50 +++++++++++++---------------- 4 files changed, 39 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 337faadd1..76242f3d8 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ src-tauri/ - Selecting a thread always calls `thread/resume` to refresh messages from disk. - CLI sessions appear if their `cwd` matches the workspace path; they are not live-streamed unless resumed. - The app uses `codex app-server` over stdio; see `src-tauri/src/lib.rs`. -- Worktree agents live in `.codex-worktrees/` and are removed on delete; the root repo gets a `.gitignore` entry. +- Codex sessions use the default Codex home (usually `~/.codex`); if a legacy `.codexmonitor/` exists in a workspace, it is used for that workspace. +- Worktree agents live under the app data directory (`worktrees/`); legacy `.codex-worktrees/` paths remain supported, and the app no longer edits repo `.gitignore` files. - UI state (panel sizes, reduced transparency toggle, recent thread activity) is stored in `localStorage`. - Custom prompts load from `$CODEX_HOME/prompts` (or `~/.codex/prompts`) with optional frontmatter description/argument hints. diff --git a/src-tauri/src/backend/app_server.rs b/src-tauri/src/backend/app_server.rs index 7a843d366..acfbf331a 100644 --- a/src-tauri/src/backend/app_server.rs +++ b/src-tauri/src/backend/app_server.rs @@ -2,7 +2,7 @@ use serde_json::{json, Value}; use std::collections::HashMap; use std::env; use std::io::ErrorKind; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -178,6 +178,7 @@ pub(crate) async fn spawn_workspace_session( default_codex_bin: Option, client_version: String, event_sink: E, + codex_home: Option, ) -> Result, String> { let codex_bin = entry .codex_bin @@ -189,6 +190,9 @@ pub(crate) async fn spawn_workspace_session( let mut command = build_codex_command_with_bin(codex_bin); command.current_dir(&entry.path); command.arg("app-server"); + if let Some(codex_home) = codex_home { + command.env("CODEX_HOME", codex_home); + } command.stdin(std::process::Stdio::piped()); command.stdout(std::process::Stdio::piped()); command.stderr(std::process::Stdio::piped()); diff --git a/src-tauri/src/codex.rs b/src-tauri/src/codex.rs index 9888cadd0..95b9c9fb7 100644 --- a/src-tauri/src/codex.rs +++ b/src-tauri/src/codex.rs @@ -1,5 +1,6 @@ use serde_json::{json, Map, Value}; use std::io::ErrorKind; +use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -20,10 +21,18 @@ pub(crate) async fn spawn_workspace_session( entry: WorkspaceEntry, default_codex_bin: Option, app_handle: AppHandle, + codex_home: Option, ) -> Result, String> { let client_version = app_handle.package_info().version.to_string(); let event_sink = TauriEventSink::new(app_handle); - spawn_workspace_session_inner(entry, default_codex_bin, client_version, event_sink).await + spawn_workspace_session_inner( + entry, + default_codex_bin, + client_version, + event_sink, + codex_home, + ) + .await } #[tauri::command] diff --git a/src-tauri/src/workspaces.rs b/src-tauri/src/workspaces.rs index 34dcd420c..f98fbfeb8 100644 --- a/src-tauri/src/workspaces.rs +++ b/src-tauri/src/workspaces.rs @@ -1,8 +1,7 @@ -use std::io::Write; use std::path::PathBuf; use ignore::WalkBuilder; -use tauri::{AppHandle, State}; +use tauri::{AppHandle, Manager, State}; use tokio::process::Command; use uuid::Uuid; @@ -14,6 +13,15 @@ use crate::types::{ }; use crate::utils::normalize_git_path; +fn resolve_codex_home(entry: &WorkspaceEntry, app: &AppHandle) -> Option { + let legacy_home = PathBuf::from(&entry.path).join(".codexmonitor"); + if legacy_home.is_dir() { + return Some(legacy_home); + } + let _ = app; + None +} + fn should_skip_dir(name: &str) -> bool { matches!( name, @@ -140,27 +148,6 @@ fn unique_worktree_path(base_dir: &PathBuf, name: &str) -> PathBuf { candidate } -fn ensure_worktree_ignored(repo_path: &PathBuf) -> Result<(), String> { - let ignore_path = repo_path.join(".gitignore"); - let entry = ".codex-worktrees/"; - let existing = std::fs::read_to_string(&ignore_path).unwrap_or_default(); - if existing.lines().any(|line| line.trim() == entry) { - return Ok(()); - } - let mut file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&ignore_path) - .map_err(|e| format!("Failed to update .gitignore: {e}"))?; - if !existing.ends_with('\n') && !existing.is_empty() { - file.write_all(b"\n") - .map_err(|e| format!("Failed to update .gitignore: {e}"))?; - } - file.write_all(format!("{entry}\n").as_bytes()) - .map_err(|e| format!("Failed to update .gitignore: {e}"))?; - Ok(()) -} - #[tauri::command] pub(crate) async fn list_workspaces( state: State<'_, AppState>, @@ -212,7 +199,8 @@ pub(crate) async fn add_workspace( let settings = state.app_settings.lock().await; settings.codex_bin.clone() }; - let session = spawn_workspace_session(entry.clone(), default_bin, app).await?; + let codex_home = resolve_codex_home(&entry, &app); + let session = spawn_workspace_session(entry.clone(), default_bin, app, codex_home).await?; { let mut workspaces = state.workspaces.lock().await; workspaces.insert(entry.id.clone(), entry.clone()); @@ -262,10 +250,14 @@ pub(crate) async fn add_worktree( return Err("Cannot create a worktree from another worktree.".to_string()); } - let worktree_root = PathBuf::from(&parent_entry.path).join(".codex-worktrees"); + let worktree_root = app + .path() + .app_data_dir() + .map_err(|e| format!("Failed to resolve app data dir: {e}"))? + .join("worktrees") + .join(&parent_entry.id); std::fs::create_dir_all(&worktree_root) .map_err(|e| format!("Failed to create worktree directory: {e}"))?; - ensure_worktree_ignored(&PathBuf::from(&parent_entry.path))?; let safe_name = sanitize_worktree_name(branch); let worktree_path = unique_worktree_path(&worktree_root, &safe_name); @@ -303,7 +295,8 @@ pub(crate) async fn add_worktree( let settings = state.app_settings.lock().await; settings.codex_bin.clone() }; - let session = spawn_workspace_session(entry.clone(), default_bin, app).await?; + let codex_home = resolve_codex_home(&entry, &app); + let session = spawn_workspace_session(entry.clone(), default_bin, app, codex_home).await?; { let mut workspaces = state.workspaces.lock().await; workspaces.insert(entry.id.clone(), entry.clone()); @@ -523,7 +516,8 @@ pub(crate) async fn connect_workspace( let settings = state.app_settings.lock().await; settings.codex_bin.clone() }; - let session = spawn_workspace_session(entry.clone(), default_bin, app).await?; + let codex_home = resolve_codex_home(&entry, &app); + let session = spawn_workspace_session(entry.clone(), default_bin, app, codex_home).await?; state.sessions.lock().await.insert(entry.id, session); Ok(()) } From 06805a27fdcd57a38d66997165910de17afcfad1 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Fri, 16 Jan 2026 07:31:13 +0100 Subject: [PATCH 2/2] Update workspaces.rs --- src-tauri/src/workspaces.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/workspaces.rs b/src-tauri/src/workspaces.rs index f98fbfeb8..9e75e8d30 100644 --- a/src-tauri/src/workspaces.rs +++ b/src-tauri/src/workspaces.rs @@ -13,12 +13,19 @@ use crate::types::{ }; use crate::utils::normalize_git_path; -fn resolve_codex_home(entry: &WorkspaceEntry, app: &AppHandle) -> Option { +fn resolve_codex_home(entry: &WorkspaceEntry, parent_path: Option<&str>) -> Option { + if entry.kind.is_worktree() { + if let Some(parent_path) = parent_path { + let legacy_home = PathBuf::from(parent_path).join(".codexmonitor"); + if legacy_home.is_dir() { + return Some(legacy_home); + } + } + } let legacy_home = PathBuf::from(&entry.path).join(".codexmonitor"); if legacy_home.is_dir() { return Some(legacy_home); } - let _ = app; None } @@ -199,7 +206,7 @@ pub(crate) async fn add_workspace( let settings = state.app_settings.lock().await; settings.codex_bin.clone() }; - let codex_home = resolve_codex_home(&entry, &app); + let codex_home = resolve_codex_home(&entry, None); let session = spawn_workspace_session(entry.clone(), default_bin, app, codex_home).await?; { let mut workspaces = state.workspaces.lock().await; @@ -295,7 +302,7 @@ pub(crate) async fn add_worktree( let settings = state.app_settings.lock().await; settings.codex_bin.clone() }; - let codex_home = resolve_codex_home(&entry, &app); + let codex_home = resolve_codex_home(&entry, Some(&parent_entry.path)); let session = spawn_workspace_session(entry.clone(), default_bin, app, codex_home).await?; { let mut workspaces = state.workspaces.lock().await; @@ -504,11 +511,19 @@ pub(crate) async fn connect_workspace( state: State<'_, AppState>, app: AppHandle, ) -> Result<(), String> { - let entry = { + let (entry, parent_path) = { let workspaces = state.workspaces.lock().await; workspaces .get(&id) .cloned() + .map(|entry| { + let parent_path = entry + .parent_id + .as_ref() + .and_then(|parent_id| workspaces.get(parent_id)) + .map(|parent| parent.path.clone()); + (entry, parent_path) + }) .ok_or("workspace not found")? }; @@ -516,7 +531,7 @@ pub(crate) async fn connect_workspace( let settings = state.app_settings.lock().await; settings.codex_bin.clone() }; - let codex_home = resolve_codex_home(&entry, &app); + let codex_home = resolve_codex_home(&entry, parent_path.as_deref()); let session = spawn_workspace_session(entry.clone(), default_bin, app, codex_home).await?; state.sessions.lock().await.insert(entry.id, session); Ok(())