Skip to content

Commit 6bdd352

Browse files
authored
feat(app): drag-n-drop to @mention file (#12569)
1 parent 4efbfcd commit 6bdd352

19 files changed

Lines changed: 48 additions & 12 deletions

File tree

packages/app/src/components/prompt-input.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,15 +205,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
205205
historyIndex: number
206206
savedPrompt: Prompt | null
207207
placeholder: number
208-
dragging: boolean
208+
draggingType: "image" | "@mention" | null
209209
mode: "normal" | "shell"
210210
applyingHistory: boolean
211211
}>({
212212
popover: null,
213213
historyIndex: -1,
214214
savedPrompt: null,
215215
placeholder: Math.floor(Math.random() * EXAMPLES.length),
216-
dragging: false,
216+
draggingType: null,
217217
mode: "normal",
218218
applyingHistory: false,
219219
})
@@ -760,7 +760,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
760760
editor: () => editorRef,
761761
isFocused,
762762
isDialogActive: () => !!dialog.active,
763-
setDragging: (value) => setStore("dragging", value),
763+
setDraggingType: (type) => setStore("draggingType", type),
764+
focusEditor: () => {
765+
editorRef.focus()
766+
setCursorPosition(editorRef, promptLength(prompt.current()))
767+
},
764768
addPart,
765769
})
766770

