Skip to content

Commit cdf4f2f

Browse files
feat: updates for Copilot CLI to include mode instructions (microsoft#303962)
* feat: updates for Copilot CLI to include mode instructions * Update src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/vs/workbench/contrib/chat/common/chatSessionsService.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updates * Updates * Updates * Updates --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 3535854 commit cdf4f2f

13 files changed

Lines changed: 303 additions & 13 deletions

src/vs/workbench/api/browser/mainThreadChatSessions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ export class ObservableChatSession extends Disposable implements IChatSession {
137137
variableData: variables ? { variables } : undefined,
138138
id: turn.id,
139139
modelId: turn.modelId,
140-
};
140+
modeInstructions: turn.modeInstructions ? revive(turn.modeInstructions) : undefined,
141+
} satisfies IChatSessionRequestHistoryItem;
141142
}
142143

143144
return {
@@ -326,6 +327,7 @@ export class ObservableChatSession extends Disposable implements IChatSession {
326327
command: request.command,
327328
variableData: undefined,
328329
modelId: request.modelId,
330+
modeInstructions: request.modeInstructions,
329331
};
330332
}
331333

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import { CallHierarchyItem } from '../../contrib/callHierarchy/common/callHierar
5858
import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, UserSelectedTools } from '../../contrib/chat/common/participants/chatAgents.js';
5959
import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/editing/chatCodeMapperService.js';
6060
import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js';
61-
import { IChatProgressHistoryResponseContent, IChatRequestVariableData } from '../../contrib/chat/common/model/chatModel.js';
61+
import { IChatProgressHistoryResponseContent, IChatRequestModeInstructions, IChatRequestVariableData } from '../../contrib/chat/common/model/chatModel.js';
6262
import { ChatResponseClearToPreviousToolInvocationReason, IChatContentInlineReference, IChatExternalEditsDto, IChatFollowup, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService/chatService.js';
6363
import { IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js';
6464
import { IChatRequestVariableValue } from '../../contrib/chat/common/attachments/chatVariables.js';
@@ -3584,6 +3584,7 @@ export type IChatSessionHistoryItemDto = {
35843584
command?: string;
35853585
variableData?: Dto<IChatRequestVariableData>;
35863586
modelId?: string;
3587+
modeInstructions?: Dto<IChatRequestModeInstructions>;
35873588
} | {
35883589
type: 'response';
35893590
parts: IChatProgressDto[];

src/vs/workbench/api/common/extHostChatAgents2.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
908908
}
909909

910910
const editedFileEvents = isProposedApiEnabled(extension, 'chatParticipantPrivate') ? h.request.editedFileEvents : undefined;
911-
const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents, h.request.requestId);
911+
const modeInstructions2 = isProposedApiEnabled(extension, 'chatParticipantPrivate') && h.request.modeInstructions ? typeConvert.ChatRequestModeInstructions.to(h.request.modeInstructions) : undefined;
912+
const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents, h.request.requestId, undefined, modeInstructions2);
912913
res.push(turn);
913914

914915
// RESPONSE turn

src/vs/workbench/api/common/extHostChatSessions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
692692
undefined,
693693
request.id,
694694
request.modelId,
695+
typeConvert.ChatRequestModeInstructions.to(request.modeInstructions),
695696
);
696697
}
697698

@@ -730,6 +731,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
730731
command: turn.command,
731732
variableData: variables.length > 0 ? { variables } : undefined,
732733
modelId: turn.modelId,
734+
modeInstructions: typeConvert.ChatRequestModeInstructions.from(turn.modeInstructions2),
733735
};
734736
}
735737

src/vs/workbench/api/common/extHostTypeConverters.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3610,13 +3610,33 @@ namespace ChatLanguageModelToolReferences {
36103610
}
36113611

