Compare commits

...

1 Commits

Author SHA1 Message Date
Hona 85905c9499 fix(app): limit prompt drag state to files 2026-08-18 22:46:52 +00:00
9 changed files with 43 additions and 4 deletions
@@ -1,5 +1,6 @@
import { useFile } from "@/context/file"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { PROMPT_FILE_DRAG_TYPE } from "@opencode-ai/session-ui/v2/prompt-input/drag"
import "@opencode-ai/ui/v2/file-tree-v2.css"
import {
createEffect,
@@ -92,6 +93,7 @@ const FileTreeNodeV2 = (
onDragStart={(event: DragEvent) => {
if (!local.draggable) return
event.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
event.dataTransfer?.setData(PROMPT_FILE_DRAG_TYPE, local.node.path)
event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"
withFileDragImage(event)
@@ -3,6 +3,7 @@ import { encodeFilePath } from "@/context/file/path"
import { Collapsible } from "@opencode-ai/ui/collapsible"
import { FileIcon } from "@opencode-ai/ui/file-icon"
import { Icon } from "@opencode-ai/ui/icon"
import { PROMPT_FILE_DRAG_TYPE } from "@opencode-ai/session-ui/v2/prompt-input/drag"
import {
createEffect,
createMemo,
@@ -157,6 +158,7 @@ const FileTreeNode = (
onDragStart={(event: DragEvent) => {
if (!local.draggable) return
event.dataTransfer?.setData("text/plain", `file:${local.node.path}`)
event.dataTransfer?.setData(PROMPT_FILE_DRAG_TYPE, local.node.path)
event.dataTransfer?.setData("text/uri-list", pathToFileUrl(local.node.path))
if (event.dataTransfer) event.dataTransfer.effectAllowed = "copy"
withFileDragImage(event)
@@ -1,5 +1,6 @@
import { onMount } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import { isPromptAttachmentDrag } from "@opencode-ai/session-ui/v2/prompt-input/drag"
import { showToast } from "@/utils/toast"
import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt"
import { useLanguage } from "@/context/language"
@@ -164,13 +165,13 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
const handleGlobalDragOver = (event: DragEvent) => {
if (input.isDialogActive()) return
if (!isPromptAttachmentDrag(event)) return
event.preventDefault()
const hasFiles = event.dataTransfer?.types.includes("Files")
const hasText = event.dataTransfer?.types.includes("text/plain")
if (hasFiles) {
input.setDraggingType("image")
} else if (hasText) {
} else {
input.setDraggingType("@mention")
}
}
@@ -184,6 +185,7 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
const handleGlobalDrop = async (event: DragEvent) => {
if (input.isDialogActive()) return
if (!isPromptAttachmentDrag(event)) return
event.preventDefault()
input.setDraggingType(null)
+1
View File
@@ -20,6 +20,7 @@
"./v2/*.css": "./src/v2/components/*.css",
"./v2/*": "./src/v2/components/*.tsx",
"./v2/prompt-input": "./src/v2/components/prompt-input/index.tsx",
"./v2/prompt-input/drag": "./src/v2/components/prompt-input/drag.ts",
"./v2/prompt-input/interaction": "./src/v2/components/prompt-input/interaction.ts",
"./v2/prompt-input/store": "./src/v2/components/prompt-input/store.ts",
"./v2/prompt-input/types": "./src/v2/components/prompt-input/types.ts"
@@ -1,6 +1,7 @@
import { onMount } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener"
import type { PromptInputV2Attachment, PromptInputV2Prompt } from "./types"
import { isPromptAttachmentDrag } from "./drag"
const accepted = [
"image/png",
@@ -177,6 +178,7 @@ export function createPromptInputV2Attachments(
}
const handleDrop = async (event: DragEvent) => {
if (input.isDialogActive()) return
if (!isPromptAttachmentDrag(event)) return
event.preventDefault()
input.setDraggingType(null)
const plainText = event.dataTransfer?.getData("text/plain")
@@ -193,9 +195,10 @@ export function createPromptInputV2Attachments(
onMount(() => {
makeEventListener(document, "dragover", (event) => {
if (input.isDialogActive()) return
if (!isPromptAttachmentDrag(event)) return
event.preventDefault()
if (event.dataTransfer?.types.includes("Files")) input.setDraggingType("image")
else if (event.dataTransfer?.types.includes("text/plain")) input.setDraggingType("@mention")
else input.setDraggingType("@mention")
})
makeEventListener(document, "dragleave", (event) => {
if (!input.isDialogActive() && !event.relatedTarget) input.setDraggingType(null)
@@ -0,0 +1,18 @@
import { describe, expect, test } from "bun:test"
import { isPromptAttachmentDrag, PROMPT_FILE_DRAG_TYPE } from "./drag"
describe("prompt attachment drag", () => {
test("ignores ordinary text and link drags", () => {
expect(drag(["text/plain"])).toBe(false)
expect(drag(["text/plain", "text/uri-list"])).toBe(false)
})
test("accepts files and OpenCode file tree entries", () => {
expect(drag(["Files"])).toBe(true)
expect(drag(["text/plain", PROMPT_FILE_DRAG_TYPE])).toBe(true)
})
})
function drag(types: string[]) {
return isPromptAttachmentDrag({ dataTransfer: { types } as unknown as DataTransfer })
}
@@ -0,0 +1,7 @@
export const PROMPT_FILE_DRAG_TYPE = "application/x-opencode-file"
export function isPromptAttachmentDrag(event: Pick<DragEvent, "dataTransfer">) {
const types = event.dataTransfer?.types
if (!types) return false
return types.includes("Files") || types.includes(PROMPT_FILE_DRAG_TYPE)
}
@@ -114,7 +114,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
class="group/prompt-input relative min-h-[96px] w-full overflow-clip rounded-xl bg-v2-background-bg-base"
classList={{
"shadow-[var(--v2-elevation-raised)]": !props.borderUnderlay,
"border border-v2-icon-icon-info border-dashed": state.drag === "active",
"outline outline-1 outline-v2-icon-icon-info outline-dashed": state.drag === "active",
}}
onSubmit={(event) => {
event.preventDefault()
@@ -2,6 +2,7 @@ import { createEffect, on, type Accessor } from "solid-js"
import { createStore, reconcile } from "solid-js/store"
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createPromptInputV2Attachments, type PromptInputV2AttachmentConfig } from "./attachments"
import { isPromptAttachmentDrag } from "./drag"
import { createPromptInputV2Store, type PromptInputV2StoreInput } from "./store"
import type {
PromptInputV2Attachment,
@@ -400,16 +401,19 @@ export function createPromptInputV2Controller(input: {
target.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertFromPaste", data: text }))
},
onDragEnter(event: DragEvent) {
if (attachments && !isPromptAttachmentDrag(event)) return
event.preventDefault()
dispatch({ type: "drag.enter" })
},
onDragOver(event: DragEvent) {
if (attachments && !isPromptAttachmentDrag(event)) return
event.preventDefault()
},
onDragLeave() {
dispatch({ type: "drag.leave" })
},
onDrop(event: DragEvent) {
if (attachments && !isPromptAttachmentDrag(event)) return
event.preventDefault()
dispatch({ type: "drag.leave" })
if (attachments) {