From 5a57ada95bfe71553495556aced11c5e67b46516 Mon Sep 17 00:00:00 2001 From: Niklas Wojtkowiak Date: Thu, 12 Mar 2026 03:31:09 -0400 Subject: [PATCH] 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 6c98229e8..6b5bc0232 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 f5b9168ab..9070d295f 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 835779517..2b248d3c6 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 879927934..6234692de 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 85e901012..daa58d0f1 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 18e76d2f9..208d59773 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 e4fad02af..d86c607a8 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 9b5fe7731..180b170a1 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 b4afcdefa..e2a43e59f 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 081b4d0d8..4c06d8a73 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)),