Files
tolaria/src/components/useAiPanelController.ts
2026-05-29 00:58:40 +02:00

211 lines
6.0 KiB
TypeScript

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
vaultPaths?: 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
sessionId?: string
}
export interface AiPanelController {
agent: ReturnType<typeof useCliAiAgent>
input: string
setInput: React.Dispatch<React.SetStateAction<string>>
linkedEntries: ReturnType<typeof useAiPanelContextSnapshot>['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<AgentFileCallbacks>(() => ({
onFileCreated,
onFileModified,
onVaultChanged,
}), [onFileCreated, onFileModified, onVaultChanged])
}
function useAiPermissionModeHandler({
agent,
defaultAiAgent,
isActive,
locale,
permissionMode,
}: {
agent: ReturnType<typeof useCliAiAgent>
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,
vaultPaths,
contextPrompt,
defaultAiAgent,
defaultAiTarget,
defaultAiAgentReady,
defaultAiAgentReadiness,
onFileCreated,
onFileModified,
onVaultChanged,
sessionId,
}: Pick<
UseAiPanelControllerArgs,
| 'vaultPath'
| 'vaultPaths'
| 'defaultAiAgent'
| 'defaultAiTarget'
| 'defaultAiAgentReady'
| 'defaultAiAgentReadiness'
| 'onFileCreated'
| 'onFileModified'
| 'onVaultChanged'
| 'sessionId'
> & { contextPrompt?: string }) {
const fileCallbacks = useAgentFileCallbacks({ onFileCreated, onFileModified, onVaultChanged })
const permissionMode = useVaultAiAgentPermissionMode()
const agent = useCliAiAgent(vaultPath, vaultPaths, contextPrompt, fileCallbacks, {
agent: defaultAiAgent,
target: defaultAiTarget,
agentReady: resolveAgentReady(defaultAiAgentReadiness, defaultAiAgentReady),
permissionMode,
sessionId,
})
return { agent, permissionMode }
}
export function useAiPanelController({
vaultPath,
vaultPaths,
defaultAiAgent,
defaultAiTarget,
defaultAiAgentReady,
defaultAiAgentReadiness,
activeEntry,
activeNoteContent,
entries,
openTabs,
noteList,
noteListFilter,
locale = 'en',
onOpenNote,
onFileCreated,
onFileModified,
onVaultChanged,
sessionId,
}: UseAiPanelControllerArgs): AiPanelController {
const [input, setInput] = useState('')
const { linkedEntries, contextPrompt } = useAiPanelContextSnapshot({
activeEntry,
activeNoteContent,
entries,
input,
openTabs,
noteList,
noteListFilter,
})
const { agent, permissionMode } = usePanelAgent({ vaultPath, vaultPaths, contextPrompt, defaultAiAgent, defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, onFileCreated, onFileModified, onVaultChanged, sessionId })
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,
}
}