From 76fafe977de3c33ccee7ae0a93556159a7189c91 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Tue, 1 Sep 2026 16:24:44 +0100 Subject: [PATCH 01/14] Enhance agent merge status display and update related tests --- .../chatAgentMergeContentPart.ts | 120 +++++++++++------- .../media/chatAgentMergeContent.css | 86 +++++++------ .../chat/test/browser/chatRequestText.test.ts | 2 +- .../promptTimelineModel.test.ts | 2 +- .../chatAgentMergeContentPart.test.ts | 44 ++++++- 5 files changed, 168 insertions(+), 86 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index bf499d7d29ada1..7153fbc812b140 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -6,6 +6,7 @@ import * as dom from '../../../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; import { Button, IButtonStyles } from '../../../../../../base/browser/ui/button/button.js'; +import { Action } from '../../../../../../base/common/actions.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; @@ -20,6 +21,7 @@ import { CommandsRegistry, ICommandService } from '../../../../../../platform/co import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { ChatPillActionViewItem } from '../../../../../browser/chatPills.js'; import { AgentFeedbackReviewCommandId, IChatAgentFeedbackPullRequestThreadLink } from '../../../common/chatService/chatService.js'; import { IChatRequestViewModel } from '../../../common/model/chatViewModel.js'; import './media/chatAgentMergeContent.css'; @@ -62,38 +64,57 @@ interface IAgentMergeCommentItem extends IAgentMergeFileLocation { readonly threadId?: string; } -const agentMergeTitle = localize('chat.agentMerge.title', "Agent Merge"); +const agentMergeSource = localize('chat.agentMerge.source', "Agent Merge"); -/** The counts shown next to the title, describing why the turn was started. */ -function describeBadges(summary: IAgentMergePromptSummary, commentCount: number): { readonly icon: ThemeIcon; readonly label: string }[] { - const badges: { icon: ThemeIcon; label: string }[] = []; +/** The status shown in the header, describing why the turn was started. */ +function describeAgentMergeStatus(summary: IAgentMergePromptSummary, commentCount: number): { readonly icon: ThemeIcon; readonly title: string } { + const events: string[] = []; if (commentCount > 0) { - badges.push({ - icon: Codicon.commentCompact, - label: commentCount === 1 - ? localize('chat.agentMerge.oneComment', "1 comment") - : localize('chat.agentMerge.comments', "{0} comments", commentCount), - }); + events.push(commentCount === 1 + ? localize('chat.agentMerge.oneReviewComment', "1 Review Comment") + : localize('chat.agentMerge.reviewComments', "{0} Review Comments", commentCount)); } if (summary.failedChecks.length > 0) { - badges.push({ - icon: Codicon.errorCompact, - label: summary.failedChecks.length === 1 - ? localize('chat.agentMerge.oneCheck', "1 check failing") - : localize('chat.agentMerge.checks', "{0} checks failing", summary.failedChecks.length), - }); + events.push(summary.failedChecks.length === 1 + ? localize('chat.agentMerge.oneFailingCheck', "1 Failing Check") + : localize('chat.agentMerge.failingChecks', "{0} Failing Checks", summary.failedChecks.length)); + } + if (summary.conflicting) { + events.push(localize('chat.agentMerge.mergeConflicts', "Merge Conflicts")); } - if (badges.length === 0) { - badges.push({ - icon: summary.conflicting ? Codicon.warningCompact : Codicon.arrowDown, - label: summary.conflicting - ? localize('chat.agentMerge.conflicting', "Merge conflicts") + if (summary.behind) { + events.push(localize('chat.agentMerge.behindBaseBranch', "Behind Base Branch")); + } + if (events.length === 0) { + events.push(localize('chat.agentMerge.noPendingFeedback', "No Pending Feedback")); + } + + const icon = summary.failedChecks.length > 0 + ? Codicon.errorCompact + : summary.conflicting + ? Codicon.warningCompact + : commentCount > 0 + ? Codicon.commentCompact : summary.behind - ? localize('chat.agentMerge.behind', "Behind base branch") - : localize('chat.agentMerge.upToDate', "No pending feedback"), - }); + ? Codicon.arrowDown + : Codicon.checkCompact; + return { + icon, + title: formatAgentMergeEvents(events), + }; +} + +function formatAgentMergeEvents(events: readonly string[]): string { + switch (events.length) { + case 1: + return events[0]; + case 2: + return localize('chat.agentMerge.twoEvents', "{0} and {1}", events[0], events[1]); + case 3: + return localize('chat.agentMerge.threeEvents', "{0}, {1}, and {2}", events[0], events[1], events[2]); + default: + return localize('chat.agentMerge.fourEvents', "{0}, {1}, {2}, and {3}", events[0], events[1], events[2], events[3]); } - return badges; } /** @@ -102,8 +123,8 @@ function describeBadges(summary: IAgentMergePromptSummary, commentCount: number) * transcript find use this in its place. */ export function getAgentMergeSummaryLabel(summary: IAgentMergePromptSummary): string { - const badges = describeBadges(summary, collectComments(summary).length); - return [agentMergeTitle, ...badges.map(badge => badge.label)].join(', '); + const status = describeAgentMergeStatus(summary, collectComments(summary).length); + return localize('chat.agentMerge.summaryLabel', "{0}, {1}", status.title, agentMergeSource); } /** @@ -163,19 +184,14 @@ export class ChatAgentMergeContentPart extends Disposable { const button = this._register(new Button(parent, { ...transparentButtonStyles, title: false })); button.element.classList.add('chat-agent-merge-header'); + const status = describeAgentMergeStatus(this._summary, this._comments.length); + const icon = dom.append(button.element, dom.$('span.chat-agent-merge-status-icon')); + icon.classList.add(...ThemeIcon.asClassNameArray(status.icon)); + const title = dom.append(button.element, dom.$('span.chat-agent-merge-title', undefined, status.title)); + this._register(this._hoverService.setupDelayedHover(title, { content: status.title })); + dom.append(button.element, dom.$('span.chat-agent-merge-source', undefined, localize('chat.agentMerge.visualSource', "• {0}", agentMergeSource))); const twistie = dom.append(button.element, dom.$('span.chat-agent-merge-twistie')); twistie.classList.add(...ThemeIcon.asClassNameArray(Codicon.chevronRightCompact)); - const icon = dom.append(button.element, dom.$('span.chat-agent-merge-icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.gitMerge)); - dom.append(button.element, dom.$('span.chat-agent-merge-title', undefined, agentMergeTitle)); - - const badges = dom.append(button.element, dom.$('span.chat-agent-merge-badges')); - for (const badge of describeBadges(this._summary, this._comments.length)) { - const badgeElement = dom.append(badges, dom.$('span.chat-agent-merge-badge')); - const badgeIcon = dom.append(badgeElement, dom.$('span')); - badgeIcon.classList.add(...ThemeIcon.asClassNameArray(badge.icon)); - dom.append(badgeElement, dom.$('span', undefined, badge.label)); - } this._register(toggleDisclosure(button, this.domNode, getAgentMergeSummaryLabel(this._summary))); } @@ -186,12 +202,24 @@ export class ChatAgentMergeContentPart extends Disposable { } const row = dom.append(body, dom.$('.chat-agent-merge-pr')); - const icon = dom.append(row, dom.$('span.chat-agent-merge-row-icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.gitPullRequest)); - - const link: HTMLAnchorElement = dom.append(row, dom.$('a.chat-agent-merge-link.chat-agent-merge-pr-title', undefined, this._summary.title || this._summary.pullRequestUrl)); - link.href = this._summary.pullRequestUrl; - this._registerLink(link, this._summary.pullRequestUrl, () => this._openerService.open(URI.parse(this._summary.pullRequestUrl))); + const title = this._summary.title || this._summary.pullRequestUrl; + const pullRequestNumber = /\/pull\/(?\d+)\/?$/.exec(URI.parse(this._summary.pullRequestUrl).path)?.groups?.number; + const label = pullRequestNumber + ? localize('chat.agentMerge.pullRequestPillLabel', "#{0} {1}", pullRequestNumber, title) + : title; + const tooltip = pullRequestNumber + ? localize('chat.agentMerge.openPullRequestWithTitle', "Open Pull Request #{0}: {1}", pullRequestNumber, title) + : localize('chat.agentMerge.openPullRequestWithTitleFallback', "Open Pull Request: {0}", title); + const action = this._register(new Action( + 'chat.agentMerge.openPullRequest', + label, + ThemeIcon.asClassName(Codicon.gitPullRequest), + true, + () => this._openerService.open(URI.parse(this._summary.pullRequestUrl)), + )); + action.tooltip = tooltip; + const viewItem = this._register(new ChatPillActionViewItem(undefined, action, {})); + viewItem.render(row); } private _createCommentsSection(body: HTMLElement): void { @@ -200,7 +228,7 @@ export class ChatAgentMergeContentPart extends Disposable { } const section = dom.append(body, dom.$('.chat-agent-merge-section')); - dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.commentsTitle', "Review Feedback"))); + dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.commentsTitle', "Feedback"))); const fileElements = new Map(); this._comments.forEach((comment, index) => { @@ -269,7 +297,7 @@ export class ChatAgentMergeContentPart extends Disposable { } const section = dom.append(body, dom.$('.chat-agent-merge-section')); - dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.checksTitle', "Failing Checks"))); + dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.checksTitle', "Checks"))); // A check's own run URL is not part of the prompt, so the pull request's // checks tab is the closest target that always resolves. const checksUrl = this._summary.pullRequestUrl ? `${this._summary.pullRequestUrl}/checks` : undefined; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index dcdc111a8c5fb0..829adcabad49dd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -6,6 +6,7 @@ .chat-agent-merge { display: flex; flex-direction: column; + align-self: flex-start; margin: var(--vscode-spacing-size40) 0; border: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); border-radius: var(--vscode-cornerRadius-medium); @@ -16,6 +17,15 @@ max-width: 100%; } +.chat-agent-merge.collapsed { + border-color: transparent; +} + +.chat-agent-merge.collapsed:hover, +.chat-agent-merge.collapsed:focus-within { + border-color: var(--vscode-chat-requestBorder); +} + .chat-agent-merge > .chat-agent-merge-header, .chat-agent-merge .chat-agent-merge-message-header { display: flex; @@ -29,6 +39,11 @@ .chat-agent-merge > .chat-agent-merge-header { padding: var(--vscode-spacing-size60) var(--vscode-spacing-size80); + border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) 0 0; +} + +.chat-agent-merge.collapsed > .chat-agent-merge-header { + border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)); } .chat-agent-merge > .chat-agent-merge-header:hover, @@ -42,7 +57,17 @@ the codicon base rule, which would otherwise force every glyph to 16px. */ flex: 0 0 auto; font-size: var(--vscode-codiconFontSize-compact); color: var(--vscode-descriptionForeground); - transition: transform 100ms ease-out; + transition: opacity 100ms ease-out, transform 100ms ease-out; +} + +.chat-agent-merge > .chat-agent-merge-header > .chat-agent-merge-twistie { + margin-inline-start: auto; + opacity: 0; +} + +.chat-agent-merge > .chat-agent-merge-header:hover > .chat-agent-merge-twistie, +.chat-agent-merge > .chat-agent-merge-header:focus-visible > .chat-agent-merge-twistie { + opacity: 1; } /* Reduced motion follows the workbench-managed `.monaco-reduce-motion` class, @@ -57,53 +82,31 @@ which sits either on an ancestor or on the workbench element itself. */ transform: rotate(90deg); } -.chat-agent-merge .chat-agent-merge-icon.codicon { +.chat-agent-merge .chat-agent-merge-status-icon.codicon { flex: 0 0 auto; font-size: var(--vscode-codiconFontSize-compact); color: var(--vscode-descriptionForeground); } .chat-agent-merge .chat-agent-merge-title { - font-weight: var(--vscode-fontWeight-semiBold); - white-space: nowrap; -} - -.chat-agent-merge .chat-agent-merge-badges { - display: flex; - align-items: center; - gap: var(--vscode-spacing-size80); min-width: 0; overflow: hidden; - color: var(--vscode-descriptionForeground); -} - -.chat-agent-merge .chat-agent-merge-badge { - display: flex; - align-items: center; - gap: var(--vscode-spacing-size40); + text-overflow: ellipsis; + font-weight: var(--vscode-fontWeight-semiBold); white-space: nowrap; } -.chat-agent-merge .chat-agent-merge-badge::before { - content: ''; +.chat-agent-merge .chat-agent-merge-source { flex: 0 0 auto; - width: 2px; - height: 2px; - border-radius: var(--vscode-cornerRadius-circle); - background-color: currentColor; - opacity: 0.6; - margin-right: var(--vscode-spacing-size40); -} - -.chat-agent-merge .chat-agent-merge-badge .codicon { - font-size: var(--vscode-codiconFontSize-compact); - color: inherit; + color: var(--vscode-descriptionForeground); + font-weight: var(--vscode-fontWeight-regular); + white-space: nowrap; } .chat-agent-merge > .chat-agent-merge-body { display: flex; flex-direction: column; - gap: var(--vscode-spacing-size120); + gap: var(--vscode-spacing-size80); padding: var(--vscode-spacing-size40) var(--vscode-spacing-size120) var(--vscode-spacing-size120); border-top: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); } @@ -124,8 +127,7 @@ which sits either on an ancestor or on the workbench element itself. */ color: var(--vscode-errorForeground); } -/* Clickable text stays in its surrounding color and announces itself on hover, -matching the pull request content part's link treatment. */ +/* Clickable text stays in its surrounding color and announces itself on hover. */ .chat-agent-merge .chat-agent-merge-link { cursor: pointer; color: inherit; @@ -139,17 +141,23 @@ matching the pull request content part's link treatment. */ .chat-agent-merge .chat-agent-merge-pr { display: flex; align-items: center; - gap: var(--vscode-spacing-size60); - padding-top: var(--vscode-spacing-size80); + padding-top: var(--vscode-spacing-size40); + margin-inline: calc(var(--vscode-spacing-size80) - var(--vscode-spacing-size120)); min-width: 0; } -.chat-agent-merge .chat-agent-merge-pr-title { +.chat-agent-merge .chat-agent-merge-pr .chat-pill-button, +.chat-agent-merge .chat-agent-merge-pr .chat-pill-label { + min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.chat-agent-merge .chat-agent-merge-pr .chat-pill-button { + max-width: 100%; +} + .chat-agent-merge .chat-agent-merge-section { display: flex; flex-direction: column; @@ -157,8 +165,12 @@ matching the pull request content part's link treatment. */ min-width: 0; } +.chat-agent-merge .chat-agent-merge-section + .chat-agent-merge-section { + padding-top: var(--vscode-spacing-size80); +} + .chat-agent-merge .chat-agent-merge-section-title { - font-size: var(--vscode-fontSize-label2); + font-size: var(--vscode-fontSize-label1); font-weight: var(--vscode-fontWeight-semiBold); color: var(--vscode-descriptionForeground); } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatRequestText.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatRequestText.test.ts index 15e67cba1fccd9..be8f338764df05 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatRequestText.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatRequestText.test.ts @@ -40,7 +40,7 @@ suite('getChatRequestText', () => { getChatRequestText(request(agentMergePrompt, 'Terminal needs input')), ], [ 'Rename the widget', - 'Agent Merge, 1 comment', + '1 Review Comment, Agent Merge', agentMergePrompt, ]); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptTimeline/promptTimelineModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptTimeline/promptTimelineModel.test.ts index fb3e10a802e2ac..0b8ddd42ac9079 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptTimeline/promptTimelineModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptTimeline/promptTimelineModel.test.ts @@ -147,7 +147,7 @@ suite('PromptTimelineModel', () => { assert.deepStrictEqual(model.promptTicks.get().map(tick => tick.text), [ 'First prompt', - 'Agent Merge, 1 comment, 1 check failing', + '1 Review Comment and 1 Failing Check, Agent Merge', ]); }); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index fe0f8dd0b1433a..03957920fc5980 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -5,7 +5,27 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; -import { describeAgentMergeFileLabels } from '../../../../browser/widget/chatContentParts/chatAgentMergeContentPart.js'; +import { IAgentMergePromptSummary } from '../../../../../../../platform/agentHost/common/agentMergePrompt.js'; +import { describeAgentMergeFileLabels, getAgentMergeSummaryLabel } from '../../../../browser/widget/chatContentParts/chatAgentMergeContentPart.js'; + +function summary(overrides: Partial = {}): IAgentMergePromptSummary { + return { + actions: [], + pullRequestUrl: '', + title: '', + headRef: '', + headSha: '', + baseRef: 'main', + reviewThreads: [], + reviewSummaries: [], + newComments: [], + failedChecks: [], + behind: false, + conflicting: false, + agentMessage: '', + ...overrides, + }; +} suite('ChatAgentMergeContentPart file labels', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -49,4 +69,26 @@ suite('ChatAgentMergeContentPart file labels', () => { { name: 'index.ts:9', title: 'src/a/index.ts' }, ]); }); + + test('summarizes encountered events in one status sentence', () => { + assert.deepStrictEqual([ + getAgentMergeSummaryLabel(summary()), + getAgentMergeSummaryLabel(summary({ behind: true })), + getAgentMergeSummaryLabel(summary({ conflicting: true })), + getAgentMergeSummaryLabel(summary({ + reviewSummaries: [ + { author: 'octocat', body: 'Please fix this.' }, + { author: 'hubot', body: 'Please add a test.' }, + ], + failedChecks: ['Compile', 'Unit Tests'], + behind: true, + conflicting: true, + })), + ], [ + 'No Pending Feedback, Agent Merge', + 'Behind Base Branch, Agent Merge', + 'Merge Conflicts, Agent Merge', + '2 Review Comments, 2 Failing Checks, Merge Conflicts, and Behind Base Branch, Agent Merge', + ]); + }); }); From 5ef1586927c9c7808641f7783dfee63036372dca Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 11:08:45 +0100 Subject: [PATCH 02/14] Refactor agent merge widget for improved message display and interaction --- .../chatAgentMergeContentPart.ts | 117 ++++++------ .../media/chatAgentMergeContent.css | 174 ++++++++++++++---- .../chat/chatAgentMerge.fixture.ts | 14 +- 3 files changed, 206 insertions(+), 99 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index 7153fbc812b140..5eb16da46c839c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -11,7 +11,7 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../../../base/common/keyCodes.js'; import { shorten } from '../../../../../../base/common/labels.js'; -import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; import { posix } from '../../../../../../base/common/path.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { URI } from '../../../../../../base/common/uri.js'; @@ -140,16 +140,7 @@ export function getAgentMergeRequestLabel(element: IChatRequestViewModel): strin return summary && getAgentMergeSummaryLabel(summary); } -/** - * Renders the machine-facing Agent Merge prompt as a compact disclosure: a - * header naming the pull request work in progress with counts of the review - * feedback and failing checks that triggered it, expanding to the feedback - * itself and a nested disclosure holding the verbatim instructions sent to the - * agent. - * - * Review threads the session mirrored into agent feedback link to their local - * comment, so a click on the file label reveals that comment in the editor. - */ +/** Renders the Agent Merge prompt as a compact disclosure whose header action switches between merge details and the agent message. Mirrored review file labels link to their local comments. */ export class ChatAgentMergeContentPart extends Disposable { readonly domNode: HTMLElement; @@ -174,26 +165,72 @@ export class ChatAgentMergeContentPart extends Disposable { this._createHeader(this.domNode); const body = dom.append(this.domNode, dom.$('.chat-agent-merge-body')); - this._createPullRequestRow(body); - this._createCommentsSection(body); - this._createChecksSection(body); - this._createAgentMessageSection(body); + const details = dom.append(body, dom.$('.chat-agent-merge-details')); + this._createPullRequestRow(details); + this._createCommentsSection(details); + this._createChecksSection(details); + this._createAgentMessage(body); } private _createHeader(parent: HTMLElement): void { - const button = this._register(new Button(parent, { ...transparentButtonStyles, title: false })); - button.element.classList.add('chat-agent-merge-header'); + const header = dom.append(parent, dom.$('.chat-agent-merge-header')); + const preventPointerFocus = (button: Button) => { + this._register(dom.addDisposableGenericMouseDownListener(button.element, event => { + event.preventDefault(); + button.element.blur(); + })); + }; + const disclosureButton = this._register(new Button(header, { ...transparentButtonStyles, title: false })); + disclosureButton.element.classList.add('chat-agent-merge-header-disclosure'); const status = describeAgentMergeStatus(this._summary, this._comments.length); - const icon = dom.append(button.element, dom.$('span.chat-agent-merge-status-icon')); + const content = dom.append(header, dom.$('.chat-agent-merge-header-content', { 'aria-hidden': 'true' })); + const icon = dom.append(content, dom.$('span.chat-agent-merge-status-icon')); icon.classList.add(...ThemeIcon.asClassNameArray(status.icon)); - const title = dom.append(button.element, dom.$('span.chat-agent-merge-title', undefined, status.title)); + const title = dom.append(content, dom.$('span.chat-agent-merge-title', undefined, status.title)); this._register(this._hoverService.setupDelayedHover(title, { content: status.title })); - dom.append(button.element, dom.$('span.chat-agent-merge-source', undefined, localize('chat.agentMerge.visualSource', "• {0}", agentMergeSource))); - const twistie = dom.append(button.element, dom.$('span.chat-agent-merge-twistie')); - twistie.classList.add(...ThemeIcon.asClassNameArray(Codicon.chevronRightCompact)); + dom.append(content, dom.$('span.chat-agent-merge-source', undefined, agentMergeSource)); + + let messageButton: Button | undefined; + const setExpanded = (expanded: boolean) => { + this.domNode.classList.toggle('collapsed', !expanded); + disclosureButton.element.ariaExpanded = String(expanded); + disclosureButton.element.ariaLabel = getAgentMergeSummaryLabel(this._summary); + if (messageButton) { + messageButton.element.tabIndex = expanded ? 0 : -1; + } + }; + setExpanded(false); + preventPointerFocus(disclosureButton); + this._register(disclosureButton.onDidClick(() => setExpanded(this.domNode.classList.contains('collapsed')))); + + if (this._summary.agentMessage) { + const showMessageLabel = localize('chat.agentMerge.showAgentMessage', "Show Agent Message"); + const showDetailsLabel = localize('chat.agentMerge.showMergeDetails', "Show Merge Details"); + messageButton = this._register(new Button(header, { ...transparentButtonStyles, title: false })); + messageButton.element.classList.add('chat-agent-merge-message-toggle'); + messageButton.icon = Codicon.commentDiscussion; + messageButton.element.tabIndex = -1; + preventPointerFocus(messageButton); + let showingAgentMessage = false; + const updateMessageVisibility = (visible: boolean) => { + showingAgentMessage = visible; + this.domNode.classList.toggle('showing-agent-message', visible); + messageButton.checked = visible; + messageButton.setAriaLabel(visible ? showDetailsLabel : showMessageLabel); + }; + updateMessageVisibility(false); + this._register(this._hoverService.setupDelayedHover(messageButton.element, () => ({ + content: showingAgentMessage ? showDetailsLabel : showMessageLabel, + }))); + this._register(messageButton.onDidClick(() => { + setExpanded(true); + updateMessageVisibility(!showingAgentMessage); + })); + } - this._register(toggleDisclosure(button, this.domNode, getAgentMergeSummaryLabel(this._summary))); + const twistie = dom.append(header, dom.$('span.chat-agent-merge-twistie', { 'aria-hidden': 'true' })); + twistie.classList.add(...ThemeIcon.asClassNameArray(Codicon.chevronRightCompact)); } private _createPullRequestRow(body: HTMLElement): void { @@ -228,7 +265,6 @@ export class ChatAgentMergeContentPart extends Disposable { } const section = dom.append(body, dom.$('.chat-agent-merge-section')); - dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.commentsTitle', "Feedback"))); const fileElements = new Map(); this._comments.forEach((comment, index) => { @@ -297,7 +333,9 @@ export class ChatAgentMergeContentPart extends Disposable { } const section = dom.append(body, dom.$('.chat-agent-merge-section')); - dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.checksTitle', "Checks"))); + if (this._comments.length > 0) { + dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.checksTitle', "Checks"))); + } // A check's own run URL is not part of the prompt, so the pull request's // checks tab is the closest target that always resolves. const checksUrl = this._summary.pullRequestUrl ? `${this._summary.pullRequestUrl}/checks` : undefined; @@ -315,23 +353,13 @@ export class ChatAgentMergeContentPart extends Disposable { } } - private _createAgentMessageSection(body: HTMLElement): void { + private _createAgentMessage(body: HTMLElement): void { if (!this._summary.agentMessage) { return; } - const label = localize('chat.agentMerge.agentMessage', "Agent message"); const section = dom.append(body, dom.$('.chat-agent-merge-message')); - const messageBody = dom.$('.chat-agent-merge-message-body', undefined, this._summary.agentMessage); - - const button = this._register(new Button(section, { ...transparentButtonStyles, title: false })); - button.element.classList.add('chat-agent-merge-message-header'); - const twistie = dom.append(button.element, dom.$('span.chat-agent-merge-twistie')); - twistie.classList.add(...ThemeIcon.asClassNameArray(Codicon.chevronRightCompact)); - dom.append(button.element, dom.$('span', undefined, label)); - - section.appendChild(messageBody); - this._register(toggleDisclosure(button, section, label)); + dom.append(section, dom.$('.chat-agent-merge-message-body', undefined, this._summary.agentMessage)); } /** Makes an element activate `run` by mouse or keyboard, with a hover describing it. */ @@ -352,21 +380,6 @@ export class ChatAgentMergeContentPart extends Disposable { } } -/** - * Wires a button to expand and collapse the region its owner holds, keeping the - * collapsed state on the owner so CSS can drive both the twistie and the - * region's visibility from one class. - */ -function toggleDisclosure(button: Button, owner: HTMLElement, ariaLabel: string): IDisposable { - const apply = (expanded: boolean) => { - owner.classList.toggle('collapsed', !expanded); - button.element.ariaExpanded = String(expanded); - button.element.ariaLabel = ariaLabel; - }; - apply(false); - return button.onDidClick(() => apply(owner.classList.contains('collapsed'))); -} - function collectComments(summary: IAgentMergePromptSummary): readonly IAgentMergeCommentItem[] { const comments: IAgentMergeCommentItem[] = []; for (const thread of summary.reviewThreads) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index 829adcabad49dd..204c80973604d9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -27,58 +27,134 @@ } .chat-agent-merge > .chat-agent-merge-header, -.chat-agent-merge .chat-agent-merge-message-header { +.chat-agent-merge .chat-agent-merge-header-content { display: flex; align-items: center; - gap: var(--vscode-spacing-size60); +} + +.chat-agent-merge > .chat-agent-merge-header { + position: relative; + gap: var(--vscode-spacing-size40); + padding: var(--vscode-spacing-size60) var(--vscode-spacing-size80); + border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) 0 0; + overflow: hidden; +} + +.chat-agent-merge .chat-agent-merge-header-disclosure { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + padding: 0; + border-radius: inherit; color: inherit; text-decoration: none; cursor: pointer; user-select: none; } -.chat-agent-merge > .chat-agent-merge-header { - padding: var(--vscode-spacing-size60) var(--vscode-spacing-size80); - border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) 0 0; +.chat-agent-merge .chat-agent-merge-header-content { + position: relative; + z-index: 1; + flex: 0 1 auto; + gap: var(--vscode-spacing-size60); + min-width: 0; + min-height: var(--vscode-spacing-size200); + pointer-events: none; } .chat-agent-merge.collapsed > .chat-agent-merge-header { border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)); } -.chat-agent-merge > .chat-agent-merge-header:hover, -.chat-agent-merge .chat-agent-merge-message-header:hover { +.chat-agent-merge > .chat-agent-merge-header:hover { background-color: var(--vscode-toolbar-hoverBackground); } +.chat-agent-merge .chat-agent-merge-header-disclosure:focus-visible, +.chat-agent-merge .chat-agent-merge-message-toggle:focus-visible { + outline-offset: -1px !important; +} + +.chat-agent-merge .chat-agent-merge-header-disclosure:focus:not(:focus-visible) { + outline: 0 !important; +} + +.chat-agent-merge .chat-agent-merge-message-toggle { + position: relative; + z-index: 1; + box-sizing: border-box; + display: none; + align-items: center; + justify-content: center; + flex: 0 0 auto; + margin-inline-start: auto; + width: var(--vscode-spacing-size200); + height: var(--vscode-spacing-size200); + padding: var(--vscode-spacing-size40); + border-radius: var(--vscode-cornerRadius-small); + color: var(--vscode-descriptionForeground); + opacity: 0; + pointer-events: none; + transition: opacity 100ms ease-out; +} + +.chat-agent-merge:not(.collapsed) .chat-agent-merge-message-toggle { + display: flex; +} + +.chat-agent-merge .chat-agent-merge-message-toggle.codicon { + font-size: var(--vscode-codiconFontSize-compact); +} + +.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header:hover .chat-agent-merge-message-toggle, +.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header:focus-within .chat-agent-merge-message-toggle { + opacity: 1; + pointer-events: auto; +} + +.chat-agent-merge .chat-agent-merge-message-toggle:hover, +.chat-agent-merge .chat-agent-merge-message-toggle:focus-visible, +.chat-agent-merge .chat-agent-merge-message-toggle.checked { + color: var(--vscode-foreground); + background-color: var(--vscode-toolbar-activeBackground); +} + /* `.codicon` joins each compound so these sizes outrank the `font` shorthand on the codicon base rule, which would otherwise force every glyph to 16px. */ .chat-agent-merge .chat-agent-merge-twistie.codicon { + position: relative; + z-index: 1; flex: 0 0 auto; font-size: var(--vscode-codiconFontSize-compact); color: var(--vscode-descriptionForeground); + pointer-events: none; transition: opacity 100ms ease-out, transform 100ms ease-out; } -.chat-agent-merge > .chat-agent-merge-header > .chat-agent-merge-twistie { +.chat-agent-merge .chat-agent-merge-header-content + .chat-agent-merge-twistie { margin-inline-start: auto; +} + +.chat-agent-merge .chat-agent-merge-twistie { opacity: 0; } .chat-agent-merge > .chat-agent-merge-header:hover > .chat-agent-merge-twistie, -.chat-agent-merge > .chat-agent-merge-header:focus-visible > .chat-agent-merge-twistie { +.chat-agent-merge > .chat-agent-merge-header:focus-within > .chat-agent-merge-twistie { opacity: 1; } /* Reduced motion follows the workbench-managed `.monaco-reduce-motion` class, which sits either on an ancestor or on the workbench element itself. */ .monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon, -.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon { +.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon, +.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle, +.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle { transition: none; } -.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header > .chat-agent-merge-twistie, -.chat-agent-merge-message:not(.collapsed) > .chat-agent-merge-message-header > .chat-agent-merge-twistie { +.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header > .chat-agent-merge-twistie { transform: rotate(90deg); } @@ -97,12 +173,19 @@ which sits either on an ancestor or on the workbench element itself. */ } .chat-agent-merge .chat-agent-merge-source { + display: flex; + align-items: center; flex: 0 0 auto; + gap: var(--vscode-spacing-size60); color: var(--vscode-descriptionForeground); font-weight: var(--vscode-fontWeight-regular); white-space: nowrap; } +.chat-agent-merge .chat-agent-merge-source::before { + content: '\2022'; +} + .chat-agent-merge > .chat-agent-merge-body { display: flex; flex-direction: column; @@ -115,6 +198,17 @@ which sits either on an ancestor or on the workbench element itself. */ display: none; } +.chat-agent-merge .chat-agent-merge-details { + display: flex; + flex-direction: column; + gap: var(--vscode-spacing-size80); + min-width: 0; +} + +.chat-agent-merge.showing-agent-message .chat-agent-merge-details { + display: none; +} + /* Shared leading icon: a fixed width keeps comment bodies and check names on one indent. */ .chat-agent-merge .chat-agent-merge-row-icon.codicon { flex: 0 0 auto; @@ -127,22 +221,11 @@ which sits either on an ancestor or on the workbench element itself. */ color: var(--vscode-errorForeground); } -/* Clickable text stays in its surrounding color and announces itself on hover. */ -.chat-agent-merge .chat-agent-merge-link { - cursor: pointer; - color: inherit; - text-decoration: none; -} - -.chat-agent-merge .chat-agent-merge-link:hover { - text-decoration: underline; -} - .chat-agent-merge .chat-agent-merge-pr { display: flex; align-items: center; padding-top: var(--vscode-spacing-size40); - margin-inline: calc(var(--vscode-spacing-size80) - var(--vscode-spacing-size120)); + margin-inline: calc(var(--vscode-spacing-size60) - var(--vscode-spacing-size120)); min-width: 0; } @@ -180,6 +263,7 @@ which sits either on an ancestor or on the workbench element itself. */ display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: var(--vscode-spacing-size60); + row-gap: var(--vscode-spacing-size20); align-items: start; } @@ -189,6 +273,10 @@ which sits either on an ancestor or on the workbench element itself. */ line-height: var(--vscode-spacing-size200); } +.chat-agent-merge .chat-agent-merge-comment-icon.codicon { + transform: translateY(1px); +} + .chat-agent-merge .chat-agent-merge-comment-header { display: flex; align-items: baseline; @@ -223,6 +311,24 @@ which sits either on an ancestor or on the workbench element itself. */ opacity: 0.8; } +.chat-agent-merge .chat-agent-merge-link, +.chat-agent-merge .chat-agent-merge-comment-body a, +.chat-agent-merge .chat-agent-merge-comment-body a code { + cursor: pointer; + color: var(--vscode-textLink-foreground); + text-decoration: none; +} + +.chat-agent-merge .chat-agent-merge-link:hover, +.chat-agent-merge .chat-agent-merge-link:active, +.chat-agent-merge .chat-agent-merge-comment-body a:hover, +.chat-agent-merge .chat-agent-merge-comment-body a:active, +.chat-agent-merge .chat-agent-merge-comment-body a:hover code, +.chat-agent-merge .chat-agent-merge-comment-body a:active code { + color: var(--vscode-textLink-activeForeground); + text-decoration: underline; +} + .chat-agent-merge .chat-agent-merge-comment-body { grid-column: 2; overflow-wrap: anywhere; @@ -251,29 +357,17 @@ which sits either on an ancestor or on the workbench element itself. */ } .chat-agent-merge .chat-agent-merge-message { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: var(--vscode-spacing-size60); + display: none; min-width: 0; } -.chat-agent-merge .chat-agent-merge-message-header { - font-size: var(--vscode-fontSize-label2); - color: var(--vscode-descriptionForeground); - padding: var(--vscode-spacing-size20) var(--vscode-spacing-size40); - margin-left: calc(-1 * var(--vscode-spacing-size40)); - border-radius: var(--vscode-cornerRadius-small); +.chat-agent-merge.showing-agent-message .chat-agent-merge-message { + display: block; + margin-top: var(--vscode-spacing-size80); } .chat-agent-merge .chat-agent-merge-message-body { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--vscode-descriptionForeground); - padding-left: var(--vscode-spacing-size80); - border-left: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); -} - -.chat-agent-merge-message.collapsed > .chat-agent-merge-message-body { - display: none; } diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts index 28fa7ebc98c9a6..c7d554f8b16c76 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts @@ -148,8 +148,8 @@ interface IRenderAgentMergeOptions { readonly summary: IAgentMergePromptSummary; /** Expands the widget by clicking its header, the way a user would. */ readonly expanded?: boolean; - /** Also expands the nested agent-message disclosure. */ - readonly agentMessageExpanded?: boolean; + /** Shows the agent message instead of the merge details. */ + readonly agentMessageVisible?: boolean; /** Review thread ids the session mirrored into agent feedback. */ readonly mirroredThreadIds?: readonly string[]; } @@ -196,11 +196,11 @@ function renderAgentMerge({ container, disposableStore, theme }: ComponentFixtur const part = disposableStore.add(instantiationService.createInstance(ChatAgentMergeContentPart, options.summary, sessionResource, markdownRenderer)); container.appendChild(part.domNode); - if (options.expanded || options.agentMessageExpanded) { - part.domNode.querySelector('.chat-agent-merge-header')?.click(); + if (options.expanded) { + part.domNode.querySelector('.chat-agent-merge-header-disclosure')?.click(); } - if (options.agentMessageExpanded) { - part.domNode.querySelector('.chat-agent-merge-message-header')?.click(); + if (options.agentMessageVisible) { + part.domNode.querySelector('.chat-agent-merge-message-toggle')?.click(); } } @@ -274,7 +274,7 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { labels: { kind: 'screenshot' }, render: (ctx) => renderAgentMerge(ctx, { summary: createSummary(['addressReviews', 'fixCI'], { reviewThreads: reviewThreads.slice(0, 1), failedChecks }), - agentMessageExpanded: true, + agentMessageVisible: true, }), }), From 8bc78e84b12a4594182ac2ebb1bdf6884101fe3c Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 11:22:15 +0100 Subject: [PATCH 03/14] Improve accessibility for agent message toggle and enhance related tests --- .../chatAgentMergeContentPart.ts | 3 +- .../media/chatAgentMergeContent.css | 1 + .../chatAgentMergeContentPart.test.ts | 40 ++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index 5eb16da46c839c..b8e818b879152a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -205,11 +205,13 @@ export class ChatAgentMergeContentPart extends Disposable { this._register(disclosureButton.onDidClick(() => setExpanded(this.domNode.classList.contains('collapsed')))); if (this._summary.agentMessage) { + const agentMessageLabel = localize('chat.agentMerge.agentMessage', "Agent Message"); const showMessageLabel = localize('chat.agentMerge.showAgentMessage', "Show Agent Message"); const showDetailsLabel = localize('chat.agentMerge.showMergeDetails', "Show Merge Details"); messageButton = this._register(new Button(header, { ...transparentButtonStyles, title: false })); messageButton.element.classList.add('chat-agent-merge-message-toggle'); messageButton.icon = Codicon.commentDiscussion; + messageButton.setAriaLabel(agentMessageLabel); messageButton.element.tabIndex = -1; preventPointerFocus(messageButton); let showingAgentMessage = false; @@ -217,7 +219,6 @@ export class ChatAgentMergeContentPart extends Disposable { showingAgentMessage = visible; this.domNode.classList.toggle('showing-agent-message', visible); messageButton.checked = visible; - messageButton.setAriaLabel(visible ? showDetailsLabel : showMessageLabel); }; updateMessageVisibility(false); this._register(this._hoverService.setupDelayedHover(messageButton.element, () => ({ diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index 204c80973604d9..9d66a3f629a051 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -96,6 +96,7 @@ color: var(--vscode-descriptionForeground); opacity: 0; pointer-events: none; + cursor: pointer; transition: opacity 100ms ease-out; } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index 03957920fc5980..ca4ca045d060b8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -4,9 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { upcastPartial } from '../../../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { IAgentMergePromptSummary } from '../../../../../../../platform/agentHost/common/agentMergePrompt.js'; -import { describeAgentMergeFileLabels, getAgentMergeSummaryLabel } from '../../../../browser/widget/chatContentParts/chatAgentMergeContentPart.js'; +import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; +import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IOpenerService } from '../../../../../../../platform/opener/common/opener.js'; +import { ChatAgentMergeContentPart, describeAgentMergeFileLabels, getAgentMergeSummaryLabel } from '../../../../browser/widget/chatContentParts/chatAgentMergeContentPart.js'; function summary(overrides: Partial = {}): IAgentMergePromptSummary { return { @@ -28,7 +35,7 @@ function summary(overrides: Partial = {}): IAgentMerge } suite('ChatAgentMergeContentPart file labels', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); test('names a unique file without a disambiguating path', () => { const labels = describeAgentMergeFileLabels([ @@ -91,4 +98,33 @@ suite('ChatAgentMergeContentPart file labels', () => { '2 Review Comments, 2 Failing Checks, Merge Conflicts, and Behind Base Branch, Agent Merge', ]); }); + + test('keeps the Agent Message toggle name stable while reporting its state', () => { + const part = store.add(new ChatAgentMergeContentPart( + summary({ agentMessage: 'Merge agent details.' }), + URI.parse('test://session'), + upcastPartial({}), + upcastPartial({}), + upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), + upcastPartial({}), + )); + const button = part.domNode.querySelector('.chat-agent-merge-message-toggle'); + assert.ok(button); + + const getAccessibleState = () => ({ + label: button.getAttribute('aria-label'), + pressed: button.getAttribute('aria-pressed'), + }); + const initial = getAccessibleState(); + button.click(); + const showingMessage = getAccessibleState(); + button.click(); + const showingDetails = getAccessibleState(); + + assert.deepStrictEqual([initial, showingMessage, showingDetails], [ + { label: 'Agent Message', pressed: 'false' }, + { label: 'Agent Message', pressed: 'true' }, + { label: 'Agent Message', pressed: 'false' }, + ]); + }); }); From 33a24de3c873d6bb0bf3e42f2da3dd93afd841e1 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 11:39:57 +0100 Subject: [PATCH 04/14] Update agent merge message toggle icon and enhance source display behavior --- .../chatAgentMergeContentPart.ts | 2 +- .../media/chatAgentMergeContent.css | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index b8e818b879152a..664ca19e73d442 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -210,7 +210,7 @@ export class ChatAgentMergeContentPart extends Disposable { const showDetailsLabel = localize('chat.agentMerge.showMergeDetails', "Show Merge Details"); messageButton = this._register(new Button(header, { ...transparentButtonStyles, title: false })); messageButton.element.classList.add('chat-agent-merge-message-toggle'); - messageButton.icon = Codicon.commentDiscussion; + messageButton.icon = Codicon.eye; messageButton.setAriaLabel(agentMessageLabel); messageButton.element.tabIndex = -1; preventPointerFocus(messageButton); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index 9d66a3f629a051..dce45e7ad7d096 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -151,7 +151,9 @@ which sits either on an ancestor or on the workbench element itself. */ .monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon, .monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon, .monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle, -.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle { +.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle, +.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-source, +.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-source { transition: none; } @@ -174,13 +176,33 @@ which sits either on an ancestor or on the workbench element itself. */ } .chat-agent-merge .chat-agent-merge-source { - display: flex; + display: none; align-items: center; flex: 0 0 auto; gap: var(--vscode-spacing-size60); color: var(--vscode-descriptionForeground); font-weight: var(--vscode-fontWeight-regular); + opacity: 0; white-space: nowrap; + transition: + opacity 60ms ease-out, + display 60ms allow-discrete; +} + +.chat-agent-merge:not(.collapsed) .chat-agent-merge-source, +.chat-agent-merge.collapsed > .chat-agent-merge-header:hover .chat-agent-merge-source, +.chat-agent-merge.collapsed > .chat-agent-merge-header:focus-within .chat-agent-merge-source { + display: flex; + opacity: 1; +} + +@starting-style { + + .chat-agent-merge:not(.collapsed) .chat-agent-merge-source, + .chat-agent-merge.collapsed > .chat-agent-merge-header:hover .chat-agent-merge-source, + .chat-agent-merge.collapsed > .chat-agent-merge-header:focus-within .chat-agent-merge-source { + opacity: 0; + } } .chat-agent-merge .chat-agent-merge-source::before { From ccb70da35e7666f8a5147287a3d4725712edb2de Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 11:54:08 +0100 Subject: [PATCH 05/14] Enhance chat agent merge styles for high contrast themes and update fixture for darkHighContrast support --- .../widget/chatContentParts/media/chatAgentMergeContent.css | 5 +++++ .../browser/componentFixtures/chat/chatAgentMerge.fixture.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index dce45e7ad7d096..fe49001ad64f23 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -21,6 +21,11 @@ border-color: transparent; } +.hc-black .chat-agent-merge.collapsed, +.hc-light .chat-agent-merge.collapsed { + border-color: var(--vscode-chat-requestBorder); +} + .chat-agent-merge.collapsed:hover, .chat-agent-merge.collapsed:focus-within { border-color: var(--vscode-chat-requestBorder); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts index c7d554f8b16c76..1f7955bc348938 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts @@ -225,6 +225,7 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { FailingChecks: defineComponentFixture({ labels: { kind: 'screenshot' }, + additionalThemes: ['darkHighContrast'], render: (ctx) => renderAgentMerge(ctx, { summary: createSummary(['fixCI'], { failedChecks }), }), From 978a00a0a2c267638d626eaa8029da81e827aeaa Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 12:03:47 +0100 Subject: [PATCH 06/14] Add hover effects to agent merge header and enhance related tests --- .../chatAgentMergeContentPart.ts | 2 ++ .../media/chatAgentMergeContent.css | 2 +- .../chatAgentMergeContentPart.test.ts | 33 ++++++++++++++----- .../chat/chatAgentMerge.fixture.ts | 30 +++++++++++++++++ .../chat/chatWidget.fixture.ts | 1 + 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index 664ca19e73d442..1c91c822356c59 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -174,6 +174,8 @@ export class ChatAgentMergeContentPart extends Disposable { private _createHeader(parent: HTMLElement): void { const header = dom.append(parent, dom.$('.chat-agent-merge-header')); + this._register(dom.addDisposableListener(header, dom.EventType.MOUSE_ENTER, () => this.domNode.classList.add('header-hovered'))); + this._register(dom.addDisposableListener(header, dom.EventType.MOUSE_LEAVE, () => this.domNode.classList.remove('header-hovered'))); const preventPointerFocus = (button: Button) => { this._register(dom.addDisposableGenericMouseDownListener(button.element, event => { event.preventDefault(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index fe49001ad64f23..a8e1c525352486 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -26,7 +26,7 @@ border-color: var(--vscode-chat-requestBorder); } -.chat-agent-merge.collapsed:hover, +.chat-agent-merge.collapsed.header-hovered, .chat-agent-merge.collapsed:focus-within { border-color: var(--vscode-chat-requestBorder); } diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index ca4ca045d060b8..90f82676d0ddc2 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -34,6 +34,17 @@ function summary(overrides: Partial = {}): IAgentMerge }; } +function createPart(data: IAgentMergePromptSummary): ChatAgentMergeContentPart { + return new ChatAgentMergeContentPart( + data, + URI.parse('test://session'), + upcastPartial({}), + upcastPartial({}), + upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), + upcastPartial({}), + ); +} + suite('ChatAgentMergeContentPart file labels', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -100,14 +111,7 @@ suite('ChatAgentMergeContentPart file labels', () => { }); test('keeps the Agent Message toggle name stable while reporting its state', () => { - const part = store.add(new ChatAgentMergeContentPart( - summary({ agentMessage: 'Merge agent details.' }), - URI.parse('test://session'), - upcastPartial({}), - upcastPartial({}), - upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), - upcastPartial({}), - )); + const part = store.add(createPart(summary({ agentMessage: 'Merge agent details.' }))); const button = part.domNode.querySelector('.chat-agent-merge-message-toggle'); assert.ok(button); @@ -127,4 +131,17 @@ suite('ChatAgentMergeContentPart file labels', () => { { label: 'Agent Message', pressed: 'false' }, ]); }); + + test('tracks hover only over the header content', () => { + const part = store.add(createPart(summary())); + const header = part.domNode.querySelector('.chat-agent-merge-header'); + assert.ok(header); + + header.dispatchEvent(new MouseEvent('mouseenter')); + const whileHovered = part.domNode.classList.contains('header-hovered'); + header.dispatchEvent(new MouseEvent('mouseleave')); + const afterLeaving = part.domNode.classList.contains('header-hovered'); + + assert.deepStrictEqual([whileHovered, afterLeaving], [true, false]); + }); }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts index 1f7955bc348938..5f2256cc55f957 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts @@ -17,6 +17,7 @@ import { ChatContentMarkdownRenderer } from '../../../../contrib/chat/browser/wi import { ChatAgentMergeContentPart } from '../../../../contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.js'; import { AgentFeedbackReviewCommandId, IChatAgentFeedbackPullRequestThreadLink } from '../../../../contrib/chat/common/chatService/chatService.js'; import { ComponentFixtureContext, createEditorServices, defineComponentFixture, defineThemedFixtureGroup } from '../fixtureUtils.js'; +import { renderChatWidget } from './chatWidget.fixture.js'; // ============================================================================ // Sample data @@ -303,4 +304,33 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { mirroredThreadIds: [reviewThreads[0].id, reviewThreads[2].id], }), }), + + InChat: defineComponentFixture({ + labels: { kind: 'screenshot' }, + render: ctx => renderChatWidget(ctx, { + width: 720, + height: 600, + inputVisible: false, + messages: [ + { + user: 'Polish the Agent Merge widget', + assistant: [{ + kind: 'markdown', + text: 'I updated the widget to make its status easier to scan and keep secondary controls quiet until they are needed.', + }], + }, + { + user: buildAgentMergePrompt(['addressReviews', 'fixCI'], createContext({ + reviewThreads: reviewThreads.slice(0, 1), + failedChecks, + })), + isSystemInitiated: true, + assistant: [{ + kind: 'markdown', + text: 'I addressed the review feedback and fixed the failing checks. The branch is ready for another review.', + }], + }, + ], + }), + }), }); diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts index a239e0019c9171..0ddc7be6ed2c06 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts @@ -55,6 +55,7 @@ export interface IFixtureFileChange { export interface IFixtureMessage { readonly user: string; // user prompt text + readonly isSystemInitiated?: boolean; readonly assistant?: ReadonlyArray< | { kind: 'markdown'; text: string } | { kind: 'progress'; text: string } From 85ca9028a9bc71d0aac873cf883975afbad45e57 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 12:49:48 +0100 Subject: [PATCH 07/14] Attach status hover to interactive disclosure in agent merge widget and update tests --- .../chatAgentMergeContentPart.ts | 4 ++-- .../chatAgentMergeContentPart.test.ts | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index 1c91c822356c59..a1d69775aa09d4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -189,8 +189,8 @@ export class ChatAgentMergeContentPart extends Disposable { const content = dom.append(header, dom.$('.chat-agent-merge-header-content', { 'aria-hidden': 'true' })); const icon = dom.append(content, dom.$('span.chat-agent-merge-status-icon')); icon.classList.add(...ThemeIcon.asClassNameArray(status.icon)); - const title = dom.append(content, dom.$('span.chat-agent-merge-title', undefined, status.title)); - this._register(this._hoverService.setupDelayedHover(title, { content: status.title })); + dom.append(content, dom.$('span.chat-agent-merge-title', undefined, status.title)); + this._register(this._hoverService.setupDelayedHover(disclosureButton.element, { content: status.title })); dom.append(content, dom.$('span.chat-agent-merge-source', undefined, agentMergeSource)); let messageButton: Button | undefined; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index 90f82676d0ddc2..a083b43b4f7b18 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -34,13 +34,16 @@ function summary(overrides: Partial = {}): IAgentMerge }; } -function createPart(data: IAgentMergePromptSummary): ChatAgentMergeContentPart { +function createPart( + data: IAgentMergePromptSummary, + hoverService: IHoverService = upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), +): ChatAgentMergeContentPart { return new ChatAgentMergeContentPart( data, URI.parse('test://session'), upcastPartial({}), upcastPartial({}), - upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), + hoverService, upcastPartial({}), ); } @@ -144,4 +147,16 @@ suite('ChatAgentMergeContentPart file labels', () => { assert.deepStrictEqual([whileHovered, afterLeaving], [true, false]); }); + + test('attaches the status hover to the interactive disclosure', () => { + let hoverTarget: HTMLElement | undefined; + const part = store.add(createPart(summary(), upcastPartial({ + setupDelayedHover: target => { + hoverTarget = target; + return toDisposable(() => { }); + }, + }))); + + assert.strictEqual(hoverTarget, part.domNode.querySelector('.chat-agent-merge-header-disclosure')); + }); }); From 1593b2dba53c596bdaad1b5dfb90ab6940d000fd Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 12:59:58 +0100 Subject: [PATCH 08/14] Refactor agent message button handling in chat agent merge widget for clarity and consistency --- .../chatAgentMergeContentPart.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index a1d69775aa09d4..f1f6c11a1fe250 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -210,23 +210,24 @@ export class ChatAgentMergeContentPart extends Disposable { const agentMessageLabel = localize('chat.agentMerge.agentMessage', "Agent Message"); const showMessageLabel = localize('chat.agentMerge.showAgentMessage', "Show Agent Message"); const showDetailsLabel = localize('chat.agentMerge.showMergeDetails', "Show Merge Details"); - messageButton = this._register(new Button(header, { ...transparentButtonStyles, title: false })); - messageButton.element.classList.add('chat-agent-merge-message-toggle'); - messageButton.icon = Codicon.eye; - messageButton.setAriaLabel(agentMessageLabel); - messageButton.element.tabIndex = -1; - preventPointerFocus(messageButton); + const agentMessageButton = this._register(new Button(header, { ...transparentButtonStyles, title: false })); + messageButton = agentMessageButton; + agentMessageButton.element.classList.add('chat-agent-merge-message-toggle'); + agentMessageButton.icon = Codicon.eye; + agentMessageButton.setAriaLabel(agentMessageLabel); + agentMessageButton.element.tabIndex = -1; + preventPointerFocus(agentMessageButton); let showingAgentMessage = false; const updateMessageVisibility = (visible: boolean) => { showingAgentMessage = visible; this.domNode.classList.toggle('showing-agent-message', visible); - messageButton.checked = visible; + agentMessageButton.checked = visible; }; updateMessageVisibility(false); - this._register(this._hoverService.setupDelayedHover(messageButton.element, () => ({ + this._register(this._hoverService.setupDelayedHover(agentMessageButton.element, () => ({ content: showingAgentMessage ? showDetailsLabel : showMessageLabel, }))); - this._register(messageButton.onDidClick(() => { + this._register(agentMessageButton.onDidClick(() => { setExpanded(true); updateMessageVisibility(!showingAgentMessage); })); From 9a4ad9fbd340f45dc42d0e838f26300e2cce112b Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 13:31:03 +0100 Subject: [PATCH 09/14] Add section title for review feedback when failed checks are present in chat agent merge widget --- .../chatAgentMergeContentPart.ts | 3 ++ .../chatAgentMergeContentPart.test.ts | 46 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index f1f6c11a1fe250..0547dc693bc596 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -269,6 +269,9 @@ export class ChatAgentMergeContentPart extends Disposable { } const section = dom.append(body, dom.$('.chat-agent-merge-section')); + if (this._summary.failedChecks.length > 0) { + dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.commentsTitle', "Review Feedback"))); + } const fileElements = new Map(); this._comments.forEach((comment, index) => { diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index a083b43b4f7b18..dd51c476b3e3ee 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import * as dom from '../../../../../../../base/browser/dom.js'; import { toDisposable } from '../../../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { upcastPartial } from '../../../../../../../base/test/common/mock.js'; @@ -36,14 +37,17 @@ function summary(overrides: Partial = {}): IAgentMerge function createPart( data: IAgentMergePromptSummary, - hoverService: IHoverService = upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), + options: { + readonly hoverService?: IHoverService; + readonly markdownRenderer?: IMarkdownRenderer; + } = {}, ): ChatAgentMergeContentPart { return new ChatAgentMergeContentPart( data, URI.parse('test://session'), - upcastPartial({}), + options.markdownRenderer ?? upcastPartial({}), upcastPartial({}), - hoverService, + options.hoverService ?? upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), upcastPartial({}), ); } @@ -150,13 +154,37 @@ suite('ChatAgentMergeContentPart file labels', () => { test('attaches the status hover to the interactive disclosure', () => { let hoverTarget: HTMLElement | undefined; - const part = store.add(createPart(summary(), upcastPartial({ - setupDelayedHover: target => { - hoverTarget = target; - return toDisposable(() => { }); - }, - }))); + const part = store.add(createPart(summary(), { + hoverService: upcastPartial({ + setupDelayedHover: target => { + hoverTarget = target; + return toDisposable(() => { }); + }, + }), + })); assert.strictEqual(hoverTarget, part.domNode.querySelector('.chat-agent-merge-header-disclosure')); }); + + test('shows section headings only when comments and checks are both present', () => { + const markdownRenderer = upcastPartial({ + render: () => ({ element: dom.$('div'), dispose: () => { } }), + }); + const reviewSummaries = [{ author: 'octocat', body: 'Please fix this.' }]; + const commentsOnly = store.add(createPart(summary({ reviewSummaries }), { markdownRenderer })); + const checksOnly = store.add(createPart(summary({ failedChecks: ['Compile'] }), { markdownRenderer })); + const mixed = store.add(createPart(summary({ reviewSummaries, failedChecks: ['Compile'] }), { markdownRenderer })); + const sectionTitles = (part: ChatAgentMergeContentPart) => + Array.from(part.domNode.querySelectorAll('.chat-agent-merge-section-title'), element => element.textContent); + + assert.deepStrictEqual([ + sectionTitles(commentsOnly), + sectionTitles(checksOnly), + sectionTitles(mixed), + ], [ + [], + [], + ['Review Feedback', 'Checks'], + ]); + }); }); From 62017e1debe392092bafe6fb44352862efa82b92 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 15:33:59 +0100 Subject: [PATCH 10/14] Add background color for agent merge widget and remove hover tracking from header --- build/lib/stylelint/vscode-known-variables.json | 1 + .../chatAgentMergeContentPart.ts | 2 -- .../media/chatAgentMergeContent.css | 17 ++++------------- .../contrib/chat/common/widget/chatColors.ts | 6 ++++++ .../chatAgentMergeContentPart.test.ts | 13 ------------- .../chat/chatAgentMerge.fixture.ts | 1 + 6 files changed, 12 insertions(+), 28 deletions(-) diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 4b0668ca3b6777..abd33957465c49 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -84,6 +84,7 @@ "--vscode-chat-requestCodeBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", + "--vscode-chat-statusBackground", "--vscode-chat-thinkingShimmer", "--vscode-agentsChatInput-background", "--vscode-agentsChatInput-border", diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index 0547dc693bc596..b4c80ef11bc8df 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -174,8 +174,6 @@ export class ChatAgentMergeContentPart extends Disposable { private _createHeader(parent: HTMLElement): void { const header = dom.append(parent, dom.$('.chat-agent-merge-header')); - this._register(dom.addDisposableListener(header, dom.EventType.MOUSE_ENTER, () => this.domNode.classList.add('header-hovered'))); - this._register(dom.addDisposableListener(header, dom.EventType.MOUSE_LEAVE, () => this.domNode.classList.remove('header-hovered'))); const preventPointerFocus = (button: Button) => { this._register(dom.addDisposableGenericMouseDownListener(button.element, event => { event.preventDefault(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index a8e1c525352486..79efdf40717881 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -8,7 +8,8 @@ flex-direction: column; align-self: flex-start; margin: var(--vscode-spacing-size40) 0; - border: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); + background-color: var(--vscode-chat-statusBackground); + border: var(--vscode-strokeThickness) solid transparent; border-radius: var(--vscode-cornerRadius-medium); overflow: hidden; font-size: var(--vscode-fontSize-label1); @@ -17,17 +18,8 @@ max-width: 100%; } -.chat-agent-merge.collapsed { - border-color: transparent; -} - -.hc-black .chat-agent-merge.collapsed, -.hc-light .chat-agent-merge.collapsed { - border-color: var(--vscode-chat-requestBorder); -} - -.chat-agent-merge.collapsed.header-hovered, -.chat-agent-merge.collapsed:focus-within { +.hc-black .chat-agent-merge, +.hc-light .chat-agent-merge { border-color: var(--vscode-chat-requestBorder); } @@ -219,7 +211,6 @@ which sits either on an ancestor or on the workbench element itself. */ flex-direction: column; gap: var(--vscode-spacing-size80); padding: var(--vscode-spacing-size40) var(--vscode-spacing-size120) var(--vscode-spacing-size120); - border-top: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); } .chat-agent-merge.collapsed > .chat-agent-merge-body { diff --git a/src/vs/workbench/contrib/chat/common/widget/chatColors.ts b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts index 2915b7ddb6ecfb..7130857d8cbc12 100644 --- a/src/vs/workbench/contrib/chat/common/widget/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts @@ -33,6 +33,12 @@ export const chatRequestBackground = registerColor( localize('chat.requestBackground', 'The background color of a chat request.') ); +export const chatStatusBackground = registerColor( + 'chat.statusBackground', + { dark: transparent(foreground, 0.08), light: transparent(foreground, 0.08), hcDark: Color.black, hcLight: Color.white }, + localize('chat.statusBackground', 'The background color of status elements in chat.') +); + export const chatSlashCommandBackground = registerColor( 'chat.slashCommandBackground', { dark: '#26477866', light: '#adceff7a', hcDark: Color.white, hcLight: badgeBackground }, diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index dd51c476b3e3ee..d512d60b8ad2c8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -139,19 +139,6 @@ suite('ChatAgentMergeContentPart file labels', () => { ]); }); - test('tracks hover only over the header content', () => { - const part = store.add(createPart(summary())); - const header = part.domNode.querySelector('.chat-agent-merge-header'); - assert.ok(header); - - header.dispatchEvent(new MouseEvent('mouseenter')); - const whileHovered = part.domNode.classList.contains('header-hovered'); - header.dispatchEvent(new MouseEvent('mouseleave')); - const afterLeaving = part.domNode.classList.contains('header-hovered'); - - assert.deepStrictEqual([whileHovered, afterLeaving], [true, false]); - }); - test('attaches the status hover to the interactive disclosure', () => { let hoverTarget: HTMLElement | undefined; const part = store.add(createPart(summary(), { diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts index 5f2256cc55f957..9661b5b02ad4dd 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts @@ -158,6 +158,7 @@ interface IRenderAgentMergeOptions { function renderAgentMerge({ container, disposableStore, theme }: ComponentFixtureContext, options: IRenderAgentMergeOptions): void { container.style.width = '640px'; container.style.padding = '8px'; + container.style.backgroundColor = 'var(--vscode-sideBar-background, var(--vscode-editor-background))'; // The widget only looks up mirrors where the Agents window registered the // feedback commands, so a fixture with mirrors registers them for real; the From 0e56abb6e8673071c3244973d11febebb6e91877 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 15:55:06 +0100 Subject: [PATCH 11/14] Add timestamp metadata to agent merge widget and update related tests --- .../chatAgentMergeContentPart.ts | 48 +++++----- .../media/chatAgentMergeContent.css | 96 ++++++++++--------- .../chat/browser/widget/chatListRenderer.ts | 2 +- .../chatAgentMergeContentPart.test.ts | 23 +++++ .../chat/chatAgentMerge.fixture.ts | 4 +- .../chat/chatWidget.fixture.ts | 3 +- 6 files changed, 104 insertions(+), 72 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index b4c80ef11bc8df..ab6b484cf3a384 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -22,6 +22,7 @@ import { IHoverService } from '../../../../../../platform/hover/browser/hover.js import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { ChatPillActionViewItem } from '../../../../../browser/chatPills.js'; +import { formatChatRequestTimestamp } from '../../../common/chatProgressFormatting.js'; import { AgentFeedbackReviewCommandId, IChatAgentFeedbackPullRequestThreadLink } from '../../../common/chatService/chatService.js'; import { IChatRequestViewModel } from '../../../common/model/chatViewModel.js'; import './media/chatAgentMergeContent.css'; @@ -67,7 +68,7 @@ interface IAgentMergeCommentItem extends IAgentMergeFileLocation { const agentMergeSource = localize('chat.agentMerge.source', "Agent Merge"); /** The status shown in the header, describing why the turn was started. */ -function describeAgentMergeStatus(summary: IAgentMergePromptSummary, commentCount: number): { readonly icon: ThemeIcon; readonly title: string } { +function describeAgentMergeStatus(summary: IAgentMergePromptSummary, commentCount: number): string { const events: string[] = []; if (commentCount > 0) { events.push(commentCount === 1 @@ -89,19 +90,7 @@ function describeAgentMergeStatus(summary: IAgentMergePromptSummary, commentCoun events.push(localize('chat.agentMerge.noPendingFeedback', "No Pending Feedback")); } - const icon = summary.failedChecks.length > 0 - ? Codicon.errorCompact - : summary.conflicting - ? Codicon.warningCompact - : commentCount > 0 - ? Codicon.commentCompact - : summary.behind - ? Codicon.arrowDown - : Codicon.checkCompact; - return { - icon, - title: formatAgentMergeEvents(events), - }; + return formatAgentMergeEvents(events); } function formatAgentMergeEvents(events: readonly string[]): string { @@ -124,7 +113,7 @@ function formatAgentMergeEvents(events: readonly string[]): string { */ export function getAgentMergeSummaryLabel(summary: IAgentMergePromptSummary): string { const status = describeAgentMergeStatus(summary, collectComments(summary).length); - return localize('chat.agentMerge.summaryLabel', "{0}, {1}", status.title, agentMergeSource); + return localize('chat.agentMerge.summaryLabel', "{0}, {1}", status, agentMergeSource); } /** @@ -152,6 +141,7 @@ export class ChatAgentMergeContentPart extends Disposable { private readonly _summary: IAgentMergePromptSummary, private readonly _sessionResource: URI, private readonly _markdownRenderer: IMarkdownRenderer, + timestamp: number | undefined, @IOpenerService private readonly _openerService: IOpenerService, @IHoverService private readonly _hoverService: IHoverService, @ICommandService private readonly _commandService: ICommandService, @@ -162,14 +152,16 @@ export class ChatAgentMergeContentPart extends Disposable { this._fileLabels = describeAgentMergeFileLabels(this._comments); this.domNode = dom.$('.chat-agent-merge'); - this._createHeader(this.domNode); + const card = dom.append(this.domNode, dom.$('.chat-agent-merge-card')); + this._createHeader(card); - const body = dom.append(this.domNode, dom.$('.chat-agent-merge-body')); + const body = dom.append(card, dom.$('.chat-agent-merge-body')); const details = dom.append(body, dom.$('.chat-agent-merge-details')); this._createPullRequestRow(details); this._createCommentsSection(details); this._createChecksSection(details); this._createAgentMessage(body); + this._createMetadata(this.domNode, timestamp); } private _createHeader(parent: HTMLElement): void { @@ -185,11 +177,8 @@ export class ChatAgentMergeContentPart extends Disposable { const status = describeAgentMergeStatus(this._summary, this._comments.length); const content = dom.append(header, dom.$('.chat-agent-merge-header-content', { 'aria-hidden': 'true' })); - const icon = dom.append(content, dom.$('span.chat-agent-merge-status-icon')); - icon.classList.add(...ThemeIcon.asClassNameArray(status.icon)); - dom.append(content, dom.$('span.chat-agent-merge-title', undefined, status.title)); - this._register(this._hoverService.setupDelayedHover(disclosureButton.element, { content: status.title })); - dom.append(content, dom.$('span.chat-agent-merge-source', undefined, agentMergeSource)); + dom.append(content, dom.$('span.chat-agent-merge-title', undefined, status)); + this._register(this._hoverService.setupDelayedHover(disclosureButton.element, { content: status })); let messageButton: Button | undefined; const setExpanded = (expanded: boolean) => { @@ -235,6 +224,21 @@ export class ChatAgentMergeContentPart extends Disposable { twistie.classList.add(...ThemeIcon.asClassNameArray(Codicon.chevronRightCompact)); } + private _createMetadata(parent: HTMLElement, timestamp: number | undefined): void { + const metadata = dom.append(parent, dom.$('.chat-agent-merge-metadata')); + const formattedTimestamp = formatChatRequestTimestamp(timestamp); + if (formattedTimestamp) { + const time = dom.append(metadata, dom.$('time.chat-agent-merge-timestamp', { + datetime: formattedTimestamp.dateTime, + 'aria-label': localize('chat.agentMerge.startedAt', "Started {0}", formattedTimestamp.fullText), + tabindex: 0, + }, formattedTimestamp.text)); + this._register(this._hoverService.setupDelayedHover(time, { content: formattedTimestamp.fullText })); + dom.append(metadata, dom.$('span.chat-agent-merge-metadata-separator', { 'aria-hidden': 'true' }, '\u2022')); + } + dom.append(metadata, dom.$('span.chat-agent-merge-participant', undefined, agentMergeSource)); + } + private _createPullRequestRow(body: HTMLElement): void { if (!this._summary.pullRequestUrl) { return; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index 79efdf40717881..0e3b10e312e599 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -6,33 +6,41 @@ .chat-agent-merge { display: flex; flex-direction: column; - align-self: flex-start; + gap: var(--vscode-spacing-size40); margin: var(--vscode-spacing-size40) 0; + width: 100%; + max-width: 100%; + font-size: var(--vscode-fontSize-label1); +} + +.chat-agent-merge > .chat-agent-merge-card { + display: flex; + flex-direction: column; + align-self: flex-start; background-color: var(--vscode-chat-statusBackground); border: var(--vscode-strokeThickness) solid transparent; border-radius: var(--vscode-cornerRadius-medium); overflow: hidden; - font-size: var(--vscode-fontSize-label1); /* Collapsed, the card hugs its header; expanded, the body grows it to the transcript width. */ width: fit-content; max-width: 100%; } -.hc-black .chat-agent-merge, -.hc-light .chat-agent-merge { +.hc-black .chat-agent-merge > .chat-agent-merge-card, +.hc-light .chat-agent-merge > .chat-agent-merge-card { border-color: var(--vscode-chat-requestBorder); } -.chat-agent-merge > .chat-agent-merge-header, +.chat-agent-merge > .chat-agent-merge-card > .chat-agent-merge-header, .chat-agent-merge .chat-agent-merge-header-content { display: flex; align-items: center; } -.chat-agent-merge > .chat-agent-merge-header { +.chat-agent-merge > .chat-agent-merge-card > .chat-agent-merge-header { position: relative; gap: var(--vscode-spacing-size40); - padding: var(--vscode-spacing-size60) var(--vscode-spacing-size80); + padding: var(--vscode-spacing-size60) var(--vscode-spacing-size80) var(--vscode-spacing-size60) var(--vscode-spacing-size120); border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)) 0 0; overflow: hidden; } @@ -54,17 +62,16 @@ position: relative; z-index: 1; flex: 0 1 auto; - gap: var(--vscode-spacing-size60); min-width: 0; min-height: var(--vscode-spacing-size200); pointer-events: none; } -.chat-agent-merge.collapsed > .chat-agent-merge-header { +.chat-agent-merge.collapsed > .chat-agent-merge-card > .chat-agent-merge-header { border-radius: calc(var(--vscode-cornerRadius-medium) - var(--vscode-strokeThickness)); } -.chat-agent-merge > .chat-agent-merge-header:hover { +.chat-agent-merge > .chat-agent-merge-card > .chat-agent-merge-header:hover { background-color: var(--vscode-toolbar-hoverBackground); } @@ -105,8 +112,8 @@ font-size: var(--vscode-codiconFontSize-compact); } -.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header:hover .chat-agent-merge-message-toggle, -.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header:focus-within .chat-agent-merge-message-toggle { +.chat-agent-merge:not(.collapsed) > .chat-agent-merge-card > .chat-agent-merge-header:hover .chat-agent-merge-message-toggle, +.chat-agent-merge:not(.collapsed) > .chat-agent-merge-card > .chat-agent-merge-header:focus-within .chat-agent-merge-message-toggle { opacity: 1; pointer-events: auto; } @@ -138,8 +145,8 @@ the codicon base rule, which would otherwise force every glyph to 16px. */ opacity: 0; } -.chat-agent-merge > .chat-agent-merge-header:hover > .chat-agent-merge-twistie, -.chat-agent-merge > .chat-agent-merge-header:focus-within > .chat-agent-merge-twistie { +.chat-agent-merge > .chat-agent-merge-card > .chat-agent-merge-header:hover > .chat-agent-merge-twistie, +.chat-agent-merge > .chat-agent-merge-card > .chat-agent-merge-header:focus-within > .chat-agent-merge-twistie { opacity: 1; } @@ -148,22 +155,14 @@ which sits either on an ancestor or on the workbench element itself. */ .monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon, .monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-twistie.codicon, .monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle, -.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle, -.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-source, -.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-source { +.monaco-workbench.monaco-reduce-motion .chat-agent-merge .chat-agent-merge-message-toggle { transition: none; } -.chat-agent-merge:not(.collapsed) > .chat-agent-merge-header > .chat-agent-merge-twistie { +.chat-agent-merge:not(.collapsed) > .chat-agent-merge-card > .chat-agent-merge-header > .chat-agent-merge-twistie { transform: rotate(90deg); } -.chat-agent-merge .chat-agent-merge-status-icon.codicon { - flex: 0 0 auto; - font-size: var(--vscode-codiconFontSize-compact); - color: var(--vscode-descriptionForeground); -} - .chat-agent-merge .chat-agent-merge-title { min-width: 0; overflow: hidden; @@ -172,48 +171,50 @@ which sits either on an ancestor or on the workbench element itself. */ white-space: nowrap; } -.chat-agent-merge .chat-agent-merge-source { - display: none; +.chat-agent-merge > .chat-agent-merge-metadata { + align-self: flex-end; + display: flex; align-items: center; - flex: 0 0 auto; - gap: var(--vscode-spacing-size60); + gap: var(--vscode-spacing-size40); + min-width: 0; color: var(--vscode-descriptionForeground); - font-weight: var(--vscode-fontWeight-regular); + font-size: var(--vscode-fontSize-body2); + line-height: var(--vscode-spacing-size160); opacity: 0; + pointer-events: none; white-space: nowrap; - transition: - opacity 60ms ease-out, - display 60ms allow-discrete; + transition: opacity 100ms ease-in-out; } -.chat-agent-merge:not(.collapsed) .chat-agent-merge-source, -.chat-agent-merge.collapsed > .chat-agent-merge-header:hover .chat-agent-merge-source, -.chat-agent-merge.collapsed > .chat-agent-merge-header:focus-within .chat-agent-merge-source { - display: flex; - opacity: 1; +.chat-agent-merge:hover > .chat-agent-merge-metadata, +.chat-agent-merge:focus-within > .chat-agent-merge-metadata { + opacity: 0.7; + pointer-events: auto; } -@starting-style { +.chat-agent-merge .chat-agent-merge-timestamp { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum"; +} - .chat-agent-merge:not(.collapsed) .chat-agent-merge-source, - .chat-agent-merge.collapsed > .chat-agent-merge-header:hover .chat-agent-merge-source, - .chat-agent-merge.collapsed > .chat-agent-merge-header:focus-within .chat-agent-merge-source { - opacity: 0; - } +.chat-agent-merge .chat-agent-merge-timestamp:focus-visible { + outline: var(--vscode-strokeThickness) solid var(--vscode-focusBorder); + outline-offset: var(--vscode-spacing-size20); } -.chat-agent-merge .chat-agent-merge-source::before { - content: '\2022'; +.monaco-reduce-motion .chat-agent-merge > .chat-agent-merge-metadata, +.monaco-workbench.monaco-reduce-motion .chat-agent-merge > .chat-agent-merge-metadata { + transition: none; } -.chat-agent-merge > .chat-agent-merge-body { +.chat-agent-merge > .chat-agent-merge-card > .chat-agent-merge-body { display: flex; flex-direction: column; gap: var(--vscode-spacing-size80); padding: var(--vscode-spacing-size40) var(--vscode-spacing-size120) var(--vscode-spacing-size120); } -.chat-agent-merge.collapsed > .chat-agent-merge-body { +.chat-agent-merge.collapsed > .chat-agent-merge-card > .chat-agent-merge-body { display: none; } @@ -244,6 +245,7 @@ which sits either on an ancestor or on the workbench element itself. */ display: flex; align-items: center; padding-top: var(--vscode-spacing-size40); + padding-bottom: var(--vscode-spacing-size40); margin-inline: calc(var(--vscode-spacing-size60) - var(--vscode-spacing-size120)); min-width: 0; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index b43b6c624c57b7..31f9332ac9b504 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -2539,7 +2539,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer({}), + options.timestamp, upcastPartial({}), options.hoverService ?? upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), upcastPartial({}), @@ -153,6 +155,27 @@ suite('ChatAgentMergeContentPart file labels', () => { assert.strictEqual(hoverTarget, part.domNode.querySelector('.chat-agent-merge-header-disclosure')); }); + test('renders the request timestamp and participant below the card', () => { + const timestamp = new Date().setHours(15, 33, 0, 0); + const part = store.add(createPart(summary(), { timestamp })); + const metadata = part.domNode.querySelector('.chat-agent-merge-metadata'); + const time = metadata?.querySelector('time'); + + assert.deepStrictEqual({ + cardParent: part.domNode.querySelector('.chat-agent-merge-card')?.parentElement, + metadataParent: metadata?.parentElement, + metadataText: metadata?.textContent, + dateTime: time?.dateTime, + tabIndex: time?.tabIndex, + }, { + cardParent: part.domNode, + metadataParent: part.domNode, + metadataText: '3:33 PM\u2022Agent Merge', + dateTime: new Date(timestamp).toISOString(), + tabIndex: 0, + }); + }); + test('shows section headings only when comments and checks are both present', () => { const markdownRenderer = upcastPartial({ render: () => ({ element: dom.$('div'), dispose: () => { } }), diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts index 9661b5b02ad4dd..36b4b95372b16d 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatAgentMerge.fixture.ts @@ -110,6 +110,7 @@ const markdownThreads: AgentMergePromptContext['reviewThreads'] = [ ]; const failedChecks = ['Compile / Compile (ubuntu-latest)', 'Linux Unit Tests (Electron)']; +const fixtureTimestamp = new Date().setHours(15, 33, 0, 0); function createContext(overrides?: Partial): AgentMergePromptContext { return { @@ -195,7 +196,7 @@ function renderAgentMerge({ container, disposableStore, theme }: ComponentFixtur instantiationService.get(IMarkdownRendererService).setDefaultCodeBlockRenderer(instantiationService.createInstance(EditorMarkdownCodeBlockRenderer)); const markdownRenderer = instantiationService.createInstance(ChatContentMarkdownRenderer); - const part = disposableStore.add(instantiationService.createInstance(ChatAgentMergeContentPart, options.summary, sessionResource, markdownRenderer)); + const part = disposableStore.add(instantiationService.createInstance(ChatAgentMergeContentPart, options.summary, sessionResource, markdownRenderer, fixtureTimestamp)); container.appendChild(part.domNode); if (options.expanded) { @@ -326,6 +327,7 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { failedChecks, })), isSystemInitiated: true, + timestamp: fixtureTimestamp, assistant: [{ kind: 'markdown', text: 'I addressed the review feedback and fixed the failing checks. The branch is ready for another review.', diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts index 0ddc7be6ed2c06..47597de15e5f60 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts @@ -56,6 +56,7 @@ export interface IFixtureFileChange { export interface IFixtureMessage { readonly user: string; // user prompt text readonly isSystemInitiated?: boolean; + readonly timestamp?: number; readonly assistant?: ReadonlyArray< | { kind: 'markdown'; text: string } | { kind: 'progress'; text: string } @@ -278,7 +279,7 @@ export async function renderChatWidget(context: ComponentFixtureContext, options undefined, undefined, undefined, - undefined, + message.timestamp, undefined, undefined, message.requestHidden, From 0a291ea33e342ad37e2cb6907c68b3ee0a817f3d Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 15:58:51 +0100 Subject: [PATCH 12/14] Update section title in agent merge widget from "Review Feedback" to "Feedback" --- .../widget/chatContentParts/chatAgentMergeContentPart.ts | 2 +- .../widget/chatContentParts/chatAgentMergeContentPart.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index ab6b484cf3a384..fa8db86f33fa27 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -272,7 +272,7 @@ export class ChatAgentMergeContentPart extends Disposable { const section = dom.append(body, dom.$('.chat-agent-merge-section')); if (this._summary.failedChecks.length > 0) { - dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.commentsTitle', "Review Feedback"))); + dom.append(section, dom.$('.chat-agent-merge-section-title', undefined, localize('chat.agentMerge.commentsTitle', "Feedback"))); } const fileElements = new Map(); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index ca3be61a2a4b78..a598b661198422 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -194,7 +194,7 @@ suite('ChatAgentMergeContentPart file labels', () => { ], [ [], [], - ['Review Feedback', 'Checks'], + ['Feedback', 'Checks'], ]); }); }); From b09f6d84d05cd16a65ea0bb4526d87c5d55e11a2 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Wed, 2 Sep 2026 16:41:14 +0100 Subject: [PATCH 13/14] Enhance pointer handling in chat agent merge widget to support touch input and suppress mouse focus --- .../chatAgentMergeContentPart.ts | 13 ++++--- .../media/chatAgentMergeContent.css | 1 + .../chatAgentMergeContentPart.test.ts | 36 +++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts index fa8db86f33fa27..ee55faad376e84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -166,8 +166,13 @@ export class ChatAgentMergeContentPart extends Disposable { private _createHeader(parent: HTMLElement): void { const header = dom.append(parent, dom.$('.chat-agent-merge-header')); - const preventPointerFocus = (button: Button) => { - this._register(dom.addDisposableGenericMouseDownListener(button.element, event => { + const handlePointerFocus = (button: Button) => { + this._register(dom.addDisposableListener(button.element, dom.EventType.POINTER_DOWN, event => { + if (event.pointerType !== 'mouse') { + this.domNode.classList.add('direct-pointer-input'); + return; + } + this.domNode.classList.remove('direct-pointer-input'); event.preventDefault(); button.element.blur(); })); @@ -190,7 +195,7 @@ export class ChatAgentMergeContentPart extends Disposable { } }; setExpanded(false); - preventPointerFocus(disclosureButton); + handlePointerFocus(disclosureButton); this._register(disclosureButton.onDidClick(() => setExpanded(this.domNode.classList.contains('collapsed')))); if (this._summary.agentMessage) { @@ -203,7 +208,7 @@ export class ChatAgentMergeContentPart extends Disposable { agentMessageButton.icon = Codicon.eye; agentMessageButton.setAriaLabel(agentMessageLabel); agentMessageButton.element.tabIndex = -1; - preventPointerFocus(agentMessageButton); + handlePointerFocus(agentMessageButton); let showingAgentMessage = false; const updateMessageVisibility = (visible: boolean) => { showingAgentMessage = visible; diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css index 0e3b10e312e599..5a99d69fbb87b7 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/media/chatAgentMergeContent.css @@ -112,6 +112,7 @@ font-size: var(--vscode-codiconFontSize-compact); } +.chat-agent-merge.direct-pointer-input:not(.collapsed) > .chat-agent-merge-card > .chat-agent-merge-header .chat-agent-merge-message-toggle, .chat-agent-merge:not(.collapsed) > .chat-agent-merge-card > .chat-agent-merge-header:hover .chat-agent-merge-message-toggle, .chat-agent-merge:not(.collapsed) > .chat-agent-merge-card > .chat-agent-merge-header:focus-within .chat-agent-merge-message-toggle { opacity: 1; diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts index a598b661198422..cb3ebcc12c99c8 100644 --- a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAgentMergeContentPart.test.ts @@ -141,6 +141,42 @@ suite('ChatAgentMergeContentPart file labels', () => { ]); }); + test('reveals secondary actions for touch input while suppressing mouse focus', () => { + const part = store.add(createPart(summary())); + const button = part.domNode.querySelector('.chat-agent-merge-header-disclosure'); + assert.ok(button); + dom.getWindow(button).document.body.append(part.domNode); + store.add(toDisposable(() => part.domNode.remove())); + + const pointerDown = (pointerType: string) => { + const event = new PointerEvent(dom.EventType.POINTER_DOWN, { bubbles: true, cancelable: true, pointerType }); + button.dispatchEvent(event); + return event.defaultPrevented; + }; + + button.focus(); + const touchPrevented = pointerDown('touch'); + const touchInput = part.domNode.classList.contains('direct-pointer-input'); + button.focus(); + const mousePrevented = pointerDown('mouse'); + const mouseInput = part.domNode.classList.contains('direct-pointer-input'); + const mouseRetainedFocus = dom.getWindow(button).document.activeElement === button; + + assert.deepStrictEqual({ + touchPrevented, + touchInput, + mousePrevented, + mouseInput, + mouseRetainedFocus, + }, { + touchPrevented: false, + touchInput: true, + mousePrevented: true, + mouseInput: false, + mouseRetainedFocus: false, + }); + }); + test('attaches the status hover to the interactive disclosure', () => { let hoverTarget: HTMLElement | undefined; const part = store.add(createPart(summary(), { From 37a9b45ea358f53500f3edb478e58febc25ff7e0 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Wed, 2 Sep 2026 23:18:56 +0200 Subject: [PATCH 14/14] Fix agent merge fixture after rebase Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test/browser/componentFixtures/chat/chatWidget.fixture.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts index 47597de15e5f60..fddb30d9c0be55 100644 --- a/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts +++ b/src/vs/workbench/test/browser/componentFixtures/chat/chatWidget.fixture.ts @@ -55,7 +55,6 @@ export interface IFixtureFileChange { export interface IFixtureMessage { readonly user: string; // user prompt text - readonly isSystemInitiated?: boolean; readonly timestamp?: number; readonly assistant?: ReadonlyArray< | { kind: 'markdown'; text: string }