Skip to content

Commit 6c89dc1

Browse files
Use VS Code native file dialog for AI panel attachments
1 parent 1f256ea commit 6c89dc1

8 files changed

Lines changed: 89 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
@@ -56,6 +56,8 @@ import {
5656
RunningServiceInfo,
5757
StopRunningServiceRequest,
5858
RunServiceRequest,
59+
SelectContextFilesRequest,
60+
Attachment,
5961
} from "./interfaces";
6062

6163
export interface AIPanelAPI {
@@ -140,4 +142,8 @@ export interface AIPanelAPI {
140142
// Vertex AI BYOK Helpers
141143
// ==================================
142144
getDefaultVertexCredsPath: () => Promise<string>;
145+
// ==================================
146+
// File Attachment via VS Code Dialog
147+
// ==================================
148+
selectContextFiles: (params: SelectContextFilesRequest) => Promise<Attachment[]>;
143149
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ export interface ExistingFunction {
240240
// ==================================
241241
// Attachment-Related Interfaces
242242
// ==================================
243+
export interface SelectContextFilesRequest {
244+
command: Command | null;
245+
}
246+
243247
export interface Attachment {
244248
name: string;
245249
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
@@ -58,6 +58,8 @@ import {
5858
RunningServiceInfo,
5959
StopRunningServiceRequest,
6060
RunServiceRequest,
61+
SelectContextFilesRequest,
62+
Attachment,
6163
} from "./interfaces";
6264
import { RequestType, NotificationType } from "vscode-messenger-common";
6365

@@ -121,3 +123,4 @@ export const stopRunningService: RequestType<StopRunningServiceRequest, boolean>
121123
export const runService: RequestType<RunServiceRequest, boolean> = { method: `${_preFix}/runService` };
122124
export const runningServicesChanged: NotificationType<RunningServiceInfo[]> = { method: `${_preFix}/runningServicesChanged` };
123125
export const getDefaultVertexCredsPath: RequestType<void, string> = { method: `${_preFix}/getDefaultVertexCredsPath` };
126+
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
@@ -106,6 +106,8 @@ import {
106106
runService,
107107
RunServiceRequest,
108108
getDefaultVertexCredsPath,
109+
selectContextFiles,
110+
SelectContextFilesRequest,
109111
} from "@wso2/ballerina-core";
110112
import { workspace } from 'vscode';
111113
import { Messenger } from "vscode-messenger";
@@ -182,6 +184,7 @@ export function registerAiPanelRpcHandlers(messenger: Messenger) {
182184
messenger.onRequest(stopRunningService, (args: StopRunningServiceRequest) => rpcManger.stopRunningService(args));
183185
messenger.onRequest(runService, (args: RunServiceRequest) => rpcManger.runService(args));
184186
messenger.onRequest(getDefaultVertexCredsPath, () => rpcManger.getDefaultVertexCredsPath());
187+
messenger.onRequest(selectContextFiles, (args: SelectContextFilesRequest) => rpcManger.selectContextFiles(args));
185188

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

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {
2424
AIPanelPrompt,
2525
AbortAIGenerationRequest,
2626
AddFilesToProjectRequest,
27+
Attachment,
28+
AttachmentStatus,
2729
CheckpointInfo,
2830
Command,
2931
DocGenerationRequest,
@@ -42,6 +44,7 @@ import {
4244
RestoreCheckpointRequest,
4345
SemanticDiffRequest,
4446
SemanticDiffResponse,
47+
SelectContextFilesRequest,
4548
SubmitFeedbackRequest,
4649
TestGenerationMentions,
4750
UIChatMessage,
@@ -864,4 +867,39 @@ User reverted the last made changes. The files have been restored to the state b
864867
return "";
865868
}
866869

870+
async selectContextFiles(params: SelectContextFilesRequest): Promise<Attachment[]> {
871+
const projectPath = StateMachine.context().projectPath;
872+
const defaultUri = projectPath ? vscode.Uri.file(projectPath) : vscode.Uri.file(os.homedir());
873+
874+
const uris = await vscode.window.showOpenDialog({
875+
canSelectMany: true,
876+
canSelectFiles: true,
877+
canSelectFolders: false,
878+
defaultUri,
879+
openLabel: "Attach",
880+
});
881+
882+
if (!uris || uris.length === 0) {
883+
return [];
884+
}
885+
886+
const MAX_FILE_SIZE = 5 * 1024 * 1024;
887+
const isDataMapper = params.command === Command.DataMap || params.command === Command.TypeCreator;
888+
889+
return uris.map((uri) => {
890+
const filePath = uri.fsPath;
891+
const name = path.basename(filePath);
892+
try {
893+
if (fs.statSync(filePath).size > MAX_FILE_SIZE) {
894+
return { name, status: AttachmentStatus.FileSizeExceeded };
895+
}
896+
const content = isDataMapper
897+
? fs.readFileSync(filePath).toString("base64")
898+
: fs.readFileSync(filePath, "utf-8");
899+
return { name, content, status: AttachmentStatus.Success };
900+
} catch {
901+
return { name, status: AttachmentStatus.UnknownError };
902+
}
903+
});
904+
}
867905
}

‎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
@@ -117,6 +117,9 @@ import {
117117
RunServiceRequest,
118118
runService,
119119
getDefaultVertexCredsPath,
120+
selectContextFiles,
121+
SelectContextFilesRequest,
122+
Attachment,
120123
} from "@wso2/ballerina-core";
121124
import { HOST_EXTENSION } from "vscode-messenger-common";
122125
import { Messenger } from "vscode-messenger-webview";
@@ -361,4 +364,8 @@ export class AiPanelRpcClient implements AIPanelAPI {
361364
getDefaultVertexCredsPath(): Promise<string> {
362365
return this._messenger.sendRequest(getDefaultVertexCredsPath, HOST_EXTENSION);
363366
}
367+
368+
selectContextFiles(params: SelectContextFilesRequest): Promise<Attachment[]> {
369+
return this._messenger.sendRequest(selectContextFiles, HOST_EXTENSION, params);
370+
}
364371
}

‎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
@@ -2180,6 +2180,8 @@ const AIChat: React.FC = () => {
21802180
multiple: true,
21812181
acceptResolver: acceptResolver,
21822182
handleAttachmentSelection: handleAttachmentSelection,
2183+
onAttachClick: (command) =>
2184+
rpcClient.getAiPanelRpcClient().selectContextFiles({ command }),
21832185
}}
21842186
suggestedCommandTemplates={footerSuggestedCommandTemplates}
21852187
inputPlaceholder={footerInputPlaceholder}

‎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 } from "@wso2/ballerina-core";
20+
import { Attachment, AttachmentStatus, Command } from "@wso2/ballerina-core";
2121

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

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

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

0 commit comments

Comments
 (0)