// Control UI view renders config form.render screen content. import { html, nothing } from "lit"; import type { ConfigUiHints } from "../api/types.ts"; import { icons } from "../components/icons.ts"; import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts"; import { matchesNodeSearch, parseConfigSearchQuery, renderNode } from "./config-form.node.ts"; import { hintForPath, humanize, schemaType, type JsonSchema } from "./config-form.shared.ts"; export type ConfigFormProps = { schema: JsonSchema | null; uiHints: ConfigUiHints; value: Record | null; rawAvailable?: boolean; disabled?: boolean; unsupportedPaths?: string[]; searchQuery?: string; activeSection?: string | null; activeSubsection?: string | null; revealSensitive?: boolean; isSensitivePathRevealed?: (path: Array) => boolean; onToggleSensitivePath?: (path: Array) => void; onPatch: (path: Array, value: unknown) => void; }; // SVG Icons for section cards (Lucide-style) const sectionIcons = { env: html` `, update: html` `, agents: html` `, auth: html` `, channels: html` `, messages: html` `, commands: html` `, hooks: html` `, skills: html` `, tools: html` `, gateway: html` `, wizard: html` `, // Additional sections meta: html` `, logging: html` `, browser: html` `, ui: html` `, models: html` `, bindings: html` `, broadcast: html` `, audio: html` `, session: html` `, cron: html` `, web: html` `, discovery: html` `, canvasHost: html` `, talk: html` `, plugins: html` `, diagnostics: html` `, cli: html` `, secrets: html` `, acp: html` `, mcp: html` `, default: html` `, }; // Section metadata export const SECTION_META: Record = { env: { label: "Environment Variables", description: "Environment variables passed to the gateway process", }, update: { label: "Updates", description: "Auto-update settings and release channel" }, agents: { label: "Agents", description: "Agent configurations, models, and identities" }, auth: { label: "Authentication", description: "API keys and authentication profiles" }, channels: { label: "Channels", description: "Messaging channels (Telegram, Discord, Slack, etc.)", }, messages: { label: "Messages", description: "Message handling and routing settings" }, commands: { label: "Commands", description: "Custom slash commands" }, hooks: { label: "Hooks", description: "Webhooks and event hooks" }, skills: { label: "Skills", description: "Skill packs and capabilities" }, tools: { label: "Tools", description: "Tool configurations (browser, search, etc.)" }, gateway: { label: "Gateway", description: "Gateway server settings (port, auth, binding)" }, wizard: { label: "Setup Wizard", description: "Setup wizard state and history" }, // Additional sections meta: { label: "Metadata", description: "Gateway metadata and version information" }, logging: { label: "Logging", description: "Log levels and output configuration" }, browser: { label: "Browser", description: "Browser automation settings" }, ui: { label: "UI", description: "User interface preferences" }, models: { label: "Models", description: "AI model configurations and providers" }, bindings: { label: "Bindings", description: "Key bindings and shortcuts" }, broadcast: { label: "Broadcast", description: "Broadcast and notification settings" }, audio: { label: "Audio", description: "Audio input/output settings" }, session: { label: "Session", description: "Session management and persistence" }, cron: { label: "Cron", description: "Scheduled tasks and automation" }, web: { label: "Web", description: "Web server and API settings" }, discovery: { label: "Discovery", description: "Service discovery and networking" }, canvasHost: { label: "Canvas Host", description: "Canvas rendering and display" }, talk: { label: "Talk", description: "Voice and speech settings" }, plugins: { label: "Plugins", description: "Plugin management and extensions" }, diagnostics: { label: "Diagnostics", description: "Instrumentation, OpenTelemetry, and cache-trace settings", }, cli: { label: "CLI", description: "CLI banner and startup behavior" }, secrets: { label: "Secrets", description: "Secret provider configuration" }, acp: { label: "ACP", description: "Agent Communication Protocol runtime and streaming settings", }, mcp: { label: "MCP", description: "Model Context Protocol server definitions" }, }; function getSectionIcon(key: string) { return sectionIcons[key as keyof typeof sectionIcons] ?? sectionIcons.default; } function matchesSearch(params: { key: string; schema: JsonSchema; sectionValue: unknown; uiHints: ConfigUiHints; query: string; }): boolean { if (!params.query) { return true; } const criteria = parseConfigSearchQuery(params.query); const q = criteria.text; const meta = SECTION_META[params.key]; const sectionMetaMatches = q && (normalizeLowercaseStringOrEmpty(params.key).includes(q) || (meta?.label ? normalizeLowercaseStringOrEmpty(meta.label).includes(q) : false) || (meta?.description ? normalizeLowercaseStringOrEmpty(meta.description).includes(q) : false)); if (sectionMetaMatches && criteria.tags.length === 0) { return true; } return matchesNodeSearch({ schema: params.schema, value: params.sectionValue, path: [params.key], hints: params.uiHints, criteria, }); } export function renderConfigForm(props: ConfigFormProps) { if (!props.schema) { return html`
Schema unavailable.
`; } const schema = props.schema; const value = props.value ?? {}; if (schemaType(schema) !== "object" || !schema.properties) { return html`
Unsupported schema. Use Raw.
`; } const unsupported = new Set(props.unsupportedPaths ?? []); const properties = schema.properties; const searchQuery = props.searchQuery ?? ""; const searchCriteria = parseConfigSearchQuery(searchQuery); const activeSection = props.activeSection; const activeSubsection = props.activeSubsection ?? null; const entries = Object.entries(properties).toSorted((a, b) => { const orderA = hintForPath([a[0]], props.uiHints)?.order ?? 50; const orderB = hintForPath([b[0]], props.uiHints)?.order ?? 50; if (orderA !== orderB) { return orderA - orderB; } return a[0].localeCompare(b[0]); }); const filteredEntries = entries.filter(([key, node]) => { if (activeSection && key !== activeSection) { return false; } if ( searchQuery && !matchesSearch({ key, schema: node, sectionValue: value[key], uiHints: props.uiHints, query: searchQuery, }) ) { return false; } return true; }); let subsectionContext: { sectionKey: string; subsectionKey: string; schema: JsonSchema } | null = null; if (activeSection && activeSubsection && filteredEntries.length === 1) { const sectionSchema = filteredEntries[0]?.[1]; if ( sectionSchema && schemaType(sectionSchema) === "object" && sectionSchema.properties && sectionSchema.properties[activeSubsection] ) { subsectionContext = { sectionKey: activeSection, subsectionKey: activeSubsection, schema: sectionSchema.properties[activeSubsection], }; } } if (filteredEntries.length === 0) { return html`
${icons.search}
${searchQuery ? `No settings match "${searchQuery}"` : "No settings in this section"}
`; } const renderSectionCard = (params: { id: string; sectionKey: string; label: string; description: string; showHeader: boolean; node: JsonSchema; nodeValue: unknown; path: Array; }) => html`
${params.showHeader ? html`
${getSectionIcon(params.sectionKey)}

${params.label}

${params.description ? html`

${params.description}

` : nothing}
` : nothing}
${renderNode({ schema: params.node, value: params.nodeValue, path: params.path, hints: props.uiHints, rawAvailable: props.rawAvailable ?? true, unsupported, disabled: props.disabled ?? false, showLabel: false, searchCriteria, revealSensitive: props.revealSensitive ?? false, isSensitivePathRevealed: props.isSensitivePathRevealed, onToggleSensitivePath: props.onToggleSensitivePath, onPatch: props.onPatch, })}
`; return html`
${subsectionContext ? (() => { const { sectionKey, subsectionKey, schema: node } = subsectionContext; const hint = hintForPath([sectionKey, subsectionKey], props.uiHints); const label = hint?.label ?? node.title ?? humanize(subsectionKey); const description = hint?.help ?? node.description ?? ""; const sectionValue = value[sectionKey]; const scopedValue = sectionValue && typeof sectionValue === "object" ? (sectionValue as Record)[subsectionKey] : undefined; return renderSectionCard({ id: `config-section-${sectionKey}-${subsectionKey}`, sectionKey, label, description, showHeader: false, node, nodeValue: scopedValue, path: [sectionKey, subsectionKey], }); })() : filteredEntries.map(([key, node]) => { const meta = SECTION_META[key] ?? { label: key.charAt(0).toUpperCase() + key.slice(1), description: node.description ?? "", }; return renderSectionCard({ id: `config-section-${key}`, sectionKey: key, label: meta.label, description: meta.description, showHeader: activeSection == null, node, nodeValue: value[key], path: [key], }); })}
`; }