| author | |
| committer | |
| log | 579a69eb7756b2169d26299d36f936838db39caa |
| tree | 594f6b85cfdb645425b60063c03580bf64fb0d41 |
| parent | 9ee8c4f5430a221d6569ba9c2f984d8501ec4fe5 |
| signature | Signed by PGP key B5690EEEBB952194 |
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"; |
| 3 | import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; | 3 | import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect"; |
| 4 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 4 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| 5 | 5 | ||
| 6 | import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts"; | ||
| 6 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; | 7 | import { sanitizeBranchFragment, sanitizeFeatureBranchName } from "@t3tools/shared/git"; |
| 7 | 8 | ||
| 8 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 9 | import { 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"; |
| 19 | 20 | ||
| 20 | const CODEX_MODEL = "gpt-5.3-codex"; | ||
| 21 | const CODEX_REASONING_EFFORT = "low"; | 21 | const CODEX_REASONING_EFFORT = "low"; |
| 22 | const CODEX_TIMEOUT_MS = 180_000; | 22 | const CODEX_TIMEOUT_MS = 180_000; |
| 23 | 23 | ||
| ... | @@ -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 | }); |
| 453 | 458 | ||
| 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))); |
| 670 | 672 | ||
| ... | @@ -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 | }); |
| 706 | 710 | ||
| 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 | }); |
| 752 | 757 | ||
| 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 | ); |
| 1048 | 1057 | ||
| 1049 | const push = wantsPush | 1058 | 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 }; |
| 1052 | 1061 | ||
| 1053 | const pr = wantsPr | 1062 | 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 }; |
| 1056 | 1065 | ||
| 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 | } |
| 23 | 25 | ||
| 24 | export interface CommitMessageGenerationResult { | 26 | export 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 | } |
| 39 | 43 | ||
| 40 | export interface PrContentGenerationResult { | 44 | export 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 | } |
| 50 | 56 | ||
| 51 | export interface BranchNameGenerationResult { | 57 | export interface BranchNameGenerationResult { |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+2| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { | 1 | import { |
| 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", () => { |
| 28 | 28 | ||
| 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 @@ |
| 1 | import { useCallback } from "react"; | 1 | import { useCallback } from "react"; |
| 2 | import { Option, Schema } from "effect"; | 2 | import { Option, Schema } from "effect"; |
| 3 | import { type ProviderKind } from "@t3tools/contracts"; | 3 | import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts"; |
| 4 | import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; | 4 | import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; |
| 5 | import { useLocalStorage } from "./hooks/useLocalStorage"; | 5 | import { useLocalStorage } from "./hooks/useLocalStorage"; |
| 6 | 6 | ||
| ... | @@ -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 | }); |
| 38 | export type AppSettings = typeof AppSettingsSchema.Type; | 39 | export type AppSettings = typeof AppSettingsSchema.Type; |
| 39 | export interface AppModelOption { | 40 | export 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"; |
| 18 | import { useAppSettings } from "~/appSettings"; | ||
| 18 | import { Button } from "~/components/ui/button"; | 19 | import { Button } from "~/components/ui/button"; |
| 19 | import { Checkbox } from "~/components/ui/checkbox"; | 20 | import { Checkbox } from "~/components/ui/checkbox"; |
| 20 | import { | 21 | import { |
| ... | @@ -154,6 +155,7 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { | ... | @@ -154,6 +155,7 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { |
| 154 | } | 155 | } |
| 155 | 156 | ||
| 156 | export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) { | 157 | export 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 })); |
| 192 | 194 | ||
| 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 })); |
| 197 | 203 |
apps/web/src/lib/gitReactQuery.ts+2| ... | @@ -112,6 +112,7 @@ export function gitCheckoutMutationOptions(input: { | ... | @@ -112,6 +112,7 @@ export function gitCheckoutMutationOptions(input: { |
| 112 | export function gitRunStackedActionMutationOptions(input: { | 112 | export 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 @@ |
| 1 | import { createFileRoute } from "@tanstack/react-router"; | 1 | import { createFileRoute } from "@tanstack/react-router"; |
| 2 | import { useQuery } from "@tanstack/react-query"; | 2 | import { useQuery } from "@tanstack/react-query"; |
| 3 | import { useCallback, useState } from "react"; | 3 | import { useCallback, useState } from "react"; |
| 4 | import { type ProviderKind } from "@t3tools/contracts"; | 4 | import { type ProviderKind, DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts"; |
| 5 | import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; | 5 | import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; |
| 6 | import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; | 6 | import { getAppModelOptions, MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; |
| 7 | import { resolveAndPersistPreferredEditor } from "../editorPreferences"; | 7 | import { resolveAndPersistPreferredEditor } from "../editorPreferences"; |
| 8 | import { isElectron } from "../env"; | 8 | import { isElectron } from "../env"; |
| 9 | import { useTheme } from "../hooks/useTheme"; | 9 | import { 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; |
| 114 | 114 | ||
| 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> |
| 504 | 515 | ||
| 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 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Option, Schema } from "effect"; |
| 2 | import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas"; | 2 | import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas"; |
| 3 | import { DEFAULT_GIT_TEXT_GENERATION_MODEL } from "./model"; | ||
| 3 | 4 | ||
| 4 | const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; | 5 | const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; |
| 5 | 6 | ||
| ... | @@ -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 | }); |
| 68 | export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type; | 72 | export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type; |
| 69 | 73 |
packages/contracts/src/model.ts+3| ... | @@ -23,6 +23,7 @@ type ModelOption = { | ... | @@ -23,6 +23,7 @@ type ModelOption = { |
| 23 | export const MODEL_OPTIONS_BY_PROVIDER = { | 23 | export 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>; |
| 40 | 41 | ||
| 42 | export const DEFAULT_GIT_TEXT_GENERATION_MODEL = "gpt-5.4-mini" as const; | ||
| 43 | |||
| 41 | export const MODEL_SLUG_ALIASES_BY_PROVIDER = { | 44 | export const MODEL_SLUG_ALIASES_BY_PROVIDER = { |
| 42 | codex: { | 45 | codex: { |
| 43 | "5.4": "gpt-5.4", | 46 | "5.4": "gpt-5.4", |