Skip to content

Commit 2f69f0c

Browse files
committed
Enable dragging workspace files into chat context
- Make file tree rows draggable with a custom tree-path payload - Accept tree-path drops in the composer and insert them as @mentions - Update the drag overlay copy for files vs. tree paths
1 parent 772b197 commit 2f69f0c

2 files changed

Lines changed: 50 additions & 6 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import PlanSidebar from "./PlanSidebar";
9191
import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
9292
import { SpotifyPlayerDrawer } from "./SpotifyPlayer";
9393
import {
94+
AtSignIcon,
9495
BotIcon,
9596
ChevronDownIcon,
9697
ChevronLeftIcon,
@@ -345,6 +346,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
345346
const promptRef = useRef(prompt);
346347
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
347348
const [isDragOverComposer, setIsDragOverComposer] = useState(false);
349+
const [dragOverType, setDragOverType] = useState<"files" | "tree-path">("files");
348350
const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
349351
const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
350352
const optimisticUserMessagesRef = useRef(optimisticUserMessages);
@@ -2444,17 +2446,25 @@ export default function ChatView({ threadId }: ChatViewProps) {
24442446
addComposerImages(imageFiles);
24452447
};
24462448

2449+
const isAcceptedDragType = (dataTransfer: DataTransfer) =>
2450+
dataTransfer.types.includes("Files") ||
2451+
dataTransfer.types.includes("application/x-okcode-tree-path");
2452+
2453+
const isDragTreePath = (dataTransfer: DataTransfer) =>
2454+
dataTransfer.types.includes("application/x-okcode-tree-path");
2455+
24472456
const onComposerDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
2448-
if (!event.dataTransfer.types.includes("Files")) {
2457+
if (!isAcceptedDragType(event.dataTransfer)) {
24492458
return;
24502459
}
24512460
event.preventDefault();
24522461
dragDepthRef.current += 1;
2462+
setDragOverType(isDragTreePath(event.dataTransfer) ? "tree-path" : "files");
24532463
setIsDragOverComposer(true);
24542464
};
24552465

24562466
const onComposerDragOver = (event: React.DragEvent<HTMLDivElement>) => {
2457-
if (!event.dataTransfer.types.includes("Files")) {
2467+
if (!isAcceptedDragType(event.dataTransfer)) {
24582468
return;
24592469
}
24602470
event.preventDefault();
@@ -2463,7 +2473,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
24632473
};
24642474

24652475
const onComposerDragLeave = (event: React.DragEvent<HTMLDivElement>) => {
2466-
if (!event.dataTransfer.types.includes("Files")) {
2476+
if (!isAcceptedDragType(event.dataTransfer)) {
24672477
return;
24682478
}
24692479
event.preventDefault();
@@ -2478,12 +2488,27 @@ export default function ChatView({ threadId }: ChatViewProps) {
24782488
};
24792489

24802490
const onComposerDrop = (event: React.DragEvent<HTMLDivElement>) => {
2481-
if (!event.dataTransfer.types.includes("Files")) {
2491+
if (!isAcceptedDragType(event.dataTransfer)) {
24822492
return;
24832493
}
24842494
event.preventDefault();
24852495
dragDepthRef.current = 0;
24862496
setIsDragOverComposer(false);
2497+
2498+
// Handle file tree path drops — insert as @mention context
2499+
if (isDragTreePath(event.dataTransfer)) {
2500+
const treePath = event.dataTransfer.getData("application/x-okcode-tree-path");
2501+
if (treePath) {
2502+
const snapshot = readComposerSnapshot();
2503+
const mention = `@${treePath} `;
2504+
// Insert at the current cursor position
2505+
applyPromptReplacement(snapshot.cursor, snapshot.cursor, mention);
2506+
}
2507+
focusComposer();
2508+
return;
2509+
}
2510+
2511+
// Handle image file drops
24872512
const files = Array.from(event.dataTransfer.files);
24882513
addComposerImages(files);
24892514
focusComposer();
@@ -4007,8 +4032,17 @@ export default function ChatView({ threadId }: ChatViewProps) {
40074032
{isDragOverComposer && (
40084033
<div className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-[22px] border-2 border-dashed border-primary/60 bg-primary/5">
40094034
<div className="flex items-center gap-2 rounded-lg bg-background/90 px-4 py-2 text-sm font-medium text-primary shadow-sm">
4010-
<ImagePlusIcon className="size-4" />
4011-
Drop images to attach
4035+
{dragOverType === "tree-path" ? (
4036+
<>
4037+
<AtSignIcon className="size-4" />
4038+
Drop to add as context
4039+
</>
4040+
) : (
4041+
<>
4042+
<ImagePlusIcon className="size-4" />
4043+
Drop images to attach
4044+
</>
4045+
)}
40124046
</div>
40134047
</div>
40144048
)}

apps/web/src/components/WorkspaceFileTree.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ const WorkspaceDirectoryRow = memo(function WorkspaceDirectoryRow(props: {
172172
return (
173173
<button
174174
type="button"
175+
draggable
176+
onDragStart={(event) => {
177+
event.dataTransfer.setData("application/x-okcode-tree-path", props.entry.path);
178+
event.dataTransfer.effectAllowed = "copy";
179+
}}
175180
className={cn(
176181
"group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-accent/60",
177182
!props.entry.hasChildren && "cursor-default",
@@ -213,6 +218,11 @@ const WorkspaceFileRow = memo(function WorkspaceFileRow(props: {
213218
return (
214219
<button
215220
type="button"
221+
draggable
222+
onDragStart={(event) => {
223+
event.dataTransfer.setData("application/x-okcode-tree-path", props.entry.path);
224+
event.dataTransfer.effectAllowed = "copy";
225+
}}
216226
className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-accent/60"
217227
style={{ paddingLeft: `${leftPadding}px` }}
218228
onClick={(event) =>

0 commit comments

Comments
 (0)