diff --git a/webviews/components/icon.tsx b/webviews/components/icon.tsx
index b7a1b3f26a..fbd30d8b37 100644
--- a/webviews/components/icon.tsx
+++ b/webviews/components/icon.tsx
@@ -34,6 +34,7 @@ export const gitPullRequestClosedIcon =
;
export const gitPullRequestIcon =
;
export const issuescon =
;
+export const layersIcon =
;
export const loadingIcon =
;
export const milestoneIcon =
;
export const notebookTemplate =
;
diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx
index c8d7d31ced..4d793c8444 100644
--- a/webviews/components/merge.tsx
+++ b/webviews/components/merge.tsx
@@ -16,6 +16,7 @@ import { AutoMerge, QueuedToMerge } from './automergeSelect';
import { ContextDropdown } from './contextDropdown';
import { Dropdown } from './dropdown';
import { checkIcon, circleFilledIcon, closeIcon, gitMergeIcon, loadingIcon, outputIcon, requestChangesIcon, skipIcon, warningIcon } from './icon';
+import { StackSection } from './pullRequestStack';
import { nbsp } from './space';
import { Avatar } from './user';
import { EventType, ReviewEvent } from '../../src/common/timelineEvent';
@@ -156,6 +157,7 @@ export const StatusChecksSection = ({ pr, isSimple }: { pr: PullRequest; isSimpl
+ {!isSimple ?
: null}
>
@@ -401,18 +403,34 @@ export const Merge = (pr: PullRequest) => {
export const PrActions = ({ pr, isSimple }: { pr: PullRequest; isSimple: boolean }) => {
const { hasWritePermission, canEdit, isDraft, mergeable, isCopilotOnMyBehalf, defaultMergeMethod } = pr;
+ if (pr.stackLoadError) {
+ return
Unable to load pull request stack details. Refresh to retry merging.
;
+ }
+ if (pr.stackLoaded === false) {
+ return
Checking pull request stack membership...
;
+ }
if (isDraft) {
// Only PR author and users with push rights can mark draft as ready for review
if (!canEdit) {
return null;
}
- return
;
+ return
;
+ }
+
+ if (pr.stackMergeStatus === 'pending') {
+ return
Stack merge is still processing on GitHub. Refresh to check its status.
;
+ }
+ if (pr.stackMergeStatus === 'enqueued') {
+ return
Pull request stack added to the merge queue.
;
+ }
+ if (pr.stack && hasWritePermission && !pr.mergeQueueEntry) {
+ return
;
}
- if (mergeable === PullRequestMergeability.Mergeable && hasWritePermission && !pr.mergeQueueEntry) {
+ if (!pr.stack && mergeable === PullRequestMergeability.Mergeable && hasWritePermission && !pr.mergeQueueEntry) {
return isSimple ?
:
;
- } else if (!isSimple && hasWritePermission && !pr.mergeQueueEntry) {
+ } else if (!pr.stack && !isSimple && hasWritePermission && !pr.mergeQueueEntry) {
const ctx = useContext(PullRequestContext);
return (
{
- const { openOnGitHub } = useContext(PullRequestContext);
- return (
- openOnGitHub()}>
- Merge on github.com
-
- );
+export const MergeStack = ({ pr }: { pr: PullRequest }) => {
+ const { mergeStack, updatePR } = useContext(PullRequestContext);
+ const [method, setMethod] = useState(pr.defaultMergeMethod);
+ const [confirm, setConfirm] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState();
+ if (!pr.stack) {
+ throw new Error('A pull request stack is required for stack merge actions.');
+ }
+ const count = 1 + pr.stack.pullRequests.filter(entry => entry.position < pr.stack.position && entry.state === GithubItemStateEnum.Open).length;
+ const downstack = count - 1;
+ const downstackDescription = `${downstack} open pull request${downstack === 1 ? '' : 's'} below it`;
+ const label = pr.mergeQueueMethod ? 'Add stack to merge queue' : `Merge stack (${count} pull request${count === 1 ? '' : 's'})`;
+
+ const submit = async () => {
+ try {
+ setBusy(true);
+ setError(undefined);
+ const result = await mergeStack(method);
+ if (result.status !== 'cancelled') {
+ updatePR({
+ state: result.state ?? pr.state,
+ revertable: result.status === 'merged',
+ stackMergeStatus: result.status === 'pending' || result.status === 'enqueued' ? result.status : undefined,
+ });
+ }
+ setConfirm(false);
+ } catch (mergeError) {
+ setError(mergeError instanceof Error ? mergeError.message || mergeError.name : String(mergeError));
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ return
+ {confirm ? <>
+
{pr.mergeQueueMethod
+ ? downstack ? `Add this pull request and ${downstackDescription} to the merge queue?` : 'Add this pull request to the merge queue?'
+ : downstack ? `Merge this pull request and ${downstackDescription}?` : 'Merge this pull request?'}
+ {error ?
Unable to merge stack: {error}
: null}
+
setConfirm(false)}>Cancel
+
+ {busy ? {loadingIcon} : null}
+ {busy ? pr.mergeQueueMethod ? 'Adding stack to merge queue...' : 'Merging stack...' : label}
+
+ > : <>
+
setConfirm(true)}>{label}
+ {!pr.mergeQueueMethod ?
+ using method
+ setMethod(event.target.value as MergeMethod)} />
+
: null}
+ >}
+
;
};
export const MergeSimple = (pr: PullRequest) => {
diff --git a/webviews/components/pullRequestStack.tsx b/webviews/components/pullRequestStack.tsx
new file mode 100644
index 0000000000..0c63616719
--- /dev/null
+++ b/webviews/components/pullRequestStack.tsx
@@ -0,0 +1,83 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as React from 'react';
+import { checkIcon, chevronDownIcon, circleFilledIcon, closeIcon, layersIcon, warningIcon } from './icon';
+import { GithubItemStateEnum, PullRequestMergeability, PullRequestStack as Stack } from '../../src/github/interface';
+import { PullRequest } from '../../src/github/views';
+
+function getReadiness(entry: Stack['pullRequests'][number]): { icon: JSX.Element; label: string; kind: string } {
+ if (entry.state === GithubItemStateEnum.Merged) {
+ return { icon: checkIcon, label: 'Already merged', kind: 'ready' };
+ }
+ if (entry.state === GithubItemStateEnum.Closed) {
+ return { icon: closeIcon, label: 'Closed pull request cannot be merged', kind: 'blocked' };
+ }
+ if (entry.isDraft) {
+ return { icon: warningIcon, label: 'Draft pull request cannot be merged', kind: 'waiting' };
+ }
+ switch (entry.mergeable) {
+ case PullRequestMergeability.Mergeable:
+ return { icon: checkIcon, label: 'Ready to merge', kind: 'ready' };
+ case PullRequestMergeability.Conflict:
+ return { icon: closeIcon, label: 'Merge conflicts', kind: 'blocked' };
+ case PullRequestMergeability.NotMergeable:
+ return { icon: closeIcon, label: 'Merge requirements not met', kind: 'blocked' };
+ case PullRequestMergeability.Behind:
+ return { icon: warningIcon, label: 'Branch is behind its base', kind: 'waiting' };
+ default:
+ return { icon: circleFilledIcon, label: 'Mergeability is being checked', kind: 'waiting' };
+ }
+}
+
+export const StackBadge = ({ stack }: { stack?: Stack }) => stack ? (
+
+ {layersIcon}
+ {stack.position}/{stack.size}
+
+) : null;
+
+export const StackSection = ({ pr }: { pr: PullRequest }) => {
+ const { stack } = pr;
+ if (!stack) {
+ return null;
+ }
+ const openBelow = stack.pullRequests.filter(entry => entry.position < stack.position && entry.state === GithubItemStateEnum.Open).length;
+
+ return (
+
+
+ {layersIcon}
+
+ Pull request stack
+
+ {pr.state === GithubItemStateEnum.Open && openBelow > 0
+ ? `Merging this pull request will also merge ${openBelow} pull request${openBelow === 1 ? '' : 's'} below it.`
+ : `${stack.size} pull requests in this stack.`}
+
+
+ {chevronDownIcon}
+
+
+ {[...stack.pullRequests].reverse().map(entry => {
+ const current = entry.number === pr.number;
+ const readiness = getReadiness(entry);
+
+ return
+ {readiness.icon}
+
+ {current ? {entry.title} : {entry.title} }
+ #{entry.number} - {entry.head}
+
+ ;
+ })}
+
+
+ {stack.base}
+
+
+
+ );
+};
diff --git a/webviews/editorWebview/index.css b/webviews/editorWebview/index.css
index 157d6f561f..8299b3b2af 100644
--- a/webviews/editorWebview/index.css
+++ b/webviews/editorWebview/index.css
@@ -633,6 +633,141 @@ body button .icon {
align-items: center;
flex-wrap: wrap;
row-gap: 12px;
+ column-gap: 8px;
+}
+
+.stack-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ border: 1px solid var(--vscode-panel-border);
+ border-radius: 16px;
+ color: var(--vscode-foreground);
+ text-decoration: none;
+}
+
+.stack-badge:hover {
+ color: var(--vscode-textLink-activeForeground);
+}
+
+.stack-badge .icon svg,
+.stack-section summary .icon svg,
+.stack-entry-readiness svg {
+ width: 16px;
+ height: 16px;
+}
+
+.stack-section summary {
+ cursor: pointer;
+}
+
+.stack-section {
+ scroll-margin-top: 64px;
+}
+
+.stack-section summary > .icon {
+ flex-shrink: 0;
+}
+
+.stack-chevron {
+ margin-left: auto;
+}
+
+.stack-section:not([open]) .stack-chevron {
+ transform: rotate(-90deg);
+}
+
+.stack-description,
+.stack-entry-meta {
+ display: block;
+ color: var(--vscode-descriptionForeground);
+ font-size: 0.9em;
+}
+
+.stack-entries {
+ list-style: none;
+ margin: 0;
+ padding: 8px 16px;
+}
+
+.stack-entry,
+.stack-base {
+ display: grid;
+ grid-template-columns: 20px minmax(0, 1fr);
+ column-gap: 8px;
+ position: relative;
+ padding: 8px;
+}
+
+.stack-entry::before {
+ content: '';
+ position: absolute;
+ left: 17px;
+ top: 30px;
+ bottom: 0;
+ border-left: 1px solid var(--vscode-descriptionForeground);
+ opacity: 0.65;
+}
+
+.stack-entry.current {
+ background: var(--vscode-panel-background);
+ border-radius: 4px;
+ box-shadow: inset 3px 0 var(--vscode-focusBorder);
+}
+
+.stack-entry-readiness {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ position: relative;
+ z-index: 1;
+ border-radius: 50%;
+ background: var(--vscode-editor-background);
+}
+
+.stack-entry.current .stack-entry-readiness {
+ background: var(--vscode-panel-background);
+}
+
+.stack-entry-readiness.ready {
+ color: var(--vscode-issues-open);
+}
+
+.stack-entry-readiness.ready svg path {
+ fill: var(--vscode-issues-open);
+}
+
+.stack-entry-readiness.blocked {
+ color: var(--vscode-errorForeground);
+}
+
+.stack-entry-readiness.waiting {
+ color: var(--vscode-list-warningForeground);
+}
+
+.stack-entry-details {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.stack-base {
+ align-items: center;
+}
+
+.stack-base-marker {
+ box-sizing: border-box;
+ width: 8px;
+ height: 8px;
+ margin-left: 5px;
+ border: 2px solid var(--vscode-descriptionForeground);
+ border-radius: 50%;
+}
+
+.stack-base .branch-tag {
+ width: fit-content;
}
.subtitle .avatar,
@@ -716,7 +851,6 @@ button.secondary.change-base {
color: var(--vscode-button-foreground);
border-radius: 18px;
padding: 4px 12px;
- margin-right: 10px;
font-weight: 600;
display: flex;
gap: 4px;
diff --git a/webviews/editorWebview/test/builder/pullRequest.ts b/webviews/editorWebview/test/builder/pullRequest.ts
index 265316eec6..b764bb8656 100644
--- a/webviews/editorWebview/test/builder/pullRequest.ts
+++ b/webviews/editorWebview/test/builder/pullRequest.ts
@@ -29,6 +29,10 @@ export const PullRequestBuilder = createBuilderClass()({
isRemoteHeadDeleted: { default: false },
isLocalHeadDeleted: { default: false },
head: { default: 'my-fork:my-branch' },
+ stack: { default: undefined },
+ stackLoaded: { default: true },
+ stackLoadError: { default: false },
+ stackMergeStatus: { default: undefined },
labels: { default: [] },
isAuthor: { default: true },
commitsCount: { default: 10 },
diff --git a/webviews/editorWebview/test/overview.test.tsx b/webviews/editorWebview/test/overview.test.tsx
index 6a23cae97d..e4cadbab10 100644
--- a/webviews/editorWebview/test/overview.test.tsx
+++ b/webviews/editorWebview/test/overview.test.tsx
@@ -5,9 +5,10 @@
import { default as assert } from 'assert';
import * as React from 'react';
-import { cleanup, fireEvent, render } from 'react-testing-library';
+import { cleanup, fireEvent, render, waitForElement } from 'react-testing-library';
import { createSandbox, SinonSandbox } from 'sinon';
+import { GithubItemStateEnum, PullRequestMergeability } from '../../../src/github/interface';
import { PRContext, default as PullRequestContext } from '../../common/context';
import { Overview } from '../overview';
import { PullRequestBuilder } from './builder/pullRequest';
@@ -59,6 +60,243 @@ describe('Overview', function () {
assert.strictEqual(openOnGitHub.callCount, 2);
});
+ it('shows the stack position and ordered pull requests in the merge section', function () {
+ const pr = new PullRequestBuilder().number(794).stack({
+ position: 2,
+ size: 3,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 793, title: 'First Change', head: 'D1', url: 'https://example.com/793', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable },
+ { position: 2, number: 794, title: 'Second Change', head: 'D2', url: 'https://example.com/794', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.NotMergeable },
+ { position: 3, number: 795, title: 'Third Change', head: 'D3', url: 'https://example.com/795', state: GithubItemStateEnum.Open, isDraft: true, mergeable: PullRequestMergeability.Mergeable },
+ ],
+ }).build();
+ const context = new PRContext(pr);
+ const mergeStack = sinon.stub(context, 'mergeStack').resolves({ status: 'merged', state: GithubItemStateEnum.Merged });
+ const openOnGitHub = sinon.stub(context, 'openOnGitHub');
+
+ const out = render(
+
+
+ ,
+ );
+
+ const badge = out.container.querySelector('.stack-badge');
+ assert.strictEqual(badge?.getAttribute('href'), '#pull-request-stack');
+ assert.strictEqual(badge?.getAttribute('title'), 'View pull request stack (2 of 3)');
+ assert.strictEqual(badge?.textContent?.trim(), '2/3');
+ const section = out.container.querySelector('#pull-request-stack');
+ assert(section?.hasAttribute('open'));
+ assert(section.textContent?.includes('also merge 1 pull request below it.'));
+ assert.deepStrictEqual([...section.querySelectorAll('.stack-entry')].map(entry => entry.querySelector('.stack-entry-details')?.textContent), [
+ 'Third Change#795 - D3',
+ 'Second Change#794 - D2',
+ 'First Change#793 - D1',
+ ]);
+ assert.deepStrictEqual([...section.querySelectorAll('.stack-entry-readiness')].map(entry => [entry.classList[1], entry.getAttribute('aria-label')]), [
+ ['waiting', 'Draft pull request cannot be merged'],
+ ['blocked', 'Merge requirements not met'],
+ ['ready', 'Ready to merge'],
+ ]);
+ assert.deepStrictEqual([...section.querySelectorAll('.stack-entry-readiness')].map(entry => entry.getAttribute('title')), [
+ 'Draft pull request cannot be merged',
+ 'Merge requirements not met',
+ 'Ready to merge',
+ ]);
+ assert.strictEqual(section.querySelector('.stack-entry-state'), null);
+ assert.strictEqual(section.querySelectorAll('.stack-entry').length, 3);
+ assert.strictEqual(section.querySelectorAll('.stack-entry-readiness svg').length, 3);
+ assert.strictEqual(section.querySelector('.stack-base-marker')?.getAttribute('aria-hidden'), 'true');
+ assert.strictEqual(section.querySelector('.stack-entries')?.lastElementChild?.className, 'stack-base');
+ assert.strictEqual(section.querySelector('.stack-entries')?.getAttribute('aria-label'), 'Pull requests merging down into main');
+ assert.strictEqual(section.querySelector('.stack-entry.current')?.getAttribute('aria-current'), 'step');
+ assert.strictEqual(section.querySelector('.stack-entry.current a'), null);
+ assert.strictEqual(section.querySelector('a[href="https://example.com/793"]')?.textContent, 'First Change');
+ assert.strictEqual(section.querySelector('.stack-base')?.textContent, 'main');
+ assert.strictEqual(out.container.querySelector('#merge-comment-form'), null);
+ assert(out.container.querySelector('.stack-merge .stack-merge-method select'));
+ const mergeButton = out.getByText('Merge stack (2 pull requests)');
+ fireEvent.change(out.getByLabelText('Select merge method'), { target: { value: 'squash' } });
+ fireEvent.click(mergeButton);
+ assert(out.getByText('Merge this pull request and 1 open pull request below it?'));
+ fireEvent.click(out.getByText('Merge stack (2 pull requests)'));
+ assert(mergeStack.calledOnceWithExactly('squash'));
+ assert(openOnGitHub.notCalled);
+ });
+
+ it('does not show a stack badge or section for an unstacked pull request', function () {
+ const pr = new PullRequestBuilder().build();
+ const out = render(
+
+
+ ,
+ );
+
+ assert.strictEqual(out.container.querySelector('.stack-badge'), null);
+ assert.strictEqual(out.container.querySelector('#pull-request-stack'), null);
+ assert(out.getByText('Merge Pull Request'));
+ });
+
+ it('shows a closed stack without suggesting it can be merged', function () {
+ const pr = new PullRequestBuilder().state(GithubItemStateEnum.Closed).stack({
+ position: 1,
+ size: 2,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 1234, title: 'First Change', head: 'D1', url: 'https://example.com/1234', state: GithubItemStateEnum.Closed, isDraft: false, mergeable: PullRequestMergeability.Unknown },
+ { position: 2, number: 1235, title: 'Second Change', head: 'D2', url: 'https://example.com/1235', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Unknown },
+ ],
+ }).build();
+ const out = render(
+
+
+ ,
+ );
+
+ assert(out.container.querySelector('#pull-request-stack')?.textContent?.includes('2 pull requests in this stack.'));
+ assert.deepStrictEqual([...out.container.querySelectorAll('.stack-entry-readiness')].map(entry => entry.getAttribute('aria-label')), [
+ 'Mergeability is being checked',
+ 'Closed pull request cannot be merged',
+ ]);
+ assert.strictEqual(out.container.querySelector('.automerge-section'), null);
+ });
+
+ it('does not count already merged pull requests in the merge impact', function () {
+ const pr = new PullRequestBuilder().number(795).stack({
+ position: 3,
+ size: 3,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 793, title: 'First Change', head: 'D1', url: 'https://example.com/793', state: GithubItemStateEnum.Merged, isDraft: false, mergeable: PullRequestMergeability.Unknown },
+ { position: 2, number: 794, title: 'Second Change', head: 'D2', url: 'https://example.com/794', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Unknown },
+ { position: 3, number: 795, title: 'Third Change', head: 'D3', url: 'https://example.com/795', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Behind },
+ ],
+ }).build();
+ const out = render(
+
+
+ ,
+ );
+
+ assert(out.container.querySelector('#pull-request-stack')?.textContent?.includes('also merge 1 pull request below it.'));
+ assert.deepStrictEqual([...out.container.querySelectorAll('.stack-entry-readiness')].map(entry => entry.getAttribute('aria-label')), [
+ 'Branch is behind its base',
+ 'Mergeability is being checked',
+ 'Already merged',
+ ]);
+ });
+
+ it('does not offer to merge a stack without write permission', function () {
+ const pr = new PullRequestBuilder().hasWritePermission(false).stack({
+ position: 1,
+ size: 1,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 1234, title: 'First Change', head: 'D1', url: 'https://example.com/1234', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable },
+ ],
+ }).build();
+ const out = render(
+
+
+ ,
+ );
+
+ assert(out.container.querySelector('#pull-request-stack'));
+ assert.strictEqual(out.container.querySelector('.automerge-section'), null);
+ assert.strictEqual(out.container.querySelector('#merge-comment-form'), null);
+ });
+
+ it('offers to queue a stack without showing a direct merge method', function () {
+ const pr = new PullRequestBuilder().mergeQueueMethod('squash').stack({
+ position: 1,
+ size: 1,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 1234, title: 'First Change', head: 'D1', url: 'https://example.com/1234', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable },
+ ],
+ }).build();
+ const out = render(
+
+
+ ,
+ );
+
+ assert(out.getByText('Add stack to merge queue'));
+ assert.strictEqual(out.container.querySelector('.automerge-section select'), null);
+ });
+
+ it('keeps the PR open when an asynchronous stack merge is pending', async function () {
+ const pr = new PullRequestBuilder().stack({
+ position: 1,
+ size: 1,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 1234, title: 'First Change', head: 'D1', url: 'https://example.com/1234', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable },
+ ],
+ }).build();
+ const context = new PRContext(pr);
+ const mergeStack = sinon.stub(context, 'mergeStack').resolves({ status: 'pending' });
+ const out = render(
+
+
+ ,
+ );
+
+ fireEvent.click(out.getByText('Merge stack (1 pull request)'));
+ fireEvent.click(out.getByText('Merge stack (1 pull request)'));
+ await Promise.resolve();
+
+ assert(mergeStack.calledOnceWithExactly('merge'));
+ assert.strictEqual(context.pr?.state, GithubItemStateEnum.Open);
+ assert.strictEqual(context.pr?.stackMergeStatus, 'pending');
+ });
+
+ it('shows stack merge failures without marking the pull request merged', async function () {
+ const pr = new PullRequestBuilder().stack({
+ position: 1,
+ size: 1,
+ base: 'main',
+ pullRequests: [
+ { position: 1, number: 1234, title: 'First Change', head: 'D1', url: 'https://example.com/1234', state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable },
+ ],
+ }).build();
+ const context = new PRContext(pr);
+ const mergeStack = sinon.stub(context, 'mergeStack').rejects(new Error('Required checks failed'));
+ const out = render(
+
+
+ ,
+ );
+
+ fireEvent.click(out.getByText('Merge stack (1 pull request)'));
+ fireEvent.click(out.getByText('Merge stack (1 pull request)'));
+
+ assert(mergeStack.calledOnce);
+ assert.strictEqual((await waitForElement(() => out.container.querySelector('[role="alert"]')))?.textContent, 'Unable to merge stack: Required checks failed');
+ assert.strictEqual(context.pr?.state, GithubItemStateEnum.Open);
+ assert.strictEqual(out.getByText('Merge stack (1 pull request)').hasAttribute('disabled'), false);
+ });
+
+ it('does not offer a legacy merge while stack membership is loading or failed', function () {
+ const pr = new PullRequestBuilder().stackLoaded(false).build();
+ const out = render(
+
+
+ ,
+ );
+ assert(out.getByText('Checking pull request stack membership...'));
+ assert.strictEqual(out.container.querySelector('.automerge-section'), null);
+
+ const failed = { ...pr, stackLoaded: true, stackLoadError: true };
+ out.rerender(
+
+
+ ,
+ );
+ assert(out.getByText('Unable to load pull request stack details. Refresh to retry merging.'));
+ assert.strictEqual(out.container.querySelector('.automerge-section'), null);
+ });
+
it('shows view changes in both headers', function () {
const pr = new PullRequestBuilder().isAgentSessionsWorkspace(true).build();
const context = new PRContext(pr);