refactor: extract app shell side effects

This commit is contained in:
lucaronin
2026-05-30 03:23:29 +02:00
parent 1a08102188
commit 944ab9705a
5 changed files with 167 additions and 108 deletions

View File

@@ -371,6 +371,8 @@ In a mounted-workspace graph, each loaded `ViewFile` carries optional renderer-o
`useAppViewActions()` owns the renderer-side saved View lifecycle: choosing the target workspace, preserving mounted-view identity, saving/deleting YAML definitions, reloading affected vault state, and exposing the available note-list fields for the create/edit dialog. `App.tsx` wires those callbacks into `Sidebar`, `NoteList`, `CreateViewDialog`, and command surfaces without duplicating the persistence rules.
`useMcpSetupDialogController()` owns MCP setup dialog state, busy actions, and manual config callbacks so `App.tsx` only passes the controller into settings/status surfaces. `useAiWorkspaceWindowBridgeEvents()` owns native AI-workspace event subscriptions and listener cleanup for popped-out workspace windows.
The renderer uses `viewOrdering` helpers to convert drag or command-palette move intent into dense order updates before saving each affected view file through `save_view_cmd`. The sidebar treats saved View rows like Type rows for direct customization: double-click starts inline rename, right-click opens edit/rename/icon-color/delete actions, and keyboard users can open that same menu from the focused row while command-palette actions remain responsible for saved View ordering.
### Neighborhood Mode

View File

@@ -869,6 +869,8 @@ No Redux or global context. State lives in the root `App.tsx` and custom hooks:
| `useAppWindowControls` | view mode, panel visibility, command refs, zoom/build labels | Keeps main-window sizing and editor command ref plumbing out of `App.tsx` |
| `useAppViewActions` | saved-view/type creation and saved-view mutation callbacks | Keeps saved-view persistence and Type auto-creation orchestration out of `App.tsx` |
| `useAiWorkspacePublishedContext` | AI workspace note-list snapshot and BroadcastChannel context publishing | Keeps AI workspace context derivation close to its cross-window publication side effect |
| `useMcpSetupDialogController` | MCP setup dialog state/actions | Keeps MCP status, manual config, and connect/disconnect dialog flow out of `App.tsx` |
| `useAiWorkspaceWindowBridgeEvents` | native AI workspace event subscriptions | Owns Tauri listener setup/cleanup for popped-out workspace window bridge events |
| `useGitFileWorkflows` | git diff/history/discard callbacks | Resolves note-scoped repository paths and owns deleted-file preview and queued diff side effects |
| `useVaultRenameDetection` | detected rename banner state | Detects external Git renames on focus and owns the wikilink update callback |
| `useNoteCreation` | — | Note/type creation with optimistic persistence |

View File

