authorgravatar for 62337003+Bil0000@users.noreply.github.comBilal Bakr <62337003+Bil0000@users.noreply.github.com> 2026-09-08 10:37:37+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-08 13:07:37+05:30
log7d9aaf6a7eb22d5e85b0e3b21686cc437fe68576
tree643dcd07672424ea5ea5e5bf41bcd9dda6a07082
parent430fbd1ffc98378f6a6a41027ddc6004a79b2e7b
signature Signed by PGP key B5690EEEBB952194

feat(web): add pull request merge defaults (#8088)


9 files changed, 219 insertions(+), 21 deletions(-)

apps/desktop/src/settings/DesktopClientSettings.test.ts+1
......@@ -64,6 +64,7 @@ const clientSettings: ClientSettings = {
6464 legacySidebarEnabled: false,
6565 loadBalancingEnabled: false,
6666 loadBalancingWeights: { "environment-1": 75, "environment-2": 0 },
67 pullRequestMergeMethodOverrides: {},
6768 timestampFormat: "24-hour",
6869 wordWrap: true,
6970};
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+65-20
......@@ -53,14 +53,21 @@ import {
5353
5454import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
5555import { useNewThreadHandler } from "~/hooks/useHandleNewThread";
56import { useClientSettings } from "~/hooks/useSettings";
5657import { useCopyToClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard";
58import {
59 deriveLogicalProjectKeyFromSettings,
60 derivePhysicalProjectKey,
61 selectProjectGroupingSettings,
62} from "~/logicalProject";
5763import { changeRequestRepositoryUrl, gitHubPullRequestBrowserUrl } from "~/lib/openPullRequestLink";
5864import { usePreparePullRequestThreadAction } from "~/lib/sourceControlActions";
5965import { cn } from "~/lib/utils";
6066import { readLocalApi } from "~/localApi";
6167import type { ReviewCommentContext } from "~/reviewCommentContext";
68import { buildPhysicalToLogicalProjectKeyMap } from "~/sidebarProjectGrouping";
6269import { useProjects } from "~/state/entities";
63import { useEnvironments } from "~/state/environments";
70import { useEnvironments, usePrimaryEnvironmentId } from "~/state/environments";
6471import { useEnvironmentQuery } from "~/state/query";
6572import { useLiveRefresh } from "~/hooks/useLiveRefresh";
6673import {
......@@ -71,6 +78,7 @@ import {
7178import { useAtomCommand } from "~/state/use-atom-command";
7279import { vcsEnvironment } from "~/state/vcs";
7380import { formatRelativeTimeLabel } from "~/timestampFormat";
81import { useUiStateStore } from "~/uiStateStore";
7482
7583import {
7684 AlertDialog,
......@@ -124,11 +132,13 @@ import {
124132 pullRequestCheckoutCommand,
125133 pullRequestFindingKey,
126134 pullRequestHandoffLabels,
135 PULL_REQUEST_MERGE_METHOD_LABELS,
127136 readableFailure,
128137 readPullRequestDetailSnapshot,
129138 resolveDisplayedPullRequestDetail,
130139 resolvePullRequestPrimaryControl,
131140 resolveBaseFreshness,
141 resolvePullRequestMergeMethod,
132142 type PullRequestFinding,
133143 shouldRefreshPullRequestActivity,
134144 writePullRequestDetailSnapshot,
......@@ -168,12 +178,6 @@ const ACTION_SUCCESS_LABELS: Record<PullRequestAction, string> = {
168178 "approve-workflows": "Workflows approved",
169179};
170180
171const MERGE_METHOD_LABELS: Record<PullRequestMergeMethod, string> = {
172 merge: "Merge",
173 squash: "Squash",
174 rebase: "Rebase",
175};
176
177181/** Said as the thing that did not happen, rather than as the operation that returned an error. */
178182const ACTION_FAILURE_LABELS: Record<PullRequestAction, string> = {
179183 merge: "Could not merge this pull request",
......@@ -553,12 +557,16 @@ export function PullRequestDetailPanel({
553557 compensationRef.current = null;
554558 if (scroller) scroller.scrollTop = Math.max(0, scroller.scrollTop + delta);
555559 }, [condensed]);
560 const lastSelectedMergeMethod = useUiStateStore((state) => state.pullRequestMergeMethod);
561 const setLastSelectedMergeMethod = useUiStateStore((state) => state.setPullRequestMergeMethod);
562 const mergeMethodOverrides = useClientSettings(
563 (settings) => settings.pullRequestMergeMethodOverrides,
564 );
565 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
556566 const [mergeMethodSelection, setMergeMethodSelection] = useState<{
557567 readonly pullRequestKey: string;
558568 readonly method: PullRequestMergeMethod;
559 }>(() => ({ pullRequestKey, method: "merge" }));
560 const mergeMethod =
561 mergeMethodSelection.pullRequestKey === pullRequestKey ? mergeMethodSelection.method : "merge";
569 } | null>(null);
562570 const setMergeMethod = (method: PullRequestMergeMethod) => {
563571 setMergeMethodSelection({ pullRequestKey, method });
564572 };
......@@ -763,6 +771,7 @@ export function PullRequestDetailPanel({
763771 const [titleSaving, setTitleSaving] = useState(false);
764772 const newThread = useNewThreadHandler();
765773 const { environments } = useEnvironments();
774 const primaryEnvironmentId = usePrimaryEnvironmentId();
766775 const projects = useProjects();
767776 const unavailableGitHubUrl = useMemo(() => {
768777 const identity = projects.find(
......@@ -770,6 +779,30 @@ export function PullRequestDetailPanel({
770779 )?.repositoryIdentity;
771780 return gitHubPullRequestBrowserUrl(identity, reference.repository, reference.number);
772781 }, [environmentId, projects, reference.number, reference.projectId, reference.repository]);
782 // Project settings store the override under the sidebar group's key, which a duplicate row
783 // borrows from its siblings, so the project alone does not always name the same key.
784 const projectDefaultMergeMethod = useMemo(() => {
785 const project = projects.find(
786 (candidate) =>
787 candidate.environmentId === environmentId && candidate.id === reference.projectId,
788 );
789 if (!project) return undefined;
790 const projectKey =
791 buildPhysicalToLogicalProjectKeyMap({
792 projects,
793 settings: projectGroupingSettings,
794 primaryEnvironmentId,
795 }).get(derivePhysicalProjectKey(project)) ??
796 deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings);
797 return mergeMethodOverrides[projectKey];
798 }, [
799 environmentId,
800 mergeMethodOverrides,
801 primaryEnvironmentId,
802 projectGroupingSettings,
803 projects,
804 reference.projectId,
805 ]);
773806 // Beside a thread there is nothing to pick: the hand-offs land in that thread's composer, and
774807 // the thread is already on one server's copy of the branch.
775808 const pickableEnvironments = useMemo(
......@@ -1234,10 +1267,15 @@ export function PullRequestDetailPanel({
12341267 const allowedMergeMethods = detail
12351268 ? detail.capabilities.mergeMethods.filter((method) => detail.mergeCapabilities[method])
12361269 : [];
1237 const selectedMergeMethod = allowedMergeMethods.includes(mergeMethod)
1238 ? mergeMethod
1239 : (allowedMergeMethods[0] ?? "merge");
1240 const selectedMergeMethodLabel = MERGE_METHOD_LABELS[selectedMergeMethod];
1270 const currentMergeMethod =
1271 mergeMethodSelection?.pullRequestKey === pullRequestKey ? mergeMethodSelection.method : null;
1272 const selectedMergeMethod = resolvePullRequestMergeMethod(
1273 allowedMergeMethods,
1274 currentMergeMethod,
1275 projectDefaultMergeMethod,
1276 lastSelectedMergeMethod,
1277 );
1278 const selectedMergeMethodLabel = PULL_REQUEST_MERGE_METHOD_LABELS[selectedMergeMethod];
12411279 const pendingAutoMergeLabel = `Auto-merge (${selectedMergeMethodLabel.toLowerCase()})`;
12421280 const conflicting = detail?.state === "open" && detail.mergeability === "conflicting";
12431281 // Only an outright yes arms it. A host that reports nothing has not said the merge is already
......@@ -1245,7 +1283,7 @@ export function PullRequestDetailPanel({
12451283 const autoMergeArmed = detail?.state === "open" && detail.autoMergeEnabled === true;
12461284 const armedMergeMethod = detail?.autoMergeMethod;
12471285 const armedAutoMergeLabel = armedMergeMethod
1248 ? `Auto-merge (${MERGE_METHOD_LABELS[armedMergeMethod].toLowerCase()})`
1286 ? `Auto-merge (${PULL_REQUEST_MERGE_METHOD_LABELS[armedMergeMethod].toLowerCase()})`
12491287 : "Auto-merge";
12501288 const workflowApprovalsRequired =
12511289 detail?.state === "open" ? (detail.workflowApprovalsRequired ?? 0) : 0;
......@@ -1783,17 +1821,24 @@ export function PullRequestDetailPanel({
17831821 ) : null}
17841822 <MenuRadioGroup
17851823 value={selectedMergeMethod}
1786 onValueChange={(method) =>
1787 setMergeMethod(method as PullRequestMergeMethod)
1788 }
1824 onValueChange={(method) => {
1825 const selectedMethod = method as PullRequestMergeMethod;
1826 setMergeMethod(selectedMethod);
1827 setLastSelectedMergeMethod(selectedMethod);
1828 }}
17891829 >
17901830 {allowedMergeMethods.map((method) => (
1791 <MenuRadioItem key={method} value={method} disabled={actionPending}>
1831 <MenuRadioItem
1832 key={method}
1833 value={method}
1834 disabled={actionPending}
1835 closeOnClick
1836 >
17921837 {/* The radio item lays its children out as one block, so the
17931838 icon and the label need their own row to share a line. */}
17941839 <span className="flex min-w-0 items-center gap-2">
17951840 <GitMergeIcon className="size-3.5" />
1796 <span>{MERGE_METHOD_LABELS[method]}</span>
1841 <span>{PULL_REQUEST_MERGE_METHOD_LABELS[method]}</span>
17971842 </span>
17981843 </MenuRadioItem>
17991844 ))}
apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts+16
......@@ -37,6 +37,7 @@ import {
3737 resolvePullRequestPrimaryControl,
3838 shouldRefreshPullRequestActivity,
3939 resolveBaseFreshness,
40 resolvePullRequestMergeMethod,
4041 buildPullRequestTimeline,
4142 editPullRequestThreadComment,
4243 writePullRequestDetailSnapshot,
......@@ -85,6 +86,21 @@ const TIMELINE_SOURCE: Pick<
8586 closedAt: null,
8687};
8788
89describe("pull request merge method", () => {
90 it("uses the current choice, then the project default, then the last choice", () => {
91 expect(
92 resolvePullRequestMergeMethod(["merge", "squash", "rebase"], null, "squash", "rebase"),
93 ).toBe("squash");
94 expect(
95 resolvePullRequestMergeMethod(["merge", "squash", "rebase"], "rebase", "squash", "merge"),
96 ).toBe("rebase");
97 expect(resolvePullRequestMergeMethod(["merge", "rebase"], null, "squash", "rebase")).toBe(
98 "rebase",
99 );
100 expect(resolvePullRequestMergeMethod(["squash"], null, "merge", "rebase")).toBe("squash");
101 });
102});
103
88104describe("pull request activity refresh", () => {
89105 const first = {
90106 key: "project:acme/web#7",
apps/web/src/components/pullRequest/pullRequestDetail.logic.ts+19
......@@ -11,6 +11,7 @@ import {
1111 type PullRequestCommit,
1212 type PullRequestDetailView,
1313 type PullRequestMergeability,
14 type PullRequestMergeMethod,
1415 type PullRequestReaction,
1516 type PullRequestReviewThread,
1617 type PullRequestState,
......@@ -21,6 +22,24 @@ import {
2122
2223import { inferReviewCommentFenceLanguage, type ReviewCommentContext } from "~/reviewCommentContext";
2324
25export const PULL_REQUEST_MERGE_METHOD_LABELS: Record<PullRequestMergeMethod, string> = {
26 merge: "Merge",
27 squash: "Squash and merge",
28 rebase: "Rebase and merge",
29};
30
31export function resolvePullRequestMergeMethod(
32 allowed: ReadonlyArray<PullRequestMergeMethod>,
33 current: PullRequestMergeMethod | null,
34 projectDefault: PullRequestMergeMethod | undefined,
35 lastSelected: PullRequestMergeMethod,
36): PullRequestMergeMethod {
37 for (const method of [current, projectDefault, lastSelected]) {
38 if (method && allowed.includes(method)) return method;
39 }
40 return allowed[0] ?? "merge";
41}
42
2443const safeShellArgument = /^[A-Za-z0-9._/@+=,-]+$/;
2544const bitbucketRepositoryName = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
2645
apps/web/src/components/settings/ProjectSettingsPanel.tsx+51
......@@ -21,6 +21,7 @@ import {
2121 type ResolvedKeybindingsConfig,
2222 type ServerSettings,
2323 type ProviderDriverKind,
24 type PullRequestMergeMethod,
2425 type SidebarProjectGroupingMode,
2526 type T3ProjectFileScript,
2627 type ThreadEnvMode,
......@@ -79,6 +80,7 @@ import { useAtomCommand } from "../../state/use-atom-command";
7980import { ProviderModelPicker } from "../chat/ProviderModelPicker";
8081import { TraitsPicker } from "../chat/TraitsPicker";
8182import { ProjectFavicon } from "../ProjectFavicon";
83import { PULL_REQUEST_MERGE_METHOD_LABELS } from "../pullRequest/pullRequestDetail.logic";
8284import {
8385 EMPTY_PROJECT_SCRIPT_INPUT,
8486 editorRequestForScript,
......@@ -480,6 +482,19 @@ function ProjectDetail({
480482 setBooleanOverride("projectAgentBrowserAccessOverrides", enabled);
481483 const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false });
482484 const projectNameEditedRef = useRef(false);
485 const mergeMethodOverrides = useClientSettings(
486 (settings) => settings.pullRequestMergeMethodOverrides,
487 );
488 const projectMergeMethod = mergeMethodOverrides[group.projectKey];
489 const setProjectMergeMethod = (method: PullRequestMergeMethod | null) => {
490 const nextOverrides = { ...mergeMethodOverrides };
491 if (method === null) {
492 delete nextOverrides[group.projectKey];
493 } else {
494 nextOverrides[group.projectKey] = method;
495 }
496 updateClientSettings({ pullRequestMergeMethodOverrides: nextOverrides });
497 };
483498
484499 const faviconPath = representative.faviconPath ?? null;
485500 const projectIcon = representative.projectIcon ?? null;
......@@ -983,6 +998,42 @@ function ProjectDetail({
983998 </div>
984999 }
9851000 />
1001 <SettingsRow
1002 title="Default merge method"
1003 description="Pull requests in this project start with this method. It overrides the last method selected."
1004 resetAction={
1005 projectMergeMethod !== undefined ? (
1006 <SettingResetButton
1007 label="project merge method"
1008 onClick={() => setProjectMergeMethod(null)}
1009 />
1010 ) : null
1011 }
1012 control={
1013 <Select
1014 value={projectMergeMethod ?? "inherit"}
1015 onValueChange={(value) =>
1016 setProjectMergeMethod(
1017 value === "inherit" ? null : (value as PullRequestMergeMethod),
1018 )
1019 }
1020 >
1021 <SelectTrigger aria-label="Default pull request merge method">
1022 <SelectValue>
1023 {projectMergeMethod === undefined
1024 ? "Last selected"
1025 : PULL_REQUEST_MERGE_METHOD_LABELS[projectMergeMethod]}
1026 </SelectValue>
1027 </SelectTrigger>
1028 <SelectPopup align="end" alignItemWithTrigger={false}>
1029 <SelectItem value="inherit">Last selected</SelectItem>
1030 <SelectItem value="merge">{PULL_REQUEST_MERGE_METHOD_LABELS.merge}</SelectItem>
1031 <SelectItem value="squash">{PULL_REQUEST_MERGE_METHOD_LABELS.squash}</SelectItem>
1032 <SelectItem value="rebase">{PULL_REQUEST_MERGE_METHOD_LABELS.rebase}</SelectItem>
1033 </SelectPopup>
1034 </Select>
1035 }
1036 />
9861037 <SettingsRow
9871038 title="Model"
9881039 status={
apps/web/src/uiStateStore.test.ts+15
......@@ -26,6 +26,7 @@ function makeUiState(overrides: Partial<UiState> = {}): UiState {
2626 threadLastVisitedAtById: {},
2727 threadChangedFilesExpandedById: {},
2828 defaultAdvertisedEndpointKey: null,
29 pullRequestMergeMethod: "merge",
2930 ...overrides,
3031 };
3132}
......@@ -158,6 +159,18 @@ describe("uiStateStore pure functions", () => {
158159});
159160
160161describe("parsePersistedState", () => {
162 it("hydrates the last selected pull request merge method", () => {
163 const parsed = parsePersistedState({
164 pullRequestMergeMethod: "squash",
165 });
166 const invalid = parsePersistedState({
167 pullRequestMergeMethod: "fast-forward",
168 });
169
170 expect(parsed.pullRequestMergeMethod).toBe("squash");
171 expect(invalid.pullRequestMergeMethod).toBe("merge");
172 });
173
161174 it("hydrates raw UI-owned state without server entities", () => {
162175 const parsed = parsePersistedState({
163176 projectExpandedById: {
......@@ -189,6 +202,7 @@ describe("parsePersistedState", () => {
189202 },
190203 defaultAdvertisedEndpointKey: "desktop-core:lan:http",
191204 sidebarProjectScopeKey: null,
205 pullRequestMergeMethod: "merge",
192206 threadChangedFilesExpandedById: {
193207 "environment:thread-1": {
194208 "turn-1": false,
......@@ -317,6 +331,7 @@ describe("uiStateStore persistence", () => {
317331 "turn-2": true,
318332 },
319333 },
334 pullRequestMergeMethod: "merge",
320335 });
321336 expect(parsePersistedState(persisted)).toEqual({
322337 ...state,
apps/web/src/uiStateStore.ts+25-1
......@@ -1,4 +1,5 @@
11import { Debouncer } from "@tanstack/react-pacer";
2import type { PullRequestMergeMethod } from "@t3tools/contracts";
23import { create } from "zustand";
34import { normalizeProjectPathForComparison } from "./lib/projectPaths";
45
......@@ -29,6 +30,7 @@ export interface PersistedUiState {
2930 sidebarProjectScopeKey?: string | null;
3031 threadChangedFilesExpansionVersion?: number;
3132 threadChangedFilesExpandedById?: Record<string, Record<string, boolean>>;
33 pullRequestMergeMethod?: string;
3234}
3335
3436export interface UiProjectState {
......@@ -49,7 +51,12 @@ export interface UiEndpointState {
4951 defaultAdvertisedEndpointKey: string | null;
5052}
5153
52export interface UiState extends UiProjectState, UiThreadState, UiEndpointState {}
54export interface UiPullRequestState {
55 pullRequestMergeMethod: PullRequestMergeMethod;
56}
57
58export interface UiState
59 extends UiProjectState, UiThreadState, UiEndpointState, UiPullRequestState {}
5360
5461const initialState: UiState = {
5562 projectExpandedById: {},
......@@ -58,6 +65,7 @@ const initialState: UiState = {
5865 threadLastVisitedAtById: {},
5966 threadChangedFilesExpandedById: {},
6067 defaultAdvertisedEndpointKey: null,
68 pullRequestMergeMethod: "merge",
6169};
6270
6371const LEGACY_PROJECT_CWD_PREFERENCE_PREFIX = "legacy-project-cwd:";
......@@ -109,6 +117,10 @@ function sanitizeTimestampRecord(value: unknown): Record<string, string> {
109117 );
110118}
111119
120function isPullRequestMergeMethod(value: unknown): value is PullRequestMergeMethod {
121 return value === "merge" || value === "squash" || value === "rebase";
122}
123
112124export function parsePersistedState(parsed: PersistedUiState): UiState {
113125 const projectExpandedById =
114126 parsed.projectExpandedById === undefined
......@@ -143,6 +155,9 @@ export function parsePersistedState(parsed: PersistedUiState): UiState {
143155 : {},
144156 defaultAdvertisedEndpointKey: sanitizeOptionalKey(parsed.defaultAdvertisedEndpointKey),
145157 sidebarProjectScopeKey: sanitizeOptionalKey(parsed.sidebarProjectScopeKey),
158 pullRequestMergeMethod: isPullRequestMergeMethod(parsed.pullRequestMergeMethod)
159 ? parsed.pullRequestMergeMethod
160 : initialState.pullRequestMergeMethod,
146161 };
147162}
148163
......@@ -216,6 +231,7 @@ export function persistState(state: UiState): void {
216231 sidebarProjectScopeKey: state.sidebarProjectScopeKey,
217232 threadChangedFilesExpansionVersion: THREAD_CHANGED_FILES_EXPANSION_VERSION,
218233 threadChangedFilesExpandedById: state.threadChangedFilesExpandedById,
234 pullRequestMergeMethod: state.pullRequestMergeMethod,
219235 } satisfies PersistedUiState),
220236 );
221237 if (!legacyKeysCleanedUp) {
......@@ -324,6 +340,12 @@ export function setSidebarProjectScopeKey(state: UiState, projectKey: string | n
324340 };
325341}
326342
343function setPullRequestMergeMethod(state: UiState, method: PullRequestMergeMethod): UiState {
344 return state.pullRequestMergeMethod === method
345 ? state
346 : { ...state, pullRequestMergeMethod: method };
347}
348
327349export function resolveProjectExpanded(
328350 projectExpandedById: Readonly<Record<string, boolean>>,
329351 preferenceKeys: readonly string[],
......@@ -407,6 +429,7 @@ interface UiStateStore extends UiState {
407429 setThreadChangedFilesExpanded: (threadId: string, turnId: string, expanded: boolean) => void;
408430 setDefaultAdvertisedEndpointKey: (key: string | null) => void;
409431 setSidebarProjectScopeKey: (projectKey: string | null) => void;
432 setPullRequestMergeMethod: (method: PullRequestMergeMethod) => void;
410433 setProjectExpanded: (projectIds: string | readonly string[], expanded: boolean) => void;
411434 reorderProjects: (
412435 currentProjectOrder: readonly string[],
......@@ -427,6 +450,7 @@ export const useUiStateStore = create<UiStateStore>((set) => ({
427450 set((state) => setDefaultAdvertisedEndpointKey(state, key)),
428451 setSidebarProjectScopeKey: (projectKey) =>
429452 set((state) => setSidebarProjectScopeKey(state, projectKey)),
453 setPullRequestMergeMethod: (method) => set((state) => setPullRequestMergeMethod(state, method)),
430454 setProjectExpanded: (projectIds, expanded) =>
431455 set((state) => setProjectExpanded(state, projectIds, expanded)),
432456 reorderProjects: (currentProjectOrder, draggedProjectIds, targetProjectIds) =>
packages/contracts/src/settings.test.ts+19
......@@ -455,6 +455,25 @@ describe("ServerSettings thread settlement", () => {
455455 });
456456});
457457
458describe("ClientSettings pull request merge methods", () => {
459 it("defaults to no project overrides and accepts supported methods", () => {
460 expect(decodeClientSettings({}).pullRequestMergeMethodOverrides).toEqual({});
461 expect(
462 decodeClientSettingsPatch({
463 pullRequestMergeMethodOverrides: { project: "squash" },
464 }).pullRequestMergeMethodOverrides,
465 ).toEqual({ project: "squash" });
466 });
467
468 it("rejects unsupported project merge methods", () => {
469 expect(() =>
470 decodeClientSettingsPatch({
471 pullRequestMergeMethodOverrides: { project: "fast-forward" },
472 }),
473 ).toThrow();
474 });
475});
476
458477describe("ServerSettings.providerInstances (slice-2 invariant)", () => {
459478 it("defaults text generation to Luna at low reasoning effort", () => {
460479 expect(DEFAULT_SERVER_SETTINGS.textGenerationModelSelection).toEqual({
packages/contracts/src/settings.ts+8
......@@ -32,6 +32,7 @@ import {
3232 ProviderInstanceId,
3333 type ProviderDriverKind,
3434} from "./providerInstance.ts";
35import { PullRequestMergeMethod } from "./pullRequest.ts";
3536
3637// ── Client Settings (local-only) ───────────────────────────────
3738
......@@ -391,6 +392,10 @@ export const ClientSettingsSchema = Schema.Struct({
391392 modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
392393 }),
393394 ).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
395 pullRequestMergeMethodOverrides: Schema.Record(
396 TrimmedNonEmptyString,
397 PullRequestMergeMethod,
398 ).pipe(Schema.withDecodingDefault(Effect.succeed({}))),
394399 // Legacy plan mode. The composer's Build/Plan toggle was removed from the
395400 // default UI; this beta flag restores it (plus the /plan and /default slash
396401 // commands) for users who still rely on the old workflow.
......@@ -1341,6 +1346,9 @@ export const ClientSettingsPatch = Schema.Struct({
13411346 }),
13421347 ),
13431348 ),
1349 pullRequestMergeMethodOverrides: Schema.optionalKey(
1350 Schema.Record(TrimmedNonEmptyString, PullRequestMergeMethod),
1351 ),
13441352 planModeEnabled: Schema.optionalKey(Schema.Boolean),
13451353 contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean),
13461354 composerCollapseOnScroll: Schema.optionalKey(Schema.Boolean),