"use client"; import { useTranslation } from "react-i18next"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select"; import { Switch } from "@kandev/ui/switch"; import { useUtilityAgents } from "@/hooks/domains/settings/use-utility-agents"; import type { PluginConfigField } from "@/lib/plugins/config-schema"; /** Sentinel for the "Not set" select item — an actual enum option can never * collide with it, and Radix rejects value="" items. */ const ENUM_UNSET_SENTINEL = "__kandev_enum_unset__"; const UTILITY_AGENT_UNSET_SENTINEL = "__kandev_utility_agent_unset__"; type PluginConfigFormProps = { fields: PluginConfigField[]; values: Record; initialValues: Record; disabled: boolean; onChange: (name: string, value: string | boolean) => void; }; /** * Schema-driven settings form: one control per config_schema property. * Secret fields render as password inputs pre-filled with the backend's * mask; editing replaces the value, leaving it untouched keeps the stored * secret. Purely controlled — load/save/dirty state lives in PluginDetail. */ export function PluginConfigForm({ fields, values, initialValues, disabled, onChange, }: PluginConfigFormProps) { return (
{fields.map((field) => ( ))}
); } type ConfigFieldRowProps = { field: PluginConfigField; value: string | boolean; isDirty: boolean; disabled: boolean; onChange: (name: string, value: string | boolean) => void; }; function ConfigFieldRow({ field, value, isDirty, disabled, onChange }: ConfigFieldRowProps) { // `field.label` and `field.description` come from the plugin's manifest — // third-party data, not our copy. const inputId = `plugin-config-${field.name}`; return (
{field.description &&

{field.description}

}
); } type ConfigFieldControlProps = ConfigFieldRowProps & { inputId: string }; function ConfigFieldControl({ field, inputId, value, isDirty, disabled, onChange, }: ConfigFieldControlProps) { const { t } = useTranslation(); if (field.type === "boolean") { return (
onChange(field.name, checked)} />
); } if (field.type === "enum") { // Radix Select forbids an item with value="", so an explicit "Not set" // sentinel lets optional enums be cleared back to unset (serialization // omits empty strings). return ( ); } if (field.type === "utility_agent") { return ( ); } return ( onChange(field.name, event.target.value)} /> ); } function UtilityAgentSelect({ field, inputId, value, isDirty, disabled, onChange, }: ConfigFieldControlProps) { const { t } = useTranslation(); const { agents, loading, error } = useUtilityAgents(); const selectedID = typeof value === "string" ? value : ""; const selectedAgent = agents.find((agent) => agent.id === selectedID); const selectedLabelKey = selectedUtilityAgentFallbackKey(selectedID, loading); const selectedLabel = selectedAgent?.name ?? (selectedLabelKey ? t(selectedLabelKey) : undefined); const placeholder = t(utilityAgentPlaceholderKey(loading, error)); return ( ); } // These two return catalog keys rather than messages: a plain function that // returns copy is invisible to the guard, and resolving at the call site keeps // the text following a locale switch. function selectedUtilityAgentFallbackKey(selectedID: string, loading: boolean): string | undefined { if (selectedID === "") return undefined; return loading ? "plugins:loadingSelectedUtilityAgent" : "plugins:selectedUtilityAgentUnavailable"; } function utilityAgentPlaceholderKey(loading: boolean, error: Error | null): string { if (loading) return "plugins:loadingUtilityAgents"; if (error) return "plugins:utilityAgentsUnavailable"; return "plugins:selectAUtilityAgent"; } function inputType(field: PluginConfigField): string { if (field.secret) return "password"; if (field.type === "number" || field.type === "integer") return "number"; return "text"; }