- Rust: add ThinkingDelta event to ClaudeStreamEvent for reasoning chunks - ai-agent.ts: forward ThinkingDelta events via onThinking callback - useAiAgent: stream reasoning live, accumulate response internally, reveal as complete block on done - AiMessage: auto-collapse reasoning when done, use MarkdownContent for response rendering, update tests for new behavior - ai-context: add buildContextSnapshot() for structured JSON context with activeNote, openTabs, noteListFilter, vault summary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
/**
|
|
* AI Agent utilities — Claude CLI agent mode with MCP vault tools.
|
|
*
|
|
* The Claude CLI handles the tool-use loop internally via MCP.
|
|
* The frontend receives streaming events for text, tool calls, and completion.
|
|
*/
|
|
|
|
import { isTauri } from '../mock-tauri'
|
|
|
|
// --- Agent system prompt ---
|
|
|
|
const AGENT_SYSTEM_PREAMBLE = `You are an AI assistant integrated into Laputa, a personal knowledge management app.
|
|
You can perform actions on the user's vault using the provided tools.
|
|
Be concise and helpful. When creating notes, use appropriate entity types and folder conventions.
|
|
When you've completed a task, briefly summarize what you did.`
|
|
|
|
export function buildAgentSystemPrompt(vaultContext?: string): string {
|
|
if (!vaultContext) return AGENT_SYSTEM_PREAMBLE
|
|
return `${AGENT_SYSTEM_PREAMBLE}\n\nVault context:\n${vaultContext}`
|
|
}
|
|
|
|
// --- Claude CLI agent streaming ---
|
|
|
|
type ClaudeAgentStreamEvent =
|
|
| { kind: 'Init'; session_id: string }
|
|
| { kind: 'TextDelta'; text: string }
|
|
| { kind: 'ThinkingDelta'; text: string }
|
|
| { kind: 'ToolStart'; tool_name: string; tool_id: string; input?: string }
|
|
| { kind: 'ToolDone'; tool_id: string; output?: string }
|
|
| { kind: 'Result'; text: string; session_id: string }
|
|
| { kind: 'Error'; message: string }
|
|
| { kind: 'Done' }
|
|
|
|
export interface AgentStreamCallbacks {
|
|
onText: (text: string) => void
|
|
onThinking: (text: string) => void
|
|
onToolStart: (toolName: string, toolId: string, input?: string) => void
|
|
onToolDone: (toolId: string, output?: string) => void
|
|
onError: (message: string) => void
|
|
onDone: () => void
|
|
}
|
|
|
|
/**
|
|
* Stream an agent task through the Claude CLI subprocess with MCP tools.
|
|
* The CLI handles the tool-use loop; we receive events for UI updates.
|
|
*/
|
|
export async function streamClaudeAgent(
|
|
message: string,
|
|
systemPrompt: string | undefined,
|
|
vaultPath: string,
|
|
callbacks: AgentStreamCallbacks,
|
|
): Promise<void> {
|
|
if (!isTauri()) {
|
|
setTimeout(() => {
|
|
callbacks.onText('AI Agent requires the Claude CLI. Install it and run the native app.')
|
|
callbacks.onDone()
|
|
}, 300)
|
|
return
|
|
}
|
|
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
const { listen } = await import('@tauri-apps/api/event')
|
|
|
|
const unlisten = await listen<ClaudeAgentStreamEvent>('claude-agent-stream', (event) => {
|
|
const data = event.payload
|
|
switch (data.kind) {
|
|
case 'TextDelta':
|
|
callbacks.onText(data.text)
|
|
break
|
|
case 'ThinkingDelta':
|
|
callbacks.onThinking(data.text)
|
|
break
|
|
case 'ToolStart':
|
|
callbacks.onToolStart(data.tool_name, data.tool_id, data.input)
|
|
break
|
|
case 'ToolDone':
|
|
callbacks.onToolDone(data.tool_id, data.output)
|
|
break
|
|
case 'Error':
|
|
callbacks.onError(data.message)
|
|
break
|
|
case 'Done':
|
|
callbacks.onDone()
|
|
break
|
|
}
|
|
})
|
|
|
|
try {
|
|
await invoke<string>('stream_claude_agent', {
|
|
request: {
|
|
message,
|
|
system_prompt: systemPrompt || null,
|
|
vault_path: vaultPath,
|
|
},
|
|
})
|
|
} catch (err) {
|
|
callbacks.onError(err instanceof Error ? err.message : String(err))
|
|
callbacks.onDone()
|
|
} finally {
|
|
unlisten()
|
|
}
|
|
}
|