Skip to content

Commit f1194f7

Browse files
mjbvzjoshspicer
andauthored
Support for resource based session API changes (#8027)
* Initial support for new resource based chat sessions * Update tests * Bump proposal version --------- Co-authored-by: Josh Spicer <23246594+joshspicer@users.noreply.github.com>
1 parent d3bec0d commit f1194f7

File tree

7 files changed

+99
-65
lines changed

7 files changed

+99
-65
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"activeComment",
1515
"chatParticipantAdditions",
1616
"chatParticipantPrivate",
17-
"chatSessionsProvider@2",
17+
"chatSessionsProvider@3",
1818
"codiconDecoration",
1919
"codeActionRanges",
2020
"commentingRangeHint",
@@ -4364,4 +4364,4 @@
43644364
"string_decoder": "^1.3.0"
43654365
},
43664366
"license": "MIT"
4367-
}
4367+
}

src/@types/vscode.proposed.chatParticipantAdditions.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ declare module 'vscode' {
652652
}
653653

654654
export interface ChatRequestModeInstructions {
655+
readonly name: string;
655656
readonly content: string;
656657
readonly toolReferences?: readonly ChatLanguageModelToolReference[];
657658
readonly metadata?: Record<string, boolean | string | number>;

src/@types/vscode.proposed.chatSessionsProvider.d.ts

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
// version: 2
6+
// version: 3
77

88
declare module 'vscode' {
99
/**
@@ -35,6 +35,14 @@ declare module 'vscode' {
3535
*/
3636
readonly onDidChangeChatSessionItems: Event<void>;
3737

38+
/**
39+
* Provides a list of chat sessions.
40+
*/
41+
// TODO: Do we need a flag to try auth if needed?
42+
provideChatSessionItems(token: CancellationToken): ProviderResult<ChatSessionItem[]>;
43+
44+
// #region Unstable parts of API
45+
3846
/**
3947
* Event that the provider can fire to signal that the current (original) chat session should be replaced with a new (modified) chat session.
4048
* The UI can use this information to gracefully migrate the user to the new session.
@@ -61,27 +69,16 @@ declare module 'vscode' {
6169
metadata?: any;
6270
}, token: CancellationToken): ProviderResult<ChatSessionItem>;
6371

64-
/**
65-
* Provides a list of chat sessions.
66-
*/
67-
// TODO: Do we need a flag to try auth if needed?
68-
provideChatSessionItems(token: CancellationToken): ProviderResult<ChatSessionItem[]>;
72+
// #endregion
6973
}
7074

7175
export interface ChatSessionItem {
72-
/**
73-
* Unique identifier for the chat session.
74-
*
75-
* @deprecated Will be replaced by `resource`
76-
*/
77-
id: string;
78-
7976
/**
8077
* The resource associated with the chat session.
8178
*
8279
* This is uniquely identifies the chat session and is used to open the chat session.
8380
*/
84-
resource: Uri | undefined;
81+
resource: Uri;
8582

8683
/**
8784
* Human readable name of the session shown in the UI
@@ -149,9 +146,12 @@ declare module 'vscode' {
149146
readonly history: ReadonlyArray<ChatRequestTurn | ChatResponseTurn2>;
150147

151148
/**
152-
* Options configured for this session.
149+
* Options configured for this session as key-value pairs.
150+
* Keys correspond to option group IDs (e.g., 'models', 'subagents')
151+
* and values are the selected option item IDs.
152+
* TODO: Strongly type the keys
153153
*/
154-
readonly options?: { model?: LanguageModelChatInformation };
154+
readonly options?: Record<string, string>;
155155

156156
/**
157157
* Callback invoked by the editor for a currently running response. This allows the session to push items for the
@@ -172,22 +172,28 @@ declare module 'vscode' {
172172
readonly requestHandler: ChatRequestHandler | undefined;
173173
}
174174

175+
/**
176+
* Provides the content for a chat session rendered using the native chat UI.
177+
*/
175178
export interface ChatSessionContentProvider {
176179
/**
177-
* Resolves a chat session into a full `ChatSession` object.
180+
* Provides the chat session content for a given uri.
178181
*
179-
* @param sessionId The id of the chat session to open.
182+
* The returned {@linkcode ChatSession} is used to populate the history of the chat UI.
183+
*
184+
* @param resource The URI of the chat session to resolve.
180185
* @param token A cancellation token that can be used to cancel the operation.
186+
*
187+
* @return The {@link ChatSession chat session} associated with the given URI.
181188
*/
182-
provideChatSessionContent(sessionId: string, token: CancellationToken): Thenable<ChatSession> | ChatSession;
189+
provideChatSessionContent(resource: Uri, token: CancellationToken): Thenable<ChatSession> | ChatSession;
183190

184191
/**
185-
*
186-
* @param sessionId Identifier of the chat session being updated.
192+
* @param resource Identifier of the chat session being updated.
187193
* @param updates Collection of option identifiers and their new values. Only the options that changed are included.
188194
* @param token A cancellation token that can be used to cancel the notification if the session is disposed.
189195
*/
190-
provideHandleOptionsChange?(sessionId: string, updates: ReadonlyArray<ChatSessionOptionUpdate>, token: CancellationToken): void;
196+
provideHandleOptionsChange?(resource: Uri, updates: ReadonlyArray<ChatSessionOptionUpdate>, token: CancellationToken): void;
191197

192198
/**
193199
* Called as soon as you register (call me once)
@@ -224,12 +230,12 @@ declare module 'vscode' {
224230
/**
225231
* Registers a new {@link ChatSessionContentProvider chat session content provider}.
226232
*
227-
* @param chatSessionType A unique identifier for the chat session type. This is used to differentiate between different chat session providers.
233+
* @param scheme The uri-scheme to register for. This must be unique.
228234
* @param provider The provider to register.
229235
*
230236
* @returns A disposable that unregisters the provider when disposed.
231237
*/
232-
export function registerChatSessionContentProvider(chatSessionType: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable;
238+
export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable;
233239
}
234240

235241
export interface ChatContext {
@@ -252,31 +258,51 @@ declare module 'vscode' {
252258
supportsInterruptions?: boolean;
253259
}
254260

255-
export interface ChatSessionProviderOptions {
261+
/**
262+
* Represents a single selectable item within a provider option group.
263+
*/
264+
export interface ChatSessionProviderOptionItem {
265+
/**
266+
* Unique identifier for the option item.
267+
*/
268+
readonly id: string;
269+
256270
/**
257-
* Set of available models.
271+
* Human-readable name displayed in the UI.
258272
*/
259-
models?: LanguageModelChatInformation[];
273+
readonly name: string;
260274
}
261275

262276
/**
263-
* @deprecated
277+
* Represents a group of related provider options (e.g., models, sub-agents).
264278
*/
265-
export interface ChatSessionShowOptions {
279+
export interface ChatSessionProviderOptionGroup {
266280
/**
267-
* The editor view column to show the chat session in.
268-
*
269-
* If not provided, the chat session will be shown in the chat panel instead.
281+
* Unique identifier for the option group (e.g., "models", "subagents").
270282
*/
271-
readonly viewColumn?: ViewColumn;
283+
readonly id: string;
284+
285+
/**
286+
* Human-readable name for the option group.
287+
*/
288+
readonly name: string;
289+
290+
/**
291+
* Optional description providing context about this option group.
292+
*/
293+
readonly description?: string;
294+
295+
/**
296+
* The selectable items within this option group.
297+
*/
298+
readonly items: ChatSessionProviderOptionItem[];
272299
}
273300

274-
export namespace window {
301+
export interface ChatSessionProviderOptions {
275302
/**
276-
* Shows a chat session in the panel or editor.
277-
*
278-
* @deprecated
303+
* Provider-defined option groups (0-2 groups supported).
304+
* Examples: models picker, sub-agents picker, etc.
279305
*/
280-
export function showChatSession(chatSessionType: string, sessionId: string, options: ChatSessionShowOptions): Thenable<void>;
306+
optionGroups?: ChatSessionProviderOptionGroup[];
281307
}
282308
}

src/extension.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,12 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
447447

448448
const provider = new class implements vscode.ChatSessionContentProvider, vscode.ChatSessionItemProvider {
449449
label = vscode.l10n.t('GitHub Copilot Coding Agent');
450-
provideChatSessionItems = async (token) => {
450+
async provideChatSessionItems(token: vscode.CancellationToken) {
451451
return await copilotRemoteAgentManager.provideChatSessions(token);
452-
};
453-
provideChatSessionContent = async (id, token) => {
454-
return await copilotRemoteAgentManager.provideChatSessionContent(id, token);
455-
};
452+
}
453+
async provideChatSessionContent(resource: vscode.Uri, token: vscode.CancellationToken) {
454+
return await copilotRemoteAgentManager.provideChatSessionContent(resource, token);
455+
}
456456
onDidChangeChatSessionItems = copilotRemoteAgentManager.onDidChangeChatSessions;
457457
onDidCommitChatSessionItem = copilotRemoteAgentManager.onDidCommitChatSession;
458458
}();

src/github/copilotRemoteAgent.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as pathLib from 'path';
7+
import { URI } from '@vscode/prompt-tsx/dist/base/util/vs/common/uri';
78
import * as marked from 'marked';
89
import vscode, { ChatPromptReference, ChatSessionItem } from 'vscode';
910
import { copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common';
@@ -56,12 +57,14 @@ export namespace SessionIdForPr {
5657

5758
const prefix = 'pull-session-by-index';
5859

59-
export function getId(prNumber: number, sessionIndex: number): string {
60-
return `${prefix}-${prNumber}-${sessionIndex}`;
60+
export function getResource(prNumber: number, sessionIndex: number): vscode.Uri {
61+
return vscode.Uri.from({
62+
scheme: COPILOT_SWE_AGENT, path: `/${prefix}-${prNumber}-${sessionIndex}`,
63+
});
6164
}
6265

63-
export function parse(id: string): { prNumber: number; sessionIndex: number } | undefined {
64-
const match = id.match(new RegExp(`^${prefix}-(\\d+)-(\\d+)$`));
66+
export function parse(resource: vscode.Uri): { prNumber: number; sessionIndex: number } | undefined {
67+
const match = resource.path.match(new RegExp(`^/${prefix}-(\\d+)-(\\d+)$`));
6568
if (match) {
6669
return {
6770
prNumber: parseInt(match[1], 10),
@@ -141,7 +144,7 @@ export class CopilotRemoteAgentManager extends Disposable {
141144
const card = new vscode.ChatResponsePullRequestPart(uri, pullRequest.title, plaintextBody, pullRequest.author.specialDisplayName ?? pullRequest.author.login, `#${pullRequest.number}`);
142145
stream.push(card);
143146
stream.markdown(vscode.l10n.t('GitHub Copilot coding agent has begun working on your request. Follow its progress in the associated chat and pull request.'));
144-
vscode.window.showChatSession(COPILOT_SWE_AGENT, String(number), { viewColumn: vscode.ViewColumn.Active });
147+
vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + number }), { viewColumn: vscode.ViewColumn.Active });
145148
break;
146149
default:
147150
stream.warning(`Unknown confirmation step: ${data.step}\n\n`);
@@ -167,7 +170,10 @@ export class CopilotRemoteAgentManager extends Disposable {
167170
return {};
168171
}
169172
// Tell UI to the new chat session
170-
const modified: vscode.ChatSessionItem = { id: String(number), label: `Pull Request ${number}` } as unknown as vscode.ChatSessionItem;
173+
const modified: vscode.ChatSessionItem = {
174+
resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + number }),
175+
label: `Pull Request ${number}`,
176+
};
171177
this._onDidCommitChatSession.fire({ original: context.chatSessionContext.chatSessionItem, modified });
172178
} else if (context.chatSessionContext) {
173179
/* Follow up to an existing coding agent session */
@@ -185,9 +191,9 @@ export class CopilotRemoteAgentManager extends Disposable {
185191

186192
stream.progress(vscode.l10n.t('Preparing'));
187193

188-
const pullRequest = await this.findPullRequestById(parseInt(context.chatSessionContext.chatSessionItem.id, 10), true);
194+
const pullRequest = await this.findPullRequestById(parseInt(context.chatSessionContext.chatSessionItem.resource.path.slice(1), 10), true);
189195
if (!pullRequest) {
190-
stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', context.chatSessionContext.chatSessionItem.id));
196+
stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', context.chatSessionContext.chatSessionItem.resource.toString));
191197
return {};
192198
}
193199

@@ -705,7 +711,7 @@ export class CopilotRemoteAgentManager extends Disposable {
705711
} else {
706712
await this.provideChatSessions(new vscode.CancellationTokenSource().token);
707713
if (pr) {
708-
vscode.window.showChatSession(COPILOT_SWE_AGENT, `${pr.number}`, {});
714+
vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pr.number }));
709715
}
710716
}
711717

@@ -1060,7 +1066,7 @@ export class CopilotRemoteAgentManager extends Disposable {
10601066
}
10611067
const description = new vscode.MarkdownString(`[${repoInfo}#${pullRequest.number}](${uri.toString()} "${prLinkTitle}")`); // pullRequest.base.ref === defaultBranch ? `PR #${pullRequest.number}`: `PR #${pullRequest.number} → ${pullRequest.base.ref}`;
10621068
const chatSession: ChatSessionWithPR = {
1063-
id: `${pullRequest.number}`,
1069+
resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pullRequest.number }),
10641070
label: pullRequest.title || `Session ${pullRequest.number}`,
10651071
iconPath: this.getIconForSession(status),
10661072
pullRequest: pullRequest,
@@ -1074,7 +1080,7 @@ export class CopilotRemoteAgentManager extends Disposable {
10741080
insertions: pullRequest.item.additions,
10751081
deletions: pullRequest.item.deletions
10761082
} : undefined
1077-
} as unknown as ChatSessionWithPR;
1083+
};
10781084
return chatSession;
10791085
}));
10801086
} catch (error) {
@@ -1083,7 +1089,7 @@ export class CopilotRemoteAgentManager extends Disposable {
10831089
return [];
10841090
}
10851091

1086-
public async provideChatSessionContent(id: string, token: vscode.CancellationToken): Promise<vscode.ChatSession> {
1092+
public async provideChatSessionContent(resource: URI, token: vscode.CancellationToken): Promise<vscode.ChatSession> {
10871093
try {
10881094
const capi = await this.copilotApi;
10891095
if (!capi || token.isCancellationRequested) {
@@ -1095,16 +1101,16 @@ export class CopilotRemoteAgentManager extends Disposable {
10951101
let pullRequestNumber: number | undefined;
10961102
let sessionIndex: number | undefined;
10971103

1098-
const indexedSessionId = SessionIdForPr.parse(id);
1104+
const indexedSessionId = SessionIdForPr.parse(resource);
10991105
if (indexedSessionId) {
11001106
pullRequestNumber = indexedSessionId.prNumber;
11011107
sessionIndex = indexedSessionId.sessionIndex;
11021108
}
11031109

11041110
if (typeof pullRequestNumber === 'undefined') {
1105-
pullRequestNumber = parseInt(id);
1111+
pullRequestNumber = parseInt(resource.path.slice(1));
11061112
if (isNaN(pullRequestNumber)) {
1107-
Logger.error(`Invalid pull request number: ${id}`, CopilotRemoteAgentManager.ID);
1113+
Logger.error(`Invalid pull request number: ${resource}`, CopilotRemoteAgentManager.ID);
11081114
return this.createEmptySession();
11091115
}
11101116
}

src/github/pullRequestOverview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
509509

510510
private async openSessionLog(message: IRequestMessage<{ link: SessionLinkInfo }>): Promise<void> {
511511
try {
512-
return vscode.window.showChatSession(COPILOT_SWE_AGENT, SessionIdForPr.getId(this._item.number, message.args.link.sessionIndex), {});
512+
return vscode.commands.executeCommand('vscode.open', COPILOT_SWE_AGENT, SessionIdForPr.getResource(this._item.number, message.args.link.sessionIndex));
513513
} catch (e) {
514514
Logger.error(`Open session log view failed: ${formatError(e)}`, PullRequestOverviewPanel.ID);
515515
}

src/test/github/copilotRemoteAgent.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { default as assert } from 'assert';
77
import { SinonSandbox, createSandbox } from 'sinon';
88
import * as vscode from 'vscode';
9-
import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent';
9+
import { CopilotRemoteAgentManager, SessionIdForPr } from '../../github/copilotRemoteAgent';
1010
import { MockCommandRegistry } from '../mocks/mockCommandRegistry';
1111
import { MockTelemetry } from '../mocks/mockTelemetry';
1212
import { CredentialStore } from '../../github/credentials';
@@ -21,6 +21,7 @@ import { ReposManagerState } from '../../github/folderRepositoryManager';
2121
import { GitApiImpl } from '../../api/api1';
2222
import { MockPrsTreeModel } from '../mocks/mockPRsTreeModel';
2323
import { PrsTreeModel } from '../../view/prsTreeModel';
24+
import { COPILOT_SWE_AGENT } from '../../common/copilot';
2425

2526
const telemetry = new MockTelemetry();
2627
const protocol = new Protocol('https://github.com/github/test.git');
@@ -271,7 +272,7 @@ describe('CopilotRemoteAgentManager', function () {
271272
it('should return empty session when copilot API is not available', async function () {
272273
const token = new vscode.CancellationTokenSource().token;
273274

274-
const result = await manager.provideChatSessionContent('123', token);
275+
const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), token);
275276

276277
assert.strictEqual(Array.isArray(result.history), true);
277278
assert.strictEqual(result.history.length, 0);
@@ -282,7 +283,7 @@ describe('CopilotRemoteAgentManager', function () {
282283
const tokenSource = new vscode.CancellationTokenSource();
283284
tokenSource.cancel();
284285

285-
const result = await manager.provideChatSessionContent('123', tokenSource.token);
286+
const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), tokenSource.token);
286287

287288
assert.strictEqual(Array.isArray(result.history), true);
288289
assert.strictEqual(result.history.length, 0);
@@ -291,7 +292,7 @@ describe('CopilotRemoteAgentManager', function () {
291292
it('should return empty session for invalid PR number', async function () {
292293
const token = new vscode.CancellationTokenSource().token;
293294

294-
const result = await manager.provideChatSessionContent('invalid', token);
295+
const result = await manager.provideChatSessionContent(vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/invalid' }), token);
295296

296297
assert.strictEqual(Array.isArray(result.history), true);
297298
assert.strictEqual(result.history.length, 0);

0 commit comments

Comments
 (0)