feat: add drag and drop file support in chat input#1228
feat: add drag and drop file support in chat input#1228fbricon merged 2 commits intoopenkaiden:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds drag-and-drop and attach-file size-validated handling to the multimodal input, registers a chat preference for max attachment size, exposes a renderer-to-main file-size IPC, and adds Vitest + Svelte tests for drag/drop, file reading, and oversized-file rejection. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Component as MultimodalInput
participant Reader as FileReader
participant Config as Main/ChatSettings
participant Toast
User->>Component: dragenter / dragover (DataTransfer)
Component-->>User: preventDefault, set isDragging (apply dashed border)
User->>Component: drop (with DataTransfer.files)
Component->>Component: snapshot FileList -> files[]
Component->>Config: request maxAttachmentFileSize
alt file size OK
Component->>Reader: read File as Data URL
Reader-->>Component: data URL
Component->>Component: append attachment {url, name, contentType}
else file too large
Component->>Toast: toast.error("file too large: " + filename)
end
Component-->>User: clear dragging state (remove dashed border)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 210-213: The drag handler is intercepting every drag type; update
handleDragOver to bail out unless event.dataTransfer?.types includes "Files" —
only then call event.preventDefault() and set isDragging = true. Apply the same
guard in the corresponding drop handler (the function around lines 228-233) so
non-file drags (text/URL) are not prevented and do not trigger file-drop UI; use
the DragEvent.dataTransfer.types check to decide whether to proceed.
- Around line 215-217: The dragleave handler handleDragLeave currently sets
isDragging = false on any child-to-parent move causing flicker; modify
handleDragLeave (and corresponding handleDragEnter if present) to only clear
isDragging when the pointer truly leaves the container by either (a) maintaining
a dragDepth counter incremented in handleDragEnter and decremented in
handleDragLeave and only setting isDragging = false when depth reaches 0, or (b)
using the event.relatedTarget and container.contains(relatedTarget) check to
ignore leaves into child elements; update the functions that reference
isDragging accordingly so child movements do not toggle the drag state.
- Around line 219-225: The drag-and-drop flow currently calls
readFileAsDataUrl(file) without validating file.size; add a size cap (e.g.,
const MAX_FILE_SIZE_BYTES = /* choose limit */) and perform a pre-check in the
drag-and-drop handler before calling readFileAsDataUrl: if file.size >
MAX_FILE_SIZE_BYTES, do not call readFileAsDataUrl, surface a user-facing error
(use the existing UI error/toast mechanism in this component) and reject/skip
that file; keep readFileAsDataUrl unchanged and only use it after the size check
so large files never get read into memory or base64-encoded.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: af30c274-e5f0-4421-a90f-6e19d0157438
📒 Files selected for processing (2)
packages/renderer/src/lib/chat/components/multimodal-input.spec.tspackages/renderer/src/lib/chat/components/multimodal-input.svelte
packages/renderer/src/lib/chat/components/multimodal-input.svelte
Outdated
Show resolved
Hide resolved
da3c442 to
6dfb199
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (3)
packages/renderer/src/lib/chat/components/multimodal-input.svelte (3)
215-217:⚠️ Potential issue | 🟡 MinorPrevent drag-highlight flicker when moving across child elements.
Line 216 clears
isDraggingon anydragleave, so moving over textarea/button children can drop the highlight even while still inside the drop zone.💡 Suggested fix (drag depth tracking)
let isDragging = $state(false); +let dragDepth = $state(0); @@ +function handleDragEnter(event: DragEvent): void { + if (!isFileDrag(event)) return; + event.preventDefault(); + dragDepth += 1; + isDragging = true; +} + -function handleDragLeave(): void { - isDragging = false; +function handleDragLeave(event: DragEvent): void { + if (!isFileDrag(event)) return; + event.preventDefault(); + dragDepth = Math.max(0, dragDepth - 1); + if (dragDepth === 0) isDragging = false; } @@ async function handleDrop(event: DragEvent): Promise<void> { if (!isFileDrag(event)) return; event.preventDefault(); + dragDepth = 0; isDragging = false; @@ <div @@ + ondragenter={handleDragEnter} ondragover={handleDragOver} ondragleave={handleDragLeave}Also applies to: 301-303
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around lines 215 - 217, The drag highlight flicker happens because handleDragLeave() directly sets isDragging = false on any dragleave; change to track drag depth instead: add a numeric counter (e.g., dragDepth) that you increment in the dragenter handler and decrement in handleDragLeave, only setting isDragging = true when dragDepth > 0 and false when dragDepth === 0; update both handleDragLeave and the corresponding enter/leave pair referenced around handleDragLeave and the similar handlers at lines ~301-303 so child element transitions don't prematurely clear isDragging.
210-213:⚠️ Potential issue | 🟠 MajorGate drag/drop handling to file drags only.
Line 211 and Line 229 currently intercept every drag payload. This blocks normal text/URL drops and shows file-drop styling for non-file drags.
💡 Suggested fix
+function isFileDrag(event: DragEvent): boolean { + return event.dataTransfer?.types?.includes('Files') ?? false; +} + function handleDragOver(event: DragEvent): void { + if (!isFileDrag(event)) return; event.preventDefault(); isDragging = true; } @@ async function handleDrop(event: DragEvent): Promise<void> { + if (!isFileDrag(event)) return; event.preventDefault(); isDragging = false; const files = event.dataTransfer?.files; if (!files?.length) return;Also applies to: 228-233
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around lines 210 - 213, handleDragOver currently prevents all drag payloads and sets file-drop styling for non-file drags; change it to only intercept file drags by checking event.dataTransfer?.types for "Files" before calling event.preventDefault() and setting isDragging. Apply the same gating logic to the corresponding drop/leave handlers (e.g., handleDrop and any handleDragLeave/handleDragEnter around lines 228-233) so non-file text/URL drops are not blocked and styling only appears for actual file drags.
238-245:⚠️ Potential issue | 🟠 MajorAdd a file-size guard before Data URL conversion.
Line 239 reads dropped files fully into renderer memory and base64-encodes them without bounds. Large files can cause noticeable UI hangs/memory spikes.
💡 Suggested fix
+const MAX_DND_ATTACHMENT_BYTES = 20 * 1024 * 1024; @@ for (const file of fileList) { + if (file.size > MAX_DND_ATTACHMENT_BYTES) { + toast.error(`${file.name} is too large to attach via drag and drop.`); + continue; + } const url = await readFileAsDataUrl(file); attachments.push({ url, name: file.name, contentType: file.type, }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around lines 238 - 245, Add a pre-read file-size guard to avoid reading huge files into memory: define a MAX_ATTACHMENT_BYTES constant and, inside the loop over fileList (where readFileAsDataUrl is called), check file.size against that limit before awaiting readFileAsDataUrl; if the file is too large, skip converting it and surface a user-facing error/notification (e.g., call a notifyFileTooLarge(file) helper or push an error to the component state) and continue to the next file, otherwise proceed to create the attachment object and push to attachments. Ensure the check is applied in the same block that currently references readFileAsDataUrl, fileList, and attachments so the behavior is consistent across dropped files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 215-217: The drag highlight flicker happens because
handleDragLeave() directly sets isDragging = false on any dragleave; change to
track drag depth instead: add a numeric counter (e.g., dragDepth) that you
increment in the dragenter handler and decrement in handleDragLeave, only
setting isDragging = true when dragDepth > 0 and false when dragDepth === 0;
update both handleDragLeave and the corresponding enter/leave pair referenced
around handleDragLeave and the similar handlers at lines ~301-303 so child
element transitions don't prematurely clear isDragging.
- Around line 210-213: handleDragOver currently prevents all drag payloads and
sets file-drop styling for non-file drags; change it to only intercept file
drags by checking event.dataTransfer?.types for "Files" before calling
event.preventDefault() and setting isDragging. Apply the same gating logic to
the corresponding drop/leave handlers (e.g., handleDrop and any
handleDragLeave/handleDragEnter around lines 228-233) so non-file text/URL drops
are not blocked and styling only appears for actual file drags.
- Around line 238-245: Add a pre-read file-size guard to avoid reading huge
files into memory: define a MAX_ATTACHMENT_BYTES constant and, inside the loop
over fileList (where readFileAsDataUrl is called), check file.size against that
limit before awaiting readFileAsDataUrl; if the file is too large, skip
converting it and surface a user-facing error/notification (e.g., call a
notifyFileTooLarge(file) helper or push an error to the component state) and
continue to the next file, otherwise proceed to create the attachment object and
push to attachments. Ensure the check is applied in the same block that
currently references readFileAsDataUrl, fileList, and attachments so the
behavior is consistent across dropped files.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 400c3079-b356-46d1-b636-8147673392dc
📒 Files selected for processing (2)
packages/renderer/src/lib/chat/components/multimodal-input.spec.tspackages/renderer/src/lib/chat/components/multimodal-input.svelte
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/renderer/src/lib/chat/components/multimodal-input.spec.ts
| let isDragging = $state(false); | ||
| let dragDepth = 0; | ||
|
|
||
| const MAX_DND_FILE_BYTES = 20 * 1024 * 1024; // 20 MB |
There was a problem hiding this comment.
we might want to :
- use a different max file size (ollama client uses 10MB)
- apply the same limit to attachments via the button (for UX consistency)
Ideas?
There was a problem hiding this comment.
could be a global configuration property of Kortex
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Uses FileReader to read dropped files as data URLs, which works with Electron's context isolation (File.path is not available). Only file drags are intercepted (text/URL drags pass through). A depth counter prevents visual flicker when dragging over child elements. Files over 20 MB are rejected with a toast to avoid loading large blobs into renderer memory. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> Signed-off-by: Fred Bricon <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 195-200: The current code uses
MAX_FILE_SIZE_KEY/DEFAULT_MAX_FILE_SIZE_MB and getMaxFileSizeBytes() for both
picker and drag-and-drop paths, which allows large drops to be base64-loaded
into renderer memory; introduce a separate, stricter DnD limit (e.g.
DEFAULT_MAX_DND_FILE_SIZE_MB and a getMaxDndFileSizeBytes() or a constant
MAX_DND_FILE_SIZE_BYTES) and enforce it before calling readFileAsDataUrl() in
the DnD handling code (and similarly in the other DnD-related block around the
readFileAsDataUrl usage referenced). Ensure the DnD path checks the new limit
and rejects/alerts on oversize files while leaving the general attachment
setting (MAX_FILE_SIZE_KEY/getMaxFileSizeBytes) unchanged for picker uploads.
- Around line 289-293: Normalize empty MIME types for dropped files before
pushing to attachments: in multimodal-input.svelte where attachments.push is
used, compute a contentType from file.type that converts an empty string (or
whitespace) into a fallback like 'application/octet-stream' (so it matches
picker attachments) and use that contentType in the attachments object; ensure
submitForm() now receives a non-empty mediaType for dropped files.
- Around line 217-220: The oversize-toast currently extracts the filename using
filepath[0].lastIndexOf('/') which fails on Windows paths; update the extraction
in the block that calls getMaxFileSizeBytes(), window.pathFileSize(), and
rejectOversizedFile() to use a cross-platform basename (e.g., use Node's
path.basename or split on both '/' and '\' and take the last segment) so the
toast shows just the filename regardless of OS; keep the rest of the logic (size
comparison and calling rejectOversizedFile) unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7fdb991e-f13e-40d6-afd2-fc6ad8880de4
📒 Files selected for processing (6)
packages/api/src/chat/chat-settings.tspackages/main/src/plugin/chat-init.tspackages/main/src/plugin/index.tspackages/preload/src/index.tspackages/renderer/src/lib/chat/components/multimodal-input.spec.tspackages/renderer/src/lib/chat/components/multimodal-input.svelte
✅ Files skipped from review due to trivial changes (1)
- packages/api/src/chat/chat-settings.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/renderer/src/lib/chat/components/multimodal-input.spec.ts
| const MAX_FILE_SIZE_KEY = `${ChatSettings.SectionName}.${ChatSettings.MaxDndFileSizeMB}`; | ||
| const DEFAULT_MAX_FILE_SIZE_MB = 20; | ||
|
|
||
| async function getMaxFileSizeBytes(): Promise<number> { | ||
| const maxSizeMB = (await window.getConfigurationValue<number>(MAX_FILE_SIZE_KEY)) ?? DEFAULT_MAX_FILE_SIZE_MB; | ||
| return maxSizeMB * 1024 * 1024; |
There was a problem hiding this comment.
Separate the drop limit from the general attachment limit.
The picker path keeps a file:// URL, but the drop path base64-encodes the full payload in renderer memory. Reusing the same configurable limit here means a higher attachment cap can still let a large drop hang the chat input. Keep a stricter hard ceiling for DnD or give it its own setting before readFileAsDataUrl().
Also applies to: 269-288
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around
lines 195 - 200, The current code uses
MAX_FILE_SIZE_KEY/DEFAULT_MAX_FILE_SIZE_MB and getMaxFileSizeBytes() for both
picker and drag-and-drop paths, which allows large drops to be base64-loaded
into renderer memory; introduce a separate, stricter DnD limit (e.g.
DEFAULT_MAX_DND_FILE_SIZE_MB and a getMaxDndFileSizeBytes() or a constant
MAX_DND_FILE_SIZE_BYTES) and enforce it before calling readFileAsDataUrl() in
the DnD handling code (and similarly in the other DnD-related block around the
readFileAsDataUrl usage referenced). Ensure the DnD path checks the new limit
and rejects/alerts on oversize files while leaving the general attachment
setting (MAX_FILE_SIZE_KEY/getMaxFileSizeBytes) unchanged for picker uploads.
There was a problem hiding this comment.
we'll keep the same limit for now, for consistency
packages/renderer/src/lib/chat/components/multimodal-input.svelte
Outdated
Show resolved
Hide resolved
Register a chat.maxAttachmentFileSize setting (default 20 MB) via ChatInit, and enforce it in both the file picker and drag-and-drop attachment paths. Oversized files are rejected with a toast that includes a Settings action linking to the chat preferences page. Adds pathFileSize IPC call to retrieve file size from the main process. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> Signed-off-by: Fred Bricon <[email protected]>


Following up on #1155, this PR adds Drag n' drop support for file attachments to the model chat.
Uses FileReader to read dropped files as data URLs, which works with Electron's context isolation (File.path is not available). Visualfeedback with dashed border on drag over.
dnd-files.mp4