36123612
export namespace ChatRequestModeInstructions {
3613-
export function to(mode: IChatRequestModeInstructions | undefined): vscode.ChatRequestModeInstructions | undefined {
3613+
export function to(mode: IChatRequestModeInstructions | Dto<IChatRequestModeInstructions> | undefined): vscode.ChatRequestModeInstructions | undefined {
36143614
if (mode) {
36153615
return {
36163616
uri: URI.revive(mode.uri),
36173617
name: mode.name,
36183618
content: mode.content,
3619-
toolReferences: ChatLanguageModelToolReferences.to(mode.toolReferences),
3619+
toolReferences: ChatLanguageModelToolReferences.to(revive(mode.toolReferences)),
3620+
metadata: mode.metadata,
3621+
isBuiltin: mode.isBuiltin,
3622+
};
3623+
}
3624+
return undefined;
3625+
}
3626+
3627+
export function from(mode: vscode.ChatRequestModeInstructions | undefined): IChatRequestModeInstructions | undefined {
3628+
if (mode) {
3629+
return {
3630+
uri: mode.uri,
3631+
name: mode.name,
3632+
content: mode.content,
3633+
toolReferences: mode.toolReferences?.map(ref => ({
3634+
kind: 'tool' as const,
3635+
id: ref.name,
3636+
name: ref.name,
3637+
value: undefined,
3638+
range: ref.range ? { start: ref.range[0], endExclusive: ref.range[1] } : undefined,
3639+
})) ?? [],
36203640
metadata: mode.metadata,
36213641
isBuiltin: mode.isBuiltin,
36223642
};

src/vs/workbench/api/common/extHostTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3530,6 +3530,7 @@ export class ChatRequestTurn implements vscode.ChatRequestTurn2 {
35303530
readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[],
35313531
readonly id?: string,
35323532
readonly modelId?: string,
3533+
readonly modeInstructions2?: vscode.ChatRequestModeInstructions,
35333534
) { }
35343535
}
35353536

src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type * as vscode from 'vscode';
99
import { CancellationToken } from '../../../../base/common/cancellation.js';
1010
import { Event } from '../../../../base/common/event.js';
1111
import { DisposableStore } from '../../../../base/common/lifecycle.js';
12+
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
1213
import { URI } from '../../../../base/common/uri.js';
1314
import { asSinonMethodStub } from '../../../../base/test/common/sinonUtils.js';
1415
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
@@ -177,6 +178,72 @@ suite('ObservableChatSession', function () {
177178
assert.ok(session.requestHandler);
178179
});
179180

181+
test('initialization revives modeInstructions in history', async function () {
182+
const sessionContent = createSessionContent({
183+
history: [
184+
{
185+
type: 'request',
186+
prompt: 'Hello',
187+
participant: 'test',
188+
modeInstructions: {
189+
uri: { $mid: MarshalledId.Uri, scheme: 'file', path: '/custom-agent' },
190+
name: 'my-agent',
191+
content: 'instructions',
192+
toolReferences: [],
193+
isBuiltin: false,
194+
},
195+
},
196+
],
197+
});
198+
199+
const session = disposables.add(await createInitializedSession(sessionContent));
200+
const requestItem = session.history[0];
201+
assert.strictEqual(requestItem.type, 'request');
202+
if (requestItem.type === 'request') {
203+
assert.ok(requestItem.modeInstructions);
204+
assert.ok(URI.isUri(requestItem.modeInstructions.uri));
205+
assert.strictEqual(requestItem.modeInstructions.name, 'my-agent');
206+
assert.strictEqual(requestItem.modeInstructions.isBuiltin, false);
207+
}
208+
});
209+
210+
test('toRequestDto passes modeInstructions through', async function () {
211+
const session = disposables.add(await createInitializedSession(createSessionContent({ hasForkHandler: true })));
212+
assert.ok(session.forkSession);
213+
214+
const modeInstructions = {
215+
uri: URI.parse('file:///custom-agent'),
216+
name: 'my-agent',
217+
content: 'agent instructions',
218+
toolReferences: [],
219+
isBuiltin: false,
220+
};
221+
const request: IChatSessionRequestHistoryItem = {
222+
type: 'request',
223+
id: 'req-1',
224+
prompt: 'Hello with mode',
225+
participant: 'participant',
226+
modeInstructions,
227+
};
228+
229+
const forkedItem = {
230+
resource: URI.file('/tmp/forked.md'),
231+
label: 'Forked',
232+
changes: [],
233+
timing: {
234+
created: 123,
235+
lastRequestStarted: 234,
236+
lastRequestEnded: 345,
237+
},
238+
};
239+
asSinonMethodStub(proxy.$forkChatSession).resolves(forkedItem);
240+
await session.forkSession?.(request, CancellationToken.None);
241+
242+
const call = asSinonMethodStub(proxy.$forkChatSession).firstCall;
243+
const sentDto = call.args[2] as IChatSessionRequestHistoryItemDto;
244+
assert.deepStrictEqual(sentDto.modeInstructions, modeInstructions);
245+
});
246+
180247
test('initialization sets forkSession and revives forked items', async function () {
181248
const session = disposables.add(await createInitializedSession(createSessionContent({ hasForkHandler: true })));
182249
assert.ok(session.forkSession);
@@ -208,6 +275,7 @@ suite('ObservableChatSession', function () {
208275
command: undefined,
209276
variableData: undefined,
210277
modelId: undefined,
278+
modeInstructions: undefined,
211279
};
212280
const result = await session.forkSession?.(request, CancellationToken.None);
213281

src/vs/workbench/api/test/common/extHostTypeConverters.test.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import assert from 'assert';
77
import { URI, UriComponents } from '../../../../base/common/uri.js';
88
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
99
import { IconPathDto } from '../../common/extHost.protocol.js';
10-
import { IconPath } from '../../common/extHostTypeConverters.js';
10+
import { ChatRequestModeInstructions, IconPath } from '../../common/extHostTypeConverters.js';
1111
import { ThemeColor, ThemeIcon } from '../../common/extHostTypes.js';
12+
import { IChatRequestModeInstructions } from '../../../contrib/chat/common/model/chatModel.js';
13+
import { Dto } from '../../../services/extensions/common/proxyIdentifier.js';
1214

1315
suite('extHostTypeConverters', function () {
1416
ensureNoDisposablesAreLeakedInTestSuite();
@@ -120,4 +122,125 @@ suite('extHostTypeConverters', function () {
120122
});
121123
});
122124
});
125+
126+
suite('ChatRequestModeInstructions', function () {
127+
test('to returns undefined for undefined input', function () {
128+
assert.strictEqual(ChatRequestModeInstructions.to(undefined), undefined);
129+
});
130+
131+
test('from returns undefined for undefined input', function () {
132+
assert.strictEqual(ChatRequestModeInstructions.from(undefined), undefined);
133+
});
134+
135+
test('to converts IChatRequestModeInstructions to API type', function () {
136+
const uri = URI.parse('file:///custom-agent');
137+
const input: IChatRequestModeInstructions = {
138+
uri,
139+
name: 'test-mode',
140+
content: 'test content',
141+
toolReferences: [{
142+
kind: 'tool',
143+
id: 'tool1',
144+
name: 'tool1',
145+
value: undefined,
146+
range: { start: 0, endExclusive: 5 },
147+
}],
148+
metadata: { key: 'value' },
149+
isBuiltin: false,
150+
};
151+
152+
const result = ChatRequestModeInstructions.to(input)!;
153+
assert.deepStrictEqual(result, {
154+
uri,
155+
name: 'test-mode',
156+
content: 'test content',
157+
toolReferences: [{ name: 'tool1', range: [0, 5] }],
158+
metadata: { key: 'value' },
159+
isBuiltin: false,
160+
});
161+
});
162+
163+
test('to handles Dto with UriComponents', function () {
164+
const input: Dto<IChatRequestModeInstructions> = {
165+
uri: { scheme: 'file', path: '/custom-agent' } as UriComponents,
166+
name: 'test-mode',
167+
content: 'test content',
168+
toolReferences: [],
169+
metadata: undefined,
170+
isBuiltin: true,
171+
};
172+
173+
const result = ChatRequestModeInstructions.to(input)!;
174+
assert.ok(URI.isUri(result.uri));
175+
assert.strictEqual(result.name, 'test-mode');
176+
assert.strictEqual(result.isBuiltin, true);
177+
assert.deepStrictEqual(result.toolReferences, []);
178+
});
179+
180+
test('from converts API type to IChatRequestModeInstructions', function () {
181+
const uri = URI.parse('file:///custom-agent');
182+
const input = {
183+
uri,
184+
name: 'test-mode',
185+
content: 'test content',
186+
toolReferences: [{ name: 'tool1', range: [0, 5] as [number, number] }],
187+
metadata: { key: 'value' },
188+
isBuiltin: false,
189+
};
190+
191+
const result = ChatRequestModeInstructions.from(input)!;
192+
assert.deepStrictEqual(result, {
193+
uri,
194+
name: 'test-mode',
195+
content: 'test content',
196+
toolReferences: [{
197+
kind: 'tool',
198+
id: 'tool1',
199+
name: 'tool1',
200+
value: undefined,
201+
range: { start: 0, endExclusive: 5 },
202+
}],
203+
metadata: { key: 'value' },
204+
isBuiltin: false,
205+
});
206+
});
207+
208+
test('from handles missing toolReferences', function () {
209+
const input = {
210+
name: 'test-mode',
211+
content: 'test content',
212+
};
213+
214+
const result = ChatRequestModeInstructions.from(input)!;
215+
assert.deepStrictEqual(result.toolReferences, []);
216+
});
217+
218+
test('roundtrip from -> to preserves data', function () {
219+
const uri = URI.parse('file:///custom-agent');
220+
const apiInput = {
221+
uri,
222+
name: 'roundtrip-mode',
223+
content: 'roundtrip content',
224+
toolReferences: [
225+
{ name: 'tool1' },
226+
{ name: 'tool2', range: [10, 20] as [number, number] },
227+
],
228+
metadata: { flag: true },
229+
isBuiltin: false,
230+
};
231+
232+
const internal = ChatRequestModeInstructions.from(apiInput)!;
233+
const backToApi = ChatRequestModeInstructions.to(internal)!;
234+
235+
assert.strictEqual(backToApi.name, apiInput.name);
236+
assert.strictEqual(backToApi.content, apiInput.content);
237+
assert.strictEqual(backToApi.isBuiltin, apiInput.isBuiltin);
238+
assert.strictEqual(backToApi.uri?.toString(), uri.toString());
239+
assert.strictEqual(backToApi.toolReferences?.length, 2);
240+
assert.strictEqual(backToApi.toolReferences?.[0].name, 'tool1');
241+
assert.strictEqual(backToApi.toolReferences?.[0].range, undefined);
242+
assert.strictEqual(backToApi.toolReferences?.[1].name, 'tool2');
243+
assert.deepStrictEqual(backToApi.toolReferences?.[1].range, [10, 20]);
244+
});
245+
});
123246
});

src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsEx
221221
description: localize('chatSessionsExtPoint.autoAttachReferences', 'Whether to automatically attach instruction files to chat requests for this session type.'),
222222
type: 'boolean',
223223
default: false
224+
},
225+
useRequestToPopulateBuiltInPickers: {
226+
description: localize('chatSessionsExtPoint.useRequestToPopulateBuiltInPickers', 'Whether to use ChatRequestTurn2 to populate built-in pickers such as the Agent and Model pickers.'),
227+
type: 'boolean',
228+
default: false
224229
}
225230
},
226231
required: ['type', 'name', 'displayName', 'description'],

