feat: replace Anthropic API with Claude CLI for AI chat and agent panels (#159)

* 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>
This commit is contained in:
Luca Rossi
2026-03-01 19:49:58 +01:00
committed by GitHub
parent 2a1b17f8c6
commit 894cb45779
18 changed files with 1221 additions and 1079 deletions

View File

@@ -2,11 +2,11 @@ import { useState, useRef, useEffect, useMemo } from 'react'
import type { VaultEntry } from '../types'
import {
X, Plus, PaperPlaneRight, Copy, ArrowClockwise,
TextIndent, Sparkle, Key, MagnifyingGlass, Minus,
TextIndent, Sparkle, MagnifyingGlass, Minus,
} from '@phosphor-icons/react'
import {
type ChatMessage, getApiKey, setApiKey,
buildSystemPrompt, MODEL_OPTIONS,
type ChatMessage,
buildSystemPrompt,
} from '../utils/ai-chat'
import { useAIChat } from '../hooks/useAIChat'
@@ -98,30 +98,6 @@ function ContextSearchDropdown({
)
}
function ApiKeyDialog({ onClose }: { onClose: () => void }) {
const [key, setKey] = useState(getApiKey())
return (
<div className="fixed inset-0 flex items-center justify-center z-50" style={{ background: 'rgba(0,0,0,0.4)' }}>
<div className="bg-background border border-border rounded-lg shadow-xl" style={{ width: 400, padding: 20 }}>
<h3 className="text-foreground" style={{ fontSize: 14, fontWeight: 600, margin: '0 0 12px' }}>Anthropic API Key</h3>
<p className="text-muted-foreground" style={{ fontSize: 12, margin: '0 0 12px', lineHeight: 1.5 }}>
Enter your Anthropic API key. Stored locally in your browser.
</p>
<input type="password" value={key} onChange={e => setKey(e.target.value)} placeholder="sk-ant-..."
className="w-full border border-border bg-transparent text-foreground rounded"
style={{ fontSize: 13, padding: '8px 10px', outline: 'none', marginBottom: 12 }} />
<div className="flex justify-end gap-2">
<button className="border border-border bg-transparent text-foreground rounded cursor-pointer hover:bg-accent"
style={{ fontSize: 12, padding: '6px 14px' }} onClick={onClose}>Cancel</button>
<button className="border-none rounded cursor-pointer"
style={{ fontSize: 12, padding: '6px 14px', background: 'var(--primary)', color: 'white' }}
onClick={() => { setApiKey(key); onClose() }}>Save</button>
</div>
</div>
</div>
)
}
function AssistantMessage({ msg, onRetry }: { msg: ChatMessage; onRetry: () => void }) {
return (
<div>
@@ -207,13 +183,11 @@ function useContextNotes(entry: VaultEntry | null) {
export function AIChatPanel({ entry, allContent, entries = [], onClose }: AIChatPanelProps) {
const [input, setInput] = useState('')
const [model, setModel] = useState<string>(MODEL_OPTIONS[0].value)
const [showSearch, setShowSearch] = useState(false)
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const ctx = useContextNotes(entry)
const chat = useAIChat(entry, allContent, ctx.contextNotes, model)
const chat = useAIChat(allContent, ctx.contextNotes)
const contextInfo = useMemo(
() => buildSystemPrompt(ctx.contextNotes, allContent),
@@ -232,7 +206,7 @@ export function AIChatPanel({ entry, allContent, entries = [], onClose }: AIChat
return (
<aside className="flex flex-1 flex-col overflow-hidden border-l border-border bg-background text-foreground">
<PanelHeader onApiKey={() => setShowApiKeyDialog(true)} onClear={chat.clearConversation} onClose={onClose} />
<PanelHeader onClear={chat.clearConversation} onClose={onClose} />
<ContextBar
notes={ctx.contextNotes} entries={entries} contextPaths={ctx.paths}
@@ -250,11 +224,9 @@ export function AIChatPanel({ entry, allContent, entries = [], onClose }: AIChat
<QuickActionsBar actions={QUICK_ACTIONS} disabled={chat.isStreaming}
onAction={msg => { chat.sendMessage(msg); setInput('') }} />
<InputArea model={model} onModelChange={setModel} input={input} onInputChange={setInput}
<InputArea input={input} onInputChange={setInput}
onKeyDown={handleKeyDown} onSend={handleSend} disabled={chat.isStreaming || !input.trim()} />
{showApiKeyDialog && <ApiKeyDialog onClose={() => setShowApiKeyDialog(false)} />}
<style>{`
.typing-dot {
width: 6px; height: 6px; border-radius: 50%;
@@ -272,15 +244,11 @@ export function AIChatPanel({ entry, allContent, entries = [], onClose }: AIChat
// --- Extracted layout sections ---
function PanelHeader({ onApiKey, onClear, onClose }: { onApiKey: () => void; onClear: () => void; onClose: () => void }) {
function PanelHeader({ onClear, onClose }: { onClear: () => void; onClose: () => void }) {
return (
<div className="flex shrink-0 items-center border-b border-border" style={{ height: 45, padding: '0 12px', gap: 8 }}>
<Sparkle size={16} className="shrink-0 text-muted-foreground" />
<span className="flex-1 text-muted-foreground" style={{ fontSize: 13, fontWeight: 600 }}>AI Chat</span>
<button className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={onApiKey} title="API Key settings">
<Key size={14} weight={getApiKey() ? 'fill' : 'regular'} />
</button>
<button className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={onClear} title="New conversation"><Plus size={16} /></button>
<button className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
@@ -332,9 +300,7 @@ function MessageList({
<div className="flex flex-col items-center justify-center text-center text-muted-foreground" style={{ paddingTop: 40 }}>
<Sparkle size={24} style={{ marginBottom: 8, opacity: 0.5 }} />
<p style={{ fontSize: 13, margin: '0 0 4px' }}>Ask anything about your notes</p>
<p style={{ fontSize: 11, margin: 0, opacity: 0.6 }}>
{getApiKey() ? 'Connected to Anthropic API' : 'Set API key for real AI responses'}
</p>
<p style={{ fontSize: 11, margin: 0, opacity: 0.6 }}>Powered by Claude CLI</p>
</div>
)}
{messages.map((msg, idx) => (
@@ -371,21 +337,13 @@ function QuickActionsBar({
}
function InputArea({
model, onModelChange, input, onInputChange, onKeyDown, onSend, disabled,
input, onInputChange, onKeyDown, onSend, disabled,
}: {
model: string; onModelChange: (m: string) => void
input: string; onInputChange: (v: string) => void
onKeyDown: (e: React.KeyboardEvent) => void; onSend: () => void; disabled: boolean
}) {
return (
<div className="flex shrink-0 flex-col border-t border-border" style={{ padding: '8px 12px' }}>
<div style={{ marginBottom: 6 }}>
<select value={model} onChange={e => onModelChange(e.target.value)}
className="border border-border bg-transparent text-muted-foreground"
style={{ fontSize: 11, borderRadius: 4, padding: '2px 6px', outline: 'none' }}>
{MODEL_OPTIONS.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
<div className="flex items-end gap-2">
<textarea value={input} onChange={e => onInputChange(e.target.value)} onKeyDown={onKeyDown}
placeholder="Ask about your notes..." rows={1}

View File

@@ -9,41 +9,28 @@ vi.mock('../hooks/useAiAgent', () => ({
status: 'idle',
sendMessage: vi.fn(),
clearConversation: vi.fn(),
canUndo: false,
undoLastRun: vi.fn(),
}),
}))
vi.mock('../utils/ai-agent', () => ({
AGENT_MODEL_OPTIONS: [
{ value: 'claude-3-5-haiku-20241022', label: 'Haiku (fast)' },
{ value: 'claude-sonnet-4-20250514', label: 'Sonnet (smart)' },
],
getAgentModel: () => 'claude-3-5-haiku-20241022',
setAgentModel: vi.fn(),
}))
vi.mock('../utils/ai-chat', () => ({
getApiKey: () => 'sk-test-key',
nextMessageId: () => `msg-${Date.now()}`,
}))
describe('AiPanel', () => {
it('renders panel with AI Agent header', () => {
render(<AiPanel onClose={vi.fn()} />)
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
expect(screen.getByText('AI Agent')).toBeTruthy()
})
it('renders data-testid ai-panel', () => {
render(<AiPanel onClose={vi.fn()} />)
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
expect(screen.getByTestId('ai-panel')).toBeTruthy()
})
it('calls onClose when close button is clicked', () => {
const onClose = vi.fn()
render(<AiPanel onClose={onClose} />)
render(<AiPanel onClose={onClose} vaultPath="/tmp/vault" />)
const panel = screen.getByTestId('ai-panel')
// Close button is the last button in the header
const buttons = panel.querySelectorAll('button')
const closeBtn = Array.from(buttons).find(b => b.title?.includes('Close'))
expect(closeBtn).toBeTruthy()
@@ -52,26 +39,19 @@ describe('AiPanel', () => {
})
it('renders empty state when no messages', () => {
render(<AiPanel onClose={vi.fn()} />)
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
expect(screen.getByText('Ask the AI agent to work with your vault')).toBeTruthy()
})
it('renders input field enabled', () => {
render(<AiPanel onClose={vi.fn()} />)
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
const input = screen.getByTestId('agent-input')
expect(input).toBeTruthy()
expect((input as HTMLInputElement).disabled).toBe(false)
})
it('renders model selector', () => {
render(<AiPanel onClose={vi.fn()} />)
const select = screen.getByTestId('agent-model-select')
expect(select).toBeTruthy()
expect((select as HTMLSelectElement).value).toBe('claude-3-5-haiku-20241022')
})
it('has send button disabled when input is empty', () => {
render(<AiPanel onClose={vi.fn()} />)
render(<AiPanel onClose={vi.fn()} vaultPath="/tmp/vault" />)
const sendBtn = screen.getByTestId('agent-send')
expect((sendBtn as HTMLButtonElement).disabled).toBe(true)
})

View File

@@ -2,14 +2,13 @@ import { useState, useRef, useEffect } from 'react'
import { Robot, X, PaperPlaneRight, Plus } from '@phosphor-icons/react'
import { AiMessage } from './AiMessage'
import { useAiAgent, type AiAgentMessage } from '../hooks/useAiAgent'
import { AGENT_MODEL_OPTIONS, getAgentModel, setAgentModel } from '../utils/ai-agent'
import { getApiKey } from '../utils/ai-chat'
export type { AiAgentMessage } from '../hooks/useAiAgent'
interface AiPanelProps {
onClose: () => void
onOpenNote?: (path: string) => void
vaultPath: string
}
function PanelHeader({ onClose, onClear }: { onClose: () => void; onClear: () => void }) {
@@ -32,7 +31,7 @@ function PanelHeader({ onClose, onClear }: { onClose: () => void; onClear: () =>
<button
className="shrink-0 border-none bg-transparent p-1 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={onClose}
title="Close AI panel (⌘I)"
title="Close AI panel"
>
<X size={16} />
</button>
@@ -41,7 +40,6 @@ function PanelHeader({ onClose, onClear }: { onClose: () => void; onClear: () =>
}
function EmptyState() {
const hasKey = !!getApiKey()
return (
<div
className="flex flex-col items-center justify-center text-center text-muted-foreground"
@@ -52,9 +50,7 @@ function EmptyState() {
Ask the AI agent to work with your vault
</p>
<p style={{ fontSize: 11, margin: 0, opacity: 0.6 }}>
{hasKey
? 'Creates notes, searches, edits frontmatter, and more'
: 'Set your Anthropic API key in Settings (⌘,)'}
Creates notes, searches, edits frontmatter, and more
</p>
</div>
)
@@ -80,10 +76,10 @@ function MessageHistory({ messages, isActive, onOpenNote }: {
)
}
function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, model, onModelChange }: {
function InputBar({ input, onInputChange, onSend, onKeyDown, isActive }: {
input: string; onInputChange: (v: string) => void
onSend: () => void; onKeyDown: (e: React.KeyboardEvent) => void
isActive: boolean; model: string; onModelChange: (m: string) => void
isActive: boolean
}) {
const sendDisabled = isActive || !input.trim()
return (
@@ -91,19 +87,6 @@ function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, model, on
className="flex shrink-0 flex-col border-t border-border"
style={{ padding: '8px 12px' }}
>
<div style={{ marginBottom: 6 }}>
<select
value={model}
onChange={e => onModelChange(e.target.value)}
className="border border-border bg-transparent text-muted-foreground"
style={{ fontSize: 11, borderRadius: 4, padding: '2px 6px', outline: 'none' }}
data-testid="agent-model-select"
>
{AGENT_MODEL_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div className="flex items-end gap-2">
<input
value={input}
@@ -138,18 +121,12 @@ function InputBar({ input, onInputChange, onSend, onKeyDown, isActive, model, on
)
}
export function AiPanel({ onClose, onOpenNote }: AiPanelProps) {
export function AiPanel({ onClose, onOpenNote, vaultPath }: AiPanelProps) {
const [input, setInput] = useState('')
const [model, setModel] = useState(getAgentModel)
const agent = useAiAgent()
const agent = useAiAgent(vaultPath)
const isActive = agent.status === 'thinking' || agent.status === 'tool-executing'
const handleModelChange = (m: string) => {
setModel(m)
setAgentModel(m)
}
const handleSend = () => {
if (!input.trim() || isActive) return
agent.sendMessage(input)
@@ -180,8 +157,6 @@ export function AiPanel({ onClose, onOpenNote }: AiPanelProps) {
onSend={handleSend}
onKeyDown={handleKeyDown}
isActive={isActive}
model={model}
onModelChange={handleModelChange}
/>
</aside>
)

View File

@@ -173,6 +173,7 @@ export const Editor = memo(function Editor({
entries={entries}
allContent={allContent}
gitHistory={gitHistory}
vaultPath={vaultPath ?? ''}
onToggleInspector={onToggleInspector}
onToggleAIChat={onToggleAIChat}
onNavigateWikilink={onNavigateWikilink}

View File

@@ -11,6 +11,7 @@ interface EditorRightPanelProps {
entries: VaultEntry[]
allContent: Record<string, string>
gitHistory: GitCommit[]
vaultPath: string
onToggleInspector: () => void
onToggleAIChat?: () => void
onNavigateWikilink: (target: string) => void
@@ -23,7 +24,7 @@ interface EditorRightPanelProps {
export function EditorRightPanel({
showAIChat, inspectorCollapsed, inspectorWidth,
inspectorEntry, inspectorContent, entries, allContent, gitHistory,
inspectorEntry, inspectorContent, entries, allContent, gitHistory, vaultPath,
onToggleInspector, onToggleAIChat, onNavigateWikilink, onViewCommitDiff,
onUpdateFrontmatter, onDeleteProperty, onAddProperty, onOpenNote,
}: EditorRightPanelProps) {
@@ -36,6 +37,7 @@ export function EditorRightPanel({
<AiPanel
onClose={() => onToggleAIChat?.()}
onOpenNote={onOpenNote}
vaultPath={vaultPath}
/>
</div>
)

View File

@@ -69,11 +69,10 @@ describe('SettingsPanel', () => {
expect(screen.getByText(/stored locally/)).toBeInTheDocument()
})
it('shows three key fields with labels', () => {
it('shows two key fields with labels', () => {
render(
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
expect(screen.getByText('Anthropic')).toBeInTheDocument()
expect(screen.getByText('OpenAI')).toBeInTheDocument()
expect(screen.getByText('Google AI')).toBeInTheDocument()
})
@@ -82,11 +81,9 @@ describe('SettingsPanel', () => {
render(
<SettingsPanel open={true} settings={populatedSettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
const anthropicInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement
const openaiInput = screen.getByTestId('settings-key-openai') as HTMLInputElement
const googleInput = screen.getByTestId('settings-key-google-ai') as HTMLInputElement
expect(anthropicInput.value).toBe('sk-ant-api03-test123')
expect(openaiInput.value).toBe('sk-openai-test456')
expect(googleInput.value).toBe('')
})
@@ -95,14 +92,14 @@ describe('SettingsPanel', () => {
render(
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
const anthropicInput = screen.getByTestId('settings-key-anthropic')
fireEvent.change(anthropicInput, { target: { value: ' sk-ant-test ' } })
const openaiInput = screen.getByTestId('settings-key-openai')
fireEvent.change(openaiInput, { target: { value: ' sk-openai-test ' } })
fireEvent.click(screen.getByTestId('settings-save'))
expect(onSave).toHaveBeenCalledWith({
anthropic_key: 'sk-ant-test',
openai_key: null,
anthropic_key: null,
openai_key: 'sk-openai-test',
google_key: null,
github_token: null,
github_username: null,
@@ -115,15 +112,15 @@ describe('SettingsPanel', () => {
render(
<SettingsPanel open={true} settings={populatedSettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
// Clear the anthropic key field
const anthropicInput = screen.getByTestId('settings-key-anthropic')
fireEvent.change(anthropicInput, { target: { value: ' ' } })
// Clear the openai key field
const openaiInput = screen.getByTestId('settings-key-openai')
fireEvent.change(openaiInput, { target: { value: ' ' } })
fireEvent.click(screen.getByTestId('settings-save'))
expect(onSave).toHaveBeenCalledWith({
anthropic_key: null,
openai_key: 'sk-openai-test456',
openai_key: null,
google_key: null,
github_token: null,
github_username: null,
@@ -159,13 +156,13 @@ describe('SettingsPanel', () => {
render(
<SettingsPanel open={true} settings={emptySettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
const anthropicInput = screen.getByTestId('settings-key-anthropic')
fireEvent.change(anthropicInput, { target: { value: 'sk-ant-test' } })
const openaiInput = screen.getByTestId('settings-key-openai')
fireEvent.change(openaiInput, { target: { value: 'sk-openai-test' } })
fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Enter', metaKey: true })
expect(onSave).toHaveBeenCalledWith({
anthropic_key: 'sk-ant-test',
openai_key: null,
anthropic_key: null,
openai_key: 'sk-openai-test',
google_key: null,
github_token: null,
github_username: null,
@@ -185,11 +182,11 @@ describe('SettingsPanel', () => {
render(
<SettingsPanel open={true} settings={populatedSettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
const clearBtn = screen.getByTestId('clear-anthropic')
const clearBtn = screen.getByTestId('clear-openai')
fireEvent.click(clearBtn)
const anthropicInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement
expect(anthropicInput.value).toBe('')
const openaiInput = screen.getByTestId('settings-key-openai') as HTMLInputElement
expect(openaiInput.value).toBe('')
})
it('shows keyboard shortcut hint in footer', () => {
@@ -204,18 +201,18 @@ describe('SettingsPanel', () => {
<SettingsPanel open={true} settings={populatedSettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
// Verify initial state
const anthropicInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement
expect(anthropicInput.value).toBe('sk-ant-api03-test123')
const openaiInput = screen.getByTestId('settings-key-openai') as HTMLInputElement
expect(openaiInput.value).toBe('sk-openai-test456')
// Close and reopen with different settings
rerender(
<SettingsPanel open={false} settings={populatedSettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
const newSettings: Settings = { ...emptySettings, anthropic_key: 'new-key' }
const newSettings: Settings = { ...emptySettings, openai_key: 'new-key' }
rerender(
<SettingsPanel open={true} settings={newSettings} onSave={onSave} onClose={onClose} themeManager={mockThemeManager} />
)
const updatedInput = screen.getByTestId('settings-key-anthropic') as HTMLInputElement
const updatedInput = screen.getByTestId('settings-key-openai') as HTMLInputElement
expect(updatedInput.value).toBe('new-key')
})

View File

@@ -121,7 +121,6 @@ export function SettingsPanel({ open, settings, onSave, onClose, themeManager }:
}
function SettingsPanelInner({ settings, onSave, onClose, themeManager }: Omit<SettingsPanelProps, 'open'>) {
const [anthropicKey, setAnthropicKey] = useState(settings.anthropic_key ?? '')
const [openaiKey, setOpenaiKey] = useState(settings.openai_key ?? '')
const [googleKey, setGoogleKey] = useState(settings.google_key ?? '')
const [githubToken, setGithubToken] = useState(settings.github_token)
@@ -139,13 +138,13 @@ function SettingsPanelInner({ settings, onSave, onClose, themeManager }: Omit<Se
}, [])
const buildSettings = useCallback((ghOverride?: { token: string | null; username: string | null }): Settings => ({
anthropic_key: anthropicKey.trim() || null,
anthropic_key: null,
openai_key: openaiKey.trim() || null,
google_key: googleKey.trim() || null,
github_token: ghOverride ? ghOverride.token : (githubToken ?? null),
github_username: ghOverride ? ghOverride.username : (githubUsername ?? null),
auto_pull_interval_minutes: pullInterval,
}), [anthropicKey, openaiKey, googleKey, githubToken, githubUsername, pullInterval])
}), [openaiKey, googleKey, githubToken, githubUsername, pullInterval])
const handleSave = () => {
onSave(buildSettings())
@@ -190,7 +189,6 @@ function SettingsPanelInner({ settings, onSave, onClose, themeManager }: Omit<Se
>
<SettingsHeader onClose={onClose} />
<SettingsBody
anthropicKey={anthropicKey} setAnthropicKey={setAnthropicKey}
openaiKey={openaiKey} setOpenaiKey={setOpenaiKey}
googleKey={googleKey} setGoogleKey={setGoogleKey}
githubToken={githubToken ?? null} githubUsername={githubUsername ?? null}
@@ -223,7 +221,6 @@ function SettingsHeader({ onClose }: { onClose: () => void }) {
}
interface SettingsBodyProps {
anthropicKey: string; setAnthropicKey: (v: string) => void
openaiKey: string; setOpenaiKey: (v: string) => void
googleKey: string; setGoogleKey: (v: string) => void
githubToken: string | null; githubUsername: string | null
@@ -243,7 +240,6 @@ function SettingsBody(props: SettingsBodyProps) {
</div>
</div>
<KeyField label="Anthropic" placeholder="sk-ant-..." value={props.anthropicKey} onChange={props.setAnthropicKey} onClear={() => props.setAnthropicKey('')} />
<KeyField label="OpenAI" placeholder="sk-..." value={props.openaiKey} onChange={props.setOpenaiKey} onClear={() => props.setOpenaiKey('')} />
<KeyField label="Google AI" placeholder="AIza..." value={props.googleKey} onChange={props.setGoogleKey} onClear={() => props.setGoogleKey('')} />