diff --git a/apps/web/src/components/chat/ThreadDetailsPrRow.browser.test.tsx b/apps/web/src/components/chat/ThreadDetailsPrRow.browser.test.tsx
index 7d1c3199b8ec8..0d32b19cf1b95 100644
--- a/apps/web/src/components/chat/ThreadDetailsPrRow.browser.test.tsx
+++ b/apps/web/src/components/chat/ThreadDetailsPrRow.browser.test.tsx
@@ -6,6 +6,8 @@ import { expect, it, vi } from "vite-plus/test";
const state = vi.hoisted(() => ({
status: "success" as PullRequestCheck["status"],
extraStatus: null as PullRequestCheck["status"] | null,
+ autoMergeEnabled: false as boolean | undefined,
+ actions: ["merge"] as Array<"merge" | "enable-auto-merge">,
mergeMethods: ["merge"] as Array<"merge" | "squash" | "rebase">,
perform: vi.fn(),
newThread: vi.fn(),
@@ -46,8 +48,9 @@ const queryData = () => ({
description: null,
url: null,
})),
- capabilities: { actions: ["merge"], mergeMethods: state.mergeMethods },
- viewerPermissions: { actions: ["merge"] },
+ autoMergeEnabled: state.autoMergeEnabled,
+ capabilities: { actions: state.actions, mergeMethods: state.mergeMethods },
+ viewerPermissions: { actions: state.actions },
mergeCapabilities: {
merge: state.mergeMethods.includes("merge"),
squash: state.mergeMethods.includes("squash"),
@@ -177,6 +180,72 @@ it("requires a new merge click after passing checks become pending and pass agai
expect(state.perform).not.toHaveBeenCalled();
});
+it("hides Merge once auto-merge is armed", async () => {
+ await using screen = await setup();
+ const { container } = screen;
+ const render = () => (
+
+ );
+ await act(async () => await screen.rerender(render()));
+ expect(
+ [...container.querySelectorAll("button")].some(
+ (button) => button.textContent?.trim() === "Merge",
+ ),
+ ).toBe(true);
+ await act(async () => {
+ state.autoMergeEnabled = true;
+ await screen.rerender(render());
+ });
+ expect(
+ [...container.querySelectorAll("button")].some(
+ (button) => button.textContent?.trim() === "Merge",
+ ),
+ ).toBe(false);
+});
+
+it("offers enable auto-merge while checks are pending", async () => {
+ await using screen = await setup();
+ const { container } = screen;
+ state.status = "pending";
+ state.actions = ["merge", "enable-auto-merge"];
+ useUiStateStore.getState().setPullRequestMergeMethod("squash");
+ state.mergeMethods = ["merge", "squash"];
+ await act(async () => {
+ await screen.rerender(
+ ,
+ );
+ });
+ const clickEnable = async (scope: ParentNode) =>
+ await act(async () => {
+ [...scope.querySelectorAll("button")]
+ .find((button) => button.textContent?.trim() === "Enable auto-merge")!
+ .click();
+ });
+ await clickEnable(container);
+ const dialog = document.querySelector('[role="alertdialog"]')!;
+ expect(dialog.textContent).toContain("not right away");
+ await clickEnable(dialog);
+ expect(state.perform).toHaveBeenCalledWith("enable-auto-merge", "squash");
+});
+
async function setup() {
state.perform.mockReset();
state.checkoutError = null;
@@ -190,6 +259,8 @@ async function setup() {
state.status = "success";
state.extraStatus = null;
+ state.autoMergeEnabled = false;
+ state.actions = ["merge"];
state.mergeMethods = ["merge"];
useUiStateStore.getState().setPullRequestMergeMethod("merge");
return renderUi(null);
diff --git a/apps/web/src/components/chat/ThreadDetailsPrRow.tsx b/apps/web/src/components/chat/ThreadDetailsPrRow.tsx
index 8c3f814427991..18418210907a3 100644
--- a/apps/web/src/components/chat/ThreadDetailsPrRow.tsx
+++ b/apps/web/src/components/chat/ThreadDetailsPrRow.tsx
@@ -174,11 +174,11 @@ export function ThreadDetailsPrRow({
environmentId,
(project?.id as ProjectId | undefined) ?? null,
);
- const [confirmingMerge, setConfirmingMerge] = useState(false);
+ const [confirming, setConfirming] = useState<"merge" | "enable-auto-merge" | null>(null);
const rowAction = resolveThreadPanelPullRequestAction(detail);
- if (confirmingMerge && rowAction !== "merge") {
- setConfirmingMerge(false);
+ if (confirming !== null && rowAction !== confirming) {
+ setConfirming(null);
}
const conflicting = isPullRequestConflicting(detail);
const checksState = detail === null ? "none" : classifyPullRequestChecks(detail.checks);
@@ -348,17 +348,27 @@ export function ThreadDetailsPrRow({
tooltip: "Fix the failing checks in a new thread",
onClick: startFixChecks,
}
- : rowAction === "merge"
+ : rowAction === "enable-auto-merge"
? {
- label: "Merge",
- pendingLabel: "Merging...",
+ label: "Enable auto-merge",
+ pendingLabel: "Enabling...",
pending: actionPending,
destructive: false,
suffix: null,
- tooltip: `Merge this ${terminology.singular} (${selectedMergeMethod})`,
- onClick: () => setConfirmingMerge(true),
+ tooltip: `Enable auto-merge (${selectedMergeMethod}): waits for requirements instead of merging right away`,
+ onClick: () => setConfirming("enable-auto-merge"),
}
- : null;
+ : rowAction === "merge"
+ ? {
+ label: "Merge",
+ pendingLabel: "Merging...",
+ pending: actionPending,
+ destructive: false,
+ suffix: null,
+ tooltip: `Merge this ${terminology.singular} (${selectedMergeMethod})`,
+ onClick: () => setConfirming("merge"),
+ }
+ : null;
const watching =
onStopWatching !== undefined && (detail?.state ?? pr?.state ?? "open") === "open";
@@ -482,13 +492,22 @@ export function ThreadDetailsPrRow({
) : (
openRow(label, "row")
)}
- {rowAction === "merge" ? (
- setConfirmingMerge(open)}>
+ {rowAction === "merge" || rowAction === "enable-auto-merge" ? (
+ setConfirming(open ? rowAction : null)}
+ >
- Merge {terminology.singular}?
+
+ {rowAction === "enable-auto-merge"
+ ? "Enable auto-merge?"
+ : `Merge ${terminology.singular}?`}
+
- This merges #{number} using {selectedMergeMethod}.
+ {rowAction === "enable-auto-merge"
+ ? `The host will merge #${number} using ${selectedMergeMethod} once its requirements are met, not right away.`
+ : `This merges #${number} using ${selectedMergeMethod}.`}
@@ -499,11 +518,11 @@ export function ThreadDetailsPrRow({
size="sm"
disabled={actionPending}
onClick={() => {
- setConfirmingMerge(false);
- void perform("merge", selectedMergeMethod);
+ setConfirming(null);
+ void perform(rowAction, selectedMergeMethod);
}}
>
- Merge
+ {rowAction === "enable-auto-merge" ? "Enable auto-merge" : "Merge"}
diff --git a/apps/web/src/components/pullRequest/PullRequestActionsMenu.tsx b/apps/web/src/components/pullRequest/PullRequestActionsMenu.tsx
index df402185ce350..627dda77a3b5c 100644
--- a/apps/web/src/components/pullRequest/PullRequestActionsMenu.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestActionsMenu.tsx
@@ -17,6 +17,7 @@ import type {
import {
ArrowUpRightIcon,
BookOpenIcon,
+ ClockIcon,
CopyIcon,
HammerIcon,
LinkIcon,
@@ -348,7 +349,7 @@ export function PullRequestActionsMenu({
closeOnClick={inlineConfirm.isArmed("enable-auto-merge")}
aria-label={
inlineConfirm.isArmed("enable-auto-merge")
- ? "Confirm auto-merge?"
+ ? "Confirm enable auto-merge?"
: "Enable auto-merge"
}
disabled={actionPending}
@@ -357,9 +358,9 @@ export function PullRequestActionsMenu({
() => void perform("enable-auto-merge", selectedMergeMethod),
)}
>
-
+
{inlineConfirm.isArmed("enable-auto-merge")
- ? "Confirm auto-merge?"
+ ? "Confirm enable auto-merge?"
: "Enable auto-merge"}
) : null}
diff --git a/apps/web/src/components/pullRequest/PullRequestDetailActions.tsx b/apps/web/src/components/pullRequest/PullRequestDetailActions.tsx
index f5a5b4ee77bd9..555e135198324 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailActions.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailActions.tsx
@@ -4,7 +4,14 @@ import type {
PullRequestMergeMethod,
PullRequestUpdateMethod,
} from "@t3tools/contracts";
-import { CheckIcon, ChevronDownIcon, FolderGit2Icon, GitBranchIcon, PlayIcon } from "lucide-react";
+import {
+ CheckIcon,
+ ChevronDownIcon,
+ ClockIcon,
+ FolderGit2Icon,
+ GitBranchIcon,
+ PlayIcon,
+} from "lucide-react";
import type { ReactNode } from "react";
import { AnimatedWidth } from "~/components/AnimatedWidth";
@@ -277,14 +284,14 @@ export function PullRequestPrimaryAction({
pendingAction === "enable-auto-merge"
? "Enabling..."
: inlineConfirm.isArmed("enable-auto-merge")
- ? "Confirm auto-merge?"
+ ? "Confirm enable auto-merge?"
: pendingAutoMergeLabel
}
>
{inlineConfirm.isArmed("enable-auto-merge") ? (
) : (
-
+
)}
@@ -295,7 +302,7 @@ export function PullRequestPrimaryAction({
{pendingAction === "enable-auto-merge"
? "Enabling..."
: inlineConfirm.isArmed("enable-auto-merge")
- ? "Confirm auto-merge?"
+ ? "Confirm enable auto-merge?"
: pendingAutoMergeLabel}
@@ -309,8 +316,8 @@ export function PullRequestPrimaryAction({
(pendingAction === "enable-auto-merge"
? "Enabling..."
: inlineConfirm.isArmed("enable-auto-merge")
- ? "Confirm auto-merge?"
- : pendingAutoMergeLabel)}
+ ? "Confirm enable auto-merge?"
+ : `${pendingAutoMergeLabel}: waits for requirements instead of merging right away`)}
) : primaryAction === "auto-merge-armed" ? (
diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
index 4c8e1e2acbcd6..11cfea2e71e93 100644
--- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
+++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
@@ -1117,13 +1117,11 @@ export function PullRequestDetailPanel({
lastSelectedMergeMethod,
);
const selectedMergeMethodLabel = PULL_REQUEST_MERGE_METHOD_LABELS[selectedMergeMethod];
- const pendingAutoMergeLabel = `Auto-merge (${selectedMergeMethodLabel.toLowerCase()})`;
+ const pendingAutoMergeLabel = `Enable auto-merge (${selectedMergeMethod})`;
const conflicting = isPullRequestConflicting(detail);
const autoMergeArmed = detail?.state === "open" && detail.autoMergeEnabled === true;
const armedMergeMethod = detail?.autoMergeMethod;
- const armedAutoMergeLabel = armedMergeMethod
- ? `Auto-merge (${PULL_REQUEST_MERGE_METHOD_LABELS[armedMergeMethod].toLowerCase()})`
- : "Auto-merge";
+ const armedAutoMergeLabel = armedMergeMethod ? `Auto-merge (${armedMergeMethod})` : "Auto-merge";
const workflowApprovalsRequired =
detail?.state === "open" ? (detail.workflowApprovalsRequired ?? 0) : 0;
const freshness = detail === null ? null : resolveBaseFreshness(detail);
diff --git a/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts b/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts
index 7c1cc47883f6f..c1aeb8bb3c38e 100644
--- a/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts
+++ b/apps/web/src/components/pullRequest/pullRequestDetail.logic.test.ts
@@ -1232,6 +1232,33 @@ describe("the compact row's single action slot", () => {
).toBeNull();
});
+ it("offers enable-auto-merge while checks run when that is available instead of Merge", () => {
+ expect(
+ resolveThreadPanelPullRequestAction(
+ openDetail({
+ autoMergeEnabled: false,
+ checks: [check("pending")],
+ capabilities: {
+ actions: ["merge", "enable-auto-merge"],
+ mergeMethods: ["merge", "squash"],
+ } as unknown as PullRequestDetailView["capabilities"],
+ viewerPermissions: {
+ actions: ["merge", "enable-auto-merge"],
+ } as unknown as PullRequestDetailView["viewerPermissions"],
+ }),
+ ),
+ ).toBe("enable-auto-merge");
+ });
+
+ it("hides Merge once auto-merge is armed, but still offers Fix for failing checks", () => {
+ expect(resolveThreadPanelPullRequestAction(openDetail({ autoMergeEnabled: true }))).toBeNull();
+ expect(
+ resolveThreadPanelPullRequestAction(
+ openDetail({ autoMergeEnabled: true, checks: [check("failure")] }),
+ ),
+ ).toBe("fix");
+ });
+
it("ranks conflicts above everything, then draft, then failing checks", () => {
expect(
resolveThreadPanelPullRequestAction(
diff --git a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts
index 2a7dc7151b92d..6017591ca7458 100644
--- a/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts
+++ b/apps/web/src/components/pullRequest/pullRequestDetail.logic.ts
@@ -276,7 +276,13 @@ export function isStackedPullRequestBase(
type PullRequestActionableDetail = Pick<
PullRequestDetail,
- "state" | "isDraft" | "mergeability" | "capabilities" | "viewerPermissions" | "mergeCapabilities"
+ | "state"
+ | "isDraft"
+ | "mergeability"
+ | "capabilities"
+ | "viewerPermissions"
+ | "mergeCapabilities"
+ | "autoMergeEnabled"
>;
export function allowedPullRequestMergeMethods(
@@ -349,7 +355,7 @@ export function groupPullRequestChecks(checks: ReadonlyArray)
};
}
-type ThreadPanelPullRequestAction = "resolve" | "ready" | "fix" | "merge";
+type ThreadPanelPullRequestAction = "resolve" | "ready" | "fix" | "merge" | "enable-auto-merge";
export function resolveThreadPanelPullRequestAction(
detail: (PullRequestActionableDetail & Pick) | null,
@@ -361,11 +367,19 @@ export function resolveThreadPanelPullRequestAction(
}
const checks = classifyPullRequestChecks(detail.checks);
if (checks === "failing") return "fix";
+ // Auto-merge already waits on the host; do not keep a Merge control beside Commit.
+ if (detail.autoMergeEnabled === true) return null;
+ const hasMergeMethod = allowedPullRequestMergeMethods(detail).length > 0;
+ if (
+ checks === "pending" &&
+ detail.autoMergeEnabled === false &&
+ canPerformPullRequestAction(detail, "enable-auto-merge") &&
+ hasMergeMethod
+ ) {
+ return "enable-auto-merge";
+ }
if (checks === "pending") return null;
- return canPerformPullRequestAction(detail, "merge") &&
- allowedPullRequestMergeMethods(detail).length > 0
- ? "merge"
- : null;
+ return canPerformPullRequestAction(detail, "merge") && hasMergeMethod ? "merge" : null;
}
export function orderPullRequestComments(
diff --git a/apps/web/src/components/pullRequest/useInlineActionConfirm.browser.test.tsx b/apps/web/src/components/pullRequest/useInlineActionConfirm.browser.test.tsx
index c94403abde4d3..202edd6766394 100644
--- a/apps/web/src/components/pullRequest/useInlineActionConfirm.browser.test.tsx
+++ b/apps/web/src/components/pullRequest/useInlineActionConfirm.browser.test.tsx
@@ -89,7 +89,7 @@ it.each(["merge", "enable-auto-merge"] as const)(
mergeBlockedReason={mergeBlockedReason}
autoMergeArmed={false}
armedAutoMergeLabel="Auto-merge enabled"
- pendingAutoMergeLabel="Auto-merge (squash)"
+ pendingAutoMergeLabel="Enable auto-merge (squash)"
selectedMergeMethod="squash"
selectedMergeMethodLabel="Squash and merge"
statePresentation={null}