"use client"; import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Label } from "@kandev/ui/label"; import { Input } from "@kandev/ui/input"; import { Switch } from "@kandev/ui/switch"; import { Button } from "@kandev/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { useAppStore } from "@/components/state-provider"; import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents"; import { ModelCombobox } from "@/components/settings/model-combobox"; import { ModeCombobox } from "@/components/settings/mode-combobox"; import { ModelFallbackFields } from "@/components/agent/cli-profile-fallback-fields"; import { CLIFlagsField } from "@/components/settings/cli-flags-field"; import { createAgentAction, createAgentProfileAction, updateAgentProfileAction, } from "@/app/actions/agents"; import { isHandledApiError } from "@/lib/api/client"; import type { Agent, AgentProfile, AvailableAgent, CLIFlag } from "@/lib/types/http"; import { seedDefaultCLIFlags } from "@/lib/cli-flags"; import { buildDefaultPermissions, PERMISSION_APPLY_AGENTCTL_AUTO_APPROVE, } from "@/lib/agent-permissions"; import { t } from "@/lib/i18n"; export type CliProfileEditorMode = "create" | "edit"; export type CliProfileEditorProps = { /** "create" mounts the inline create flow; "edit" patches an existing profile. */ mode: CliProfileEditorMode; /** When `mode === "edit"`, the existing profile to patch. */ profile?: AgentProfile; /** Default profile name suggestion in create mode. */ defaultProfileName?: string; /** Called once a profile is successfully saved. Receives the saved profile. */ onSaved: (profile: AgentProfile) => void; /** Called on cancel; only used by callers that wrap us in a dialog. */ onCancel?: () => void; /** * Show the advanced (permissions / passthrough) toggles. Defaults to * collapsed; the wizard hides them entirely, the configuration tab * exposes them inline. */ showAdvanced?: boolean; /** Whether the editor exposes CLI passthrough. Office agents should keep ACP enabled. */ allowCliPassthrough?: boolean; }; type FormState = { agentName: string; profileName: string; model: string; fallbackModel: string; autoFallback: boolean; requireExactModel: boolean; mode: string; cliFlags: CLIFlag[]; cliPassthrough: boolean; allowIndexing: boolean; autoApprove: boolean; }; function pickDefaultAgent(available: AvailableAgent[]): AvailableAgent | undefined { return available.find((a) => a.available) ?? available[0]; } function fromExistingProfile(profile: AgentProfile): FormState { return { agentName: profile.agentId ?? "", profileName: profile.name, model: profile.model ?? "", fallbackModel: profile.fallbackModel ?? "", autoFallback: profile.autoFallback ?? false, requireExactModel: profile.requireExactModel ?? false, mode: profile.mode ?? "", cliFlags: profile.cliFlags ?? [], cliPassthrough: profile.cliPassthrough ?? false, allowIndexing: profile.allowIndexing ?? false, autoApprove: profile.autoApprove ?? false, }; } function fromDefaultAgent( defaultName: string, defaultAgent: AvailableAgent | undefined, ): FormState { const cfg = defaultAgent?.model_config; const permissionSettings = defaultAgent?.permission_settings ?? {}; const defaults = buildDefaultPermissions(permissionSettings); return { agentName: defaultAgent?.name ?? "", profileName: defaultName, model: cfg?.default_model ?? "", fallbackModel: "", autoFallback: false, requireExactModel: false, mode: cfg?.current_mode_id ?? "", cliFlags: seedDefaultCLIFlags(permissionSettings), cliPassthrough: false, ...defaults, }; } function initialState( mode: CliProfileEditorMode, profile: AgentProfile | undefined, defaultName: string, defaultAgent: AvailableAgent | undefined, ): FormState { if (mode === "edit" && profile) return fromExistingProfile(profile); return fromDefaultAgent(defaultName, defaultAgent); } function ProfileNameField({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { const { t } = useTranslation(); return (
onChange(e.target.value)} placeholder={t("common:default")} className="mt-1" />
); } export function CliProfileEditor({ mode, profile, defaultProfileName = "default", onSaved, onCancel, showAdvanced = false, allowCliPassthrough = true, }: CliProfileEditorProps) { const availableAgents = useAvailableAgents(); const settingsAgents = useAppStore((s) => s.settingsAgents.items); const installed = useMemo( () => availableAgents.items.filter((a) => a.available), [availableAgents.items], ); const [form, setForm] = useState(() => initialState(mode, profile, defaultProfileName, pickDefaultAgent(installed)), ); const [saving, setSaving] = useState(false); const [advancedOpen, setAdvancedOpen] = useState(showAdvanced); const currentAgent = installed.find((a) => a.name === form.agentName); const modelConfig = currentAgent?.model_config; const permissionSettings = currentAgent?.permission_settings; const patch = useCallback((p: Partial) => { setForm((prev) => ({ ...prev, ...p })); }, []); const handleSave = useCallback(async () => { if (!form.agentName || !form.profileName.trim()) return; setSaving(true); try { const saved = mode === "edit" && profile ? await saveExistingProfile(profile.id, form) : await saveNewProfile(form, settingsAgents); onSaved(saved); } catch (error) { if (isHandledApiError(error)) return; throw error; } finally { setSaving(false); } }, [form, mode, profile, settingsAgents, onSaved]); const canSave = form.agentName !== "" && form.profileName.trim() !== ""; return (
{mode === "create" && ( patch({ agentName: name, model: installed.find((a) => a.name === name)?.model_config.default_model ?? "", // A fallback model belongs to the previous client's model // vocabulary; carrying it over would save a false gone-model // configuration for the new client. fallbackModel: "", mode: installed.find((a) => a.name === name)?.model_config.current_mode_id ?? "", cliFlags: seedDefaultCLIFlags( installed.find((a) => a.name === name)?.permission_settings ?? {}, ), }) } /> )} patch({ profileName })} /> setAdvancedOpen((o) => !o)} cliPassthrough={form.cliPassthrough} allowIndexing={form.allowIndexing} autoApprove={form.autoApprove} cliFlags={form.cliFlags} permissionSettings={permissionSettings ?? {}} allowCliPassthrough={allowCliPassthrough} showAllowIndexing={Boolean(permissionSettings?.allow_indexing?.supported)} onCliPassthroughChange={(v) => patch({ cliPassthrough: v })} onAllowIndexingChange={(v) => patch({ allowIndexing: v })} onAutoApproveChange={(v) => patch({ autoApprove: v })} onCliFlagsChange={(v) => patch({ cliFlags: v })} />
); } function buttonLabel(mode: CliProfileEditorMode, saving: boolean): string { if (saving) return mode === "edit" ? t("agents:savingProfile") : t("agents:creatingProfile"); return mode === "edit" ? t("agents:saveProfile") : t("agents:createProfile"); } function EditorFooter({ mode, saving, canSave, onSave, onCancel, }: { mode: CliProfileEditorMode; saving: boolean; canSave: boolean; onSave: () => void; onCancel?: () => void; }) { const { t } = useTranslation(); return (
{onCancel && ( )}
); } function CliClientPicker({ installed, value, onChange, }: { installed: AvailableAgent[]; value: string; onChange: (name: string) => void; }) { const { t } = useTranslation(); return (
{installed.length === 0 && (

{t("common:noCliClientsInstalledYetInstall")}

)}
); } // ModelModeFieldsBinding forwards the editor's form state to ModelModeFields. // Extracted so CliProfileEditor stays under the linter's line budget. function ModelModeFieldsBinding({ form, patch, modelConfig, }: { form: FormState; patch: (p: Partial) => void; modelConfig: NonNullable | null; }) { return ( patch({ model: v })} onFallbackModelChange={(v) => patch({ fallbackModel: v })} onAutoFallbackChange={(v) => patch({ autoFallback: v })} onRequireExactModelChange={(v) => patch({ requireExactModel: v })} onModeChange={(v) => patch({ mode: v })} /> ); } type ModelModeFieldsProps = { modelConfig: NonNullable | null; model: string; fallbackModel: string; autoFallback: boolean; requireExactModel: boolean; mode: string; onModelChange: (v: string) => void; onFallbackModelChange: (v: string) => void; onAutoFallbackChange: (v: boolean) => void; onRequireExactModelChange: (v: boolean) => void; onModeChange: (v: string) => void; }; function ModelModeFields({ modelConfig, model, fallbackModel, autoFallback, requireExactModel, mode, onModelChange, onFallbackModelChange, onAutoFallbackChange, onRequireExactModelChange, onModeChange, }: ModelModeFieldsProps) { const { t } = useTranslation(); if (!modelConfig) { return

{t("common:pickACliClientToLoad")}

; } const availableModels = modelConfig.available_models ?? []; const startModelGone = Boolean(model && !availableModels.some((m) => m.id === model)); const fallbackModelGone = Boolean( fallbackModel && !availableModels.some((m) => m.id === fallbackModel), ); return (
{(modelConfig.available_modes ?? []).length > 0 && (
)}
); } type AdvancedTogglesProps = { open: boolean; onToggle: () => void; cliPassthrough: boolean; allowIndexing: boolean; autoApprove: boolean; cliFlags: CLIFlag[]; permissionSettings: AvailableAgent["permission_settings"]; allowCliPassthrough: boolean; showAllowIndexing: boolean; onCliPassthroughChange: (v: boolean) => void; onAllowIndexingChange: (v: boolean) => void; onAutoApproveChange: (v: boolean) => void; onCliFlagsChange: (v: CLIFlag[]) => void; }; function AdvancedToggles({ open, onToggle, cliPassthrough, allowIndexing, autoApprove, cliFlags, permissionSettings, allowCliPassthrough, showAllowIndexing, onCliPassthroughChange, onAllowIndexingChange, onAutoApproveChange, onCliFlagsChange, }: AdvancedTogglesProps) { const { t } = useTranslation(); const autoSetting = permissionSettings?.auto_approve; const showAgentctlAutoApprove = Boolean( autoSetting?.supported && autoSetting.apply_method === PERMISSION_APPLY_AGENTCTL_AUTO_APPROVE, ); return (
{open && (
{allowCliPassthrough && ( )} {showAgentctlAutoApprove && autoSetting && ( )} {showAllowIndexing && ( )}
)}
); } function ToggleRow({ id, label, description, checked, onChange, }: { id: string; label: string; description: string; checked: boolean; onChange: (v: boolean) => void; }) { return (

{description}

); } function AgentctlAutoApproveRow({ id, setting, checked, onChange, }: { id: string; setting: NonNullable["auto_approve"]; checked: boolean; onChange: (v: boolean) => void; }) { const { t } = useTranslation(); return (

{setting?.description ?? t("common:kandevAllowsEveryAgentPermissionRequest")}

); } async function saveExistingProfile(id: string, form: FormState): Promise { return updateAgentProfileAction(id, { name: form.profileName.trim(), model: form.model, fallback_model: form.fallbackModel ?? "", auto_fallback: form.autoFallback, require_exact_model: form.requireExactModel, mode: form.mode || undefined, allow_indexing: form.allowIndexing, auto_approve: form.autoApprove, cli_flags: form.cliFlags, cli_passthrough: form.cliPassthrough, }); } async function saveNewProfile(form: FormState, settingsAgents: Agent[]): Promise { const existingAgent = settingsAgents.find((a) => a.name === form.agentName); const profilePayload = { name: form.profileName.trim(), model: form.model, fallback_model: form.fallbackModel ?? "", auto_fallback: form.autoFallback, require_exact_model: form.requireExactModel, mode: form.mode || undefined, allow_indexing: form.allowIndexing, auto_approve: form.autoApprove, cli_passthrough: form.cliPassthrough, cli_flags: form.cliFlags, }; if (existingAgent) { return createAgentProfileAction(existingAgent.id, profilePayload); } // Otherwise create the kanban Agent row + the first profile in one POST. const created = await createAgentAction({ name: form.agentName, profiles: [profilePayload], }); const newProfile = created.profiles.find((p) => p.name === profilePayload.name); if (!newProfile) { throw new Error( `Created agent ${form.agentName} but the response did not include the new profile.`, ); } return { ...newProfile, agentId: newProfile.agentId || created.id, agentDisplayName: newProfile.agentDisplayName || created.name, }; }