diff --git a/apps/server/src/git/Layers/CodexTextGeneration.ts b/apps/server/src/git/Layers/CodexTextGeneration.ts index 9a8d1d93f0..6a8e79340c 100644 --- a/apps/server/src/git/Layers/CodexTextGeneration.ts +++ b/apps/server/src/git/Layers/CodexTextGeneration.ts @@ -17,7 +17,7 @@ import { TextGeneration, } from "../Services/TextGeneration.ts"; -const CODEX_MODEL = "gpt-5.3-codex"; +const DEFAULT_CODEX_MODEL = "gpt-5.3-codex"; const CODEX_REASONING_EFFORT = "low"; const CODEX_TIMEOUT_MS = 180_000; @@ -187,6 +187,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { outputSchemaJson, imagePaths = [], cleanupPaths = [], + model, }: { operation: "generateCommitMessage" | "generatePrContent" | "generateBranchName"; cwd: string; @@ -194,6 +195,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { outputSchemaJson: S; imagePaths?: ReadonlyArray; cleanupPaths?: ReadonlyArray; + model?: string; }): Effect.Effect => Effect.gen(function* () { const schemaPath = yield* writeTempFile( @@ -212,7 +214,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { "-s", "read-only", "--model", - CODEX_MODEL, + model ?? DEFAULT_CODEX_MODEL, "--config", `model_reasoning_effort="${CODEX_REASONING_EFFORT}"`, "--output-schema", @@ -353,6 +355,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { cwd: input.cwd, prompt, outputSchemaJson, + ...(input.model ? { model: input.model } : {}), }).pipe( Effect.map( (generated) => @@ -398,6 +401,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { title: Schema.String, body: Schema.String, }), + ...(input.model ? { model: input.model } : {}), }).pipe( Effect.map( (generated) => @@ -449,6 +453,7 @@ const makeCodexTextGeneration = Effect.gen(function* () { branch: Schema.String, }), imagePaths, + ...(input.model ? { model: input.model } : {}), }); return { diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index 8357795173..84a07102a0 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -639,6 +639,8 @@ export const makeGitManager = Effect.gen(function* () { /** When true, also produce a semantic feature branch name. */ includeBranch?: boolean; filePaths?: readonly string[]; + /** Optional model to use for commit message generation. */ + model?: string; }) => Effect.gen(function* () { const context = yield* gitCore.prepareCommitContext(input.cwd, input.filePaths); @@ -665,6 +667,7 @@ export const makeGitManager = Effect.gen(function* () { stagedSummary: limitContext(context.stagedSummary, 8_000), stagedPatch: limitContext(context.stagedPatch, 50_000), ...(input.includeBranch ? { includeBranch: true } : {}), + ...(input.model ? { model: input.model } : {}), }) .pipe(Effect.map((result) => sanitizeCommitMessage(result))); @@ -682,6 +685,7 @@ export const makeGitManager = Effect.gen(function* () { commitMessage?: string, preResolvedSuggestion?: CommitAndBranchSuggestion, filePaths?: readonly string[], + model?: string, ) => Effect.gen(function* () { const suggestion = @@ -691,6 +695,7 @@ export const makeGitManager = Effect.gen(function* () { branch, ...(commitMessage ? { commitMessage } : {}), ...(filePaths ? { filePaths } : {}), + ...(model ? { model } : {}), })); if (!suggestion) { return { status: "skipped_no_changes" as const }; @@ -704,7 +709,7 @@ export const makeGitManager = Effect.gen(function* () { }; }); - const runPrStep = (cwd: string, fallbackBranch: string | null) => + const runPrStep = (cwd: string, fallbackBranch: string | null, model?: string) => Effect.gen(function* () { const details = yield* gitCore.statusDetails(cwd); const branch = details.branch ?? fallbackBranch; @@ -748,6 +753,7 @@ export const makeGitManager = Effect.gen(function* () { commitSummary: limitContext(rangeContext.commitSummary, 20_000), diffSummary: limitContext(rangeContext.diffSummary, 20_000), diffPatch: limitContext(rangeContext.diffPatch, 60_000), + ...(model ? { model } : {}), }); const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${randomUUID()}.md`); @@ -972,6 +978,7 @@ export const makeGitManager = Effect.gen(function* () { branch: string | null, commitMessage?: string, filePaths?: readonly string[], + model?: string, ) => Effect.gen(function* () { const suggestion = yield* resolveCommitAndBranchSuggestion({ @@ -980,6 +987,7 @@ export const makeGitManager = Effect.gen(function* () { ...(commitMessage ? { commitMessage } : {}), ...(filePaths ? { filePaths } : {}), includeBranch: true, + ...(model ? { model } : {}), }); if (!suggestion) { return yield* gitManagerError( @@ -1028,6 +1036,7 @@ export const makeGitManager = Effect.gen(function* () { initialStatus.branch, input.commitMessage, input.filePaths, + input.model, ); branchStep = result.branchStep; commitMessageForStep = result.resolvedCommitMessage; @@ -1044,6 +1053,7 @@ export const makeGitManager = Effect.gen(function* () { commitMessageForStep, preResolvedCommitSuggestion, input.filePaths, + input.model, ); const push = wantsPush @@ -1051,7 +1061,7 @@ export const makeGitManager = Effect.gen(function* () { : { status: "skipped_not_requested" as const }; const pr = wantsPr - ? yield* runPrStep(input.cwd, currentBranch) + ? yield* runPrStep(input.cwd, currentBranch, input.model) : { status: "skipped_not_requested" as const }; return { diff --git a/apps/server/src/git/Services/TextGeneration.ts b/apps/server/src/git/Services/TextGeneration.ts index daae27fe66..95c180dc31 100644 --- a/apps/server/src/git/Services/TextGeneration.ts +++ b/apps/server/src/git/Services/TextGeneration.ts @@ -19,6 +19,8 @@ export interface CommitMessageGenerationInput { stagedPatch: string; /** When true, the model also returns a semantic branch name for the change. */ includeBranch?: boolean; + /** Optional model to use for generation. Falls back to default if not provided. */ + model?: string; } export interface CommitMessageGenerationResult { @@ -35,6 +37,8 @@ export interface PrContentGenerationInput { commitSummary: string; diffSummary: string; diffPatch: string; + /** Optional model to use for generation. Falls back to default if not provided. */ + model?: string; } export interface PrContentGenerationResult { @@ -46,6 +50,8 @@ export interface BranchNameGenerationInput { cwd: string; message: string; attachments?: ReadonlyArray | undefined; + /** Optional model to use for generation. Falls back to default if not provided. */ + model?: string; } export interface BranchNameGenerationResult { diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index fe02188450..7acaa8bcc6 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -365,6 +365,7 @@ const make = Effect.gen(function* () { readonly messageId: string; readonly messageText: string; readonly attachments?: ReadonlyArray; + readonly model?: string; }) { if (!input.branch || !input.worktreePath) { return; @@ -391,6 +392,7 @@ const make = Effect.gen(function* () { cwd, message: input.messageText, ...(attachments.length > 0 ? { attachments } : {}), + ...(input.model ? { model: input.model } : {}), }) .pipe( Effect.catch((error) => @@ -459,6 +461,7 @@ const make = Effect.gen(function* () { messageId: message.id, messageText: message.text, ...(message.attachments !== undefined ? { attachments: message.attachments } : {}), + ...(event.payload.model !== undefined ? { model: event.payload.model } : {}), }).pipe(Effect.forkScoped); yield* sendTurnForThread({ diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 2784b65180..81cf880c22 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3241,6 +3241,7 @@ export default function ChatView({ threadId }: ChatViewProps) { diffToggleShortcutLabel={diffPanelShortcutLabel} gitCwd={gitCwd} diffOpen={diffOpen} + selectedModel={selectedModel} onRunProjectScript={(script) => { void runProjectScript(script); }} diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index e4fad02af2..dae3745b25 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -1,4 +1,4 @@ -import type { GitStackedAction, GitStatusResult, ThreadId } from "@t3tools/contracts"; +import type { GitStackedAction, GitStatusResult, ModelSlug, ThreadId } from "@t3tools/contracts"; import { useIsMutating, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon, InfoIcon } from "lucide-react"; @@ -48,6 +48,7 @@ import { readNativeApi } from "~/nativeApi"; interface GitActionsControlProps { gitCwd: string | null; activeThreadId: ThreadId | null; + selectedModel?: ModelSlug | undefined; } interface PendingDefaultBranchAction { @@ -153,7 +154,11 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { return ; } -export default function GitActionsControl({ gitCwd, activeThreadId }: GitActionsControlProps) { +export default function GitActionsControl({ + gitCwd, + activeThreadId, + selectedModel, +}: GitActionsControlProps) { const threadToastData = useMemo( () => (activeThreadId ? { threadId: activeThreadId } : undefined), [activeThreadId], @@ -351,6 +356,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions ...(commitMessage ? { commitMessage } : {}), ...(featureBranch ? { featureBranch } : {}), ...(filePaths ? { filePaths } : {}), + ...(selectedModel ? { model: selectedModel } : {}), }); try { @@ -447,6 +453,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions setPendingDefaultBranchAction, threadToastData, gitStatusForActions, + selectedModel, ], ); diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index ea7f911bec..90d5c38f55 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -3,6 +3,7 @@ import { type ProjectScript, type ResolvedKeybindingsConfig, type ThreadId, + type ModelSlug, } from "@t3tools/contracts"; import { memo } from "react"; import GitActionsControl from "../GitActionsControl"; @@ -27,6 +28,7 @@ interface ChatHeaderProps { diffToggleShortcutLabel: string | null; gitCwd: string | null; diffOpen: boolean; + selectedModel: ModelSlug | undefined; onRunProjectScript: (script: ProjectScript) => void; onAddProjectScript: (input: NewProjectScriptInput) => Promise; onUpdateProjectScript: (scriptId: string, input: NewProjectScriptInput) => Promise; @@ -47,6 +49,7 @@ export const ChatHeader = memo(function ChatHeader({ diffToggleShortcutLabel, gitCwd, diffOpen, + selectedModel, onRunProjectScript, onAddProjectScript, onUpdateProjectScript, @@ -93,7 +96,13 @@ export const ChatHeader = memo(function ChatHeader({ openInCwd={openInCwd} /> )} - {activeProjectName && } + {activeProjectName && ( + + )} { const api = ensureNativeApi(); if (!input.cwd) throw new Error("Git action is unavailable."); @@ -134,6 +136,7 @@ export function gitRunStackedActionMutationOptions(input: { ...(commitMessage ? { commitMessage } : {}), ...(featureBranch ? { featureBranch } : {}), ...(filePaths ? { filePaths } : {}), + ...(model ? { model } : {}), }); }, onSettled: async () => { diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 081b4d0d82..ed7d93ebd5 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -64,6 +64,7 @@ export const GitRunStackedActionInput = Schema.Struct({ filePaths: Schema.optional( Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)), ), + model: Schema.optional(Schema.String), }); export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;