diff --git a/src/components/chat-components/RelevantNotes.tsx b/src/components/chat-components/RelevantNotes.tsx index afc51a45..70b5466c 100644 --- a/src/components/chat-components/RelevantNotes.tsx +++ b/src/components/chat-components/RelevantNotes.tsx @@ -6,6 +6,7 @@ import { HelpTooltip } from "@/components/ui/help-tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useChatInput } from "@/context/ChatInputContext"; import { useActiveFile } from "@/hooks/useActiveFile"; +import { useNoteDrag } from "@/hooks/useNoteDrag"; import { cn } from "@/lib/utils"; import { logWarn } from "@/logger"; import { SemanticSearchToggleModal } from "@/components/modals/SemanticSearchToggleModal"; @@ -131,6 +132,7 @@ function RelevantNote({ }) { const [isOpen, setIsOpen] = useState(false); const [fileContent, setFileContent] = useState(null); + const handleDragStart = useNoteDrag(); const loadContent = useCallback(async () => { if (fileContent) return; // Don't load if we already have content @@ -181,6 +183,13 @@ function RelevantNote({
{ + const file = app.vault.getAbstractFileByPath(note.document.path); + if (file instanceof TFile) { + handleDragStart(e, file); + } + }} onClick={(e) => { e.preventDefault(); const openInNewLeaf = e.metaKey || e.ctrlKey; @@ -194,7 +203,7 @@ function RelevantNote({ } }} className="tw-block tw-w-full tw-truncate tw-text-sm tw-font-bold tw-text-normal" - title={note.document.title} + title={`${note.document.title} - drag to insert wikilink`} > {note.document.title} @@ -293,6 +302,7 @@ export const RelevantNotes = memo( const activeFile = useActiveFile(); const chatInput = useChatInput(); const hasIndex = useHasIndex(activeFile?.path ?? "", refresher); + const handleDragStart = useNoteDrag(); const navigateToNote = (notePath: string, openInNewLeaf = false) => { const file = app.vault.getAbstractFileByPath(notePath); if (file instanceof TFile) { @@ -404,7 +414,15 @@ export const RelevantNotes = memo( { + const file = app.vault.getAbstractFileByPath(note.document.path); + if (file instanceof TFile) { + handleDragStart(e, file); + } + }} className="tw-max-w-40 tw-text-xs tw-text-muted hover:tw-cursor-pointer hover:tw-bg-interactive-hover" + title={`${note.document.title} - drag to insert wikilink`} > {note.document.title} diff --git a/src/components/modals/SourcesModal.tsx b/src/components/modals/SourcesModal.tsx index 3af6445f..01e3141d 100644 --- a/src/components/modals/SourcesModal.tsx +++ b/src/components/modals/SourcesModal.tsx @@ -1,5 +1,5 @@ // src/components/SourcesModal.tsx -import { App, Modal } from "obsidian"; +import { App, Modal, TFile } from "obsidian"; export class SourcesModal extends Modal { sources: { title: string; path: string; score: number; explanation?: any }[]; @@ -55,6 +55,19 @@ export class SourcesModal extends Modal { href: `obsidian://open?vault=${encodeURIComponent(this.app.vault.getName())}&file=${encodeURIComponent(source.path || source.title)}`, text: displayText, }); + link.title = `${displayText} - drag to insert wikilink`; + link.draggable = true; + link.addEventListener("dragstart", (e) => { + const filePath = source.path || source.title; + const file = this.app.vault.getAbstractFileByPath(filePath); + if (file instanceof TFile) { + const dragManager = (this.app as any).dragManager; + if (!dragManager) return; + const linkText = this.app.metadataCache.fileToLinktext(file, ""); + const dragData = dragManager.dragLink(e, linkText); + dragManager.onDragStart(e, dragData); + } + }); link.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); diff --git a/src/hooks/useChatFileDrop.ts b/src/hooks/useChatFileDrop.ts index 14bb29fc..6094c9ed 100644 --- a/src/hooks/useChatFileDrop.ts +++ b/src/hooks/useChatFileDrop.ts @@ -104,6 +104,11 @@ export function useChatFileDrop(props: UseChatFileDropProps): UseChatFileDropRet if (e.dataTransfer) { e.dataTransfer.dropEffect = "copy"; + // Skip showing drop zone for drags originating from within the plugin (e.g. relevant notes) + if (e.dataTransfer.types.includes("copilot/internal-drag")) { + return; + } + // Check if we have string items (Obsidian nav bar drag) or file items (external files) const hasStringItems = Array.from(e.dataTransfer.items).some( (item) => item.kind === "string" diff --git a/src/hooks/useNoteDrag.ts b/src/hooks/useNoteDrag.ts new file mode 100644 index 00000000..d2056fd6 --- /dev/null +++ b/src/hooks/useNoteDrag.ts @@ -0,0 +1,32 @@ +import { useCallback } from "react"; +import { TFile } from "obsidian"; + +/** + * Returns a drag-start handler that integrates with Obsidian's native dragManager API. + * When a note element is dropped onto the Obsidian editor, the editor automatically + * inserts the corresponding `[[wikilink]]` without any additional drop handler. + * + * Usage: + * ```tsx + * const handleDragStart = useNoteDrag(); + * const file = app.vault.getAbstractFileByPath(path); + * if (file instanceof TFile) { + *
handleDragStart(e, file)}> ...
+ * } + * ``` + */ +export function useNoteDrag() { + const handleDragStart = useCallback((e: React.DragEvent, file: TFile): void => { + const dragManager = (app as any).dragManager; + if (!dragManager) return; + + // Mark this drag as internal so the chat drop zone overlay doesn't appear + e.dataTransfer.setData("copilot/internal-drag", "true"); + + const linkText = app.metadataCache.fileToLinktext(file, ""); + const dragData = dragManager.dragLink(e.nativeEvent, linkText); + dragManager.onDragStart(e.nativeEvent, dragData); + }, []); + + return handleDragStart; +}