* feat: add claude CLI subprocess backend for AI panels Add claude_cli.rs module that spawns the local `claude` CLI as a subprocess instead of calling the Anthropic API directly. This removes the need for users to configure an API key — the CLI uses the user's existing Claude authentication. - find_claude_binary(): discovers claude in PATH or common locations - check_cli(): returns install/version status for the frontend - run_chat_stream(): spawns claude -p with stream-json for chat panel - run_agent_stream(): spawns claude with MCP vault tools for agent panel - Parses stream-json NDJSON events and emits typed Tauri events - Remove http:default capability (no longer calling APIs from frontend) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: replace Anthropic API with Claude CLI for AI chat and agent panels Remove direct Anthropic API calls and the entire tool-use loop from the frontend. Both AI Chat and AI Agent panels now delegate to the `claude` CLI subprocess via Tauri commands, streaming NDJSON events back to the UI. Key changes: - ai-chat.ts / ai-agent.ts: replace SSE/API streaming with Tauri invoke + listen - useAIChat / useAiAgent hooks: simplified to use CLI streaming callbacks - AIChatPanel / AiPanel: remove API key dialogs, model selectors, undo support - SettingsPanel: remove Anthropic key field (no longer needed) - claude_cli.rs: add comprehensive tests (37 total, 90% coverage) - mock-handlers: replace ai_chat with check_claude_cli / stream_claude_* stubs - Net -432 lines across 17 files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add --verbose flag to claude CLI subprocess args * chore: exclude search.rs and lib.rs from coverage (qmd integration + Tauri setup) --------- Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
/**
|
|
* Custom hook encapsulating AI chat state and message handling.
|
|
* Uses Claude CLI subprocess via Tauri for streaming responses.
|
|
*/
|
|
import { useState, useCallback, useRef } from 'react'
|
|
import type { VaultEntry } from '../types'
|
|
import {
|
|
type ChatMessage, nextMessageId,
|
|
buildSystemPrompt, streamClaudeChat,
|
|
} from '../utils/ai-chat'
|
|
|
|
export function useAIChat(
|
|
allContent: Record<string, string>,
|
|
contextNotes: VaultEntry[],
|
|
) {
|
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
const [isStreaming, setIsStreaming] = useState(false)
|
|
const [streamingContent, setStreamingContent] = useState('')
|
|
const abortRef = useRef(false)
|
|
const sessionIdRef = useRef<string | undefined>(undefined)
|
|
|
|
const sendMessage = useCallback((text: string) => {
|
|
if (!text.trim() || isStreaming) return
|
|
|
|
const userMsg: ChatMessage = { role: 'user', content: text.trim(), id: nextMessageId() }
|
|
setMessages(prev => [...prev, userMsg])
|
|
setIsStreaming(true)
|
|
setStreamingContent('')
|
|
abortRef.current = false
|
|
|
|
const { prompt: systemPrompt } = buildSystemPrompt(contextNotes, allContent)
|
|
let accumulated = ''
|
|
|
|
streamClaudeChat(text.trim(), systemPrompt || undefined, sessionIdRef.current, {
|
|
onInit: (sid) => { sessionIdRef.current = sid },
|
|
|
|
onText: (chunk) => {
|
|
if (abortRef.current) return
|
|
accumulated += chunk
|
|
setStreamingContent(accumulated)
|
|
},
|
|
|
|
onError: (error) => {
|
|
if (abortRef.current) return
|
|
setMessages(prev => [...prev, { role: 'assistant', content: `Error: ${error}`, id: nextMessageId() }])
|
|
setStreamingContent('')
|
|
setIsStreaming(false)
|
|
},
|
|
|
|
onDone: () => {
|
|
if (abortRef.current) return
|
|
if (accumulated) {
|
|
setMessages(prev => [...prev, { role: 'assistant', content: accumulated, id: nextMessageId() }])
|
|
}
|
|
setStreamingContent('')
|
|
setIsStreaming(false)
|
|
},
|
|
}).then(sid => {
|
|
if (sid) sessionIdRef.current = sid
|
|
})
|
|
}, [isStreaming, allContent, contextNotes])
|
|
|
|
const clearConversation = useCallback(() => {
|
|
abortRef.current = true
|
|
setMessages([])
|
|
setIsStreaming(false)
|
|
setStreamingContent('')
|
|
sessionIdRef.current = undefined
|
|
}, [])
|
|
|
|
const retryMessage = useCallback((msgIndex: number) => {
|
|
const userMsgIndex = msgIndex - 1
|
|
if (userMsgIndex < 0) return
|
|
const userMsg = messages[userMsgIndex]
|
|
if (userMsg.role !== 'user') return
|
|
|
|
setMessages(prev => prev.slice(0, msgIndex))
|
|
sendMessage(userMsg.content)
|
|
}, [messages, sendMessage])
|
|
|
|
return { messages, isStreaming, streamingContent, sendMessage, clearConversation, retryMessage }
|
|
}
|