From 68374109f47c5fb70392e71ac4cd575996d4c9eb Mon Sep 17 00:00:00 2001 From: lucaronin Date: Tue, 12 May 2026 22:30:24 +0200 Subject: [PATCH] feat: add ai visibility setting --- docs/ABSTRACTIONS.md | 4 +- lara.lock | 2 + src-tauri/src/settings.rs | 5 ++ src/App.tsx | 39 +++++----- src/components/BreadcrumbBar.test.tsx | 16 ++++ src/components/BreadcrumbBar.tsx | 4 +- src/components/CommandPalette.test.tsx | 19 +++++ src/components/CommandPalette.tsx | 6 +- src/components/Editor.tsx | 2 +- src/components/SettingsPanel.test.tsx | 19 +++++ src/components/SettingsPanel.tsx | 82 +++++++++++++------- src/components/settingsPreferenceTracking.ts | 8 ++ src/hooks/commands/aiAgentCommands.ts | 4 + src/hooks/commands/viewCommands.ts | 19 ++++- src/hooks/useAppCommandAiActions.ts | 65 ++++++++++++++++ src/hooks/useAppCommands.ts | 26 ++++++- src/hooks/useCommandRegistry.test.ts | 21 +++++ src/hooks/useCommandRegistry.ts | 8 +- src/hooks/useSettings.test.ts | 3 + src/hooks/useSettings.ts | 2 + src/lib/aiFeatures.test.ts | 11 +++ src/lib/aiFeatures.ts | 5 ++ src/lib/locales/de-DE.json | 2 + src/lib/locales/en.json | 2 + src/lib/locales/es-419.json | 2 + src/lib/locales/es-ES.json | 2 + src/lib/locales/fr-FR.json | 2 + src/lib/locales/it-IT.json | 2 + src/lib/locales/ja-JP.json | 2 + src/lib/locales/ko-KR.json | 2 + src/lib/locales/pl-PL.json | 2 + src/lib/locales/pt-BR.json | 2 + src/lib/locales/pt-PT.json | 2 + src/lib/locales/ru-RU.json | 2 + src/lib/locales/vi.json | 2 + src/lib/locales/zh-CN.json | 2 + src/lib/locales/zh-TW.json | 2 + src/lib/productAnalytics.ts | 6 ++ src/types.ts | 1 + 39 files changed, 351 insertions(+), 56 deletions(-) create mode 100644 src/hooks/useAppCommandAiActions.ts create mode 100644 src/lib/aiFeatures.test.ts create mode 100644 src/lib/aiFeatures.ts diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 93fd7c1e..3ef6ae1a 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -845,6 +845,7 @@ interface Settings { date_display_format: 'us' | 'european' | 'friendly' | 'iso' | null note_width_mode: 'normal' | 'wide' | null sidebar_type_pluralization_enabled: boolean | null // null = default true + ai_features_enabled: boolean | null // null = default true default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | 'gemini' | null default_ai_target: string | null // "agent:codex" or "model:/" ai_model_providers: AiModelProvider[] | null @@ -855,7 +856,7 @@ interface Settings { } ``` -Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette Light/Dark/System actions both update that same value. `system` remains a stored preference, while the runtime resolves it to `light` or `dark` for `data-theme` and app consumers. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `date_display_format` is installation-local and controls rendered dates in note rows, property chips/cells, note info, table-of-contents metadata, and search result subtitles; `AppPreferencesProvider` owns the UI-level value so rendering surfaces can consume it without prop forwarding, while date picker text input remains ISO for predictable manual entry and storage. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `sidebar_type_pluralization_enabled` is installation-local and defaults to `true`; when false, type rows use exact type names unless the type document defines an explicit `sidebar_label` override. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings. Provider defaults and local/API grouping come from the shared `src/shared/aiModelProviderCatalog.json` catalog used by both renderer settings and the Tauri direct-model runtime. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. +Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette Light/Dark/System actions both update that same value. `system` remains a stored preference, while the runtime resolves it to `light` or `dark` for `data-theme` and app consumers. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `date_display_format` is installation-local and controls rendered dates in note rows, property chips/cells, note info, table-of-contents metadata, and search result subtitles; `AppPreferencesProvider` owns the UI-level value so rendering surfaces can consume it without prop forwarding, while date picker text input remains ISO for predictable manual entry and storage. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `sidebar_type_pluralization_enabled` is installation-local and defaults to `true`; when false, type rows use exact type names unless the type document defines an explicit `sidebar_label` override. `ai_features_enabled` is installation-local and defaults to `true`; when false, Tolaria hides AI panel controls, status bar AI indicators, command-palette AI mode, and missing-agent prompts while leaving Settings as the re-enable path. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings. Provider defaults and local/API grouping come from the shared `src/shared/aiModelProviderCatalog.json` catalog used by both renderer settings and the Tauri direct-model runtime. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. ## Telemetry @@ -876,6 +877,7 @@ Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is ins - **Inline image lightbox** — `inline_image_lightbox_opened` records that a rich-editor inline image was opened from double-click, without sending note paths, image URLs, alt text, or file names. - **Code block copy** — `code_block_copied` records that the rich-editor code-block copy action was used, without sending note paths, languages, or code content. - **AI agent sessions** — `ai_agent_message_sent`, `ai_agent_message_blocked`, `ai_agent_response_completed`, `ai_agent_response_failed`, and `ai_agent_permission_mode_changed` use only agent ids, permission modes, counts, and coarse status categories. +- **AI feature visibility** — `ai_features_visibility_changed` records only whether installation-level AI surfaces were enabled or hidden. - **All Notes visibility** — `all_notes_visibility_changed` records only the toggled category and enabled state. ### Tauri Commands diff --git a/lara.lock b/lara.lock index 66df3330..7f57592b 100644 --- a/lara.lock +++ b/lara.lock @@ -175,6 +175,8 @@ files: settings.allNotesVisibility.unsupportedDescription: a3b8aa66900c742f001cd4533b3df44a settings.aiAgents.title: 27f08387b26e1ceeb1b60bd9c97e7a47 settings.aiAgents.description: 042a8037f1a4f90c76ce31a49fdff59c + settings.aiFeatures.enable: af47a6b2bd32a7b7309bac7bef34f026 + settings.aiFeatures.enableDescription: 7d3916f7f9e1bf8b8797f50f4533b9da settings.aiAgents.default: 6af573559fd05d8c35bc57b41cc78e24 settings.aiAgents.defaultTarget: 5acfc74fd97a6dd2d67d15c66de5eed5 settings.aiAgents.agentGroup: 965530cbbec45b1754ed3ece120399f7 diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 90c34f01..b4f0aea0 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -82,6 +82,7 @@ pub struct Settings { pub note_width_mode: Option, pub sidebar_type_pluralization_enabled: Option, pub initial_h1_auto_rename_enabled: Option, + pub ai_features_enabled: Option, pub default_ai_agent: Option, pub default_ai_target: Option, pub ai_model_providers: Option>, @@ -195,6 +196,7 @@ fn normalize_settings(settings: Settings) -> Settings { note_width_mode: normalize_note_width_mode(settings.note_width_mode.as_deref()), sidebar_type_pluralization_enabled: settings.sidebar_type_pluralization_enabled, initial_h1_auto_rename_enabled: settings.initial_h1_auto_rename_enabled, + ai_features_enabled: settings.ai_features_enabled, default_ai_agent: normalize_default_ai_agent(settings.default_ai_agent.as_deref()), default_ai_target: normalize_optional_string(settings.default_ai_target), ai_model_providers: normalize_ai_model_providers(settings.ai_model_providers), @@ -347,6 +349,7 @@ mod tests { note_width_mode: Some("wide".to_string()), sidebar_type_pluralization_enabled: Some(false), initial_h1_auto_rename_enabled: Some(false), + ai_features_enabled: Some(false), default_ai_agent: Some("codex".to_string()), default_ai_target: Some("agent:codex".to_string()), ai_model_providers: None, @@ -384,6 +387,7 @@ mod tests { note_width_mode: Some("wide".to_string()), sidebar_type_pluralization_enabled: Some(false), initial_h1_auto_rename_enabled: Some(false), + ai_features_enabled: Some(false), default_ai_agent: Some("codex".to_string()), hide_gitignored_files: Some(false), multi_workspace_enabled: Some(true), @@ -404,6 +408,7 @@ mod tests { assert_eq!(loaded.note_width_mode.as_deref(), Some("wide")); assert_eq!(loaded.sidebar_type_pluralization_enabled, Some(false)); assert_eq!(loaded.initial_h1_auto_rename_enabled, Some(false)); + assert_eq!(loaded.ai_features_enabled, Some(false)); assert_eq!(loaded.default_ai_agent.as_deref(), Some("codex")); assert_eq!(loaded.hide_gitignored_files, Some(false)); assert_eq!(loaded.multi_workspace_enabled, Some(true)); diff --git a/src/App.tsx b/src/App.tsx index bafde1c9..69b8bcdb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -94,6 +94,8 @@ import { openNoteListPropertiesPicker } from './components/note-list/noteListPro import type { NoteListMultiSelectionCommands } from './components/note-list/multiSelectionCommands' import { focusNoteIconPropertyEditor } from './components/noteIconPropertyEvents' import { trackEvent } from './lib/telemetry' +import { areAiFeaturesEnabled } from './lib/aiFeatures' +import { useAppCommandAiActions } from './hooks/useAppCommandAiActions' import { TOLARIA_DOCS_URL } from './constants/feedback' import { openExternalUrl } from './utils/url' import { @@ -256,15 +258,23 @@ function App() { void openExternalUrl(TOLARIA_DOCS_URL) }, []) const networkStatus = useNetworkStatus() + const { settings, loaded: settingsLoaded, saveSettings } = useSettings() + const aiFeaturesEnabled = areAiFeaturesEnabled(settings) + const effectiveShowAIChat = aiFeaturesEnabled && showAIChat useEffect(() => { const handleOpenAiChat = () => { + if (!aiFeaturesEnabled) return if (!showAIChat) toggleAIChat() } window.addEventListener(OPEN_AI_CHAT_EVENT, handleOpenAiChat) return () => window.removeEventListener(OPEN_AI_CHAT_EVENT, handleOpenAiChat) - }, [showAIChat, toggleAIChat]) + }, [aiFeaturesEnabled, showAIChat, toggleAIChat]) + + useEffect(() => { + if (!aiFeaturesEnabled && showAIChat) toggleAIChat() + }, [aiFeaturesEnabled, showAIChat, toggleAIChat]) // onSwitch closure captures `notes` declared below — safe because it's only // called on user interaction, never during render (refs inside the hook @@ -318,7 +328,7 @@ function App() { registerVault: registerVaultSelection, }, vaultSwitcher.loaded) const aiAgentsStatus = useAiAgentsStatus() - const aiAgentsOnboarding = useAiAgentsOnboarding(onboarding.state.status === 'ready' && !noteWindowParams) + const aiAgentsOnboarding = useAiAgentsOnboarding(aiFeaturesEnabled && onboarding.state.status === 'ready' && !noteWindowParams) // Onboarding can briefly own the vault path for a newly created/opened vault // before the persisted switcher catches up, but once the path is already in @@ -328,7 +338,6 @@ function App() { ? onboarding.state.vaultPath : vaultSwitcher.vaultPath ) - const { settings, loaded: settingsLoaded, saveSettings } = useSettings() const [settingsInitialSectionId, setSettingsInitialSectionId] = useState(null) const { folderVaults, @@ -399,7 +408,7 @@ function App() { status: vaultAiGuidanceStatus, refresh: refreshVaultAiGuidance, } = useVaultAiGuidanceStatus( - resolvedPath, + aiFeaturesEnabled ? resolvedPath : null, buildVaultAiGuidanceRefreshKey(vault.entries), ) const { config: vaultConfig, updateConfig } = useVaultConfig(resolvedPath) @@ -1105,7 +1114,7 @@ function App() { deleteActions.handleDeleteNote(typeEntry.path) }, [deleteActions, visibleEntries]) - const shouldLoadGitHistory = !layout.inspectorCollapsed && !showAIChat + const shouldLoadGitHistory = !layout.inspectorCollapsed && !effectiveShowAIChat const gitHistory = useGitHistory(notes.activeTabPath, loadGitHistoryForPath, shouldLoadGitHistory) const handleCreateType = useCallback(async (name: string) => { @@ -1376,7 +1385,7 @@ function App() { dialogs.showCreateTypeDialog || dialogs.showQuickOpen || dialogs.showCommandPalette - || dialogs.showAIChat + || effectiveShowAIChat || dialogs.showSettings || dialogs.showCloneVault || dialogs.showSearch @@ -1493,6 +1502,7 @@ function App() { setToastMessage, }) const activeEditorVaultPath = activeTab ? vaultPathForEntry(activeTab.entry, resolvedPath) : resolvedPath + const commandAiActions = useAppCommandAiActions(aiFeaturesEnabled, dialogs, aiAgentsStatus, vaultAiGuidanceStatus, restoreVaultAiGuidanceCommand, aiAgentPreferences) const commands = useAppCommands({ activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef, @@ -1548,7 +1558,7 @@ function App() { onOpenVault: vaultSwitcher.handleOpenLocalFolder, onCreateEmptyVault: vaultSwitcher.handleCreateEmptyVault, onCreateType: dialogs.openCreateType, - onToggleAIChat: dialogs.toggleAIChat, + ...commandAiActions, onCheckForUpdates: handleCheckForUpdates, onRemoveActiveVault: removeActiveVaultCommand, onRestoreGettingStarted: cloneGettingStartedVault, @@ -1561,14 +1571,6 @@ function App() { onSetThemeMode: handleSetThemeMode, mcpStatus, onInstallMcp: openMcpSetupDialog, - onOpenAiAgents: dialogs.openSettings, - aiAgentsStatus, - vaultAiGuidanceStatus, - onRestoreVaultAiGuidance: restoreVaultAiGuidanceCommand, - selectedAiAgent: aiAgentPreferences.defaultAiAgent, - onSetDefaultAiAgent: aiAgentPreferences.setDefaultAiAgent, - onCycleDefaultAiAgent: aiAgentPreferences.cycleDefaultAiAgent, - selectedAiAgentLabel: aiAgentPreferences.defaultAiAgentLabel, onReloadVault: handleManualVaultReload, onRepairVault: handleRepairVault, onSetNoteIcon: handleSetNoteIconCommand, @@ -1711,8 +1713,8 @@ function App() { onCreateAndOpenNote={notes.handleCreateNoteForRelationship} onChangeWorkspace={activeDeletedFile ? undefined : handleChangeWorkspace} onInitializeProperties={handleInitializeProperties} - showAIChat={dialogs.showAIChat} - onToggleAIChat={dialogs.toggleAIChat} + showAIChat={effectiveShowAIChat} + onToggleAIChat={aiFeaturesEnabled ? dialogs.toggleAIChat : undefined} vaultPath={activeEditorVaultPath} vaultPaths={writableVaultPaths} noteList={aiNoteList} @@ -1755,7 +1757,7 @@ function App() { - handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} defaultAiTarget={settings.default_ai_target ?? undefined} aiModelProviders={settings.ai_model_providers ?? []} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onSetDefaultAiTarget={aiAgentPreferences.setDefaultAiTarget} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} locale={appLocale} /> + handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} commitActionPending={commitFlow.isOpeningCommitDialog} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} onUpdateWorkspaceIdentity={vaultSwitcher.updateWorkspaceIdentity} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiFeaturesEnabled ? aiAgentsStatus : undefined} vaultAiGuidanceStatus={aiFeaturesEnabled ? vaultAiGuidanceStatus : undefined} defaultAiAgent={aiFeaturesEnabled ? aiAgentPreferences.defaultAiAgent : undefined} defaultAiTarget={aiFeaturesEnabled ? settings.default_ai_target ?? undefined : undefined} aiModelProviders={aiFeaturesEnabled ? settings.ai_model_providers ?? [] : []} onSetDefaultAiAgent={aiFeaturesEnabled ? aiAgentPreferences.setDefaultAiAgent : undefined} onSetDefaultAiTarget={aiFeaturesEnabled ? aiAgentPreferences.setDefaultAiTarget : undefined} onRestoreVaultAiGuidance={aiFeaturesEnabled ? () => { void restoreVaultAiGuidance() } : undefined} locale={appLocale} /> setToastMessage(null)} /> @@ -1766,6 +1768,7 @@ function App() { entries={visibleEntries} aiAgentReady={aiAgentPreferences.defaultAiAgentReady} aiAgentLabel={aiAgentPreferences.defaultAiAgentLabel} + aiModeEnabled={aiFeaturesEnabled} locale={appLocale} onClose={dialogs.closeCommandPalette} /> diff --git a/src/components/BreadcrumbBar.test.tsx b/src/components/BreadcrumbBar.test.tsx index d7319e8b..2386fc7c 100644 --- a/src/components/BreadcrumbBar.test.tsx +++ b/src/components/BreadcrumbBar.test.tsx @@ -673,6 +673,22 @@ describe('BreadcrumbBar — note width toggle', () => { }) }) +describe('BreadcrumbBar — AI panel toggle', () => { + it('hides the AI panel action when no toggle callback is available', () => { + render() + expect(screen.queryByRole('button', { name: 'Open the AI panel' })).not.toBeInTheDocument() + }) + + it('shows and runs the AI panel action when a toggle callback is available', () => { + const onToggleAIChat = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: 'Open the AI panel' })) + + expect(onToggleAIChat).toHaveBeenCalledOnce() + }) +}) + describe('BreadcrumbBar — table of contents toggle', () => { it('shows the table of contents action and calls the toggle handler', () => { const onToggleTableOfContents = vi.fn() diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index c77ca4d2..2795fa49 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -854,7 +854,9 @@ function BreadcrumbActions({ - + {onToggleAIChat ? ( + + ) : null} { expect(screen.queryByText('Search Notes')).not.toBeInTheDocument() }) + it('keeps leading-space input in command search when AI mode is disabled', () => { + render( + , + ) + + const input = screen.getByPlaceholderText('Type a command...') + fireEvent.change(input, { target: { value: ' search' } }) + + expect(screen.queryByTestId('command-palette-ai-input')).not.toBeInTheDocument() + expect(input).toHaveValue(' search') + expect(queueAiPrompt).not.toHaveBeenCalled() + expect(requestOpenAiChat).not.toHaveBeenCalled() + }) + it('inserts Tauri native folder drops into the command query input', async () => { nativeDropState.tauriMode = true render() diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index e7caca39..1bbfb7a1 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -21,6 +21,7 @@ interface CommandPaletteProps { claudeCodeReady?: boolean aiAgentReady?: boolean aiAgentLabel?: string + aiModeEnabled?: boolean locale?: AppLocale onClose: () => void } @@ -280,6 +281,7 @@ function OpenCommandPalette({ claudeCodeReady = true, aiAgentReady, aiAgentLabel = 'Claude Code', + aiModeEnabled = true, locale = 'en', onClose, }: Omit) { @@ -289,7 +291,7 @@ function OpenCommandPalette({ const inputRef = useRef(null) const aiInputRef = useRef(null) const listRef = useRef(null) - const aiMode = aiValue.startsWith(' ') + const aiMode = aiModeEnabled && aiValue.startsWith(' ') const resolvedAiAgentReady = aiAgentReady ?? claudeCodeReady const { groups, flatList } = usePaletteResults(commands, query) const t = createTranslator(locale) @@ -358,7 +360,7 @@ function OpenCommandPalette({ const handleQueryChange = (nextQuery: string) => { setSelectedIndex(0) - if (nextQuery.startsWith(' ')) { + if (aiModeEnabled && nextQuery.startsWith(' ')) { setAiValue(nextQuery) setQuery('') return diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 026e7279..a9d635fb 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -628,7 +628,7 @@ export const Editor = memo(function Editor(props: EditorProps) { {...buildEditorLayoutProps(props, runtime, findRequest)} onToggleInspector={rightPanel.handleToggleInspectorPanel} showAIChat={props.showAIChat} - onToggleAIChat={rightPanel.handleToggleAIChatPanel} + onToggleAIChat={props.onToggleAIChat ? rightPanel.handleToggleAIChatPanel : undefined} showTableOfContents={rightPanel.showTableOfContents} onToggleTableOfContents={rightPanel.handleToggleTableOfContents} /> diff --git a/src/components/SettingsPanel.test.tsx b/src/components/SettingsPanel.test.tsx index be386438..80b85bb7 100644 --- a/src/components/SettingsPanel.test.tsx +++ b/src/components/SettingsPanel.test.tsx @@ -170,6 +170,25 @@ describe('SettingsPanel', () => { expect(screen.getByPlaceholderText('gemini-2.5-flash')).toBeInTheDocument() }) + it('lets users disable AI surfaces without showing missing-agent setup', () => { + render( + + ) + + expect(within(screen.getByTestId('settings-ai-features-enabled')).getByRole('switch')).toHaveAttribute('aria-checked', 'false') + expect(screen.queryByText('Recognized coding agents')).not.toBeInTheDocument() + + fireEvent.click(within(screen.getByTestId('settings-ai-features-enabled')).getByRole('switch')) + saveSettingsPanel() + + expectSettingsSaved({ ai_features_enabled: true }) + }) + it('updates the draft language when stored settings finish loading', () => { const { rerender } = render( diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index d33505c9..4a2d63f8 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -44,6 +44,7 @@ import { } from '../lib/themeMode' import { normalizeReleaseChannel, serializeReleaseChannel, type ReleaseChannel } from '../lib/releaseChannel' import { shouldHideGitignoredFiles } from '../lib/gitignoredVisibility' +import { areAiFeaturesEnabled } from '../lib/aiFeatures' import { trackAllNotesVisibilityChanged } from '../lib/productAnalytics' import { AiProviderSettings } from './AiProviderSettings' import { PrivacySettingsSection } from './PrivacySettingsSection' @@ -108,6 +109,7 @@ interface SettingsDraft { autoGitIdleThresholdSeconds: number autoGitInactiveThresholdSeconds: number autoAdvanceInboxAfterOrganize: boolean + aiFeaturesEnabled: boolean defaultAiAgent: AiAgentId defaultAiTarget: string aiModelProviders: AiModelProvider[] @@ -139,6 +141,8 @@ interface SettingsBodyProps { setAutoGitInactiveThresholdSeconds: (value: number) => void autoAdvanceInboxAfterOrganize: boolean setAutoAdvanceInboxAfterOrganize: (value: boolean) => void + aiFeaturesEnabled: boolean + setAiFeaturesEnabled: (value: boolean) => void aiAgentsStatus: AiAgentsStatus defaultAiAgent: AiAgentId setDefaultAiAgent: (value: AiAgentId) => void @@ -207,6 +211,7 @@ function createSettingsDraft( DEFAULT_AUTOGIT_INACTIVE_THRESHOLD_SECONDS, ), autoAdvanceInboxAfterOrganize: settings.auto_advance_inbox_after_organize ?? false, + aiFeaturesEnabled: areAiFeaturesEnabled(settings), defaultAiAgent: resolveDefaultAiAgent(settings.default_ai_agent), defaultAiTarget: resolveAiTarget(settings).id, aiModelProviders: normalizeAiModelProviders(settings.ai_model_providers), @@ -263,6 +268,7 @@ function buildSettingsFromDraft(settings: Settings, draft: SettingsDraft): Setti note_width_mode: draft.defaultNoteWidth, sidebar_type_pluralization_enabled: draft.sidebarTypePluralizationEnabled, initial_h1_auto_rename_enabled: draft.initialH1AutoRename, + ai_features_enabled: draft.aiFeaturesEnabled, default_ai_agent: draft.defaultAiAgent, default_ai_target: draft.defaultAiTarget, ai_model_providers: draft.aiModelProviders.length > 0 ? draft.aiModelProviders : null, @@ -535,6 +541,8 @@ function SettingsBodyFromDraft({ setAutoGitInactiveThresholdSeconds={(value) => updateDraft('autoGitInactiveThresholdSeconds', value)} autoAdvanceInboxAfterOrganize={draft.autoAdvanceInboxAfterOrganize} setAutoAdvanceInboxAfterOrganize={(value) => updateDraft('autoAdvanceInboxAfterOrganize', value)} + aiFeaturesEnabled={draft.aiFeaturesEnabled} + setAiFeaturesEnabled={(value) => updateDraft('aiFeaturesEnabled', value)} aiAgentsStatus={aiAgentsStatus} defaultAiAgent={draft.defaultAiAgent} setDefaultAiAgent={(value) => updateDraft('defaultAiAgent', value)} @@ -716,6 +724,8 @@ function SettingsAgentWorkflowSections({ t, autoAdvanceInboxAfterOrganize, setAutoAdvanceInboxAfterOrganize, + aiFeaturesEnabled, + setAiFeaturesEnabled, aiAgentsStatus, defaultAiAgent, setDefaultAiAgent, @@ -736,6 +746,8 @@ function SettingsAgentWorkflowSections({ - - { - setDefaultAiTarget(value) - if (value.startsWith('agent:')) { - const agent = value.replace('agent:', '') as AiAgentId - setDefaultAiAgent(agent) - } - }} - options={buildDefaultAiTargetOptions(aiAgentsStatus, aiModelProviders, t)} - testId="settings-default-ai-agent" - /> - + - + {aiFeaturesEnabled ? ( + <> + + + { + setDefaultAiTarget(value) + if (value.startsWith('agent:')) { + const agent = value.replace('agent:', '') as AiAgentId + setDefaultAiAgent(agent) + } + }} + options={buildDefaultAiTargetOptions(aiAgentsStatus, aiModelProviders, t)} + testId="settings-default-ai-agent" + /> + + + + + + ) : null} ) } diff --git a/src/components/settingsPreferenceTracking.ts b/src/components/settingsPreferenceTracking.ts index a976fcad..2167779f 100644 --- a/src/components/settingsPreferenceTracking.ts +++ b/src/components/settingsPreferenceTracking.ts @@ -1,10 +1,12 @@ import type { Settings, NoteWidthMode } from '../types' import { trackEvent } from '../lib/telemetry' import { + trackAiFeaturesEnabledChanged, trackDateDisplayFormatChanged, trackDefaultNoteWidthChanged, trackSidebarTypePluralizationChanged, } from '../lib/productAnalytics' +import { areAiFeaturesEnabled } from '../lib/aiFeatures' import { DEFAULT_DATE_DISPLAY_FORMAT, normalizeDateDisplayFormat, @@ -14,6 +16,7 @@ import { DEFAULT_NOTE_WIDTH_MODE, normalizeNoteWidthMode } from '../utils/noteWi export interface SettingsPreferenceDraft { analytics: boolean + aiFeaturesEnabled: boolean dateDisplayFormat: DateDisplayFormat defaultNoteWidth: NoteWidthMode multiWorkspaceEnabled: boolean @@ -26,6 +29,11 @@ export function trackTelemetryConsentChange(previousAnalytics: boolean, nextAnal } export function trackSettingsPreferenceChanges(settings: Settings, draft: SettingsPreferenceDraft): void { + const previousAiFeaturesEnabled = areAiFeaturesEnabled(settings) + if (previousAiFeaturesEnabled !== draft.aiFeaturesEnabled) { + trackAiFeaturesEnabledChanged(draft.aiFeaturesEnabled) + } + const previousDateDisplayFormat = normalizeDateDisplayFormat(settings.date_display_format) ?? DEFAULT_DATE_DISPLAY_FORMAT if (previousDateDisplayFormat !== draft.dateDisplayFormat) { trackDateDisplayFormatChanged(draft.dateDisplayFormat) diff --git a/src/hooks/commands/aiAgentCommands.ts b/src/hooks/commands/aiAgentCommands.ts index 12950077..0d4797e0 100644 --- a/src/hooks/commands/aiAgentCommands.ts +++ b/src/hooks/commands/aiAgentCommands.ts @@ -24,6 +24,7 @@ function aiAgentKeywords(...keywords: string[]): string[] { } interface AiAgentCommandsConfig { + aiFeaturesEnabled?: boolean aiAgentsStatus?: AiAgentsStatus vaultAiGuidanceStatus?: VaultAiGuidanceStatus selectedAiAgent?: AiAgentId @@ -75,6 +76,7 @@ function restoreGuidanceCommands({ } export function buildAiAgentCommands({ + aiFeaturesEnabled = true, aiAgentsStatus, vaultAiGuidanceStatus, selectedAiAgent, @@ -84,6 +86,8 @@ export function buildAiAgentCommands({ onCycleDefaultAiAgent, onSetDefaultAiAgent, }: AiAgentCommandsConfig): CommandAction[] { + if (!aiFeaturesEnabled) return [] + const commands: CommandAction[] = [ { id: 'open-ai-agents', diff --git a/src/hooks/commands/viewCommands.ts b/src/hooks/commands/viewCommands.ts index 9c5c0d15..0cb3d89b 100644 --- a/src/hooks/commands/viewCommands.ts +++ b/src/hooks/commands/viewCommands.ts @@ -18,6 +18,7 @@ const DEFAULT_NOTE_WIDTH_COMMAND_LABELS: Record = { const noop = () => {} interface ViewCommandsConfig { + aiFeaturesEnabled?: boolean hasActiveNote: boolean activeNoteModified: boolean onSetViewMode: (mode: ViewMode) => void @@ -108,8 +109,21 @@ function buildToggleTableOfContentsCommand( } } +function buildAiViewCommands( + aiFeaturesEnabled: boolean, + onToggleAIChat?: () => void, +): CommandAction[] { + if (!aiFeaturesEnabled) return [] + + return [ + { id: 'toggle-ai-panel', label: 'Toggle AI Panel', group: 'View', shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewToggleAiChat), keywords: ['ai', 'agent', 'chat', 'assistant', 'contextual'], enabled: true, execute: () => onToggleAIChat?.() }, + { id: 'new-ai-chat', label: 'New AI chat', group: 'View', keywords: ['ai', 'agent', 'chat', 'assistant', 'new', 'fresh', 'conversation', 'reset'], enabled: true, execute: requestNewAiChat }, + ] +} + export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] { const { + aiFeaturesEnabled = true, hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, noteWidth = DEFAULT_NOTE_WIDTH_MODE, defaultNoteWidth = DEFAULT_NOTE_WIDTH_MODE, @@ -120,6 +134,8 @@ export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] { canMoveSelectedViewUp, canMoveSelectedViewDown, } = config + const aiCommands = buildAiViewCommands(aiFeaturesEnabled, onToggleAIChat) + return [ { id: 'view-editor', label: 'Editor Only', group: 'View', shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewEditorOnly), keywords: ['layout', 'focus'], enabled: true, execute: () => onSetViewMode('editor-only') }, { id: 'view-editor-list', label: 'Editor + Note List', group: 'View', shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewEditorList), keywords: ['layout'], enabled: true, execute: () => onSetViewMode('editor-list') }, @@ -131,9 +147,8 @@ export function buildViewCommands(config: ViewCommandsConfig): CommandAction[] { buildSetNoteWidthCommand('wide', noteWidth, hasActiveNote, onSetNoteWidth), buildSetDefaultNoteWidthCommand('normal', defaultNoteWidth, onSetDefaultNoteWidth), buildSetDefaultNoteWidthCommand('wide', defaultNoteWidth, onSetDefaultNoteWidth), - { id: 'toggle-ai-panel', label: 'Toggle AI Panel', group: 'View', shortcut: getAppCommandShortcutDisplay(APP_COMMAND_IDS.viewToggleAiChat), keywords: ['ai', 'agent', 'chat', 'assistant', 'contextual'], enabled: true, execute: () => onToggleAIChat?.() }, + ...aiCommands, buildToggleTableOfContentsCommand(hasActiveNote, onToggleTableOfContents), - { id: 'new-ai-chat', label: 'New AI chat', group: 'View', keywords: ['ai', 'agent', 'chat', 'assistant', 'new', 'fresh', 'conversation', 'reset'], enabled: true, execute: requestNewAiChat }, { id: 'toggle-backlinks', label: 'Toggle Backlinks', group: 'View', keywords: ['backlinks', 'references', 'links', 'mentions', 'incoming'], enabled: hasActiveNote, execute: onToggleInspector }, buildMoveSavedViewCommand('Up', selectedViewName, onMoveSelectedViewUp, canMoveSelectedViewUp), buildMoveSavedViewCommand('Down', selectedViewName, onMoveSelectedViewDown, canMoveSelectedViewDown), diff --git a/src/hooks/useAppCommandAiActions.ts b/src/hooks/useAppCommandAiActions.ts new file mode 100644 index 00000000..87f4964b --- /dev/null +++ b/src/hooks/useAppCommandAiActions.ts @@ -0,0 +1,65 @@ +import { useMemo } from 'react' +import type { AiAgentId, AiAgentsStatus } from '../lib/aiAgents' +import type { VaultAiGuidanceStatus } from '../lib/vaultAiGuidance' + +interface AppCommandDialogs { + toggleAIChat: () => void + openSettings: () => void +} + +interface AppCommandAiPreferences { + defaultAiAgent: AiAgentId + setDefaultAiAgent: (agent: AiAgentId) => void + cycleDefaultAiAgent: () => void + defaultAiAgentLabel: string +} + +interface AppCommandAiActions { + aiFeaturesEnabled: boolean + onToggleAIChat?: () => void + onOpenAiAgents?: () => void + aiAgentsStatus?: AiAgentsStatus + vaultAiGuidanceStatus?: VaultAiGuidanceStatus + onRestoreVaultAiGuidance?: () => void + selectedAiAgent?: AiAgentId + onSetDefaultAiAgent?: (agent: AiAgentId) => void + onCycleDefaultAiAgent?: () => void + selectedAiAgentLabel?: string +} + +export function useAppCommandAiActions( + aiFeaturesEnabled: boolean, + dialogs: AppCommandDialogs, + aiAgentsStatus: AiAgentsStatus, + vaultAiGuidanceStatus: VaultAiGuidanceStatus, + restoreVaultAiGuidanceCommand: (() => void) | undefined, + aiAgentPreferences: AppCommandAiPreferences, +): AppCommandAiActions { + return useMemo(() => { + if (!aiFeaturesEnabled) return { aiFeaturesEnabled: false } + + return { + aiFeaturesEnabled: true, + onToggleAIChat: dialogs.toggleAIChat, + onOpenAiAgents: dialogs.openSettings, + aiAgentsStatus, + vaultAiGuidanceStatus, + onRestoreVaultAiGuidance: restoreVaultAiGuidanceCommand, + selectedAiAgent: aiAgentPreferences.defaultAiAgent, + onSetDefaultAiAgent: aiAgentPreferences.setDefaultAiAgent, + onCycleDefaultAiAgent: aiAgentPreferences.cycleDefaultAiAgent, + selectedAiAgentLabel: aiAgentPreferences.defaultAiAgentLabel, + } + }, [ + aiFeaturesEnabled, + dialogs.toggleAIChat, + dialogs.openSettings, + aiAgentsStatus, + vaultAiGuidanceStatus, + restoreVaultAiGuidanceCommand, + aiAgentPreferences.defaultAiAgent, + aiAgentPreferences.setDefaultAiAgent, + aiAgentPreferences.cycleDefaultAiAgent, + aiAgentPreferences.defaultAiAgentLabel, + ]) +} diff --git a/src/hooks/useAppCommands.ts b/src/hooks/useAppCommands.ts index 2c8111df..881a0352 100644 --- a/src/hooks/useAppCommands.ts +++ b/src/hooks/useAppCommands.ts @@ -74,6 +74,7 @@ interface AppCommandsConfig { isGitVault?: boolean onInitializeGit?: () => void onCreateType?: () => void + aiFeaturesEnabled?: boolean onToggleAIChat?: () => void onToggleTableOfContents?: () => void onCheckForUpdates?: () => void @@ -209,6 +210,7 @@ type CommandRegistryVaultActions = Pick< > type CommandRegistryAiActions = Pick< CommandRegistryConfig, + | 'aiFeaturesEnabled' | 'mcpStatus' | 'onInstallMcp' | 'aiAgentsStatus' @@ -237,6 +239,14 @@ type CommandRegistryNoteActions = Pick< | 'noteListColumnsLabel' > +function aiFeaturesAreEnabled(config: Pick): boolean { + return config.aiFeaturesEnabled !== false +} + +function enabledAiChatToggle(config: Pick): (() => void) | undefined { + return aiFeaturesAreEnabled(config) ? config.onToggleAIChat : undefined +} + function createKeyboardActions( config: AppCommandsConfig, ): Omit[0], 'onArchiveNote'> { @@ -257,7 +267,7 @@ function createKeyboardActions( onZoomReset: config.onZoomReset, onGoBack: config.onGoBack, onGoForward: config.onGoForward, - onToggleAIChat: config.onToggleAIChat, + onToggleAIChat: enabledAiChatToggle(config), onToggleTableOfContents: config.onToggleTableOfContents, onToggleRawEditor: config.onToggleRawEditor, onToggleInspector: config.onToggleInspector, @@ -331,7 +341,7 @@ function createMenuEventActionHandlers( onSearch: config.onSearch, onToggleRawEditor: config.onToggleRawEditor, onToggleDiff: config.onToggleDiff, - onToggleAIChat: config.onToggleAIChat, + onToggleAIChat: enabledAiChatToggle(config), onToggleTableOfContents: config.onToggleTableOfContents, onToggleOrganized: config.onToggleOrganized, onGoBack: config.onGoBack, @@ -456,7 +466,7 @@ function createCommandRegistryCoreConfig( defaultNoteWidth: config.defaultNoteWidth, onSetNoteWidth: config.onSetNoteWidth, onSetDefaultNoteWidth: config.onSetDefaultNoteWidth, - onToggleAIChat: config.onToggleAIChat, + onToggleAIChat: enabledAiChatToggle(config), onToggleTableOfContents: config.onToggleTableOfContents, } } @@ -496,9 +506,17 @@ function createCommandRegistryVaultConfig( function createCommandRegistryAiConfig( config: AppCommandsConfig, ): CommandRegistryAiActions { - return { + const aiFeaturesEnabled = aiFeaturesAreEnabled(config) + const sharedConfig = { + aiFeaturesEnabled, mcpStatus: config.mcpStatus, onInstallMcp: config.onInstallMcp, + } + + if (!aiFeaturesEnabled) return sharedConfig + + return { + ...sharedConfig, aiAgentsStatus: config.aiAgentsStatus, vaultAiGuidanceStatus: config.vaultAiGuidanceStatus, onOpenAiAgents: config.onOpenAiAgents, diff --git a/src/hooks/useCommandRegistry.test.ts b/src/hooks/useCommandRegistry.test.ts index 47f5c677..4bf8aa2b 100644 --- a/src/hooks/useCommandRegistry.test.ts +++ b/src/hooks/useCommandRegistry.test.ts @@ -298,6 +298,27 @@ describe('useCommandRegistry', () => { expect(findCommand(result.current, 'archive-note')?.shortcut).toBeUndefined() }) + it('removes AI commands when AI features are disabled', () => { + const config = makeConfig({ + aiFeaturesEnabled: false, + onToggleAIChat: vi.fn(), + onOpenAiAgents: vi.fn(), + aiAgentsStatus: { + claude_code: { status: 'installed', version: '1.0.0' }, + codex: { status: 'missing', version: null }, + opencode: { status: 'missing', version: null }, + pi: { status: 'missing', version: null }, + gemini: { status: 'missing', version: null }, + }, + selectedAiAgent: 'claude_code', + }) + const { result } = renderHook(() => useCommandRegistry(config)) + + expect(findCommand(result.current, 'toggle-ai-panel')).toBeUndefined() + expect(findCommand(result.current, 'new-ai-chat')).toBeUndefined() + expect(findCommand(result.current, 'open-ai-agents')).toBeUndefined() + }) + it('exposes active file actions when a note is selected', () => { const onRevealActiveFile = vi.fn() const onCopyActiveFilePath = vi.fn() diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index d6b353de..4c490c79 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -31,6 +31,7 @@ interface CommandRegistryConfig { activeNoteHasIcon?: boolean mcpStatus?: string onInstallMcp?: () => void + aiFeaturesEnabled?: boolean aiAgentsStatus?: AiAgentsStatus vaultAiGuidanceStatus?: VaultAiGuidanceStatus onOpenAiAgents?: () => void @@ -139,7 +140,8 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com onGoBack, onGoForward, canGoBack, canGoForward, onCheckForUpdates, onCreateType, onRemoveActiveVault, onRestoreGettingStarted, isGettingStartedHidden, vaultCount, - mcpStatus, onInstallMcp, aiAgentsStatus, vaultAiGuidanceStatus, + mcpStatus, onInstallMcp, aiFeaturesEnabled, + aiAgentsStatus, vaultAiGuidanceStatus, onOpenAiAgents, onRestoreVaultAiGuidance, onSetDefaultAiAgent, selectedAiAgent, onCycleDefaultAiAgent, selectedAiAgentLabel, onReloadVault, onRepairVault, locale, systemLocale, selectedUiLanguage, onSetUiLanguage, onSetThemeMode, @@ -224,11 +226,13 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com ]) const viewCommands = useMemo(() => buildViewCommands({ + aiFeaturesEnabled, hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onToggleTableOfContents, zoomLevel, onZoomIn, onZoomOut, onZoomReset, onCustomizeNoteListColumns, canCustomizeNoteListColumns, noteListColumnsLabel, selectedViewName, onMoveSelectedViewUp, onMoveSelectedViewDown, canMoveSelectedViewUp, canMoveSelectedViewDown, }), [ + aiFeaturesEnabled, hasActiveNote, activeNoteModified, onSetViewMode, onToggleInspector, onToggleDiff, onToggleRawEditor, noteWidth, defaultNoteWidth, onSetNoteWidth, onSetDefaultNoteWidth, onToggleAIChat, onToggleTableOfContents, zoomLevel, onZoomIn, onZoomOut, onZoomReset, @@ -249,6 +253,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com ]) const aiCommands = useMemo(() => buildAiAgentCommands({ + aiFeaturesEnabled, aiAgentsStatus, vaultAiGuidanceStatus, selectedAiAgent, @@ -258,6 +263,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): import('./com onSetDefaultAiAgent, onCycleDefaultAiAgent, }), [ + aiFeaturesEnabled, aiAgentsStatus, vaultAiGuidanceStatus, selectedAiAgent, selectedAiAgentLabel, onOpenAiAgents, onRestoreVaultAiGuidance, onSetDefaultAiAgent, onCycleDefaultAiAgent, ]) diff --git a/src/hooks/useSettings.test.ts b/src/hooks/useSettings.test.ts index d0423873..5e552ecd 100644 --- a/src/hooks/useSettings.test.ts +++ b/src/hooks/useSettings.test.ts @@ -28,6 +28,7 @@ const defaultSettings: Settings = { date_display_format: null, note_width_mode: null, sidebar_type_pluralization_enabled: null, + ai_features_enabled: null, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, @@ -54,6 +55,7 @@ const savedSettings: Settings = { date_display_format: null, note_width_mode: null, sidebar_type_pluralization_enabled: null, + ai_features_enabled: null, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, @@ -117,6 +119,7 @@ function changedSettings(): Settings { date_display_format: 'iso', note_width_mode: 'wide', sidebar_type_pluralization_enabled: false, + ai_features_enabled: null, default_ai_agent: null, default_ai_target: null, ai_model_providers: null, diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 8d2496cd..eb589414 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -51,6 +51,7 @@ const EMPTY_SETTINGS: Settings = { note_width_mode: null, sidebar_type_pluralization_enabled: null, default_ai_agent: null, + ai_features_enabled: null, default_ai_target: null, ai_model_providers: null, hide_gitignored_files: null, @@ -73,6 +74,7 @@ function normalizeSettings(settings: Settings): Settings { date_display_format: normalizeDateDisplayFormat(settings.date_display_format), note_width_mode: normalizeNoteWidthMode(settings.note_width_mode), sidebar_type_pluralization_enabled: settings.sidebar_type_pluralization_enabled ?? null, + ai_features_enabled: settings.ai_features_enabled ?? null, default_ai_agent: normalizeStoredAiAgent(settings.default_ai_agent), default_ai_target: settings.default_ai_target?.trim() || null, ai_model_providers: aiModelProviders.length > 0 ? aiModelProviders : null, diff --git a/src/lib/aiFeatures.test.ts b/src/lib/aiFeatures.test.ts new file mode 100644 index 00000000..8872b916 --- /dev/null +++ b/src/lib/aiFeatures.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest' +import { areAiFeaturesEnabled } from './aiFeatures' + +describe('areAiFeaturesEnabled', () => { + it('defaults AI features on unless the user explicitly disables them', () => { + expect(areAiFeaturesEnabled(undefined)).toBe(true) + expect(areAiFeaturesEnabled({ ai_features_enabled: null })).toBe(true) + expect(areAiFeaturesEnabled({ ai_features_enabled: true })).toBe(true) + expect(areAiFeaturesEnabled({ ai_features_enabled: false })).toBe(false) + }) +}) diff --git a/src/lib/aiFeatures.ts b/src/lib/aiFeatures.ts new file mode 100644 index 00000000..7243ef1a --- /dev/null +++ b/src/lib/aiFeatures.ts @@ -0,0 +1,5 @@ +import type { Settings } from '../types' + +export function areAiFeaturesEnabled(settings: Pick | null | undefined): boolean { + return settings?.ai_features_enabled !== false +} diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json index c677316c..52dbdb03 100644 --- a/src/lib/locales/de-DE.json +++ b/src/lib/locales/de-DE.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Andere Nicht-Markdown-Dateien ohne In-App-Vorschau in „Alle Notizen“ anzeigen. Beim Durchsuchen von Ordnern werden sie weiterhin in ihren jeweiligen Ordnern angezeigt.", "settings.aiAgents.title": "KI-Agents", "settings.aiAgents.description": "Wählen Sie das Standard-KI-Ziel für Tolaria aus. Coding-Agents können mithilfe von Tools bearbeiten; API-Modelle und lokale Modelle werden im Chat-Modus ohne Vault-Write-Tools ausgeführt.", + "settings.aiFeatures.enable": "Tolaria-KI-Funktionen aktivieren", + "settings.aiFeatures.enableDescription": "AI-Panel-Steuerelemente, AI-Befehlspalettenmodus und AI-Statusmeldungen anzeigen. Deaktiviere diese Option, um KI-Oberflächen auszublenden, bis du sie hier wieder aktivierst.", "settings.aiAgents.default": "Standard-KI-Agent", "settings.aiAgents.defaultTarget": "Standard-AI-Ziel", "settings.aiAgents.agentGroup": "Coding-Agent", diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 279b41a9..dd57f371 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Show other non-Markdown files without an in-app preview in All Notes. Folder browsing still shows them in their folders.", "settings.aiAgents.title": "AI Agents", "settings.aiAgents.description": "Choose Tolaria's default AI target. Coding agents can edit through tools; API and local models run in chat mode without vault-write tools.", + "settings.aiFeatures.enable": "Enable Tolaria AI features", + "settings.aiFeatures.enableDescription": "Show AI panel controls, AI command-palette mode, and AI status prompts. Turn this off to hide AI surfaces until you re-enable them here.", "settings.aiAgents.default": "Default AI agent", "settings.aiAgents.defaultTarget": "Default AI target", "settings.aiAgents.agentGroup": "Coding agent", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 0ccccdc9..3088617e 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Mostrar en Todas las notas otros archivos que no sean Markdown y que no tengan vista previa en la aplicación. Al explorar las carpetas, estos archivos aún se muestran en sus carpetas.", "settings.aiAgents.title": "Agentes de IA", "settings.aiAgents.description": "Elija el objetivo de IA predeterminado de Tolaria. Los agentes de programación pueden editar mediante herramientas; los modelos de API y los modelos locales se ejecutan en modo chat sin herramientas de escritura en el repositorio.", + "settings.aiFeatures.enable": "Habilitar las funciones de IA de Tolaria", + "settings.aiFeatures.enableDescription": "Mostrar los controles del panel de IA, el modo de paleta de comandos de IA y los mensajes de estado de IA. Desactiva esta opción para ocultar las superficies de IA hasta que las vuelvas a habilitar aquí.", "settings.aiAgents.default": "Agente de IA predeterminado", "settings.aiAgents.defaultTarget": "Objetivo de IA predeterminado", "settings.aiAgents.agentGroup": "Agente de programación", diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json index 6d89bdac..81f88e39 100644 --- a/src/lib/locales/es-ES.json +++ b/src/lib/locales/es-ES.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Mostrar en Todas las notas otros archivos que no sean de Markdown y que no tengan vista previa en la aplicación. Al explorar las carpetas, siguen mostrándose en sus carpetas.", "settings.aiAgents.title": "Agentes de IA", "settings.aiAgents.description": "Elija el objetivo de IA predeterminado de Tolaria. Los agentes de programación pueden editar mediante herramientas; los modelos de API y los modelos locales se ejecutan en modo chat sin herramientas de escritura en el almacén.", + "settings.aiFeatures.enable": "Habilitar las funciones de IA de Tolaria", + "settings.aiFeatures.enableDescription": "Mostrar los controles del panel de IA, el modo de paleta de comandos de IA y los mensajes de estado de IA. Desactiva esta opción para ocultar las superficies de IA hasta que las vuelvas a habilitar aquí.", "settings.aiAgents.default": "Agente de IA predeterminado", "settings.aiAgents.defaultTarget": "Objetivo de IA predeterminado", "settings.aiAgents.agentGroup": "Agente de programación", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index c6e65bb8..3f81b4e3 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Afficher dans Toutes les notes les autres fichiers non Markdown sans aperçu dans l'application. Lors de la navigation dans les dossiers, ils apparaissent toujours dans leurs dossiers respectifs.", "settings.aiAgents.title": "Agents d'IA", "settings.aiAgents.description": "Choisir la cible d'IA par défaut de Tolaria. Les agents de développement peuvent effectuer des modifications à l'aide d'outils ; les modèles API et les modèles locaux s'exécutent en mode chat, sans outils d'écriture dans le coffre-fort.", + "settings.aiFeatures.enable": "Activer les fonctionnalités d'IA de Tolaria", + "settings.aiFeatures.enableDescription": "Afficher les commandes du panneau IA, le mode palette de commandes IA et les invites d'état IA. Désactivez cette option pour masquer les surfaces de l'IA jusqu'à ce que vous les réactiviez ici.", "settings.aiAgents.default": "Agent d'IA par défaut", "settings.aiAgents.defaultTarget": "Cible d'IA par défaut", "settings.aiAgents.agentGroup": "Agent de codage", diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json index 8a2a6c6e..a8a5d740 100644 --- a/src/lib/locales/it-IT.json +++ b/src/lib/locales/it-IT.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Mostra in Tutte le note gli altri file non Markdown senza anteprima in-app. Durante la navigazione tra le cartelle, questi file continuano a essere visualizzati nelle rispettive cartelle.", "settings.aiAgents.title": "Agenti IA", "settings.aiAgents.description": "Scegli la destinazione IA predefinita di Tolaria. Gli agenti di codifica possono apportare modifiche tramite strumenti; i modelli API e locali vengono eseguiti in modalità chat senza strumenti di scrittura nel vault.", + "settings.aiFeatures.enable": "Abilita le funzionalità di Tolaria AI", + "settings.aiFeatures.enableDescription": "Mostra i controlli del pannello IA, la modalità tavolozza dei comandi IA e i messaggi di stato IA. Disattiva questa opzione per nascondere le superfici AI finché non le riattivi qui.", "settings.aiAgents.default": "Agente IA predefinito", "settings.aiAgents.defaultTarget": "Target IA predefinito", "settings.aiAgents.agentGroup": "Agente di codifica", diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json index c5ea7d15..896b942a 100644 --- a/src/lib/locales/ja-JP.json +++ b/src/lib/locales/ja-JP.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "アプリ内プレビューのないその他の非Markdownファイルを「すべてのノート」に表示する。フォルダー参照では、引き続きこれらのファイルがそれぞれのフォルダー内に表示されます。", "settings.aiAgents.title": "AIエージェント", "settings.aiAgents.description": "TolariaのデフォルトのAIターゲットを選択してください。コーディングエージェントはツールを使用して編集できます。APIモデルとローカルモデルは、Vault書き込みツールなしでチャットモードで実行されます。", + "settings.aiFeatures.enable": "Tolaria AI機能を有効にする", + "settings.aiFeatures.enableDescription": "AIパネルコントロール、AIコマンドパレットモード、AIステータスプロンプトを表示します。これをオフにすると、ここで再度有効にするまでAIサーフェスが非表示になります。", "settings.aiAgents.default": "デフォルトのAIエージェント", "settings.aiAgents.defaultTarget": "デフォルトのAIターゲット", "settings.aiAgents.agentGroup": "Coding agent", diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json index 15dd4716..6f7c4523 100644 --- a/src/lib/locales/ko-KR.json +++ b/src/lib/locales/ko-KR.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "모든 노트에서 앱 내 미리 보기가 없는 기타 비 Markdown 파일을 표시합니다. 폴더 탐색 시에는 여전히 해당 파일이 해당 폴더에 표시됩니다.", "settings.aiAgents.title": "AI 에이전트", "settings.aiAgents.description": "Tolaria의 기본 AI 대상을 선택하세요. 코딩 에이전트는 도구를 통해 편집할 수 있습니다. API 모델과 로컬 모델은 Vault 쓰기 도구 없이 채팅 모드에서 실행됩니다.", + "settings.aiFeatures.enable": "Tolaria AI 기능 활성화", + "settings.aiFeatures.enableDescription": "AI 패널 컨트롤, AI 명령 팔레트 모드 및 AI 상태 프롬프트를 표시합니다. 이 기능을 끄면 AI 표면이 숨겨지며, 여기에서 다시 활성화할 때까지 숨겨진 상태로 유지됩니다.", "settings.aiAgents.default": "기본 AI 에이전트", "settings.aiAgents.defaultTarget": "기본 AI 대상", "settings.aiAgents.agentGroup": "Coding agent", diff --git a/src/lib/locales/pl-PL.json b/src/lib/locales/pl-PL.json index 4949ace9..8c31364b 100644 --- a/src/lib/locales/pl-PL.json +++ b/src/lib/locales/pl-PL.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Pokaż w sekcji Wszystkie notatki inne pliki inne niż Markdown, które nie mają podglądu w aplikacji. Podczas przeglądania folderów nadal są one wyświetlane w swoich folderach.", "settings.aiAgents.title": "Agenci AI", "settings.aiAgents.description": "Wybierz domyślny cel AI dla Tolarii. Agenci programistyczni mogą edytować za pomocą narzędzi; modele API i modele lokalne działają w trybie czatu bez narzędzi do zapisu w skarbcu.", + "settings.aiFeatures.enable": "Włącz funkcje Tolaria AI", + "settings.aiFeatures.enableDescription": "Pokaż elementy sterujące panelu AI, tryb palety poleceń AI i komunikaty o stanie AI. Wyłącz tę opcję, aby ukryć powierzchnie AI, dopóki nie włączysz ich ponownie tutaj.", "settings.aiAgents.default": "Domyślny agent AI", "settings.aiAgents.defaultTarget": "Domyślny cel AI", "settings.aiAgents.agentGroup": "Agent programistyczny", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index 3beccb77..7c87c074 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Exibir outros arquivos que não sejam Markdown e que não tenham visualização no aplicativo em Todas as notas. Ao navegar pelas pastas, eles ainda aparecem em suas respectivas pastas.", "settings.aiAgents.title": "Agentes de IA", "settings.aiAgents.description": "Escolha o destino de IA padrão do Tolaria. Os agentes de programação podem editar por meio de ferramentas; a API e os modelos locais são executados no modo de chat, sem ferramentas de gravação no vault.", + "settings.aiFeatures.enable": "Ativar recursos de IA da Tolaria", + "settings.aiFeatures.enableDescription": "Mostrar controles do painel de IA, modo de paleta de comandos de IA e avisos de status de IA. Desative esta opção para ocultar as superfícies de IA até reativá-las aqui.", "settings.aiAgents.default": "Agente de IA padrão", "settings.aiAgents.defaultTarget": "Destino de IA padrão", "settings.aiAgents.agentGroup": "Agente de programação", diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json index 786b2e68..bb982900 100644 --- a/src/lib/locales/pt-PT.json +++ b/src/lib/locales/pt-PT.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Mostrar outros ficheiros não Markdown sem pré-visualização na aplicação em Todas as notas. A navegação por pastas continua a mostrá-los nas respetivas pastas.", "settings.aiAgents.title": "Agentes de IA", "settings.aiAgents.description": "Escolha o alvo de IA predefinido da Tolaria. Os agentes de programação podem editar através de ferramentas; os modelos de API e locais são executados em modo de chat sem ferramentas de escrita no cofre.", + "settings.aiFeatures.enable": "Ativar as funcionalidades de IA da Tolaria", + "settings.aiFeatures.enableDescription": "Mostrar os controlos do painel de IA, o modo de paleta de comandos de IA e as mensagens de estado de IA. Desative esta opção para ocultar as superfícies de IA até as reativar aqui.", "settings.aiAgents.default": "Agente de IA predefinido", "settings.aiAgents.defaultTarget": "Destino de IA predefinido", "settings.aiAgents.agentGroup": "Agente de programação", diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json index 51609dc8..f98dd8a5 100644 --- a/src/lib/locales/ru-RU.json +++ b/src/lib/locales/ru-RU.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Показывать в разделе «Все заметки» другие файлы, не относящиеся к Markdown, без предварительного просмотра в приложении. При просмотре папок они по-прежнему отображаются в своих папках.", "settings.aiAgents.title": "AI-агенты", "settings.aiAgents.description": "Выберите целевую ИИ-среду Tolaria по умолчанию. Агенты-разработчики могут вносить изменения с помощью инструментов; API-модели и локальные модели работают в режиме чата без инструментов для записи в хранилище.", + "settings.aiFeatures.enable": "Включить функции Tolaria AI", + "settings.aiFeatures.enableDescription": "Показывать элементы управления панели ИИ, режим палитры команд ИИ и подсказки о состоянии ИИ. Отключите эту функцию, чтобы скрыть интерфейсы ИИ, пока вы снова не включите их здесь.", "settings.aiAgents.default": "AI-агент по умолчанию", "settings.aiAgents.defaultTarget": "Целевая ИИ-модель по умолчанию", "settings.aiAgents.agentGroup": "Агент для кодирования", diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index 55ecf1b2..715005d1 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "Hiển thị các tệp không phải Markdown khác không có chế độ xem trước trong ứng dụng trong mục Tất cả ghi chú. Khi duyệt thư mục, các tệp này vẫn hiển thị trong thư mục tương ứng.", "settings.aiAgents.title": "Tác nhân AI", "settings.aiAgents.description": "Chọn mục tiêu AI mặc định của Tolaria. Đại lý lập trình có thể chỉnh sửa thông qua các công cụ; các mô hình API và mô hình cục bộ chạy ở chế độ trò chuyện mà không cần công cụ ghi vào vault.", + "settings.aiFeatures.enable": "Bật các tính năng AI của Tolaria", + "settings.aiFeatures.enableDescription": "Hiển thị các điều khiển trên bảng điều khiển AI, chế độ bảng lệnh AI và lời nhắc trạng thái AI. Tắt tùy chọn này để ẩn các bề mặt AI cho đến khi bạn bật lại tại đây.", "settings.aiAgents.default": "Tác nhân AI mặc định", "settings.aiAgents.defaultTarget": "Mục tiêu AI mặc định", "settings.aiAgents.agentGroup": "Coding agent", diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json index 45bb8bbc..b585d7ae 100644 --- a/src/lib/locales/zh-CN.json +++ b/src/lib/locales/zh-CN.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "在“所有笔记”中显示其他无法在应用内预览的非 Markdown 文件。在文件夹浏览模式下,这些文件仍会显示在其所在的文件夹中。", "settings.aiAgents.title": "AI 代理", "settings.aiAgents.description": "选择 Tolaria 的默认 AI 目标。编程代理可以通过工具进行编辑;API 和本地模型在聊天模式下运行,无需使用 Vault 写入工具。", + "settings.aiFeatures.enable": "启用 Tolaria AI 功能", + "settings.aiFeatures.enableDescription": "显示 AI 面板控件、AI 命令面板模式和 AI 状态提示。关闭此选项可隐藏 AI 界面,直到您在此处重新启用。", "settings.aiAgents.default": "默认 AI 代理", "settings.aiAgents.defaultTarget": "默认 AI 目标", "settings.aiAgents.agentGroup": "编码代理", diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json index 83b040bc..e3dde058 100644 --- a/src/lib/locales/zh-TW.json +++ b/src/lib/locales/zh-TW.json @@ -173,6 +173,8 @@ "settings.allNotesVisibility.unsupportedDescription": "在「所有筆記」中顯示其他無法在應用程式內預覽的非 Markdown 檔案。在瀏覽資料夾時,這些檔案仍會顯示在各自的資料夾中。", "settings.aiAgents.title": "AI 代理", "settings.aiAgents.description": "選擇 Tolaria 的預設 AI 目標。程式開發代理可以透過工具進行編輯;API 和本機模型在聊天模式下執行,無需使用 Vault 寫入工具。", + "settings.aiFeatures.enable": "啟用 Tolaria AI 功能", + "settings.aiFeatures.enableDescription": "顯示 AI 面板控制項、AI 指令調色盤模式和 AI 狀態提示。關閉此功能即可隱藏 AI 介面,直到您在此處重新啟用為止。", "settings.aiAgents.default": "預設 AI 代理", "settings.aiAgents.defaultTarget": "預設 AI 目標", "settings.aiAgents.agentGroup": "程式開發代理程式", diff --git a/src/lib/productAnalytics.ts b/src/lib/productAnalytics.ts index 019816bd..576e2bb8 100644 --- a/src/lib/productAnalytics.ts +++ b/src/lib/productAnalytics.ts @@ -57,6 +57,12 @@ export function trackAllNotesVisibilityChanged( } } +export function trackAiFeaturesEnabledChanged(enabled: boolean): void { + trackEvent('ai_features_visibility_changed', { + enabled: numericFlag(enabled), + }) +} + export function trackDefaultNoteWidthChanged(mode: NoteWidthMode): void { trackEvent('note_width_default_changed', { mode }) } diff --git a/src/types.ts b/src/types.ts index 99a5c518..a18ff828 100644 --- a/src/types.ts +++ b/src/types.ts @@ -117,6 +117,7 @@ export interface Settings { note_width_mode?: NoteWidthMode | null sidebar_type_pluralization_enabled?: boolean | null initial_h1_auto_rename_enabled?: boolean | null + ai_features_enabled?: boolean | null default_ai_agent?: AiAgentId | null default_ai_target?: string | null ai_model_providers?: AiModelProvider[] | null