| 1 | import { useEffect, useState } from "react"; |
| 2 | import { ExternalLink, FileText, FolderOpen, MessageSquarePlus, Pin, PinOff, TerminalSquare } from "lucide-react"; |
| 3 | import { app } from "../lib/bridge"; |
| 4 | import { useT } from "../lib/i18n"; |
| 5 | import { useToast } from "../lib/toast"; |
| 6 | import { |
| 7 | WORKSPACE_CONTEXT_MENU_FILE_HEIGHT, |
| 8 | WORKSPACE_CONTEXT_MENU_REF_HEIGHT, |
| 9 | workspacePathCopyMenuItems, |
| 10 | } from "../lib/workspacePathCopyMenuItems"; |
| 11 | import { FloatingMenu, FloatingMenuItems } from "./FloatingMenu"; |
| 12 | |
| 13 | export interface WorkspaceTreeMenuTarget { |
| 14 | x: number; |
| 15 | y: number; |
| 16 | path: string; |
| 17 | isDir: boolean; |
| 18 | } |
| 19 | |
| 20 | export function WorkspaceTreeMenu({ |
| 21 | target, |
| 22 | workspaceTabId, |
| 23 | isScopeCurrent, |
| 24 | onClose, |
| 25 | onOpenInTerminal, |
| 26 | onAddReference, |
| 27 | onAddFile, |
| 28 | }: { |
| 29 | target: WorkspaceTreeMenuTarget; |
| 30 | workspaceTabId: string; |
| 31 | isScopeCurrent: () => boolean; |
| 32 | onClose: () => void; |
| 33 | onOpenInTerminal?: (path: string) => void; |
| 34 | onAddReference: () => void; |
| 35 | onAddFile: () => void; |
| 36 | }) { |
| 37 | const t = useT(); |
| 38 | const { showToast } = useToast(); |
| 39 | const [isPinned, setIsPinned] = useState(false); |
| 40 | |
| 41 | useEffect(() => { |
| 42 | if (target.isDir || !workspaceTabId) return; |
| 43 | let active = true; |
| 44 | app.GetPinnedFilesForTab(workspaceTabId) |
| 45 | .then((files) => { |
| 46 | if (!active || !Array.isArray(files)) return; |
| 47 | const normalizedTarget = target.path.replace(/^[/\\]+/, "").replace(/\\/g, "/"); |
| 48 | const found = files.some( |
| 49 | (f) => f.path.replace(/^[/\\]+/, "").replace(/\\/g, "/") === normalizedTarget |
| 50 | ); |
| 51 | setIsPinned(found); |
| 52 | }) |
| 53 | .catch(() => {}); |
| 54 | return () => { |
| 55 | active = false; |
| 56 | }; |
| 57 | }, [workspaceTabId, target.path, target.isDir]); |
| 58 | |
| 59 | const closeThen = (action: () => void) => { |
| 60 | onClose(); |
| 61 | action(); |
| 62 | }; |
| 63 | |
| 64 | return ( |
| 65 | <FloatingMenu |
| 66 | x={target.x} |
| 67 | y={target.y} |
| 68 | estimatedHeight={target.isDir ? WORKSPACE_CONTEXT_MENU_REF_HEIGHT : WORKSPACE_CONTEXT_MENU_FILE_HEIGHT + 32} |
| 69 | className="workspace-tree-menu" |
| 70 | > |
| 71 | <FloatingMenuItems |
| 72 | items={[ |
| 73 | ...(target.isDir |
| 74 | ? [] |
| 75 | : [{ |
| 76 | icon: <ExternalLink size={14} />, |
| 77 | label: t("workspace.openWithDefaultApp"), |
| 78 | onSelect: () => closeThen(() => void app.OpenWorkspacePathForTab(workspaceTabId, target.path).catch(() => {})), |
| 79 | }]), |
| 80 | { |
| 81 | icon: <FolderOpen size={14} />, |
| 82 | label: t("workspace.revealInFileManager"), |
| 83 | onSelect: () => closeThen(() => void app.RevealWorkspacePathForTab(workspaceTabId, target.path).catch(() => {})), |
| 84 | }, |
| 85 | ...(onOpenInTerminal |
| 86 | ? [{ |
| 87 | icon: <TerminalSquare size={14} />, |
| 88 | label: t("workspace.openInTerminal"), |
| 89 | onSelect: () => closeThen(() => onOpenInTerminal(target.path)), |
| 90 | }] |
| 91 | : []), |
| 92 | ...workspacePathCopyMenuItems({ |
| 93 | path: target.path, |
| 94 | resolveAbsolutePath: () => app.ResolveWorkspacePathForTab(workspaceTabId, target.path), |
| 95 | isScopeCurrent, |
| 96 | close: onClose, |
| 97 | relativeLabel: t("workspace.copyRelativePath"), |
| 98 | absoluteLabel: t("workspace.copyAbsolutePath"), |
| 99 | }), |
| 100 | { separator: true }, |
| 101 | { |
| 102 | icon: <MessageSquarePlus size={14} />, |
| 103 | label: target.isDir ? t("workspace.addFolderReferenceToChat") : t("workspace.addFileReferenceToChat"), |
| 104 | onSelect: onAddReference, |
| 105 | }, |
| 106 | ...(target.isDir |
| 107 | ? [] |
| 108 | : [ |
| 109 | { |
| 110 | icon: <FileText size={14} />, |
| 111 | label: t("workspace.addFileContentToChat"), |
| 112 | onSelect: onAddFile, |
| 113 | }, |
| 114 | { |
| 115 | icon: isPinned ? <PinOff size={14} /> : <Pin size={14} />, |
| 116 | label: isPinned ? t("workspace.unpinFileFromContext") : t("workspace.pinFileToContext"), |
| 117 | onSelect: () => |
| 118 | closeThen(() => { |
| 119 | if (isPinned) { |
| 120 | void app.UnpinFileForTab(workspaceTabId, target.path).catch((err: unknown) => { |
| 121 | showToast(String((err as Error)?.message || err), "error"); |
| 122 | }); |
| 123 | } else { |
| 124 | void app.PinFileForTab(workspaceTabId, target.path).catch((err: unknown) => { |
| 125 | showToast(String((err as Error)?.message || err), "error"); |
| 126 | }); |
| 127 | } |
| 128 | }), |
| 129 | }, |
| 130 | ]), |
| 131 | ]} |
| 132 | /> |
| 133 | </FloatingMenu> |
| 134 | ); |
| 135 | } |
| 136 |