Skip to content

Commit 79de6aa

Browse files
authored
Display chat references when delegating to background (#2394)
1 parent e7201b0 commit 79de6aa

File tree

4 files changed

+259
-11
lines changed

4 files changed

+259
-11
lines changed

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,19 @@ export class CopilotCLIPromptResolver {
3636
* @param token
3737
* @returns
3838
*/
39-
public async resolvePrompt(request: vscode.ChatRequest, prompt: string | undefined, additionalReferences: vscode.ChatPromptReference[], isIsolationEnabled: boolean, token: vscode.CancellationToken): Promise<{ prompt: string; attachments: Attachment[] }> {
40-
const references = request.references.concat(additionalReferences.filter(ref => !request.references.includes(ref)));
39+
public async resolvePrompt(request: vscode.ChatRequest, prompt: string | undefined, additionalReferences: vscode.ChatPromptReference[], isIsolationEnabled: boolean, token: vscode.CancellationToken): Promise<{ prompt: string; attachments: Attachment[]; references: vscode.ChatPromptReference[] }> {
40+
const allReferences = request.references.concat(additionalReferences.filter(ref => !request.references.includes(ref)));
4141
prompt = prompt ?? request.prompt;
4242
if (prompt.startsWith('/')) {
43-
return { prompt, attachments: [] }; // likely a slash command, don't modify
43+
return { prompt, attachments: [], references: [] }; // likely a slash command, don't modify
4444
}
45-
const [variables, attachments] = await this.constructChatVariablesAndAttachments(new ChatVariablesCollection(references), isIsolationEnabled, token);
45+
const [variables, attachments] = await this.constructChatVariablesAndAttachments(new ChatVariablesCollection(allReferences), isIsolationEnabled, token);
4646
if (token.isCancellationRequested) {
47-
return { prompt, attachments: [] };
47+
return { prompt, attachments: [], references: allReferences };
4848
}
4949
prompt = await raceCancellation(generateUserPrompt(request, prompt, variables, this.instantiationService), token);
50-
return { prompt: prompt ?? '', attachments };
50+
const references = Array.from(variables).map(v => v.reference);
51+
return { prompt: prompt ?? '', attachments, references };
5152
}
5253

5354
private async constructChatVariablesAndAttachments(variables: ChatVariablesCollection, isIsolationEnabled: boolean, token: vscode.CancellationToken): Promise<[variables: ChatVariablesCollection, Attachment[]]> {

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { ChatVariablesCollection, isPromptFile } from '../../prompt/common/chatV
3232
import { IToolsService } from '../../tools/common/toolsService';
3333
import { ICopilotCLITerminalIntegration } from './copilotCLITerminalIntegration';
3434
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
35+
import { convertReferenceToVariable } from './copilotPromptReferences';
3536

3637
const AGENTS_OPTION_ID = 'agent';
3738
const MODELS_OPTION_ID = 'model';
@@ -850,7 +851,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
850851
private async createCLISessionAndSubmitRequest(
851852
request: vscode.ChatRequest,
852853
userPrompt: string | undefined,
853-
references: readonly vscode.ChatPromptReference[] | undefined,
854+
otherReferences: readonly vscode.ChatPromptReference[] | undefined,
854855
context: vscode.ChatContext,
855856
workingDirectory: Uri | undefined,
856857
isolationEnabled: boolean,
@@ -883,8 +884,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
883884
}
884885
};
885886

886-
const [{ prompt, attachments }, model, agent] = await Promise.all([
887-
requestPromptPromise.then(prompt => this.promptResolver.resolvePrompt(request, prompt, (references || []).concat([]), isolationEnabled, token)),
887+
const [{ prompt, attachments, references }, model, agent] = await Promise.all([
888+
requestPromptPromise.then(prompt => this.promptResolver.resolvePrompt(request, prompt, (otherReferences || []).concat([]), isolationEnabled, token)),
888889
this.getModelId(undefined, request, true, token), // prefer model in request, as we're delegating from another session here.
889890
this.getAgent(undefined, undefined, token),
890891
getWorkingDirectory()
@@ -905,7 +906,8 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
905906
this.sessionItemProvider.notifySessionsChange();
906907
await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {
907908
resource: SessionIdForCLI.getResource(session.object.sessionId),
908-
prompt: userPrompt || request.prompt
909+
prompt: userPrompt || request.prompt,
910+
attachedContext: references.map(ref => convertReferenceToVariable(ref, attachments))
909911
});
910912
} catch {
911913
this.contextForRequest.delete(session.object.sessionId);
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as l10n from '@vscode/l10n';
7+
import type { ChatPromptReference } from 'vscode';
8+
import { isLocation } from '../../../util/common/types';
9+
import { coalesce } from '../../../util/vs/base/common/arrays';
10+
import { Codicon } from '../../../util/vs/base/common/codicons';
11+
import { basename } from '../../../util/vs/base/common/resources';
12+
import { isNumber, isString } from '../../../util/vs/base/common/types';
13+
import { URI } from '../../../util/vs/base/common/uri';
14+
import { Range as InternalRange } from '../../../util/vs/editor/common/core/range';
15+
import { SymbolKind } from '../../../util/vs/workbench/api/common/extHostTypes/symbolInformation';
16+
import { ChatReferenceDiagnostic, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Range, Uri } from '../../../vscodeTypes';
17+
import { Attachment } from '@github/copilot/sdk';
18+
import { ResourceSet } from '../../../util/vs/base/common/map';
19+
20+
export function convertReferenceToVariable(ref: ChatPromptReference, attachments: readonly Attachment[]) {
21+
const value = ref.value;
22+
const range = ref.range ? { start: ref.range[0], endExclusive: ref.range[1] } : undefined;
23+
24+
if (value && value instanceof ChatReferenceDiagnostic && Array.isArray(value.diagnostics) && value.diagnostics.length && value.diagnostics[0][1].length) {
25+
const marker = DiagnosticConverter.from(value.diagnostics[0][1][0]);
26+
const refValue = {
27+
filterRange: { startLineNumber: marker.startLineNumber, startColumn: marker.startColumn, endLineNumber: marker.endLineNumber, endColumn: marker.endColumn },
28+
filterSeverity: marker.severity,
29+
filterUri: value.diagnostics[0][0],
30+
problemMessage: value.diagnostics[0][1][0].message
31+
};
32+
return IDiagnosticVariableEntryFilterData.toEntry(refValue);
33+
}
34+
35+
if (isLocation(ref.value) && ref.name.startsWith(`sym:`)) {
36+
return {
37+
id: ref.id,
38+
name: ref.name,
39+
fullName: ref.name.substring(4),
40+
value: { uri: ref.value.uri, range: toInternalRange(ref.value.range) },
41+
// We never send this information to extensions, so default to Property
42+
symbolKind: SymbolKind.Property,
43+
// We never send this information to extensions, so default to Property
44+
icon: Codicon.symbolProperty,
45+
kind: 'symbol',
46+
range,
47+
};
48+
}
49+
50+
if (URI.isUri(value) && ref.name.startsWith(`prompt:`) &&
51+
ref.id.startsWith(PromptFileVariableKind.PromptFile) &&
52+
ref.id.endsWith(value.toString())) {
53+
return {
54+
id: ref.id,
55+
name: `prompt:${basename(value)}`,
56+
value,
57+
kind: 'promptFile',
58+
modelDescription: 'Prompt instructions file',
59+
isRoot: true,
60+
automaticallyAdded: false,
61+
range,
62+
};
63+
}
64+
65+
const folders = new ResourceSet(attachments.filter(att => att.type === 'directory').map(att => URI.file(att.path)));
66+
const isFile = URI.isUri(value) || (value && typeof value === 'object' && 'uri' in value);
67+
const isFolder = isFile && URI.isUri(value) && (value.path.endsWith('/') || folders.has(value));
68+
return {
69+
id: ref.id,
70+
name: ref.name,
71+
value,
72+
modelDescription: ref.modelDescription,
73+
range,
74+
kind: isFolder ? 'directory' as const : isFile ? 'file' as const : 'generic' as const
75+
};
76+
}
77+
78+
function toInternalRange(range: Range): InternalRange {
79+
return new InternalRange(range.start.line + 1, range.start.character + 1, range.end.line + 1, range.end.character + 1);
80+
}
81+
82+
enum PromptFileVariableKind {
83+
Instruction = 'vscode.prompt.instructions.root',
84+
InstructionReference = `vscode.prompt.instructions`,
85+
PromptFile = 'vscode.prompt.file'
86+
}
87+
88+
namespace DiagnosticTagConverter {
89+
90+
/**
91+
* Additional metadata about the type of a diagnostic.
92+
*/
93+
export enum DiagnosticTag {
94+
/**
95+
* Unused or unnecessary code.
96+
*
97+
* Diagnostics with this tag are rendered faded out. The amount of fading
98+
* is controlled by the `"editorUnnecessaryCode.opacity"` theme color. For
99+
* example, `"editorUnnecessaryCode.opacity": "#000000c0"` will render the
100+
* code with 75% opacity. For high contrast themes, use the
101+
* `"editorUnnecessaryCode.border"` theme color to underline unnecessary code
102+
* instead of fading it out.
103+
*/
104+
Unnecessary = 1,
105+
106+
/**
107+
* Deprecated or obsolete code.
108+
*
109+
* Diagnostics with this tag are rendered with a strike through.
110+
*/
111+
Deprecated = 2,
112+
}
113+
export const enum MarkerTag {
114+
Unnecessary = 1,
115+
Deprecated = 2
116+
}
117+
118+
119+
export function from(value: DiagnosticTag) {
120+
121+
switch (value) {
122+
case DiagnosticTag.Unnecessary:
123+
return MarkerTag.Unnecessary;
124+
case DiagnosticTag.Deprecated:
125+
return MarkerTag.Deprecated;
126+
default:
127+
return undefined;
128+
}
129+
}
130+
}
131+
132+
133+
namespace IDiagnosticVariableEntryFilterData {
134+
export const icon = Codicon.error;
135+
136+
export function fromMarker(marker: Record<string, unknown>) {
137+
return {
138+
filterUri: marker.resource,
139+
owner: marker.owner,
140+
problemMessage: marker.message,
141+
filterRange: { startLineNumber: marker.startLineNumber, endLineNumber: marker.endLineNumber, startColumn: marker.startColumn, endColumn: marker.endColumn }
142+
};
143+
}
144+
145+
export function toEntry(data: Record<string, unknown>) {
146+
return {
147+
id: id(data),
148+
name: label(data),
149+
icon,
150+
value: data,
151+
kind: 'diagnostic',
152+
...data,
153+
};
154+
}
155+
156+
export function id(data: Record<string, unknown> & { filterRange?: InternalRange }) {
157+
return [data.filterUri, data.owner, data.filterSeverity, data.filterRange?.startLineNumber, data.filterRange?.startColumn].join(':');
158+
}
159+
160+
export function label(data: Record<string, unknown> & { problemMessage?: string; filterUri?: Uri }) {
161+
const enum TrimThreshold {
162+
MaxChars = 30,
163+
MaxSpaceLookback = 10,
164+
}
165+
if (data.problemMessage) {
166+
if (data.problemMessage.length < TrimThreshold.MaxChars) {
167+
return data.problemMessage;
168+
}
169+
170+
// Trim the message, on a space if it would not lose too much
171+
// data (MaxSpaceLookback) or just blindly otherwise.
172+
const lastSpace = data.problemMessage.lastIndexOf(' ', TrimThreshold.MaxChars);
173+
if (lastSpace === -1 || lastSpace + TrimThreshold.MaxSpaceLookback < TrimThreshold.MaxChars) {
174+
return data.problemMessage.substring(0, TrimThreshold.MaxChars) + '…';
175+
}
176+
return data.problemMessage.substring(0, lastSpace) + '…';
177+
}
178+
let labelStr = l10n.t("All Problems");
179+
if (data.filterUri) {
180+
labelStr = l10n.t("Problems in {0}", basename(data.filterUri));
181+
}
182+
183+
return labelStr;
184+
}
185+
}
186+
187+
namespace DiagnosticConverter {
188+
export function from(value: Diagnostic) {
189+
let code: string | { value: string; target: Uri } | undefined;
190+
191+
if (value.code) {
192+
if (isString(value.code) || isNumber(value.code)) {
193+
code = String(value.code);
194+
} else {
195+
code = {
196+
value: String(value.code.value),
197+
target: value.code.target,
198+
};
199+
}
200+
}
201+
202+
return {
203+
...toInternalRange(value.range),
204+
message: value.message,
205+
source: value.source,
206+
code,
207+
severity: DiagnosticSeverityConveter.from(value.severity),
208+
relatedInformation: value.relatedInformation && value.relatedInformation.map(DiagnosticRelatedInformationConverter.from),
209+
tags: Array.isArray(value.tags) ? coalesce(value.tags.map(DiagnosticTagConverter.from)) : undefined,
210+
};
211+
}
212+
}
213+
214+
namespace DiagnosticRelatedInformationConverter {
215+
export function from(value: DiagnosticRelatedInformation) {
216+
return {
217+
...toInternalRange(value.location.range),
218+
message: value.message,
219+
resource: value.location.uri
220+
};
221+
}
222+
}
223+
224+
namespace DiagnosticSeverityConveter {
225+
export enum MarkerSeverity {
226+
Hint = 1,
227+
Info = 2,
228+
Warning = 4,
229+
Error = 8,
230+
}
231+
232+
export function from(value: number): MarkerSeverity {
233+
switch (value) {
234+
case DiagnosticSeverity.Error:
235+
return MarkerSeverity.Error;
236+
case DiagnosticSeverity.Warning:
237+
return MarkerSeverity.Warning;
238+
case DiagnosticSeverity.Information:
239+
return MarkerSeverity.Info;
240+
case DiagnosticSeverity.Hint:
241+
return MarkerSeverity.Hint;
242+
}
243+
return MarkerSeverity.Error;
244+
}
245+
}

src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
134134
const accessor = services.createTestingAccessor();
135135
promptResolver = new class extends mock<CopilotCLIPromptResolver>() {
136136
override resolvePrompt(request: vscode.ChatRequest, prompt: string | undefined) {
137-
return Promise.resolve({ prompt: prompt ?? request.prompt, attachments: [] });
137+
return Promise.resolve({ prompt: prompt ?? request.prompt, attachments: [], references: [] });
138138
}
139139
}();
140140
itemProvider = new class extends mock<CopilotCLIChatSessionItemProvider>() {

0 commit comments

Comments
 (0)