Skip to content
Open
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
108 changes: 108 additions & 0 deletions packages/app/e2e/prompt/prompt-drop-native-event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { test, expect } from "../fixtures"
import { promptSelector } from "../selectors"

test("desktop native drop event inserts a file pill", async ({ page, gotoSession }) => {
await gotoSession()

const prompt = page.locator(promptSelector)
await prompt.click()

const path = process.platform === "win32" ? "C:\\opencode-e2e-native-drop.ts" : "/tmp/opencode-e2e-native-drop.ts"

await page.evaluate((value) => {
window.dispatchEvent(new CustomEvent("opencode:native-file-drop", { detail: { paths: [value] } }))
}, path)

const pill = page.locator(`${promptSelector} [data-type="file"]`).first()
await expect(pill).toBeVisible()
await expect(pill).toHaveAttribute("data-path", path)
})

test("native and browser drop do not duplicate file pill", async ({ page, gotoSession }) => {
await gotoSession()

const prompt = page.locator(promptSelector)
await prompt.click()

const path =
process.platform === "win32" ? "C:\\opencode-e2e-native-drop-once.ts" : "/tmp/opencode-e2e-native-drop-once.ts"

await page.evaluate((value) => {
window.dispatchEvent(new CustomEvent("opencode:native-file-drop", { detail: { paths: [value] } }))
}, path)

const dt = await page.evaluateHandle((value) => {
const dt = new DataTransfer()
dt.setData("text/plain", value)
return dt
}, path)

await page.dispatchEvent("body", "drop", { dataTransfer: dt })

const pills = page.locator(`${promptSelector} [data-type="file"][data-path="${path}"]`)
await expect(pills).toHaveCount(1)
})

test("desktop native drop event adds image attachment preview", async ({ page, gotoSession }) => {
const dataUrl =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3+4uQAAAAASUVORK5CYII="

await page.addInitScript((url) => {
const target = window as Window & {
__OPENCODE__?: {
readAttachmentFromPath?: (
path: string,
) =>
| Promise<{ filename: string; mime: string; dataUrl: string } | null>
| { filename: string; mime: string; dataUrl: string }
| null
}
}
target.__OPENCODE__ ??= {}
target.__OPENCODE__.readAttachmentFromPath = async (path: string) => {
if (!path.toLowerCase().endsWith(".png")) return null
return { filename: "native-drop.png", mime: "image/png", dataUrl: url }
}
}, dataUrl)

await gotoSession()

const prompt = page.locator(promptSelector)
await prompt.click()

const path = process.platform === "win32" ? "C:\\opencode-e2e-native-drop.png" : "/tmp/opencode-e2e-native-drop.png"

await page.evaluate((value) => {
window.dispatchEvent(new CustomEvent("opencode:native-file-drop", { detail: { paths: [value] } }))
}, path)

await expect(page.locator('img[alt="native-drop.png"]').first()).toBeVisible()
await expect(page.locator(`${promptSelector} [data-type="file"][data-path="${path}"]`)).toHaveCount(0)
})

test("browser and native drop do not duplicate file pill", async ({ page, gotoSession }) => {
await gotoSession()

const prompt = page.locator(promptSelector)
await prompt.click()

const path =
process.platform === "win32"
? "C:\\opencode-e2e-native-drop-reverse.ts"
: "/tmp/opencode-e2e-native-drop-reverse.ts"

const dt = await page.evaluateHandle((value) => {
const dt = new DataTransfer()
dt.setData("text/plain", value)
return dt
}, path)

await page.dispatchEvent("body", "drop", { dataTransfer: dt })

await page.evaluate((value) => {
window.dispatchEvent(new CustomEvent("opencode:native-file-drop", { detail: { paths: [value] } }))
}, path)

const pills = page.locator(`${promptSelector} [data-type="file"][data-path="${path}"]`)
await expect(pills).toHaveCount(1)
})
6 changes: 6 additions & 0 deletions packages/app/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ declare global {
updaterEnabled?: boolean
deepLinks?: string[]
wsl?: boolean
readAttachmentFromPath?: (
path: string,
) =>
| Promise<{ filename: string; mime: string; dataUrl: string } | null>
| { filename: string; mime: string; dataUrl: string }
| null
}
api?: {
setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise<void>
Expand Down
15 changes: 13 additions & 2 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return true
}

const pickDesktopFiles = async () => {
const result = await platform.openFilePickerDialog?.({
title: language.t("prompt.action.attachFile"),
multiple: true,
})
if (!result) return
for (const path of Array.from(new Set(Array.isArray(result) ? result : [result]))) {
await addPath(path)
}
}
const addToHistory = (prompt: Prompt, mode: "normal" | "shell") => {
const currentHistory = mode === "shell" ? shellHistory : history
const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
Expand Down Expand Up @@ -1043,7 +1053,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return true
}

const { addAttachment, removeAttachment, handlePaste } = createPromptAttachments({
const { addAttachment, addPath, removeImageAttachment, handlePaste } = createPromptAttachments({
editor: () => editorRef,
isDialogActive: () => !!dialog.active,
setDraggingType: (type) => setStore("draggingType", type),
Expand All @@ -1053,6 +1063,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
},
addPart,
readClipboardImage: platform.readClipboardImage,
readAttachmentFromPath: platform.readAttachmentFromPath,
})

const variants = createMemo(() => ["default", ...local.model.variant.list()])
Expand Down Expand Up @@ -1293,7 +1304,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onOpen={(attachment) =>
dialog.show(() => <ImagePreview src={attachment.dataUrl} alt={attachment.filename} />)
}
onRemove={removeAttachment}
onRemove={removeImageAttachment}
removeLabel={language.t("prompt.attachment.remove")}
/>
<div
Expand Down
Loading
Loading