import { useCallback, useEffect, useRef, useState } from "react"; import { Loader2 } from "lucide-react"; import { fetchDroidCliStatus, setDroidCliEnabled, type DroidCliStatus, } from "../api"; import { ProviderIcon } from "./ProviderIcon"; import "./DroidCliProviderCard.css"; interface DroidCliProviderCardProps { authenticated: boolean; onToggled?: (nextEnabled: boolean) => void; compact?: boolean; } export function DroidCliProviderCard({ authenticated, onToggled, compact = false, }: DroidCliProviderCardProps) { 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); const mountedRef = useRef(true); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const refresh = useCallback(async () => { try { const next = await fetchDroidCliStatus(); 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; } }, []); 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 setDroidCliEnabled(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 droid CLI. Uses your existing Factory AI subscription / quota instead of an API key. ); const actions = ( <> {currentlyEnabled ? ( ) : ( )} ); if (compact) { return (
Factory AI — via Droid CLI
{actions}
Details
{description} {lastAction && }
); } return (
Factory AI — via Droid CLI {description}
{actions}
{lastAction && }
); } function DroidCliBadge({ status, authenticated, }: { status: DroidCliStatus | 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; } function DroidCliStatusLine({ status, authenticated, }: { status: DroidCliStatus | null; authenticated: boolean; }) { if (!status) { return ( Probing local CLI… ); } const { binary, enabled, extension, ready } = status; if (!binary.available) { return ( ✗ {binary.reason ?? "`droid` not found on PATH"} ); } if (!enabled) { return ( droid {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}` : ""} ); } return Enabled. Validating…; } function DroidCliActionToast({ 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" ? "Factory AI (via Droid CLI) models are now visible in the model picker." : "Factory AI (via Droid CLI) models are hidden from the model picker."} {action.restartRequired ? " Restart required: restart your active CLI/chat session for routing changes to take effect." : ""}

); }