Skip to content
Open
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
1 change: 1 addition & 0 deletions src-tauri/src/bin/codex_monitor_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ impl DaemonState {
let settings_path = config.data_dir.join("settings.json");
let workspaces = read_workspaces(&storage_path).unwrap_or_default();
let app_settings = read_settings(&settings_path).unwrap_or_default();
settings_core::apply_default_codex_home_env(&app_settings, None);
let daemon_mode = if config.orbit_url.is_some() {
"orbit".to_string()
} else {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/shared/codex_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ pub(crate) async fn list_threads_core(
"sortKey": sort_key,
// Keep spawned sub-agent sessions visible in thread/list so UI refreshes
// do not drop parent -> child sidebar relationships.
"sourceKinds": ["cli", "vscode", "subAgentThreadSpawn"]
"sourceKinds": ["cli", "vscode", "jetbrains", "subAgentThreadSpawn"]
});
session.send_request("thread/list", params).await
}
Expand Down
26 changes: 26 additions & 0 deletions src-tauri/src/shared/settings_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub(crate) async fn update_app_settings_core(
app_settings: &Mutex<AppSettings>,
settings_path: &PathBuf,
) -> Result<AppSettings, String> {
let previous = app_settings.lock().await.clone();
apply_default_codex_home_env(&settings, Some(&previous));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply CODEX_HOME override after settings write succeeds

update_app_settings_core mutates process state via apply_default_codex_home_env before write_settings can fail. In any I/O failure case (e.g., permissions or disk-full), the call returns an error but CODEX_HOME has already been changed/cleared, so later Codex operations run with an environment that no longer matches persisted AppSettings. This introduces user-visible drift and can route reads/writes to the wrong home until restart.

Useful? React with 👍 / 👎.

let _ = codex_config::write_collab_enabled(settings.experimental_collab_enabled);
let _ = codex_config::write_collaboration_modes_enabled(settings.collaboration_modes_enabled);
let _ = codex_config::write_steer_enabled(settings.steer_enabled);
Expand All @@ -59,6 +61,30 @@ pub(crate) async fn update_app_settings_core(
Ok(settings)
}

pub(crate) fn apply_default_codex_home_env(
settings: &AppSettings,
previous: Option<&AppSettings>,
) {
let next = settings
.default_codex_home
.as_ref()
.map(|value| value.trim())
.filter(|value| !value.is_empty())
.map(str::to_string);
if let Some(next) = next {
std::env::set_var("CODEX_HOME", next);
return;
}

let should_clear = previous
.and_then(|prior| prior.default_codex_home.as_ref())
.map(|value| value.trim())
.is_some_and(|value| !value.is_empty());
if should_clear {
std::env::remove_var("CODEX_HOME");
}
}

pub(crate) async fn update_remote_backend_token_core(
app_settings: &Mutex<AppSettings>,
settings_path: &PathBuf,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl AppState {
let settings_path = data_dir.join("settings.json");
let workspaces = read_workspaces(&storage_path).unwrap_or_default();
let app_settings = read_settings(&settings_path).unwrap_or_default();
crate::shared::settings_core::apply_default_codex_home_env(&app_settings, None);
Self {
workspaces: Mutex::new(workspaces),
sessions: Mutex::new(HashMap::new()),
Expand Down
24 changes: 23 additions & 1 deletion src-tauri/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,32 @@ async fn get_terminal_session(
.ok_or_else(|| "Terminal session not found".to_string())
}

#[cfg(target_os = "windows")]
fn shell_path() -> String {
std::env::var("COMSPEC").unwrap_or_else(|_| "powershell.exe".to_string())
}

#[cfg(not(target_os = "windows"))]
fn shell_path() -> String {
std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string())
}

#[cfg(target_os = "windows")]
fn configure_shell_args(cmd: &mut CommandBuilder) {
let shell = shell_path().to_ascii_lowercase();
if shell.contains("powershell") || shell.ends_with("pwsh.exe") {
cmd.arg("-NoLogo");
cmd.arg("-NoExit");
} else if shell.ends_with("cmd.exe") || shell.ends_with("\\cmd") {
cmd.arg("/K");
}
}

#[cfg(not(target_os = "windows"))]
fn configure_shell_args(cmd: &mut CommandBuilder) {
cmd.arg("-i");
}

fn resolve_locale() -> String {
let candidate = std::env::var("LC_ALL")
.or_else(|_| std::env::var("LANG"))
Expand Down Expand Up @@ -195,7 +217,7 @@ pub(crate) async fn terminal_open(

let mut cmd = CommandBuilder::new(shell_path());
cmd.cwd(cwd);
cmd.arg("-i");
configure_shell_args(&mut cmd);
cmd.env("TERM", "xterm-256color");
let locale = resolve_locale();
cmd.env("LANG", &locale);
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ pub(crate) struct AppSettings {
pub(crate) codex_bin: Option<String>,
#[serde(default, rename = "codexArgs")]
pub(crate) codex_args: Option<String>,
#[serde(default, rename = "defaultCodexHome")]
pub(crate) default_codex_home: Option<String>,
#[serde(default, rename = "backendMode")]
pub(crate) backend_mode: BackendMode,
#[serde(default, rename = "remoteBackendProvider")]
Expand Down Expand Up @@ -1150,6 +1152,7 @@ impl Default for AppSettings {
Self {
codex_bin: None,
codex_args: None,
default_codex_home: None,
backend_mode: default_backend_mode(),
remote_backend_provider: RemoteBackendProvider::Tcp,
remote_backend_host: default_remote_backend_host(),
Expand Down
28 changes: 28 additions & 0 deletions src/features/settings/components/sections/SettingsCodexSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type SettingsCodexSectionProps = {
onRefreshDefaultModels: () => void;
codexPathDraft: string;
codexArgsDraft: string;
codexHomeDraft: string;
codexDirty: boolean;
isSavingSettings: boolean;
doctorState: {
Expand Down Expand Up @@ -50,6 +51,7 @@ type SettingsCodexSectionProps = {
codexArgsOverrideDrafts: Record<string, string>;
onSetCodexPathDraft: Dispatch<SetStateAction<string>>;
onSetCodexArgsDraft: Dispatch<SetStateAction<string>>;
onSetCodexHomeDraft: Dispatch<SetStateAction<string>>;
onSetGlobalAgentsContent: (value: string) => void;
onSetGlobalConfigContent: (value: string) => void;
onSetCodexBinOverrideDrafts: Dispatch<SetStateAction<Record<string, string>>>;
Expand Down Expand Up @@ -129,6 +131,7 @@ export function SettingsCodexSection({
onRefreshDefaultModels,
codexPathDraft,
codexArgsDraft,
codexHomeDraft,
codexDirty,
isSavingSettings,
doctorState,
Expand All @@ -153,6 +156,7 @@ export function SettingsCodexSection({
codexArgsOverrideDrafts,
onSetCodexPathDraft,
onSetCodexArgsDraft,
onSetCodexHomeDraft,
onSetGlobalAgentsContent,
onSetGlobalConfigContent,
onSetCodexBinOverrideDrafts,
Expand Down Expand Up @@ -286,6 +290,30 @@ export function SettingsCodexSection({
</button>
</div>
<div className="settings-help">Leave empty to use the system PATH resolution.</div>
<label className="settings-field-label" htmlFor="codex-home">
Default CODEX_HOME
</label>
<div className="settings-field-row">
<input
id="codex-home"
className="settings-input"
value={codexHomeDraft}
placeholder="%LOCALAPPDATA%\\JetBrains\\IntelliJIdea2025.3\\aia\\codex"
onChange={(event) => onSetCodexHomeDraft(event.target.value)}
/>
<button
type="button"
className="ghost"
onClick={() => onSetCodexHomeDraft("")}
>
Clear
</button>
</div>
<div className="settings-help">
Optional default for session discovery. Useful for IntelliJ Codex threads when they live
outside <code>~/.codex</code>. Workspace <code>CODEX_HOME</code> overrides still take
priority.
</div>
<label className="settings-field-label" htmlFor="codex-args">
Default Codex args
</label>
Expand Down
4 changes: 4 additions & 0 deletions src/features/settings/hooks/useAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function buildDefaultSettings(): AppSettings {
return {
codexBin: null,
codexArgs: null,
defaultCodexHome: null,
backendMode: isMobile ? "remote" : "local",
remoteBackendProvider: "tcp",
remoteBackendHost: "127.0.0.1:4732",
Expand Down Expand Up @@ -130,6 +131,9 @@ function normalizeAppSettings(settings: AppSettings): AppSettings {
...settings,
codexBin: settings.codexBin?.trim() ? settings.codexBin.trim() : null,
codexArgs: settings.codexArgs?.trim() ? settings.codexArgs.trim() : null,
defaultCodexHome: settings.defaultCodexHome?.trim()
? settings.defaultCodexHome.trim()
: null,
uiScale: clampUiScale(settings.uiScale),
theme: allowedThemes.has(settings.theme) ? settings.theme : "system",
uiFontFamily: normalizeFontFamily(
Expand Down
14 changes: 13 additions & 1 deletion src/features/settings/hooks/useSettingsCodexSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type SettingsCodexSectionProps = {
onRefreshDefaultModels: () => void;
codexPathDraft: string;
codexArgsDraft: string;
codexHomeDraft: string;
codexDirty: boolean;
isSavingSettings: boolean;
doctorState: {
Expand Down Expand Up @@ -75,6 +76,7 @@ export type SettingsCodexSectionProps = {
codexArgsOverrideDrafts: Record<string, string>;
onSetCodexPathDraft: Dispatch<SetStateAction<string>>;
onSetCodexArgsDraft: Dispatch<SetStateAction<string>>;
onSetCodexHomeDraft: Dispatch<SetStateAction<string>>;
onSetGlobalAgentsContent: (value: string) => void;
onSetGlobalConfigContent: (value: string) => void;
onSetCodexBinOverrideDrafts: Dispatch<SetStateAction<Record<string, string>>>;
Expand Down Expand Up @@ -106,6 +108,7 @@ export const useSettingsCodexSection = ({
}: UseSettingsCodexSectionArgs): SettingsCodexSectionProps => {
const [codexPathDraft, setCodexPathDraft] = useState(appSettings.codexBin ?? "");
const [codexArgsDraft, setCodexArgsDraft] = useState(appSettings.codexArgs ?? "");
const [codexHomeDraft, setCodexHomeDraft] = useState(appSettings.defaultCodexHome ?? "");
const [codexBinOverrideDrafts, setCodexBinOverrideDrafts] = useState<
Record<string, string>
>({});
Expand Down Expand Up @@ -183,6 +186,10 @@ export const useSettingsCodexSection = ({
setCodexArgsDraft(appSettings.codexArgs ?? "");
}, [appSettings.codexArgs]);

useEffect(() => {
setCodexHomeDraft(appSettings.defaultCodexHome ?? "");
}, [appSettings.defaultCodexHome]);

useEffect(() => {
setCodexBinOverrideDrafts((prev) =>
buildWorkspaceOverrideDrafts(projects, prev, (workspace) => workspace.codex_bin ?? null),
Expand All @@ -205,9 +212,11 @@ export const useSettingsCodexSection = ({

const nextCodexBin = codexPathDraft.trim() ? codexPathDraft.trim() : null;
const nextCodexArgs = codexArgsDraft.trim() ? codexArgsDraft.trim() : null;
const nextDefaultCodexHome = codexHomeDraft.trim() ? codexHomeDraft.trim() : null;
const codexDirty =
nextCodexBin !== (appSettings.codexBin ?? null) ||
nextCodexArgs !== (appSettings.codexArgs ?? null);
nextCodexArgs !== (appSettings.codexArgs ?? null) ||
nextDefaultCodexHome !== (appSettings.defaultCodexHome ?? null);

const handleBrowseCodex = async () => {
const selection = await open({ multiple: false, directory: false });
Expand All @@ -224,6 +233,7 @@ export const useSettingsCodexSection = ({
...appSettings,
codexBin: nextCodexBin,
codexArgs: nextCodexArgs,
defaultCodexHome: nextDefaultCodexHome,
});
} finally {
setIsSavingSettings(false);
Expand Down Expand Up @@ -304,6 +314,7 @@ export const useSettingsCodexSection = ({
},
codexPathDraft,
codexArgsDraft,
codexHomeDraft,
codexDirty,
isSavingSettings,
doctorState,
Expand All @@ -328,6 +339,7 @@ export const useSettingsCodexSection = ({
codexArgsOverrideDrafts,
onSetCodexPathDraft: setCodexPathDraft,
onSetCodexArgsDraft: setCodexArgsDraft,
onSetCodexHomeDraft: setCodexHomeDraft,
onSetGlobalAgentsContent: setGlobalAgentsContent,
onSetGlobalConfigContent: setGlobalConfigContent,
onSetCodexBinOverrideDrafts: setCodexBinOverrideDrafts,
Expand Down
40 changes: 40 additions & 0 deletions src/features/threads/hooks/useThreadActions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,46 @@ describe("useThreadActions", () => {
expect(updateThreadParent).toHaveBeenCalledWith("parent-thread", ["child-thread"]);
});

it("matches thread cwd on Windows paths even when drive-letter casing differs", async () => {
const windowsWorkspace: WorkspaceInfo = {
...workspace,
path: "C:\\dev\\codexMon",
};
vi.mocked(listThreads).mockResolvedValue({
result: {
data: [
{
id: "thread-win-1",
cwd: "c:/dev/codexMon",
preview: "Windows thread",
updated_at: 5000,
},
],
nextCursor: null,
},
});
vi.mocked(getThreadTimestamp).mockReturnValue(5000);

const { result, dispatch } = renderActions();

await act(async () => {
await result.current.listThreadsForWorkspace(windowsWorkspace);
});

expect(dispatch).toHaveBeenCalledWith({
type: "setThreads",
workspaceId: "ws-1",
sortKey: "updated_at",
threads: [
{
id: "thread-win-1",
name: "Windows thread",
updatedAt: 5000,
},
],
});
});

it("preserves list state when requested", async () => {
vi.mocked(listThreads).mockResolvedValue({
result: {
Expand Down
17 changes: 16 additions & 1 deletion src/features/threads/utils/threadNormalize.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { normalizePlanUpdate } from "./threadNormalize";
import { normalizePlanUpdate, normalizeRootPath } from "./threadNormalize";

describe("normalizePlanUpdate", () => {
it("normalizes a plan when the payload uses an array", () => {
Expand Down Expand Up @@ -29,3 +29,18 @@ describe("normalizePlanUpdate", () => {
expect(normalizePlanUpdate("turn-3", "", { steps: [] })).toBeNull();
});
});

describe("normalizeRootPath", () => {
it("preserves significant leading and trailing whitespace", () => {
expect(normalizeRootPath(" /tmp/repo ")).toBe(" /tmp/repo ");
});

it("normalizes Windows drive-letter paths case-insensitively", () => {
expect(normalizeRootPath("C:\\Dev\\Repo\\")).toBe("c:/Dev/Repo");
expect(normalizeRootPath("c:/Dev/Repo")).toBe("c:/Dev/Repo");
});

it("normalizes UNC paths case-insensitively", () => {
expect(normalizeRootPath("\\\\SERVER\\Share\\Repo\\")).toBe("//server/share/repo");
});
});
12 changes: 11 additions & 1 deletion src/features/threads/utils/threadNormalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ export function normalizeStringList(value: unknown) {
}

export function normalizeRootPath(value: string) {
return value.replace(/\\/g, "/").replace(/\/+$/, "");
const normalized = value.replace(/\\/g, "/").replace(/\/+$/, "");
if (!normalized) {
return "";
}
if (/^[A-Za-z]:\//.test(normalized)) {
return `${normalized.charAt(0).toLowerCase()}${normalized.slice(1)}`;
}
if (normalized.startsWith("//")) {
return normalized.toLowerCase();
}
return normalized;
}

export function extractRpcErrorMessage(response: unknown) {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export type OpenAppTarget = {
export type AppSettings = {
codexBin: string | null;
codexArgs: string | null;
defaultCodexHome?: string | null;
backendMode: BackendMode;
remoteBackendProvider: RemoteBackendProvider;
remoteBackendHost: string;
Expand Down