Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4357,7 +4357,8 @@
"capabilities": {
"supportsFileAttachments": true,
"supportsProblemAttachments": true,
"supportsToolAttachments": false
"supportsToolAttachments": false,
"supportsImageAttachments": true
},
"commands": [
{
Expand Down
2 changes: 1 addition & 1 deletion src/extension/chatSessions/vscode-node/chatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { ClaudeAgentManager } from '../../agents/claude/node/claudeCodeAgent';
import { ClaudeCodeSdkService, IClaudeCodeSdkService } from '../../agents/claude/node/claudeCodeSdkService';
import { ClaudeCodeSessionService, IClaudeCodeSessionService } from '../../agents/claude/node/claudeCodeSessionService';
import { CopilotCLIModels, CopilotCLISDK, CopilotCLISessionOptionsService, ICopilotCLIModels, ICopilotCLISDK, ICopilotCLISessionOptionsService } from '../../agents/copilotcli/node/copilotCli';
import { CopilotCLIPromptResolver } from '../../agents/copilotcli/node/copilotcliPromptResolver';
import { CopilotCLISessionService, ICopilotCLISessionService } from '../../agents/copilotcli/node/copilotcliSessionService';
import { ILanguageModelServer, LanguageModelServer } from '../../agents/node/langModelServer';
import { IExtensionContribution } from '../../common/contributions';
Expand All @@ -24,6 +23,7 @@ import { ClaudeChatSessionContentProvider } from './claudeChatSessionContentProv
import { ClaudeChatSessionItemProvider } from './claudeChatSessionItemProvider';
import { ClaudeChatSessionParticipant } from './claudeChatSessionParticipant';
import { CopilotCLIChatSessionContentProvider, CopilotCLIChatSessionItemProvider, CopilotCLIChatSessionParticipant, CopilotCLIWorktreeManager, registerCLIChatCommands } from './copilotCLIChatSessionsContribution';
import { CopilotCLIPromptResolver } from './copilotCLIPromptResolver';
import { CopilotCLITerminalIntegration, ICopilotCLITerminalIntegration } from './copilotCLITerminalIntegration';
import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';
import { PRContentProvider } from './prContentProvider';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import { Emitter, Event } from '../../../util/vs/base/common/event';
import { Disposable, DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
import { localize } from '../../../util/vs/nls';
import { ICopilotCLIModels } from '../../agents/copilotcli/node/copilotCli';
import { CopilotCLIPromptResolver } from '../../agents/copilotcli/node/copilotcliPromptResolver';
import { ICopilotCLISession } from '../../agents/copilotcli/node/copilotcliSession';
import { ICopilotCLISessionService } from '../../agents/copilotcli/node/copilotcliSessionService';
import { ChatSummarizerProvider } from '../../prompt/node/summarizer';
import { CopilotCLIPromptResolver } from './copilotCLIPromptResolver';
import { ICopilotCLITerminalIntegration } from './copilotCLITerminalIntegration';
import { ConfirmationResult, CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';

Expand Down Expand Up @@ -311,7 +311,6 @@ export class CopilotCLIChatSessionParticipant {
private async handleRequest(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult | void> {
const { chatSessionContext } = context;


/* __GDPR__
"copilotcli.chat.invoke" : {
"owner": "joshspicer",
Expand Down
90 changes: 90 additions & 0 deletions src/extension/chatSessions/vscode-node/copilotCLIImageSupport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { URI } from '../../../util/vs/base/common/uri';

export class ImageStorage {
private readonly storageDir: URI;

constructor(private readonly context: IVSCodeExtensionContext) {
this.storageDir = URI.joinPath(this.context.globalStorageUri, 'copilot-cli-images');
this.initialize();
}

Comment on lines +13 to +17
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initialize() method is called in the constructor but is async and not awaited. This means the storage directory creation and cleanup may not complete before storeImage() is called, potentially causing file operation failures. Consider making initialization synchronous or ensuring it completes before allowing image storage operations.

Suggested change
constructor(private readonly context: IVSCodeExtensionContext) {
this.storageDir = URI.joinPath(this.context.globalStorageUri, 'copilot-cli-images');
this.initialize();
}
private constructor(private readonly context: IVSCodeExtensionContext) {
this.storageDir = URI.joinPath(this.context.globalStorageUri, 'copilot-cli-images');
}
static async create(context: IVSCodeExtensionContext): Promise<ImageStorage> {
const instance = new ImageStorage(context);
await instance.initialize();
return instance;
}

Copilot uses AI. Check for mistakes.
private async initialize(): Promise<void> {
try {
await vscode.workspace.fs.createDirectory(this.storageDir);
await this.cleanupOldImages();
} catch (error) {
console.error('ImageStorage: Failed to initialize', error);
}
}
Comment on lines +10 to +25
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ImageStorage class is directly using vscode.workspace.fs and console.error instead of the proper service abstractions. According to the coding guidelines:

  • Use IFileService instead of vscode.workspace.fs for file operations
  • Use ILogService instead of console.error for logging

The class should inject these services in the constructor to maintain consistency with the project's service-oriented architecture.

Copilot generated this review using guidance from repository custom instructions.

async storeImage(imageData: Uint8Array, mimeType: string): Promise<URI> {
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 10);
const extension = this.getExtension(mimeType);
const filename = `${timestamp}-${randomId}${extension}`;
const imageUri = URI.joinPath(this.storageDir, filename);

await vscode.workspace.fs.writeFile(imageUri, imageData);
return imageUri;
}

async getImage(uri: URI): Promise<Uint8Array | undefined> {
try {
const data = await vscode.workspace.fs.readFile(uri);
return data;
} catch {
return undefined;
}
}

async deleteImage(uri: URI): Promise<void> {
try {
await vscode.workspace.fs.delete(uri);
} catch {
// Already deleted
}
}

async cleanupOldImages(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): Promise<void> {
try {
const entries = await vscode.workspace.fs.readDirectory(this.storageDir);
const now = Date.now();
const cutoff = now - maxAgeMs;

for (const [filename, fileType] of entries) {
if (fileType === vscode.FileType.File) {
const fileUri = URI.joinPath(this.storageDir, filename);
try {
const stat = await vscode.workspace.fs.stat(fileUri);
if (stat.mtime < cutoff) {
await vscode.workspace.fs.delete(fileUri);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be more efficient to do a Promise.all(..)
But thats fine, can change after this is merged.

}
} catch {
// Skip files we can't access
}
}
}
} catch (error) {
console.error('ImageStorage: Failed to cleanup old images', error);
Comment on lines +23 to +75
Copy link

Copilot AI Nov 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using console.error for logging violates the coding guideline to use ILogService instead. All error logging should be done through the injected log service to maintain consistency and enable proper log management.

Copilot generated this review using guidance from repository custom instructions.
}
}

private getExtension(mimeType: string): string {
const map: Record<string, string> = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/jpg': '.jpg',
'image/gif': '.gif',
'image/webp': '.webp',
'image/bmp': '.bmp',
};
return map[mimeType.toLowerCase()] || '.bin';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@

import type { Attachment } from '@github/copilot/sdk';
import type * as vscode from 'vscode';
import { IFileSystemService } from '../../../../platform/filesystem/common/fileSystemService';
import { ILogService } from '../../../../platform/log/common/logService';
import { isLocation } from '../../../../util/common/types';
import { raceCancellationError } from '../../../../util/vs/base/common/async';
import * as path from '../../../../util/vs/base/common/path';
import { URI } from '../../../../util/vs/base/common/uri';
import { ChatReferenceDiagnostic, FileType } from '../../../../vscodeTypes';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { ILogService } from '../../../platform/log/common/logService';
import { isLocation } from '../../../util/common/types';
import { raceCancellationError } from '../../../util/vs/base/common/async';
import * as path from '../../../util/vs/base/common/path';
import { URI } from '../../../util/vs/base/common/uri';
import { ChatReferenceBinaryData, ChatReferenceDiagnostic, FileType } from '../../../vscodeTypes';
import { ImageStorage } from './copilotCLIImageSupport';

export class CopilotCLIPromptResolver {
private readonly imageStorage: ImageStorage;

constructor(
@ILogService private readonly logService: ILogService,
@IFileSystemService private readonly fileSystemService: IFileSystemService,
) { }
@IVSCodeExtensionContext extensionContext: IVSCodeExtensionContext,
) {
this.imageStorage = new ImageStorage(extensionContext);
}

public async resolvePrompt(request: vscode.ChatRequest, token: vscode.CancellationToken): Promise<{ prompt: string; attachments: Attachment[] }> {
if (request.prompt.startsWith('/')) {
Expand All @@ -27,10 +34,26 @@ export class CopilotCLIPromptResolver {
const attachments: Attachment[] = [];
const allRefsTexts: string[] = [];
const diagnosticTexts: string[] = [];
const files: { path: string; name: string }[] = [];

// TODO@rebornix: filter out implicit references for now. Will need to figure out how to support `<reminder>` without poluting user prompt
request.references.filter(ref => !ref.id.startsWith('vscode.prompt.instructions')).forEach(ref => {
if (ref.value instanceof ChatReferenceDiagnostic) {
const relevantRefs = request.references.filter(ref => !ref.id.startsWith('vscode.prompt.instructions'));

// Process all references in parallel
await Promise.all(relevantRefs.map(async ref => {
if (ref.value instanceof ChatReferenceBinaryData) {
// Handle image attachments
try {
const buffer = await ref.value.data();
const uri = await this.imageStorage.storeImage(buffer, ref.value.mimeType);
attachments.push({
type: 'file',
displayName: path.basename(uri.fsPath),
path: uri.fsPath
});
} catch (error) {
this.logService.error(`[CopilotCLIPromptResolver] Failed to store image: ${error}`);
}
} else if (ref.value instanceof ChatReferenceDiagnostic) {
// Handle diagnostic reference
for (const [uri, diagnostics] of ref.value.diagnostics) {
if (uri.scheme !== 'file') {
Expand All @@ -48,7 +71,21 @@ export class CopilotCLIPromptResolver {
const codeStr = code ? ` [${code}]` : '';
const line = diagnostic.range.start.line + 1;
diagnosticTexts.push(`- ${severity}${codeStr} at ${uri.fsPath}:${line}: ${diagnostic.message}`);
files.push({ path: uri.fsPath, name: path.basename(uri.fsPath) });

// Add the file from diagnostic
try {
const stat = await raceCancellationError(this.fileSystemService.stat(uri), token);
const type = stat.type === FileType.Directory ? 'directory' : stat.type === FileType.File ? 'file' : undefined;
if (type) {
attachments.push({
type,
displayName: path.basename(uri.fsPath),
path: uri.fsPath
});
}
} catch (error) {
this.logService.error(`[CopilotCLIPromptResolver] Failed to attach ${uri.fsPath}: ${error}`);
}
}
}
} else {
Expand All @@ -57,7 +94,6 @@ export class CopilotCLIPromptResolver {
return;
}
const filePath = uri.fsPath;
files.push({ path: filePath, name: ref.name || path.basename(filePath) });
const valueText = URI.isUri(ref.value) ?
ref.value.fsPath :
isLocation(ref.value) ?
Expand All @@ -68,24 +104,21 @@ export class CopilotCLIPromptResolver {
const variableText = request.prompt.substring(ref.range[0], ref.range[1]);
allRefsTexts.push(`- ${variableText} → ${valueText}`);
}
}
});

await Promise.all(files.map(async (file) => {
try {
const stat = await raceCancellationError(this.fileSystemService.stat(URI.file(file.path)), token);
const type = stat.type === FileType.Directory ? 'directory' : stat.type === FileType.File ? 'file' : undefined;
if (!type) {
this.logService.error(`[CopilotCLIAgentManager] Ignoring attachment as its not a file/directory (${file.path})`);
return;
// Add the file reference
try {
const stat = await raceCancellationError(this.fileSystemService.stat(uri), token);
const type = stat.type === FileType.Directory ? 'directory' : stat.type === FileType.File ? 'file' : undefined;
if (type) {
attachments.push({
type,
displayName: ref.name || path.basename(filePath),
path: filePath
});
}
} catch (error) {
this.logService.error(`[CopilotCLIPromptResolver] Failed to attach ${filePath}: ${error}`);
}
attachments.push({
type,
displayName: file.name,
path: file.path
});
} catch (error) {
this.logService.error(`[CopilotCLIAgentManager] Failed to attach ${file.path}: ${error}`);
}
}));

Expand Down
Loading