* 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>
99 lines
2.7 KiB
TypeScript
99 lines
2.7 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
|
|
// Mock the mock-tauri module before importing ai-chat
|
|
vi.mock('../mock-tauri', () => ({
|
|
isTauri: () => false,
|
|
}))
|
|
|
|
import {
|
|
estimateTokens, buildSystemPrompt,
|
|
nextMessageId, checkClaudeCli, streamClaudeChat,
|
|
} from './ai-chat'
|
|
import type { VaultEntry } from '../types'
|
|
|
|
// --- estimateTokens ---
|
|
|
|
describe('estimateTokens', () => {
|
|
it('estimates tokens from string length', () => {
|
|
expect(estimateTokens('abcd')).toBe(1)
|
|
expect(estimateTokens('abcdefgh')).toBe(2)
|
|
})
|
|
|
|
it('accepts a number (char count)', () => {
|
|
expect(estimateTokens(100)).toBe(25)
|
|
})
|
|
})
|
|
|
|
// --- buildSystemPrompt ---
|
|
|
|
describe('buildSystemPrompt', () => {
|
|
const makeEntry = (path: string, title: string): VaultEntry => ({
|
|
path, title, filename: `${title}.md`, isA: 'Note',
|
|
aliases: [], belongsTo: [], relatedTo: [],
|
|
status: null, owner: null, cadence: null,
|
|
modifiedAt: null, createdAt: null, fileSize: 100,
|
|
snippet: '', relationships: {},
|
|
})
|
|
|
|
it('returns empty prompt for no notes', () => {
|
|
const result = buildSystemPrompt([], {})
|
|
expect(result.prompt).toBe('')
|
|
expect(result.totalTokens).toBe(0)
|
|
expect(result.truncated).toBe(false)
|
|
})
|
|
|
|
it('includes note content in the prompt', () => {
|
|
const notes = [makeEntry('/test.md', 'Test Note')]
|
|
const content = { '/test.md': '# Test Note\nHello world' }
|
|
const result = buildSystemPrompt(notes, content)
|
|
expect(result.prompt).toContain('Test Note')
|
|
expect(result.prompt).toContain('Hello world')
|
|
expect(result.totalTokens).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
// --- nextMessageId ---
|
|
|
|
describe('nextMessageId', () => {
|
|
it('returns unique IDs', () => {
|
|
const id1 = nextMessageId()
|
|
const id2 = nextMessageId()
|
|
expect(id1).not.toBe(id2)
|
|
expect(id1).toMatch(/^msg-/)
|
|
})
|
|
})
|
|
|
|
// --- checkClaudeCli ---
|
|
|
|
describe('checkClaudeCli', () => {
|
|
it('returns not installed in non-Tauri environment', async () => {
|
|
const status = await checkClaudeCli()
|
|
expect(status.installed).toBe(false)
|
|
expect(status.version).toBeNull()
|
|
})
|
|
})
|
|
|
|
// --- streamClaudeChat ---
|
|
|
|
describe('streamClaudeChat', () => {
|
|
it('returns mock session in non-Tauri environment', async () => {
|
|
const onText = vi.fn()
|
|
const onDone = vi.fn()
|
|
const onError = vi.fn()
|
|
|
|
const sessionId = await streamClaudeChat('hello', undefined, undefined, {
|
|
onText,
|
|
onError,
|
|
onDone,
|
|
})
|
|
|
|
// Wait for the setTimeout mock response
|
|
await new Promise(r => setTimeout(r, 400))
|
|
|
|
expect(sessionId).toBe('mock-session')
|
|
expect(onText).toHaveBeenCalledWith(expect.stringContaining('Claude CLI'))
|
|
expect(onDone).toHaveBeenCalled()
|
|
expect(onError).not.toHaveBeenCalled()
|
|
})
|
|
})
|