From 0e22c6d129cd6a97959f6e908df92d80ae5dec9f Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:16:44 +0200 Subject: [PATCH 1/2] Polish stack view --- webviews/components/merge.tsx | 2 +- webviews/components/pullRequestStack.tsx | 18 +++--- webviews/editorWebview/index.css | 15 ++++- webviews/editorWebview/test/overview.test.tsx | 62 +++++++++++++++++++ 4 files changed, 86 insertions(+), 11 deletions(-) diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx index 347d2299f7..b13faf6ca2 100644 --- a/webviews/components/merge.tsx +++ b/webviews/components/merge.tsx @@ -542,7 +542,7 @@ export const DeleteBranch = (pr: PullRequest) => { return
; } else { return ( -
+
{ event.preventDefault(); diff --git a/webviews/components/pullRequestStack.tsx b/webviews/components/pullRequestStack.tsx index 219e30e9a6..c57c54ae2b 100644 --- a/webviews/components/pullRequestStack.tsx +++ b/webviews/components/pullRequestStack.tsx @@ -4,30 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { checkIcon, chevronDownIcon, circleFilledIcon, closeIcon, layersIcon, warningIcon } from './icon'; +import { checkIcon, chevronDownIcon, circleFilledIcon, gitPullRequestDraftIcon, layersIcon, passIcon, skipIcon } from './icon'; import { GithubItemStateEnum, PullRequestMergeability, PullRequestStack as Stack } from '../../src/github/interface'; import { PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; -function getReadiness(entry: Stack['pullRequests'][number]): { icon: JSX.Element; label: string; kind: string } { +function getReadiness(entry: Stack['pullRequests'][number], currentPosition: 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' }; + return { icon: skipIcon, label: 'Closed pull request cannot be merged', kind: 'blocked' }; } if (entry.isDraft) { - return { icon: warningIcon, label: 'Draft pull request cannot be merged', kind: 'waiting' }; + return { icon: gitPullRequestDraftIcon, label: 'Draft pull request cannot be merged', kind: 'waiting' }; } switch (entry.mergeable) { case PullRequestMergeability.Mergeable: - return { icon: checkIcon, label: 'Ready to merge', kind: 'ready' }; + return { icon: entry.position > currentPosition ? circleFilledIcon : passIcon, label: 'Ready to merge', kind: 'ready' }; case PullRequestMergeability.Conflict: - return { icon: closeIcon, label: 'Merge conflicts', kind: 'blocked' }; + return { icon: circleFilledIcon, label: 'Merge conflicts', kind: 'waiting' }; case PullRequestMergeability.NotMergeable: - return { icon: closeIcon, label: 'Merge requirements not met', kind: 'blocked' }; + return { icon: circleFilledIcon, label: 'Merge requirements not met', kind: 'waiting' }; case PullRequestMergeability.Behind: - return { icon: warningIcon, label: 'Branch is behind its base', kind: 'waiting' }; + return { icon: circleFilledIcon, label: 'Branch is behind its base', kind: 'waiting' }; default: return { icon: circleFilledIcon, label: 'Mergeability is being checked', kind: 'waiting' }; } @@ -87,7 +87,7 @@ export const StackSection = ({ pr }: { pr: PullRequest }) => {
    {[...stack.pullRequests].reverse().map(entry => { const current = entry.number === pr.number; - const readiness = getReadiness(entry); + const readiness = getReadiness(entry, stack.position); return
  1. {readiness.icon} diff --git a/webviews/editorWebview/index.css b/webviews/editorWebview/index.css index fa0489db81..846322d6d0 100644 --- a/webviews/editorWebview/index.css +++ b/webviews/editorWebview/index.css @@ -544,6 +544,15 @@ button.input-box { display: inline-block; } +#status-checks .stacked-delete-branch-container { + display: block; + padding: 12px 16px; +} + +#status-checks .stacked-delete-branch-container form { + margin: 0; +} + #status-checks .branch-status-message { display: inline-block; line-height: 100%; @@ -790,7 +799,11 @@ body button .icon { } .stack-entry-readiness.blocked { - color: var(--vscode-errorForeground); + color: var(--vscode-pullRequests-closed); +} + +.stack-entry-readiness.blocked svg path { + fill: var(--vscode-pullRequests-closed); } .stack-entry-readiness.waiting { diff --git a/webviews/editorWebview/test/overview.test.tsx b/webviews/editorWebview/test/overview.test.tsx index bd0a624280..97cb57102c 100644 --- a/webviews/editorWebview/test/overview.test.tsx +++ b/webviews/editorWebview/test/overview.test.tsx @@ -126,6 +126,54 @@ describe('Overview', function () { assert(openOnGitHub.notCalled); }); + it('shows stack state icons appropriate to each pull request and its position', function () { + const states = [ + { state: GithubItemStateEnum.Merged, isDraft: false, mergeable: PullRequestMergeability.Unknown }, + { state: GithubItemStateEnum.Closed, isDraft: false, mergeable: PullRequestMergeability.Unknown }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Mergeable }, + { state: GithubItemStateEnum.Open, isDraft: true, mergeable: PullRequestMergeability.Mergeable }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Conflict }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.NotMergeable }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Behind }, + { state: GithubItemStateEnum.Open, isDraft: false, mergeable: PullRequestMergeability.Unknown }, + ]; + const pr = new PullRequestBuilder().number(4).stack({ + position: 4, size: states.length, base: 'main', + pullRequests: states.map((state, index) => ({ + ...state, position: index + 1, number: index + 1, title: `PR ${index + 1}`, + head: `D${index + 1}`, url: `https://example.com/${index + 1}`, + })), + }).build(); + const out = render( + + + , + ); + const iconPath = (svg: string) => { + const element = document.createElement('div'); + element.innerHTML = svg; + return element.querySelector('path')?.getAttribute('d'); + }; + const dot = iconPath(require('../../../resources/icons/codicons/circle-filled.svg')); + const pass = iconPath(require('../../../resources/icons/codicons/pass.svg')); + assert.deepStrictEqual([...out.container.querySelectorAll('.stack-entry-readiness')].map(entry => [ + entry.getAttribute('aria-label'), entry.classList[1], entry.querySelector('svg path')?.getAttribute('d'), + ]), [ + ['Mergeability is being checked', 'waiting', dot], + ['Branch is behind its base', 'waiting', dot], + ['Merge requirements not met', 'waiting', dot], + ['Merge conflicts', 'waiting', dot], + ['Draft pull request cannot be merged', 'waiting', iconPath(require('../../../resources/icons/codicons/git-pull-request-draft.svg'))], + ['Ready to merge', 'ready', dot], + ['Ready to merge', 'ready', pass], + ['Ready to merge', 'ready', pass], + ['Closed pull request cannot be merged', 'blocked', iconPath(require('../../../resources/icons/codicons/skip.svg'))], + ['Already merged', 'ready', iconPath(require('../../../resources/icons/codicons/check.svg'))], + ]); + }); + it('does not show a stack badge or section for an unstacked pull request', function () { const pr = new PullRequestBuilder().build(); const out = render( @@ -221,9 +269,23 @@ describe('Overview', function () { 'Mergeability is being checked', 'Closed pull request cannot be merged', ]); + assert(out.container.querySelector('.stack-entry-readiness.blocked .icon.skip')); + assert.strictEqual(out.container.querySelector('#status-checks > .stacked-delete-branch-container button')?.textContent?.trim(), 'Delete Branch...'); assert.strictEqual(out.container.querySelector('.automerge-section'), null); }); + it('keeps the original Delete Branch placement outside stacks', function () { + const pr = new PullRequestBuilder().state(GithubItemStateEnum.Closed).build(); + const out = render( + + + , + ); + + assert.strictEqual(out.container.querySelector('#pull-request-stack'), null); + assert.strictEqual(out.container.querySelector('#status-checks > .branch-status-container:not(.stacked-delete-branch-container) button')?.textContent?.trim(), 'Delete Branch...'); + }); + it('does not count already merged pull requests in the merge impact', function () { const pr = new PullRequestBuilder().number(795).stack({ position: 3, From 6d2cddb412f9004ace995ef49ff5040b59cea992 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:43:57 +0200 Subject: [PATCH 2/2] More polish --- webviews/components/pullRequestStack.tsx | 6 ++-- webviews/editorWebview/index.css | 34 +++++++++++++++++++ webviews/editorWebview/test/overview.test.tsx | 6 ++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/webviews/components/pullRequestStack.tsx b/webviews/components/pullRequestStack.tsx index c57c54ae2b..d5fba43a44 100644 --- a/webviews/components/pullRequestStack.tsx +++ b/webviews/components/pullRequestStack.tsx @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { checkIcon, chevronDownIcon, circleFilledIcon, gitPullRequestDraftIcon, layersIcon, passIcon, skipIcon } from './icon'; +import { chevronDownIcon, circleFilledIcon, gitMergeIcon, gitPullRequestDraftIcon, layersIcon, passIcon, skipIcon } from './icon'; import { GithubItemStateEnum, PullRequestMergeability, PullRequestStack as Stack } from '../../src/github/interface'; import { PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; function getReadiness(entry: Stack['pullRequests'][number], currentPosition: number): { icon: JSX.Element; label: string; kind: string } { if (entry.state === GithubItemStateEnum.Merged) { - return { icon: checkIcon, label: 'Already merged', kind: 'ready' }; + return { icon: gitMergeIcon, label: 'Already merged', kind: 'merged' }; } if (entry.state === GithubItemStateEnum.Closed) { return { icon: skipIcon, label: 'Closed pull request cannot be merged', kind: 'blocked' }; } if (entry.isDraft) { - return { icon: gitPullRequestDraftIcon, label: 'Draft pull request cannot be merged', kind: 'waiting' }; + return { icon: gitPullRequestDraftIcon, label: 'Draft pull request cannot be merged', kind: 'draft' }; } switch (entry.mergeable) { case PullRequestMergeability.Mergeable: diff --git a/webviews/editorWebview/index.css b/webviews/editorWebview/index.css index 846322d6d0..90b10eccd0 100644 --- a/webviews/editorWebview/index.css +++ b/webviews/editorWebview/index.css @@ -798,6 +798,36 @@ body button .icon { fill: var(--vscode-issues-open); } +.stack-entry-readiness.merged { + color: var(--vscode-pullRequests-merged); +} + +.stack-entry-readiness.merged svg path { + fill: var(--vscode-pullRequests-merged); +} + +.stack-entry-readiness.draft { + color: var(--vscode-pullRequests-draft); +} + +.stack-entry-readiness.draft svg path { + fill: var(--vscode-pullRequests-draft); +} + +.vscode-high-contrast .stack-entry-readiness.merged, +.vscode-high-contrast-light .stack-entry-readiness.merged, +.vscode-high-contrast .stack-entry-readiness.draft, +.vscode-high-contrast-light .stack-entry-readiness.draft { + color: var(--vscode-foreground); +} + +.vscode-high-contrast .stack-entry-readiness.merged svg path, +.vscode-high-contrast-light .stack-entry-readiness.merged svg path, +.vscode-high-contrast .stack-entry-readiness.draft svg path, +.vscode-high-contrast-light .stack-entry-readiness.draft svg path { + fill: var(--vscode-foreground); +} + .stack-entry-readiness.blocked { color: var(--vscode-pullRequests-closed); } @@ -810,6 +840,10 @@ body button .icon { color: var(--vscode-list-warningForeground); } +.stack-entry-readiness.waiting svg path { + fill: var(--vscode-list-warningForeground); +} + .stack-entry-details { min-width: 0; overflow-wrap: anywhere; diff --git a/webviews/editorWebview/test/overview.test.tsx b/webviews/editorWebview/test/overview.test.tsx index 97cb57102c..b8d4243c8d 100644 --- a/webviews/editorWebview/test/overview.test.tsx +++ b/webviews/editorWebview/test/overview.test.tsx @@ -95,7 +95,7 @@ describe('Overview', function () { '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'], + ['draft', 'Draft pull request cannot be merged'], ['ready', 'Ready to merge'], ['ready', 'Ready to merge'], ]); @@ -165,12 +165,12 @@ describe('Overview', function () { ['Branch is behind its base', 'waiting', dot], ['Merge requirements not met', 'waiting', dot], ['Merge conflicts', 'waiting', dot], - ['Draft pull request cannot be merged', 'waiting', iconPath(require('../../../resources/icons/codicons/git-pull-request-draft.svg'))], + ['Draft pull request cannot be merged', 'draft', iconPath(require('../../../resources/icons/codicons/git-pull-request-draft.svg'))], ['Ready to merge', 'ready', dot], ['Ready to merge', 'ready', pass], ['Ready to merge', 'ready', pass], ['Closed pull request cannot be merged', 'blocked', iconPath(require('../../../resources/icons/codicons/skip.svg'))], - ['Already merged', 'ready', iconPath(require('../../../resources/icons/codicons/check.svg'))], + ['Already merged', 'merged', iconPath(require('../../../resources/icons/codicons/git-merge.svg'))], ]); });