@@ -946,11 +950,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
946950
"group/prompt-input": true,
947951
"bg-surface-raised-stronger-non-alpha shadow-xs-border relative": true,
948952
"rounded-[14px] overflow-clip focus-within:shadow-xs-border": true,
949-
"border-icon-info-active border-dashed": store.dragging,
953+
"border-icon-info-active border-dashed": store.draggingType !== null,
950954
[props.class ?? ""]: !!props.class,
951955
}}
952956
>
953-
<PromptDragOverlay dragging={store.dragging} label={language.t("prompt.dropzone.label")} />
957+
<PromptDragOverlay
958+
type={store.draggingType}
959+
label={language.t(store.draggingType === "@mention" ? "prompt.dropzone.file.label" : "prompt.dropzone.label")}
960+
/>
954961
<PromptContextItems
955962
items={prompt.context.items()}
956963
active={(item) => {

packages/app/src/components/prompt-input/attachments.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ type PromptAttachmentsInput = {
1111
editor: () => HTMLDivElement | undefined
1212
isFocused: () => boolean
1313
isDialogActive: () => boolean
14-
setDragging: (value: boolean) => void
14+
setDraggingType: (type: "image" | "@mention" | null) => void
15+
focusEditor: () => void
1516
addPart: (part: ContentPart) => void
1617
}
1718

@@ -84,23 +85,35 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
8485

8586
event.preventDefault()
8687
const hasFiles = event.dataTransfer?.types.includes("Files")
88+
const hasText = event.dataTransfer?.types.includes("text/plain")
8789
if (hasFiles) {
88-
input.setDragging(true)
90+
input.setDraggingType("image")
91+
} else if (hasText) {
92+
input.setDraggingType("@mention")
8993
}
9094
}
9195

9296
const handleGlobalDragLeave = (event: DragEvent) => {
9397
if (input.isDialogActive()) return
9498
if (!event.relatedTarget) {
95-
input.setDragging(false)
99+
input.setDraggingType(null)
96100
}
97101
}
98102

99103
const handleGlobalDrop = async (event: DragEvent) => {
100104
if (input.isDialogActive()) return
101105

102106
event.preventDefault()
103-
input.setDragging(false)
107+
input.setDraggingType(null)
108+
109+
const plainText = event.dataTransfer?.getData("text/plain")
110+
const filePrefix = "file:"
111+
if (plainText?.startsWith(filePrefix)) {
112+
const filePath = plainText.slice(filePrefix.length)
113+
input.focusEditor()
114+
input.addPart({ type: "file", path: filePath, content: "@" + filePath, start: 0, end: 0 })
115+
return
116+
}
104117

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

packages/app/src/components/prompt-input/drag-overlay.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { Component, Show } from "solid-js"
22
import { Icon } from "@opencode-ai/ui/icon"
33

44
type PromptDragOverlayProps = {
5-
dragging: boolean
5+
type: "image" | "@mention" | null
66
label: string
77
}
88

99
export const PromptDragOverlay: Component<PromptDragOverlayProps> = (props) => {
1010
return (
11-
<Show when={props.dragging}>
11+
<Show when={props.type !== null}>
1212
<div class="absolute inset-0 z-10 flex items-center justify-center bg-surface-raised-stronger-non-alpha/90 pointer-events-none">
1313
<div class="flex flex-col items-center gap-2 text-text-weak">
14-
<Icon name="photo" class="size-8" />
14+
<Icon name={props.type === "@mention" ? "link" : "photo"} class="size-8" />
1515
<span class="text-14-regular">{props.label}</span>
1616
</div>
1717
</div>

packages/app/src/i18n/ar.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export const dict = {
211211
"prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
212212
"prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
213213
"prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا",
214+
"prompt.dropzone.file.label": "أفلت لإشارة @ للملف",
214215
"prompt.slash.badge.custom": "مخصص",
215216
"prompt.slash.badge.skill": "مهارة",
216217
"prompt.slash.badge.mcp": "mcp",

packages/app/src/i18n/br.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export const dict = {
211211
"prompt.popover.emptyResults": "Nenhum resultado correspondente",
212212
"prompt.popover.emptyCommands": "Nenhum comando correspondente",
213213
"prompt.dropzone.label": "Solte imagens ou PDFs aqui",
214+
"prompt.dropzone.file.label": "Solte para @mencionar arquivo",
214215
"prompt.slash.badge.custom": "personalizado",
215216
"prompt.slash.badge.skill": "skill",
216217
"prompt.slash.badge.mcp": "mcp",

packages/app/src/i18n/bs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export const dict = {
219219
"prompt.popover.emptyResults": "Nema rezultata",
220220
"prompt.popover.emptyCommands": "Nema komandi",
221221
"prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje",
222+
"prompt.dropzone.file.label": "Spusti za @spominjanje datoteke",
222223
"prompt.slash.badge.custom": "prilagođeno",
223224
"prompt.slash.badge.skill": "skill",
224225
"prompt.slash.badge.mcp": "mcp",

packages/app/src/i18n/da.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export const dict = {
211211
"prompt.popover.emptyResults": "Ingen matchende resultater",
212212
"prompt.popover.emptyCommands": "Ingen matchende kommandoer",
213213
"prompt.dropzone.label": "Slip billeder eller PDF'er her",
214+
"prompt.dropzone.file.label": "Slip for at @nævne fil",
214215
"prompt.slash.badge.custom": "brugerdefineret",
215216
"prompt.slash.badge.skill": "skill",
216217
"prompt.slash.badge.mcp": "mcp",

packages/app/src/i18n/de.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export const dict = {
253253
"prompt.popover.emptyResults": "Keine passenden Ergebnisse",
254254
"prompt.popover.emptyCommands": "Keine passenden Befehle",
255255
"prompt.dropzone.label": "Bilder oder PDFs hier ablegen",
256+
"prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei",
256257
"prompt.slash.badge.custom": "benutzerdefiniert",
257258
"prompt.slash.badge.skill": "skill",
258259
"prompt.slash.badge.mcp": "mcp",

packages/app/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ export const dict = {
256256
"prompt.popover.emptyResults": "No matching results",
257257
"prompt.popover.emptyCommands": "No matching commands",
258258
"prompt.dropzone.label": "Drop images or PDFs here",
259+
"prompt.dropzone.file.label": "Drop to @mention file",
259260
"prompt.slash.badge.custom": "custom",
260261
"prompt.slash.badge.skill": "skill",
261262
"prompt.slash.badge.mcp": "mcp",

packages/app/src/i18n/es.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export const dict = {
211211
"prompt.popover.emptyResults": "Sin resultados coincidentes",
212212
"prompt.popover.emptyCommands": "Sin comandos coincidentes",
213213
"prompt.dropzone.label": "Suelta imágenes o PDFs aquí",
214+
"prompt.dropzone.file.label": "Suelta para @mencionar archivo",
214215
"prompt.slash.badge.custom": "personalizado",
215216
"prompt.slash.badge.skill": "skill",
216217
"prompt.slash.badge.mcp": "mcp",

0 commit comments

Comments
 (0)