Skip to content

Commit bf0312f

Browse files
Copilotsawka
andauthored
Add drag-and-drop from preview directory to WaveAI panel (#2502)
Enables dragging files from preview directory listings directly into the WaveAI panel for analysis. ## Changes **Modified `frontend/app/aipanel/aipanel.tsx`:** - Added `useDrop` hook to accept `FILE_ITEM` drag type from preview directory - Implemented `handleFileItemDrop` to: - Read file content via `RpcApi.FileReadCommand` using the remote URI - Convert base64 data to browser `File` object with proper MIME type - Validate and add to panel using existing `model.addFile()` flow - Integrated with existing drag overlay for visual feedback - Rejects directories with appropriate error messaging ## Implementation ```typescript const handleFileItemDrop = useCallback( async (draggedFile: DraggedFile) => { if (draggedFile.isDir) { model.setError("Cannot add directories to Wave AI. Please select a file."); return; } const fileData = await RpcApi.FileReadCommand(TabRpcClient, { info: { path: draggedFile.uri } }, null); const bytes = new Uint8Array(atob(fileData.data64).split('').map(c => c.charCodeAt(0))); const file = new File([bytes], draggedFile.relName, { type: fileData.info?.mimetype || "application/octet-stream" }); // Existing validation and addFile flow await model.addFile(file); }, [model] ); const [{ isOver, canDrop }, drop] = useDrop(() => ({ accept: "FILE_ITEM", drop: handleFileItemDrop, collect: (monitor) => ({ isOver: monitor.isOver(), canDrop: monitor.canDrop() }) }), [handleFileItemDrop]); ``` No changes required to preview directory—it already exports `FILE_ITEM` drag items. Works independently from native file system drag-and-drop. Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
1 parent a19cb6f commit bf0312f

4 files changed

Lines changed: 301 additions & 39 deletions

File tree

frontend/app/aipanel/ai-utils.ts

Lines changed: 174 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
const TextFileLimit = 200 * 1024; // 200KB
5+
const PdfLimit = 5 * 1024 * 1024; // 5MB
6+
const ImageLimit = 10 * 1024 * 1024; // 10MB
7+
const ImagePreviewSize = 128;
8+
const ImagePreviewWebPQuality = 0.8;
9+
const ImageMaxEdge = 4096;
10+
411
export const isAcceptableFile = (file: File): boolean => {
512
const acceptableTypes = [
613
// Images
@@ -34,10 +41,15 @@ export const isAcceptableFile = (file: File): boolean => {
3441
const extension = file.name.split(".").pop()?.toLowerCase();
3542
const acceptableExtensions = [
3643
"txt",
44+
"log",
3745
"md",
3846
"js",
47+
"mjs",
48+
"cjs",
3949
"jsx",
4050
"ts",
51+
"mts",
52+
"cts",
4153
"tsx",
4254
"go",
4355
"py",
@@ -47,10 +59,15 @@ export const isAcceptableFile = (file: File): boolean => {
4759
"h",
4860
"hpp",
4961
"html",
62+
"htm",
5063
"css",
5164
"scss",
5265
"sass",
5366
"json",
67+
"jsonc",
68+
"json5",
69+
"jsonl",
70+
"ndjson",
5471
"xml",
5572
"yaml",
5673
"yml",
@@ -69,9 +86,116 @@ export const isAcceptableFile = (file: File): boolean => {
6986
"clj",
7087
"ex",
7188
"exs",
89+
"ini",
90+
"toml",
91+
"conf",
92+
"cfg",
93+
"env",
94+
"zsh",
95+
"fish",
96+
"ps1",
97+
"psm1",
98+
"bazel",
99+
"bzl",
100+
"csv",
101+
"tsv",
102+
"properties",
103+
"ipynb",
104+
"rmd",
105+
"gradle",
106+
"groovy",
107+
"cmake",
72108
];
73109

74-
return extension ? acceptableExtensions.includes(extension) : false;
110+
if (extension && acceptableExtensions.includes(extension)) {
111+
return true;
112+
}
113+
114+
// Check for specific filenames (case-insensitive)
115+
const fileName = file.name.toLowerCase();
116+
const acceptableFilenames = [
117+
"makefile",
118+
"dockerfile",
119+
"containerfile",
120+
"go.mod",
121+
"go.sum",
122+
"go.work",
123+
"go.work.sum",
124+
"package.json",
125+
"package-lock.json",
126+
"yarn.lock",
127+
"pnpm-lock.yaml",
128+
"composer.json",
129+
"composer.lock",
130+
"gemfile",
131+
"gemfile.lock",
132+
"podfile",
133+
"podfile.lock",
134+
"cargo.toml",
135+
"cargo.lock",
136+
"pipfile",
137+
"pipfile.lock",
138+
"requirements.txt",
139+
"setup.py",
140+
"pyproject.toml",
141+
"poetry.lock",
142+
"build.gradle",
143+
"settings.gradle",
144+
"pom.xml",
145+
"build.xml",
146+
"readme",
147+
"readme.md",
148+
"license",
149+
"license.md",
150+
"changelog",
151+
"changelog.md",
152+
"contributing",
153+
"contributing.md",
154+
"authors",
155+
"codeowners",
156+
"procfile",
157+
"jenkinsfile",
158+
"vagrantfile",
159+
"rakefile",
160+
"gruntfile.js",
161+
"gulpfile.js",
162+
"webpack.config.js",
163+
"rollup.config.js",
164+
"vite.config.js",
165+
"jest.config.js",
166+
"vitest.config.js",
167+
".dockerignore",
168+
".gitignore",
169+
".gitattributes",
170+
".gitmodules",
171+
".editorconfig",
172+
".eslintrc",
173+
".prettierrc",
174+
".pylintrc",
175+
".bashrc",
176+
".bash_profile",
177+
".bash_login",
178+
".bash_logout",
179+
".profile",
180+
".zshrc",
181+
".zprofile",
182+
".zshenv",
183+
".zlogin",
184+
".zlogout",
185+
".kshrc",
186+
".cshrc",
187+
".tcshrc",
188+
".xonshrc",
189+
".shrc",
190+
".aliases",
191+
".functions",
192+
".exports",
193+
".direnvrc",
194+
".vimrc",
195+
".gvimrc",
196+
];
197+
198+
return acceptableFilenames.includes(fileName);
75199
};
76200

77201
export const getFileIcon = (fileName: string, fileType: string): string => {
@@ -182,34 +306,30 @@ export interface FileSizeError {
182306
}
183307

184308
export const validateFileSize = (file: File): FileSizeError | null => {
185-
const TEXT_FILE_LIMIT = 200 * 1024; // 200KB
186-
const PDF_LIMIT = 5 * 1024 * 1024; // 5MB
187-
const IMAGE_LIMIT = 10 * 1024 * 1024; // 10MB
188-
189309
if (file.type.startsWith("image/")) {
190-
if (file.size > IMAGE_LIMIT) {
310+
if (file.size > ImageLimit) {
191311
return {
192312
fileName: file.name,
193313
fileSize: file.size,
194-
maxSize: IMAGE_LIMIT,
314+
maxSize: ImageLimit,
195315
fileType: "image",
196316
};
197317
}
198318
} else if (file.type === "application/pdf") {
199-
if (file.size > PDF_LIMIT) {
319+
if (file.size > PdfLimit) {
200320
return {
201321
fileName: file.name,
202322
fileSize: file.size,
203-
maxSize: PDF_LIMIT,
323+
maxSize: PdfLimit,
204324
fileType: "pdf",
205325
};
206326
}
207327
} else {
208-
if (file.size > TEXT_FILE_LIMIT) {
328+
if (file.size > TextFileLimit) {
209329
return {
210330
fileName: file.name,
211331
fileSize: file.size,
212-
maxSize: TEXT_FILE_LIMIT,
332+
maxSize: TextFileLimit,
213333
fileType: "text",
214334
};
215335
}
@@ -218,6 +338,37 @@ export const validateFileSize = (file: File): FileSizeError | null => {
218338
return null;
219339
};
220340

341+
export const validateFileSizeFromInfo = (
342+
fileName: string,
343+
fileSize: number,
344+
mimeType: string
345+
): FileSizeError | null => {
346+
let maxSize: number;
347+
let fileType: "text" | "pdf" | "image";
348+
349+
if (mimeType.startsWith("image/")) {
350+
maxSize = ImageLimit;
351+
fileType = "image";
352+
} else if (mimeType === "application/pdf") {
353+
maxSize = PdfLimit;
354+
fileType = "pdf";
355+
} else {
356+
maxSize = TextFileLimit;
357+
fileType = "text";
358+
}
359+
360+
if (fileSize > maxSize) {
361+
return {
362+
fileName,
363+
fileSize,
364+
maxSize,
365+
fileType,
366+
};
367+
}
368+
369+
return null;
370+
};
371+
221372
export const formatFileSizeError = (error: FileSizeError): string => {
222373
const typeLabel = error.fileType === "image" ? "Image" : error.fileType === "pdf" ? "PDF" : "Text file";
223374
return `${typeLabel} "${error.fileName}" is too large (${formatFileSize(error.fileSize)}). Maximum size is ${formatFileSize(error.maxSize)}.`;
@@ -233,9 +384,6 @@ export const resizeImage = async (file: File): Promise<File> => {
233384
return file;
234385
}
235386

236-
const MAX_EDGE = 4096;
237-
const WEBP_QUALITY = 0.8;
238-
239387
return new Promise((resolve) => {
240388
const img = new Image();
241389
const url = URL.createObjectURL(file);
@@ -246,7 +394,7 @@ export const resizeImage = async (file: File): Promise<File> => {
246394
let { width, height } = img;
247395

248396
// Check if resizing is needed
249-
if (width <= MAX_EDGE && height <= MAX_EDGE) {
397+
if (width <= ImageMaxEdge && height <= ImageMaxEdge) {
250398
// Image is already small enough, just try WebP conversion
251399
const canvas = document.createElement("canvas");
252400
canvas.width = width;
@@ -272,18 +420,18 @@ export const resizeImage = async (file: File): Promise<File> => {
272420
}
273421
},
274422
"image/webp",
275-
WEBP_QUALITY
423+
ImagePreviewWebPQuality
276424
);
277425
return;
278426
}
279427

280428
// Calculate new dimensions while maintaining aspect ratio
281429
if (width > height) {
282-
height = Math.round((height * MAX_EDGE) / width);
283-
width = MAX_EDGE;
430+
height = Math.round((height * ImageMaxEdge) / width);
431+
width = ImageMaxEdge;
284432
} else {
285-
width = Math.round((width * MAX_EDGE) / height);
286-
height = MAX_EDGE;
433+
width = Math.round((width * ImageMaxEdge) / height);
434+
height = ImageMaxEdge;
287435
}
288436

289437
// Create canvas and resize
@@ -312,7 +460,7 @@ export const resizeImage = async (file: File): Promise<File> => {
312460
}
313461
},
314462
"image/webp",
315-
WEBP_QUALITY
463+
ImagePreviewWebPQuality
316464
);
317465
};
318466

@@ -333,9 +481,6 @@ export const createImagePreview = async (file: File): Promise<string | null> =>
333481
return null;
334482
}
335483

336-
const PREVIEW_SIZE = 128;
337-
const WEBP_QUALITY = 0.8;
338-
339484
return new Promise((resolve) => {
340485
const img = new Image();
341486
const url = URL.createObjectURL(file);
@@ -346,11 +491,11 @@ export const createImagePreview = async (file: File): Promise<string | null> =>
346491
let { width, height } = img;
347492

348493
if (width > height) {
349-
height = Math.round((height * PREVIEW_SIZE) / width);
350-
width = PREVIEW_SIZE;
494+
height = Math.round((height * ImagePreviewSize) / width);
495+
width = ImagePreviewSize;
351496
} else {
352-
width = Math.round((width * PREVIEW_SIZE) / height);
353-
height = PREVIEW_SIZE;
497+
width = Math.round((width * ImagePreviewSize) / height);
498+
height = ImagePreviewSize;
354499
}
355500

356501
const canvas = document.createElement("canvas");
@@ -372,7 +517,7 @@ export const createImagePreview = async (file: File): Promise<string | null> =>
372517
}
373518
},
374519
"image/webp",
375-
WEBP_QUALITY
520+
ImagePreviewWebPQuality
376521
);
377522
};
378523

0 commit comments

Comments
 (0)