import { useCallback, useMemo, useState, useSyncExternalStore } from 'react' import type { AiAgentId, AiAgentReadiness } from '../lib/aiAgents' import type { AiTarget } from '../lib/aiTargets' import type { AppLocale } from '../lib/i18n' import { trackAiAgentPermissionModeChanged } from '../lib/productAnalytics' import { aiAgentPermissionModeMarker, normalizeAiAgentPermissionMode, type AiAgentPermissionMode, } from '../lib/aiAgentPermissionMode' import { useCliAiAgent, type AgentFileCallbacks } from '../hooks/useCliAiAgent' import type { VaultEntry } from '../types' import { getVaultConfig, subscribeVaultConfig, updateVaultConfigField, } from '../utils/vaultConfigStore' import { type NoteListItem, type NoteReference, } from '../utils/ai-context' import { useAiPanelContextSnapshot } from './useAiPanelContextSnapshot' interface UseAiPanelControllerArgs { vaultPath: string defaultAiAgent: AiAgentId defaultAiTarget?: AiTarget defaultAiAgentReady: boolean defaultAiAgentReadiness?: AiAgentReadiness activeEntry?: VaultEntry | null activeNoteContent?: string | null entries?: VaultEntry[] openTabs?: VaultEntry[] noteList?: NoteListItem[] noteListFilter?: { type: string | null; query: string } locale?: AppLocale onOpenNote?: (path: string) => void onFileCreated?: (relativePath: string) => void onFileModified?: (relativePath: string) => void onVaultChanged?: () => void } export interface AiPanelController { agent: ReturnType input: string setInput: React.Dispatch> linkedEntries: ReturnType['linkedEntries'] hasContext: boolean isActive: boolean permissionMode: AiAgentPermissionMode handleSend: (text: string, references: NoteReference[]) => void handleNavigateWikilink: (target: string) => void handlePermissionModeChange: (mode: AiAgentPermissionMode) => void handleNewChat: () => void } function resolveAgentReady( readiness: AiAgentReadiness | undefined, ready: boolean, ): boolean { return (readiness ?? (ready ? 'ready' : 'missing')) === 'ready' } function useVaultAiAgentPermissionMode(): AiAgentPermissionMode { const vaultConfig = useSyncExternalStore(subscribeVaultConfig, getVaultConfig) return normalizeAiAgentPermissionMode(vaultConfig.ai_agent_permission_mode) } function useAgentFileCallbacks({ onFileCreated, onFileModified, onVaultChanged, }: Pick< UseAiPanelControllerArgs, 'onFileCreated' | 'onFileModified' | 'onVaultChanged' >): AgentFileCallbacks { return useMemo(() => ({ onFileCreated, onFileModified, onVaultChanged, }), [onFileCreated, onFileModified, onVaultChanged]) } function useAiPermissionModeHandler({ agent, defaultAiAgent, isActive, locale, permissionMode, }: { agent: ReturnType defaultAiAgent: AiAgentId isActive: boolean locale: AppLocale permissionMode: AiAgentPermissionMode }) { return useCallback((mode: AiAgentPermissionMode) => { const nextMode = normalizeAiAgentPermissionMode(mode) if (isActive || nextMode === permissionMode) return updateVaultConfigField('ai_agent_permission_mode', nextMode) trackAiAgentPermissionModeChanged(defaultAiAgent, nextMode) agent.addLocalMarker(aiAgentPermissionModeMarker(nextMode, locale)) }, [agent, defaultAiAgent, isActive, locale, permissionMode]) } function usePanelAgent({ vaultPath, contextPrompt, defaultAiAgent, defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, onFileCreated, onFileModified, onVaultChanged, }: Pick< UseAiPanelControllerArgs, | 'vaultPath' | 'defaultAiAgent' | 'defaultAiTarget' | 'defaultAiAgentReady' | 'defaultAiAgentReadiness' | 'onFileCreated' | 'onFileModified' | 'onVaultChanged' > & { contextPrompt?: string }) { const fileCallbacks = useAgentFileCallbacks({ onFileCreated, onFileModified, onVaultChanged }) const permissionMode = useVaultAiAgentPermissionMode() const agent = useCliAiAgent(vaultPath, contextPrompt, fileCallbacks, { agent: defaultAiAgent, target: defaultAiTarget, agentReady: resolveAgentReady(defaultAiAgentReadiness, defaultAiAgentReady), permissionMode, }) return { agent, permissionMode } } export function useAiPanelController({ vaultPath, defaultAiAgent, defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, activeEntry, activeNoteContent, entries, openTabs, noteList, noteListFilter, locale = 'en', onOpenNote, onFileCreated, onFileModified, onVaultChanged, }: UseAiPanelControllerArgs): AiPanelController { const [input, setInput] = useState('') const { linkedEntries, contextPrompt } = useAiPanelContextSnapshot({ activeEntry, activeNoteContent, entries, input, openTabs, noteList, noteListFilter, }) const { agent, permissionMode } = usePanelAgent({ vaultPath, contextPrompt, defaultAiAgent, defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, onFileCreated, onFileModified, onVaultChanged }) const isActive = agent.status === 'thinking' || agent.status === 'tool-executing' const handleSend = useCallback((text: string, references: NoteReference[]) => { if (!text.trim() || isActive) return agent.sendMessage(text, references) setInput('') }, [agent, isActive]) const handleNavigateWikilink = useCallback((target: string) => { onOpenNote?.(target) }, [onOpenNote]) const handlePermissionModeChange = useAiPermissionModeHandler({ agent, defaultAiAgent, isActive, locale, permissionMode }) const handleNewChat = useCallback(() => { agent.clearConversation() setInput('') }, [agent]) return { agent, input, setInput, linkedEntries, hasContext: !!activeEntry, isActive, permissionMode, handleSend, handleNavigateWikilink, handlePermissionModeChange, handleNewChat, } }