diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts index d4c4768d2c8..6c41085c3f6 100644 --- a/apps/desktop/src/clientPersistence.test.ts +++ b/apps/desktop/src/clientPersistence.test.ts @@ -49,6 +49,7 @@ function makeSecretStorage(available: boolean): DesktopSecretStorage { } const clientSettings: ClientSettings = { + autoCreatePrOnPush: true, autoOpenPlanSidebar: false, confirmThreadArchive: true, confirmThreadDelete: false, diff --git a/apps/web/src/components/GitActionsControl.logic.test.ts b/apps/web/src/components/GitActionsControl.logic.test.ts index 7950753330e..2cc8913cb60 100644 --- a/apps/web/src/components/GitActionsControl.logic.test.ts +++ b/apps/web/src/components/GitActionsControl.logic.test.ts @@ -1131,3 +1131,73 @@ describe("resolveAutoFeatureBranchName", () => { assert.equal(ref, "feature/update"); }); }); + +describe("when: autoCreatePr is disabled", () => { + it("downgrades feature-branch commit+push from PR to plain commit & push", () => { + const quick = resolveQuickAction( + status({ hasWorkingTreeChanges: true }), + false, + false, + true, + false, + ); + assert.deepInclude(quick, { + kind: "run_action", + action: "commit_push", + label: "Commit & push", + disabled: false, + }); + }); + + it("downgrades feature-branch ahead-with-upstream from create PR to plain push", () => { + const quick = resolveQuickAction(status({ aheadCount: 2 }), false, false, true, false); + assert.deepInclude(quick, { + kind: "run_action", + action: "push", + label: "Push", + disabled: false, + }); + }); + + it("downgrades feature-branch ahead-without-upstream from create PR to plain push", () => { + const quick = resolveQuickAction( + status({ aheadCount: 1, hasUpstream: false }), + false, + false, + true, + false, + ); + assert.deepInclude(quick, { + kind: "run_action", + action: "push", + label: "Push", + disabled: false, + }); + }); + + it("preserves Commit, push & PR when autoCreatePr is true (default)", () => { + const quick = resolveQuickAction( + status({ hasWorkingTreeChanges: true }), + false, + false, + true, + true, + ); + assert.deepInclude(quick, { + kind: "run_action", + action: "commit_push_pr", + label: "Commit, push & PR", + disabled: false, + }); + }); + + it("preserves Push & create PR when autoCreatePr is true (default)", () => { + const quick = resolveQuickAction(status({ aheadCount: 2 }), false, false, true, true); + assert.deepInclude(quick, { + kind: "run_action", + action: "create_pr", + label: "Push & create PR", + disabled: false, + }); + }); +}); diff --git a/apps/web/src/components/GitActionsControl.logic.ts b/apps/web/src/components/GitActionsControl.logic.ts index 3f6bae61cdd..9e75697c788 100644 --- a/apps/web/src/components/GitActionsControl.logic.ts +++ b/apps/web/src/components/GitActionsControl.logic.ts @@ -169,6 +169,7 @@ export function resolveQuickAction( isBusy: boolean, isDefaultRef = false, hasPrimaryRemote = true, + autoCreatePr = true, ): GitQuickAction { if (isBusy) { return { label: "Commit", disabled: true, kind: "show_hint", hint: "Git action in progress." }; @@ -205,7 +206,7 @@ export function resolveQuickAction( if (!gitStatus.hasUpstream && !hasPrimaryRemote) { return { label: "Commit", disabled: false, kind: "run_action", action: "commit" }; } - if (hasOpenPr || isDefaultRef) { + if (hasOpenPr || isDefaultRef || !autoCreatePr) { return { label: "Commit & push", disabled: false, kind: "run_action", action: "commit_push" }; } return { @@ -238,7 +239,7 @@ export function resolveQuickAction( hint: "No local commits to push.", }; } - if (hasOpenPr || isDefaultRef) { + if (hasOpenPr || isDefaultRef || !autoCreatePr) { return { label: "Push", disabled: false, @@ -272,7 +273,7 @@ export function resolveQuickAction( } if (isAhead) { - if (hasOpenPr || isDefaultRef) { + if (hasOpenPr || isDefaultRef || !autoCreatePr) { return { label: "Push", disabled: false, diff --git a/apps/web/src/components/GitActionsControl.tsx b/apps/web/src/components/GitActionsControl.tsx index 01b84bd94a5..55c22357170 100644 --- a/apps/web/src/components/GitActionsControl.tsx +++ b/apps/web/src/components/GitActionsControl.tsx @@ -44,6 +44,7 @@ import { resolveThreadBranchUpdate, } from "./GitActionsControl.logic"; import { AnimatedHeight } from "./AnimatedHeight"; +import { useSettings } from "../hooks/useSettings"; import { Button } from "~/components/ui/button"; import { Checkbox } from "~/components/ui/checkbox"; import { @@ -1136,10 +1137,17 @@ export default function GitActionsControl({ () => buildMenuItems(gitStatusForActions, isGitActionRunning, hasPrimaryRemote), [gitStatusForActions, hasPrimaryRemote, isGitActionRunning], ); + const autoCreatePrOnPush = useSettings((s) => s.autoCreatePrOnPush); const quickAction = useMemo( () => - resolveQuickAction(gitStatusForActions, isGitActionRunning, isDefaultRef, hasPrimaryRemote), - [gitStatusForActions, hasPrimaryRemote, isDefaultRef, isGitActionRunning], + resolveQuickAction( + gitStatusForActions, + isGitActionRunning, + isDefaultRef, + hasPrimaryRemote, + autoCreatePrOnPush, + ), + [autoCreatePrOnPush, gitStatusForActions, hasPrimaryRemote, isDefaultRef, isGitActionRunning], ); const quickActionDisabledReason = quickAction.disabled ? (quickAction.hint ?? "This action is currently unavailable.") diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index 8fc36d4a32b..632929e9029 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -972,6 +972,32 @@ export function GeneralSettingsPanel() { } /> + + updateSettings({ + autoCreatePrOnPush: DEFAULT_UNIFIED_SETTINGS.autoCreatePrOnPush, + }) + } + /> + ) : null + } + control={ + + updateSettings({ autoCreatePrOnPush: Boolean(checked) }) + } + aria-label="Auto-create PR on push" + /> + } + /> + { it("reads and writes persistence through the desktop bridge when available", async () => { const clientSettings = { + autoCreatePrOnPush: true, autoOpenPlanSidebar: false, confirmThreadArchive: true, confirmThreadDelete: false, @@ -631,6 +632,7 @@ describe("wsApi", () => { const { createLocalApi } = await import("./localApi"); const api = createLocalApi(rpcClientMock as never); const clientSettings = { + autoCreatePrOnPush: true, autoOpenPlanSidebar: false, confirmThreadArchive: true, confirmThreadDelete: false, diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 90b9099d177..89a80782edc 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -29,6 +29,7 @@ export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type; export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository"; export const ClientSettingsSchema = Schema.Struct({ + autoCreatePrOnPush: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),