feat(ui-models): schema-driven provider configuration page

The Models settings section joins llm.providers (the configurable
directory with live state), settings.describe (schemas, layered redacted
values, secret slots), and credentials.describe (value-free badges) into
provider rows with one editor card at a time. The editor renders the
provider's profile subtree through dsh-client-schema-form; the
credential-ref role mounts a control that shows configured/source state
and stores keys write-only through credentials.set. Apply without
removals merges a minimal patch (stored secrets outside it survive);
apply after a reset — and row deletion — replace the user section so
removals land. The client runtime bridges the three new host frames to
typed ctx events (settings/credentials/models changed), the page
refetches on any of them once loaded, and ui-model's per-session picker
directories reload on models/changed so a settings-born route appears in
open pickers without a reopen.
This commit is contained in:
Yichen Jiang
2026-07-30 00:45:55 +08:00
parent 9592c8f271
commit 686e40ebf6
21 changed files with 1750 additions and 27 deletions
+1 -1
View File
@@ -11,6 +11,6 @@ export type {
SchemaFieldContext, SchemaFormLabels, SchemaFormProps, SchemaFormSecret,
} from './SchemaForm.tsx'
export {
deletePath, getPath, hasPath, nodeKind, rehydrateSchema, setPath, unionChoices, validateDraft,
deletePath, getPath, hasPath, nodeAtPath, nodeKind, rehydrateSchema, setPath, unionChoices, validateDraft,
} from './model.ts'
export type { NodeKind, SchemaNode } from './model.ts'
+20
View File
@@ -78,6 +78,26 @@ export function unionChoices(node: SchemaNode): unknown[] {
return (node.list ?? []).map(branch => (branch as { value?: unknown }).value)
}
/**
* Resolve the schema node at a settings path (the configurable-provider
* directory's `settingsPath` vocabulary): object properties by name, dict
* entries through `inner`. An unresolvable segment returns `undefined` so
* the caller falls back instead of rendering a wrong subtree.
* @param root - rehydrated section root node.
* @param path - key path from the section root.
* @returns the node describing that position, or `undefined`.
*/
export function nodeAtPath(root: SchemaNode, path: readonly string[]): SchemaNode | undefined {
let node: SchemaNode | undefined = root
for (const key of path) {
if (node === undefined) return undefined
if (node.type === 'object') node = (node.dict as Record<string, SchemaNode> | undefined)?.[key]
else if (node.type === 'dict' || node.type === 'array') node = node.inner as SchemaNode | undefined
else return undefined
}
return node
}
/**
* Read a nested value by path.
* @param value - root value (draft or fallback layer).