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.
|
- 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.
|
- Doctor: clarify plugin auto-enable hint text in the startup banner.
|
||||||
- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance.
|
- 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).
|
- 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: 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.
|
- 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 {
|
export class CustomEditor extends Editor {
|
||||||
onEscape?: () => void;
|
onEscape?: () => void;
|
||||||
@@ -12,8 +12,8 @@ export class CustomEditor extends Editor {
|
|||||||
onShiftTab?: () => void;
|
onShiftTab?: () => void;
|
||||||
onAltEnter?: () => void;
|
onAltEnter?: () => void;
|
||||||
|
|
||||||
constructor(theme: EditorTheme) {
|
constructor(tui: TUI, theme: EditorTheme) {
|
||||||
super(theme);
|
super(tui, theme);
|
||||||
}
|
}
|
||||||
handleInput(data: string): void {
|
handleInput(data: string): void {
|
||||||
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
|
if (matchesKey(data, Key.alt("enter")) && this.onAltEnter) {
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
const statusContainer = new Container();
|
const statusContainer = new Container();
|
||||||
const footer = new Text("", 1, 0);
|
const footer = new Text("", 1, 0);
|
||||||
const chatLog = new ChatLog();
|
const chatLog = new ChatLog();
|
||||||
const editor = new CustomEditor(editorTheme);
|
const editor = new CustomEditor(tui, editorTheme);
|
||||||
const root = new Container();
|
const root = new Container();
|
||||||
root.addChild(header);
|
root.addChild(header);
|
||||||
root.addChild(chatLog);
|
root.addChild(chatLog);
|
||||||
|
|||||||
@@ -43,19 +43,15 @@ export function renderNode(params: {
|
|||||||
unsupported: Set<string>;
|
unsupported: Set<string>;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
searchQuery?: string;
|
|
||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
}): TemplateResult | typeof nothing {
|
}): 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 showLabel = params.showLabel ?? true;
|
||||||
const type = schemaType(schema);
|
const type = schemaType(schema);
|
||||||
const hint = hintForPath(path, hints);
|
const hint = hintForPath(path, hints);
|
||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
const help = hint?.help ?? schema.description;
|
const help = hint?.help ?? schema.description;
|
||||||
const key = pathKey(path);
|
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)) {
|
if (unsupported.has(key)) {
|
||||||
return html`<div class="cfg-field cfg-field--error">
|
return html`<div class="cfg-field cfg-field--error">
|
||||||
@@ -109,7 +105,7 @@ export function renderNode(params: {
|
|||||||
|
|
||||||
if (allLiterals && literals.length > 5) {
|
if (allLiterals && literals.length > 5) {
|
||||||
// Use dropdown for larger sets
|
// 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
|
// Handle mixed primitive types
|
||||||
@@ -123,6 +119,14 @@ export function renderNode(params: {
|
|||||||
if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) {
|
if ([...normalizedTypes].every((v) => ["string", "number", "boolean"].includes(v as string))) {
|
||||||
const hasString = normalizedTypes.has("string");
|
const hasString = normalizedTypes.has("string");
|
||||||
const hasNumber = normalizedTypes.has("number");
|
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) {
|
if (hasString || hasNumber) {
|
||||||
return renderTextInput({
|
return renderTextInput({
|
||||||
@@ -157,7 +161,7 @@ export function renderNode(params: {
|
|||||||
</div>
|
</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
|
// Object type - collapsible section
|
||||||
@@ -227,7 +231,9 @@ function renderTextInput(params: {
|
|||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
const help = hint?.help ?? schema.description;
|
const help = hint?.help ?? schema.description;
|
||||||
const isSensitive = hint?.sensitive ?? isSensitivePath(path);
|
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 ?? "";
|
const displayValue = value ?? "";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -243,12 +249,16 @@ function renderTextInput(params: {
|
|||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
@input=${(e: Event) => {
|
@input=${(e: Event) => {
|
||||||
const raw = (e.target as HTMLInputElement).value;
|
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);
|
const parsed = Number(raw);
|
||||||
onPatch(path, Number.isNaN(parsed) ? raw : parsed);
|
onPatch(path, Number.isNaN(parsed) ? raw : parsed);
|
||||||
} else {
|
return;
|
||||||
onPatch(path, raw === "" ? undefined : raw);
|
|
||||||
}
|
}
|
||||||
|
onPatch(path, raw);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
${schema.default !== undefined ? html`
|
${schema.default !== undefined ? html`
|
||||||
@@ -317,12 +327,12 @@ function renderNumberInput(params: {
|
|||||||
|
|
||||||
function renderSelect(params: {
|
function renderSelect(params: {
|
||||||
schema: JsonSchema;
|
schema: JsonSchema;
|
||||||
value: string;
|
value: unknown;
|
||||||
path: Array<string | number>;
|
path: Array<string | number>;
|
||||||
hints: ConfigUiHints;
|
hints: ConfigUiHints;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
options: string[];
|
options: unknown[];
|
||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
}): TemplateResult {
|
}): TemplateResult {
|
||||||
const { schema, value, path, hints, disabled, options, onPatch } = params;
|
const { schema, value, path, hints, disabled, options, onPatch } = params;
|
||||||
@@ -330,6 +340,11 @@ function renderSelect(params: {
|
|||||||
const hint = hintForPath(path, hints);
|
const hint = hintForPath(path, hints);
|
||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
const help = hint?.help ?? schema.description;
|
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`
|
return html`
|
||||||
<div class="cfg-field">
|
<div class="cfg-field">
|
||||||
@@ -338,14 +353,15 @@ function renderSelect(params: {
|
|||||||
<select
|
<select
|
||||||
class="cfg-select"
|
class="cfg-select"
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
|
.value=${currentIndex >= 0 ? String(currentIndex) : unset}
|
||||||
@change=${(e: Event) => {
|
@change=${(e: Event) => {
|
||||||
const val = (e.target as HTMLSelectElement).value;
|
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>
|
<option value=${unset}>Select...</option>
|
||||||
${options.map(opt => html`
|
${options.map((opt, idx) => html`
|
||||||
<option value=${opt} ?selected=${opt === value}>${opt}</option>
|
<option value=${String(idx)}>${String(opt)}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,10 +376,9 @@ function renderObject(params: {
|
|||||||
unsupported: Set<string>;
|
unsupported: Set<string>;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
searchQuery?: string;
|
|
||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
}): TemplateResult {
|
}): 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 showLabel = params.showLabel ?? true;
|
||||||
const hint = hintForPath(path, hints);
|
const hint = hintForPath(path, hints);
|
||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
@@ -401,7 +416,6 @@ function renderObject(params: {
|
|||||||
unsupported,
|
unsupported,
|
||||||
disabled,
|
disabled,
|
||||||
onPatch,
|
onPatch,
|
||||||
searchQuery,
|
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
${allowExtra ? renderMapField({
|
${allowExtra ? renderMapField({
|
||||||
@@ -436,7 +450,6 @@ function renderObject(params: {
|
|||||||
unsupported,
|
unsupported,
|
||||||
disabled,
|
disabled,
|
||||||
onPatch,
|
onPatch,
|
||||||
searchQuery,
|
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
${allowExtra ? renderMapField({
|
${allowExtra ? renderMapField({
|
||||||
@@ -462,10 +475,9 @@ function renderArray(params: {
|
|||||||
unsupported: Set<string>;
|
unsupported: Set<string>;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
searchQuery?: string;
|
|
||||||
onPatch: (path: Array<string | number>, value: unknown) => void;
|
onPatch: (path: Array<string | number>, value: unknown) => void;
|
||||||
}): TemplateResult {
|
}): 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 showLabel = params.showLabel ?? true;
|
||||||
const hint = hintForPath(path, hints);
|
const hint = hintForPath(path, hints);
|
||||||
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));
|
||||||
@@ -537,7 +549,6 @@ function renderArray(params: {
|
|||||||
disabled,
|
disabled,
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
onPatch,
|
onPatch,
|
||||||
searchQuery,
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,16 +97,36 @@ function matchesSearch(key: string, schema: JsonSchema, query: string): boolean
|
|||||||
if (meta.description.toLowerCase().includes(q)) return true;
|
if (meta.description.toLowerCase().includes(q)) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check schema title/description
|
return schemaMatches(schema, q);
|
||||||
if (schema.title?.toLowerCase().includes(q)) return true;
|
}
|
||||||
if (schema.description?.toLowerCase().includes(q)) return true;
|
|
||||||
|
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;
|
||||||
|
|
||||||
// Deep search in properties
|
|
||||||
if (schema.properties) {
|
if (schema.properties) {
|
||||||
for (const [propKey, propSchema] of Object.entries(schema.properties)) {
|
for (const [propKey, propSchema] of Object.entries(schema.properties)) {
|
||||||
if (propKey.toLowerCase().includes(q)) return true;
|
if (propKey.toLowerCase().includes(query)) return true;
|
||||||
if (propSchema.title?.toLowerCase().includes(q)) return true;
|
if (schemaMatches(propSchema, query)) return true;
|
||||||
if (propSchema.description?.toLowerCase().includes(q)) 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +210,6 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||||||
disabled: props.disabled ?? false,
|
disabled: props.disabled ?? false,
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
onPatch: props.onPatch,
|
onPatch: props.onPatch,
|
||||||
searchQuery,
|
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -122,7 +122,13 @@ function computeDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function truncateValue(value: unknown, maxLen = 40): string {
|
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;
|
if (str.length <= maxLen) return str;
|
||||||
return str.slice(0, maxLen - 3) + "...";
|
return str.slice(0, maxLen - 3) + "...";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user