From b751b5af494dcd9b79f030a16383aa003b5383e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:07:37 +0000 Subject: [PATCH 1/8] Initial plan From 737c6a72c0dd48fd2f23c58c0174b79c44873869 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:18:12 +0000 Subject: [PATCH 2/8] Add drag-and-drop support from preview directory to AI panel - Import useDrop from react-dnd - Add RpcApi and TabRpcClient imports for file reading - Implement handleFileItemDrop to convert DraggedFile to File object - Add useDrop hook to accept FILE_ITEM drops - Update isDragOver state for FILE_ITEM drags - Handle file reading via RPC API and base64 decoding - Validate file types and sizes before adding to AI panel Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/aipanel/aipanel.tsx | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 96b8540726..e25cf47ec6 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -6,6 +6,8 @@ import { ErrorBoundary } from "@/app/element/errorboundary"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, getSettingsKeyAtom } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; +import { RpcApi } from "@/app/store/wshclientapi"; +import { TabRpcClient } from "@/app/store/wshrpcutil"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { isMacOS } from "@/util/platformutil"; import { cn } from "@/util/util"; @@ -13,6 +15,7 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import * as jotai from "jotai"; import { memo, useCallback, useEffect, useRef, useState } from "react"; +import { useDrop } from "react-dnd"; import { formatFileSizeError, isAcceptableFile, validateFileSize } from "./ai-utils"; import { AIDroppedFiles } from "./aidroppedfiles"; import { AIPanelHeader } from "./aipanelheader"; @@ -381,6 +384,98 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { } }; + // Handle FILE_ITEM drops from preview directory + const handleFileItemDrop = useCallback( + async (draggedFile: DraggedFile) => { + try { + // Don't drop directories + if (draggedFile.isDir) { + model.setError("Cannot add directories to Wave AI. Please select a file."); + return; + } + + // Read the file from the remote URI + const fileData = await RpcApi.FileReadCommand( + TabRpcClient, + { + info: { + path: draggedFile.uri, + }, + }, + null + ); + + if (!fileData.data64) { + model.setError(`Failed to read file: ${draggedFile.relName}`); + return; + } + + // Convert base64 to Uint8Array + const binaryString = atob(fileData.data64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Determine MIME type + const mimeType = fileData.info?.mimetype || "application/octet-stream"; + + // Create a File object + const file = new File([bytes], draggedFile.relName, { type: mimeType }); + + // Validate the file + if (!isAcceptableFile(file)) { + model.setError( + `File type not supported: ${draggedFile.relName}. Supported: images, PDFs, and text/code files.` + ); + return; + } + + const sizeError = validateFileSize(file); + if (sizeError) { + model.setError(formatFileSizeError(sizeError)); + return; + } + + // Add the file to the AI panel + await model.addFile(file); + } catch (error) { + console.error("Error handling FILE_ITEM drop:", error); + model.setError(`Failed to add file: ${error.message || "Unknown error"}`); + } + }, + [model] + ); + + // Set up react-dnd drop handler for FILE_ITEM + const [{ isOver, canDrop }, drop] = useDrop( + () => ({ + accept: "FILE_ITEM", + drop: handleFileItemDrop, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }), + [handleFileItemDrop] + ); + + // Update drag over state for FILE_ITEM drags + useEffect(() => { + if (isOver && canDrop) { + setIsDragOver(true); + } else if (!isOver && isDragOver && !hasFilesDragged({ types: [] } as any)) { + setIsDragOver(false); + } + }, [isOver, canDrop]); + + // Attach the drop ref to the container + useEffect(() => { + if (containerRef.current) { + drop(containerRef.current); + } + }, [drop]); + const handleFocusCapture = useCallback( (event: React.FocusEvent) => { // console.log("Wave AI focus capture", getElemAsStr(event.target)); From c79d97750756bced4907ecc1d2326a7771f660d2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:20:52 +0000 Subject: [PATCH 3/8] Fix drag over state handling for FILE_ITEM drops Simplify the drag over state logic to properly clear when neither FILE_ITEM nor native files are being dragged. Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/aipanel/aipanel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index e25cf47ec6..a37b2cb96b 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -464,7 +464,8 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { useEffect(() => { if (isOver && canDrop) { setIsDragOver(true); - } else if (!isOver && isDragOver && !hasFilesDragged({ types: [] } as any)) { + } else if (!isOver && !hasFilesDragged({ types: [] } as any)) { + // Only clear drag over if neither FILE_ITEM nor native files are being dragged setIsDragOver(false); } }, [isOver, canDrop]); From 16c9b9062ec5410659f4bae0e1c2c01d555edcc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:26:04 +0000 Subject: [PATCH 4/8] Address code review feedback - Improve error handling with instanceof Error check - Simplify drag state logic by removing mock object usage - Add clearer comments for drag state handling Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/app/aipanel/aipanel.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index a37b2cb96b..c049ba4137 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -441,7 +441,8 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { await model.addFile(file); } catch (error) { console.error("Error handling FILE_ITEM drop:", error); - model.setError(`Failed to add file: ${error.message || "Unknown error"}`); + const errorMsg = error instanceof Error ? error.message : String(error); + model.setError(`Failed to add file: ${errorMsg}`); } }, [model] @@ -464,8 +465,9 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { useEffect(() => { if (isOver && canDrop) { setIsDragOver(true); - } else if (!isOver && !hasFilesDragged({ types: [] } as any)) { - // Only clear drag over if neither FILE_ITEM nor native files are being dragged + } else if (!isOver) { + // Clear drag over when FILE_ITEM is no longer over the panel + // Native file drags are handled separately by handleDragLeave setIsDragOver(false); } }, [isOver, canDrop]); From 8b3edf610b7ba4d876f3ec63b5b661690efcd1ea Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Oct 2025 13:48:02 -0700 Subject: [PATCH 5/8] updates to allow react-dnd coexist with native dnd handlers --- frontend/app/aipanel/aipanel.tsx | 114 +++++++++----------------- frontend/app/aipanel/waveai-model.tsx | 51 +++++++++++- 2 files changed, 87 insertions(+), 78 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index c049ba4137..aa06c2bcca 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -6,8 +6,6 @@ import { ErrorBoundary } from "@/app/element/errorboundary"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { atoms, getSettingsKeyAtom } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { isMacOS } from "@/util/platformutil"; import { cn } from "@/util/util"; @@ -212,6 +210,7 @@ interface AIPanelProps { const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { const [isDragOver, setIsDragOver] = useState(false); + const [isReactDndDragOver, setIsReactDndDragOver] = useState(false); const [initialLoadDone, setInitialLoadDone] = useState(false); const model = WaveAIModel.getInstance(); const containerRef = useRef(null); @@ -323,27 +322,43 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { }; const handleDragOver = (e: React.DragEvent) => { + const hasFiles = hasFilesDragged(e.dataTransfer); + + // Only handle native file drags here, let react-dnd handle FILE_ITEM drags + if (!hasFiles) { + return; + } + e.preventDefault(); e.stopPropagation(); - const hasFiles = hasFilesDragged(e.dataTransfer); - if (hasFiles && !isDragOver) { + if (!isDragOver) { setIsDragOver(true); - } else if (!hasFiles && isDragOver) { - setIsDragOver(false); } }; const handleDragEnter = (e: React.DragEvent) => { + const hasFiles = hasFilesDragged(e.dataTransfer); + + // Only handle native file drags here, let react-dnd handle FILE_ITEM drags + if (!hasFiles) { + return; + } + e.preventDefault(); e.stopPropagation(); - if (hasFilesDragged(e.dataTransfer)) { - setIsDragOver(true); - } + setIsDragOver(true); }; const handleDragLeave = (e: React.DragEvent) => { + const hasFiles = hasFilesDragged(e.dataTransfer); + + // Only handle native file drags here, let react-dnd handle FILE_ITEM drags + if (!hasFiles) { + return; + } + e.preventDefault(); e.stopPropagation(); @@ -358,6 +373,12 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { }; const handleDrop = async (e: React.DragEvent) => { + // Check if this is a FILE_ITEM drag from react-dnd + // If so, let react-dnd handle it instead + if (!e.dataTransfer.files.length) { + return; // Let react-dnd handle FILE_ITEM drags + } + e.preventDefault(); e.stopPropagation(); setIsDragOver(false); @@ -384,71 +405,11 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { } }; - // Handle FILE_ITEM drops from preview directory const handleFileItemDrop = useCallback( - async (draggedFile: DraggedFile) => { - try { - // Don't drop directories - if (draggedFile.isDir) { - model.setError("Cannot add directories to Wave AI. Please select a file."); - return; - } - - // Read the file from the remote URI - const fileData = await RpcApi.FileReadCommand( - TabRpcClient, - { - info: { - path: draggedFile.uri, - }, - }, - null - ); - - if (!fileData.data64) { - model.setError(`Failed to read file: ${draggedFile.relName}`); - return; - } - - // Convert base64 to Uint8Array - const binaryString = atob(fileData.data64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - // Determine MIME type - const mimeType = fileData.info?.mimetype || "application/octet-stream"; - - // Create a File object - const file = new File([bytes], draggedFile.relName, { type: mimeType }); - - // Validate the file - if (!isAcceptableFile(file)) { - model.setError( - `File type not supported: ${draggedFile.relName}. Supported: images, PDFs, and text/code files.` - ); - return; - } - - const sizeError = validateFileSize(file); - if (sizeError) { - model.setError(formatFileSizeError(sizeError)); - return; - } - - // Add the file to the AI panel - await model.addFile(file); - } catch (error) { - console.error("Error handling FILE_ITEM drop:", error); - const errorMsg = error instanceof Error ? error.message : String(error); - model.setError(`Failed to add file: ${errorMsg}`); - } - }, + (draggedFile: DraggedFile) => model.addFileFromRemoteUri(draggedFile), [model] ); - // Set up react-dnd drop handler for FILE_ITEM const [{ isOver, canDrop }, drop] = useDrop( () => ({ accept: "FILE_ITEM", @@ -463,12 +424,11 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { // Update drag over state for FILE_ITEM drags useEffect(() => { + console.log("FILE_ITEM drag state:", { isOver, canDrop }); if (isOver && canDrop) { - setIsDragOver(true); - } else if (!isOver) { - // Clear drag over when FILE_ITEM is no longer over the panel - // Native file drags are handled separately by handleDragLeave - setIsDragOver(false); + setIsReactDndDragOver(true); + } else { + setIsReactDndDragOver(false); } }, [isOver, canDrop]); @@ -551,7 +511,7 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { "bg-gray-900 flex flex-col relative h-[calc(100%-4px)]", model.inBuilder ? "mt-0" : "mt-1", className, - isDragOver && "bg-gray-800 border-accent", + (isDragOver || isReactDndDragOver) && "bg-gray-800 border-accent", isFocused ? "border-2 border-accent" : "border-2 border-transparent" )} style={{ @@ -567,7 +527,7 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { onClick={handleClick} inert={!isPanelVisible ? true : undefined} > - {isDragOver && } + {(isDragOver || isReactDndDragOver) && } {showBlockMask && } diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index 98942100b9..ff64e2dd5d 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -19,7 +19,8 @@ import { getWebServerEndpoint } from "@/util/endpoints"; import { ChatStatus } from "ai"; import * as jotai from "jotai"; import type React from "react"; -import { createDataUrl, createImagePreview, normalizeMimeType, resizeImage } from "./ai-utils"; +import { base64ToArrayBuffer } from "@/util/util"; +import { createDataUrl, createImagePreview, isAcceptableFile, normalizeMimeType, resizeImage, validateFileSize, formatFileSizeError } from "./ai-utils"; import type { AIPanelInputRef } from "./aipanelinput"; export interface DroppedFile { @@ -151,6 +152,54 @@ export class WaveAIModel { return droppedFile; } + async addFileFromRemoteUri(draggedFile: DraggedFile): Promise { + if (draggedFile.isDir) { + this.setError("Cannot add directories to Wave AI. Please select a file."); + return; + } + + try { + const fileData = await RpcApi.FileReadCommand( + TabRpcClient, + { + info: { + path: draggedFile.uri, + }, + }, + null + ); + + if (!fileData.data64) { + this.setError(`Failed to read file: ${draggedFile.relName}`); + return; + } + + const buffer = base64ToArrayBuffer(fileData.data64); + + const mimeType = fileData.info?.mimetype || "application/octet-stream"; + const file = new File([buffer], draggedFile.relName, { type: mimeType }); + + if (!isAcceptableFile(file)) { + this.setError( + `File type not supported: ${draggedFile.relName}. Supported: images, PDFs, and text/code files.` + ); + return; + } + + const sizeError = validateFileSize(file); + if (sizeError) { + this.setError(formatFileSizeError(sizeError)); + return; + } + + await this.addFile(file); + } catch (error) { + console.error("Error handling FILE_ITEM drop:", error); + const errorMsg = error instanceof Error ? error.message : String(error); + this.setError(`Failed to add file: ${errorMsg}`); + } + } + removeFile(fileId: string) { const currentFiles = globalStore.get(this.droppedFiles); const updatedFiles = currentFiles.filter((f) => f.id !== fileId); From d59c0580e89efd85c40fcb4c9fe55dcb25401c57 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Oct 2025 13:55:23 -0700 Subject: [PATCH 6/8] check filesize/info before reading full contents --- frontend/app/aipanel/ai-utils.ts | 35 ++++++++++++++++--- frontend/app/aipanel/aipanel.tsx | 13 ++++---- frontend/app/aipanel/waveai-model.tsx | 48 ++++++++++++++++----------- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/frontend/app/aipanel/ai-utils.ts b/frontend/app/aipanel/ai-utils.ts index 0e80886b2f..c8f33089b5 100644 --- a/frontend/app/aipanel/ai-utils.ts +++ b/frontend/app/aipanel/ai-utils.ts @@ -181,11 +181,11 @@ export interface FileSizeError { fileType: "text" | "pdf" | "image"; } -export const validateFileSize = (file: File): FileSizeError | null => { - const TEXT_FILE_LIMIT = 200 * 1024; // 200KB - const PDF_LIMIT = 5 * 1024 * 1024; // 5MB - const IMAGE_LIMIT = 10 * 1024 * 1024; // 10MB +const TEXT_FILE_LIMIT = 200 * 1024; // 200KB +const PDF_LIMIT = 5 * 1024 * 1024; // 5MB +const IMAGE_LIMIT = 10 * 1024 * 1024; // 10MB +export const validateFileSize = (file: File): FileSizeError | null => { if (file.type.startsWith("image/")) { if (file.size > IMAGE_LIMIT) { return { @@ -218,6 +218,33 @@ export const validateFileSize = (file: File): FileSizeError | null => { return null; }; +export const validateFileSizeFromInfo = (fileName: string, fileSize: number, mimeType: string): FileSizeError | null => { + let maxSize: number; + let fileType: "text" | "pdf" | "image"; + + if (mimeType.startsWith("image/")) { + maxSize = IMAGE_LIMIT; + fileType = "image"; + } else if (mimeType === "application/pdf") { + maxSize = PDF_LIMIT; + fileType = "pdf"; + } else { + maxSize = TEXT_FILE_LIMIT; + fileType = "text"; + } + + if (fileSize > maxSize) { + return { + fileName, + fileSize, + maxSize, + fileType, + }; + } + + return null; +}; + export const formatFileSizeError = (error: FileSizeError): string => { const typeLabel = error.fileType === "image" ? "Image" : error.fileType === "pdf" ? "PDF" : "Text file"; return `${typeLabel} "${error.fileName}" is too large (${formatFileSize(error.fileSize)}). Maximum size is ${formatFileSize(error.maxSize)}.`; diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index aa06c2bcca..43ace431b4 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -323,12 +323,12 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { const handleDragOver = (e: React.DragEvent) => { const hasFiles = hasFilesDragged(e.dataTransfer); - + // Only handle native file drags here, let react-dnd handle FILE_ITEM drags if (!hasFiles) { return; } - + e.preventDefault(); e.stopPropagation(); @@ -339,12 +339,12 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { const handleDragEnter = (e: React.DragEvent) => { const hasFiles = hasFilesDragged(e.dataTransfer); - + // Only handle native file drags here, let react-dnd handle FILE_ITEM drags if (!hasFiles) { return; } - + e.preventDefault(); e.stopPropagation(); @@ -353,12 +353,12 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { const handleDragLeave = (e: React.DragEvent) => { const hasFiles = hasFilesDragged(e.dataTransfer); - + // Only handle native file drags here, let react-dnd handle FILE_ITEM drags if (!hasFiles) { return; } - + e.preventDefault(); e.stopPropagation(); @@ -424,7 +424,6 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => { // Update drag over state for FILE_ITEM drags useEffect(() => { - console.log("FILE_ITEM drag state:", { isOver, canDrop }); if (isOver && canDrop) { setIsReactDndDragOver(true); } else { diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index ff64e2dd5d..68a114e2e3 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -16,11 +16,19 @@ import { TabRpcClient } from "@/app/store/wshrpcutil"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; import { BuilderFocusManager } from "@/builder/store/builder-focusmanager"; import { getWebServerEndpoint } from "@/util/endpoints"; +import { base64ToArrayBuffer } from "@/util/util"; import { ChatStatus } from "ai"; import * as jotai from "jotai"; import type React from "react"; -import { base64ToArrayBuffer } from "@/util/util"; -import { createDataUrl, createImagePreview, isAcceptableFile, normalizeMimeType, resizeImage, validateFileSize, formatFileSizeError } from "./ai-utils"; +import { + createDataUrl, + createImagePreview, + formatFileSizeError, + isAcceptableFile, + normalizeMimeType, + resizeImage, + validateFileSizeFromInfo, +} from "./ai-utils"; import type { AIPanelInputRef } from "./aipanelinput"; export interface DroppedFile { @@ -159,26 +167,32 @@ export class WaveAIModel { } try { - const fileData = await RpcApi.FileReadCommand( - TabRpcClient, - { - info: { - path: draggedFile.uri, - }, - }, - null - ); + const fileInfo = await RpcApi.FileInfoCommand(TabRpcClient, { info: { path: draggedFile.uri } }, null); + if (fileInfo.notfound) { + this.setError(`File not found: ${draggedFile.relName}`); + return; + } + if (fileInfo.isdir) { + this.setError("Cannot add directories to Wave AI. Please select a file."); + return; + } + + const mimeType = fileInfo.mimetype || "application/octet-stream"; + const fileSize = fileInfo.size || 0; + const sizeError = validateFileSizeFromInfo(draggedFile.relName, fileSize, mimeType); + if (sizeError) { + this.setError(formatFileSizeError(sizeError)); + return; + } + const fileData = await RpcApi.FileReadCommand(TabRpcClient, { info: { path: draggedFile.uri } }, null); if (!fileData.data64) { this.setError(`Failed to read file: ${draggedFile.relName}`); return; } const buffer = base64ToArrayBuffer(fileData.data64); - - const mimeType = fileData.info?.mimetype || "application/octet-stream"; const file = new File([buffer], draggedFile.relName, { type: mimeType }); - if (!isAcceptableFile(file)) { this.setError( `File type not supported: ${draggedFile.relName}. Supported: images, PDFs, and text/code files.` @@ -186,12 +200,6 @@ export class WaveAIModel { return; } - const sizeError = validateFileSize(file); - if (sizeError) { - this.setError(formatFileSizeError(sizeError)); - return; - } - await this.addFile(file); } catch (error) { console.error("Error handling FILE_ITEM drop:", error); From 1ddaed0f48ce87d5081eb4bfb3fde9e8a22c1e43 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Oct 2025 15:00:01 -0700 Subject: [PATCH 7/8] extract consts and better file list... --- frontend/app/aipanel/ai-utils.ts | 175 +++++++++++++++++++++++++------ 1 file changed, 142 insertions(+), 33 deletions(-) diff --git a/frontend/app/aipanel/ai-utils.ts b/frontend/app/aipanel/ai-utils.ts index c8f33089b5..94d4a6281c 100644 --- a/frontend/app/aipanel/ai-utils.ts +++ b/frontend/app/aipanel/ai-utils.ts @@ -1,6 +1,13 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 +const TextFileLimit = 200 * 1024; // 200KB +const PdfLimit = 5 * 1024 * 1024; // 5MB +const ImageLimit = 10 * 1024 * 1024; // 10MB +const ImagePreviewSize = 128; +const ImagePreviewWebPQuality = 0.8; +const ImageMaxEdge = 4096; + export const isAcceptableFile = (file: File): boolean => { const acceptableTypes = [ // Images @@ -34,10 +41,15 @@ export const isAcceptableFile = (file: File): boolean => { const extension = file.name.split(".").pop()?.toLowerCase(); const acceptableExtensions = [ "txt", + "log", "md", "js", + "mjs", + "cjs", "jsx", "ts", + "mts", + "cts", "tsx", "go", "py", @@ -47,10 +59,15 @@ export const isAcceptableFile = (file: File): boolean => { "h", "hpp", "html", + "htm", "css", "scss", "sass", "json", + "jsonc", + "json5", + "jsonl", + "ndjson", "xml", "yaml", "yml", @@ -69,9 +86,107 @@ export const isAcceptableFile = (file: File): boolean => { "clj", "ex", "exs", + "ini", + "toml", + "conf", + "cfg", + "env", + "zsh", + "fish", + "ps1", + "psm1", + "bazel", + "bzl", + "csv", + "tsv", + "properties", + "ipynb", + "rmd", + "gradle", + "groovy", + "cmake", ]; - return extension ? acceptableExtensions.includes(extension) : false; + if (extension && acceptableExtensions.includes(extension)) { + return true; + } + + // Check for specific filenames (case-insensitive) + const fileName = file.name.toLowerCase(); + const acceptableFilenames = [ + "makefile", + "dockerfile", + "containerfile", + "go.mod", + "go.sum", + "go.work", + "go.work.sum", + "package.json", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "composer.json", + "composer.lock", + "gemfile", + "gemfile.lock", + "podfile", + "podfile.lock", + "cargo.toml", + "cargo.lock", + "pipfile", + "pipfile.lock", + "requirements.txt", + "setup.py", + "pyproject.toml", + "poetry.lock", + "build.gradle", + "settings.gradle", + "pom.xml", + "build.xml", + "readme", + "readme.md", + "license", + "license.md", + "changelog", + "changelog.md", + "contributing", + "contributing.md", + "authors", + "codeowners", + "procfile", + "jenkinsfile", + "vagrantfile", + "rakefile", + "gruntfile.js", + "gulpfile.js", + "webpack.config.js", + "rollup.config.js", + "vite.config.js", + "jest.config.js", + "vitest.config.js", + ".dockerignore", + ".gitignore", + ".gitattributes", + ".gitmodules", + ".editorconfig", + ".eslintrc", + ".prettierrc", + ".pylintrc", + ".bashrc", + ".bash_profile", + ".bash_login", + ".bash_logout", + ".profile", + ".zshrc", + ".zprofile", + ".zshenv", + ".zlogin", + ".zlogout", + ".vimrc", + ".gvimrc", + ]; + + return acceptableFilenames.includes(fileName); }; export const getFileIcon = (fileName: string, fileType: string): string => { @@ -181,35 +296,31 @@ export interface FileSizeError { fileType: "text" | "pdf" | "image"; } -const TEXT_FILE_LIMIT = 200 * 1024; // 200KB -const PDF_LIMIT = 5 * 1024 * 1024; // 5MB -const IMAGE_LIMIT = 10 * 1024 * 1024; // 10MB - export const validateFileSize = (file: File): FileSizeError | null => { if (file.type.startsWith("image/")) { - if (file.size > IMAGE_LIMIT) { + if (file.size > ImageLimit) { return { fileName: file.name, fileSize: file.size, - maxSize: IMAGE_LIMIT, + maxSize: ImageLimit, fileType: "image", }; } } else if (file.type === "application/pdf") { - if (file.size > PDF_LIMIT) { + if (file.size > PdfLimit) { return { fileName: file.name, fileSize: file.size, - maxSize: PDF_LIMIT, + maxSize: PdfLimit, fileType: "pdf", }; } } else { - if (file.size > TEXT_FILE_LIMIT) { + if (file.size > TextFileLimit) { return { fileName: file.name, fileSize: file.size, - maxSize: TEXT_FILE_LIMIT, + maxSize: TextFileLimit, fileType: "text", }; } @@ -218,18 +329,22 @@ export const validateFileSize = (file: File): FileSizeError | null => { return null; }; -export const validateFileSizeFromInfo = (fileName: string, fileSize: number, mimeType: string): FileSizeError | null => { +export const validateFileSizeFromInfo = ( + fileName: string, + fileSize: number, + mimeType: string +): FileSizeError | null => { let maxSize: number; let fileType: "text" | "pdf" | "image"; if (mimeType.startsWith("image/")) { - maxSize = IMAGE_LIMIT; + maxSize = ImageLimit; fileType = "image"; } else if (mimeType === "application/pdf") { - maxSize = PDF_LIMIT; + maxSize = PdfLimit; fileType = "pdf"; } else { - maxSize = TEXT_FILE_LIMIT; + maxSize = TextFileLimit; fileType = "text"; } @@ -260,9 +375,6 @@ export const resizeImage = async (file: File): Promise => { return file; } - const MAX_EDGE = 4096; - const WEBP_QUALITY = 0.8; - return new Promise((resolve) => { const img = new Image(); const url = URL.createObjectURL(file); @@ -273,7 +385,7 @@ export const resizeImage = async (file: File): Promise => { let { width, height } = img; // Check if resizing is needed - if (width <= MAX_EDGE && height <= MAX_EDGE) { + if (width <= ImageMaxEdge && height <= ImageMaxEdge) { // Image is already small enough, just try WebP conversion const canvas = document.createElement("canvas"); canvas.width = width; @@ -299,18 +411,18 @@ export const resizeImage = async (file: File): Promise => { } }, "image/webp", - WEBP_QUALITY + ImagePreviewWebPQuality ); return; } // Calculate new dimensions while maintaining aspect ratio if (width > height) { - height = Math.round((height * MAX_EDGE) / width); - width = MAX_EDGE; + height = Math.round((height * ImageMaxEdge) / width); + width = ImageMaxEdge; } else { - width = Math.round((width * MAX_EDGE) / height); - height = MAX_EDGE; + width = Math.round((width * ImageMaxEdge) / height); + height = ImageMaxEdge; } // Create canvas and resize @@ -339,7 +451,7 @@ export const resizeImage = async (file: File): Promise => { } }, "image/webp", - WEBP_QUALITY + ImagePreviewWebPQuality ); }; @@ -360,9 +472,6 @@ export const createImagePreview = async (file: File): Promise => return null; } - const PREVIEW_SIZE = 128; - const WEBP_QUALITY = 0.8; - return new Promise((resolve) => { const img = new Image(); const url = URL.createObjectURL(file); @@ -373,11 +482,11 @@ export const createImagePreview = async (file: File): Promise => let { width, height } = img; if (width > height) { - height = Math.round((height * PREVIEW_SIZE) / width); - width = PREVIEW_SIZE; + height = Math.round((height * ImagePreviewSize) / width); + width = ImagePreviewSize; } else { - width = Math.round((width * PREVIEW_SIZE) / height); - height = PREVIEW_SIZE; + width = Math.round((width * ImagePreviewSize) / height); + height = ImagePreviewSize; } const canvas = document.createElement("canvas"); @@ -399,7 +508,7 @@ export const createImagePreview = async (file: File): Promise => } }, "image/webp", - WEBP_QUALITY + ImagePreviewWebPQuality ); }; From 05dee9f885729dfa7acc0c411b8380a421a38753 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 31 Oct 2025 16:27:49 -0700 Subject: [PATCH 8/8] sync more known files... --- frontend/app/aipanel/ai-utils.ts | 9 +++++++++ frontend/app/view/preview/preview-edit.tsx | 3 +++ 2 files changed, 12 insertions(+) diff --git a/frontend/app/aipanel/ai-utils.ts b/frontend/app/aipanel/ai-utils.ts index 94d4a6281c..1477db6af5 100644 --- a/frontend/app/aipanel/ai-utils.ts +++ b/frontend/app/aipanel/ai-utils.ts @@ -182,6 +182,15 @@ export const isAcceptableFile = (file: File): boolean => { ".zshenv", ".zlogin", ".zlogout", + ".kshrc", + ".cshrc", + ".tcshrc", + ".xonshrc", + ".shrc", + ".aliases", + ".functions", + ".exports", + ".direnvrc", ".vimrc", ".gvimrc", ]; diff --git a/frontend/app/view/preview/preview-edit.tsx b/frontend/app/view/preview/preview-edit.tsx index a7ec656cc2..3e79dbced2 100644 --- a/frontend/app/view/preview/preview-edit.tsx +++ b/frontend/app/view/preview/preview-edit.tsx @@ -20,6 +20,7 @@ export const shellFileMap: Record = { ".profile": "shell", ".zshrc": "shell", ".zprofile": "shell", + ".zshenv": "shell", ".zlogin": "shell", ".zlogout": "shell", ".kshrc": "shell", @@ -31,6 +32,8 @@ export const shellFileMap: Record = { ".functions": "shell", ".exports": "shell", ".direnvrc": "shell", + ".vimrc": "shell", + ".gvimrc": "shell", }; function CodeEditPreview({ model }: SpecializedViewProps) {