Skip to content

Commit 1e88515

Browse files
azizmejri1claude
andcommitted
Prompt the user for attachement type after dragging the file (dyad-sh#2563)
<!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2563" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- This is an auto-generated description by cubic. --> --- Show a dialog after dragging or pasting files into chat to choose how to attach them—as chat context or upload to codebase. This makes intent explicit and blocks submit, drop, and paste until you choose. - **New Features** - Added FileAttachmentTypeDialog; integrated in ChatInput and HomeChatInput. i18n (en, pt-BR, zh-CN) with singular/plural titles and descriptions. - Updated useAttachments with pendingFiles and confirm/cancel. Drag/paste set pendingFiles; prevent attaching while pending; clearAttachments also clears pendingFiles; submit blocked when dialog is open. - Fixed e2e to select “Attach file as chat context”; dialog buttons use type="button" with focus-visible ring. - **Refactors** - confirmPendingFiles reuses addAttachments to deduplicate logic. <sup>Written for commit b625847. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 05feb90 commit 1e88515

8 files changed

Lines changed: 174 additions & 16 deletions

File tree

e2e-tests/attach_image.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ test("attach image via drag - chat", async ({ po }) => {
154154
});
155155
}, fileBase64);
156156

157+
// Choose "Attach as chat context" in the attachment type dialog
158+
const chatContextButton = po.page.getByRole("button", {
159+
name: "Attach file as chat context",
160+
});
161+
await expect(chatContextButton).toBeVisible();
162+
await chatContextButton.click();
163+
157164
// submit and verify
158165
await po.sendPrompt("[dump]");
159166
// Note: this should match EXACTLY the server dump from the previous test.

