From 5a57ada95bfe71553495556aced11c5e67b46516 Mon Sep 17 00:00:00 2001 From: Niklas Wojtkowiak Date: Thu, 12 Mar 2026 03:31:09 -0400 Subject: [PATCH 01/12] feat(git): support custom commit flags for app-run commits --- apps/server/src/git/Layers/GitCore.test.ts | 2 +- apps/server/src/git/Layers/GitCore.ts | 4 +- apps/server/src/git/Layers/GitManager.ts | 16 ++++++- apps/server/src/git/Services/GitCore.ts | 1 + apps/web/public/mockServiceWorker.js | 2 +- apps/web/src/appSettings.ts | 3 ++ apps/web/src/components/GitActionsControl.tsx | 5 +++ apps/web/src/lib/gitReactQuery.ts | 3 ++ apps/web/src/routes/_chat.settings.tsx | 43 +++++++++++++++++++ packages/contracts/src/git.ts | 1 + 10 files changed, 75 insertions(+), 5 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 6c98229e8a..6b5bc0232f 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -98,7 +98,7 @@ const makeIsolatedGitCore = (gitService: GitServiceShape) => status: (input) => core.status(input), statusDetails: (cwd) => core.statusDetails(cwd), prepareCommitContext: (cwd, filePaths?) => core.prepareCommitContext(cwd, filePaths), - commit: (cwd, subject, body) => core.commit(cwd, subject, body), + commit: (cwd, subject, body, extraArgs?) => core.commit(cwd, subject, body, extraArgs), pushCurrentBranch: (cwd, fallbackBranch) => core.pushCurrentBranch(cwd, fallbackBranch), pullCurrentBranch: (cwd) => core.pullCurrentBranch(cwd), readRangeContext: (cwd, baseBranch) => core.readRangeContext(cwd, baseBranch), diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index f5b9168abb..9070d295f3 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -804,9 +804,9 @@ const makeGitCore = Effect.gen(function* () { }; }); - const commit: GitCoreShape["commit"] = (cwd, subject, body) => + const commit: GitCoreShape["commit"] = (cwd, subject, body, extraArgs = []) => Effect.gen(function* () { - const args = ["commit", "-m", subject]; + const args = ["commit", ...extraArgs, "-m", subject]; const trimmedBody = body.trim(); if (trimmedBody.length > 0) { args.push("-m", trimmedBody); diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index 8357795173..2b248d3c64 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -207,6 +207,13 @@ interface CommitAndBranchSuggestion { commitMessage: string; } +function tokenizeCommitFlags(rawFlags?: string): string[] { + return (rawFlags ?? "") + .trim() + .split(/\s+/g) + .filter((value) => value.length > 0); +} + function formatCommitMessage(subject: string, body: string): string { const trimmedBody = body.trim(); if (trimmedBody.length === 0) { @@ -680,6 +687,7 @@ export const makeGitManager = Effect.gen(function* () { cwd: string, branch: string | null, commitMessage?: string, + commitFlags?: string, preResolvedSuggestion?: CommitAndBranchSuggestion, filePaths?: readonly string[], ) => @@ -696,7 +704,12 @@ export const makeGitManager = Effect.gen(function* () { return { status: "skipped_no_changes" as const }; } - const { commitSha } = yield* gitCore.commit(cwd, suggestion.subject, suggestion.body); + const { commitSha } = yield* gitCore.commit( + cwd, + suggestion.subject, + suggestion.body, + tokenizeCommitFlags(commitFlags), + ); return { status: "created" as const, commitSha, @@ -1042,6 +1055,7 @@ export const makeGitManager = Effect.gen(function* () { input.cwd, currentBranch, commitMessageForStep, + input.commitFlags, preResolvedCommitSuggestion, input.filePaths, ); diff --git a/apps/server/src/git/Services/GitCore.ts b/apps/server/src/git/Services/GitCore.ts index 879927934e..6234692dec 100644 --- a/apps/server/src/git/Services/GitCore.ts +++ b/apps/server/src/git/Services/GitCore.ts @@ -111,6 +111,7 @@ export interface GitCoreShape { cwd: string, subject: string, body: string, + extraArgs?: readonly string[], ) => Effect.Effect<{ commitSha: string }, GitCommandError>; /** diff --git a/apps/web/public/mockServiceWorker.js b/apps/web/public/mockServiceWorker.js index 85e9010123..daa58d0f12 100644 --- a/apps/web/public/mockServiceWorker.js +++ b/apps/web/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.12.9' +const PACKAGE_VERSION = '2.12.10' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index 18e76d2f92..208d59773e 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -21,6 +21,9 @@ const AppSettingsSchema = Schema.Struct({ codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe( Schema.withConstructorDefault(() => Option.some("")), ), + gitCommitFlags: Schema.String.check(Schema.isMaxLength(4096)).pipe( + Schema.withConstructorDefault(() => Option.some("")), + ), defaultThreadEnvMode: Schema.Literals(["local", "worktree"]).pipe( Schema.withConstructorDefault(() => Option.some("local")), ), diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index e4fad02af2..d86c607a87 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -32,6 +32,7 @@ import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Textarea } from "~/components/ui/textarea"; import { toastManager } from "~/components/ui/toast"; +import { useAppSettings } from "~/appSettings"; import { openInPreferredEditor } from "~/editorPreferences"; import { gitBranchesQueryOptions, @@ -158,6 +159,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions () => (activeThreadId ? { threadId: activeThreadId } : undefined), [activeThreadId], ); + const { settings } = useAppSettings(); const queryClient = useQueryClient(); const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); const [dialogCommitMessage, setDialogCommitMessage] = useState(""); @@ -199,6 +201,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions useIsMutating({ mutationKey: gitMutationKeys.runStackedAction(gitCwd) }) > 0; const isPullRunning = useIsMutating({ mutationKey: gitMutationKeys.pull(gitCwd) }) > 0; const isGitActionRunning = isRunStackedActionRunning || isPullRunning; + const configuredCommitFlags = settings.gitCommitFlags.trim(); const isDefaultBranch = useMemo(() => { const branchName = gitStatusForActions?.branch; if (!branchName) return false; @@ -349,6 +352,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions const promise = runImmediateGitActionMutation.mutateAsync({ action, ...(commitMessage ? { commitMessage } : {}), + ...(configuredCommitFlags ? { commitFlags: configuredCommitFlags } : {}), ...(featureBranch ? { featureBranch } : {}), ...(filePaths ? { filePaths } : {}), }); @@ -442,6 +446,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions }, [ + configuredCommitFlags, isDefaultBranch, runImmediateGitActionMutation, setPendingDefaultBranchAction, diff --git a/apps/web/src/lib/gitReactQuery.ts b/apps/web/src/lib/gitReactQuery.ts index 9b5fe7731f..180b170a1c 100644 --- a/apps/web/src/lib/gitReactQuery.ts +++ b/apps/web/src/lib/gitReactQuery.ts @@ -118,11 +118,13 @@ export function gitRunStackedActionMutationOptions(input: { mutationFn: async ({ action, commitMessage, + commitFlags, featureBranch, filePaths, }: { action: GitStackedAction; commitMessage?: string; + commitFlags?: string; featureBranch?: boolean; filePaths?: string[]; }) => { @@ -132,6 +134,7 @@ export function gitRunStackedActionMutationOptions(input: { cwd: input.cwd, action, ...(commitMessage ? { commitMessage } : {}), + ...(commitFlags ? { commitFlags } : {}), ...(featureBranch ? { featureBranch } : {}), ...(filePaths ? { filePaths } : {}), }); diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index b4afcdefa1..e2a43e59ff 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -109,6 +109,7 @@ function SettingsRouteView() { const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; + const gitCommitFlags = settings.gitCommitFlags; const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; @@ -545,6 +546,48 @@ function SettingsRouteView() { ) : null} +
+
+

Git

+

+ Configure extra flags for app-run git commit commands. +

+
+ +
+ + +
+ +
+
+
+

Responses

diff --git a/packages/contracts/src/git.ts b/packages/contracts/src/git.ts index 081b4d0d82..4c06d8a737 100644 --- a/packages/contracts/src/git.ts +++ b/packages/contracts/src/git.ts @@ -60,6 +60,7 @@ export const GitRunStackedActionInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, action: GitStackedAction, commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))), + commitFlags: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(4_096))), featureBranch: Schema.optional(Schema.Boolean), filePaths: Schema.optional( Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)), From 77a64493bbdc664609f4b55d1f041ff441550d03 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:05:40 -0700 Subject: [PATCH 02/12] Harden git commit extra flags - Move extra commit args after message flags to avoid swallowing `-m` - Drop non-flag tokens when tokenizing user-supplied commit flags - Add tests and clarify the settings helper text --- apps/server/src/git/Layers/GitCore.test.ts | 87 ++++++++++++++++++++++ apps/server/src/git/Layers/GitCore.ts | 5 +- apps/server/src/git/Layers/GitManager.ts | 14 +++- apps/web/src/routes/_chat.settings.tsx | 4 +- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index 6b5bc0232f..c1a6aa9984 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -12,6 +12,7 @@ import { GitCoreLive } from "./GitCore.ts"; import { GitCore, type GitCoreShape } from "../Services/GitCore.ts"; import { GitCommandError } from "../Errors.ts"; import { type ProcessRunResult, runProcess } from "../../processRunner.ts"; +import { tokenizeCommitFlags } from "./GitManager.ts"; // ── Helpers ── @@ -1903,4 +1904,90 @@ it.layer(TestLayer)("git integration", (it) => { }), ); }); + + describe("tokenizeCommitFlags", () => { + it("returns empty array for undefined", () => { + expect(tokenizeCommitFlags(undefined)).toEqual([]); + }); + + it("returns empty array for empty string", () => { + expect(tokenizeCommitFlags("")).toEqual([]); + }); + + it("returns empty array for whitespace-only string", () => { + expect(tokenizeCommitFlags(" ")).toEqual([]); + }); + + it("tokenizes a single flag", () => { + expect(tokenizeCommitFlags("--no-gpg-sign")).toEqual(["--no-gpg-sign"]); + }); + + it("tokenizes multiple flags separated by spaces", () => { + expect(tokenizeCommitFlags("--no-gpg-sign --signoff")).toEqual([ + "--no-gpg-sign", + "--signoff", + ]); + }); + + it("handles extra whitespace between flags", () => { + expect(tokenizeCommitFlags(" --no-gpg-sign --signoff ")).toEqual([ + "--no-gpg-sign", + "--signoff", + ]); + }); + + it("keeps flags with = values intact", () => { + expect(tokenizeCommitFlags("--cleanup=verbatim")).toEqual(["--cleanup=verbatim"]); + }); + + it("drops tokens that do not start with a dash", () => { + expect(tokenizeCommitFlags("--author Foo")).toEqual(["--author"]); + }); + + it("drops all non-flag tokens from a mixed input", () => { + expect(tokenizeCommitFlags("HEAD --no-verify somefile.txt -S")).toEqual([ + "--no-verify", + "-S", + ]); + }); + }); + + describe("commit with extraArgs", () => { + it.effect("passes extra flags through to git commit", () => + Effect.gen(function* () { + const tmp = yield* makeTmpDir(); + yield* initRepoWithCommit(tmp); + const core = yield* GitCore; + + yield* writeTextFile(path.join(tmp, "extra.txt"), "extra args test\n"); + yield* git(tmp, ["add", "extra.txt"]); + + const { commitSha } = yield* core.commit(tmp, "test extra args", "", [ + "--no-gpg-sign", + ]); + expect(commitSha).toBeTruthy(); + + // Verify the commit was created with the right message + const logOutput = yield* git(tmp, ["log", "-1", "--pretty=%s"]); + expect(logOutput).toBe("test extra args"); + }), + ); + + it.effect("works without extra args (backward compat)", () => + Effect.gen(function* () { + const tmp = yield* makeTmpDir(); + yield* initRepoWithCommit(tmp); + const core = yield* GitCore; + + yield* writeTextFile(path.join(tmp, "compat.txt"), "compat test\n"); + yield* git(tmp, ["add", "compat.txt"]); + + const { commitSha } = yield* core.commit(tmp, "no extra args", ""); + expect(commitSha).toBeTruthy(); + + const logOutput = yield* git(tmp, ["log", "-1", "--pretty=%s"]); + expect(logOutput).toBe("no extra args"); + }), + ); + }); }); diff --git a/apps/server/src/git/Layers/GitCore.ts b/apps/server/src/git/Layers/GitCore.ts index 9070d295f3..2f86e58d8a 100644 --- a/apps/server/src/git/Layers/GitCore.ts +++ b/apps/server/src/git/Layers/GitCore.ts @@ -806,11 +806,14 @@ const makeGitCore = Effect.gen(function* () { const commit: GitCoreShape["commit"] = (cwd, subject, body, extraArgs = []) => Effect.gen(function* () { - const args = ["commit", ...extraArgs, "-m", subject]; + const args = ["commit", "-m", subject]; const trimmedBody = body.trim(); if (trimmedBody.length > 0) { args.push("-m", trimmedBody); } + // Append extra flags after the message args so an argument-consuming + // flag (e.g. `-C`) cannot accidentally swallow `-m` or the subject. + args.push(...extraArgs); yield* runGit("GitCore.commit.commit", cwd, args); const commitSha = yield* runGitStdout("GitCore.commit.revParseHead", cwd, [ "rev-parse", diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index 2b248d3c64..9ed2add0f1 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -207,11 +207,21 @@ interface CommitAndBranchSuggestion { commitMessage: string; } -function tokenizeCommitFlags(rawFlags?: string): string[] { - return (rawFlags ?? "") +/** + * Tokenize and sanitise user-provided extra flags for `git commit`. + * + * Each token must look like a CLI flag (start with `-`). Tokens that don't + * start with a dash are silently dropped so that a malformed value like + * `--author "Foo"` (which the naive split turns into `["--author", "Foo"]`) + * cannot inject a positional argument into the git invocation. + */ +export function tokenizeCommitFlags(rawFlags?: string): string[] { + const tokens = (rawFlags ?? "") .trim() .split(/\s+/g) .filter((value) => value.length > 0); + + return tokens.filter((token) => token.startsWith("-")); } function formatCommitMessage(subject: string, body: string): string { diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index e2a43e59ff..50537c94f7 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -567,8 +567,8 @@ function SettingsRouteView() { spellCheck={false} /> - Applied to app-run git commit commands only. Example: --no-gpg-sign - . Quoted arguments are not supported yet. + Applied to app-run git commit commands only. Example:{" "} + --no-gpg-sign. Quoted arguments are not supported yet. From ee12fb6924a39b87967cc654e05a16eb1370efd8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:07:52 -0700 Subject: [PATCH 03/12] fmt --- apps/server/src/git/Layers/GitCore.test.ts | 4 +--- apps/web/src/routes/_chat.settings.tsx | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index c1a6aa9984..fe3f45d72c 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -1962,9 +1962,7 @@ it.layer(TestLayer)("git integration", (it) => { yield* writeTextFile(path.join(tmp, "extra.txt"), "extra args test\n"); yield* git(tmp, ["add", "extra.txt"]); - const { commitSha } = yield* core.commit(tmp, "test extra args", "", [ - "--no-gpg-sign", - ]); + const { commitSha } = yield* core.commit(tmp, "test extra args", "", ["--no-gpg-sign"]); expect(commitSha).toBeTruthy(); // Verify the commit was created with the right message diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 708b75aeb8..9bd021c27f 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -637,8 +637,8 @@ function SettingsRouteView() { spellCheck={false} /> - Applied to app-run git commit commands only. Example:{" "} - --no-gpg-sign. Quoted arguments are not supported yet. + Applied to app-run git commit commands only. Example: --no-gpg-sign + . Quoted arguments are not supported yet. From 5a07724cb13fe6901a54877e09c0e4e524d20a13 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:21:22 -0700 Subject: [PATCH 04/12] defaults --- apps/web/src/appSettings.ts | 39 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/web/src/appSettings.ts b/apps/web/src/appSettings.ts index b3f9f981c2..e5794bbc60 100644 --- a/apps/web/src/appSettings.ts +++ b/apps/web/src/appSettings.ts @@ -14,30 +14,33 @@ const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record codex: new Set(getModelOptions("codex").map((option) => option.slug)), }; +const withDefaults = + < + S extends Schema.Top & Schema.WithoutConstructorDefault, + D extends S["~type.make.in"] & S["Encoded"], + >( + fallback: () => D, + ) => + (schema: S) => + schema.pipe( + Schema.withConstructorDefault(() => Option.some(fallback())), + Schema.withDecodingDefault(() => fallback()), + ); + const AppSettingsSchema = Schema.Struct({ - codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe( - Schema.withConstructorDefault(() => Option.some("")), - ), - codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe( - Schema.withConstructorDefault(() => Option.some("")), - ), - gitCommitFlags: Schema.String.check(Schema.isMaxLength(4096)).pipe( - Schema.withConstructorDefault(() => Option.some("")), - ), + codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), + codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), + gitCommitFlags: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")), defaultThreadEnvMode: Schema.Literals(["local", "worktree"]).pipe( - Schema.withConstructorDefault(() => Option.some("local")), - ), - confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))), - enableAssistantStreaming: Schema.Boolean.pipe( - Schema.withConstructorDefault(() => Option.some(false)), + withDefaults(() => "local" as const), ), + confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)), + enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)), timestampFormat: Schema.Literals(["locale", "12-hour", "24-hour"]).pipe( - Schema.withConstructorDefault(() => Option.some(DEFAULT_TIMESTAMP_FORMAT)), - ), - customCodexModels: Schema.Array(Schema.String).pipe( - Schema.withConstructorDefault(() => Option.some([])), + withDefaults(() => DEFAULT_TIMESTAMP_FORMAT), ), textGenerationModel: Schema.optional(TrimmedNonEmptyString), + customCodexModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])), }); export type AppSettings = typeof AppSettingsSchema.Type; export interface AppModelOption { From c7c060fb5e6d46e336f96d78c96e770bdefb788b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:26:48 -0700 Subject: [PATCH 05/12] fix: merge duplicate Git settings sections into one Co-Authored-By: Claude Opus 4.6 --- apps/web/src/routes/_chat.settings.tsx | 133 ++++++++++--------------- 1 file changed, 54 insertions(+), 79 deletions(-) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 9bd021c27f..c21632fff8 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -518,59 +518,76 @@ function SettingsRouteView() {

Git

- Configure the model used for generating commit messages, PR titles, and branch - names. + Configure git-related settings for text generation and commit commands.

-
-
-

Text generation model

-

- Model used for auto-generated git content. -

-
- { + if (value) { + updateSettings({ + textGenerationModel: value, + }); + } + }} > - {selectedGitTextGenerationModelLabel} - - - {gitTextGenerationModelOptions.map((option) => ( - - {option.name} - - ))} - - -
+ + {selectedGitTextGenerationModelLabel} + + + {gitTextGenerationModelOptions.map((option) => ( + + {option.name} + + ))} + + +
- {settings.textGenerationModel !== defaults.textGenerationModel ? ( -
+ + +
- ) : null} +
@@ -616,48 +633,6 @@ function SettingsRouteView() { ) : null}
-
-
-

Git

-

- Configure extra flags for app-run git commit commands. -

-
- -
- - -
- -
-
-
-

Responses

From e106c1b1a16ee7e17cbd3bffbe3fc856cb1b7cc7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:30:27 -0700 Subject: [PATCH 06/12] fix: remove duplicate useAppSettings import and call Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/GitActionsControl.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 697380ea76..5b9d505d16 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -33,7 +33,6 @@ import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; import { ScrollArea } from "~/components/ui/scroll-area"; import { Textarea } from "~/components/ui/textarea"; import { toastManager } from "~/components/ui/toast"; -import { useAppSettings } from "~/appSettings"; import { openInPreferredEditor } from "~/editorPreferences"; import { gitBranchesQueryOptions, @@ -161,7 +160,6 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions () => (activeThreadId ? { threadId: activeThreadId } : undefined), [activeThreadId], ); - const { settings } = useAppSettings(); const queryClient = useQueryClient(); const [isCommitDialogOpen, setIsCommitDialogOpen] = useState(false); const [dialogCommitMessage, setDialogCommitMessage] = useState(""); From 5016e598e4e1d323765beb8a64d0038756a96815 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:34:42 -0700 Subject: [PATCH 07/12] fix: flatten text generation model input to match commit flags style Co-Authored-By: Claude Opus 4.6 --- apps/web/src/routes/_chat.settings.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index c21632fff8..b01588da01 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -523,13 +523,10 @@ function SettingsRouteView() {
-
-
-

Text generation model

-

- Model used for auto-generated git content. -

-
+
+ + Model used for auto-generated git content. + + -
- -
+ {(settings.textGenerationModel !== defaults.textGenerationModel || + settings.gitCommitFlags !== defaults.gitCommitFlags) ? ( +
+ +
+ ) : null}
From bc62ef204753b3c4089746da167df63a8dc94689 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:38:09 -0700 Subject: [PATCH 09/12] feat: add client-side validation for git commit flags input Shows a yellow warning when non-flag tokens are entered (tokens not starting with a dash), matching the server-side sanitization behavior. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/routes/_chat.settings.tsx | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 869a7d1c5d..23e83d9a23 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { type ProviderKind, DEFAULT_GIT_TEXT_GENERATION_MODEL } from "@t3tools/contracts"; import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { getAppModelOptions, MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; @@ -110,6 +110,16 @@ function SettingsRouteView() { const codexBinaryPath = settings.codexBinaryPath; const codexHomePath = settings.codexHomePath; const gitCommitFlags = settings.gitCommitFlags; + const gitCommitFlagsWarning = useMemo(() => { + const trimmed = gitCommitFlags.trim(); + if (!trimmed) return null; + const tokens = trimmed.split(/\s+/); + const invalidTokens = tokens.filter((t) => !t.startsWith("-")); + if (invalidTokens.length > 0) { + return `Non-flag tokens will be ignored: ${invalidTokens.join(", ")}`; + } + return null; + }, [gitCommitFlags]); const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; const availableEditors = serverConfigQuery.data?.availableEditors; @@ -566,11 +576,18 @@ function SettingsRouteView() { onChange={(event) => updateSettings({ gitCommitFlags: event.target.value })} placeholder="--no-gpg-sign" spellCheck={false} + className={gitCommitFlagsWarning ? "border-yellow-500" : undefined} /> - - Applied to app-run git commit commands only. Example:{" "} - --no-gpg-sign. Quoted arguments are not supported yet. - + {gitCommitFlagsWarning ? ( + + {gitCommitFlagsWarning} + + ) : ( + + Applied to app-run git commit commands only. Example:{" "} + --no-gpg-sign. Quoted arguments are not supported yet. + + )} {(settings.textGenerationModel !== defaults.textGenerationModel || From a65db9afe4354d04aad30d77b85e11e199730de5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:42:33 -0700 Subject: [PATCH 10/12] fix: detect quoted arguments in commit flags validation Show a clear message about using = syntax instead of quotes, before falling through to the non-flag token check. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/routes/_chat.settings.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 23e83d9a23..91b9c39db8 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -113,6 +113,9 @@ function SettingsRouteView() { const gitCommitFlagsWarning = useMemo(() => { const trimmed = gitCommitFlags.trim(); if (!trimmed) return null; + if (/["']/.test(trimmed)) { + return "Quoted arguments are not supported. Use = syntax instead (e.g. --cleanup=verbatim)."; + } const tokens = trimmed.split(/\s+/); const invalidTokens = tokens.filter((t) => !t.startsWith("-")); if (invalidTokens.length > 0) { From cb39166476662aec9982b67d1e271af342237130 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:49:57 -0700 Subject: [PATCH 11/12] feat: support quoted arguments in git commit flags Update tokenizer to handle --flag="value with spaces" and --flag='value with spaces' syntax. Update client-side validation to match. Remove "not supported yet" disclaimer from UI. Co-Authored-By: Claude Opus 4.6 --- apps/server/src/git/Layers/GitCore.test.ts | 19 +++++++++++++++++++ apps/server/src/git/Layers/GitManager.ts | 22 ++++++++++++++-------- apps/web/src/routes/_chat.settings.tsx | 10 ++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/apps/server/src/git/Layers/GitCore.test.ts b/apps/server/src/git/Layers/GitCore.test.ts index fe3f45d72c..2ac369f460 100644 --- a/apps/server/src/git/Layers/GitCore.test.ts +++ b/apps/server/src/git/Layers/GitCore.test.ts @@ -1950,6 +1950,25 @@ it.layer(TestLayer)("git integration", (it) => { "-S", ]); }); + + it("handles double-quoted values", () => { + expect(tokenizeCommitFlags('--author="Foo Bar"')).toEqual(['--author="Foo Bar"']); + }); + + it("handles double-quoted values with space separation", () => { + expect(tokenizeCommitFlags('--author "Foo Bar"')).toEqual(["--author"]); + }); + + it("handles single-quoted values", () => { + expect(tokenizeCommitFlags("--author='Foo Bar'")).toEqual(["--author='Foo Bar'"]); + }); + + it("handles mixed quoted and unquoted flags", () => { + expect(tokenizeCommitFlags('--no-gpg-sign --cleanup="strip"')).toEqual([ + "--no-gpg-sign", + '--cleanup="strip"', + ]); + }); }); describe("commit with extraArgs", () => { diff --git a/apps/server/src/git/Layers/GitManager.ts b/apps/server/src/git/Layers/GitManager.ts index 10152f5352..379703a890 100644 --- a/apps/server/src/git/Layers/GitManager.ts +++ b/apps/server/src/git/Layers/GitManager.ts @@ -210,16 +210,22 @@ interface CommitAndBranchSuggestion { /** * Tokenize and sanitise user-provided extra flags for `git commit`. * - * Each token must look like a CLI flag (start with `-`). Tokens that don't - * start with a dash are silently dropped so that a malformed value like - * `--author "Foo"` (which the naive split turns into `["--author", "Foo"]`) - * cannot inject a positional argument into the git invocation. + * Supports quoted values: `--author="Foo Bar"` or `--author "Foo Bar"`. + * Each resulting token must look like a CLI flag (start with `-`) to be + * included — bare positional arguments are silently dropped. */ export function tokenizeCommitFlags(rawFlags?: string): string[] { - const tokens = (rawFlags ?? "") - .trim() - .split(/\s+/g) - .filter((value) => value.length > 0); + const input = (rawFlags ?? "").trim(); + if (!input) return []; + + // Matches: unquoted runs of non-whitespace that may contain quoted segments + // e.g. --author="Foo Bar", --flag='val ue', --simple, -S + const tokens: string[] = []; + const pattern = /(?:[^\s"']+(?:(?:"[^"]*"|'[^']*')[^\s"']*)*)|"[^"]*"|'[^']*'/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(input)) !== null) { + tokens.push(match[0]); + } return tokens.filter((token) => token.startsWith("-")); } diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 91b9c39db8..f3cab8c84b 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -113,10 +113,12 @@ function SettingsRouteView() { const gitCommitFlagsWarning = useMemo(() => { const trimmed = gitCommitFlags.trim(); if (!trimmed) return null; - if (/["']/.test(trimmed)) { - return "Quoted arguments are not supported. Use = syntax instead (e.g. --cleanup=verbatim)."; + const tokens: string[] = []; + const pattern = /(?:[^\s"']+(?:(?:"[^"]*"|'[^']*')[^\s"']*)*)|"[^"]*"|'[^']*'/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(trimmed)) !== null) { + tokens.push(match[0]); } - const tokens = trimmed.split(/\s+/); const invalidTokens = tokens.filter((t) => !t.startsWith("-")); if (invalidTokens.length > 0) { return `Non-flag tokens will be ignored: ${invalidTokens.join(", ")}`; @@ -588,7 +590,7 @@ function SettingsRouteView() { ) : ( Applied to app-run git commit commands only. Example:{" "} - --no-gpg-sign. Quoted arguments are not supported yet. + --no-gpg-sign. Supports quoted values. )} From b570ff5a6b5db040b4be4d1b411194041b63b65b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 19 Mar 2026 12:53:20 -0700 Subject: [PATCH 12/12] Tighten settings layout for git options - Keep the text generation and git commit flag controls compact - Remove the extra git commit flag help text - Simplify the reset button conditional formatting --- apps/web/src/routes/_chat.settings.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index f3cab8c84b..1ffe89bb75 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -539,9 +539,7 @@ function SettingsRouteView() {