import React, { useEffect, useState } from "react"; import { ChevronDown, RefreshCw, RotateCcw } from "lucide-react"; import { useI18n } from "../../../../application/i18n/I18nProvider"; import { Button } from "../../../ui/button"; import { cn } from "../../../../lib/utils"; import type { AgentPathInfo } from "./types"; import type { CodebuddyAdvancedOptions } from "../../../../infrastructure/ai/types"; import { parseEnvLines, serializeEnvLines } from "./codebuddyConfigEnv"; const INTERNET_ENV_OPTIONS = [ { value: "", labelKey: "ai.codebuddy.internetEnv.default" }, { value: "internal", labelKey: "ai.codebuddy.internetEnv.internal" }, { value: "ioa", labelKey: "ai.codebuddy.internetEnv.ioa" }, ] as const; const EFFORT_OPTIONS = [ { value: "", labelKey: "ai.codebuddy.effort.default" }, { value: "low", labelKey: "ai.codebuddy.effort.low" }, { value: "medium", labelKey: "ai.codebuddy.effort.medium" }, { value: "high", labelKey: "ai.codebuddy.effort.high" }, { value: "xhigh", labelKey: "ai.codebuddy.effort.xhigh" }, ] as const; export const CodebuddyCard: React.FC<{ pathInfo: AgentPathInfo | null; isResolvingPath: boolean; customPath: string; onCustomPathChange: (path: string) => void; onRecheckPath: () => void; onResetPath: () => void; internetEnv: string; onInternetEnvChange: (value: string) => void; envText: string; onEnvTextChange: (value: string) => void; advancedOptions?: CodebuddyAdvancedOptions; onAdvancedOptionsChange?: (options: CodebuddyAdvancedOptions | undefined) => void; }> = ({ pathInfo, isResolvingPath, customPath, onCustomPathChange, onRecheckPath, onResetPath, internetEnv, onInternetEnvChange, envText, onEnvTextChange, advancedOptions, onAdvancedOptionsChange, }) => { const { t } = useI18n(); const found = pathInfo?.available; // Collapsed by default; auto-expand when the user already has config so it // isn't hidden. Local UI state — not persisted. const [configOpen, setConfigOpen] = useState( () => Boolean(internetEnv.trim() || envText.trim()), ); const [advancedOpen, setAdvancedOpen] = useState( () => Boolean(advancedOptions && Object.keys(advancedOptions).length > 0), ); const updateAdvanced = (patch: Partial) => { if (!onAdvancedOptionsChange) return; const next = { ...(advancedOptions || {}), ...patch }; // Remove undefined/empty values to keep storage clean. const cleaned = Object.fromEntries( Object.entries(next).filter(([, v]) => v != null && v !== "" && v !== 0), ) as CodebuddyAdvancedOptions; onAdvancedOptionsChange(Object.keys(cleaned).length > 0 ? cleaned : undefined); }; // The env editor keeps the raw text the user types. Persisting parses it into // a record (dropping incomplete lines), so binding the textarea directly to // the persisted value would erase a key the moment it's typed before its "=". // Only resync from the persisted value when it changes for some reason other // than our own parse→serialize round-trip. const [envDraft, setEnvDraft] = useState(envText); useEffect(() => { setEnvDraft((prev) => serializeEnvLines(parseEnvLines(prev)) === envText ? prev : envText, ); }, [envText]); const statusText = isResolvingPath ? t('ai.codebuddy.detecting') : found ? t('ai.codebuddy.detected') : t('ai.codebuddy.notFound'); const statusClassName = isResolvingPath ? "text-muted-foreground" : found ? "text-emerald-500" : "text-amber-500"; return (

{t('ai.codebuddy.description')}

{statusText}
{found && (
{t('ai.codebuddy.path')} {pathInfo.path} {pathInfo.version && ( <> | {pathInfo.version} )}
)} {!isResolvingPath && (
{!found && (

{t('ai.codebuddy.notFoundHint')}

)}
onCustomPathChange(e.target.value)} placeholder={t('ai.codebuddy.customPathPlaceholder')} className="flex-1 h-8 rounded-md border border-input bg-background px-3 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring" />
)} {/* Authentication & config (optional, collapsible) */}
{configOpen && (

{t('ai.codebuddy.internetEnv.hint')}