mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-02 14:28:59 +02:00
[eric] tool-ui: string columns coerce instead of failing, validation runs per payload not per render, rowIdKey nags only when actionable
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { Suspense, useEffect, useState } from 'react';
|
||||
import React, { Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
import { TOOL_UI_REGISTRY } from './registry';
|
||||
|
||||
@@ -90,6 +90,8 @@ function VendoredToolUi({ name, props, extraProps, quietFail = false }: Vendored
|
||||
const { mode } = useThemeMode();
|
||||
const entry = TOOL_UI_REGISTRY[name];
|
||||
const [gate, setGate] = useState<Gate>({ state: 'pending' });
|
||||
// Parents rebuild the props object every render; keying the validation on identity re-ran an async zod parse per transcript render (real typing-lag cost in table-bearing chats). Content is the real dependency.
|
||||
const propsKey = useMemo(() => { try { return JSON.stringify(props); } catch { return String(Math.random()); } }, [props]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -101,12 +103,17 @@ function VendoredToolUi({ name, props, extraProps, quietFail = false }: Vendored
|
||||
})
|
||||
.catch(() => { if (!cancelled) setGate({ state: 'bad', problem: 'component failed to load' }); });
|
||||
return () => { cancelled = true; };
|
||||
}, [entry, props]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [entry, propsKey]);
|
||||
|
||||
const warnedRef = useRef<string | null>(null);
|
||||
if (!entry) return null;
|
||||
if (gate.state === 'bad') {
|
||||
// Schema jargon is for the console; the transcript gets one quiet human line.
|
||||
console.warn(`[tool-ui] ${name} payload didn't validate:`, gate.problem);
|
||||
// Schema jargon is for the console; the transcript gets one quiet human line. Once per payload, not per render.
|
||||
if (warnedRef.current !== propsKey) {
|
||||
warnedRef.current = propsKey;
|
||||
console.warn(`[tool-ui] ${name} payload didn't validate:`, gate.problem);
|
||||
}
|
||||
if (quietFail) return null;
|
||||
return (
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.45, padding: '4px 0', fontStyle: 'italic' }}>
|
||||
|
||||
@@ -651,7 +651,9 @@ function DataTableBody() {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasWarnedRowKeyRef.current) return;
|
||||
if (process.env.NODE_ENV !== "production" && !rowIdKey && data.length > 0) {
|
||||
// Only nag when the data actually CARRIES an id-like field the caller forgot to point at; model payloads usually have none, and the warning was pure console noise for them.
|
||||
const hasIdLikeField = data.length > 0 && ["id", "uuid", "key", "symbol"].some((k) => k in (data[0] as Record<string, unknown>));
|
||||
if (process.env.NODE_ENV !== "production" && !rowIdKey && hasIdLikeField) {
|
||||
hasWarnedRowKeyRef.current = true;
|
||||
console.warn(
|
||||
"[DataTable] Missing `rowIdKey` prop. Falling back to inferred/content-derived row keys. " +
|
||||
|
||||
@@ -79,18 +79,31 @@ const formatSchema = z.discriminatedUnion("kind", [
|
||||
}),
|
||||
]);
|
||||
|
||||
export const serializableColumnSchema = z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
abbr: z.string().optional(),
|
||||
sortable: z.boolean().optional(),
|
||||
align: AlignEnum.optional(),
|
||||
width: z.string().optional(),
|
||||
truncate: z.boolean().optional(),
|
||||
priority: PriorityEnum.optional(),
|
||||
hideOnMobile: z.boolean().optional(),
|
||||
format: formatSchema.optional(),
|
||||
});
|
||||
// Models routinely send columns as bare strings or as {key}-only / {label}-only objects; rejecting those threw away perfectly renderable tables, so coerce instead of failing.
|
||||
export const serializableColumnSchema = z.preprocess(
|
||||
(raw) => {
|
||||
if (typeof raw === "string") return { key: raw, label: raw };
|
||||
if (raw && typeof raw === "object") {
|
||||
const o = raw as Record<string, unknown>;
|
||||
const key = typeof o.key === "string" ? o.key : typeof o.label === "string" ? o.label : undefined;
|
||||
const label = typeof o.label === "string" ? o.label : typeof o.key === "string" ? o.key : undefined;
|
||||
if (key !== undefined || label !== undefined) return { ...o, key, label };
|
||||
}
|
||||
return raw;
|
||||
},
|
||||
z.object({
|
||||
key: z.string(),
|
||||
label: z.string(),
|
||||
abbr: z.string().optional(),
|
||||
sortable: z.boolean().optional(),
|
||||
align: AlignEnum.optional(),
|
||||
width: z.string().optional(),
|
||||
truncate: z.boolean().optional(),
|
||||
priority: PriorityEnum.optional(),
|
||||
hideOnMobile: z.boolean().optional(),
|
||||
format: formatSchema.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const JsonPrimitiveSchema = z.union([
|
||||
z.string(),
|
||||
|
||||
Reference in New Issue
Block a user