Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { Terminal } from "../../integrations/terminal/Terminal"
import { openFile } from "../../integrations/misc/open-file"
import { openImage, saveImage } from "../../integrations/misc/image-handler"
import { selectImages } from "../../integrations/misc/process-images"
import { selectFiles } from "../../integrations/misc/process-files"
import { getTheme } from "../../integrations/theme/getTheme"
import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
import { searchWorkspaceFiles } from "../../services/search/file-search"
Expand Down Expand Up @@ -381,6 +382,15 @@ export const webviewMessageHandler = async (
messageTs: message.messageTs,
})
break
case "selectFiles":
const files = await selectFiles()
await provider.postMessageToWebview({
type: "selectedFiles",
files,
context: message.context,
messageTs: message.messageTs,
})
break
case "exportCurrentTask":
const currentTaskId = provider.getCurrentCline()?.taskId
if (currentTaskId) {
Expand Down
246 changes: 246 additions & 0 deletions src/integrations/misc/process-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import * as vscode from "vscode"
import fs from "fs/promises"
import * as path from "path"

// File size limit: 10MB
const MAX_FILE_SIZE = 10 * 1024 * 1024

// Supported file types with their MIME types
const FILE_TYPE_CATEGORIES = {
images: ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"],
documents: ["pdf", "doc", "docx", "txt", "rtf", "odt", "md"],
code: ["js", "ts", "py", "java", "cpp", "c", "h", "hpp", "html", "css", "json", "xml", "yaml", "yml", "php", "rb", "go", "rs", "swift", "kt", "scala", "sh", "bat", "ps1"],
data: ["csv", "xls", "xlsx", "sql", "db", "sqlite"],
archives: ["zip", "rar", "tar", "gz", "7z"],
config: ["ini", "conf", "config", "env", "properties"],
}

// Text file extensions that should be read as content
const TEXT_FILE_EXTENSIONS = [
...FILE_TYPE_CATEGORIES.code,
...FILE_TYPE_CATEGORIES.documents.filter(ext => ["txt", "md"].includes(ext)),
...FILE_TYPE_CATEGORIES.config,
...FILE_TYPE_CATEGORIES.data.filter(ext => ["csv", "sql"].includes(ext)),
]

export interface ProcessedFile {
name: string
path: string
size: number
type: string
category: string
content?: string // For text files
dataUrl?: string // For images and binary files
error?: string
}

export async function selectFiles(): Promise<ProcessedFile[]> {
const options: vscode.OpenDialogOptions = {
canSelectMany: true,
openLabel: "Select",
filters: {
"All Files": ["*"],
"Images": FILE_TYPE_CATEGORIES.images,
"Documents": FILE_TYPE_CATEGORIES.documents,
"Code Files": FILE_TYPE_CATEGORIES.code,
"Data Files": FILE_TYPE_CATEGORIES.data,
"Archives": FILE_TYPE_CATEGORIES.archives,
"Config Files": FILE_TYPE_CATEGORIES.config,
},
}

const fileUris = await vscode.window.showOpenDialog(options)

if (!fileUris || fileUris.length === 0) {
return []
}

return await Promise.all(
fileUris.map(async (uri: vscode.Uri) => {
try {
return await processFile(uri.fsPath)
} catch (error) {
const fileName = path.basename(uri.fsPath)
return {
name: fileName,
path: uri.fsPath,
size: 0,
type: "unknown",
category: "unknown",
error: error instanceof Error ? error.message : String(error),
}
}
}),
)
}

export async function processFile(filePath: string): Promise<ProcessedFile> {
const fileName = path.basename(filePath)
const fileExt = path.extname(filePath).toLowerCase().slice(1)

// Check file size
const stats = await fs.stat(filePath)
if (stats.size > MAX_FILE_SIZE) {
throw new Error(`File size (${formatFileSize(stats.size)}) exceeds the 10MB limit`)
}

const mimeType = getMimeType(filePath)
const category = getFileCategory(fileExt)

const processedFile: ProcessedFile = {
name: fileName,
path: filePath,
size: stats.size,
type: mimeType,
category,
}

// Handle different file types
if (category === "images") {
// Process images as data URLs (existing behavior)
const buffer = await fs.readFile(filePath)
const base64 = buffer.toString("base64")
processedFile.dataUrl = `data:${mimeType};base64,${base64}`
} else if (isTextFile(fileExt)) {
// Read text files as content
try {
processedFile.content = await fs.readFile(filePath, "utf-8")
} catch (error) {
// If UTF-8 reading fails, treat as binary
const buffer = await fs.readFile(filePath)
const base64 = buffer.toString("base64")
processedFile.dataUrl = `data:${mimeType};base64,${base64}`
}
} else {
// Handle binary files as base64
const buffer = await fs.readFile(filePath)
const base64 = buffer.toString("base64")
processedFile.dataUrl = `data:${mimeType};base64,${base64}`
}

return processedFile
}

// Backward compatibility function for existing image functionality
export async function selectImages(): Promise<string[]> {
const options: vscode.OpenDialogOptions = {
canSelectMany: true,
openLabel: "Select",
filters: {
Images: FILE_TYPE_CATEGORIES.images,
},
}

const fileUris = await vscode.window.showOpenDialog(options)

if (!fileUris || fileUris.length === 0) {
return []
}

return await Promise.all(
fileUris.map(async (uri: vscode.Uri) => {
const filePath = uri.fsPath
const buffer = await fs.readFile(filePath)
const base64 = buffer.toString("base64")
const mimeType = getMimeType(filePath)
return `data:${mimeType};base64,${base64}`
}),
)
}

function getMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase()

// Image types
switch (ext) {
case ".png": return "image/png"
case ".jpeg":
case ".jpg": return "image/jpeg"
case ".gif": return "image/gif"
case ".bmp": return "image/bmp"
case ".webp": return "image/webp"
case ".svg": return "image/svg+xml"

// Document types
case ".pdf": return "application/pdf"
case ".doc": return "application/msword"
case ".docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
case ".txt": return "text/plain"
case ".rtf": return "application/rtf"
case ".odt": return "application/vnd.oasis.opendocument.text"
case ".md": return "text/markdown"

// Code types
case ".js": return "text/javascript"
case ".ts": return "text/typescript"
case ".py": return "text/x-python"
case ".java": return "text/x-java-source"
case ".cpp":
case ".c": return "text/x-c"
case ".h":
case ".hpp": return "text/x-c"
case ".html": return "text/html"
case ".css": return "text/css"
case ".json": return "application/json"
case ".xml": return "application/xml"
case ".yaml":
case ".yml": return "application/x-yaml"
case ".php": return "text/x-php"
case ".rb": return "text/x-ruby"
case ".go": return "text/x-go"
case ".rs": return "text/x-rust"
case ".swift": return "text/x-swift"
case ".kt": return "text/x-kotlin"
case ".scala": return "text/x-scala"
case ".sh": return "text/x-shellscript"
case ".bat": return "text/x-msdos-batch"
case ".ps1": return "text/x-powershell"

// Data types
case ".csv": return "text/csv"
case ".xls": return "application/vnd.ms-excel"
case ".xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case ".sql": return "text/x-sql"
case ".db":
case ".sqlite": return "application/x-sqlite3"

// Archive types
case ".zip": return "application/zip"
case ".rar": return "application/x-rar-compressed"
case ".tar": return "application/x-tar"
case ".gz": return "application/gzip"
case ".7z": return "application/x-7z-compressed"

// Config types
case ".ini": return "text/plain"
case ".conf":
case ".config": return "text/plain"
case ".env": return "text/plain"
case ".properties": return "text/plain"

default: return "application/octet-stream"
}
}

