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
19 changes: 9 additions & 10 deletions packages/app/src/components/prompt-input/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { uuid } from "@/utils/uuid"
import { getCursorPosition } from "./editor-dom"
import { attachmentMime } from "./files"
import { normalizePaste, pasteMode } from "./paste"
import { getDroppedPromptData } from "./drop"

function dataUrl(file: File, mime: string) {
return new Promise<string>((resolve) => {
Expand Down Expand Up @@ -165,19 +166,17 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
event.preventDefault()
input.setDraggingType(null)

const plainText = event.dataTransfer?.getData("text/plain")
const filePrefix = "file:"
if (plainText?.startsWith(filePrefix)) {
const filePath = plainText.slice(filePrefix.length)
input.focusEditor()
input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 })
const dropped = getDroppedPromptData(event.dataTransfer)
if (dropped.files.length > 0) {
await addAttachments(dropped.files)
return
}

const dropped = event.dataTransfer?.files
if (!dropped) return

await addAttachments(Array.from(dropped))
if (dropped.filePath) {
input.focusEditor()
input.addPart({ type: "file", path: dropped.filePath, content: "@" + dropped.filePath, start: 0, end: 0 })
return
}
}

onMount(() => {
Expand Down
28 changes: 28 additions & 0 deletions packages/app/src/components/prompt-input/drop.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test"
import { getDroppedPromptData } from "./drop"

describe("getDroppedPromptData", () => {
test("prefers dropped files over file uri text", () => {
const file = new File(["png"], "drop.png", { type: "image/png" })
const dataTransfer = {
files: [file] as unknown as FileList,
getData: () => "file:/tmp/drop.png",
}

const result = getDroppedPromptData(dataTransfer)
expect(result.files).toEqual([file])
expect(result.filePath).toBeUndefined()
})

test("falls back to file uri text when no files are present", () => {
const dataTransfer = {
files: [] as unknown as FileList,
getData: () => "file:/tmp/drop.png",
}

expect(getDroppedPromptData(dataTransfer)).toEqual({
files: [],
filePath: "/tmp/drop.png",
})
})
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

Current unit coverage only exercises the custom file:/tmp/... style string. If this helper is intended to support real file URIs from the browser (file:///..., encoded characters, UNC/host forms), adding explicit test cases for those formats would help prevent regressions once parsing is corrected.

Suggested change
})
})
test("handles browser-style file URI with triple slash", () => {
const dataTransfer = {
files: [] as unknown as FileList,
getData: () => "file:///tmp/drop.png",
}
expect(getDroppedPromptData(dataTransfer)).toEqual({
files: [],
// current behavior is to strip only the "file:" prefix
filePath: "///tmp/drop.png",
})
})
test("handles file URI with encoded characters", () => {
const dataTransfer = {
files: [] as unknown as FileList,
getData: () => "file:/tmp/My%20File.txt",
}
expect(getDroppedPromptData(dataTransfer)).toEqual({
files: [],
filePath: "/tmp/My%20File.txt",
})
})
test("handles UNC-style file URI with host", () => {
const dataTransfer = {
files: [] as unknown as FileList,
getData: () => "file://server/share/file.txt",
}
expect(getDroppedPromptData(dataTransfer)).toEqual({
files: [],
filePath: "//server/share/file.txt",
})
})

Copilot uses AI. Check for mistakes.
})
17 changes: 17 additions & 0 deletions packages/app/src/components/prompt-input/drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
type DropDataTransfer = Pick<DataTransfer, "files" | "getData">

export function getDroppedPromptData(dataTransfer: DropDataTransfer | null | undefined): {
files: File[]
filePath?: string
} {
const files = Array.from(dataTransfer?.files ?? [])
if (files.length > 0) return { files }

const plainText = dataTransfer?.getData("text/plain")
const filePrefix = "file:"
if (plainText?.startsWith(filePrefix)) {
return { files: [], filePath: plainText.slice(filePrefix.length) }
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

getDroppedPromptData treats any text/plain value starting with file: as a path by doing a raw slice. For standard file URIs (e.g. file:///tmp/a%20b.txt or file://server/share/a.txt) this produces an incorrect filePath (extra leading slashes / missing host handling / no percent-decoding), which can later generate malformed file:// URLs when building request parts. Consider explicitly handling file:// URIs via new URL(...) (and decoding), while keeping the internal file:${path} drag format as a separate case.

Suggested change
return { files: [], filePath: plainText.slice(filePrefix.length) }
let filePath: string
// Handle standards-compliant file:// URIs (e.g. file:///tmp/a%20b.txt, file://server/share/a.txt)
if (plainText.startsWith("file://")) {
try {
const url = new URL(plainText)
const decodedPath = decodeURIComponent(url.pathname)
if (url.host) {
// UNC-style path: //server/share/path
filePath = `//${url.host}${decodedPath}`
} else {
// Local path: /tmp/a b.txt
filePath = decodedPath
}
} catch {
// Fallback to legacy behavior if URL parsing fails
filePath = plainText.slice(filePrefix.length)
}
} else {
// Internal drag format: file:${path}
filePath = plainText.slice(filePrefix.length)
}
return { files: [], filePath }

Copilot uses AI. Check for mistakes.
}

return { files: [] }
}
Loading