Просмотр исходного кода

feat(app): drag-n-drop to @mention file (#12569)

Devin Griffin 2 недель назад
Родитель
Сommit
6bdd3528ac

+ 12 - 5
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) => {

+ 17 - 4
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

+ 3 - 3
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>

+ 1 - 0
packages/app/src/i18n/ar.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "لا توجد نتائج مطابقة",
   "prompt.popover.emptyCommands": "لا توجد أوامر مطابقة",
   "prompt.dropzone.label": "أفلت الصور أو ملفات PDF هنا",
+  "prompt.dropzone.file.label": "أفلت لإشارة @ للملف",
   "prompt.slash.badge.custom": "مخصص",
   "prompt.slash.badge.skill": "مهارة",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/br.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Nenhum resultado correspondente",
   "prompt.popover.emptyCommands": "Nenhum comando correspondente",
   "prompt.dropzone.label": "Solte imagens ou PDFs aqui",
+  "prompt.dropzone.file.label": "Solte para @mencionar arquivo",
   "prompt.slash.badge.custom": "personalizado",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/bs.ts

@@ -219,6 +219,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Nema rezultata",
   "prompt.popover.emptyCommands": "Nema komandi",
   "prompt.dropzone.label": "Spusti slike ili PDF-ove ovdje",
+  "prompt.dropzone.file.label": "Spusti za @spominjanje datoteke",
   "prompt.slash.badge.custom": "prilagođeno",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/da.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Ingen matchende resultater",
   "prompt.popover.emptyCommands": "Ingen matchende kommandoer",
   "prompt.dropzone.label": "Slip billeder eller PDF'er her",
+  "prompt.dropzone.file.label": "Slip for at @nævne fil",
   "prompt.slash.badge.custom": "brugerdefineret",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/de.ts

@@ -253,6 +253,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Keine passenden Ergebnisse",
   "prompt.popover.emptyCommands": "Keine passenden Befehle",
   "prompt.dropzone.label": "Bilder oder PDFs hier ablegen",
+  "prompt.dropzone.file.label": "Ablegen zum @Erwähnen der Datei",
   "prompt.slash.badge.custom": "benutzerdefiniert",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/en.ts

@@ -256,6 +256,7 @@ export const dict = {
   "prompt.popover.emptyResults": "No matching results",
   "prompt.popover.emptyCommands": "No matching commands",
   "prompt.dropzone.label": "Drop images or PDFs here",
+  "prompt.dropzone.file.label": "Drop to @mention file",
   "prompt.slash.badge.custom": "custom",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/es.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Sin resultados coincidentes",
   "prompt.popover.emptyCommands": "Sin comandos coincidentes",
   "prompt.dropzone.label": "Suelta imágenes o PDFs aquí",
+  "prompt.dropzone.file.label": "Suelta para @mencionar archivo",
   "prompt.slash.badge.custom": "personalizado",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/fr.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Aucun résultat correspondant",
   "prompt.popover.emptyCommands": "Aucune commande correspondante",
   "prompt.dropzone.label": "Déposez des images ou des PDF ici",
+  "prompt.dropzone.file.label": "Déposez pour @mentionner le fichier",
   "prompt.slash.badge.custom": "personnalisé",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/ja.ts

@@ -210,6 +210,7 @@ export const dict = {
   "prompt.popover.emptyResults": "一致する結果がありません",
   "prompt.popover.emptyCommands": "一致するコマンドがありません",
   "prompt.dropzone.label": "画像またはPDFをここにドロップ",
+  "prompt.dropzone.file.label": "ドロップして@メンションファイルを追加",
   "prompt.slash.badge.custom": "カスタム",
   "prompt.slash.badge.skill": "スキル",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/ko.ts

@@ -214,6 +214,7 @@ export const dict = {
   "prompt.popover.emptyResults": "일치하는 결과 없음",
   "prompt.popover.emptyCommands": "일치하는 명령어 없음",
   "prompt.dropzone.label": "이미지나 PDF를 여기에 드롭하세요",
+  "prompt.dropzone.file.label": "드롭하여 파일 @멘션 추가",
   "prompt.slash.badge.custom": "사용자 지정",
   "prompt.slash.badge.skill": "스킬",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/no.ts

@@ -214,6 +214,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Ingen matchende resultater",
   "prompt.popover.emptyCommands": "Ingen matchende kommandoer",
   "prompt.dropzone.label": "Slipp bilder eller PDF-er her",
+  "prompt.dropzone.file.label": "Slipp for å @nevne fil",
   "prompt.slash.badge.custom": "egendefinert",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/pl.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Brak pasujących wyników",
   "prompt.popover.emptyCommands": "Brak pasujących poleceń",
   "prompt.dropzone.label": "Upuść obrazy lub pliki PDF tutaj",
+  "prompt.dropzone.file.label": "Upuść, aby @wspomnieć plik",
   "prompt.slash.badge.custom": "własne",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/ru.ts

@@ -211,6 +211,7 @@ export const dict = {
   "prompt.popover.emptyResults": "Нет совпадений",
   "prompt.popover.emptyCommands": "Нет совпадающих команд",
   "prompt.dropzone.label": "Перетащите изображения или PDF сюда",
+  "prompt.dropzone.file.label": "Отпустите для @упоминания файла",
   "prompt.slash.badge.custom": "своё",
   "prompt.slash.badge.skill": "навык",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/th.ts

@@ -216,6 +216,7 @@ export const dict = {
   "prompt.popover.emptyResults": "ไม่พบผลลัพธ์ที่ตรงกัน",
   "prompt.popover.emptyCommands": "ไม่พบคำสั่งที่ตรงกัน",
   "prompt.dropzone.label": "วางรูปภาพหรือ PDF ที่นี่",
+  "prompt.dropzone.file.label": "วางเพื่อ @กล่าวถึงไฟล์",
   "prompt.slash.badge.custom": "กำหนดเอง",
   "prompt.slash.badge.skill": "skill",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/zh.ts

@@ -252,6 +252,7 @@ export const dict = {
   "prompt.popover.emptyResults": "没有匹配的结果",
   "prompt.popover.emptyCommands": "没有匹配的命令",
   "prompt.dropzone.label": "将图片或 PDF 拖到这里",
+  "prompt.dropzone.file.label": "拖放以 @提及文件",
   "prompt.slash.badge.custom": "自定义",
   "prompt.slash.badge.skill": "技能",
   "prompt.slash.badge.mcp": "mcp",

+ 1 - 0
packages/app/src/i18n/zht.ts

@@ -249,6 +249,7 @@ export const dict = {
   "prompt.popover.emptyResults": "沒有符合的結果",
   "prompt.popover.emptyCommands": "沒有符合的命令",
   "prompt.dropzone.label": "將圖片或 PDF 拖到這裡",
+  "prompt.dropzone.file.label": "拖放以 @提及檔案",
   "prompt.slash.badge.custom": "自訂",
   "prompt.slash.badge.skill": "技能",
   "prompt.slash.badge.mcp": "mcp",