Skip to content

Commit 1e5d782

Browse files
chrywCopilot
andcommitted
sessions: preserve recorded PR titles in editor chat
Carry pull request artifact labels into regular editor and panel pill dropdowns, accessible names, and compact tooltips to match the Agents Window. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b75b97c commit 1e5d782

4 files changed

Lines changed: 121 additions & 42 deletions

File tree

‎src/vs/sessions/contrib/chat/browser/sessionChatInputToolbar.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export function buildSessionPullRequestSections(pullRequests: readonly IResolved
112112
})],
113113
...getChatPillResourceLocation(ref.uri, label),
114114
ariaDescription: localize('sessionChatPills.pullRequestDescription', "{0}. {1}", stateDescription, ref.uri.toString(true)),
115+
...(!pullRequest && ref.title ? { tooltip: `${label}\n${ref.uri.toString(true)}` } : {}),
115116
...(pullRequest ? {
116117
pillHover: {
117118
element: () => createPullRequestHoverElement({

‎src/vs/sessions/contrib/chat/test/browser/sessionChatInputToolbar.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ suite('SessionChatInputToolbar', () => {
155155
repo: 'vscode',
156156
number: 332982,
157157
uri: URI.parse('https://github.com/microsoft/vscode/pull/332982'),
158+
title: 'Recorded pull request title',
158159
};
159160
const pullRequest: IGitHubPullRequest = {
160161
number: pullRequestRef.number,
@@ -236,11 +237,15 @@ suite('SessionChatInputToolbar', () => {
236237

237238
assert.deepStrictEqual({
238239
pullRequest: {
240+
label: pullRequestEntry?.label,
239241
className: pullRequestHover?.className,
240242
repository: pullRequestHover?.querySelector('.sessions-pr-hover-repository')?.textContent,
241243
title: pullRequestHover?.querySelector('.sessions-pr-hover-title')?.textContent,
242244
description: pullRequestHover?.querySelector('.sessions-pr-hover-description-content')?.textContent,
243245
branches: [...pullRequestHover?.querySelectorAll('.sessions-pr-hover-branch') ?? []].map(element => element.textContent),
246+
unresolvedLabel: unresolvedPullRequestEntry?.label,
247+
unresolvedAriaLabel: unresolvedPullRequestEntry?.ariaLabel,
248+
unresolvedTooltip: unresolvedPullRequestEntry?.tooltip,
244249
unresolvedHover: unresolvedPullRequestEntry?.pillHover,
245250
},
246251
issue: {
@@ -257,11 +262,15 @@ suite('SessionChatInputToolbar', () => {
257262
},
258263
}, {
259264
pullRequest: {
265+
label: 'Pull Request #332982: Restore rich pill hovers',
260266
className: 'sessions-pr-hover',
261267
repository: 'microsoft/vscode',
262268
title: 'Restore rich pill hovers',
263269
description: 'Provides detailed pull request context.',
264270
branches: ['main', 'feature/rich-hover'],
271+
unresolvedLabel: 'Pull Request #332982: Recorded pull request title',
272+
unresolvedAriaLabel: 'Open Pull Request #332982: Recorded pull request title',
273+
unresolvedTooltip: 'Pull Request #332982: Recorded pull request title\nhttps://github.com/microsoft/vscode/pull/332982',
265274
unresolvedHover: undefined,
266275
},
267276
issue: {

‎src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionInputPills.ts‎

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const artifactSectionOrder: readonly { readonly type: SessionArtifactType; reado
6767

6868
export interface IAgentHostSessionPillMetadata {
6969
readonly pullRequestUrls: readonly string[];
70+
readonly pullRequestTitles: ReadonlyMap<string, string>;
7071
readonly issueUrls: readonly string[];
7172
readonly issueTitles: ReadonlyMap<string, string>;
7273
readonly artifacts: readonly ISessionArtifact[];
@@ -112,16 +113,18 @@ function isPromotedArtifact(artifact: ISessionArtifact, type: SessionArtifactTyp
112113
export function getAgentHostSessionPillMetadata(meta: SessionSummaryMeta | undefined): IAgentHostSessionPillMetadata {
113114
const entries = readSessionArtifactsNewestFirst(meta);
114115
const github = readSessionGitHubState(meta);
115-
const artifactPullRequests = entries.filter(entry => isPromotedArtifact(entry, SessionArtifactType.PullRequest)).map(entry => entry.link);
116+
const artifactPullRequests = entries.filter(entry => isPromotedArtifact(entry, SessionArtifactType.PullRequest));
116117
const artifactIssues = entries.filter(entry => isPromotedArtifact(entry, SessionArtifactType.Issue));
117118
// Recorded pull requests lead discovered ones, as in the Agents Window.
118-
const pullRequestUrls = dedupeLinks(artifactPullRequests, getSessionRelatedPullRequestUrls(github));
119+
const pullRequestUrls = dedupeLinks(artifactPullRequests.map(entry => entry.link), getSessionRelatedPullRequestUrls(github));
120+
const pullRequestTitles = new Map(artifactPullRequests.map(entry => [linkKey(entry.link), entry.label]));
119121
const issueUrls = dedupeLinks(artifactIssues.map(entry => entry.link));
120122
const issueTitles = new Map(artifactIssues.map(entry => [linkKey(entry.link), entry.label]));
121123
const promotedLinks = new Set([...pullRequestUrls, ...issueUrls].map(linkKey));
122124
const remaining = entries.filter(entry => !entry.link || !promotedLinks.has(linkKey(entry.link)));
123125
return {
124126
pullRequestUrls,
127+
pullRequestTitles,
125128
issueUrls,
126129
issueTitles,
127130
artifacts: remaining.filter(entry => entry.isArtifact),
@@ -210,16 +213,21 @@ function parseUri(value: string | undefined): URI | undefined {
210213
function referenceLabel(link: string, kind: 'pullRequest' | 'issue', title?: string): string {
211214
const resource = parseUri(link);
212215
const number = resource ? githubReferenceNumber(resource, kind) : undefined;
213-
if (kind === 'pullRequest') {
214-
return number
215-
? localize('agentHostSessionPills.pullRequest.number', "Pull Request #{0}", number)
216-
: localize('agentHostSessionPills.pullRequest', "Pull Request");
217-
}
218216
if (title) {
217+
if (kind === 'pullRequest') {
218+
return number
219+
? localize('agentHostSessionPills.pullRequest.numberWithTitle', "Pull Request #{0}: {1}", number, title)
220+
: title;
221+
}
219222
return number
220223
? localize('agentHostSessionPills.issue.numberWithTitle', "Issue #{0}: {1}", number, title)
221224
: title;
222225
}
226+
if (kind === 'pullRequest') {
227+
return number
228+
? localize('agentHostSessionPills.pullRequest.number', "Pull Request #{0}", number)
229+
: localize('agentHostSessionPills.pullRequest', "Pull Request");
230+
}
223231
return number
224232
? localize('agentHostSessionPills.issue.number', "Issue #{0}", number)
225233
: localize('agentHostSessionPills.issue', "Issue");
@@ -353,7 +361,10 @@ export class AgentHostSessionInputPills extends Disposable {
353361
: new Set();
354362
});
355363

356-
const pullRequestSections = derived(this, reader => this._buildReferenceSections(metadata.read(reader).pullRequestUrls, 'pullRequest', gitHubState.read(reader)));
364+
const pullRequestSections = derived(this, reader => {
365+
const currentMetadata = metadata.read(reader);
366+
return this._buildReferenceSections(currentMetadata.pullRequestUrls, 'pullRequest', gitHubState.read(reader), currentMetadata.pullRequestTitles);
367+
});
357368
const pullRequestIcon = derived(this, reader => {
358369
const icons = getChatPillEntries(pullRequestSections.read(reader)).map(entry => entry.icon);
359370
return getHighestPriorityPullRequestIcon(icons) ?? computePullRequestIcon('open');
@@ -445,7 +456,7 @@ export class AgentHostSessionInputPills extends Disposable {
445456
run: () => this._clipboardService.writeText(resource.toString(true)),
446457
})],
447458
...getChatPillResourceLocation(resource, label),
448-
...(kind === 'issue' && title ? { tooltip: `${label}\n${resource.toString(true)}` } : {}),
459+
...(title ? { tooltip: `${label}\n${resource.toString(true)}` } : {}),
449460
open: () => this._openExternal(resource),
450461
};
451462
}).filter(isDefined);

‎src/vs/workbench/contrib/chat/test/browser/agentHost/agentHostSessionInputPills.test.ts‎

Lines changed: 91 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { mock, upcastPartial } from '../../../../../../base/test/common/mock.js'
1212
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
1313
import { IAgentHostConnectionsService } from '../../../../../../platform/agentHost/common/agentHostConnectionsService.js';
1414
import { IAgentConnection } from '../../../../../../platform/agentHost/common/agentService.js';
15+
import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js';
1516
import { ChangesetKind } from '../../../../../../platform/agentHost/common/changesetUri.js';
1617
import { IAgentSubscription } from '../../../../../../platform/agentHost/common/state/agentSubscription.js';
1718
import { ISessionArtifact, SessionArtifactType, withSessionArtifacts } from '../../../../../../platform/agentHost/common/sessionArtifacts.js';
@@ -66,6 +67,18 @@ class StaticAgentConnection extends mock<IAgentConnection>() {
6667
}
6768
}
6869

70+
class TestOpenerService extends mock<IOpenerService>() {
71+
readonly opened: { readonly resource: URI; readonly openExternal: boolean | undefined }[] = [];
72+
73+
override async open(resource: URI | string, options?: Parameters<IOpenerService['open']>[1]): Promise<boolean> {
74+
this.opened.push({
75+
resource: typeof resource === 'string' ? URI.parse(resource) : resource,
76+
openExternal: options && 'openExternal' in options ? options.openExternal : undefined,
77+
});
78+
return true;
79+
}
80+
}
81+
6982
suite('AgentHostSessionInputPills', () => {
7083
const store = ensureNoDisposablesAreLeakedInTestSuite();
7184

@@ -89,6 +102,7 @@ suite('AgentHostSessionInputPills', () => {
89102

90103
assert.deepStrictEqual({
91104
pullRequestUrls: metadata.pullRequestUrls,
105+
pullRequestTitles: [...metadata.pullRequestTitles],
92106
issueUrls: metadata.issueUrls,
93107
issueTitles: [...metadata.issueTitles],
94108
artifactIds: metadata.artifacts.map(artifact => artifact.id),
@@ -98,6 +112,7 @@ suite('AgentHostSessionInputPills', () => {
98112
'https://github.com/microsoft/vscode/pull/2',
99113
'https://github.com/microsoft/vscode/pull/1',
100114
],
115+
pullRequestTitles: [['https://github.com/microsoft/vscode/pull/2', 'Created PR']],
101116
issueUrls: ['https://github.com/microsoft/vscode/issues/3'],
102117
issueTitles: [['https://github.com/microsoft/vscode/issues/3', 'Created Issue']],
103118
artifactIds: ['website'],
@@ -122,6 +137,7 @@ suite('AgentHostSessionInputPills', () => {
122137

123138
assert.deepStrictEqual({
124139
pullRequestUrls: metadata.pullRequestUrls,
140+
pullRequestTitles: [...metadata.pullRequestTitles],
125141
issueUrls: metadata.issueUrls,
126142
issueTitles: [...metadata.issueTitles],
127143
artifactIds: metadata.artifacts.map(artifact => artifact.id),
@@ -131,6 +147,10 @@ suite('AgentHostSessionInputPills', () => {
131147
'https://github.com/microsoft/vscode/pull/2',
132148
'https://github.com/microsoft/vscode/pull/1',
133149
],
150+
pullRequestTitles: [
151+
['https://github.com/microsoft/vscode/pull/2', 'New PR'],
152+
['https://github.com/microsoft/vscode/pull/1', 'Old PR'],
153+
],
134154
issueUrls: [
135155
'https://github.com/microsoft/vscode/issues/2',
136156
'https://github.com/microsoft/vscode/issues/1',
@@ -144,23 +164,43 @@ suite('AgentHostSessionInputPills', () => {
144164
});
145165
});
146166

147-
test('renders recorded issue titles in editor and panel pills', () => {
167+
test('renders recorded GitHub titles in editor and panel pills', () => {
148168
const instantiationService = workbenchInstantiationService(undefined, store);
149169
const sessionResource = URI.parse('agent-host-copilot:/session');
150170
const backendSession = URI.parse('copilot:/session');
151171
const issueUrl = 'https://github.com/microsoft/vscode/issues/335383';
172+
const firstPullRequestUrl = 'https://github.com/microsoft/vscode/pull/335387';
173+
const secondPullRequestUrl = 'https://github.com/microsoft/vscode/pull/332982';
152174
const connection = new StaticAgentConnection(new Map<StateComponents, SessionState | ChangesetState>([
153175
[StateComponents.Session, {
154176
defaultChat: buildDefaultChatUri(backendSession),
155177
chats: [],
156-
_meta: withSessionArtifacts(undefined, [{
157-
id: 'issue',
158-
type: SessionArtifactType.Issue,
159-
label: 'Agent Window issue pill discards the recorded issue title',
160-
link: issueUrl,
161-
isGitHub: true,
162-
isArtifact: true,
163-
}]),
178+
_meta: withSessionArtifacts(undefined, [
179+
{
180+
id: 'issue',
181+
type: SessionArtifactType.Issue,
182+
label: 'Agent Window issue pill discards the recorded issue title',
183+
link: issueUrl,
184+
isGitHub: true,
185+
isArtifact: true,
186+
},
187+
{
188+
id: 'first-pr',
189+
type: SessionArtifactType.PullRequest,
190+
label: 'sessions: preserve recorded issue titles in pills',
191+
link: firstPullRequestUrl,
192+
isGitHub: true,
193+
isArtifact: true,
194+
},
195+
{
196+
id: 'second-pr',
197+
type: SessionArtifactType.PullRequest,
198+
label: 'Chat: unify Agent Host status pills across chat surfaces',
199+
link: secondPullRequestUrl,
200+
isGitHub: true,
201+
isArtifact: true,
202+
},
203+
]),
164204
} as unknown as SessionState],
165205
]));
166206
const persistentContent = document.createElement('div');
@@ -187,18 +227,22 @@ suite('AgentHostSessionInputPills', () => {
187227
});
188228
const visibility = store.add(instantiationService.createInstance(SessionChatPillVisibility));
189229
instantiationService.stub(ISessionChatPillVisibilityService, visibility);
230+
let dropdownLabels: readonly string[] = [];
231+
instantiationService.stub(IActionWidgetService, upcastPartial<IActionWidgetService>({
232+
isVisible: false,
233+
show: (_user, _supportsPreview, items) => {
234+
dropdownLabels = items.map(item => item.label ?? '');
235+
},
236+
hide: () => { },
237+
updateItems: () => { },
238+
focusItemById: () => { },
239+
}));
190240
const [clipboardService, configurationService, editorService] = instantiationService.invokeFunction(accessor => [
191241
accessor.get(IClipboardService),
192242
accessor.get(IConfigurationService),
193243
accessor.get(IEditorService),
194244
] as const);
195-
const opened: { readonly resource: URI; readonly openExternal: boolean | undefined }[] = [];
196-
const openerService = upcastPartial<IOpenerService>({
197-
open: async (resource, options) => {
198-
opened.push({ resource, openExternal: options?.openExternal });
199-
return true;
200-
},
201-
});
245+
const openerService = new TestOpenerService();
202246

203247
store.add(new AgentHostSessionInputPills(
204248
widget,
@@ -213,17 +257,37 @@ suite('AgentHostSessionInputPills', () => {
213257
visibility,
214258
));
215259

216-
const button = persistentContent.querySelector<HTMLElement>('.chat-dropdown-pill-button');
217-
button?.click();
260+
const buttons = [...persistentContent.querySelectorAll<HTMLElement>('.chat-dropdown-pill-button')];
261+
const [pullRequestButton, issueButton] = buttons;
262+
pullRequestButton?.click();
263+
issueButton?.click();
218264
assert.deepStrictEqual({
219-
label: button?.querySelector('.chat-pill-label')?.textContent,
220-
ariaLabel: button?.getAttribute('aria-label'),
221-
ariaDescription: button?.getAttribute('aria-description'),
222-
opened: opened.map(({ resource, openExternal }) => ({ resource: resource.toString(true), openExternal })),
265+
pullRequests: {
266+
label: pullRequestButton?.querySelector('.chat-pill-label')?.textContent,
267+
ariaLabel: pullRequestButton?.getAttribute('aria-label'),
268+
dropdownLabels,
269+
},
270+
issue: {
271+
label: issueButton?.querySelector('.chat-pill-label')?.textContent,
272+
ariaLabel: issueButton?.getAttribute('aria-label'),
273+
ariaDescription: issueButton?.getAttribute('aria-description'),
274+
},
275+
opened: openerService.opened.map(({ resource, openExternal }) => ({ resource: resource.toString(true), openExternal })),
223276
}, {
224-
label: 'Issue #335383: Agent Window issue pill discards the recorded issue title',
225-
ariaLabel: 'Open Issue #335383: Agent Window issue pill discards the recorded issue title',
226-
ariaDescription: issueUrl,
277+
pullRequests: {
278+
label: '2 Pull Requests',
279+
ariaLabel: 'Show 2 pull requests',
280+
dropdownLabels: [
281+
'Pull Requests',
282+
'Pull Request #332982: Chat: unify Agent Host status pills across chat surfaces',
283+
'Pull Request #335387: sessions: preserve recorded issue titles in pills',
284+
],
285+
},
286+
issue: {
287+
label: 'Issue #335383: Agent Window issue pill discards the recorded issue title',
288+
ariaLabel: 'Open Issue #335383: Agent Window issue pill discards the recorded issue title',
289+
ariaDescription: issueUrl,
290+
},
227291
opened: [{ resource: issueUrl, openExternal: true }],
228292
});
229293
});
@@ -575,13 +639,7 @@ suite('AgentHostSessionInputPills', () => {
575639
accessor.get(IConfigurationService),
576640
accessor.get(IEditorService),
577641
] as const);
578-
const opened: { readonly resource: URI; readonly openExternal: boolean | undefined }[] = [];
579-
const openerService = upcastPartial<IOpenerService>({
580-
open: async (resource, options) => {
581-
opened.push({ resource, openExternal: options?.openExternal });
582-
return true;
583-
},
584-
});
642+
const openerService = new TestOpenerService();
585643

586644
store.add(new AgentHostSessionInputPills(
587645
widget,
@@ -632,7 +690,7 @@ suite('AgentHostSessionInputPills', () => {
632690
multiple,
633691
filteredLabel,
634692
single,
635-
opened: opened.map(({ resource, openExternal }) => ({ resource: resource.toString(true), openExternal })),
693+
opened: openerService.opened.map(({ resource, openExternal }) => ({ resource: resource.toString(true), openExternal })),
636694
filteredOnly: persistentContent.querySelector('.chat-dropdown-pill-button'),
637695
canConfigure: persistentContent.querySelector('.chat-pills-row')?.classList.contains('empty'),
638696
}, {

0 commit comments

Comments
 (0)