diff --git a/CLAUDE.md b/CLAUDE.md index 1a03facc..41370906 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,6 +156,8 @@ Send Annotations → feedback sent to agent session | `/api/image` | GET | Serve image by path query param | | `/api/upload` | POST | Upload image, returns `{ path, originalName }` | | `/api/obsidian/vaults`| GET | Detect available Obsidian vaults | +| `/api/reference/obsidian/files` | GET | List vault markdown files as nested tree (`?vaultPath=`) | +| `/api/reference/obsidian/doc` | GET | Read a vault markdown file (`?vaultPath=&path=`) | | `/api/plan/vscode-diff` | POST | Open diff in VS Code (body: baseVersion) | | `/api/doc` | GET | Serve linked .md/.mdx file (`?path=`) | diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index c749e3f8..158ffaff 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -41,6 +41,8 @@ import { deriveImageName } from '@plannotator/ui/components/AttachmentsButton'; import { useSidebar } from '@plannotator/ui/hooks/useSidebar'; import { usePlanDiff, type VersionInfo } from '@plannotator/ui/hooks/usePlanDiff'; import { useLinkedDoc } from '@plannotator/ui/hooks/useLinkedDoc'; +import { useVaultBrowser } from '@plannotator/ui/hooks/useVaultBrowser'; +import { isVaultBrowserEnabled } from '@plannotator/ui/utils/obsidian'; import { SidebarTabs } from '@plannotator/ui/components/sidebar/SidebarTabs'; import { SidebarContainer } from '@plannotator/ui/components/sidebar/SidebarContainer'; import { PlanDiffViewer } from '@plannotator/ui/components/plan-diff/PlanDiffViewer'; @@ -435,6 +437,59 @@ const App: React.FC = () => { viewerRef, sidebar, }); + // Obsidian vault browser + const vaultBrowser = useVaultBrowser(); + + const showVaultTab = useMemo(() => isVaultBrowserEnabled(), [uiPrefs]); + const vaultPath = useMemo(() => { + if (!showVaultTab) return ''; + const settings = getObsidianSettings(); + return getEffectiveVaultPath(settings); + }, [showVaultTab, uiPrefs]); + + // Clear active file when vault browser is disabled + useEffect(() => { + if (!showVaultTab) vaultBrowser.setActiveFile(null); + }, [showVaultTab]); + + // Auto-fetch vault tree when vault tab is first opened + useEffect(() => { + if (sidebar.activeTab === 'vault' && showVaultTab && vaultPath && vaultBrowser.tree.length === 0 && !vaultBrowser.isLoading) { + vaultBrowser.fetchTree(vaultPath); + } + }, [sidebar.activeTab, showVaultTab, vaultPath]); + + const buildVaultDocUrl = React.useCallback( + (vp: string) => (path: string) => + `/api/reference/obsidian/doc?vaultPath=${encodeURIComponent(vp)}&path=${encodeURIComponent(path)}`, + [] + ); + + // Vault file selection: open via linked doc system with vault endpoint + const handleVaultFileSelect = React.useCallback((relativePath: string) => { + linkedDocHook.open(relativePath, buildVaultDocUrl(vaultPath)); + vaultBrowser.setActiveFile(relativePath); + }, [vaultPath, linkedDocHook, vaultBrowser, buildVaultDocUrl]); + + // Route linked doc opens through vault endpoint when viewing a vault file + const handleOpenLinkedDoc = React.useCallback((docPath: string) => { + if (vaultBrowser.activeFile && vaultPath) { + linkedDocHook.open(docPath, buildVaultDocUrl(vaultPath)); + } else { + linkedDocHook.open(docPath); + } + }, [vaultBrowser.activeFile, vaultPath, linkedDocHook, buildVaultDocUrl]); + + // Wrap linked doc back to also clear vault active file + const handleLinkedDocBack = React.useCallback(() => { + linkedDocHook.back(); + vaultBrowser.setActiveFile(null); + }, [linkedDocHook, vaultBrowser]); + + const handleVaultFetchTree = React.useCallback(() => { + vaultBrowser.fetchTree(vaultPath); + }, [vaultBrowser, vaultPath]); + // Track active section for TOC highlighting const headingCount = useMemo(() => blocks.filter(b => b.type === 'heading').length, [blocks]); const activeSection = useActiveSection(containerRef, headingCount); @@ -1199,6 +1254,7 @@ const App: React.FC = () => { activeTab={sidebar.activeTab} onToggleTab={sidebar.toggleTab} hasDiff={planDiff.hasPreviousVersion} + showVaultTab={showVaultTab} className="hidden lg:flex" /> )} @@ -1216,7 +1272,12 @@ const App: React.FC = () => { activeSection={activeSection} onTocNavigate={handleTocNavigate} linkedDocFilepath={linkedDocHook.filepath} - onLinkedDocBack={linkedDocHook.isActive ? linkedDocHook.back : undefined} + onLinkedDocBack={linkedDocHook.isActive ? handleLinkedDocBack : undefined} + showVaultTab={showVaultTab} + vaultPath={vaultPath} + vaultBrowser={vaultBrowser} + onVaultSelectFile={handleVaultFileSelect} + onVaultFetchTree={handleVaultFetchTree} versionInfo={versionInfo} versions={planDiff.versions} projectPlans={planDiff.projectPlans} @@ -1280,8 +1341,8 @@ const App: React.FC = () => { onPlanDiffToggle={() => setIsPlanDiffActive(!isPlanDiffActive)} hasPreviousVersion={!linkedDocHook.isActive && planDiff.hasPreviousVersion} showDemoBadge={!isApiMode && !isLoadingShared && !isSharedSession} - onOpenLinkedDoc={linkedDocHook.open} - linkedDocInfo={linkedDocHook.isActive ? { filepath: linkedDocHook.filepath!, onBack: linkedDocHook.back } : null} + onOpenLinkedDoc={handleOpenLinkedDoc} + linkedDocInfo={linkedDocHook.isActive ? { filepath: linkedDocHook.filepath!, onBack: handleLinkedDocBack, label: vaultBrowser.activeFile ? 'Vault File' : undefined } : null} /> )} diff --git a/packages/server/index.ts b/packages/server/index.ts index 18bc72ed..e2aecbb0 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -9,7 +9,7 @@ * PLANNOTATOR_ORIGIN - Origin identifier ("claude-code" or "opencode") */ -import { mkdirSync } from "fs"; +import { mkdirSync, existsSync, statSync } from "fs"; import { resolve } from "path"; import { isRemoteSession, getServerPort } from "./remote"; import { openBrowser } from "./browser"; @@ -350,6 +350,84 @@ export async function startPlannotatorServer( return Response.json({ vaults }); } + // API: List Obsidian vault files as a tree + if (url.pathname === "/api/reference/obsidian/files" && req.method === "GET") { + const vaultPath = url.searchParams.get("vaultPath"); + if (!vaultPath) { + return Response.json({ error: "Missing vaultPath parameter" }, { status: 400 }); + } + + const resolvedVault = resolve(vaultPath); + if (!existsSync(resolvedVault) || !statSync(resolvedVault).isDirectory()) { + return Response.json({ error: "Invalid vault path" }, { status: 400 }); + } + + try { + const glob = new Bun.Glob("**/*.md"); + const files: string[] = []; + for await (const match of glob.scan({ cwd: resolvedVault, onlyFiles: true })) { + if (match.includes(".obsidian/") || match.includes(".trash/")) continue; + files.push(match); + } + files.sort(); + + const tree = buildFileTree(files); + return Response.json({ tree }); + } catch { + return Response.json({ error: "Failed to list vault files" }, { status: 500 }); + } + } + + // API: Read an Obsidian vault document + if (url.pathname === "/api/reference/obsidian/doc" && req.method === "GET") { + const vaultPath = url.searchParams.get("vaultPath"); + const filePath = url.searchParams.get("path"); + if (!vaultPath || !filePath) { + return Response.json({ error: "Missing vaultPath or path parameter" }, { status: 400 }); + } + if (!/\.mdx?$/i.test(filePath)) { + return Response.json({ error: "Only markdown files are supported" }, { status: 400 }); + } + + const resolvedVault = resolve(vaultPath); + let resolvedFile = resolve(resolvedVault, filePath); + + // If direct path doesn't exist and it's a bare filename, search the vault + if (!existsSync(resolvedFile) && !filePath.includes("/")) { + const glob = new Bun.Glob(`**/${filePath}`); + const matches: string[] = []; + for await (const match of glob.scan({ cwd: resolvedVault, onlyFiles: true })) { + if (match.includes(".obsidian/") || match.includes(".trash/")) continue; + matches.push(resolve(resolvedVault, match)); + } + if (matches.length === 1) { + resolvedFile = matches[0]; + } else if (matches.length > 1) { + const relativePaths = matches.map((m) => m.replace(resolvedVault + "/", "")); + return Response.json( + { error: `Ambiguous filename '${filePath}': found ${matches.length} matches`, matches: relativePaths }, + { status: 400 } + ); + } + } + + // Security: must be within vault + if (!resolvedFile.startsWith(resolvedVault + "/")) { + return Response.json({ error: "Access denied: path is outside vault" }, { status: 403 }); + } + + try { + const file = Bun.file(resolvedFile); + if (!(await file.exists())) { + return Response.json({ error: `File not found: ${filePath}` }, { status: 404 }); + } + const markdown = await file.text(); + return Response.json({ markdown, filepath: resolvedFile }); + } catch { + return Response.json({ error: "Failed to read file" }, { status: 500 }); + } + } + // API: Get available agents (OpenCode only) if (url.pathname === "/api/agents") { if (!options.opencodeClient) { @@ -572,3 +650,56 @@ export async function handleServerReady( await openBrowser(url); } } + +// --- Vault file tree helpers --- + +export interface VaultNode { + name: string; + path: string; // relative path within vault + type: "file" | "folder"; + children?: VaultNode[]; +} + +/** + * Build a nested file tree from a sorted list of relative paths. + * Folders are sorted before files at each level. + */ +function buildFileTree(relativePaths: string[]): VaultNode[] { + const root: VaultNode[] = []; + + for (const filePath of relativePaths) { + const parts = filePath.split("/"); + let current = root; + let pathSoFar = ""; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + pathSoFar = pathSoFar ? `${pathSoFar}/${part}` : part; + const isFile = i === parts.length - 1; + + let node = current.find((n) => n.name === part && n.type === (isFile ? "file" : "folder")); + if (!node) { + node = { name: part, path: pathSoFar, type: isFile ? "file" : "folder" }; + if (!isFile) node.children = []; + current.push(node); + } + if (!isFile) { + current = node.children!; + } + } + } + + // Sort: folders first (alphabetical), then files (alphabetical) + const sortNodes = (nodes: VaultNode[]) => { + nodes.sort((a, b) => { + if (a.type !== b.type) return a.type === "folder" ? -1 : 1; + return a.name.localeCompare(b.name); + }); + for (const node of nodes) { + if (node.children) sortNodes(node.children); + } + }; + sortNodes(root); + + return root; +} diff --git a/packages/ui/components/Settings.tsx b/packages/ui/components/Settings.tsx index 30df54b1..f71dee08 100644 --- a/packages/ui/components/Settings.tsx +++ b/packages/ui/components/Settings.tsx @@ -678,6 +678,29 @@ tags: [plan, ...] ---`} + +
+
+
+
Vault Browser
+
+ Browse and annotate vault files from the sidebar +
+
+ +
+
)} diff --git a/packages/ui/components/Viewer.tsx b/packages/ui/components/Viewer.tsx index 6884e606..878523e2 100644 --- a/packages/ui/components/Viewer.tsx +++ b/packages/ui/components/Viewer.tsx @@ -27,7 +27,7 @@ interface ViewerProps { repoInfo?: { display: string; branch?: string } | null; stickyActions?: boolean; onOpenLinkedDoc?: (path: string) => void; - linkedDocInfo?: { filepath: string; onBack: () => void } | null; + linkedDocInfo?: { filepath: string; onBack: () => void; label?: string } | null; // Plan diff props planDiffStats?: { additions: number; deletions: number; modifications: number } | null; isPlanDiffActive?: boolean; @@ -694,7 +694,7 @@ export const Viewer = forwardRef(({ plan - Linked File + {linkedDocInfo.label || 'Linked File'} { + e.preventDefault(); + onOpenLinkedDoc(targetPath); + }} + className="text-primary underline underline-offset-2 hover:text-primary/80 inline-flex items-center gap-1 cursor-pointer" + title={`Open: ${target}`} + > + {display} + + + ); + } else { + parts.push( + {display} + ); + } + remaining = remaining.slice(match[0].length); + continue; + } + // Links: [text](url) match = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/); if (match) { diff --git a/packages/ui/components/sidebar/SidebarContainer.tsx b/packages/ui/components/sidebar/SidebarContainer.tsx index 0a413807..d67c8d1c 100644 --- a/packages/ui/components/sidebar/SidebarContainer.tsx +++ b/packages/ui/components/sidebar/SidebarContainer.tsx @@ -1,7 +1,7 @@ /** * SidebarContainer — Shared sidebar shell * - * Houses both the Table of Contents and Version Browser views. + * Houses the Table of Contents, Version Browser, and Vault Browser views. * Tab bar at top switches between them. */ @@ -9,8 +9,10 @@ import React from "react"; import type { SidebarTab } from "../../hooks/useSidebar"; import type { Block, Annotation } from "../../types"; import type { VersionInfo, VersionEntry, ProjectPlan } from "../../hooks/usePlanDiff"; +import type { UseVaultBrowserReturn } from "../../hooks/useVaultBrowser"; import { TableOfContents } from "../TableOfContents"; import { VersionBrowser } from "./VersionBrowser"; +import { VaultBrowser } from "./VaultBrowser"; interface SidebarContainerProps { activeTab: SidebarTab; @@ -24,6 +26,12 @@ interface SidebarContainerProps { onTocNavigate: (blockId: string) => void; linkedDocFilepath?: string | null; onLinkedDocBack?: () => void; + // Vault Browser props + showVaultTab?: boolean; + vaultPath?: string; + vaultBrowser?: UseVaultBrowserReturn; + onVaultSelectFile?: (relativePath: string) => void; + onVaultFetchTree?: () => void; // Version Browser props versionInfo: VersionInfo | null; versions: VersionEntry[]; @@ -51,6 +59,11 @@ export const SidebarContainer: React.FC = ({ onTocNavigate, linkedDocFilepath, onLinkedDocBack, + showVaultTab, + vaultPath, + vaultBrowser, + onVaultSelectFile, + onVaultFetchTree, versionInfo, versions, projectPlans, @@ -112,6 +125,28 @@ export const SidebarContainer: React.FC = ({ } label="Versions" /> + {showVaultTab && ( + onTabChange("vault")} + icon={ + + + + } + label="Vault" + /> + )}
); diff --git a/packages/ui/components/sidebar/SidebarTabs.tsx b/packages/ui/components/sidebar/SidebarTabs.tsx index 8c3bb174..cf1215de 100644 --- a/packages/ui/components/sidebar/SidebarTabs.tsx +++ b/packages/ui/components/sidebar/SidebarTabs.tsx @@ -1,7 +1,7 @@ /** * SidebarTabs — Collapsed tab flags * - * When the sidebar is closed, two small vertical tabs protrude from the left edge. + * When the sidebar is closed, small vertical tabs protrude from the left edge. * Clicking a tab opens the sidebar in that mode. */ @@ -12,6 +12,7 @@ interface SidebarTabsProps { activeTab: SidebarTab; onToggleTab: (tab: SidebarTab) => void; hasDiff: boolean; + showVaultTab?: boolean; className?: string; } @@ -19,6 +20,7 @@ export const SidebarTabs: React.FC = ({ activeTab, onToggleTab, hasDiff, + showVaultTab, className, }) => { return ( @@ -70,6 +72,29 @@ export const SidebarTabs: React.FC = ({ )} + + {/* Vault tab */} + {showVaultTab && ( + + )} ); }; diff --git a/packages/ui/components/sidebar/VaultBrowser.tsx b/packages/ui/components/sidebar/VaultBrowser.tsx new file mode 100644 index 00000000..fb3ed943 --- /dev/null +++ b/packages/ui/components/sidebar/VaultBrowser.tsx @@ -0,0 +1,159 @@ +/** + * VaultBrowser — Obsidian vault file tree for the sidebar + * + * Displays a collapsible tree of markdown files from the user's Obsidian vault. + * Clicking a file opens it in the main viewer for annotation. + */ + +import React from "react"; +import type { VaultNode } from "../../types"; + +interface VaultBrowserProps { + vaultPath: string; + tree: VaultNode[]; + isLoading: boolean; + error: string | null; + expandedFolders: Set; + onToggleFolder: (path: string) => void; + onSelectFile: (relativePath: string) => void; + activeFile: string | null; + onFetchTree: () => void; +} + +const TreeNode: React.FC<{ + node: VaultNode; + depth: number; + expandedFolders: Set; + onToggleFolder: (path: string) => void; + onSelectFile: (path: string) => void; + activeFile: string | null; +}> = ({ node, depth, expandedFolders, onToggleFolder, onSelectFile, activeFile }) => { + const isExpanded = expandedFolders.has(node.path); + const isActive = node.type === "file" && node.path === activeFile; + const paddingLeft = 8 + depth * 14; + + if (node.type === "folder") { + return ( + <> + + {isExpanded && node.children?.map((child) => ( + + ))} + + ); + } + + // File node + const displayName = node.name.replace(/\.md$/i, ""); + return ( + + ); +}; + +export const VaultBrowser: React.FC = ({ + vaultPath, + tree, + isLoading, + error, + expandedFolders, + onToggleFolder, + onSelectFile, + activeFile, + onFetchTree, +}) => { + const vaultName = vaultPath.split("/").pop() || "Vault"; + + if (isLoading) { + return ( +
+ Loading vault... +
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } + + return ( +
+ {/* Header */} +
+
+ {vaultName} +
+
+ + {/* Tree */} +
+ {tree.length === 0 ? ( +
+ No markdown files found +
+ ) : ( + tree.map((node) => ( + + )) + )} +
+
+ ); +}; diff --git a/packages/ui/hooks/useLinkedDoc.ts b/packages/ui/hooks/useLinkedDoc.ts index 6d0d39a1..d73ac791 100644 --- a/packages/ui/hooks/useLinkedDoc.ts +++ b/packages/ui/hooks/useLinkedDoc.ts @@ -45,7 +45,7 @@ export interface UseLinkedDocReturn { /** Whether a fetch is in progress */ isLoading: boolean; /** Open a linked document by path (saves plan state, fetches doc, swaps) */ - open: (docPath: string) => Promise; + open: (docPath: string, buildUrl?: (path: string) => string) => Promise; /** Return to the plan (caches doc annotations, restores plan state) */ back: () => void; /** Dismiss the current error */ @@ -80,15 +80,19 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn { // Cache linked doc annotations keyed by filepath (persists across back/forth within session) const docCache = useRef>(new Map()); + const defaultBuildUrl = useCallback( + (path: string) => `/api/doc?path=${encodeURIComponent(path)}`, + [] + ); + const open = useCallback( - async (docPath: string) => { + async (docPath: string, buildUrl?: (path: string) => string) => { setIsLoading(true); setError(null); try { - const res = await fetch( - `/api/doc?path=${encodeURIComponent(docPath)}` - ); + const url = (buildUrl ?? defaultBuildUrl)(docPath); + const res = await fetch(url); const data = (await res.json()) as { markdown?: string; filepath?: string; diff --git a/packages/ui/hooks/useSidebar.ts b/packages/ui/hooks/useSidebar.ts index c2ecb9fe..f8d46f37 100644 --- a/packages/ui/hooks/useSidebar.ts +++ b/packages/ui/hooks/useSidebar.ts @@ -1,13 +1,13 @@ /** * Sidebar Hook * - * Manages the shared left sidebar state: open/close and active tab (TOC or Versions). - * The sidebar is shared between the Table of Contents and Version Browser views. + * Manages the shared left sidebar state: open/close and active tab (TOC, Versions, or Vault). + * The sidebar is shared between the Table of Contents, Version Browser, and Vault Browser views. */ import { useState, useCallback } from "react"; -export type SidebarTab = "toc" | "versions"; +export type SidebarTab = "toc" | "versions" | "vault"; export interface UseSidebarReturn { isOpen: boolean; diff --git a/packages/ui/hooks/useVaultBrowser.ts b/packages/ui/hooks/useVaultBrowser.ts new file mode 100644 index 00000000..09ce51d2 --- /dev/null +++ b/packages/ui/hooks/useVaultBrowser.ts @@ -0,0 +1,83 @@ +/** + * Vault Browser Hook + * + * Manages Obsidian vault file tree state for the sidebar vault tab. + * Fetches the full tree from /api/reference/obsidian/files, tracks + * expanded folders and the currently active file. + */ + +import { useState, useCallback } from "react"; +import type { VaultNode } from "../types"; + +export type { VaultNode }; + +export interface UseVaultBrowserReturn { + tree: VaultNode[]; + isLoading: boolean; + error: string | null; + expandedFolders: Set; + toggleFolder: (path: string) => void; + fetchTree: (vaultPath: string) => void; + activeFile: string | null; + setActiveFile: (path: string | null) => void; +} + +export function useVaultBrowser(): UseVaultBrowserReturn { + const [tree, setTree] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [activeFile, setActiveFile] = useState(null); + + const fetchTree = useCallback(async (vaultPath: string) => { + setIsLoading(true); + setError(null); + + try { + const res = await fetch( + `/api/reference/obsidian/files?vaultPath=${encodeURIComponent(vaultPath)}` + ); + const data = await res.json(); + + if (!res.ok || data.error) { + setError(data.error || "Failed to load vault"); + return; + } + + setTree(data.tree); + + // Auto-expand root-level folders + const rootFolders = (data.tree as VaultNode[]) + .filter((n) => n.type === "folder") + .map((n) => n.path); + setExpandedFolders(new Set(rootFolders)); + } catch { + setError("Failed to connect to server"); + } finally { + setIsLoading(false); + } + }, []); + + const toggleFolder = useCallback((path: string) => { + setExpandedFolders((prev) => { + const next = new Set(prev); + if (next.has(path)) { + next.delete(path); + } else { + next.add(path); + } + return next; + }); + }, []); + + return { + tree, + isLoading, + error, + expandedFolders, + toggleFolder, + fetchTree, + activeFile, + setActiveFile, + }; +} diff --git a/packages/ui/types.ts b/packages/ui/types.ts index d8ec7775..78e532f1 100644 --- a/packages/ui/types.ts +++ b/packages/ui/types.ts @@ -87,3 +87,10 @@ export interface SelectedLineRange { side: 'deletions' | 'additions'; endSide?: 'deletions' | 'additions'; } + +export interface VaultNode { + name: string; + path: string; // relative path within vault + type: "file" | "folder"; + children?: VaultNode[]; +} diff --git a/packages/ui/utils/obsidian.ts b/packages/ui/utils/obsidian.ts index 91933633..a8a057c7 100644 --- a/packages/ui/utils/obsidian.ts +++ b/packages/ui/utils/obsidian.ts @@ -14,6 +14,7 @@ const STORAGE_KEY_VAULT = 'plannotator-obsidian-vault'; const STORAGE_KEY_FOLDER = 'plannotator-obsidian-folder'; const STORAGE_KEY_CUSTOM_PATH = 'plannotator-obsidian-custom-path'; const STORAGE_KEY_FILENAME_FORMAT = 'plannotator-obsidian-filename-format'; +const STORAGE_KEY_VAULT_BROWSER = 'plannotator-obsidian-vault-browser'; // Sentinel value for custom path selection export const CUSTOM_PATH_SENTINEL = '__custom__'; @@ -33,6 +34,7 @@ export interface ObsidianSettings { folder: string; customPath?: string; // User-entered path when vaultPath === '__custom__' filenameFormat?: string; // Custom filename format (e.g. '{YYYY}-{MM}-{DD} - {title}') + vaultBrowserEnabled: boolean; // Show vault file browser in sidebar } /** @@ -45,6 +47,7 @@ export function getObsidianSettings(): ObsidianSettings { folder: storage.getItem(STORAGE_KEY_FOLDER) || DEFAULT_FOLDER, customPath: storage.getItem(STORAGE_KEY_CUSTOM_PATH) || undefined, filenameFormat: storage.getItem(STORAGE_KEY_FILENAME_FORMAT) || undefined, + vaultBrowserEnabled: storage.getItem(STORAGE_KEY_VAULT_BROWSER) === 'true', }; } @@ -57,6 +60,7 @@ export function saveObsidianSettings(settings: ObsidianSettings): void { storage.setItem(STORAGE_KEY_FOLDER, settings.folder); storage.setItem(STORAGE_KEY_CUSTOM_PATH, settings.customPath || ''); storage.setItem(STORAGE_KEY_FILENAME_FORMAT, settings.filenameFormat || ''); + storage.setItem(STORAGE_KEY_VAULT_BROWSER, String(settings.vaultBrowserEnabled)); } /** @@ -78,6 +82,15 @@ export function isObsidianConfigured(): boolean { return settings.enabled && effectivePath.trim().length > 0; } +/** + * Check if the vault browser sidebar tab should be shown + */ +export function isVaultBrowserEnabled(): boolean { + const settings = getObsidianSettings(); + const effectivePath = getEffectiveVaultPath(settings); + return settings.enabled && settings.vaultBrowserEnabled && effectivePath.trim().length > 0; +} + /** * Extract tags from markdown content using simple heuristics *