src/components/chat/ChatInput.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { useVersions } from "@/hooks/useVersions";
5555
import { useAttachments } from "@/hooks/useAttachments";
5656
import { AttachmentsList } from "./AttachmentsList";
5757
import { DragDropOverlay } from "./DragDropOverlay";
58+
import { FileAttachmentTypeDialog } from "./FileAttachmentTypeDialog";
5859
import { showExtraFilesToast, showInfo } from "@/lib/toast";
5960
import { useSummarizeInNewChat } from "./SummarizeInNewChatButton";
6061
import { ChatInputControls } from "../ChatInputControls";
@@ -162,13 +163,16 @@ export function ChatInput({ chatId }: { chatId?: number }) {
162163
const {
163164
attachments,
164165
isDraggingOver,
166+
pendingFiles,
165167
handleFileSelect,
166168
removeAttachment,
167169
handleDragOver,
168170
handleDragLeave,
169171
handleDrop,
170172
clearAttachments,
171173
handlePaste,
174+
confirmPendingFiles,
175+
cancelPendingFiles,
172176
} = useAttachments();
173177

174178
// Use the hook to fetch the proposal
@@ -298,7 +302,11 @@ export function ChatInput({ chatId }: { chatId?: number }) {
298302
);
299303

300304
const handleSubmit = async () => {
301-
if ((!inputValue.trim() && attachments.length === 0) || !chatId) {
305+
if (
306+
(!inputValue.trim() && attachments.length === 0) ||
307+
!chatId ||
308+
pendingFiles
309+
) {
302310
return;
303311
}
304312

@@ -683,6 +691,13 @@ export function ChatInput({ chatId }: { chatId?: number }) {
683691
{/* Use the DragDropOverlay component */}
684692
<DragDropOverlay isDraggingOver={isDraggingOver} />
685693

694+
{/* Dialog for choosing attachment type */}
695+
<FileAttachmentTypeDialog
696+
pendingFiles={pendingFiles}
697+
onConfirm={confirmPendingFiles}
698+
onCancel={cancelPendingFiles}
699+
/>
700+
686701
<div className="flex items-start space-x-2 ">
687702
<LexicalChatInput
688703
value={inputValue}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { MessageSquare, Upload } from "lucide-react";
2+
import {
3+
Dialog,
4+
DialogContent,
5+
DialogHeader,
6+
DialogTitle,
7+
DialogDescription,
8+
} from "@/components/ui/dialog";
9+
import { useTranslation } from "react-i18next";
10+
11+
interface FileAttachmentTypeDialogProps {
12+
pendingFiles: File[] | null;
13+
onConfirm: (type: "chat-context" | "upload-to-codebase") => void;
14+
onCancel: () => void;
15+
}
16+
17+
export function FileAttachmentTypeDialog({
18+
pendingFiles,
19+
onConfirm,
20+
onCancel,
21+
}: FileAttachmentTypeDialogProps) {
22+
const { t } = useTranslation("chat");
23+
const isOpen = !!pendingFiles && pendingFiles.length > 0;
24+
const fileCount = pendingFiles?.length ?? 0;
25+
26+
return (
27+
<Dialog
28+
open={isOpen}
29+
onOpenChange={(open) => {
30+
if (!open) onCancel();
31+
}}
32+
>
33+
<DialogContent className="sm:max-w-md">
34+
<DialogHeader>
35+
<DialogTitle>
36+
{fileCount === 1
37+
? t("attachmentTypeDialog.titleSingular")
38+
: t("attachmentTypeDialog.titlePlural", { count: fileCount })}
39+
</DialogTitle>
40+
<DialogDescription>
41+
{fileCount === 1
42+
? t("attachmentTypeDialog.descriptionSingular")
43+
: t("attachmentTypeDialog.descriptionPlural")}
44+
</DialogDescription>
45+
</DialogHeader>
46+
<div className="flex flex-col gap-2">
47+
<button
48+
type="button"
49+
className="flex items-start gap-3 rounded-lg border border-border p-4 text-left hover:bg-muted/50 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
50+
onClick={() => onConfirm("chat-context")}
51+
>
52+
<MessageSquare
53+
size={20}
54+
className="mt-0.5 text-green-500 flex-shrink-0"
55+
/>
56+
<div>
57+
<div className="font-medium text-sm">
58+
{t("attachFileContext")}
59+
</div>
60+
<div className="text-xs text-muted-foreground mt-0.5">
61+
{t("attachFileContextExample")}
62+
</div>
63+
</div>
64+
</button>
65+
<button
66+
type="button"
67+
className="flex items-start gap-3 rounded-lg border border-border p-4 text-left hover:bg-muted/50 transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
68+
onClick={() => onConfirm("upload-to-codebase")}
69+
>
70+
<Upload size={20} className="mt-0.5 text-blue-500 flex-shrink-0" />
71+
<div>
72+
<div className="font-medium text-sm">
73+
{t("uploadFileCodebase")}
74+
</div>
75+
<div className="text-xs text-muted-foreground mt-0.5">
76+
{t("uploadFileCodebaseExample")}
77+
</div>
78+
</div>
79+
</button>
80+
</div>
81+
</DialogContent>
82+
</Dialog>
83+
);
84+
}

src/components/chat/HomeChatInput.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useStreamChat } from "@/hooks/useStreamChat";
1212
import { useAttachments } from "@/hooks/useAttachments";
1313
import { AttachmentsList } from "./AttachmentsList";
1414
import { DragDropOverlay } from "./DragDropOverlay";
15+
import { FileAttachmentTypeDialog } from "./FileAttachmentTypeDialog";
1516
import { usePostHog } from "posthog-js/react";
1617
import { HomeSubmitOptions } from "@/pages/home";
1718
import { ChatInputControls } from "../ChatInputControls";
@@ -44,18 +45,25 @@ export function HomeChatInput({
4445
const {
4546
attachments,
4647
isDraggingOver,
48+
pendingFiles,
4749
handleFileSelect,
4850
removeAttachment,
4951
handleDragOver,
5052
handleDragLeave,
5153
handleDrop,
5254
clearAttachments,
5355
handlePaste,
56+
confirmPendingFiles,
57+
cancelPendingFiles,
5458
} = useAttachments();
5559

5660
// Custom submit function that wraps the provided onSubmit
5761
const handleCustomSubmit = () => {
58-
if ((!inputValue.trim() && attachments.length === 0) || isStreaming) {
62+
if (
63+
(!inputValue.trim() && attachments.length === 0) ||
64+
isStreaming ||
65+
pendingFiles
66+
) {
5967
return;
6068
}
6169

@@ -93,6 +101,13 @@ export function HomeChatInput({
93101
{/* Drag and drop overlay */}
94102
<DragDropOverlay isDraggingOver={isDraggingOver} />
95103

104+
{/* Dialog for choosing attachment type */}
105+
<FileAttachmentTypeDialog
106+
pendingFiles={pendingFiles}
107+
onConfirm={confirmPendingFiles}
108+
onCancel={cancelPendingFiles}
109+
/>
110+
96111
<div className="flex items-start space-x-2 ">
97112
<LexicalChatInput
98113
value={inputValue}

src/hooks/useAttachments.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef, useState } from "react";
1+
import React, { useCallback, useRef, useState } from "react";
22
import type { FileAttachment } from "@/ipc/types";
33
import { useAtom } from "jotai";
44
import { attachmentsAtom } from "@/atoms/chatAtoms";
@@ -7,6 +7,7 @@ export function useAttachments() {
77
const [attachments, setAttachments] = useAtom(attachmentsAtom);
88
const fileInputRef = useRef<HTMLInputElement>(null);
99
const [isDraggingOver, setIsDraggingOver] = useState(false);
10+
const [pendingFiles, setPendingFiles] = useState<File[] | null>(null);
1011

1112
const handleAttachmentClick = () => {
1213
fileInputRef.current?.click();
@@ -46,7 +47,9 @@ export function useAttachments() {
4647

4748
const handleDragOver = (e: React.DragEvent) => {
4849
e.preventDefault();
49-
setIsDraggingOver(true);
50+
if (!pendingFiles) {
51+
setIsDraggingOver(true);
52+
}
5053
};
5154

5255
const handleDragLeave = () => {
@@ -57,20 +60,14 @@ export function useAttachments() {
5760
e.preventDefault();
5861
setIsDraggingOver(false);
5962

63+
if (pendingFiles) return;
64+
6065
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
6166
const files = Array.from(e.dataTransfer.files);
62-
const fileAttachments: FileAttachment[] = files.map((file) => ({
63-
file,
64-
type: "chat-context" as const,
65-
}));
66-
setAttachments((attachments) => [...attachments, ...fileAttachments]);
67+
setPendingFiles(files);
6768
}
6869
};
6970

70-
const clearAttachments = () => {
71-
setAttachments([]);
72-
};
73-
7471
const addAttachments = (
7572
files: File[],
7673
type: "chat-context" | "upload-to-codebase" = "chat-context",
@@ -82,7 +79,28 @@ export function useAttachments() {
8279
setAttachments((attachments) => [...attachments, ...fileAttachments]);
8380
};
8481

82+
const confirmPendingFiles = useCallback(
83+
(type: "chat-context" | "upload-to-codebase") => {
84+
if (pendingFiles) {
85+
addAttachments(pendingFiles, type);
86+
setPendingFiles(null);
87+
}
88+
},
89+
[pendingFiles, addAttachments],
90+
);
91+
92+
const cancelPendingFiles = useCallback(() => {
93+
setPendingFiles(null);
94+
}, []);
95+
96+
const clearAttachments = () => {
97+
setAttachments([]);
98+
setPendingFiles(null);
99+
};
100+
85101
const handlePaste = async (e: React.ClipboardEvent) => {
102+
if (pendingFiles) return;
103+
86104
const clipboardData = e.clipboardData;
87105
if (!clipboardData) return;
88106

@@ -115,9 +133,7 @@ export function useAttachments() {
115133
}
116134

117135
if (imageFiles.length > 0) {
118-
addAttachments(imageFiles, "chat-context");
119-
// Show a brief toast or indication that image was pasted
120-
console.log(`Pasted ${imageFiles.length} image(s) from clipboard`);
136+
setPendingFiles(imageFiles);
121137
}
122138
}
123139
};
@@ -126,6 +142,7 @@ export function useAttachments() {
126142
attachments,
127143
fileInputRef,
128144
isDraggingOver,
145+
pendingFiles,
129146
handleAttachmentClick,
130147
handleFileChange,
131148
handleFileSelect,
@@ -136,5 +153,7 @@ export function useAttachments() {
136153
clearAttachments,
137154
handlePaste,
138155
addAttachments,
156+
confirmPendingFiles,
157+
cancelPendingFiles,
139158
};
140159
}

src/i18n/locales/en/chat.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@
126126
"uploadFileCodebase": "Upload file to codebase",
127127
"uploadFileCodebaseExample": "Example use case: add an image to use for your app",
128128
"dropFilesToAttach": "Drop files to attach",
129+
"attachmentTypeDialog": {
130+
"titleSingular": "How would you like to attach this file?",
131+
"titlePlural": "How would you like to attach these {{count}} files?",
132+
"descriptionSingular": "Choose how the file should be used.",
133+
"descriptionPlural": "Choose how the files should be used."
134+
},
129135
"selectedComponents": "Selected Components ({{count}})",
130136
"clearAllComponents": "Clear all selected components",
131137
"deselectComponent": "Deselect component",

src/i18n/locales/pt-BR/chat.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@
126126
"uploadFileCodebase": "Enviar arquivo para a base de código",
127127
"uploadFileCodebaseExample": "Exemplo de uso: adicionar uma imagem para usar no seu app",
128128
"dropFilesToAttach": "Solte os arquivos para anexar",
129+
"attachmentTypeDialog": {
130+
"titleSingular": "Como você gostaria de anexar este arquivo?",
131+
"titlePlural": "Como você gostaria de anexar estes {{count}} arquivos?",
132+
"descriptionSingular": "Escolha como o arquivo deve ser usado.",
133+
"descriptionPlural": "Escolha como os arquivos devem ser usados."
134+
},
129135
"selectedComponents": "Componentes Selecionados ({{count}})",
130136
"clearAllComponents": "Limpar todos os componentes selecionados",
131137
"deselectComponent": "Desmarcar componente",

src/i18n/locales/zh-CN/chat.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@
126126
"uploadFileCodebase": "上传文件到代码库",
127127
"uploadFileCodebaseExample": "示例用途:添加图片供应用使用",
128128
"dropFilesToAttach": "拖放文件以附加",
129+
"attachmentTypeDialog": {
130+
"titleSingular": "您想如何附加此文件?",
131+
"titlePlural": "您想如何附加这 {{count}} 个文件?",
132+
"descriptionSingular": "选择文件的使用方式。",
133+
"descriptionPlural": "选择文件的使用方式。"
134+
},
129135
"selectedComponents": "已选择的组件 ({{count}})",
130136
"clearAllComponents": "清除所有已选择的组件",
131137
"deselectComponent": "取消选择组件",

0 commit comments

Comments
 (0)