import { useEffect, useMemo, useRef, useState } from 'react'; import { Check, ChevronDown, RefreshCw, Shield, Brain, Bird, Bot, Dog, Terminal as TermIcon } from 'lucide-react'; import { ClaudeIcon, OpenCodeIcon } from '@/components/icons/ProviderIcons'; import { api } from '@/api/client'; import type { AgentSessionConfig, ProviderSnapshotEntry, AgentCommand } from '@/api/types'; import { useProviderSnapshot, refreshProviderSnapshot } from '@/hooks/useProviderSnapshot'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { BottomSheet } from '@/components/BottomSheet'; import { useViewport } from '@/hooks/useViewport'; import { cn } from '@/lib/utils'; const PREFS_KEY = 'boocode.coder.agent-prefs'; type ProviderPrefs = Record; function loadPrefs(): ProviderPrefs { try { const raw = localStorage.getItem(PREFS_KEY); return raw ? (JSON.parse(raw) as ProviderPrefs) : {}; } catch { return {}; } } function savePrefs(prefs: ProviderPrefs): void { localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)); } function defaultsForProvider(entry: ProviderSnapshotEntry): AgentSessionConfig { const model = entry.models.find((m) => m.isDefault)?.id ?? entry.models[0]?.id ?? ''; const selectedModel = entry.models.find((m) => m.id === model); const modeId = entry.defaultModeId ?? entry.modes[0]?.id ?? null; const thinkingOptionId = selectedModel?.defaultThinkingOptionId ?? selectedModel?.thinkingOptions?.find((t) => t.isDefault)?.id ?? selectedModel?.thinkingOptions?.[0]?.id ?? null; return { provider: entry.name, model, modeId, thinkingOptionId, }; } function resolveConfig( entry: ProviderSnapshotEntry, prefs: ProviderPrefs, ): AgentSessionConfig { const saved = prefs[entry.name]; const base = defaultsForProvider(entry); const model = saved?.model && entry.models.some((m) => m.id === saved.model) ? saved.model : base.model; const selectedModel = entry.models.find((m) => m.id === model); const modeId = saved?.modeId && entry.modes.some((m) => m.id === saved.modeId) ? saved.modeId : base.modeId; const thinkingOptions = selectedModel?.thinkingOptions ?? []; const thinkingOptionId = saved?.thinkingOptionId && thinkingOptions.some((t) => t.id === saved.thinkingOptionId) ? saved.thinkingOptionId : base.thinkingOptionId; return { provider: entry.name, model, modeId, thinkingOptionId }; } interface PickerProps { label: string; value: string; disabled?: boolean; options: Array<{ id: string; label: string }>; onPick: (id: string) => void; icon?: React.ReactNode; } function CompactPicker({ label, value, disabled, options, onPick, icon }: PickerProps) { const { isMobile } = useViewport(); const [open, setOpen] = useState(false); const currentLabel = options.find((o) => o.id === value)?.label ?? (value || label); const list = (
{options.map((o) => ( ))}
); if (isMobile) { return ( <> setOpen(false)} title={label}>
{list}
); } return ( {options.map((o) => ( onPick(o.id)} className="text-xs"> {o.label} ))} ); } interface Props { projectPath?: string; value: AgentSessionConfig; onChange: (next: AgentSessionConfig) => void; onProviderCommandsChange?: (commands: AgentCommand[]) => void; connected?: boolean; } export function AgentComposerBar({ projectPath, value, onChange, onProviderCommandsChange, connected }: Props) { const allEntries = useProviderSnapshot(projectPath); const entries = useMemo( () => allEntries?.filter((e) => e.installed && e.status !== 'error') ?? null, [allEntries], ); const [refreshing, setRefreshing] = useState(false); const hydratedRef = useRef(false); useEffect(() => { hydratedRef.current = false; }, [projectPath]); useEffect(() => { if (!entries?.length || hydratedRef.current) return; hydratedRef.current = true; const prefs = loadPrefs(); const entry = entries.find((e) => e.name === value.provider) ?? entries.find((e) => e.name === 'boocode') ?? entries[0]; if (!entry) return; onChange(resolveConfig(entry, prefs)); }, [entries, onChange, value.provider]); const currentEntry = useMemo( () => entries?.find((e) => e.name === value.provider), [entries, value.provider], ); const currentModel = useMemo( () => currentEntry?.models.find((m) => m.id === value.model), [currentEntry, value.model], ); const thinkingOptions = currentModel?.thinkingOptions ?? []; useEffect(() => { onProviderCommandsChange?.(currentEntry?.commands ?? []); }, [currentEntry, onProviderCommandsChange]); function persist(next: AgentSessionConfig): void { const prefs = loadPrefs(); prefs[next.provider] = { model: next.model, modeId: next.modeId, thinkingOptionId: next.thinkingOptionId, }; savePrefs(prefs); onChange(next); } function pickProvider(name: string): void { const entry = entries?.find((e) => e.name === name); if (!entry) return; persist(resolveConfig(entry, loadPrefs())); } function pickModel(model: string): void { const entry = currentEntry; if (!entry) return; const selected = entry.models.find((m) => m.id === model); const thinkingOptionId = selected?.defaultThinkingOptionId ?? selected?.thinkingOptions?.find((t) => t.isDefault)?.id ?? selected?.thinkingOptions?.[0]?.id ?? null; persist({ ...value, model, thinkingOptionId }); } async function handleRefresh(): Promise { setRefreshing(true); try { await api.coder.refreshProviders(); await refreshProviderSnapshot(projectPath); } finally { setRefreshing(false); } } if (!entries) { return (
Loading agents…
); } const providerIcon = (name: string) => { switch (name) { case 'claude': return ; case 'opencode': return ; case 'goose': return ; case 'qwen': return ; default: return ; } }; const providerOptions = entries.map((e) => ({ id: e.name, label: e.label })); const modeOptions = (currentEntry?.modes ?? []).map((m) => ({ id: m.id, label: m.label })); const modelOptions = (currentEntry?.models ?? []).map((m) => ({ id: m.id, label: m.label })); const thinkingOpts = thinkingOptions.map((t) => ({ id: t.id, label: t.label })); return (
persist({ ...value, modeId })} icon={} /> } /> {thinkingOpts.length > 0 && ( persist({ ...value, thinkingOptionId })} icon={} /> )} {connected !== undefined && ( )}
); }