From 3044cf58f3e9ccf24bef105f44a2163caf94f8f2 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:38:14 +0000 Subject: [PATCH 1/4] feat(web): add pull request merge defaults --- .../pullRequest/PullRequestDetailPanel.tsx | 65 +++++++++++++------ .../pullRequestDetail.logic.test.ts | 16 +++++ .../pullRequest/pullRequestDetail.logic.ts | 19 ++++++ .../settings/ProjectSettingsPanel.tsx | 51 +++++++++++++++ apps/web/src/uiStateStore.test.ts | 15 +++++ apps/web/src/uiStateStore.ts | 26 +++++++- packages/contracts/src/settings.test.ts | 19 ++++++ packages/contracts/src/settings.ts | 8 +++ 8 files changed, 199 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 733a25a35204..e6a23f263764 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -53,7 +53,12 @@ import { import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { useNewThreadHandler } from "~/hooks/useHandleNewThread"; +import { useClientSettings } from "~/hooks/useSettings"; import { useCopyToClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard"; +import { + deriveLogicalProjectKeyFromSettings, + selectProjectGroupingSettings, +} from "~/logicalProject"; import { changeRequestRepositoryUrl, gitHubPullRequestBrowserUrl } from "~/lib/openPullRequestLink"; import { usePreparePullRequestThreadAction } from "~/lib/sourceControlActions"; import { cn } from "~/lib/utils"; @@ -71,6 +76,7 @@ import { import { useAtomCommand } from "~/state/use-atom-command"; import { vcsEnvironment } from "~/state/vcs"; import { formatRelativeTimeLabel } from "~/timestampFormat"; +import { useUiStateStore } from "~/uiStateStore"; import { AlertDialog, @@ -124,11 +130,13 @@ import { pullRequestCheckoutCommand, pullRequestFindingKey, pullRequestHandoffLabels, + PULL_REQUEST_MERGE_METHOD_LABELS, readableFailure, readPullRequestDetailSnapshot, resolveDisplayedPullRequestDetail, resolvePullRequestPrimaryControl, resolveBaseFreshness, + resolvePullRequestMergeMethod, type PullRequestFinding, shouldRefreshPullRequestActivity, writePullRequestDetailSnapshot, @@ -168,12 +176,6 @@ const ACTION_SUCCESS_LABELS: Record = { "approve-workflows": "Workflows approved", }; -const MERGE_METHOD_LABELS: Record = { - merge: "Merge", - squash: "Squash", - rebase: "Rebase", -}; - /** Said as the thing that did not happen, rather than as the operation that returned an error. */ const ACTION_FAILURE_LABELS: Record = { merge: "Could not merge this pull request", @@ -553,12 +555,16 @@ export function PullRequestDetailPanel({ compensationRef.current = null; if (scroller) scroller.scrollTop = Math.max(0, scroller.scrollTop + delta); }, [condensed]); + const lastSelectedMergeMethod = useUiStateStore((state) => state.pullRequestMergeMethod); + const setLastSelectedMergeMethod = useUiStateStore((state) => state.setPullRequestMergeMethod); + const mergeMethodOverrides = useClientSettings( + (settings) => settings.pullRequestMergeMethodOverrides, + ); + const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const [mergeMethodSelection, setMergeMethodSelection] = useState<{ readonly pullRequestKey: string; readonly method: PullRequestMergeMethod; - }>(() => ({ pullRequestKey, method: "merge" })); - const mergeMethod = - mergeMethodSelection.pullRequestKey === pullRequestKey ? mergeMethodSelection.method : "merge"; + } | null>(null); const setMergeMethod = (method: PullRequestMergeMethod) => { setMergeMethodSelection({ pullRequestKey, method }); }; @@ -770,6 +776,15 @@ export function PullRequestDetailPanel({ )?.repositoryIdentity; return gitHubPullRequestBrowserUrl(identity, reference.repository, reference.number); }, [environmentId, projects, reference.number, reference.projectId, reference.repository]); + const mergeMethodProject = projects.find( + (project) => project.environmentId === environmentId && project.id === reference.projectId, + ); + const mergeMethodProjectKey = mergeMethodProject + ? deriveLogicalProjectKeyFromSettings(mergeMethodProject, projectGroupingSettings) + : null; + const projectDefaultMergeMethod = mergeMethodProjectKey + ? mergeMethodOverrides[mergeMethodProjectKey] + : undefined; // Beside a thread there is nothing to pick: the hand-offs land in that thread's composer, and // the thread is already on one server's copy of the branch. const pickableEnvironments = useMemo( @@ -1234,10 +1249,15 @@ export function PullRequestDetailPanel({ const allowedMergeMethods = detail ? detail.capabilities.mergeMethods.filter((method) => detail.mergeCapabilities[method]) : []; - const selectedMergeMethod = allowedMergeMethods.includes(mergeMethod) - ? mergeMethod - : (allowedMergeMethods[0] ?? "merge"); - const selectedMergeMethodLabel = MERGE_METHOD_LABELS[selectedMergeMethod]; + const currentMergeMethod = + mergeMethodSelection?.pullRequestKey === pullRequestKey ? mergeMethodSelection.method : null; + const selectedMergeMethod = resolvePullRequestMergeMethod( + allowedMergeMethods, + currentMergeMethod, + projectDefaultMergeMethod, + lastSelectedMergeMethod, + ); + const selectedMergeMethodLabel = PULL_REQUEST_MERGE_METHOD_LABELS[selectedMergeMethod]; const pendingAutoMergeLabel = `Auto-merge (${selectedMergeMethodLabel.toLowerCase()})`; const conflicting = detail?.state === "open" && detail.mergeability === "conflicting"; // Only an outright yes arms it. A host that reports nothing has not said the merge is already @@ -1245,7 +1265,7 @@ export function PullRequestDetailPanel({ const autoMergeArmed = detail?.state === "open" && detail.autoMergeEnabled === true; const armedMergeMethod = detail?.autoMergeMethod; const armedAutoMergeLabel = armedMergeMethod - ? `Auto-merge (${MERGE_METHOD_LABELS[armedMergeMethod].toLowerCase()})` + ? `Auto-merge (${PULL_REQUEST_MERGE_METHOD_LABELS[armedMergeMethod].toLowerCase()})` : "Auto-merge"; const workflowApprovalsRequired = detail?.state === "open" ? (detail.workflowApprovalsRequired ?? 0) : 0; @@ -1783,17 +1803,24 @@ export function PullRequestDetailPanel({ ) : null} - setMergeMethod(method as PullRequestMergeMethod) - } + onValueChange={(method) => { + const selectedMethod = method as PullRequestMergeMethod; + setMergeMethod(selectedMethod); + setLastSelectedMergeMethod(selectedMethod); + }} > {allowedMergeMethods.map((method) => ( - + {/* The radio item lays its children out as one block, so the icon and the label need their own row to share a line. */} - {MERGE_METHOD_LABELS[method]} + {PULL_REQUEST_MERGE_METHOD_LABELS[method]} ))} diff --git a/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts b/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts index 61c630c815e4..000c594ec6ec 100644 --- a/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts +++ b/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts @@ -37,6 +37,7 @@ import { resolvePullRequestPrimaryControl, shouldRefreshPullRequestActivity, resolveBaseFreshness, + resolvePullRequestMergeMethod, buildPullRequestTimeline, editPullRequestThreadComment, writePullRequestDetailSnapshot, @@ -85,6 +86,21 @@ const TIMELINE_SOURCE: Pick< closedAt: null, }; +describe("pull request merge method", () => { + it("uses the current choice, then the project default, then the last choice", () => { + expect( + resolvePullRequestMergeMethod(["merge", "squash", "rebase"], null, "squash", "rebase"), + ).toBe("squash"); + expect( + resolvePullRequestMergeMethod(["merge", "squash", "rebase"], "rebase", "squash", "merge"), + ).toBe("rebase"); + expect(resolvePullRequestMergeMethod(["merge", "rebase"], null, "squash", "rebase")).toBe( + "rebase", + ); + expect(resolvePullRequestMergeMethod(["squash"], null, "merge", "rebase")).toBe("squash"); + }); +}); + describe("pull request activity refresh", () => { const first = { key: "project:acme/web#7", diff --git a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts index d00215f02d41..7fc951549221 100644 --- a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts +++ b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts @@ -11,6 +11,7 @@ import { type PullRequestCommit, type PullRequestDetailView, type PullRequestMergeability, + type PullRequestMergeMethod, type PullRequestReaction, type PullRequestReviewThread, type PullRequestState, @@ -21,6 +22,24 @@ import { import { inferReviewCommentFenceLanguage, type ReviewCommentContext } from "~/reviewCommentContext"; +export const PULL_REQUEST_MERGE_METHOD_LABELS: Record = { + merge: "Create a merge commit", + squash: "Squash and merge", + rebase: "Rebase and merge", +}; + +export function resolvePullRequestMergeMethod( + allowed: ReadonlyArray, + current: PullRequestMergeMethod | null, + projectDefault: PullRequestMergeMethod | undefined, + lastSelected: PullRequestMergeMethod, +): PullRequestMergeMethod { + for (const method of [current, projectDefault, lastSelected]) { + if (method && allowed.includes(method)) return method; + } + return allowed[0] ?? "merge"; +} + const safeShellArgument = /^[A-Za-z0-9._/@+=,-]+$/; const bitbucketRepositoryName = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/; diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx index 0401fd733d01..e9cd1bbed43a 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx @@ -21,6 +21,7 @@ import { type ResolvedKeybindingsConfig, type ServerSettings, type ProviderDriverKind, + type PullRequestMergeMethod, type SidebarProjectGroupingMode, type T3ProjectFileScript, type ThreadEnvMode, @@ -79,6 +80,7 @@ import { useAtomCommand } from "../../state/use-atom-command"; import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { TraitsPicker } from "../chat/TraitsPicker"; import { ProjectFavicon } from "../ProjectFavicon"; +import { PULL_REQUEST_MERGE_METHOD_LABELS } from "../pullRequest/pullRequestDetail.logic"; import { EMPTY_PROJECT_SCRIPT_INPUT, editorRequestForScript, @@ -480,6 +482,19 @@ function ProjectDetail({ setBooleanOverride("projectAgentBrowserAccessOverrides", enabled); const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false }); const projectNameEditedRef = useRef(false); + const mergeMethodOverrides = useClientSettings( + (settings) => settings.pullRequestMergeMethodOverrides, + ); + const projectMergeMethod = mergeMethodOverrides[group.projectKey]; + const setProjectMergeMethod = (method: PullRequestMergeMethod | null) => { + const nextOverrides = { ...mergeMethodOverrides }; + if (method === null) { + delete nextOverrides[group.projectKey]; + } else { + nextOverrides[group.projectKey] = method; + } + updateClientSettings({ pullRequestMergeMethodOverrides: nextOverrides }); + }; const faviconPath = representative.faviconPath ?? null; const projectIcon = representative.projectIcon ?? null; @@ -983,6 +998,42 @@ function ProjectDetail({ } /> + setProjectMergeMethod(null)} + /> + ) : null + } + control={ + + } + /> = {}): UiState { threadLastVisitedAtById: {}, threadChangedFilesExpandedById: {}, defaultAdvertisedEndpointKey: null, + pullRequestMergeMethod: "merge", ...overrides, }; } @@ -158,6 +159,18 @@ describe("uiStateStore pure functions", () => { }); describe("parsePersistedState", () => { + it("hydrates the last selected pull request merge method", () => { + const parsed = parsePersistedState({ + pullRequestMergeMethod: "squash", + }); + const invalid = parsePersistedState({ + pullRequestMergeMethod: "fast-forward", + }); + + expect(parsed.pullRequestMergeMethod).toBe("squash"); + expect(invalid.pullRequestMergeMethod).toBe("merge"); + }); + it("hydrates raw UI-owned state without server entities", () => { const parsed = parsePersistedState({ projectExpandedById: { @@ -189,6 +202,7 @@ describe("parsePersistedState", () => { }, defaultAdvertisedEndpointKey: "desktop-core:lan:http", sidebarProjectScopeKey: null, + pullRequestMergeMethod: "merge", threadChangedFilesExpandedById: { "environment:thread-1": { "turn-1": false, @@ -317,6 +331,7 @@ describe("uiStateStore persistence", () => { "turn-2": true, }, }, + pullRequestMergeMethod: "merge", }); expect(parsePersistedState(persisted)).toEqual({ ...state, diff --git a/apps/web/src/uiStateStore.ts b/apps/web/src/uiStateStore.ts index b14ce917c861..e82c86f26404 100644 --- a/apps/web/src/uiStateStore.ts +++ b/apps/web/src/uiStateStore.ts @@ -1,4 +1,5 @@ import { Debouncer } from "@tanstack/react-pacer"; +import type { PullRequestMergeMethod } from "@t3tools/contracts"; import { create } from "zustand"; import { normalizeProjectPathForComparison } from "./lib/projectPaths"; @@ -29,6 +30,7 @@ export interface PersistedUiState { sidebarProjectScopeKey?: string | null; threadChangedFilesExpansionVersion?: number; threadChangedFilesExpandedById?: Record>; + pullRequestMergeMethod?: string; } export interface UiProjectState { @@ -49,7 +51,12 @@ export interface UiEndpointState { defaultAdvertisedEndpointKey: string | null; } -export interface UiState extends UiProjectState, UiThreadState, UiEndpointState {} +export interface UiPullRequestState { + pullRequestMergeMethod: PullRequestMergeMethod; +} + +export interface UiState + extends UiProjectState, UiThreadState, UiEndpointState, UiPullRequestState {} const initialState: UiState = { projectExpandedById: {}, @@ -58,6 +65,7 @@ const initialState: UiState = { threadLastVisitedAtById: {}, threadChangedFilesExpandedById: {}, defaultAdvertisedEndpointKey: null, + pullRequestMergeMethod: "merge", }; const LEGACY_PROJECT_CWD_PREFERENCE_PREFIX = "legacy-project-cwd:"; @@ -109,6 +117,10 @@ function sanitizeTimestampRecord(value: unknown): Record { ); } +function isPullRequestMergeMethod(value: unknown): value is PullRequestMergeMethod { + return value === "merge" || value === "squash" || value === "rebase"; +} + export function parsePersistedState(parsed: PersistedUiState): UiState { const projectExpandedById = parsed.projectExpandedById === undefined @@ -143,6 +155,9 @@ export function parsePersistedState(parsed: PersistedUiState): UiState { : {}, defaultAdvertisedEndpointKey: sanitizeOptionalKey(parsed.defaultAdvertisedEndpointKey), sidebarProjectScopeKey: sanitizeOptionalKey(parsed.sidebarProjectScopeKey), + pullRequestMergeMethod: isPullRequestMergeMethod(parsed.pullRequestMergeMethod) + ? parsed.pullRequestMergeMethod + : initialState.pullRequestMergeMethod, }; } @@ -216,6 +231,7 @@ export function persistState(state: UiState): void { sidebarProjectScopeKey: state.sidebarProjectScopeKey, threadChangedFilesExpansionVersion: THREAD_CHANGED_FILES_EXPANSION_VERSION, threadChangedFilesExpandedById: state.threadChangedFilesExpandedById, + pullRequestMergeMethod: state.pullRequestMergeMethod, } satisfies PersistedUiState), ); if (!legacyKeysCleanedUp) { @@ -324,6 +340,12 @@ export function setSidebarProjectScopeKey(state: UiState, projectKey: string | n }; } +function setPullRequestMergeMethod(state: UiState, method: PullRequestMergeMethod): UiState { + return state.pullRequestMergeMethod === method + ? state + : { ...state, pullRequestMergeMethod: method }; +} + export function resolveProjectExpanded( projectExpandedById: Readonly>, preferenceKeys: readonly string[], @@ -407,6 +429,7 @@ interface UiStateStore extends UiState { setThreadChangedFilesExpanded: (threadId: string, turnId: string, expanded: boolean) => void; setDefaultAdvertisedEndpointKey: (key: string | null) => void; setSidebarProjectScopeKey: (projectKey: string | null) => void; + setPullRequestMergeMethod: (method: PullRequestMergeMethod) => void; setProjectExpanded: (projectIds: string | readonly string[], expanded: boolean) => void; reorderProjects: ( currentProjectOrder: readonly string[], @@ -427,6 +450,7 @@ export const useUiStateStore = create((set) => ({ set((state) => setDefaultAdvertisedEndpointKey(state, key)), setSidebarProjectScopeKey: (projectKey) => set((state) => setSidebarProjectScopeKey(state, projectKey)), + setPullRequestMergeMethod: (method) => set((state) => setPullRequestMergeMethod(state, method)), setProjectExpanded: (projectIds, expanded) => set((state) => setProjectExpanded(state, projectIds, expanded)), reorderProjects: (currentProjectOrder, draggedProjectIds, targetProjectIds) => diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index b0fc4031d6dc..7d3cd2ceafe7 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -455,6 +455,25 @@ describe("ServerSettings thread settlement", () => { }); }); +describe("ClientSettings pull request merge methods", () => { + it("defaults to no project overrides and accepts supported methods", () => { + expect(decodeClientSettings({}).pullRequestMergeMethodOverrides).toEqual({}); + expect( + decodeClientSettingsPatch({ + pullRequestMergeMethodOverrides: { project: "squash" }, + }).pullRequestMergeMethodOverrides, + ).toEqual({ project: "squash" }); + }); + + it("rejects unsupported project merge methods", () => { + expect(() => + decodeClientSettingsPatch({ + pullRequestMergeMethodOverrides: { project: "fast-forward" }, + }), + ).toThrow(); + }); +}); + describe("ServerSettings.providerInstances (slice-2 invariant)", () => { it("defaults text generation to Luna at low reasoning effort", () => { expect(DEFAULT_SERVER_SETTINGS.textGenerationModelSelection).toEqual({ diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 3d24e488ce7a..3491103da94f 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -32,6 +32,7 @@ import { ProviderInstanceId, type ProviderDriverKind, } from "./providerInstance.ts"; +import { PullRequestMergeMethod } from "./pullRequest.ts"; // ── Client Settings (local-only) ─────────────────────────────── @@ -391,6 +392,10 @@ export const ClientSettingsSchema = Schema.Struct({ modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))), }), ).pipe(Schema.withDecodingDefault(Effect.succeed({}))), + pullRequestMergeMethodOverrides: Schema.Record( + TrimmedNonEmptyString, + PullRequestMergeMethod, + ).pipe(Schema.withDecodingDefault(Effect.succeed({}))), // Legacy plan mode. The composer's Build/Plan toggle was removed from the // default UI; this beta flag restores it (plus the /plan and /default slash // commands) for users who still rely on the old workflow. @@ -1341,6 +1346,9 @@ export const ClientSettingsPatch = Schema.Struct({ }), ), ), + pullRequestMergeMethodOverrides: Schema.optionalKey( + Schema.Record(TrimmedNonEmptyString, PullRequestMergeMethod), + ), planModeEnabled: Schema.optionalKey(Schema.Boolean), contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), composerCollapseOnScroll: Schema.optionalKey(Schema.Boolean), From 833ce3683a20b07d0da3521f5bffd87ad3af1314 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:54:26 +0000 Subject: [PATCH 2/4] fix(web): shorten merge method label --- apps/web/src/components/pullRequest/pullRequestDetail.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts index 7fc951549221..24b37aceafc2 100644 --- a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts +++ b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts @@ -23,7 +23,7 @@ import { import { inferReviewCommentFenceLanguage, type ReviewCommentContext } from "~/reviewCommentContext"; export const PULL_REQUEST_MERGE_METHOD_LABELS: Record = { - merge: "Create a merge commit", + merge: "Merge", squash: "Squash and merge", rebase: "Rebase and merge", }; From ba6e9085b30724667272ed06f16cd680fe1cf8f0 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:30:12 +0000 Subject: [PATCH 3/4] fix(desktop): add pullRequestMergeMethodOverrides to test fixture The new ClientSettings field is required; the desktop test fixture was missing it, causing a TS2741 type error and a deep-equal mismatch in the persist-and-reload test. --- apps/desktop/src/settings/DesktopClientSettings.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 83ad62ccffdc..ea2a80010124 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -64,6 +64,7 @@ const clientSettings: ClientSettings = { legacySidebarEnabled: false, loadBalancingEnabled: false, loadBalancingWeights: { "environment-1": 75, "environment-2": 0 }, + pullRequestMergeMethodOverrides: {}, timestampFormat: "24-hour", wordWrap: true, }; From e2f7a18817acba7b8209ae02d22aff2b8cddc6e2 Mon Sep 17 00:00:00 2001 From: Bil0000 <62337003+Bil0000@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:45:45 +0200 Subject: [PATCH 4/4] fix(web): look up project merge default by sidebar group key Project settings store the merge method override under the sidebar group's key. A duplicate checkout without repository identity borrows that key from its siblings, so deriving the key from the project alone missed the configured default. Resolve it through the same physical-to-logical map the sidebar uses. --- .../pullRequest/PullRequestDetailPanel.tsx | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index e6a23f263764..de6f32aaa121 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -57,6 +57,7 @@ import { useClientSettings } from "~/hooks/useSettings"; import { useCopyToClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { deriveLogicalProjectKeyFromSettings, + derivePhysicalProjectKey, selectProjectGroupingSettings, } from "~/logicalProject"; import { changeRequestRepositoryUrl, gitHubPullRequestBrowserUrl } from "~/lib/openPullRequestLink"; @@ -64,8 +65,9 @@ import { usePreparePullRequestThreadAction } from "~/lib/sourceControlActions"; import { cn } from "~/lib/utils"; import { readLocalApi } from "~/localApi"; import type { ReviewCommentContext } from "~/reviewCommentContext"; +import { buildPhysicalToLogicalProjectKeyMap } from "~/sidebarProjectGrouping"; import { useProjects } from "~/state/entities"; -import { useEnvironments } from "~/state/environments"; +import { useEnvironments, usePrimaryEnvironmentId } from "~/state/environments"; import { useEnvironmentQuery } from "~/state/query"; import { useLiveRefresh } from "~/hooks/useLiveRefresh"; import { @@ -769,6 +771,7 @@ export function PullRequestDetailPanel({ const [titleSaving, setTitleSaving] = useState(false); const newThread = useNewThreadHandler(); const { environments } = useEnvironments(); + const primaryEnvironmentId = usePrimaryEnvironmentId(); const projects = useProjects(); const unavailableGitHubUrl = useMemo(() => { const identity = projects.find( @@ -776,15 +779,30 @@ export function PullRequestDetailPanel({ )?.repositoryIdentity; return gitHubPullRequestBrowserUrl(identity, reference.repository, reference.number); }, [environmentId, projects, reference.number, reference.projectId, reference.repository]); - const mergeMethodProject = projects.find( - (project) => project.environmentId === environmentId && project.id === reference.projectId, - ); - const mergeMethodProjectKey = mergeMethodProject - ? deriveLogicalProjectKeyFromSettings(mergeMethodProject, projectGroupingSettings) - : null; - const projectDefaultMergeMethod = mergeMethodProjectKey - ? mergeMethodOverrides[mergeMethodProjectKey] - : undefined; + // Project settings store the override under the sidebar group's key, which a duplicate row + // borrows from its siblings, so the project alone does not always name the same key. + const projectDefaultMergeMethod = useMemo(() => { + const project = projects.find( + (candidate) => + candidate.environmentId === environmentId && candidate.id === reference.projectId, + ); + if (!project) return undefined; + const projectKey = + buildPhysicalToLogicalProjectKeyMap({ + projects, + settings: projectGroupingSettings, + primaryEnvironmentId, + }).get(derivePhysicalProjectKey(project)) ?? + deriveLogicalProjectKeyFromSettings(project, projectGroupingSettings); + return mergeMethodOverrides[projectKey]; + }, [ + environmentId, + mergeMethodOverrides, + primaryEnvironmentId, + projectGroupingSettings, + projects, + reference.projectId, + ]); // Beside a thread there is nothing to pick: the hand-offs land in that thread's composer, and // the thread is already on one server's copy of the branch. const pickableEnvironments = useMemo(