import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Sidebar } from './components/Sidebar' import { NoteList } from './components/NoteList' import { Editor } from './components/Editor' import { ResizeHandle } from './components/ResizeHandle' import { CreateTypeDialog } from './components/CreateTypeDialog' import { QuickOpenPalette } from './components/QuickOpenPalette' import { CommandPalette } from './components/CommandPalette' import { SearchPanel } from './components/SearchPanel' import { Toast } from './components/Toast' import { CommitDialog } from './components/CommitDialog' import { PulseView } from './components/PulseView' import { StatusBar } from './components/StatusBar' import { SettingsPanel } from './components/SettingsPanel' import { GitHubVaultModal } from './components/GitHubVaultModal' import { WelcomeScreen } from './components/WelcomeScreen' import { useMcpStatus } from './hooks/useMcpStatus' import { useVaultLoader } from './hooks/useVaultLoader' import { useSettings } from './hooks/useSettings' import { useNoteActions, needsRenameOnSave } from './hooks/useNoteActions' import { useCommitFlow } from './hooks/useCommitFlow' import { useViewMode } from './hooks/useViewMode' import { useEntryActions } from './hooks/useEntryActions' import { useAppCommands } from './hooks/useAppCommands' import { useDialogs } from './hooks/useDialogs' import { useVaultSwitcher } from './hooks/useVaultSwitcher' import { useGitHistory } from './hooks/useGitHistory' import { useUpdater, restartApp } from './hooks/useUpdater' import { useNavigationHistory } from './hooks/useNavigationHistory' import { useAutoSync } from './hooks/useAutoSync' import { useConflictResolver } from './hooks/useConflictResolver' import { useIndexing } from './hooks/useIndexing' import { useZoom } from './hooks/useZoom' import { useVaultConfig } from './hooks/useVaultConfig' import { useBuildNumber } from './hooks/useBuildNumber' import { useOnboarding } from './hooks/useOnboarding' import { useThemeManager } from './hooks/useThemeManager' import { useEditorSaveWithLinks } from './hooks/useEditorSaveWithLinks' import { useNavigationGestures } from './hooks/useNavigationGestures' import { useAiActivity } from './hooks/useAiActivity' import { ConflictResolverModal } from './components/ConflictResolverModal' import { UpdateBanner } from './components/UpdateBanner' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from './mock-tauri' import type { SidebarSelection, VaultEntry } from './types' import type { NoteListItem } from './utils/ai-context' import { filterEntries } from './utils/noteListHelpers' import { openLocalFile } from './utils/url' import { flushEditorContent } from './utils/autoSave' import './App.css' // Type declarations for mock content storage and test overrides declare global { interface Window { __mockContent?: Record // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mock handler map for Playwright test overrides __mockHandlers?: Record any> } } const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' } function useBulkActions( entryActions: { handleArchiveNote: (path: string) => Promise; handleTrashNote: (path: string) => Promise }, setToastMessage: (msg: string | null) => void, ) { const handleBulkArchive = useCallback(async (paths: string[]) => { let ok = 0 for (const path of paths) { try { await entryActions.handleArchiveNote(path); ok++ } catch { /* error toast already shown by flushBeforeAction */ } } if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} archived`) }, [entryActions, setToastMessage]) const handleBulkTrash = useCallback(async (paths: string[]) => { let ok = 0 for (const path of paths) { try { await entryActions.handleTrashNote(path); ok++ } catch { /* error toast already shown by flushBeforeAction */ } } if (ok > 0) setToastMessage(`${ok} note${ok > 1 ? 's' : ''} moved to trash`) }, [entryActions, setToastMessage]) return { handleBulkArchive, handleBulkTrash } } function useLayoutPanels() { const [sidebarWidth, setSidebarWidth] = useState(250) const [noteListWidth, setNoteListWidth] = useState(300) const [inspectorWidth, setInspectorWidth] = useState(280) const [inspectorCollapsed, setInspectorCollapsed] = useState(false) const handleSidebarResize = useCallback((delta: number) => setSidebarWidth((w) => Math.max(150, Math.min(400, w + delta))), []) const handleNoteListResize = useCallback((delta: number) => setNoteListWidth((w) => Math.max(200, Math.min(500, w + delta))), []) const handleInspectorResize = useCallback((delta: number) => setInspectorWidth((w) => Math.max(200, Math.min(500, w - delta))), []) return { sidebarWidth, noteListWidth, inspectorWidth, inspectorCollapsed, setInspectorCollapsed, handleSidebarResize, handleNoteListResize, handleInspectorResize } } /** Wraps useEditorSave to also keep outgoingLinks in sync on save and on content change. */ function App() { const [selection, setSelection] = useState(DEFAULT_SELECTION) const layout = useLayoutPanels() const [toastMessage, setToastMessage] = useState(null) const dialogs = useDialogs() // onSwitch closure captures `notes` declared below — safe because it's only // called on user interaction, never during render (refs inside the hook // guarantee the latest closure is always used). const vaultSwitcher = useVaultSwitcher({ onSwitch: () => { setSelection(DEFAULT_SELECTION); notes.closeAllTabs() }, onToast: (msg) => setToastMessage(msg), }) const onboarding = useOnboarding(vaultSwitcher.vaultPath) // When onboarding resolves to a different vault path, update the switcher const resolvedPath = onboarding.state.status === 'ready' ? onboarding.state.vaultPath : vaultSwitcher.vaultPath const vault = useVaultLoader(resolvedPath) useVaultConfig(resolvedPath) const { settings, saveSettings } = useSettings() const themeManager = useThemeManager(resolvedPath, vault.entries) const { mcpStatus, installMcp } = useMcpStatus(resolvedPath, setToastMessage) const indexing = useIndexing(resolvedPath) const autoSync = useAutoSync({ vaultPath: resolvedPath, intervalMinutes: settings.auto_pull_interval_minutes, onVaultUpdated: vault.reloadVault, onSyncUpdated: indexing.triggerIncrementalIndex, onConflict: (files) => { const names = files.map((f) => f.split('/').pop()).join(', ') setToastMessage(`Conflict in ${names} — click to resolve`) }, onToast: (msg) => setToastMessage(msg), }) // Ref bridges for conflict resolution callbacks (notes declared below) const openConflictFileRef = useRef<(relativePath: string) => void>(() => {}) const conflictResolver = useConflictResolver({ vaultPath: resolvedPath, onResolved: () => { dialogs.closeConflictResolver() autoSync.resumePull() vault.reloadVault() autoSync.triggerSync() }, onToast: (msg) => setToastMessage(msg), onOpenFile: (relativePath) => openConflictFileRef.current(relativePath), }) const handleOpenConflictResolver = useCallback(async () => { let files = autoSync.conflictFiles // If no cached conflicts, check directly — there may be pre-existing // conflicts from a prior session that the pull flow didn't detect. if (files.length === 0) { try { files = isTauri() ? await invoke('get_conflict_files', { vaultPath: resolvedPath }) : await mockInvoke('get_conflict_files', { vaultPath: resolvedPath }) } catch { return } if (files.length === 0) { setToastMessage('No merge conflicts to resolve') return } } autoSync.pausePull() conflictResolver.initFiles(files) dialogs.openConflictResolver() }, [autoSync, conflictResolver, dialogs, resolvedPath]) const handleCloseConflictResolver = useCallback(() => { autoSync.resumePull() dialogs.closeConflictResolver() }, [autoSync, dialogs]) // Ref bridges handleContentChange (created after notes) into useNoteActions. // Read at callback time, so it's always current when user presses Cmd+N. const contentChangeRef = useRef<(path: string, content: string) => void>(() => {}) const notes = useNoteActions({ addEntry: vault.addEntry, removeEntry: vault.removeEntry, entries: vault.entries, setToastMessage, updateEntry: vault.updateEntry, vaultPath: resolvedPath, addPendingSave: vault.addPendingSave, removePendingSave: vault.removePendingSave, trackUnsaved: vault.trackUnsaved, clearUnsaved: vault.clearUnsaved, unsavedPaths: vault.unsavedPaths, markContentPending: (path, content) => contentChangeRef.current(path, content), onNewNotePersisted: vault.loadModifiedFiles, replaceEntry: vault.replaceEntry }) // Keep tab entries in sync with vault entries so banners (trash/archive) // and read-only state react immediately without reopening the note. useEffect(() => { notes.setTabs(prev => { let changed = false const next = prev.map(tab => { const fresh = vault.entries.find(e => e.path === tab.entry.path) if (fresh && fresh !== tab.entry) { changed = true return { ...tab, entry: fresh } } return tab }) return changed ? next : prev }) }, [vault.entries]) // eslint-disable-line react-hooks/exhaustive-deps -- notes.setTabs is stable (useState setter) const navHistory = useNavigationHistory() // Push to navigation history whenever the active tab changes (user-initiated) const navFromHistoryRef = useRef(false) useEffect(() => { if (notes.activeTabPath && !navFromHistoryRef.current) { navHistory.push(notes.activeTabPath) } navFromHistoryRef.current = false }, [notes.activeTabPath]) // eslint-disable-line react-hooks/exhaustive-deps -- navHistory.push is stable const isEntryExists = useCallback((path: string) => vault.entries.some(e => e.path === path), [vault.entries]) const handleGoBack = useCallback(() => { const target = navHistory.goBack(isEntryExists) if (target) { navFromHistoryRef.current = true if (notes.tabs.some(t => t.entry.path === target)) { notes.handleSwitchTab(target) } else { const entry = vault.entries.find(e => e.path === target) if (entry) notes.handleSelectNote(entry) } } }, [navHistory, isEntryExists, vault.entries, notes]) const handleGoForward = useCallback(() => { const target = navHistory.goForward(isEntryExists) if (target) { navFromHistoryRef.current = true if (notes.tabs.some(t => t.entry.path === target)) { notes.handleSwitchTab(target) } else { const entry = vault.entries.find(e => e.path === target) if (entry) notes.handleSelectNote(entry) } } }, [navHistory, isEntryExists, vault.entries, notes]) useNavigationGestures({ onGoBack: handleGoBack, onGoForward: handleGoForward }) // O(1) path lookup map — rebuilt only when vault.entries changes const entriesByPath = useMemo(() => { const map = new Map() for (const e of vault.entries) map.set(e.path, e) return map }, [vault.entries]) // MCP UI bridge: react to AI-driven open/highlight/vault-change events const openNoteByPath = useCallback((path: string) => { const entry = entriesByPath.get(path) ?? entriesByPath.get(`${resolvedPath}/${path}`) if (entry) { notes.handleSelectNote(entry) } else { // Entry not yet in vault (just created) — reload then open vault.reloadVault().then(freshEntries => { const fresh = (freshEntries as VaultEntry[]).find(e => e.path === path || e.path === `${resolvedPath}/${path}`) if (fresh) notes.handleSelectNote(fresh) }) } }, [entriesByPath, vault, notes, resolvedPath]) const aiActivity = useAiActivity({ onOpenNote: openNoteByPath, onOpenTab: openNoteByPath, onSetFilter: (filterType) => { setSelection({ kind: 'sectionGroup', type: filterType }) }, onVaultChanged: () => { vault.reloadVault() }, }) // Stable callback for Pulse "open note" — never triggers reloadVault. // Pulse files always exist in the vault; if somehow not found, silently skip. const handlePulseOpenNote = useCallback((relativePath: string) => { const fullPath = `${resolvedPath}/${relativePath}` const entry = entriesByPath.get(fullPath) ?? entriesByPath.get(relativePath) if (entry) notes.handleSelectNote(entry) }, [entriesByPath, resolvedPath, notes]) // Agent file operation handlers: auto-open created notes, live-refresh modified notes const handleAgentFileCreated = useCallback((relativePath: string) => { vault.reloadVault().then(freshEntries => { const entry = (freshEntries as VaultEntry[]).find(e => e.path === relativePath || e.path === `${resolvedPath}/${relativePath}`) if (entry) notes.handleSelectNote(entry) }) }, [vault, notes, resolvedPath]) const handleAgentFileModified = useCallback((relativePath: string) => { const fullPath = `${resolvedPath}/${relativePath}` const matchPath = notes.tabs.some(t => t.entry.path === relativePath) ? relativePath : fullPath if (notes.tabs.some(t => t.entry.path === matchPath)) { vault.reloadVault() } }, [vault, notes, resolvedPath]) const handleAgentVaultChanged = useCallback(() => { vault.reloadVault() }, [vault]) const { triggerIncrementalIndex } = indexing const onAfterSave = useCallback(() => { vault.loadModifiedFiles() triggerIncrementalIndex() }, [vault, triggerIncrementalIndex]) const { notifyThemeSaved } = themeManager const onNotePersisted = useCallback((path: string, content: string) => { vault.clearUnsaved(path) notifyThemeSaved(path, content) }, [vault, notifyThemeSaved]) const { handleSave: handleSaveRaw, handleContentChange, savePendingForPath, savePending } = useEditorSaveWithLinks({ updateEntry: vault.updateEntry, setTabs: notes.setTabs, setToastMessage, onAfterSave, onNotePersisted, }) useEffect(() => { contentChangeRef.current = handleContentChange }, [handleContentChange]) // Refs for stable closure in flushBeforeAction (avoids re-creating on every tab/content change) const tabsRef = useRef(notes.tabs) tabsRef.current = notes.tabs // eslint-disable-line react-hooks/refs -- ref sync pattern const unsavedPathsRef = useRef(vault.unsavedPaths) unsavedPathsRef.current = vault.unsavedPaths // eslint-disable-line react-hooks/refs -- ref sync pattern /** Auto-save unsaved editor content before a destructive action (trash/archive). */ const { clearUnsaved: vaultClearUnsaved } = vault const flushBeforeAction = useCallback(async (path: string) => { try { await flushEditorContent(path, { savePendingForPath, getTabContent: (p) => tabsRef.current.find(t => t.entry.path === p)?.content, isUnsaved: (p) => unsavedPathsRef.current.has(p), onSaved: (p) => { vaultClearUnsaved(p) }, }) } catch (err) { setToastMessage(`Auto-save failed: ${err}`) throw err } }, [savePendingForPath, vaultClearUnsaved, setToastMessage]) // Wire conflict file opener now that notes is available useEffect(() => { openConflictFileRef.current = (relativePath: string) => { const fullPath = `${resolvedPath}/${relativePath}` const entry = vault.entries.find(e => e.path === fullPath) if (entry) { // Markdown note — open inside Laputa editor notes.handleSelectNote(entry) dialogs.closeConflictResolver() } else { // Non-note file (e.g. settings.json) — // open with system default app so the user can inspect/edit it openLocalFile(fullPath) } } }, [resolvedPath, vault.entries, notes, dialogs]) const handleRenameTab = useCallback(async (path: string, newTitle: string) => { await savePendingForPath(path) await notes.handleRenameNote(path, newTitle, resolvedPath, vault.replaceEntry).then(vault.loadModifiedFiles) }, [notes, resolvedPath, vault, savePendingForPath]) // Wrap handleSave to also persist unsaved notes that have no pending edits (user pressed Cmd+S without typing) // and trigger file rename when the title slug doesn't match the filename. const handleSave = useCallback(async () => { const activeTab = notes.tabs.find(t => t.entry.path === notes.activeTabPath) const fallback = activeTab && vault.unsavedPaths.has(activeTab.entry.path) ? { path: activeTab.entry.path, content: activeTab.content } : undefined await handleSaveRaw(fallback) // After saving, check if filename needs to match the current title if (activeTab && needsRenameOnSave(activeTab.entry.title, activeTab.entry.filename)) { await handleRenameTab(activeTab.entry.path, activeTab.entry.title) } }, [handleSaveRaw, handleRenameTab, notes.tabs, notes.activeTabPath, vault.unsavedPaths]) const commitFlow = useCommitFlow({ savePending, loadModifiedFiles: vault.loadModifiedFiles, commitAndPush: vault.commitAndPush, setToastMessage }) const entryActions = useEntryActions({ entries: vault.entries, updateEntry: vault.updateEntry, handleUpdateFrontmatter: notes.handleUpdateFrontmatter, handleDeleteProperty: notes.handleDeleteProperty, setToastMessage, createTypeEntry: notes.createTypeEntrySilent, onFrontmatterPersisted: vault.loadModifiedFiles, onBeforeAction: flushBeforeAction, }) const handleDeleteNote = useCallback(async (path: string) => { try { if (isTauri()) await invoke('delete_note', { path }) else await mockInvoke('delete_note', { path }) notes.handleCloseTab(path) vault.removeEntry(path) setToastMessage('Note permanently deleted') } catch (e) { setToastMessage(`Failed to delete note: ${e}`) } }, [notes, vault, setToastMessage]) const gitHistory = useGitHistory(notes.activeTabPath, vault.loadGitHistory) const handleCreateType = useCallback((name: string) => { notes.handleCreateType(name) setToastMessage(`Type "${name}" created`) }, [notes]) /** H1→title sync: save pending content then rename file + update wikilinks. */ const handleTitleSync = useCallback(async (path: string, newTitle: string) => { await savePendingForPath(path) await notes.handleRenameNote(path, newTitle, resolvedPath, vault.replaceEntry).then(vault.loadModifiedFiles) }, [notes, resolvedPath, vault, savePendingForPath]) const bulkActions = useBulkActions(entryActions, setToastMessage) // Raw-toggle ref: Editor registers its handleToggleRaw here so the command palette can call it const rawToggleRef = useRef<() => void>(() => {}) // Diff-toggle ref: Editor registers its handleToggleDiff here so the command palette can call it const diffToggleRef = useRef<() => void>(() => {}) const { setViewMode, sidebarVisible, noteListVisible } = useViewMode() const zoom = useZoom() const buildNumber = useBuildNumber() const { status: updateStatus, actions: updateActions } = useUpdater() const handleCheckForUpdates = useCallback(async () => { if (updateStatus.state === 'downloading') { setToastMessage('Update is downloading…') return } if (updateStatus.state === 'ready') { await restartApp() return } const result = await updateActions.checkForUpdates() if (result === 'up-to-date') { setToastMessage("You're on the latest version") } else if (result === 'error') { setToastMessage('Could not check for updates') } // 'available' → UpdateBanner handles it automatically }, [updateActions, updateStatus.state, setToastMessage]) const handleRestoreDefaultThemes = useCallback(async () => { if (!resolvedPath) return try { const tauriInvoke = isTauri() ? invoke : mockInvoke const msg = await tauriInvoke('restore_default_themes', { vaultPath: resolvedPath }) await vault.reloadVault() await themeManager.reloadThemes() setToastMessage(msg) } catch (err) { setToastMessage(`Failed to restore themes: ${err}`) } }, [resolvedPath, vault, themeManager, setToastMessage]) const handleRepairVault = useCallback(async () => { if (!resolvedPath) return try { const tauriInvoke = isTauri() ? invoke : mockInvoke const msg = await tauriInvoke('repair_vault', { vaultPath: resolvedPath }) await vault.reloadVault() await themeManager.reloadThemes() setToastMessage(msg) } catch (err) { setToastMessage(`Failed to repair vault: ${err}`) } }, [resolvedPath, vault, themeManager, setToastMessage]) const commands = useAppCommands({ activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef, handleCloseTabRef: notes.handleCloseTabRef, tabs: notes.tabs, entries: vault.entries, modifiedCount: vault.modifiedFiles.length, activeNoteModified: vault.modifiedFiles.some(f => f.path === notes.activeTabPath), selection, onQuickOpen: dialogs.openQuickOpen, onCommandPalette: dialogs.openCommandPalette, onSearch: dialogs.openSearch, onCreateNote: notes.handleCreateNoteImmediate, onOpenDailyNote: notes.handleOpenDailyNote, onCreateNoteOfType: notes.handleCreateNoteImmediate, onSave: handleSave, onOpenSettings: dialogs.openSettings, onTrashNote: entryActions.handleTrashNote, onRestoreNote: entryActions.handleRestoreNote, onArchiveNote: entryActions.handleArchiveNote, onUnarchiveNote: entryActions.handleUnarchiveNote, onCommitPush: commitFlow.openCommitDialog, onResolveConflicts: handleOpenConflictResolver, onSetViewMode: setViewMode, onToggleInspector: () => layout.setInspectorCollapsed(c => !c), onToggleDiff: () => diffToggleRef.current(), onToggleRawEditor: () => rawToggleRef.current(), onZoomIn: zoom.zoomIn, onZoomOut: zoom.zoomOut, onZoomReset: zoom.zoomReset, zoomLevel: zoom.zoomLevel, onSelect: setSelection, onCloseTab: notes.handleCloseTab, onSwitchTab: notes.handleSwitchTab, onReplaceActiveTab: notes.handleReplaceActiveTab, onSelectNote: notes.handleSelectNote, onGoBack: handleGoBack, onGoForward: handleGoForward, canGoBack: navHistory.canGoBack, canGoForward: navHistory.canGoForward, themes: themeManager.themes, activeThemeId: themeManager.activeThemeId, onSwitchTheme: themeManager.switchTheme, onCreateTheme: async () => { const path = await themeManager.createTheme() const freshEntries = await vault.reloadVault() setSelection({ kind: 'sectionGroup', type: 'Theme' }) if (path) { const entry = freshEntries.find(e => e.path === path) if (entry) notes.handleSelectNote(entry) } }, onOpenTheme: (themeId: string) => { const entry = vault.entries.find(e => e.path === themeId) if (entry) notes.handleSelectNote(entry) }, onOpenVault: vaultSwitcher.handleOpenLocalFolder, onCreateType: dialogs.openCreateType, onToggleAIChat: dialogs.toggleAIChat, onCheckForUpdates: handleCheckForUpdates, onRemoveActiveVault: () => vaultSwitcher.removeVault(vaultSwitcher.vaultPath), onRestoreGettingStarted: vaultSwitcher.restoreGettingStarted, onRestoreDefaultThemes: handleRestoreDefaultThemes, isGettingStartedHidden: vaultSwitcher.isGettingStartedHidden, vaultCount: vaultSwitcher.allVaults.length, mcpStatus, onInstallMcp: installMcp, onReindexVault: indexing.triggerFullReindex, onReloadVault: vault.reloadVault, onRepairVault: handleRepairVault, }) const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null const aiNoteList = useMemo(() => { return filterEntries(vault.entries, selection).map(e => ({ path: e.path, title: e.title, type: e.isA ?? 'Note', })) }, [vault.entries, selection]) const aiNoteListFilter = useMemo(() => { if (selection.kind === 'sectionGroup') return { type: selection.type, query: '' } if (selection.kind === 'topic') return { type: null, query: selection.entry.title } if (selection.kind === 'entity') return { type: null, query: selection.entry.title } return { type: null, query: '' } }, [selection]) // Show welcome/onboarding screen when vault doesn't exist if (onboarding.state.status === 'welcome' || onboarding.state.status === 'vault-missing') { const defaultPath = onboarding.state.defaultPath return (
) } // Show loading spinner while checking vault if (onboarding.state.status === 'loading') { return (
Loading…
) } return (
{sidebarVisible && ( <>
)} {noteListVisible && ( <>
{selection.kind === 'filter' && selection.filter === 'pulse' ? ( setViewMode('all')} /> ) : ( )}
)}
layout.setInspectorCollapsed((c) => !c)} inspectorWidth={layout.inspectorWidth} onInspectorResize={layout.handleInspectorResize} inspectorEntry={activeTab?.entry ?? null} inspectorContent={activeTab?.content ?? null} gitHistory={gitHistory} onUpdateFrontmatter={notes.handleUpdateFrontmatter} onDeleteProperty={notes.handleDeleteProperty} onAddProperty={notes.handleAddProperty} onCreateAndOpenNote={notes.handleCreateNoteForRelationship} showAIChat={dialogs.showAIChat} onToggleAIChat={dialogs.toggleAIChat} vaultPath={resolvedPath} noteList={aiNoteList} noteListFilter={aiNoteListFilter} onTrashNote={entryActions.handleTrashNote} onRestoreNote={entryActions.handleRestoreNote} onDeleteNote={handleDeleteNote} onArchiveNote={entryActions.handleArchiveNote} onUnarchiveNote={entryActions.handleUnarchiveNote} onRenameTab={handleRenameTab} onContentChange={handleContentChange} onSave={handleSave} onTitleSync={handleTitleSync} rawToggleRef={rawToggleRef} diffToggleRef={diffToggleRef} canGoBack={navHistory.canGoBack} canGoForward={navHistory.canGoForward} onGoBack={handleGoBack} onGoForward={handleGoForward} leftPanelsCollapsed={!sidebarVisible && !noteListVisible} isDarkTheme={themeManager.isDark} onFileCreated={handleAgentFileCreated} onFileModified={handleAgentFileModified} onVaultChanged={handleAgentVaultChanged} />
setSelection({ kind: 'filter', filter: 'changes' })} hasGitHub={!!settings.github_token} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} lastCommitInfo={autoSync.lastCommitInfo} onTriggerSync={autoSync.triggerSync} onOpenConflictResolver={handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} indexingProgress={indexing.progress} lastIndexedTime={indexing.lastIndexedTime} onRetryIndexing={indexing.retryIndexing} onReindexVault={indexing.triggerFullReindex} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={installMcp} /> setToastMessage(null)} /> { dialogs.closeGitHubVault(); dialogs.openSettings() }} onGitHubConnected={(token, username) => saveSettings({ ...settings, github_token: token, github_username: username })} />
) } export default App