summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorDevin Griffin <[email protected]>2026-02-07 13:33:00 -0600
committerGitHub <[email protected]>2026-02-07 13:33:00 -0600
commit6bdd3528ac1b1824bd560678366ac82bb777db9c (patch)
tree531fb5a371700fc76390e6055787282c3ace2ae9 /packages/app/src/components
parent4efbfcd08735bece0a3e479f23296871780a01b4 (diff)
downloadopencode-6bdd3528ac1b1824bd560678366ac82bb777db9c.tar.gz
opencode-6bdd3528ac1b1824bd560678366ac82bb777db9c.zip
feat(app): drag-n-drop to @mention file (#12569)
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/prompt-input.tsx17
-rw-r--r--packages/app/src/components/prompt-input/attachments.ts21
-rw-r--r--packages/app/src/components/prompt-input/drag-overlay.tsx6
3 files changed, 32 insertions, 12 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index 2bccddc29..3ac64fad2 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -205,7 +205,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
historyIndex: number
savedPrompt: Prompt | null
placeholder: number
- dragging: boolean
+ draggingType: "image" | "@mention" | null
mode: "normal" | "shell"
applyingHistory: boolean
}>({
@@ -213,7 +213,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
historyIndex: -1,
savedPrompt: null,
placeholder: Math.floor(Math.random() * EXAMPLES.length),
- dragging: false,
+ draggingType: null,
mode: "normal",
applyingHistory: false,
})
@@ -760,7 +760,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
editor: () => editorRef,
isFocused,
isDialogActive: () => !!dialog.active,
- setDragging: (value) => setStore("dragging", value),
+ setDraggingType: (type) => setStore("draggingType", type),
+ focusEditor: () => {
+ editorRef.focus()
+ setCursorPosition(editorRef, promptLength(prompt.current()))
+ },
addPart,
})
@@ -946,11 +950,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"group/prompt-input": true,
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
"rounded-[14px] overflow-clip focus-within:shadow-xs-border": true,
- "border-icon-info-active border-dashed": store.dragging,
+ "border-icon-info-active border-dashed": store.draggingType !== null,
[props.class ?? ""]: !!props.class,
}}
>
- <PromptDragOverlay dragging={store.dragging} label={language.t("prompt.dropzone.label")} />
+ <PromptDragOverlay
+ type={store.draggingType}
+ label={language.t(store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label")}
+ />
<PromptContextItems
items={prompt.context.items()}
active={(item) => {
diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts
index 4ea2cfb90..48eda3742 100644
--- a/packages/app/src/components/prompt-input/attachments.ts
+++ b/packages/app/src/components/prompt-input/attachments.ts
@@ -11,7 +11,8 @@ type PromptAttachmentsInput = {
editor: () => HTMLDivElement | undefined
isFocused: () => boolean
isDialogActive: () => boolean
- setDragging: (value: boolean) => void
+ setDraggingType: (type: "image" | "@mention" | null) => void
+ focusEditor: () => void
addPart: (part: ContentPart) => void
}
@@ -84,15 +85,18 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
event.preventDefault()
const hasFiles = event.dataTransfer?.types.includes("Files")
+ const hasText = event.dataTransfer?.types.includes("text/plain")
if (hasFiles) {
- input.setDragging(true)
+ input.setDraggingType("image")
+ } else if (hasText) {
+ input.setDraggingType("@mention")
}
}
const handleGlobalDragLeave = (event: DragEvent) => {
if (input.isDialogActive()) return
if (!event.relatedTarget) {
- input.setDragging(false)
+ input.setDraggingType(null)
}
}
@@ -100,7 +104,16 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
if (input.isDialogActive()) return
event.preventDefault()
- input.setDragging(false)
+ 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 })
+ return
+ }
const dropped = event.dataTransfer?.files
if (!dropped) return
diff --git a/packages/app/src/components/prompt-input/drag-overlay.tsx b/packages/app/src/components/prompt-input/drag-overlay.tsx
index f5a4d399e..e05b47d7c 100644
--- a/packages/app/src/components/prompt-input/drag-overlay.tsx
+++ b/packages/app/src/components/prompt-input/drag-overlay.tsx
@@ -2,16 +2,16 @@ import { Component, Show } from "solid-js"
import { Icon } from "@opencode-ai/ui/icon"
type PromptDragOverlayProps = {
- dragging: boolean
+ type: "image" | "@mention" | null
label: string
}
export const PromptDragOverlay: Component<PromptDragOverlayProps> = (props) => {
return (
- <Show when={props.dragging}>
+ <Show when={props.type !== null}>
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
<div class="flex flex-col items-center gap-2 text-text-weak">
- <Icon name="photo" class="size-8" />
+ <Icon name={props.type === "@mention" ? "link" : "photo"} class="size-8" />
<span class="text-14-regular">{props.label}</span>
</div>
</div>