src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ import { WorkspacePickerActionItem } from './workspacePickerActionItem.js';
129129
import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js';
130130
import { Target } from '../../../common/promptSyntax/promptTypes.js';
131131
import { EnhancedModelPickerActionItem } from './modelPickerActionItem2.js';
132+
import { findLast } from '../../../../../../base/common/arraysFind.js';
132133
import { ConfigureToolsAction } from '../../actions/chatToolActions.js';
133134

134135
const $ = dom.$;
@@ -1176,6 +1177,14 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
11761177
* that was last used - providing continuity.
11771178
*/
11781179
private preselectModelFromSessionHistory(): void {
1180+
const sessionType = this.getCurrentSessionType();
1181+
if (!sessionType) {
1182+
return;
1183+
}
1184+
const contribution = this.chatSessionsService.getChatSessionContribution(sessionType);
1185+
if (contribution?.useRequestToPopulateBuiltInPickers) {
1186+
return;
1187+
}
11791188
const sessionResource = this._widget?.viewModel?.model.sessionResource;
11801189
const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined;
11811190
const requiresCustomModels = ctx && this.chatSessionsService.requiresCustomModelsForSessionType(getChatSessionType(ctx.chatSessionResource));
@@ -1197,6 +1206,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
11971206
}
11981207
}
11991208

1209+
const modeInfo = findLast(requests, req => !!req.modeInfo)?.modeInfo;
1210+
if (modeInfo && modeInfo.modeInstructions?.uri) {
1211+
this.setChatMode(modeInfo.modeInstructions.uri.toString());
1212+
}
1213+
12001214
if (!lastModelId) {
12011215
return;
12021216
}
@@ -1623,8 +1637,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
16231637

16241638
// Handle agent option from session - set initial mode
16251639
if (customAgentTarget) {
1640+
const contribution = ctx && this.chatSessionsService.getChatSessionContribution(getChatSessionType(ctx.chatSessionResource));
16261641
const agentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, agentOptionId);
1627-
if (typeof agentOption !== 'undefined') {
1642+
if (typeof agentOption !== 'undefined' && !contribution?.useRequestToPopulateBuiltInPickers) {
16281643
const agentId = (typeof agentOption === 'string' ? agentOption : agentOption.id) || ChatMode.Agent.id;
16291644
const currentMode = this._currentModeObservable.get();
16301645
const isDefaultAgent = agentId === ChatMode.Agent.id;

0 commit comments

Comments
 (0)