function getFileCategory(extension: string): string {
for (const [category, extensions] of Object.entries(FILE_TYPE_CATEGORIES)) {
if (extensions.includes(extension)) {
return category
}
}
return "other"
}

function isTextFile(extension: string): boolean {
return TEXT_FILE_EXTENSIONS.includes(extension)
}

function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes"

const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))

return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface ExtensionMessage {
| "action"
| "state"
| "selectedImages"
| "selectedFiles"
| "theme"
| "workspaceUpdated"
| "invoke"
Expand Down Expand Up @@ -123,6 +124,7 @@ export interface ExtensionMessage {
invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
state?: ExtensionState
images?: string[]
files?: any[] // ProcessedFile array from process-files.ts
filePaths?: string[]
openedTabs?: Array<{
label: string
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface WebviewMessage {
| "clearTask"
| "didShowAnnouncement"
| "selectImages"
| "selectFiles"
| "exportCurrentTask"
| "shareCurrentTask"
| "showTaskWithId"
Expand Down
12 changes: 7 additions & 5 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import Thumbnails from "../common/Thumbnails"
import ModeSelector from "./ModeSelector"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react"
import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal, Paperclip } from "lucide-react"
import { IndexingStatusBadge } from "./IndexingStatusBadge"
import { cn } from "@/lib/utils"
import { usePromptHistory } from "./hooks/usePromptHistory"
Expand All @@ -41,6 +41,7 @@ interface ChatTextAreaProps {
setSelectedImages: React.Dispatch<React.SetStateAction<string[]>>
onSend: () => void
onSelectImages: () => void
onSelectFiles?: () => void
shouldDisableImages: boolean
onHeightChange?: (height: number) => void
mode: Mode
Expand All @@ -63,6 +64,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
setSelectedImages,
onSend,
onSelectImages,
onSelectFiles,
shouldDisableImages,
onHeightChange,
mode,
Expand Down Expand Up @@ -984,11 +986,11 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</StandardTooltip>
)}
<IndexingStatusBadge />
<StandardTooltip content={t("chat:addImages")}>
<StandardTooltip content={t("chat:attachFiles")}>
<button
aria-label={t("chat:addImages")}
aria-label={t("chat:attachFiles")}
disabled={shouldDisableImages}
onClick={!shouldDisableImages ? onSelectImages : undefined}
onClick={!shouldDisableImages ? (onSelectFiles || onSelectImages) : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
Expand All @@ -1003,7 +1005,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
"mr-1",
)}>
<Image className="w-4 h-4" />
<Paperclip className="w-4 h-4" />
</button>
</StandardTooltip>
</div>
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const { info: model } = useSelectedModel(apiConfiguration)

const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
const selectFiles = useCallback(() => vscode.postMessage({ type: "selectFiles" }), [])

const shouldDisableImages =
!model?.supportsImages || sendingDisabled || selectedImages.length >= MAX_IMAGES_PER_MESSAGE
Expand Down Expand Up @@ -1858,6 +1859,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setSelectedImages={setSelectedImages}
onSend={() => handleSendMessage(inputValue, selectedImages)}
onSelectImages={selectImages}
onSelectFiles={selectFiles}
shouldDisableImages={shouldDisableImages}
onHeightChange={() => {
if (isAtBottom) {
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/ca/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"selectApiConfig": "Seleccioneu la configuració de l'API",
"enhancePrompt": "Millora la sol·licitud amb context addicional",
"addImages": "Afegeix imatges al missatge",
"attachFiles": "Attach files to message",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that the new key "attachFiles" still uses an English phrase ('Attach files to message') in a Catalan locale file. Please update it to a Catalan translation.

Suggested change
"attachFiles": "Attach files to message",
"attachFiles": "Adjunta fitxers al missatge",

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

"sendMessage": "Envia el missatge",
"stopTts": "Atura la síntesi de veu",
"typeMessage": "Escriu un missatge...",
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/de/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"selectApiConfig": "API-Konfiguration auswählen",
"enhancePrompt": "Prompt mit zusätzlichem Kontext verbessern",
"addImages": "Bilder zur Nachricht hinzufügen",
"attachFiles": "Attach files to message",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typographical Note: The value for the key "attachFiles" is in English, which is inconsistent with the rest of the German locale file. Consider providing the correct German translation, e.g., "Dateien zur Nachricht anhängen".

Suggested change
"attachFiles": "Attach files to message",
"attachFiles": "Dateien zur Nachricht anhängen",

This comment was generated because it violated a code review rule: irule_C0ez7Rji6ANcGkkX.

"sendMessage": "Nachricht senden",
"stopTts": "Text-in-Sprache beenden",
"typeMessage": "Nachricht eingeben...",
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
},
"enhancePromptDescription": "The 'Enhance Prompt' button helps improve your prompt by providing additional context, clarification, or rephrasing. Try typing a prompt in here and clicking the button again to see how it works.",
"addImages": "Add images to message",
"attachFiles": "Attach files to message",
"sendMessage": "Send message",
"stopTts": "Stop text-to-speech",
"typeMessage": "Type a message...",
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/es/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"selectApiConfig": "Seleccionar configuración de API",
"enhancePrompt": "Mejorar el mensaje con contexto adicional",
"addImages": "Agregar imágenes al mensaje",
"attachFiles": "Attach files to message",
"sendMessage": "Enviar mensaje",
"stopTts": "Detener texto a voz",
"typeMessage": "Escribe un mensaje...",
Expand Down
Loading
Loading