Skip to content

Commit 526f0da

Browse files
authored
Pass chat references as attachments to @github/copilot sdk (#1456)
* Pass chat references as attachments to @github/copilot sdk * fixes
1 parent b67707e commit 526f0da

4 files changed

Lines changed: 111 additions & 85 deletions

File tree

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4098,8 +4098,7 @@
40984098
"description": "The Claude Code Agent works on your local machine",
40994099
"when": "config.github.copilot.chat.advanced.claudeCode.enabled",
41004100
"capabilities": {
4101-
"supportsFileAttachments": true,
4102-
"supportsToolAttachments": false
4101+
"supportsFileAttachments": true
41034102
},
41044103
"commands": [
41054104
{
@@ -4136,8 +4135,7 @@
41364135
"when": "config.github.copilot.chat.advanced.copilotCLI.enabled",
41374136
"capabilities": {
41384137
"supportsFileAttachments": true,
4139-
"supportsProblemAttachments": true,
4140-
"supportsToolAttachments": false
4138+
"supportsProblemAttachments": true
41414139
}
41424140
},
41434141
{

src/extension/agents/copilotcli/node/copilotcliAgentManager.ts

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

6-
import type { AgentOptions, ModelProvider, Session, SessionEvent } from '@github/copilot/sdk';
6+
import type { AgentOptions, Attachment, ModelProvider, Session, SessionEvent } from '@github/copilot/sdk';
7+
import * as fs from 'fs/promises';
78
import type * as vscode from 'vscode';
89
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
910
import { IEnvService } from '../../../../platform/env/common/envService';
1011
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
1112
import { ILogService } from '../../../../platform/log/common/logService';
1213
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
14+
import { isLocation } from '../../../../util/common/types';
1315
import { CancellationToken } from '../../../../util/vs/base/common/cancellation';
1416
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
17+
import * as path from '../../../../util/vs/base/common/path';
18+
import { URI } from '../../../../util/vs/base/common/uri';
1519
import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation';
16-
import { ChatResponseThinkingProgressPart, LanguageModelTextPart } from '../../../../vscodeTypes';
20+
import { ChatReferenceDiagnostic, ChatResponseThinkingProgressPart, LanguageModelTextPart } from '../../../../vscodeTypes';
1721
import { ToolName } from '../../../tools/common/toolNames';
1822
import { IToolsService } from '../../../tools/common/toolsService';
1923
import { ICopilotCLISessionService } from './copilotcliSessionService';
@@ -48,13 +52,14 @@ export class CopilotCLIAgentManager extends Disposable {
4852
const sessionIdForLog = copilotcliSessionId ?? 'new';
4953
this.logService.trace(`[CopilotCLIAgentManager] Handling request for sessionId=${sessionIdForLog}.`);
5054

55+
const { prompt, attachments } = await this.resolvePrompt(request);
5156
// Check if we already have a session wrapper
5257
let session = copilotcliSessionId ? this.sessionService.findSessionWrapper<CopilotCLISession>(copilotcliSessionId) : undefined;
5358

5459
if (session) {
5560
this.logService.trace(`[CopilotCLIAgentManager] Reusing CopilotCLI session ${copilotcliSessionId}.`);
5661
} else {
57-
const sdkSession = await this.sessionService.getOrCreateSDKSession(copilotcliSessionId, request.prompt);
62+
const sdkSession = await this.sessionService.getOrCreateSDKSession(copilotcliSessionId, prompt);
5863
session = this.instantiationService.createInstance(CopilotCLISession, sdkSession);
5964
this.sessionService.trackSessionWrapper(sdkSession.sessionId, session);
6065
}
@@ -63,10 +68,91 @@ export class CopilotCLIAgentManager extends Disposable {
6368
this.sessionService.setPendingRequest(session.sessionId);
6469
}
6570

66-
await session.invoke(request.prompt, request.toolInvocationToken, stream, modelId, token);
71+
await session.invoke(prompt, attachments, request.toolInvocationToken, stream, modelId, token);
6772

6873
return { copilotcliSessionId: session.sessionId };
6974
}
75+
76+
private async resolvePrompt(request: vscode.ChatRequest): Promise<{ prompt: string; attachments: Attachment[] }> {
77+
if (request.prompt.startsWith('/')) {
78+
return { prompt: request.prompt, attachments: [] }; // likely a slash command, don't modify
79+
}
80+
81+
const attachments: Attachment[] = [];
82+
const allRefsTexts: string[] = [];
83+
const diagnosticTexts: string[] = [];
84+
const files: { path: string; name: string }[] = [];
85+
// TODO@rebornix: filter out implicit references for now. Will need to figure out how to support `<reminder>` without poluting user prompt
86+
request.references.filter(ref => !ref.id.startsWith('vscode.prompt.instructions')).forEach(ref => {
87+
if (ref.value instanceof ChatReferenceDiagnostic) {
88+
// Handle diagnostic reference
89+
for (const [uri, diagnostics] of ref.value.diagnostics) {
90+
for (const diagnostic of diagnostics) {
91+
const severityMap: { [key: number]: string } = {
92+
0: 'error',
93+
1: 'warning',
94+
2: 'info',
95+
3: 'hint'
96+
};
97+
const severity = severityMap[diagnostic.severity] ?? 'error';
98+
const code = (typeof diagnostic.code === 'object' && diagnostic.code !== null) ? diagnostic.code.value : diagnostic.code;
99+
const codeStr = code ? ` [${code}]` : '';
100+
const line = diagnostic.range.start.line + 1;
101+
diagnosticTexts.push(`- ${severity}${codeStr} at ${uri.fsPath}:${line}: ${diagnostic.message}`);
102+
files.push({ path: uri.fsPath, name: path.basename(uri.fsPath) });
103+
}
104+
}
105+
} else {
106+
const filePath = URI.isUri(ref.value) ? ref.value.fsPath : isLocation(ref.value) ? ref.value.uri.fsPath : undefined;
107+
if (filePath) {
108+
files.push({ path: filePath, name: ref.name || path.basename(filePath) });
109+
}
110+
const valueText = URI.isUri(ref.value) ?
111+
ref.value.fsPath :
112+
isLocation(ref.value) ?
113+
`${ref.value.uri.fsPath}:${ref.value.range.start.line + 1}` :
114+
undefined;
115+
if (valueText && ref.range) {
116+
// Keep the original prompt untouched, just collect resolved paths
117+
const variableText = request.prompt.substring(ref.range[0], ref.range[1]);
118+
allRefsTexts.push(`- ${variableText}${valueText}`);
119+
}
120+
}
121+
});
122+
123+
await Promise.all(files.map(async (file) => {
124+
try {
125+
const stat = await fs.stat(file.path);
126+
const type = stat.isDirectory() ? 'directory' : stat.isFile() ? 'file' : undefined;
127+
if (!type) {
128+
this.logService.error(`[CopilotCLIAgentManager] Ignoring attachment as its not a file/directory (${file.path})`);
129+
return;
130+
}
131+
attachments.push({
132+
type,
133+
displayName: file.name,
134+
path: file.path
135+
});
136+
} catch (error) {
137+
this.logService.error(`[CopilotCLIAgentManager] Failed to attach ${file.path}: ${error}`);
138+
}
139+
}));
140+
141+
const reminderParts: string[] = [];
142+
if (allRefsTexts.length > 0) {
143+
reminderParts.push(`The user provided the following references:\n${allRefsTexts.join('\n')}`);
144+
}
145+
if (diagnosticTexts.length > 0) {
146+
reminderParts.push(`The user provided the following diagnostics:\n${diagnosticTexts.join('\n')}`);
147+
}
148+
149+
let prompt = request.prompt;
150+
if (reminderParts.length > 0) {
151+
prompt = `<reminder>\n${reminderParts.join('\n\n')}\n\nIMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</reminder>\n\n${prompt}`;
152+
}
153+
154+
return { prompt, attachments };
155+
}
70156
}
71157

72158
export class CopilotCLISession extends Disposable {
@@ -92,19 +178,20 @@ export class CopilotCLISession extends Disposable {
92178
super.dispose();
93179
}
94180

95-
async *query(prompt: string, options: AgentOptions): AsyncGenerator<SessionEvent> {
181+
async *query(prompt: string, attachments: Attachment[], options: AgentOptions): AsyncGenerator<SessionEvent> {
96182
// Ensure node-pty shim exists before importing SDK
97183
// @github/copilot has hardcoded: import{spawn}from"node-pty"
98184
await ensureNodePtyShim(this.extensionContext.extensionPath, this.envService.appRoot);
99185

100186
// Dynamically import the SDK
101187
const { Agent } = await import('@github/copilot/sdk');
102188
const agent = new Agent(options);
103-
yield* agent.query(prompt);
189+
yield* agent.query(prompt, attachments);
104190
}
105191

106192
public async invoke(
107193
prompt: string,
194+
attachments: Attachment[],
108195
toolInvocationToken: vscode.ChatParticipantToolToken,
109196
stream: vscode.ChatResponseStream,
110197
modelId: ModelProvider | undefined,
@@ -148,7 +235,7 @@ export class CopilotCLISession extends Disposable {
148235
};
149236

150237
try {
151-
for await (const event of this.query(prompt, options)) {
238+
for await (const event of this.query(prompt, attachments, options)) {
152239
if (token.isCancellationRequested) {
153240
break;
154241
}

src/extension/agents/copilotcli/node/copilotcliToolInvocationFormatter.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
import type { SessionEvent, ToolExecutionCompleteEvent, ToolExecutionStartEvent } from '@github/copilot/sdk';
77
import * as l10n from '@vscode/l10n';
8-
import type { ExtendedChatResponsePart } from 'vscode';
8+
import type { ChatPromptReference, ExtendedChatResponsePart } from 'vscode';
99
import { URI } from '../../../../util/vs/base/common/uri';
10-
import { ChatRequestTurn2, ChatResponseMarkdownPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatToolInvocationPart, MarkdownString } from '../../../../vscodeTypes';
10+
import { ChatRequestTurn2, ChatResponseMarkdownPart, ChatResponseThinkingProgressPart, ChatResponseTurn2, ChatToolInvocationPart, MarkdownString, Uri } from '../../../../vscodeTypes';
1111

1212
/**
1313
* CopilotCLI tool names
@@ -62,7 +62,14 @@ export function buildChatHistoryFromEvents(events: readonly SessionEvent[]): (Ch
6262
turns.push(new ChatResponseTurn2(currentResponseParts, {}, ''));
6363
currentResponseParts = [];
6464
}
65-
turns.push(new ChatRequestTurn2(stripReminders(event.data.content || ''), undefined, [], '', [], undefined));
65+
// TODO @DonJayamanne Temporary work around until we get the zod types.
66+
type Attachment = {
67+
path: string;
68+
type: "file" | "directory";
69+
displayName: string;
70+
};
71+
const references: ChatPromptReference[] = ((event.data.attachments || []) as Attachment[]).map(attachment => ({ id: attachment.path, name: attachment.displayName, value: Uri.file(attachment.path) } as ChatPromptReference));
72+
turns.push(new ChatRequestTurn2(stripReminders(event.data.content || ''), undefined, references, '', [], undefined));
6673
break;
6774
}
6875
case 'assistant.message': {

src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts

Lines changed: 5 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { ChatExtendedRequestHandler, ChatReferenceDiagnostic, l10n } from 'vscode';
7+
import { ChatExtendedRequestHandler, l10n } from 'vscode';
88
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
9-
import { isLocation } from '../../../util/common/types';
109
import { Emitter, Event } from '../../../util/vs/base/common/event';
1110
import { Disposable, DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
12-
import { URI } from '../../../util/vs/base/common/uri';
1311
import { localize } from '../../../util/vs/nls';
1412
import { CopilotCLIAgentManager } from '../../agents/copilotcli/node/copilotcliAgentManager';
15-
import { ExtendedChatRequest, ICopilotCLISessionService } from '../../agents/copilotcli/node/copilotcliSessionService';
13+
import { ICopilotCLISessionService } from '../../agents/copilotcli/node/copilotcliSessionService';
1614
import { buildChatHistoryFromEvents } from '../../agents/copilotcli/node/copilotcliToolInvocationFormatter';
1715
import { ICopilotCLITerminalIntegration } from './copilotCLITerminalIntegration';
1816

@@ -194,28 +192,24 @@ export class CopilotCLIChatSessionParticipant {
194192
}
195193

196194
private async handleRequest(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult | void> {
197-
// Resolve the prompt with references before processing
198-
const resolvedPrompt = this.resolvePrompt(request);
199-
const processedRequest: ExtendedChatRequest = { ...request, prompt: resolvedPrompt };
200-
201195
const { chatSessionContext } = context;
202196
if (chatSessionContext) {
203197
if (chatSessionContext.isUntitled) {
204-
const { copilotcliSessionId } = await this.copilotcliAgentManager.handleRequest(undefined, processedRequest, context, stream, undefined, token);
198+
const { copilotcliSessionId } = await this.copilotcliAgentManager.handleRequest(undefined, request, context, stream, undefined, token);
205199
if (!copilotcliSessionId) {
206200
stream.warning(localize('copilotcli.failedToCreateSession', "Failed to create a new CopilotCLI session."));
207201
return {};
208202
}
209203
if (copilotcliSessionId) {
210-
this.sessionItemProvider.swap(chatSessionContext.chatSessionItem, { id: copilotcliSessionId, resource: undefined, label: processedRequest.prompt ?? 'CopilotCLI' });
204+
this.sessionItemProvider.swap(chatSessionContext.chatSessionItem, { id: copilotcliSessionId, resource: undefined, label: request.prompt ?? 'CopilotCLI' });
211205
this.sessionService.clearPendingRequest(copilotcliSessionId);
212206
}
213207
return {};
214208
}
215209

216210
const { id } = chatSessionContext.chatSessionItem;
217211
this.sessionService.setSessionStatus(id, vscode.ChatSessionStatus.InProgress);
218-
await this.copilotcliAgentManager.handleRequest(id, processedRequest, context, stream, getModelProvider(_sessionModel.get(id)?.id), token);
212+
await this.copilotcliAgentManager.handleRequest(id, request, context, stream, getModelProvider(_sessionModel.get(id)?.id), token);
219213
this.sessionService.setSessionStatus(id, vscode.ChatSessionStatus.Completed);
220214
return {};
221215
}
@@ -224,66 +218,6 @@ export class CopilotCLIChatSessionParticipant {
224218
stream.button({ command: `workbench.action.chat.openNewSessionEditor.${this.sessionType}`, title: localize('copilotcli.startNewSession', "Start Session") });
225219
return {};
226220
}
227-
228-
private resolvePrompt(request: vscode.ChatRequest): string {
229-
if (request.prompt.startsWith('/')) {
230-
return request.prompt; // likely a slash command, don't modify
231-
}
232-
233-
const allRefsTexts: string[] = [];
234-
const diagnosticTexts: string[] = [];
235-
const prompt = request.prompt;
236-
// TODO@rebornix: filter out implicit references for now. Will need to figure out how to support `<reminder>` without poluting user prompt
237-
request.references.filter(ref => !ref.id.startsWith('vscode.prompt.instructions')).forEach(ref => {
238-
if (ref.value instanceof ChatReferenceDiagnostic) {
239-
// Handle diagnostic reference
240-
for (const [uri, diagnostics] of ref.value.diagnostics) {
241-
for (const diagnostic of diagnostics) {
242-
const severityMap: { [key: number]: string } = {
243-
0: 'error',
244-
1: 'warning',
245-
2: 'info',
246-
3: 'hint'
247-
};
248-
const severity = severityMap[diagnostic.severity] ?? 'error';
249-
const code = (typeof diagnostic.code === 'object' && diagnostic.code !== null) ? diagnostic.code.value : diagnostic.code;
250-
const codeStr = code ? ` [${code}]` : '';
251-
const line = diagnostic.range.start.line + 1;
252-
diagnosticTexts.push(`- ${severity}${codeStr} at ${uri.fsPath}:${line}: ${diagnostic.message}`);
253-
}
254-
}
255-
} else {
256-
const valueText = URI.isUri(ref.value) ?
257-
ref.value.fsPath :
258-
isLocation(ref.value) ?
259-
`${ref.value.uri.fsPath}:${ref.value.range.start.line + 1}` :
260-
undefined;
261-
if (valueText) {
262-
// Keep the original prompt untouched, just collect resolved paths
263-
const variableText = ref.range ? prompt.substring(ref.range[0], ref.range[1]) : undefined;
264-
if (variableText) {
265-
allRefsTexts.push(`- ${variableText}${valueText}`);
266-
} else {
267-
allRefsTexts.push(`- ${valueText}`);
268-
}
269-
}
270-
}
271-
});
272-
273-
const reminderParts: string[] = [];
274-
if (allRefsTexts.length > 0) {
275-
reminderParts.push(`The user provided the following references:\n${allRefsTexts.join('\n')}`);
276-
}
277-
if (diagnosticTexts.length > 0) {
278-
reminderParts.push(`The user provided the following diagnostics:\n${diagnosticTexts.join('\n')}`);
279-
}
280-
281-
if (reminderParts.length > 0) {
282-
return `<reminder>\n${reminderParts.join('\n\n')}\n\nIMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</reminder>\n\n${prompt}`;
283-
}
284-
285-
return prompt;
286-
}
287221
}
288222

289223
export function registerCLIChatCommands(copilotcliSessionItemProvider: CopilotCLIChatSessionItemProvider, copilotCLISessionService: ICopilotCLISessionService): IDisposable {

0 commit comments

Comments
 (0)