Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions src/extension/agents/copilotcli/node/copilotcliPromptResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import { ChatReferenceBinaryData, FileType } from '../../../../vscodeTypes';
import { ChatVariablesCollection, isPromptFile, isPromptInstruction } from '../../../prompt/common/chatVariablesCollection';
import { generateUserPrompt } from '../../../prompts/node/agent/copilotCLIPrompt';

// Use dynamic import to work around ESLint restrictions
const vscodeRuntime = import('vscode');

export class CopilotCLIPromptResolver {
constructor(
@ILogService private readonly logService: ILogService,
Expand All @@ -30,17 +33,25 @@ export class CopilotCLIPromptResolver {
if (request.prompt.startsWith('/')) {
return { prompt: request.prompt, attachments: [] }; // likely a slash command, don't modify
}
const [variables, attachments] = await this.constructChatVariablesAndAttachments(new ChatVariablesCollection(request.references), token);
const [variables, attachments, untitledFilesContext] = await this.constructChatVariablesAndAttachments(new ChatVariablesCollection(request.references), token);
if (token.isCancellationRequested) {
return { prompt: request.prompt, attachments: [] };
}
const prompt = await raceCancellation(generateUserPrompt(request, variables, this.instantiationService), token);
let prompt = await raceCancellation(generateUserPrompt(request, variables, this.instantiationService), token);

// Append untitled file context to the prompt if present
if (untitledFilesContext && prompt) {
prompt = `${prompt}\n\n${untitledFilesContext}`;
}

return { prompt: prompt ?? '', attachments };
}

private async constructChatVariablesAndAttachments(variables: ChatVariablesCollection, token: vscode.CancellationToken): Promise<[variables: ChatVariablesCollection, Attachment[]]> {
private async constructChatVariablesAndAttachments(variables: ChatVariablesCollection, token: vscode.CancellationToken): Promise<[variables: ChatVariablesCollection, Attachment[], untitledFilesContext: string | undefined]> {
const validReferences: vscode.ChatPromptReference[] = [];
const fileFolderReferences: vscode.ChatPromptReference[] = [];
const untitledFileReferences: vscode.ChatPromptReference[] = [];

for (const variable of variables) {
// Unsupported references.
if (isPromptInstruction(variable) || isPromptFile(variable)) {
Expand All @@ -60,6 +71,13 @@ export class CopilotCLIPromptResolver {
continue;
}

// Handle untitled files separately
if (variable.value.scheme === 'untitled') {
validReferences.push(variable.reference);
untitledFileReferences.push(variable.reference);
continue;
}

// Files and directories will be attached using regular attachments via Copilot CLI SDK.
validReferences.push(variable.reference);
fileFolderReferences.push(variable.reference);
Expand All @@ -71,7 +89,8 @@ export class CopilotCLIPromptResolver {

variables = new ChatVariablesCollection(validReferences);
const attachments = await this.constructFileOrFolderAttachments(fileFolderReferences, token);
return [variables, attachments];
const untitledFilesContext = await this.extractUntitledFileContent(untitledFileReferences, token);
return [variables, attachments, untitledFilesContext];
}


Expand All @@ -82,6 +101,12 @@ export class CopilotCLIPromptResolver {
if (!URI.isUri(uri)) {
return;
}

// Skip untitled files - they will be handled in constructChatVariablesAndAttachments
if (uri.scheme === 'untitled') {
return;
}

if (await this.ignoreService.isCopilotIgnored(uri)) {
return;
}
Expand All @@ -108,4 +133,35 @@ export class CopilotCLIPromptResolver {

return attachments;
}

private async extractUntitledFileContent(untitledFileReferences: vscode.ChatPromptReference[], token: vscode.CancellationToken): Promise<string | undefined> {
if (!untitledFileReferences || untitledFileReferences.length === 0) {
return undefined;
}

const fullFileParts: string[] = [];
const vscodeMod = await vscodeRuntime;

for (const ref of untitledFileReferences) {
if (!URI.isUri(ref.value) || ref.value.scheme !== 'untitled') {
continue;
}

try {
const document = await vscodeMod.workspace.openTextDocument(ref.value);
const content = document.getText();
fullFileParts.push(`<file-start>${ref.value.path}</file-start>`);
fullFileParts.push(content);
fullFileParts.push(`<file-end>${ref.value.path}</file-end>`);
} catch (error) {
this.logService.error(`Error reading untitled file content for reference: ${ref.value.toString()}: ${error}`);
}
}

if (fullFileParts.length === 0) {
return undefined;
}

return ['The user has attached the following untitled files as relevant context:', ...fullFileParts].join('\n');
}
}
Loading