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 bf499d7d29ada1..ee55faad376e84 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAgentMergeContentPart.ts @@ -6,11 +6,12 @@ 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'; 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'; @@ -20,6 +21,8 @@ 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 { formatChatRequestTimestamp } from '../../../common/chatProgressFormatting.js'; import { AgentFeedbackReviewCommandId, IChatAgentFeedbackPullRequestThreadLink } from '../../../common/chatService/chatService.js'; import { IChatRequestViewModel } from '../../../common/model/chatViewModel.js'; import './media/chatAgentMergeContent.css'; @@ -62,38 +65,45 @@ 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): 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 (badges.length === 0) { - badges.push({ - icon: summary.conflicting ? Codicon.warningCompact : Codicon.arrowDown, - label: summary.conflicting - ? localize('chat.agentMerge.conflicting', "Merge conflicts") - : summary.behind - ? localize('chat.agentMerge.behind', "Behind base branch") - : localize('chat.agentMerge.upToDate', "No pending feedback"), - }); + if (summary.conflicting) { + events.push(localize('chat.agentMerge.mergeConflicts', "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")); + } + + return 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 +112,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, agentMergeSource); } /** @@ -119,16 +129,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; @@ -140,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, @@ -150,34 +152,96 @@ export class ChatAgentMergeContentPart extends Disposable { this._fileLabels = describeAgentMergeFileLabels(this._comments); this.domNode = dom.$('.chat-agent-merge'); - 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 card = dom.append(this.domNode, dom.$('.chat-agent-merge-card')); + this._createHeader(card); + + 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 { - 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 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(); + })); + }; + 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 content = dom.append(header, dom.$('.chat-agent-merge-header-content', { 'aria-hidden': 'true' })); + 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) => { + 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); + handlePointerFocus(disclosureButton); + 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"); + 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; + handlePointerFocus(agentMessageButton); + let showingAgentMessage = false; + const updateMessageVisibility = (visible: boolean) => { + showingAgentMessage = visible; + this.domNode.classList.toggle('showing-agent-message', visible); + agentMessageButton.checked = visible; + }; + updateMessageVisibility(false); + this._register(this._hoverService.setupDelayedHover(agentMessageButton.element, () => ({ + content: showingAgentMessage ? showDetailsLabel : showMessageLabel, + }))); + this._register(agentMessageButton.onDidClick(() => { + setExpanded(true); + updateMessageVisibility(!showingAgentMessage); + })); + } - const twistie = dom.append(button.element, dom.$('span.chat-agent-merge-twistie')); + const twistie = dom.append(header, dom.$('span.chat-agent-merge-twistie', { 'aria-hidden': 'true' })); 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))); + 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 { @@ -186,12 +250,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 +276,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.commentsTitle', "Review Feedback"))); + if (this._summary.failedChecks.length > 0) { + 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 +347,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', "Failing 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; @@ -287,23 +367,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. */ @@ -324,21 +394,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 dcdc111a8c5fb0..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 @@ -6,109 +6,227 @@ .chat-agent-merge { display: flex; flex-direction: column; + gap: var(--vscode-spacing-size40); margin: var(--vscode-spacing-size40) 0; - border: var(--vscode-strokeThickness) solid var(--vscode-chat-requestBorder); + 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%; } -.chat-agent-merge > .chat-agent-merge-header, -.chat-agent-merge .chat-agent-merge-message-header { +.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-card > .chat-agent-merge-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-card > .chat-agent-merge-header { + position: relative; + gap: var(--vscode-spacing-size40); + 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; +} + +.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); +.chat-agent-merge .chat-agent-merge-header-content { + position: relative; + z-index: 1; + flex: 0 1 auto; + min-width: 0; + min-height: var(--vscode-spacing-size200); + pointer-events: none; +} + +.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-message-header:hover { +.chat-agent-merge > .chat-agent-merge-card > .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; + cursor: pointer; + 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.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; + 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); - transition: transform 100ms ease-out; + pointer-events: none; + transition: opacity 100ms ease-out, transform 100ms ease-out; +} + +.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-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; } /* 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-card > .chat-agent-merge-header > .chat-agent-merge-twistie { transform: rotate(90deg); } -.chat-agent-merge .chat-agent-merge-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; + text-overflow: ellipsis; font-weight: var(--vscode-fontWeight-semiBold); white-space: nowrap; } -.chat-agent-merge .chat-agent-merge-badges { +.chat-agent-merge > .chat-agent-merge-metadata { + align-self: flex-end; display: flex; align-items: center; - gap: var(--vscode-spacing-size80); + gap: var(--vscode-spacing-size40); min-width: 0; - overflow: hidden; color: var(--vscode-descriptionForeground); + font-size: var(--vscode-fontSize-body2); + line-height: var(--vscode-spacing-size160); + opacity: 0; + pointer-events: none; + white-space: nowrap; + transition: opacity 100ms ease-in-out; } -.chat-agent-merge .chat-agent-merge-badge { - display: flex; - align-items: center; - gap: var(--vscode-spacing-size40); - white-space: nowrap; +.chat-agent-merge:hover > .chat-agent-merge-metadata, +.chat-agent-merge:focus-within > .chat-agent-merge-metadata { + opacity: 0.7; + pointer-events: auto; } -.chat-agent-merge .chat-agent-merge-badge::before { - content: ''; - 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-timestamp { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum"; } -.chat-agent-merge .chat-agent-merge-badge .codicon { - font-size: var(--vscode-codiconFontSize-compact); - color: inherit; +.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-body { +.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-card > .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); } -.chat-agent-merge.collapsed > .chat-agent-merge-body { +.chat-agent-merge.collapsed > .chat-agent-merge-card > .chat-agent-merge-body { + 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; } @@ -124,32 +242,27 @@ 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. */ -.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; - gap: var(--vscode-spacing-size60); - padding-top: var(--vscode-spacing-size80); + 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; } -.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 +270,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); } @@ -168,6 +285,7 @@ matching the pull request content part's link treatment. */ display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: var(--vscode-spacing-size60); + row-gap: var(--vscode-spacing-size20); align-items: start; } @@ -177,6 +295,10 @@ matching the pull request content part's link treatment. */ 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; @@ -211,6 +333,24 @@ matching the pull request content part's link treatment. */ 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; @@ -239,29 +379,17 @@ matching the pull request content part's link treatment. */ } .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/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 { 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..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 @@ -4,11 +4,58 @@ *--------------------------------------------------------------------------------------------*/ 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'; 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 { 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 { + actions: [], + pullRequestUrl: '', + title: '', + headRef: '', + headSha: '', + baseRef: 'main', + reviewThreads: [], + reviewSummaries: [], + newComments: [], + failedChecks: [], + behind: false, + conflicting: false, + agentMessage: '', + ...overrides, + }; +} + +function createPart( + data: IAgentMergePromptSummary, + options: { + readonly hoverService?: IHoverService; + readonly markdownRenderer?: IMarkdownRenderer; + readonly timestamp?: number; + } = {}, +): ChatAgentMergeContentPart { + return new ChatAgentMergeContentPart( + data, + URI.parse('test://session'), + options.markdownRenderer ?? upcastPartial({}), + options.timestamp, + upcastPartial({}), + options.hoverService ?? upcastPartial({ setupDelayedHover: () => toDisposable(() => { }) }), + upcastPartial({}), + ); +} suite('ChatAgentMergeContentPart file labels', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const store = ensureNoDisposablesAreLeakedInTestSuite(); test('names a unique file without a disambiguating path', () => { const labels = describeAgentMergeFileLabels([ @@ -49,4 +96,141 @@ 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', + ]); + }); + + test('keeps the Agent Message toggle name stable while reporting its state', () => { + const part = store.add(createPart(summary({ agentMessage: 'Merge agent details.' }))); + 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' }, + ]); + }); + + 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(), { + hoverService: upcastPartial({ + setupDelayedHover: target => { + hoverTarget = target; + return toDisposable(() => { }); + }, + }), + })); + + 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: () => { } }), + }); + 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), + ], [ + [], + [], + ['Feedback', 'Checks'], + ]); + }); }); 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..36b4b95372b16d 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 @@ -109,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 { @@ -148,8 +150,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[]; } @@ -157,6 +159,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 @@ -193,14 +196,14 @@ 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 || 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(); } } @@ -225,6 +228,7 @@ export default defineThemedFixtureGroup({ path: 'chat/' }, { FailingChecks: defineComponentFixture({ labels: { kind: 'screenshot' }, + additionalThemes: ['darkHighContrast'], render: (ctx) => renderAgentMerge(ctx, { summary: createSummary(['fixCI'], { failedChecks }), }), @@ -274,7 +278,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, }), }), @@ -302,4 +306,34 @@ 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, + 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 a239e0019c9171..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,6 +55,7 @@ export interface IFixtureFileChange { export interface IFixtureMessage { readonly user: string; // user prompt text + readonly timestamp?: number; readonly assistant?: ReadonlyArray< | { kind: 'markdown'; text: string } | { kind: 'progress'; text: string } @@ -277,7 +278,7 @@ export async function renderChatWidget(context: ComponentFixtureContext, options undefined, undefined, undefined, - undefined, + message.timestamp, undefined, undefined, message.requestHidden,