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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ dist-ssr
CodexMonitor.zip
.codex-worktrees/
.codexmonitor/
.agent/
.agents/
.claude/
.cursor/
public/assets/material-icons/

# Nix
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/bin/codex_monitor_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1205,7 +1205,11 @@ impl DaemonState {
codex_aux_core::codex_doctor_core(&self.app_settings, codex_bin, codex_args).await
}

async fn generate_commit_message(&self, workspace_id: String) -> Result<String, String> {
async fn generate_commit_message(
&self,
workspace_id: String,
commit_message_model_id: Option<String>,
) -> Result<String, String> {
let repo_root = git_ui_core::resolve_repo_root_for_workspace_core(
&self.workspaces,
workspace_id.clone(),
Expand All @@ -1221,6 +1225,7 @@ impl DaemonState {
workspace_id,
&diff,
&commit_message_prompt,
commit_message_model_id.as_deref(),
|workspace_id, thread_id| {
emit_background_thread_hide(&self.event_sink, workspace_id, thread_id);
},
Expand Down
6 changes: 5 additions & 1 deletion src-tauri/src/bin/codex_monitor_daemon/rpc/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,11 @@ pub(super) async fn try_handle(
Ok(value) => value,
Err(err) => return Some(Err(err)),
};
let message = match state.generate_commit_message(workspace_id).await {
let commit_message_model_id = parse_optional_string(params, "commitMessageModelId");
let message = match state
.generate_commit_message(workspace_id, commit_message_model_id)
.await
{
Ok(value) => value,
Err(err) => return Some(Err(err)),
};
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/codex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,7 @@ pub(crate) async fn get_config_model(
#[tauri::command]
pub(crate) async fn generate_commit_message(
workspace_id: String,
commit_message_model_id: Option<String>,
state: State<'_, AppState>,
app: AppHandle,
) -> Result<String, String> {
Expand All @@ -883,7 +884,10 @@ pub(crate) async fn generate_commit_message(
&*state,
app,
"generate_commit_message",
json!({ "workspaceId": workspace_id }),
json!({
"workspaceId": workspace_id,
"commitMessageModelId": commit_message_model_id,
}),
)
.await?;
return serde_json::from_value(value).map_err(|err| err.to_string());
Expand All @@ -900,6 +904,7 @@ pub(crate) async fn generate_commit_message(
workspace_id,
&diff,
&commit_message_prompt,
commit_message_model_id.as_deref(),
|workspace_id, thread_id| {
let _ = app.emit(
"app-server-event",
Expand Down
10 changes: 9 additions & 1 deletion src-tauri/src/shared/codex_aux_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ pub(crate) async fn run_background_prompt_core<F>(
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
workspace_id: String,
prompt: String,
model: Option<&str>,
on_hide_thread: F,
timeout_error: &str,
turn_error_fallback: &str,
Expand Down Expand Up @@ -452,13 +453,16 @@ where
callbacks.insert(thread_id.clone(), tx);
}

let turn_params = json!({
let mut turn_params = json!({
"threadId": thread_id,
"input": [{ "type": "text", "text": prompt }],
"cwd": session.entry.path,
"approvalPolicy": "never",
"sandboxPolicy": { "type": "readOnly" },
});
if let Some(model_id) = model {
turn_params["model"] = json!(model_id);
}
let turn_result = session.send_request("turn/start", turn_params).await;
let turn_result = match turn_result {
Ok(result) => result,
Expand Down Expand Up @@ -545,6 +549,7 @@ pub(crate) async fn generate_commit_message_core<F>(
workspace_id: String,
diff: &str,
template: &str,
model: Option<&str>,
on_hide_thread: F,
) -> Result<String, String>
where
Expand All @@ -555,6 +560,7 @@ where
sessions,
workspace_id,
prompt,
model,
on_hide_thread,
"Timeout waiting for commit message generation",
"Unknown error during commit message generation",
Expand All @@ -581,6 +587,7 @@ where
sessions,
workspace_id,
metadata_prompt,
None,
on_hide_thread,
"Timeout waiting for metadata generation",
"Unknown error during metadata generation",
Expand Down Expand Up @@ -609,6 +616,7 @@ where
sessions,
workspace_id,
prompt,
None,
on_hide_thread,
"Timeout waiting for agent configuration generation",
"Unknown error during agent configuration generation",
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 @@ -533,6 +533,8 @@ pub(crate) struct AppSettings {
rename = "commitMessagePrompt"
)]
pub(crate) commit_message_prompt: String,
#[serde(default, rename = "commitMessageModelId")]
pub(crate) commit_message_model_id: Option<String>,
#[serde(
default = "default_system_notifications_enabled",
rename = "systemNotificationsEnabled"
Expand Down Expand Up @@ -1140,6 +1142,7 @@ impl Default for AppSettings {
preload_git_diffs: default_preload_git_diffs(),
git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(),
commit_message_prompt: default_commit_message_prompt(),
commit_message_model_id: None,
collaboration_modes_enabled: true,
steer_enabled: true,
pause_queued_messages_when_response_required:
Expand Down
6 changes: 6 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import { useWorkspaceLaunchScript } from "@app/hooks/useWorkspaceLaunchScript";
import { useWorkspaceLaunchScripts } from "@app/hooks/useWorkspaceLaunchScripts";
import { useWorktreeSetupScript } from "@app/hooks/useWorktreeSetupScript";
import { useGitCommitController } from "@app/hooks/useGitCommitController";
import { effectiveCommitMessageModelId } from "@/features/git/utils/commitMessageModelSelection";
import { WorkspaceHome } from "@/features/workspaces/components/WorkspaceHome";
import { MobileServerSetupWizard } from "@/features/mobile/components/MobileServerSetupWizard";
import { useMobileServerSetup } from "@/features/mobile/hooks/useMobileServerSetup";
Expand Down Expand Up @@ -428,6 +429,10 @@ function MainApp() {
setAccessMode,
persistThreadCodexParams,
});
const commitMessageModelId = useMemo(
() => effectiveCommitMessageModelId(models, appSettings.commitMessageModelId),
[models, appSettings.commitMessageModelId],
);

const composerShortcuts = {
modelShortcut: appSettings.composerModelShortcut,
Expand Down Expand Up @@ -1448,6 +1453,7 @@ function MainApp() {
activeWorkspace,
activeWorkspaceId,
activeWorkspaceIdRef,
commitMessageModelId,
gitStatus,
refreshGitStatus,
refreshGitLog,
Expand Down
6 changes: 4 additions & 2 deletions src/features/app/hooks/useGitCommitController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type GitCommitControllerOptions = {
activeWorkspace: WorkspaceInfo | null;
activeWorkspaceId: string | null;
activeWorkspaceIdRef: RefObject<string | null>;
commitMessageModelId: string | null;
gitStatus: GitStatusState;
refreshGitStatus: () => void;
refreshGitLog?: () => void;
Expand Down Expand Up @@ -53,6 +54,7 @@ export function useGitCommitController({
activeWorkspace,
activeWorkspaceId,
activeWorkspaceIdRef,
commitMessageModelId,
gitStatus,
refreshGitStatus,
refreshGitLog,
Expand Down Expand Up @@ -100,7 +102,7 @@ export function useGitCommitController({
setCommitMessageLoading(true);
setCommitMessageError(null);
try {
const message = await generateCommitMessage(workspaceId);
const message = await generateCommitMessage(workspaceId, commitMessageModelId);
if (!shouldApplyCommitMessage(activeWorkspaceIdRef.current, workspaceId)) {
return;
}
Expand All @@ -117,7 +119,7 @@ export function useGitCommitController({
setCommitMessageLoading(false);
}
}
}, [activeWorkspace, commitMessageLoading, activeWorkspaceIdRef]);
}, [activeWorkspace, commitMessageLoading, activeWorkspaceIdRef, commitMessageModelId]);

useEffect(() => {
setCommitMessage("");
Expand Down
42 changes: 42 additions & 0 deletions src/features/git/utils/commitMessageModelSelection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import type { ModelOption } from "@/types";
import { effectiveCommitMessageModelId } from "./commitMessageModelSelection";

const MODELS: ModelOption[] = [
{
id: "m-1",
model: "gpt-5.1",
displayName: "GPT-5.1",
description: "",
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
isDefault: false,
},
{
id: "m-2",
model: "gpt-5.2",
displayName: "GPT-5.2",
description: "",
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
isDefault: true,
},
];

describe("effectiveCommitMessageModelId", () => {
it("passes through null when no model is saved", () => {
expect(effectiveCommitMessageModelId(MODELS, null)).toBeNull();
});

it("returns the saved model when it exists in the workspace", () => {
expect(effectiveCommitMessageModelId(MODELS, "gpt-5.1")).toBe("gpt-5.1");
});

it("falls back to null when saved model is unavailable in the workspace", () => {
expect(effectiveCommitMessageModelId(MODELS, "gpt-4.1")).toBeNull();
});

it("falls back to null when no models are available", () => {
expect(effectiveCommitMessageModelId([], "gpt-5.1")).toBeNull();
});
});
15 changes: 15 additions & 0 deletions src/features/git/utils/commitMessageModelSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { ModelOption } from "@/types";

/**
* Returns the saved commit-message model ID when available for the active
* workspace, or `null` to let the backend fall back to the workspace default.
*
* This is a pure runtime guard — it never mutates the persisted setting.
*/
export function effectiveCommitMessageModelId(
models: ModelOption[],
savedModelId: string | null,
): string | null {
if (savedModelId == null) return null;
return models.some((m) => m.model === savedModelId) ? savedModelId : null;
}
98 changes: 98 additions & 0 deletions src/features/models/utils/modelListResponse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest";
import { formatModelSlug, parseModelListResponse } from "./modelListResponse";

describe("formatModelSlug", () => {
it("capitalizes plain segments", () => {
expect(formatModelSlug("codex-mini")).toBe("Codex-Mini");
});

it("uppercases known acronyms", () => {
expect(formatModelSlug("gpt-5.3-codex")).toBe("GPT-5.3-Codex");
});

it("leaves version-like segments unchanged", () => {
expect(formatModelSlug("gpt-5.1-codex-max")).toBe("GPT-5.1-Codex-Max");
});

it("handles a version-only slug", () => {
expect(formatModelSlug("gpt-5.2")).toBe("GPT-5.2");
});
it("is case-insensitive for acronym detection", () => {
expect(formatModelSlug("GPT-5.3-codex")).toBe("GPT-5.3-Codex");
expect(formatModelSlug("Gpt-5.3-codex")).toBe("GPT-5.3-Codex");
});

it("returns empty string for non-string input", () => {
expect(formatModelSlug(null)).toBe("");
expect(formatModelSlug(undefined)).toBe("");
expect(formatModelSlug(42)).toBe("");
});

it("returns empty string for blank strings", () => {
expect(formatModelSlug("")).toBe("");
expect(formatModelSlug(" ")).toBe("");
});

it("handles a single segment", () => {
expect(formatModelSlug("codex")).toBe("Codex");
expect(formatModelSlug("gpt")).toBe("GPT");
});
});

describe("parseModelListResponse", () => {
it("uses displayName when present", () => {
const response = {
result: {
data: [
{ id: "m1", model: "gpt-5.3-codex-spark", displayName: "GPT-5.3-Codex-Spark" },
],
},
};
const [model] = parseModelListResponse(response);
expect(model.displayName).toBe("GPT-5.3-Codex-Spark");
});

it("formats the slug when displayName is missing", () => {
const response = {
result: {
data: [{ id: "m1", model: "gpt-5.3-codex" }],
},
};
const [model] = parseModelListResponse(response);
expect(model.displayName).toBe("GPT-5.3-Codex");
});

it("formats the slug when displayName is an empty string", () => {
const response = {
result: {
data: [{ id: "m1", model: "gpt-5.1-codex-mini", displayName: "" }],
},
};
const [model] = parseModelListResponse(response);
expect(model.displayName).toBe("GPT-5.1-Codex-Mini");
});

it("formats the slug when displayName equals the model slug", () => {
const response = {
result: {
data: [{ id: "m1", model: "gpt-5.3-codex", displayName: "gpt-5.3-codex" }],
},
};
const [model] = parseModelListResponse(response);
expect(model.displayName).toBe("GPT-5.3-Codex");
});

it("preserves displayName when it differs from the slug", () => {
const response = {
result: {
data: [
{ id: "m1", model: "gpt-5.3-codex-spark", displayName: "GPT-5.3-Codex-Spark" },
{ id: "m2", model: "gpt-5.2-codex", displayName: "gpt-5.2-codex" },
],
},
};
const models = parseModelListResponse(response);
expect(models[0].displayName).toBe("GPT-5.3-Codex-Spark");
expect(models[1].displayName).toBe("GPT-5.2-Codex");
});
});
Loading
Loading