fix: config form semantics + editor ctor (#1315) (thanks @MaudeBot)
This commit is contained in:
@@ -22,6 +22,7 @@ Docs: https://docs.clawd.bot
|
||||
- Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs.
|
||||
- Doctor: clarify plugin auto-enable hint text in the startup banner.
|
||||
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
|
||||
- UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315) — thanks @MaudeBot.
|
||||
- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter).
|
||||
- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) — thanks @aaronveklabs.
|
||||
- TUI: align custom editor initialization with the latest pi-tui API. (#1298) — thanks @sibbl.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Editor, type EditorTheme, Key, matchesKey } from "@mariozechner/pi-tui";
|
||||
import { Editor, type EditorTheme, Key, matchesKey, type TUI } from "@mariozechner/pi-tui";
|
||||
|
||||
export class CustomEditor extends Editor {
|
||||
onEscape?: () => void;
|
||||
@@ -12,8 +12,8 @@ export class CustomEditor extends Editor {
|
||||
onShiftTab?: () => void;
|
||||
onAltEnter?: () => void;
|
||||
|
||||
constructor(theme: EditorTheme) {
|
||||
super(theme);
|
||||
constructor(tui: TUI, theme: EditorTheme) {
|
||||
super(tui, theme);
|
||||
}
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function runTui(opts: TuiOptions) {
|
||||
const statusContainer = new Container();
|
||||
const footer = new Text("", 1, 0);
|
||||
const chatLog = new ChatLog();
|
||||
const editor = new CustomEditor(editorTheme);
|
||||
const editor = new CustomEditor(tui, editorTheme);
|
||||
const root = new Container();
|
||||
root.addChild(header);
|
||||
root.addChild(chatLog);
|
||||
|
||||
@@ -43,19 +43,15 @@ export function renderNode(params: {
|
||||
unsupported: Set<string>;
|
||||
disabled: boolean;
|
||||
showLabel?: boolean;
|
||||
searchQuery?: string;
|
||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
}): TemplateResult | typeof nothing {
|
||||
const { schema, value, path, hints, unsupported, disabled, onPatch, searchQuery } = params;
|
||||
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
||||
const showLabel = params.showLabel ?? true;
|
||||
const type = schemaType(schema);
|
||||
const hint = hintForPath(path, hints);
|
||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||
const help = hint?.help ?? schema.description;
|
||||
const key = pathKey(path);
|
||||
const hasDefault = schema.default !== undefined;
|
||||
const isDefault = hasDefault && JSON.stringify(value) === JSON.stringify(schema.default);
|
||||
const isEmpty = value === undefined || value === null || value === "";
|
||||
|
||||
if (unsupported.has(key)) {
|
||||
return html`<div class="cfg-field cfg-field--error">
|
||||
@@ -109,7 +105,7 @@ export function renderNode(params: {
|
||||
|
||||
if (allLiterals && literals.length > 5) {
|
||||
// Use dropdown for larger sets
|
||||
return renderSelect({ ...params, options: literals.map(String), value: String(value ?? schema.default ?? "") });
|
||||
return renderSelect({ ...params, options: literals, value: value ?? schema.default });
|
||||
}
|
||||
|
||||
// Handle mixed primitive types
|
||||
@@ -123,7 +119,15 @@ export function renderNode(params: {
|
||||
if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) {
|
||||
const hasString = normalizedTypes.has("string");
|
||||
const hasNumber = normalizedTypes.has("number");
|
||||
const hasBoolean = normalizedTypes.has("boolean");
|
||||
|
||||
if (hasBoolean && normalizedTypes.size === 1) {
|
||||
return renderNode({
|
||||
...params,
|
||||
schema: { ...schema, type: "boolean", anyOf: undefined, oneOf: undefined },
|
||||
});
|
||||
}
|
||||
|
||||
if (hasString || hasNumber) {
|
||||
return renderTextInput({
|
||||
...params,
|
||||
@@ -157,7 +161,7 @@ export function renderNode(params: {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return renderSelect({ ...params, options: options.map(String), value: String(value ?? schema.default ?? "") });
|
||||
return renderSelect({ ...params, options, value: value ?? schema.default });
|
||||
}
|
||||
|
||||
// Object type - collapsible section
|
||||
@@ -227,7 +231,9 @@ function renderTextInput(params: {
|
||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||
const help = hint?.help ?? schema.description;
|
||||
const isSensitive = hint?.sensitive ?? isSensitivePath(path);
|
||||
const placeholder = hint?.placeholder ?? (schema.default !== undefined ? `Default: ${schema.default}` : "");
|
||||
const placeholder =
|
||||
hint?.placeholder ??
|
||||
(isSensitive ? "••••" : schema.default !== undefined ? `Default: ${schema.default}` : "");
|
||||
const displayValue = value ?? "";
|
||||
|
||||
return html`
|
||||
@@ -243,12 +249,16 @@ function renderTextInput(params: {
|
||||
?disabled=${disabled}
|
||||
@input=${(e: Event) => {
|
||||
const raw = (e.target as HTMLInputElement).value;
|
||||
if (inputType === "number" && raw.trim() !== "") {
|
||||
if (inputType === "number") {
|
||||
if (raw.trim() === "") {
|
||||
onPatch(path, undefined);
|
||||
return;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
onPatch(path, Number.isNaN(parsed) ? raw : parsed);
|
||||
} else {
|
||||
onPatch(path, raw === "" ? undefined : raw);
|
||||
return;
|
||||
}
|
||||
onPatch(path, raw);
|
||||
}}
|
||||
/>
|
||||
${schema.default !== undefined ? html`
|
||||
@@ -317,12 +327,12 @@ function renderNumberInput(params: {
|
||||
|
||||
function renderSelect(params: {
|
||||
schema: JsonSchema;
|
||||
value: string;
|
||||
value: unknown;
|
||||
path: Array<string | number>;
|
||||
hints: ConfigUiHints;
|
||||
disabled: boolean;
|
||||
showLabel?: boolean;
|
||||
options: string[];
|
||||
options: unknown[];
|
||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
}): TemplateResult {
|
||||
const { schema, value, path, hints, disabled, options, onPatch } = params;
|
||||
@@ -330,6 +340,11 @@ function renderSelect(params: {
|
||||
const hint = hintForPath(path, hints);
|
||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||
const help = hint?.help ?? schema.description;
|
||||
const resolvedValue = value ?? schema.default;
|
||||
const currentIndex = options.findIndex(
|
||||
(opt) => opt === resolvedValue || String(opt) === String(resolvedValue),
|
||||
);
|
||||
const unset = "__unset__";
|
||||
|
||||
return html`
|
||||
<div class="cfg-field">
|
||||
@@ -338,14 +353,15 @@ function renderSelect(params: {
|
||||
<select
|
||||
class="cfg-select"
|
||||
?disabled=${disabled}
|
||||
.value=${currentIndex >= 0 ? String(currentIndex) : unset}
|
||||
@change=${(e: Event) => {
|
||||
const val = (e.target as HTMLSelectElement).value;
|
||||
onPatch(path, val === "" ? undefined : val);
|
||||
onPatch(path, val === unset ? undefined : options[Number(val)]);
|
||||
}}
|
||||
>
|
||||
<option value="" ?selected=${!value}>Select...</option>
|
||||
${options.map(opt => html`
|
||||
<option value=${opt} ?selected=${opt === value}>${opt}</option>
|
||||
<option value=${unset}>Select...</option>
|
||||
${options.map((opt, idx) => html`
|
||||
<option value=${String(idx)}>${String(opt)}</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -360,10 +376,9 @@ function renderObject(params: {
|
||||
unsupported: Set<string>;
|
||||
disabled: boolean;
|
||||
showLabel?: boolean;
|
||||
searchQuery?: string;
|
||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
}): TemplateResult {
|
||||
const { schema, value, path, hints, unsupported, disabled, onPatch, searchQuery } = params;
|
||||
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
||||
const showLabel = params.showLabel ?? true;
|
||||
const hint = hintForPath(path, hints);
|
||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||
@@ -401,7 +416,6 @@ function renderObject(params: {
|
||||
unsupported,
|
||||
disabled,
|
||||
onPatch,
|
||||
searchQuery,
|
||||
})
|
||||
)}
|
||||
${allowExtra ? renderMapField({
|
||||
@@ -436,7 +450,6 @@ function renderObject(params: {
|
||||
unsupported,
|
||||
disabled,
|
||||
onPatch,
|
||||
searchQuery,
|
||||
})
|
||||
)}
|
||||
${allowExtra ? renderMapField({
|
||||
@@ -462,10 +475,9 @@ function renderArray(params: {
|
||||
unsupported: Set<string>;
|
||||
disabled: boolean;
|
||||
showLabel?: boolean;
|
||||
searchQuery?: string;
|
||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||
}): TemplateResult {
|
||||
const { schema, value, path, hints, unsupported, disabled, onPatch, searchQuery } = params;
|
||||
const { schema, value, path, hints, unsupported, disabled, onPatch } = params;
|
||||
const showLabel = params.showLabel ?? true;
|
||||
const hint = hintForPath(path, hints);
|
||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||
@@ -537,7 +549,6 @@ function renderArray(params: {
|
||||
disabled,
|
||||
showLabel: false,
|
||||
onPatch,
|
||||
searchQuery,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,19 +97,39 @@ function matchesSearch(key: string, schema: JsonSchema, query: string): boolean
|
||||
if (meta.description.toLowerCase().includes(q)) return true;
|
||||
}
|
||||
|
||||
// Check schema title/description
|
||||
if (schema.title?.toLowerCase().includes(q)) return true;
|
||||
if (schema.description?.toLowerCase().includes(q)) return true;
|
||||
|
||||
// Deep search in properties
|
||||
return schemaMatches(schema, q);
|
||||
}
|
||||
|
||||
function schemaMatches(schema: JsonSchema, query: string): boolean {
|
||||
if (schema.title?.toLowerCase().includes(query)) return true;
|
||||
if (schema.description?.toLowerCase().includes(query)) return true;
|
||||
if (schema.enum?.some((value) => String(value).toLowerCase().includes(query))) return true;
|
||||
|
||||
if (schema.properties) {
|
||||
for (const [propKey, propSchema] of Object.entries(schema.properties)) {
|
||||
if (propKey.toLowerCase().includes(q)) return true;
|
||||
if (propSchema.title?.toLowerCase().includes(q)) return true;
|
||||
if (propSchema.description?.toLowerCase().includes(q)) return true;
|
||||
if (propKey.toLowerCase().includes(query)) return true;
|
||||
if (schemaMatches(propSchema, query)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (schema.items) {
|
||||
const items = Array.isArray(schema.items) ? schema.items : [schema.items];
|
||||
for (const item of items) {
|
||||
if (item && schemaMatches(item, query)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
||||
if (schemaMatches(schema.additionalProperties, query)) return true;
|
||||
}
|
||||
|
||||
const unions = schema.anyOf ?? schema.oneOf ?? schema.allOf;
|
||||
if (unions) {
|
||||
for (const entry of unions) {
|
||||
if (entry && schemaMatches(entry, query)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -190,7 +210,6 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
disabled: props.disabled ?? false,
|
||||
showLabel: false,
|
||||
onPatch: props.onPatch,
|
||||
searchQuery,
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -122,7 +122,13 @@ function computeDiff(
|
||||
}
|
||||
|
||||
function truncateValue(value: unknown, maxLen = 40): string {
|
||||
const str = JSON.stringify(value);
|
||||
let str: string;
|
||||
try {
|
||||
const json = JSON.stringify(value);
|
||||
str = json ?? String(value);
|
||||
} catch {
|
||||
str = String(value);
|
||||
}
|
||||
if (str.length <= maxLen) return str;
|
||||
return str.slice(0, maxLen - 3) + "...";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user