diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index f07a1542..b4728739 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -679,6 +679,11 @@ export function setupIpcHandlers() { const error = await shell.openPath(filePath); return { error: error || undefined }; }, + 'shell:showItemInFolder': async (_event, args) => { + const filePath = resolveShellPath(args.path); + shell.showItemInFolder(filePath); + return { success: true }; + }, 'shell:readFileBase64': async (_event, args) => { const filePath = resolveShellPath(args.path); const stat = await fs.stat(filePath); diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index b3297ccd..7f76e2e9 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -3966,6 +3966,12 @@ function App() { document.body.removeChild(textarea) }) }, + revealInFileManager: (path: string, isDir: boolean) => { + const channel = isDir ? 'shell:openPath' : 'shell:showItemInFolder' + void window.ipc.invoke(channel, { path }).catch((err) => { + console.error('Failed to open in file manager:', err) + }) + }, onOpenInNewTab: (path: string) => { openFileInNewTab(path) }, @@ -4769,6 +4775,7 @@ function App() { rename: knowledgeActions.rename, remove: knowledgeActions.remove, copyPath: knowledgeActions.copyPath, + revealInFileManager: knowledgeActions.revealInFileManager, }} /> diff --git a/apps/x/apps/renderer/src/components/bases-view.tsx b/apps/x/apps/renderer/src/components/bases-view.tsx index a68eb360..b1f5413c 100644 --- a/apps/x/apps/renderer/src/components/bases-view.tsx +++ b/apps/x/apps/renderer/src/components/bases-view.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useEffect, useState, useMemo, useCallback, useRef } from 'react' -import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, X, Check, ListFilter, Filter, Search, Save, Copy, Pencil, Trash2 } from 'lucide-react' +import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, X, Check, ListFilter, Filter, Search, Save, Copy, FolderOpen, Pencil, Trash2 } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover' import { Command, CommandInput, CommandList, CommandItem, CommandEmpty, CommandGroup } from '@/components/ui/command' @@ -103,9 +103,18 @@ type BasesViewProps = { rename: (oldPath: string, newName: string, isDir: boolean) => Promise remove: (path: string) => Promise copyPath: (path: string) => void + revealInFileManager: (path: string, isDir: boolean) => void } } +function getFileManagerName(): string { + if (typeof navigator === 'undefined') return 'File Manager' + const platform = navigator.platform.toLowerCase() + if (platform.includes('mac')) return 'Finder' + if (platform.includes('win')) return 'Explorer' + return 'File Manager' +} + function collectFiles(nodes: TreeNode[]): { path: string; name: string; mtimeMs: number }[] { return nodes.flatMap((n) => n.kind === 'file' && n.name.endsWith('.md') @@ -919,6 +928,10 @@ function NoteRow({ Copy Path + actions?.revealInFileManager(note.path, false)}> + + Open in {getFileManagerName()} + { setNewName(baseName); isSubmittingRef.current = false; setIsRenaming(true) }}> diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 4f0947a6..51aa3940 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -11,6 +11,7 @@ import { ExternalLink, FilePlus, Folder, + FolderOpen, FolderPlus, Globe, AlertTriangle, @@ -119,9 +120,18 @@ type KnowledgeActions = { rename: (path: string, newName: string, isDir: boolean) => Promise remove: (path: string) => Promise copyPath: (path: string) => void + revealInFileManager: (path: string, isDir: boolean) => void onOpenInNewTab?: (path: string) => void } +function getFileManagerName(): string { + if (typeof navigator === 'undefined') return 'File Manager' + const platform = navigator.platform.toLowerCase() + if (platform.includes('mac')) return 'Finder' + if (platform.includes('win')) return 'Explorer' + return 'File Manager' +} + type RunListItem = { id: string title?: string @@ -1177,11 +1187,13 @@ function KnowledgeSection({ }, }), [actions, deriveParent]) + const fileManagerName = getFileManagerName() const quickActions = [ { icon: FilePlus, label: "New Note", action: () => wrappedActions.createNote() }, { icon: FolderPlus, label: "New Folder", action: () => void wrappedActions.createFolder() }, { icon: Network, label: "Graph View", action: () => actions.openGraph() }, { icon: Table2, label: "Bases", action: () => actions.openBases() }, + { icon: FolderOpen, label: `Open in ${fileManagerName}`, action: () => actions.revealInFileManager('knowledge', true) }, ] return ( @@ -1389,6 +1401,10 @@ function Tree({ Copy Path + actions.revealInFileManager(item.path, isDir)}> + + Open in {getFileManagerName()} + { setNewName(baseName); isSubmittingRef.current = false; setIsRenaming(true) }}> diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 2def4998..9ac31ce6 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -489,6 +489,10 @@ const ipcSchemas = { req: z.object({ path: z.string() }), res: z.object({ error: z.string().optional() }), }, + 'shell:showItemInFolder': { + req: z.object({ path: z.string() }), + res: z.object({ success: z.literal(true) }), + }, 'shell:readFileBase64': { req: z.object({ path: z.string() }), res: z.object({ data: z.string(), mimeType: z.string(), size: z.number() }),