authorgravatar for seifalmotazbellah@gmail.comSeif Almotaz Bellah <seifalmotazbellah@gmail.com> 2026-03-18 00:33:36+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-17 15:33:36-07:00
log579a69eb7756b2169d26299d36f936838db39caa
tree594f6b85cfdb645425b60063c03580bf64fb0d41
parent9ee8c4f5430a221d6569ba9c2f984d8501ec4fe5
signature Signed by PGP key B5690EEEBB952194

feat: add configurable model for git text generation (#1171)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

11 files changed, 118 insertions(+), 9 deletions(-)

apps/server/src/git/Layers/CodexTextGeneration.ts+7-2
...@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";...@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";3import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
4import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";4import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
55
6import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts";
6import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";7import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git";
78
8import { resolveAttachmentPath } from "../../attachmentStore.ts";9import { resolveAttachmentPath } from "../../attachmentStore.ts";
...@@ -17,7 +18,6 @@ import {...@@ -17,7 +18,6 @@ import {
17 TextGeneration,18 TextGeneration,
18} from "../Services/TextGeneration.ts";19} from "../Services/TextGeneration.ts";
1920
20const CODEX_MODEL = "gpt-5.3-codex";
21const CODEX_REASONING_EFFORT = "low";21const CODEX_REASONING_EFFORT = "low";
22const CODEX_TIMEOUT_MS = 180_000;22const CODEX_TIMEOUT_MS = 180_000;
2323
...@@ -187,6 +187,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -187,6 +187,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
187 outputSchemaJson,187 outputSchemaJson,
188 imagePaths = [],188 imagePaths = [],
189 cleanupPaths = [],189 cleanupPaths = [],
190 model,
190 }: {191 }: {
191 operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName";192 operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName";
192 cwd: string;193 cwd: string;
...@@ -194,6 +195,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -194,6 +195,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
194 outputSchemaJson: S;195 outputSchemaJson: S;
195 imagePaths?: ReadonlyArray<string>;196 imagePaths?: ReadonlyArray<string>;
196 cleanupPaths?: ReadonlyArray<string>;197 cleanupPaths?: ReadonlyArray<string>;
198 model?: string;
197 }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>199 }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> =>
198 Effect.gen(function* () {200 Effect.gen(function* () {
199 const schemaPath = yield* writeTempFile(201 const schemaPath = yield* writeTempFile(
...@@ -212,7 +214,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -212,7 +214,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
212 "-s",214 "-s",
213 "read-only",215 "read-only",
214 "--model",216 "--model",
215 CODEX_MODEL,217 model ?? DEFAULT_GIT_TEXT_GENERATION_MODEL,
216 "--config",218 "--config",
217 `model_reasoning_effort="${CODEX_REASONING_EFFORT}"`,219 `model_reasoning_effort="${CODEX_REASONING_EFFORT}"`,
218 "--output-schema",220 "--output-schema",
...@@ -353,6 +355,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -353,6 +355,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
353 cwd: input.cwd,355 cwd: input.cwd,
354 prompt,356 prompt,
355 outputSchemaJson,357 outputSchemaJson,
358 ...(input.model ? { model: input.model } : {}),
356 }).pipe(359 }).pipe(
357 Effect.map(360 Effect.map(
358 (generated) =>361 (generated) =>
...@@ -398,6 +401,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -398,6 +401,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
398 title: Schema.String,401 title: Schema.String,
399 body: Schema.String,402 body: Schema.String,
400 }),403 }),
404 ...(input.model ? { model: input.model } : {}),
401 }).pipe(405 }).pipe(
402 Effect.map(406 Effect.map(
403 (generated) =>407 (generated) =>
...@@ -449,6 +453,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {...@@ -449,6 +453,7 @@ const makeCodexTextGeneration = Effect.gen(function* () {
449 branch: Schema.String,453 branch: Schema.String,
450 }),454 }),
451 imagePaths,455 imagePaths,
456 ...(input.model ? { model: input.model } : {}),
452 });457 });
453458
454 return {459 return {
apps/server/src/git/Layers/GitManager.ts+11-2
...@@ -639,6 +639,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -639,6 +639,7 @@ export const makeGitManager = Effect.gen(function* () {
639 /** When true, also produce a semantic feature branch name. */639 /** When true, also produce a semantic feature branch name. */
640 includeBranch?: boolean;640 includeBranch?: boolean;
641 filePaths?: readonly string[];641 filePaths?: readonly string[];
642 model?: string;
642 }) =>643 }) =>
643 Effect.gen(function* () {644 Effect.gen(function* () {
644 const context = yield* gitCore.prepareCommitContext(input.cwd, input.filePaths);645 const context = yield* gitCore.prepareCommitContext(input.cwd, input.filePaths);
...@@ -665,6 +666,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -665,6 +666,7 @@ export const makeGitManager = Effect.gen(function* () {
665 stagedSummary: limitContext(context.stagedSummary, 8_000),666 stagedSummary: limitContext(context.stagedSummary, 8_000),
666 stagedPatch: limitContext(context.stagedPatch, 50_000),667 stagedPatch: limitContext(context.stagedPatch, 50_000),
667 ...(input.includeBranch ? { includeBranch: true } : {}),668 ...(input.includeBranch ? { includeBranch: true } : {}),
669 ...(input.model ? { model: input.model } : {}),
668 })670 })
669 .pipe(Effect.map((result) => sanitizeCommitMessage(result)));671 .pipe(Effect.map((result) => sanitizeCommitMessage(result)));
670672
...@@ -682,6 +684,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -682,6 +684,7 @@ export const makeGitManager = Effect.gen(function* () {
682 commitMessage?: string,684 commitMessage?: string,
683 preResolvedSuggestion?: CommitAndBranchSuggestion,685 preResolvedSuggestion?: CommitAndBranchSuggestion,
684 filePaths?: readonly string[],686 filePaths?: readonly string[],
687 model?: string,
685 ) =>688 ) =>
686 Effect.gen(function* () {689 Effect.gen(function* () {
687 const suggestion =690 const suggestion =
...@@ -691,6 +694,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -691,6 +694,7 @@ export const makeGitManager = Effect.gen(function* () {
691 branch,694 branch,
692 ...(commitMessage ? { commitMessage } : {}),695 ...(commitMessage ? { commitMessage } : {}),
693 ...(filePaths ? { filePaths } : {}),696 ...(filePaths ? { filePaths } : {}),
697 ...(model ? { model } : {}),
694 }));698 }));
695 if (!suggestion) {699 if (!suggestion) {
696 return { status: "skipped_no_changes" as const };700 return { status: "skipped_no_changes" as const };
...@@ -704,7 +708,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -704,7 +708,7 @@ export const makeGitManager = Effect.gen(function* () {
704 };708 };
705 });709 });
706710
707 const runPrStep = (cwd: string, fallbackBranch: string | null) =>711 const runPrStep = (cwd: string, fallbackBranch: string | null, model?: string) =>
708 Effect.gen(function* () {712 Effect.gen(function* () {
709 const details = yield* gitCore.statusDetails(cwd);713 const details = yield* gitCore.statusDetails(cwd);
710 const branch = details.branch ?? fallbackBranch;714 const branch = details.branch ?? fallbackBranch;
...@@ -748,6 +752,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -748,6 +752,7 @@ export const makeGitManager = Effect.gen(function* () {
748 commitSummary: limitContext(rangeContext.commitSummary, 20_000),752 commitSummary: limitContext(rangeContext.commitSummary, 20_000),
749 diffSummary: limitContext(rangeContext.diffSummary, 20_000),753 diffSummary: limitContext(rangeContext.diffSummary, 20_000),
750 diffPatch: limitContext(rangeContext.diffPatch, 60_000),754 diffPatch: limitContext(rangeContext.diffPatch, 60_000),
755 ...(model ? { model } : {}),
751 });756 });
752757
753 const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${randomUUID()}.md`);758 const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${randomUUID()}.md`);
...@@ -972,6 +977,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -972,6 +977,7 @@ export const makeGitManager = Effect.gen(function* () {
972 branch: string | null,977 branch: string | null,
973 commitMessage?: string,978 commitMessage?: string,
974 filePaths?: readonly string[],979 filePaths?: readonly string[],
980 model?: string,
975 ) =>981 ) =>
976 Effect.gen(function* () {982 Effect.gen(function* () {
977 const suggestion = yield* resolveCommitAndBranchSuggestion({983 const suggestion = yield* resolveCommitAndBranchSuggestion({
...@@ -980,6 +986,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -980,6 +986,7 @@ export const makeGitManager = Effect.gen(function* () {
980 ...(commitMessage ? { commitMessage } : {}),986 ...(commitMessage ? { commitMessage } : {}),
981 ...(filePaths ? { filePaths } : {}),987 ...(filePaths ? { filePaths } : {}),
982 includeBranch: true,988 includeBranch: true,
989 ...(model ? { model } : {}),
983 });990 });
984 if (!suggestion) {991 if (!suggestion) {
985 return yield* gitManagerError(992 return yield* gitManagerError(
...@@ -1028,6 +1035,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -1028,6 +1035,7 @@ export const makeGitManager = Effect.gen(function* () {
1028 initialStatus.branch,1035 initialStatus.branch,
1029 input.commitMessage,1036 input.commitMessage,
1030 input.filePaths,1037 input.filePaths,
1038 input.textGenerationModel,
1031 );1039 );
1032 branchStep = result.branchStep;1040 branchStep = result.branchStep;
1033 commitMessageForStep = result.resolvedCommitMessage;1041 commitMessageForStep = result.resolvedCommitMessage;
...@@ -1044,6 +1052,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -1044,6 +1052,7 @@ export const makeGitManager = Effect.gen(function* () {
1044 commitMessageForStep,1052 commitMessageForStep,
1045 preResolvedCommitSuggestion,1053 preResolvedCommitSuggestion,
1046 input.filePaths,1054 input.filePaths,
1055 input.textGenerationModel,
1047 );1056 );
10481057
1049 const push = wantsPush1058 const push = wantsPush
...@@ -1051,7 +1060,7 @@ export const makeGitManager = Effect.gen(function* () {...@@ -1051,7 +1060,7 @@ export const makeGitManager = Effect.gen(function* () {
1051 : { status: "skipped_not_requested" as const };1060 : { status: "skipped_not_requested" as const };
10521061
1053 const pr = wantsPr1062 const pr = wantsPr
1054 ? yield* runPrStep(input.cwd, currentBranch)1063 ? yield* runPrStep(input.cwd, currentBranch, input.textGenerationModel)
1055 : { status: "skipped_not_requested" as const };1064 : { status: "skipped_not_requested" as const };
10561065
1057 return {1066 return {
apps/server/src/git/Services/TextGeneration.ts+6
...@@ -19,6 +19,8 @@ export interface CommitMessageGenerationInput {...@@ -19,6 +19,8 @@ export interface CommitMessageGenerationInput {
19 stagedPatch: string;19 stagedPatch: string;
20 /** When true, the model also returns a semantic branch name for the change. */20 /** When true, the model also returns a semantic branch name for the change. */
21 includeBranch?: boolean;21 includeBranch?: boolean;
22 /** Model to use for generation. Defaults to gpt-5.4-mini if not specified. */
23 model?: string;
22}24}
2325
24export interface CommitMessageGenerationResult {26export interface CommitMessageGenerationResult {
...@@ -35,6 +37,8 @@ export interface PrContentGenerationInput {...@@ -35,6 +37,8 @@ export interface PrContentGenerationInput {
35 commitSummary: string;37 commitSummary: string;
36 diffSummary: string;38 diffSummary: string;
37 diffPatch: string;39 diffPatch: string;
40 /** Model to use for generation. Defaults to gpt-5.4-mini if not specified. */
41 model?: string;
38}42}
3943
40export interface PrContentGenerationResult {44export interface PrContentGenerationResult {
...@@ -46,6 +50,8 @@ export interface BranchNameGenerationInput {...@@ -46,6 +50,8 @@ export interface BranchNameGenerationInput {
46 cwd: string;50 cwd: string;
47 message: string;51 message: string;
48 attachments?: ReadonlyArray<ChatAttachment> | undefined;52 attachments?: ReadonlyArray<ChatAttachment> | undefined;
53 /** Model to use for generation. Defaults to gpt-5.4-mini if not specified. */
54 model?: string;
49}55}
5056
51export interface BranchNameGenerationResult {57export interface BranchNameGenerationResult {
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+2
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import {1import {
2 type ChatAttachment,2 type ChatAttachment,
3 CommandId,3 CommandId,
4 DEFAULT_GIT_TEXT_GENERATION_MODEL,
4 EventId,5 EventId,
5 type OrchestrationEvent,6 type OrchestrationEvent,
6 type ProviderModelOptions,7 type ProviderModelOptions,
...@@ -391,6 +392,7 @@ const make = Effect.gen(function* () {...@@ -391,6 +392,7 @@ const make = Effect.gen(function* () {
391 cwd,392 cwd,
392 message: input.messageText,393 message: input.messageText,
393 ...(attachments.length > 0 ? { attachments } : {}),394 ...(attachments.length > 0 ? { attachments } : {}),
395 model: DEFAULT_GIT_TEXT_GENERATION_MODEL,
394 })396 })
395 .pipe(397 .pipe(
396 Effect.catch((error) =>398 Effect.catch((error) =>
apps/web/src/appSettings.test.ts+1
...@@ -28,6 +28,7 @@ describe("getAppModelOptions", () => {...@@ -28,6 +28,7 @@ describe("getAppModelOptions", () => {
2828
29 expect(options.map((option) => option.slug)).toEqual([29 expect(options.map((option) => option.slug)).toEqual([
30 "gpt-5.4",30 "gpt-5.4",
31 "gpt-5.4-mini",
31 "gpt-5.3-codex",32 "gpt-5.3-codex",
32 "gpt-5.3-codex-spark",33 "gpt-5.3-codex-spark",
33 "gpt-5.2-codex",34 "gpt-5.2-codex",
apps/web/src/appSettings.ts+2-1
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { useCallback } from "react";1import { useCallback } from "react";
2import { Option, Schema } from "effect";2import { Option, Schema } from "effect";
3import { type ProviderKind } from "@t3tools/contracts";3import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts";
4import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";4import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
5import { useLocalStorage } from "./hooks/useLocalStorage";5import { useLocalStorage } from "./hooks/useLocalStorage";
66
...@@ -34,6 +34,7 @@ const AppSettingsSchema = Schema.Struct({...@@ -34,6 +34,7 @@ const AppSettingsSchema = Schema.Struct({
34 customCodexModels: Schema.Array(Schema.String).pipe(34 customCodexModels: Schema.Array(Schema.String).pipe(
35 Schema.withConstructorDefault(() => Option.some([])),35 Schema.withConstructorDefault(() => Option.some([])),
36 ),36 ),
37 textGenerationModel: Schema.optional(TrimmedNonEmptyString),
37});38});
38export type AppSettings = typeof AppSettingsSchema.Type;39export type AppSettings = typeof AppSettingsSchema.Type;
39export interface AppModelOption {40export interface AppModelOption {
apps/web/src/components/GitActionsControl.tsx+7-1
...@@ -15,6 +15,7 @@ import {...@@ -15,6 +15,7 @@ import {
15 resolveQuickAction,15 resolveQuickAction,
16 summarizeGitResult,16 summarizeGitResult,
17} from "./GitActionsControl.logic";17} from "./GitActionsControl.logic";
18import { useAppSettings } from "~/appSettings";
18import { Button } from "~/components/ui/button";19import { Button } from "~/components/ui/button";
19import { Checkbox } from "~/components/ui/checkbox";20import { Checkbox } from "~/components/ui/checkbox";
20import {21import {
...@@ -154,6 +155,7 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {...@@ -154,6 +155,7 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
154}155}
155156
156export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) {157export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) {
158 const { settings } = useAppSettings();
157 const threadToastData = useMemo(159 const threadToastData = useMemo(
158 () => (activeThreadId ? { threadId: activeThreadId } : undefined),160 () => (activeThreadId ? { threadId: activeThreadId } : undefined),
159 [activeThreadId],161 [activeThreadId],
...@@ -191,7 +193,11 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -191,7 +193,11 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
191 const initMutation = useMutation(gitInitMutationOptions({ cwd: gitCwd, queryClient }));193 const initMutation = useMutation(gitInitMutationOptions({ cwd: gitCwd, queryClient }));
192194
193 const runImmediateGitActionMutation = useMutation(195 const runImmediateGitActionMutation = useMutation(
194 gitRunStackedActionMutationOptions({ cwd: gitCwd, queryClient }),196 gitRunStackedActionMutationOptions({
197 cwd: gitCwd,
198 queryClient,
199 model: settings.textGenerationModel ?? null,
200 }),
195 );201 );
196 const pullMutation = useMutation(gitPullMutationOptions({ cwd: gitCwd, queryClient }));202 const pullMutation = useMutation(gitPullMutationOptions({ cwd: gitCwd, queryClient }));
197203
apps/web/src/lib/gitReactQuery.ts+2
...@@ -112,6 +112,7 @@ export function gitCheckoutMutationOptions(input: {...@@ -112,6 +112,7 @@ export function gitCheckoutMutationOptions(input: {
112export function gitRunStackedActionMutationOptions(input: {112export function gitRunStackedActionMutationOptions(input: {
113 cwd: string | null;113 cwd: string | null;
114 queryClient: QueryClient;114 queryClient: QueryClient;
115 model?: string | null;
115}) {116}) {
116 return mutationOptions({117 return mutationOptions({
117 mutationKey: gitMutationKeys.runStackedAction(input.cwd),118 mutationKey: gitMutationKeys.runStackedAction(input.cwd),
...@@ -134,6 +135,7 @@ export function gitRunStackedActionMutationOptions(input: {...@@ -134,6 +135,7 @@ export function gitRunStackedActionMutationOptions(input: {
134 ...(commitMessage ? { commitMessage } : {}),135 ...(commitMessage ? { commitMessage } : {}),
135 ...(featureBranch ? { featureBranch } : {}),136 ...(featureBranch ? { featureBranch } : {}),
136 ...(filePaths ? { filePaths } : {}),137 ...(filePaths ? { filePaths } : {}),
138 ...(input.model ? { model: input.model } : {}),
137 });139 });
138 },140 },
139 onSettled: async () => {141 onSettled: async () => {
apps/web/src/routes/_chat.settings.tsx+72-2
...@@ -1,9 +1,9 @@...@@ -1,9 +1,9 @@
1import { createFileRoute } from "@tanstack/react-router";1import { createFileRoute } from "@tanstack/react-router";
2import { useQuery } from "@tanstack/react-query";2import { useQuery } from "@tanstack/react-query";
3import { useCallback, useState } from "react";3import { useCallback, useState } from "react";
4import { type ProviderKind } from "@t3tools/contracts";4import { type ProviderKind, DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts";
5import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";5import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
6import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings";6import { getAppModelOptions, MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings";
7import { resolveAndPersistPreferredEditor } from "../editorPreferences";7import { resolveAndPersistPreferredEditor } from "../editorPreferences";
8import { isElectron } from "../env";8import { isElectron } from "../env";
9import { useTheme } from "../hooks/useTheme";9import { useTheme } from "../hooks/useTheme";
...@@ -112,6 +112,17 @@ function SettingsRouteView() {...@@ -112,6 +112,17 @@ function SettingsRouteView() {
112 const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null;112 const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null;
113 const availableEditors = serverConfigQuery.data?.availableEditors;113 const availableEditors = serverConfigQuery.data?.availableEditors;
114114
115 const gitTextGenerationModelOptions = getAppModelOptions(
116 "codex",
117 settings.customCodexModels,
118 settings.textGenerationModel,
119 );
120 const selectedGitTextGenerationModelLabel =
121 gitTextGenerationModelOptions.find(
122 (option) =>
123 option.slug === (settings.textGenerationModel ?? DEFAULT_GIT_TEXT_GENERATION_MODEL),
124 )?.name ?? settings.textGenerationModel;
125
115 const openKeybindingsFile = useCallback(() => {126 const openKeybindingsFile = useCallback(() => {
116 if (!keybindingsConfigPath) return;127 if (!keybindingsConfigPath) return;
117 setOpenKeybindingsError(null);128 setOpenKeybindingsError(null);
...@@ -502,6 +513,65 @@ function SettingsRouteView() {...@@ -502,6 +513,65 @@ function SettingsRouteView() {
502 </div>513 </div>
503 </section>514 </section>
504515
516 <section className="rounded-2xl border border-border bg-card p-5">
517 <div className="mb-4">
518 <h2 className="text-sm font-medium text-foreground">Git</h2>
519 <p className="mt-1 text-xs text-muted-foreground">
520 Configure the model used for generating commit messages, PR titles, and branch
521 names.
522 </p>
523 </div>
524
525 <div className="flex flex-col gap-4 rounded-lg border border-border bg-background px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
526 <div className="min-w-0 flex-1">
527 <p className="text-sm font-medium text-foreground">Text generation model</p>
528 <p className="text-xs text-muted-foreground">
529 Model used for auto-generated git content.
530 </p>
531 </div>
532 <Select
533 value={settings.textGenerationModel ?? DEFAULT_GIT_TEXT_GENERATION_MODEL}
534 onValueChange={(value) => {
535 if (value) {
536 updateSettings({
537 textGenerationModel: value,
538 });
539 }
540 }}
541 >
542 <SelectTrigger
543 className="w-full shrink-0 sm:w-48"
544 aria-label="Git text generation model"
545 >
546 <SelectValue>{selectedGitTextGenerationModelLabel}</SelectValue>
547 </SelectTrigger>
548 <SelectPopup align="end">
549 {gitTextGenerationModelOptions.map((option) => (
550 <SelectItem key={option.slug} value={option.slug}>
551 {option.name}
552 </SelectItem>
553 ))}
554 </SelectPopup>
555 </Select>
556 </div>
557
558 {settings.textGenerationModel !== defaults.textGenerationModel ? (
559 <div className="mt-3 flex justify-end">
560 <Button
561 size="xs"
562 variant="outline"
563 onClick={() =>
564 updateSettings({
565 textGenerationModel: defaults.textGenerationModel,
566 })
567 }
568 >
569 Restore default
570 </Button>
571 </div>
572 ) : null}
573 </section>
574
505 <section className="rounded-2xl border border-border bg-card p-5">575 <section className="rounded-2xl border border-border bg-card p-5">
506 <div className="mb-4">576 <div className="mb-4">
507 <h2 className="text-sm font-medium text-foreground">Threads</h2>577 <h2 className="text-sm font-medium text-foreground">Threads</h2>
packages/contracts/src/git.ts+5-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { Schema } from "effect";1import { Option, Schema } from "effect";
2import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";2import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";
3import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "./model";
34
4const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;5const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
56
...@@ -64,6 +65,9 @@ export const GitRunStackedActionInput = Schema.Struct({...@@ -64,6 +65,9 @@ export const GitRunStackedActionInput = Schema.Struct({
64 filePaths: Schema.optional(65 filePaths: Schema.optional(
65 Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)),66 Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)),
66 ),67 ),
68 textGenerationModel: Schema.optional(TrimmedNonEmptyStringSchema).pipe(
69 Schema.withConstructorDefault(() => Option.some(DEFAULT_GIT_TEXT_GENERATION_MODEL)),
70 ),
67});71});
68export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;72export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;
6973
packages/contracts/src/model.ts+3
...@@ -23,6 +23,7 @@ type ModelOption = {...@@ -23,6 +23,7 @@ type ModelOption = {
23export const MODEL_OPTIONS_BY_PROVIDER = {23export const MODEL_OPTIONS_BY_PROVIDER = {
24 codex: [24 codex: [
25 { slug: "gpt-5.4", name: "GPT-5.4" },25 { slug: "gpt-5.4", name: "GPT-5.4" },
26 { slug: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
26 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },27 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
27 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },28 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
28 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },29 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
...@@ -38,6 +39,8 @@ export const DEFAULT_MODEL_BY_PROVIDER = {...@@ -38,6 +39,8 @@ export const DEFAULT_MODEL_BY_PROVIDER = {
38 codex: "gpt-5.4",39 codex: "gpt-5.4",
39} as const satisfies Record<ProviderKind, ModelSlug>;40} as const satisfies Record<ProviderKind, ModelSlug>;
4041
42export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini" as const;
43
41export const MODEL_SLUG_ALIASES_BY_PROVIDER = {44export const MODEL_SLUG_ALIASES_BY_PROVIDER = {
42 codex: {45 codex: {
43 "5.4": "gpt-5.4",46 "5.4": "gpt-5.4",