Skip to content

Commit 6d41bb0

Browse files
committed
feat(git): add model selection for commit message generation
Allow users to choose which model generates commit messages via a dropdown in the git diff panel. The selection persists in app settings and falls back to the workspace default when the chosen model is no longer available.
1 parent 005cd77 commit 6d41bb0

22 files changed

Lines changed: 489 additions & 16 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ dist-ssr
2727
CodexMonitor.zip
2828
.codex-worktrees/
2929
.codexmonitor/
30+
.agent/
31+
.agents/
32+
.claude/
33+
.cursor/
3034
public/assets/material-icons/
3135

3236
# Nix

src-tauri/src/bin/codex_monitor_daemon.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,22 +1118,31 @@ impl DaemonState {
11181118
codex_aux_core::codex_doctor_core(&self.app_settings, codex_bin, codex_args).await
11191119
}
11201120

1121-
async fn generate_commit_message(&self, workspace_id: String) -> Result<String, String> {
1121+
async fn generate_commit_message(
1122+
&self,
1123+
workspace_id: String,
1124+
commit_message_model_id: Option<String>,
1125+
) -> Result<String, String> {
11221126
let repo_root = git_ui_core::resolve_repo_root_for_workspace_core(
11231127
&self.workspaces,
11241128
workspace_id.clone(),
11251129
)
11261130
.await?;
11271131
let diff = git_ui_core::collect_workspace_diff_core(&repo_root)?;
1128-
let commit_message_prompt = {
1132+
let (commit_message_prompt, default_commit_message_model_id) = {
11291133
let settings = self.app_settings.lock().await;
1130-
settings.commit_message_prompt.clone()
1134+
(
1135+
settings.commit_message_prompt.clone(),
1136+
settings.commit_message_model_id.clone(),
1137+
)
11311138
};
1139+
let commit_message_model_id = commit_message_model_id.or(default_commit_message_model_id);
11321140
codex_aux_core::generate_commit_message_core(
11331141
&self.sessions,
11341142
workspace_id,
11351143
&diff,
11361144
&commit_message_prompt,
1145+
commit_message_model_id.as_deref(),
11371146
|workspace_id, thread_id| {
11381147
emit_background_thread_hide(&self.event_sink, workspace_id, thread_id);
11391148
},

src-tauri/src/bin/codex_monitor_daemon/rpc/git.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,11 @@ pub(super) async fn try_handle(
366366
Ok(value) => value,
367367
Err(err) => return Some(Err(err)),
368368
};
369-
let message = match state.generate_commit_message(workspace_id).await {
369+
let commit_message_model_id = parse_optional_string(params, "commitMessageModelId");
370+
let message = match state
371+
.generate_commit_message(workspace_id, commit_message_model_id)
372+
.await
373+
{
370374
Ok(value) => value,
371375
Err(err) => return Some(Err(err)),
372376
};

src-tauri/src/codex/mod.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,31 +736,46 @@ pub(crate) async fn get_config_model(
736736
#[tauri::command]
737737
pub(crate) async fn generate_commit_message(
738738
workspace_id: String,
739+
commit_message_model_id: Option<String>,
739740
state: State<'_, AppState>,
740741
app: AppHandle,
741742
) -> Result<String, String> {
742743
if remote_backend::is_remote_mode(&*state).await {
744+
let commit_message_model_id = if commit_message_model_id.is_some() {
745+
commit_message_model_id
746+
} else {
747+
let settings = state.app_settings.lock().await;
748+
settings.commit_message_model_id.clone()
749+
};
743750
let value = remote_backend::call_remote(
744751
&*state,
745752
app,
746753
"generate_commit_message",
747-
json!({ "workspaceId": workspace_id }),
754+
json!({
755+
"workspaceId": workspace_id,
756+
"commitMessageModelId": commit_message_model_id,
757+
}),
748758
)
749759
.await?;
750760
return serde_json::from_value(value).map_err(|err| err.to_string());
751761
}
752762

753763
let diff = crate::git::get_workspace_diff(&workspace_id, &state).await?;
754764

755-
let commit_message_prompt = {
765+
let (commit_message_prompt, default_commit_message_model_id) = {
756766
let settings = state.app_settings.lock().await;
757-
settings.commit_message_prompt.clone()
767+
(
768+
settings.commit_message_prompt.clone(),
769+
settings.commit_message_model_id.clone(),
770+
)
758771
};
772+
let commit_message_model_id = commit_message_model_id.or(default_commit_message_model_id);
759773
crate::shared::codex_aux_core::generate_commit_message_core(
760774
&state.sessions,
761775
workspace_id,
762776
&diff,
763777
&commit_message_prompt,
778+
commit_message_model_id.as_deref(),
764779
|workspace_id, thread_id| {
765780
let _ = app.emit(
766781
"app-server-event",

src-tauri/src/shared/codex_aux_core.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ pub(crate) async fn run_background_prompt_core<F>(
258258
sessions: &Mutex<HashMap<String, Arc<WorkspaceSession>>>,
259259
workspace_id: String,
260260
prompt: String,
261+
model: Option<&str>,
261262
on_hide_thread: F,
262263
timeout_error: &str,
263264
turn_error_fallback: &str,
@@ -315,13 +316,16 @@ where
315316
callbacks.insert(thread_id.clone(), tx);
316317
}
317318

318-
let turn_params = json!({
319+
let mut turn_params = json!({
319320
"threadId": thread_id,
320321
"input": [{ "type": "text", "text": prompt }],
321322
"cwd": session.entry.path,
322323
"approvalPolicy": "never",
323324
"sandboxPolicy": { "type": "readOnly" },
324325
});
326+
if let Some(model_id) = model {
327+
turn_params["model"] = json!(model_id);
328+
}
325329
let turn_result = session.send_request("turn/start", turn_params).await;
326330
let turn_result = match turn_result {
327331
Ok(result) => result,
@@ -408,6 +412,7 @@ pub(crate) async fn generate_commit_message_core<F>(
408412
workspace_id: String,
409413
diff: &str,
410414
template: &str,
415+
model: Option<&str>,
411416
on_hide_thread: F,
412417
) -> Result<String, String>
413418
where
@@ -418,6 +423,7 @@ where
418423
sessions,
419424
workspace_id,
420425
prompt,
426+
model,
421427
on_hide_thread,
422428
"Timeout waiting for commit message generation",
423429
"Unknown error during commit message generation",
@@ -444,6 +450,7 @@ where
444450
sessions,
445451
workspace_id,
446452
metadata_prompt,
453+
None,
447454
on_hide_thread,
448455
"Timeout waiting for metadata generation",
449456
"Unknown error during metadata generation",

src-tauri/src/types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,8 @@ pub(crate) struct AppSettings {
533533
rename = "commitMessagePrompt"
534534
)]
535535
pub(crate) commit_message_prompt: String,
536+
#[serde(default, rename = "commitMessageModelId")]
537+
pub(crate) commit_message_model_id: Option<String>,
536538
#[serde(
537539
default = "default_system_notifications_enabled",
538540
rename = "systemNotificationsEnabled"
@@ -1149,6 +1151,7 @@ impl Default for AppSettings {
11491151
preload_git_diffs: default_preload_git_diffs(),
11501152
git_diff_ignore_whitespace_changes: default_git_diff_ignore_whitespace_changes(),
11511153
commit_message_prompt: default_commit_message_prompt(),
1154+
commit_message_model_id: None,
11521155
experimental_collab_enabled: false,
11531156
collaboration_modes_enabled: true,
11541157
steer_enabled: true,

src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import { useWorkspaceLaunchScript } from "@app/hooks/useWorkspaceLaunchScript";
105105
import { useWorkspaceLaunchScripts } from "@app/hooks/useWorkspaceLaunchScripts";
106106
import { useWorktreeSetupScript } from "@app/hooks/useWorktreeSetupScript";
107107
import { useGitCommitController } from "@app/hooks/useGitCommitController";
108+
import { useCommitMessageModelSelection } from "@app/hooks/useCommitMessageModelSelection";
108109
import { WorkspaceHome } from "@/features/workspaces/components/WorkspaceHome";
109110
import { MobileServerSetupWizard } from "@/features/mobile/components/MobileServerSetupWizard";
110111
import { useMobileServerSetup } from "@/features/mobile/hooks/useMobileServerSetup";
@@ -425,6 +426,15 @@ function MainApp() {
425426
setAccessMode,
426427
persistThreadCodexParams,
427428
});
429+
const {
430+
resolvedCommitMessageModelId,
431+
onCommitMessageModelChange,
432+
} = useCommitMessageModelSelection({
433+
models,
434+
commitMessageModelId: appSettings.commitMessageModelId,
435+
setAppSettings,
436+
queueSaveSettings,
437+
});
428438

429439
const composerShortcuts = {
430440
modelShortcut: appSettings.composerModelShortcut,
@@ -1428,6 +1438,7 @@ function MainApp() {
14281438
activeWorkspace,
14291439
activeWorkspaceId,
14301440
activeWorkspaceIdRef,
1441+
commitMessageModelId: resolvedCommitMessageModelId,
14311442
gitStatus,
14321443
refreshGitStatus,
14331444
refreshGitLog,
@@ -2160,6 +2171,8 @@ function MainApp() {
21602171
commitMessageError,
21612172
onCommitMessageChange: handleCommitMessageChange,
21622173
onGenerateCommitMessage: handleGenerateCommitMessage,
2174+
commitMessageModelId: resolvedCommitMessageModelId,
2175+
onCommitMessageModelChange,
21632176
onCommit: handleCommit,
21642177
onCommitAndPush: handleCommitAndPush,
21652178
onCommitAndSync: handleCommitAndSync,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useCallback, useEffect, useMemo } from "react";
2+
import type { Dispatch, SetStateAction } from "react";
3+
import type { AppSettings, ModelOption } from "@/types";
4+
import { resolveCommitMessageModelSelection } from "@/features/git/utils/commitMessageModelSelection";
5+
6+
type UseCommitMessageModelSelectionOptions = {
7+
models: ModelOption[];
8+
commitMessageModelId: string | null;
9+
setAppSettings: Dispatch<SetStateAction<AppSettings>>;
10+
queueSaveSettings: (next: AppSettings) => Promise<AppSettings | void>;
11+
};
12+
13+
type UseCommitMessageModelSelectionResult = {
14+
resolvedCommitMessageModelId: string | null;
15+
onCommitMessageModelChange: (id: string | null) => void;
16+
};
17+
18+
export function useCommitMessageModelSelection({
19+
models,
20+
commitMessageModelId,
21+
setAppSettings,
22+
queueSaveSettings,
23+
}: UseCommitMessageModelSelectionOptions): UseCommitMessageModelSelectionResult {
24+
const selection = useMemo(
25+
() => resolveCommitMessageModelSelection(models, commitMessageModelId),
26+
[models, commitMessageModelId],
27+
);
28+
29+
const persistCommitMessageModelId = useCallback(
30+
(id: string | null) => {
31+
setAppSettings((current) => {
32+
if (current.commitMessageModelId === id) {
33+
return current;
34+
}
35+
const next = { ...current, commitMessageModelId: id };
36+
void queueSaveSettings(next);
37+
return next;
38+
});
39+
},
40+
[queueSaveSettings, setAppSettings],
41+
);
42+
43+
useEffect(() => {
44+
if (!selection.shouldNormalize) {
45+
return;
46+
}
47+
persistCommitMessageModelId(selection.normalizedModelId);
48+
}, [persistCommitMessageModelId, selection.normalizedModelId, selection.shouldNormalize]);
49+
50+
return {
51+
resolvedCommitMessageModelId: selection.resolvedModelId,
52+
onCommitMessageModelChange: persistCommitMessageModelId,
53+
};
54+
}

src/features/app/hooks/useGitCommitController.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type GitCommitControllerOptions = {
1818
activeWorkspace: WorkspaceInfo | null;
1919
activeWorkspaceId: string | null;
2020
activeWorkspaceIdRef: RefObject<string | null>;
21+
commitMessageModelId: string | null;
2122
gitStatus: GitStatusState;
2223
refreshGitStatus: () => void;
2324
refreshGitLog?: () => void;
@@ -53,6 +54,7 @@ export function useGitCommitController({
5354
activeWorkspace,
5455
activeWorkspaceId,
5556
activeWorkspaceIdRef,
57+
commitMessageModelId,
5658
gitStatus,
5759
refreshGitStatus,
5860
refreshGitLog,
@@ -100,7 +102,7 @@ export function useGitCommitController({
100102
setCommitMessageLoading(true);
101103
setCommitMessageError(null);
102104
try {
103-
const message = await generateCommitMessage(workspaceId);
105+
const message = await generateCommitMessage(workspaceId, commitMessageModelId);
104106
if (!shouldApplyCommitMessage(activeWorkspaceIdRef.current, workspaceId)) {
105107
return;
106108
}
@@ -117,7 +119,7 @@ export function useGitCommitController({
117119
setCommitMessageLoading(false);
118120
}
119121
}
120-
}, [activeWorkspace, commitMessageLoading, activeWorkspaceIdRef]);
122+
}, [activeWorkspace, commitMessageLoading, activeWorkspaceIdRef, commitMessageModelId]);
121123

122124
useEffect(() => {
123125
setCommitMessage("");

src/features/git/components/GitDiffPanel.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { GitHubIssue, GitHubPullRequest, GitLogEntry } from "../../../types";
1+
import type { GitHubIssue, GitHubPullRequest, GitLogEntry, ModelOption } from "../../../types";
22
import type { MouseEvent as ReactMouseEvent } from "react";
33
import { Menu, MenuItem } from "@tauri-apps/api/menu";
44
import { LogicalPosition } from "@tauri-apps/api/dpi";
@@ -127,6 +127,9 @@ type GitDiffPanelProps = {
127127
commitMessageError?: string | null;
128128
onCommitMessageChange?: (value: string) => void;
129129
onGenerateCommitMessage?: () => void | Promise<void>;
130+
models?: ModelOption[];
131+
commitMessageModelId?: string | null;
132+
onCommitMessageModelChange?: (id: string | null) => void;
130133
// Git operations
131134
onCommit?: () => void | Promise<void>;
132135
onCommitAndPush?: () => void | Promise<void>;
@@ -216,6 +219,9 @@ export function GitDiffPanel({
216219
commitMessageError = null,
217220
onCommitMessageChange,
218221
onGenerateCommitMessage,
222+
models = [],
223+
commitMessageModelId = null,
224+
onCommitMessageModelChange,
219225
onCommit,
220226
onCommitAndPush: _onCommitAndPush,
221227
onCommitAndSync: _onCommitAndSync,
@@ -730,6 +736,9 @@ export function GitDiffPanel({
730736
commitMessageLoading={commitMessageLoading}
731737
canGenerateCommitMessage={canGenerateCommitMessage}
732738
onGenerateCommitMessage={onGenerateCommitMessage}
739+
models={models}
740+
commitMessageModelId={commitMessageModelId}
741+
onCommitMessageModelChange={onCommitMessageModelChange}
733742
stagedFiles={stagedFiles}
734743
unstagedFiles={unstagedFiles}
735744
commitLoading={commitLoading}

0 commit comments

Comments
 (0)