@@ -21,7 +21,6 @@ import { FeedbackDialog } from './components/FeedbackDialog'
import { McpSetupDialog } from './components/McpSetupDialog'
import { NoteRetargetingDialogs } from './components/note-retargeting/NoteRetargetingDialogs'
import { StartupScreen } from './components/StartupScreen'
import { useMcpStatus } from './hooks/useMcpStatus'
import { useAiAgentsOnboarding } from './hooks/useAiAgentsOnboarding'
import { useAiAgentsStatus } from './hooks/useAiAgentsStatus'
import { useVaultAiGuidanceStatus } from './hooks/useVaultAiGuidanceStatus'
@@ -123,6 +122,8 @@ import { useStartupScreenState } from './hooks/useStartupScreenState'
import { useGitFileWorkflows } from './hooks/useGitFileWorkflows'
import { useAutoGitWork } from './hooks/useAutoGitWork'
import { useAppAiWorkspaceBridge } from './hooks/useAppAiWorkspaceBridge'
import { useAiWorkspaceWindowBridgeEvents } from './hooks/useAiWorkspaceWindowBridgeEvents'
import { useMcpSetupDialogController } from './hooks/useMcpSetupDialogController'
import {
activeVaultModifiedFiles,
aiWorkspaceWindowContextForPath,
@@ -132,13 +133,6 @@ import {
runNativeTextHistoryCommand,
shouldPreferOnboardingVaultPath,
} from './utils/appOrchestration'
import { cleanupTauriEventListeners, type TauriUnlisten } from './utils/tauriEventCleanup'
import {
AI_WORKSPACE_FILE_CREATED_EVENT,
AI_WORKSPACE_FILE_MODIFIED_EVENT,
AI_WORKSPACE_OPEN_NOTE_REQUESTED_EVENT,
AI_WORKSPACE_VAULT_CHANGED_EVENT,
} from './utils/aiPromptBridge'
import './App.css'
// Type declarations for mock content storage and test overrides
@@ -190,8 +184,6 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
const dialogs = useDialogs()
const { closeAIChat, openAIChat, showAIChat } = dialogs
const [showFeedback, setShowFeedback] = useState(false)
const [showMcpSetupDialog, setShowMcpSetupDialog] = useState(false)
const [mcpDialogAction, setMcpDialogAction] = useState<'connect' | 'disconnect' | null>(null)
const openFeedback = useCallback(() => setShowFeedback(true), [])
const closeFeedback = useCallback(() => setShowFeedback(false), [])
const openDocs = useCallback(() => {
@@ -419,16 +411,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
gitRepoState,
resolvedPath,
})
const {
mcpStatus,
connectMcp,
disconnectMcp,
mcpConfigSnippet,
mcpConfigLoading,
mcpConfigError,
loadMcpConfigSnippet,
copyMcpConfig,
} = useMcpStatus(resolvedPath, setToastMessage, appLocale)
const mcpSetupDialog = useMcpSetupDialogController(resolvedPath, setToastMessage, appLocale)
const loadDefaultVaultModifiedFiles = vault.loadModifiedFiles
const loadAllGitModifiedFiles = gitSurfaces.loadAllModifiedFiles
const loadModifiedFilesForRepository = gitSurfaces.loadModifiedFilesForRepository
@@ -455,39 +438,6 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
void refreshAllGitRemoteStatuses()
}, [gitFeaturesEnabled, gitRepoState, loadVaultModifiedFiles, refreshAllGitRemoteStatuses, refreshGitRemoteStatus])
const openMcpSetupDialog = useCallback(() => {
setShowMcpSetupDialog(true)
}, [])
const closeMcpSetupDialog = useCallback(() => {
if (mcpDialogAction !== null) return
setShowMcpSetupDialog(false)
}, [mcpDialogAction])
const handleConnectMcp = useCallback(async () => {
setMcpDialogAction('connect')
try {
const didConnect = await connectMcp()
if (didConnect) setShowMcpSetupDialog(false)
} finally {
setMcpDialogAction(null)
}
}, [connectMcp])
const handleDisconnectMcp = useCallback(async () => {
setMcpDialogAction('disconnect')
try {
const didDisconnect = await disconnectMcp()
if (didDisconnect) setShowMcpSetupDialog(false)
} finally {
setMcpDialogAction(null)
}
}, [disconnectMcp])
const handleCopyMcpConfig = useCallback(() => {
void copyMcpConfig()
}, [copyMcpConfig])
const handleOpenSettings = useCallback(() => {
setSettingsInitialSectionId(null)
dialogs.openSettings()
@@ -498,10 +448,6 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
dialogs.openSettings()
}, [dialogs])
const handleLoadMcpConfigSnippet = useCallback(() => {
void loadMcpConfigSnippet().catch(() => undefined)
}, [loadMcpConfigSnippet])
const {
detectedRenames,
handleUpdateWikilinks,
@@ -575,6 +521,8 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
closeAllTabs,
openTabWithContent,
} = notes
const noteActiveTabPath = notes.activeTabPath
const noteActiveTabPathRef = notes.activeTabPathRef
useNoteWindowLifecycle({
activeTabPath: notes.activeTabPath,
handleSelectNote,
@@ -589,9 +537,9 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
) => {
const updateVaultPath = options.vaultPath ?? resolvedPath
await refreshPulledVaultState({
activeTabPath: notes.activeTabPath,
activeTabPath: noteActiveTabPath,
closeAllTabs,
getActiveTabPath: () => notes.activeTabPathRef.current,
getActiveTabPath: () => noteActiveTabPathRef.current,
hasUnsavedChanges: (path) => vault.unsavedPaths.has(path),
shouldKeepActiveEditorMounted: options.preserveFocusedEditor
? isActiveElementInsideEditorSurface
@@ -607,8 +555,8 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
}, [
closeAllTabs,
handleReplaceActiveTab,
notes.activeTabPath,
notes.activeTabPathRef,
noteActiveTabPath,
noteActiveTabPathRef,
refreshGitModifiedFiles,
resolvedPath,
vault.reloadFolders,
@@ -705,46 +653,12 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
handleAgentFileModified: handleAiWorkspaceWindowFileModified,
handleAgentVaultChanged: handleAiWorkspaceWindowVaultChanged,
} = vaultBridge
useEffect(() => {
if (!isTauri()) return
let disposed = false
let unlisteners: TauriUnlisten[] = []
void import('@tauri-apps/api/event')
.then(({ listen }) => Promise.all([
listen<string>(AI_WORKSPACE_OPEN_NOTE_REQUESTED_EVENT, (event) => {
if (typeof event.payload === 'string') handleAiWorkspaceWindowOpenNote(event.payload)
}),
listen<string>(AI_WORKSPACE_FILE_CREATED_EVENT, (event) => {
if (typeof event.payload === 'string') handleAiWorkspaceWindowFileCreated(event.payload)
}),
listen<string>(AI_WORKSPACE_FILE_MODIFIED_EVENT, (event) => {
if (typeof event.payload === 'string') handleAiWorkspaceWindowFileModified(event.payload)
}),
listen(AI_WORKSPACE_VAULT_CHANGED_EVENT, () => {
handleAiWorkspaceWindowVaultChanged()
}),
]))
.then((nextUnlisteners) => {
if (disposed) {
cleanupTauriEventListeners(nextUnlisteners)
return
}
unlisteners = nextUnlisteners
})
.catch(() => undefined)
return () => {
disposed = true
cleanupTauriEventListeners(unlisteners)
}
}, [
handleAiWorkspaceWindowFileCreated,
handleAiWorkspaceWindowFileModified,
handleAiWorkspaceWindowOpenNote,
handleAiWorkspaceWindowVaultChanged,
])
useAiWorkspaceWindowBridgeEvents({
onFileCreated: handleAiWorkspaceWindowFileCreated,
onFileModified: handleAiWorkspaceWindowFileModified,
onOpenNote: handleAiWorkspaceWindowOpenNote,
onVaultChanged: handleAiWorkspaceWindowVaultChanged,
})
const conflictFlow = useConflictFlow({
resolvedPath: autoSync.conflictVaultPath ?? graphDefaultWorkspacePath,
@@ -1412,7 +1326,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
runtimeMissingVaultPath,
selectedVaultPath,
settingsLoaded,
showMcpSetupDialog,
showMcpSetupDialog: mcpSetupDialog.open,
telemetryConsent: settings.telemetry_consent,
vaultIsLoading: vault.isLoading,
vaultSwitcher,
@@ -1517,8 +1431,8 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
selectedUiLanguage,
onSetUiLanguage: handleSetUiLanguage,
onSetThemeMode: handleSetThemeMode,
mcpStatus,
onInstallMcp: openMcpSetupDialog,
mcpStatus: mcpSetupDialog.status,
onInstallMcp: mcpSetupDialog.openDialog,
onReloadVault: handleManualVaultReload,
onRepairVault: handleRepairVault,
onSetNoteIcon: handleSetNoteIconCommand,
@@ -1610,7 +1524,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
settings={settings}
settingsLoaded={settingsLoaded}
shouldResumeFreshStartOnboarding={shouldResumeFreshStartOnboarding}
showMcpSetupDialog={showMcpSetupDialog}
showMcpSetupDialog={mcpSetupDialog.open}
setToastMessage={setToastMessage}
toastMessage={toastMessage}
vaultSwitcher={vaultSwitcher}
@@ -1733,7 +1647,7 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
</div>
<UpdateBanner status={updateStatus} actions={updateActions} locale={appLocale} />
<RenameDetectedBanner renames={detectedRenames} onUpdate={handleUpdateWikilinks} onDismiss={handleDismissRenames} />
<StatusBar noteCount={visibleEntries.length} modifiedCount={gitModifiedCount} vaultPath={resolvedPath} defaultWorkspacePath={defaultWorkspacePath} vaults={vaultSwitcher.allVaults} multiWorkspaceEnabled={multiWorkspaceEnabled} onSwitchVault={vaultSwitcher.switchVault} onSetDefaultWorkspace={vaultSwitcher.setDefaultWorkspace} onOpenSettings={handleOpenSettings} onOpenVaultSettings={handleOpenVaultSettings} onOpenFeedback={openFeedback} onOpenDocs={openDocs} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onCreateEmptyVault={vaultSwitcher.handleCreateEmptyVault} onCloneVault={dialogs.openCloneVault} onCloneGettingStarted={cloneGettingStartedVault} onClickPending={() => handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} gitFeaturesEnabled={gitFeaturesEnabled} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.syncRepositoryPath} onRepositoryChange={gitSurfaces.setSyncRepositoryPath} onTriggerSync={handlePullSelectedRepository} onPullAndPush={handlePullAndPushSelectedRepository} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} aiFeaturesEnabled={aiFeaturesEnabled} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} locale={appLocale} />
<StatusBar noteCount={visibleEntries.length} modifiedCount={gitModifiedCount} vaultPath={resolvedPath} defaultWorkspacePath={defaultWorkspacePath} vaults={vaultSwitcher.allVaults} multiWorkspaceEnabled={multiWorkspaceEnabled} onSwitchVault={vaultSwitcher.switchVault} onSetDefaultWorkspace={vaultSwitcher.setDefaultWorkspace} onOpenSettings={handleOpenSettings} onOpenVaultSettings={handleOpenVaultSettings} onOpenFeedback={openFeedback} onOpenDocs={openDocs} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onCreateEmptyVault={vaultSwitcher.handleCreateEmptyVault} onCloneVault={dialogs.openCloneVault} onCloneGettingStarted={cloneGettingStartedVault} onClickPending={() => handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} gitFeaturesEnabled={gitFeaturesEnabled} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} repositories={gitRepositories} selectedRepositoryPath={gitSurfaces.syncRepositoryPath} onRepositoryChange={gitSurfaces.setSyncRepositoryPath} onTriggerSync={handlePullSelectedRepository} onPullAndPush={handlePullAndPushSelectedRepository} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} aiFeaturesEnabled={aiFeaturesEnabled} mcpStatus={mcpSetupDialog.status} onInstallMcp={mcpSetupDialog.openDialog} locale={appLocale} />
{aiFeaturesEnabled && !effectiveShowAIChat ? (
<AiWorkspaceFloatingButton
statuses={aiAgentsStatus}
@@ -1794,9 +1708,9 @@ function MainApp({ noteWindowParams }: { noteWindowParams: NoteWindowParams | nu
onCommit={conflictResolver.commitResolution}
onClose={conflictFlow.handleCloseConflictResolver}
/>
<SettingsPanel open={dialogs.showSettings} initialSectionId={settingsInitialSectionId} settings={settings} aiAgentsStatus={aiAgentsStatus} locale={appLocale} systemLocale={systemLocale} vaults={vaultSwitcher.allVaults} defaultWorkspacePath={vaultSwitcher.defaultWorkspacePath} onSetDefaultWorkspace={vaultSwitcher.setDefaultWorkspace} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} isGitVault={gitRepoState !== 'missing'} onSave={saveSettings} onCopyMcpConfig={handleCopyMcpConfig} explicitOrganizationEnabled={explicitOrganizationEnabled} onSaveExplicitOrganization={handleSaveExplicitOrganization} onClose={dialogs.closeSettings} />
<SettingsPanel open={dialogs.showSettings} initialSectionId={settingsInitialSectionId} settings={settings} aiAgentsStatus={aiAgentsStatus} locale={appLocale} systemLocale={systemLocale} vaults={vaultSwitcher.allVaults} defaultWorkspacePath={vaultSwitcher.defaultWorkspacePath} onSetDefaultWorkspace={vaultSwitcher.setDefaultWorkspace} onRemoveVault={vaultSwitcher.removeVault} onReorderVaults={vaultSwitcher.reorderVaults} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} isGitVault={gitRepoState !== 'missing'} onSave={saveSettings} onCopyMcpConfig={mcpSetupDialog.copyManualConfig} explicitOrganizationEnabled={explicitOrganizationEnabled} onSaveExplicitOrganization={handleSaveExplicitOrganization} onClose={dialogs.closeSettings} />
<FeedbackDialog open={showFeedback} onClose={closeFeedback} />
<McpSetupDialog open={showMcpSetupDialog} status={mcpStatus} busyAction={mcpDialogAction} manualConfigSnippet={mcpConfigSnippet} manualConfigLoading={mcpConfigLoading} manualConfigError={mcpConfigError} locale={appLocale} onClose={closeMcpSetupDialog} onConnect={handleConnectMcp} onCopyManualConfig={handleCopyMcpConfig} onDisconnect={handleDisconnectMcp} onLoadManualConfig={handleLoadMcpConfigSnippet} />
<McpSetupDialog open={mcpSetupDialog.open} status={mcpSetupDialog.status} busyAction={mcpSetupDialog.busyAction} manualConfigSnippet={mcpSetupDialog.manualConfigSnippet} manualConfigLoading={mcpSetupDialog.manualConfigLoading} manualConfigError={mcpSetupDialog.manualConfigError} locale={appLocale} onClose={mcpSetupDialog.closeDialog} onConnect={mcpSetupDialog.connect} onCopyManualConfig={mcpSetupDialog.copyManualConfig} onDisconnect={mcpSetupDialog.disconnect} onLoadManualConfig={mcpSetupDialog.loadManualConfig} />
<CloneVaultModal key={dialogs.showCloneVault ? 'clone-open' : 'clone-closed'} open={dialogs.showCloneVault} onClose={dialogs.closeCloneVault} onVaultCloned={vaultSwitcher.handleVaultCloned} />
{deleteActions.confirmDelete && (
<ConfirmDeleteDialog

View File

@@ -0,0 +1,64 @@
import { useEffect } from 'react'
import { isTauri } from '../mock-tauri'
import { cleanupTauriEventListeners, type TauriUnlisten } from '../utils/tauriEventCleanup'
import {
AI_WORKSPACE_FILE_CREATED_EVENT,
AI_WORKSPACE_FILE_MODIFIED_EVENT,
AI_WORKSPACE_OPEN_NOTE_REQUESTED_EVENT,
AI_WORKSPACE_VAULT_CHANGED_EVENT,
} from '../utils/aiPromptBridge'
interface AiWorkspaceWindowBridgeEvents {
onFileCreated: (path: string) => void
onFileModified: (path: string) => void
onOpenNote: (path: string) => void
onVaultChanged: () => void
}
export function useAiWorkspaceWindowBridgeEvents({
onFileCreated,
onFileModified,
onOpenNote,
onVaultChanged,
}: AiWorkspaceWindowBridgeEvents) {
useEffect(() => {
if (!isTauri()) return
let disposed = false
let unlisteners: TauriUnlisten[] = []
void import('@tauri-apps/api/event')
.then(({ listen }) => Promise.all([
listen<string>(AI_WORKSPACE_OPEN_NOTE_REQUESTED_EVENT, (event) => {
if (typeof event.payload === 'string') onOpenNote(event.payload)
}),
listen<string>(AI_WORKSPACE_FILE_CREATED_EVENT, (event) => {
if (typeof event.payload === 'string') onFileCreated(event.payload)
}),
listen<string>(AI_WORKSPACE_FILE_MODIFIED_EVENT, (event) => {
if (typeof event.payload === 'string') onFileModified(event.payload)
}),
listen(AI_WORKSPACE_VAULT_CHANGED_EVENT, () => {
onVaultChanged()
}),
]))
.then((nextUnlisteners) => {
if (disposed) {
cleanupTauriEventListeners(nextUnlisteners)
return
}
unlisteners = nextUnlisteners
})
.catch(() => undefined)
return () => {
disposed = true
cleanupTauriEventListeners(unlisteners)
}
}, [
onFileCreated,
onFileModified,
onOpenNote,
onVaultChanged,
])
}

View File

@@ -0,0 +1,77 @@
import { useCallback, useState } from 'react'
import { useMcpStatus } from './useMcpStatus'
import type { AppLocale } from '../lib/i18n'
type ToastHandler = (message: string) => void
type McpDialogAction = 'connect' | 'disconnect' | null
export function useMcpSetupDialogController(
vaultPath: string,
onToast: ToastHandler,
locale: AppLocale,
) {
const [open, setOpen] = useState(false)
const [busyAction, setBusyAction] = useState<McpDialogAction>(null)
const {
mcpStatus,
connectMcp,
disconnectMcp,
mcpConfigSnippet,
mcpConfigLoading,
mcpConfigError,
loadMcpConfigSnippet,
copyMcpConfig,
} = useMcpStatus(vaultPath, onToast, locale)
const openDialog = useCallback(() => {
setOpen(true)
}, [])
const closeDialog = useCallback(() => {
if (busyAction !== null) return
setOpen(false)
}, [busyAction])
const connect = useCallback(async () => {
setBusyAction('connect')
try {
const didConnect = await connectMcp()
if (didConnect) setOpen(false)
} finally {
setBusyAction(null)
}
}, [connectMcp])
const disconnect = useCallback(async () => {
setBusyAction('disconnect')
try {
const didDisconnect = await disconnectMcp()
if (didDisconnect) setOpen(false)
} finally {
setBusyAction(null)
}
}, [disconnectMcp])
const copyManualConfig = useCallback(() => {
void copyMcpConfig()
}, [copyMcpConfig])
const loadManualConfig = useCallback(() => {
void loadMcpConfigSnippet().catch(() => undefined)
}, [loadMcpConfigSnippet])
return {
busyAction,
closeDialog,
connect,
copyManualConfig,
disconnect,
loadManualConfig,
manualConfigError: mcpConfigError,
manualConfigLoading: mcpConfigLoading,
manualConfigSnippet: mcpConfigSnippet,
open,
openDialog,
status: mcpStatus,
}
}