Skip to content

Commit cb10dca

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

8 files changed

Lines changed: 87 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: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
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+
/** If set, clicking "Attach" uses this instead of the hidden file input,
27+
* allowing the VS Code file dialog to open (instead of the browser file picker). */
28+
onAttachClick?: (command: Command | null) => Promise<Attachment[]>;
2629
}
2730

2831
interface UseAttachmentsProps {
@@ -34,9 +37,26 @@ export function useAttachments({ attachmentOptions, activeCommand }: UseAttachme
3437
const [attachments, setAttachments] = useState<Attachment[]>([]);
3538
const fileInputRef = useRef<HTMLInputElement | null>(null);
3639

37-
// open file input
38-
function handleAttachClick() {
39-
if (fileInputRef.current) {
40+
// open file picker
41+
async function handleAttachClick() {
42+
if (attachmentOptions.onAttachClick) {
43+
const results = await attachmentOptions.onAttachClick(activeCommand);
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+
} else if (fileInputRef.current) {
4060
fileInputRef.current.click();
4161
}
4262
}

0 commit comments

Comments
 (0)