Skip to content

Commit 9a0b47f

Browse files
Use VS Code native file dialog for AI panel attachments
1 parent 78deb03 commit 9a0b47f

8 files changed

Lines changed: 92 additions & 4 deletions

File tree

workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ import {
7575
SetMcpToolsEnabledRequest,
7676
McpLoadErrorsDTO,
7777
AgentsMdFileInfoDTO,
78+
SelectContextFilesRequest,
79+
Attachment,
7880
} from "./interfaces";
7981

8082
export interface AIPanelAPI {
@@ -185,4 +187,8 @@ export interface AIPanelAPI {
185187
getMcpLoadErrors: () => Promise<McpLoadErrorsDTO>;
186188
getAgentsMdFileInfo: () => Promise<AgentsMdFileInfoDTO>;
187189
openOrCreateAgentsMd: () => Promise<void>;
190+
// ==================================
191+
// File Attachment via VS Code Dialog
192+
// ==================================
193+
selectContextFiles: (params: SelectContextFilesRequest) => Promise<Attachment[]>;
188194
}

workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ export interface ExistingFunction {
241241
// ==================================
242242
// Attachment-Related Interfaces
243243
// ==================================
244+
export interface SelectContextFilesRequest {
245+
command: Command | null;
246+
skillCommand?: SkillCommand;
247+
}
248+
244249
export interface Attachment {
245250
name: string;
246251
path?: string

workspaces/ballerina/ballerina-core/src/rpc-types/ai-panel/rpc-type.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ import {
7676
AgentsMdFileInfoDTO,
7777
ParseSkillFileRequest,
7878
ParseSkillFileResponse,
79+
SelectContextFilesRequest,
80+
Attachment,
7981
} from "./interfaces";
8082
import { RequestType, NotificationType } from "vscode-messenger-common";
8183

@@ -162,3 +164,4 @@ export const mcpLoadErrorsChanged: NotificationType<McpLoadErrorsDTO> = { method
162164
export const getAgentsMdFileInfo: RequestType<void, AgentsMdFileInfoDTO> = { method: `${_preFix}/getAgentsMdFileInfo` };
163165
export const openOrCreateAgentsMd: RequestType<void, void> = { method: `${_preFix}/openOrCreateAgentsMd` };
164166
export const agentsMdFileInfoChanged: NotificationType<AgentsMdFileInfoDTO> = { method: `${_preFix}/agentsMdFileInfoChanged` };
167+
export const selectContextFiles: RequestType<SelectContextFilesRequest, Attachment[]> = { method: `${_preFix}/selectContextFiles` };

workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-handler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ import {
137137
OpenMcpConfigRequest,
138138
getAgentsMdFileInfo,
139139
openOrCreateAgentsMd,
140+
selectContextFiles,
141+
SelectContextFilesRequest,
140142
} from "@wso2/ballerina-core";
141143
import { workspace } from 'vscode';
142144
import { Messenger } from "vscode-messenger";
@@ -233,6 +235,7 @@ export function registerAiPanelRpcHandlers(messenger: Messenger) {
233235
messenger.onRequest(getMcpLoadErrors, () => rpcManger.getMcpLoadErrors());
234236
messenger.onRequest(getAgentsMdFileInfo, () => rpcManger.getAgentsMdFileInfo());
235237
messenger.onRequest(openOrCreateAgentsMd, () => rpcManger.openOrCreateAgentsMd());
238+
messenger.onRequest(selectContextFiles, (args: SelectContextFilesRequest) => rpcManger.selectContextFiles(args));
236239

237240
// Push updates to the webview whenever the set of running services changes.
238241
runningServicesManager.onChange = (services) => {

workspaces/ballerina/ballerina-extension/src/rpc-managers/ai-panel/rpc-manager.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ import {
2424
AIPanelPrompt,
2525
AbortAIGenerationRequest,
2626
AddFilesToProjectRequest,
27+
Attachment,
28+
AttachmentStatus,
2729
CheckpointInfo,
2830
Command,
31+
SkillCommand,
2932
DocGenerationRequest,
3033
GenerateAgentCodeRequest,
3134
GenerateOpenAPIRequest,
@@ -40,6 +43,7 @@ import {
4043
RestoreCheckpointRequest,
4144
SemanticDiffRequest,
4245
SemanticDiffResponse,
46+
SelectContextFilesRequest,
4347
SubmitFeedbackRequest,
4448
TestGenerationMentions,
4549
UIChatMessage,
@@ -1273,4 +1277,40 @@ User reverted the last made changes. The files have been restored to the state b
12731277
}
12741278
}
12751279

1280+
async selectContextFiles(params: SelectContextFilesRequest): Promise<Attachment[]> {
1281+
const projectPath = StateMachine.context().projectPath;
1282+
const defaultUri = projectPath ? vscode.Uri.file(projectPath) : vscode.Uri.file(os.homedir());
1283+
1284+
const uris = await vscode.window.showOpenDialog({
1285+
canSelectMany: true,
1286+
canSelectFiles: true,
1287+
canSelectFolders: false,
1288+
defaultUri,
1289+
openLabel: "Attach",
1290+
});
1291+
1292+
if (!uris || uris.length === 0) {
1293+
return [];
1294+
}
1295+
1296+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
1297+
const isDataMapper = params.skillCommand === SkillCommand.DataMap || params.command === Command.TypeCreator;
1298+
1299+
return Promise.all(uris.map(async (uri) => {
1300+
const filePath = uri.fsPath;
1301+
const name = path.basename(filePath);
1302+
try {
1303+
const stat = await fs.promises.stat(filePath);
1304+
if (stat.size > MAX_FILE_SIZE) {
1305+
return { name, status: AttachmentStatus.FileSizeExceeded };
1306+
}
1307+
const content = isDataMapper
1308+
? (await fs.promises.readFile(filePath)).toString("base64")
1309+
: await fs.promises.readFile(filePath, "utf-8");
1310+
return { name, content, status: AttachmentStatus.Success };
1311+
} catch {
1312+
return { name, status: AttachmentStatus.UnknownError };
1313+
}
1314+
}));
1315+
}
12761316
}

workspaces/ballerina/ballerina-rpc-client/src/rpc-clients/ai-panel/rpc-client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ import {
155155
SetMcpToolsEnabledRequest,
156156
McpLoadErrorsDTO,
157157
AgentsMdFileInfoDTO,
158+
selectContextFiles,
159+
SelectContextFilesRequest,
160+
Attachment,
158161
} from "@wso2/ballerina-core";
159162
import { HOST_EXTENSION } from "vscode-messenger-common";
160163
import { Messenger } from "vscode-messenger-webview";
@@ -479,4 +482,8 @@ export class AiPanelRpcClient implements AIPanelAPI {
479482
openOrCreateAgentsMd(): Promise<void> {
480483
return this._messenger.sendRequest(openOrCreateAgentsMd, HOST_EXTENSION);
481484
}
485+
486+
selectContextFiles(params: SelectContextFilesRequest): Promise<Attachment[]> {
487+
return this._messenger.sendRequest(selectContextFiles, HOST_EXTENSION, params);
488+
}
482489
}

workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChat/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,6 +2283,8 @@ const AIChat: React.FC = () => {
22832283
multiple: true,
22842284
acceptResolver: acceptResolver,
22852285
handleAttachmentSelection: handleAttachmentSelection,
2286+
onAttachClick: (command, skillCommand) =>
2287+
rpcClient.getAiPanelRpcClient().selectContextFiles({ command, skillCommand }),
22862288
}}
22872289
suggestedCommandTemplates={footerSuggestedCommandTemplates}
22882290
inputPlaceholder={

workspaces/ballerina/ballerina-visualizer/src/views/AIPanel/components/AIChatInput/hooks/useAttachments.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@
1717
*/
1818

1919
import { useState, useRef, ChangeEvent } from "react";
20-
import { Attachment, Command, SkillCommand } from "@wso2/ballerina-core";
20+
import { Attachment, AttachmentStatus, Command, SkillCommand } from "@wso2/ballerina-core";
2121

2222
export interface AttachmentOptions {
2323
multiple: boolean;
2424
acceptResolver: (command: Command | null, skillCommand?: SkillCommand) => string;
2525
handleAttachmentSelection: (e: ChangeEvent<HTMLInputElement>, command: Command | null, skillCommand?: SkillCommand) => Promise<Attachment[]>;
26+
onAttachClick?: (command: Command | null, skillCommand?: SkillCommand) => Promise<Attachment[]>;
2627
}
2728

2829
interface UseAttachmentsProps {
@@ -35,9 +36,30 @@ export function useAttachments({ attachmentOptions, activeCommand, activeSkillCo
3536
const [attachments, setAttachments] = useState<Attachment[]>([]);
3637
const fileInputRef = useRef<HTMLInputElement | null>(null);
3738

38-
// open file input
39-
function handleAttachClick() {
40-
if (fileInputRef.current) {
39+
// open file picker
40+
async function handleAttachClick() {
41+
if (attachmentOptions.onAttachClick) {
42+
try {
43+
const results = await attachmentOptions.onAttachClick(activeCommand, activeSkillCommand);
44+
setAttachments((prev) => {
45+
const updated = [...prev];
46+
results
47+
.filter((newFile) => newFile.status === AttachmentStatus.Success)
48+
.forEach((newFile) => {
49+
const existingIndex = updated.findIndex(
50+
(existing) => existing.name === newFile.name && existing.content === newFile.content
51+
);
52+
if (existingIndex !== -1) {
53+
updated.splice(existingIndex, 1);
54+
}
55+
updated.push(newFile);
56+
});
57+
return updated;
58+
});
59+
} catch (error) {
60+
console.error("Failed to select context files", error);
61+
}
62+
} else if (fileInputRef.current) {
4163
fileInputRef.current.click();
4264
}
4365
}

0 commit comments

Comments
 (0)