import { useCallback, useEffect, useRef, useState } from "react"; import { Loader2 } from "lucide-react"; import { fetchClaudeCliStatus, setClaudeCliEnabled, type ClaudeCliStatus, } from "../api"; import { ProviderIcon } from "./ProviderIcon"; /** * "Anthropic — via Claude CLI" provider card. * * Shown alongside the OAuth + API-key provider cards in onboarding and * settings. Wraps three actions: * * 1. **Test** — polls GET /providers/claude-cli/status to re-probe the * claude binary. Surfaces the binary path, version, and any reason * it's unreachable. * 2. **Enable / Disable** — POST /auth/claude-cli to flip * GlobalSettings.useClaudeCli. Refused server-side if the binary is * missing. On transition the server fires the same hook PUT * /settings/global fires, so skills get backfilled into every * registered project immediately. * 3. **Surface "restart required"** — pi extension registrations can't * be swapped mid-process, so the model-routing change only takes * effect on next Fusion restart. We show that explicitly rather * than letting users wonder why their model picker still shows * non-Anthropic entries right after clicking Enable. * * The card avoids rendering as "authenticated" on its own — that state * comes from the AuthProvider entry in the parent component's list so * every consumer (onboarding, settings) shows the same truth. */ interface ClaudeCliProviderCardProps { /** Authenticated flag from the parent AuthProvider entry. */ authenticated: boolean; /** Optional callback fired after Enable/Disable to let the parent refetch the provider list. */ onToggled?: (nextEnabled: boolean) => void; /** Render a smaller card with the description and status tucked behind a disclosure triangle. */ compact?: boolean; } export function ClaudeCliProviderCard({ authenticated, onToggled, compact = false, }: ClaudeCliProviderCardProps) { const [status, setStatus] = useState(null); const [busy, setBusy] = useState<"enabling" | "disabling" | "testing" | null>( null, ); const [lastAction, setLastAction] = useState< | { kind: "enabled"; restartRequired: boolean } | { kind: "disabled"; restartRequired: boolean } | { kind: "error"; message: string } | null >(null); // Guard against state updates after unmount — React complains otherwise. const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const refresh = useCallback(async () => { try { const next = await fetchClaudeCliStatus(); if (mountedRef.current) setStatus(next); return next; } catch (err) { if (mountedRef.current) { setLastAction({ kind: "error", message: err instanceof Error ? err.message : String(err), }); } return null; } }, []); // Initial probe — cheap, happens once per mount. useEffect(() => { void refresh(); }, [refresh]); const handleTest = useCallback(async () => { setBusy("testing"); setLastAction(null); await refresh(); if (mountedRef.current) setBusy(null); }, [refresh]); const handleToggle = useCallback( async (next: boolean) => { setBusy(next ? "enabling" : "disabling"); setLastAction(null); try { const result = await setClaudeCliEnabled(next); if (mountedRef.current) { setLastAction({ kind: result.enabled ? "enabled" : "disabled", restartRequired: result.restartRequired, }); } onToggled?.(result.enabled); await refresh(); } catch (err) { if (mountedRef.current) { setLastAction({ kind: "error", message: err instanceof Error ? err.message : String(err), }); } } finally { if (mountedRef.current) setBusy(null); } }, [onToggled, refresh], ); const binaryAvailable = status?.binary.available ?? false; const currentlyEnabled = status?.enabled ?? authenticated; const description = ( Route AI calls through your locally-installed claude CLI. Uses your existing Claude subscription / quota instead of an API key. ); const actions = ( <> {currentlyEnabled ? ( ) : ( )} ); // Compact layout mirrors `.auth-provider-card` so it slots cleanly into // the Settings > Authentication list and picks up the shared mobile rules. if (compact) { return (
Anthropic — via Claude CLI
{actions}
Details
{description} {lastAction && }
); } return (
Anthropic — via Claude CLI {description}
{actions}
{lastAction && }
); } function ClaudeCliBadge({ status, authenticated, }: { status: ClaudeCliStatus | null; authenticated: boolean; }) { const enabled = status?.enabled ?? authenticated; const available = status?.binary.available ?? false; if (enabled) { return ✓ Active; } if (!available && status) { return ✗ Not installed; } return ✗ Not connected; } /** * One-line health summary. Renders different text for "binary missing" * vs "binary ok but disabled" vs "fully ready" so the user can quickly * see why the provider is or isn't working. */ function ClaudeCliStatusLine({ status, authenticated, }: { status: ClaudeCliStatus | null; authenticated: boolean; }) { if (!status) { return ( Probing local CLI… ); } const { binary, enabled, extension, ready } = status; if (!binary.available) { return ( ✗ {binary.reason ?? "`claude` not found on PATH"} ); } if (!enabled) { return ( claude {binary.version ? `(${binary.version})` : ""} detected {binary.binaryPath ? ` at ${binary.binaryPath}` : ""}. Click Enable to route AI calls through it. ); } if (extension && extension.status !== "ok") { return ( ⚠ Extension load failed: {extension.reason ?? extension.status} ); } if (ready || authenticated) { return ( ✓ Connected{binary.version ? ` — ${binary.version}` : ""} ); } // Enabled but `ready` is false and we have no specific reason — usually a // transient state after flipping the toggle before the first probe // completes. return Enabled. Validating…; } function ClaudeCliActionToast({ action, }: { action: | { kind: "enabled"; restartRequired: boolean } | { kind: "disabled"; restartRequired: boolean } | { kind: "error"; message: string }; }) { if (action.kind === "error") { return (

{action.message}

); } const verb = action.kind === "enabled" ? "Enabled" : "Disabled"; return (

{verb}.{" "} {action.kind === "enabled" ? "Claude-CLI-routed models are now visible in the model picker." : "Claude-CLI-routed models are hidden from the model picker."}

); }