- {/* Header */}
-
-
- {totalCount > 0 && (
-
- {totalCount} annotation{totalCount === 1 ? '' : 's'} in {fileCount} file{fileCount === 1 ? '' : 's'}
-
- )}
-
- {/* Tree */}
-
- {tree.length === 0 ? (
-
- No markdown files found
-
- ) : (
- tree.map((node) => (
-
- ))
- )}
-
-
- );
-};
diff --git a/packages/ui/hooks/useFileBrowser.ts b/packages/ui/hooks/useFileBrowser.ts
index c34d2229..0547e8b5 100644
--- a/packages/ui/hooks/useFileBrowser.ts
+++ b/packages/ui/hooks/useFileBrowser.ts
@@ -3,6 +3,8 @@
*
* Manages multiple directory file trees for the sidebar Files tab.
* Each directory gets its own tree, loading, and error state.
+ * Vault directories are supported via the isVault flag — they fetch
+ * from the Obsidian vault endpoint instead of the generic files endpoint.
*/
import { useState, useCallback } from "react";
@@ -14,6 +16,8 @@ export interface DirState {
tree: VaultNode[];
isLoading: boolean;
error: string | null;
+ /** When true, fetches via /api/reference/obsidian/files and opens docs via /api/reference/obsidian/doc */
+ isVault?: boolean;
}
export interface UseFileBrowserReturn {
@@ -24,6 +28,8 @@ export interface UseFileBrowserReturn {
toggleCollapse: (dirPath: string) => void;
fetchTree: (dirPath: string) => void;
fetchAll: (directories: string[]) => void;
+ addVaultDir: (vaultPath: string) => void;
+ clearVaultDirs: () => void;
activeFile: string | null;
activeDirPath: string | null;
setActiveFile: (path: string | null) => void;
@@ -97,20 +103,74 @@ export function useFileBrowser(): UseFileBrowserReturn {
const fetchAll = useCallback(
(directories: string[]) => {
- setDirs(
- directories.map((path) => ({
+ setDirs((prev) => {
+ // Preserve any vault dirs that were already loaded
+ const vaultDirs = prev.filter((d) => d.isVault);
+ const regularDirs = directories.map((path) => ({
path,
name: path.split("/").pop() || path,
tree: [],
isLoading: false,
error: null,
- }))
- );
+ }));
+ return [...regularDirs, ...vaultDirs];
+ });
directories.forEach((d) => fetchTree(d));
},
[fetchTree]
);
+ const clearVaultDirs = useCallback(() => {
+ setDirs((prev) => prev.filter((d) => !d.isVault));
+ }, []);
+
+ const addVaultDir = useCallback(async (vaultPath: string) => {
+ const name = vaultPath.split("/").pop() || vaultPath;
+
+ // Atomically replace any existing vault dirs (handles vault path change without accumulating stale entries)
+ setDirs((prev) => {
+ const nonVaultDirs = prev.filter((d) => !d.isVault);
+ return [...nonVaultDirs, { path: vaultPath, name, tree: [], isLoading: true, error: null, isVault: true }];
+ });
+
+ try {
+ const res = await fetch(
+ `/api/reference/obsidian/files?vaultPath=${encodeURIComponent(vaultPath)}`
+ );
+ const data = await res.json();
+
+ if (!res.ok || data.error) {
+ setDirs((prev) =>
+ prev.map((d) =>
+ d.path === vaultPath ? { ...d, isLoading: false, error: data.error || "Failed to load" } : d
+ )
+ );
+ return;
+ }
+
+ setDirs((prev) =>
+ prev.map((d) =>
+ d.path === vaultPath ? { ...d, tree: data.tree, isLoading: false, isVault: true } : d
+ )
+ );
+
+ const rootFolders = (data.tree as VaultNode[])
+ .filter((n) => n.type === "folder")
+ .map((n) => `${vaultPath}:${n.path}`);
+ setExpandedFolders((prev) => {
+ const next = new Set(prev);
+ rootFolders.forEach((f) => next.add(f));
+ return next;
+ });
+ } catch {
+ setDirs((prev) =>
+ prev.map((d) =>
+ d.path === vaultPath ? { ...d, isLoading: false, error: "Failed to connect to server" } : d
+ )
+ );
+ }
+ }, []);
+
const toggleFolder = useCallback((key: string) => {
setExpandedFolders((prev) => {
const next = new Set(prev);
@@ -131,6 +191,8 @@ export function useFileBrowser(): UseFileBrowserReturn {
toggleCollapse,
fetchTree,
fetchAll,
+ addVaultDir,
+ clearVaultDirs,
activeFile,
activeDirPath: activeFile ? (dirs.find((d) => activeFile.startsWith(d.path + "/"))?.path ?? null) : null,
setActiveFile,
diff --git a/packages/ui/hooks/useSidebar.ts b/packages/ui/hooks/useSidebar.ts
index 8d16893b..dc69131f 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, Versions, or Vault).
- * The sidebar is shared between the Table of Contents, Version Browser, and Vault Browser views.
+ * Manages the shared left sidebar state: open/close and active tab.
+ * The sidebar is shared between the Table of Contents, Version Browser, File Browser, and Archive views.
*/
import { useState, useCallback } from "react";
-export type SidebarTab = "toc" | "versions" | "files" | "vault" | "archive";
+export type SidebarTab = "toc" | "versions" | "files" | "archive";
export interface UseSidebarReturn {
isOpen: boolean;
diff --git a/packages/ui/hooks/useVaultBrowser.ts b/packages/ui/hooks/useVaultBrowser.ts
deleted file mode 100644
index 09ce51d2..00000000
--- a/packages/ui/hooks/useVaultBrowser.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * 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