@tangle-network/ui 11.16.3 → 11.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/chat/agent-timeline.js +3 -2
- package/dist/chat/chat-message.js +2 -2
- package/dist/chat/thinking-indicator.js +1 -1
- package/dist/chat/user-message.js +2 -2
- package/dist/markdown/code-block.js +9 -1
- package/dist/openui/schema-compiled.js +108 -209
- package/dist/openui/schema-data.js +0 -1
- package/dist/openui-schema.js +4 -0
- package/dist/run/expandable-content.js +2 -2
- package/dist/run/expanded-tool-detail.js +3 -0
- package/package.json +1 -1
- package/src/chat/agent-timeline.test.tsx +0 -14
- package/src/chat/agent-timeline.tsx +18 -2
- package/src/chat/chat-message.tsx +2 -2
- package/src/chat/thinking-indicator.tsx +1 -1
- package/src/chat/user-message.tsx +2 -2
- package/src/markdown/code-block.tsx +14 -2
- package/src/openui/schema-compiled.ts +1 -1
- package/src/openui/schema-data.ts +5 -1
- package/src/openui/schema.test.tsx +45 -0
- package/src/openui/schema.ts +5 -0
- package/src/run/expandable-content.tsx +8 -1
- package/src/run/expanded-tool-detail.tsx +6 -2
|
@@ -7,7 +7,11 @@ const string = { type: "string" } as const;
|
|
|
7
7
|
const boolean = { type: "boolean" } as const;
|
|
8
8
|
const gap = { enum: ["sm", "md", "lg"] } as const;
|
|
9
9
|
const primitive = { type: ["string", "number", "boolean", "null"] } as const;
|
|
10
|
-
|
|
10
|
+
// No minItems here: stack and grid require `children` and a child can itself be
|
|
11
|
+
// a stack, so a nonempty minimum makes a required recursive loop that model
|
|
12
|
+
// tool-schema converters reject (opencode refused render_ui outright). The
|
|
13
|
+
// persistence gate in schema.ts still rejects an empty container.
|
|
14
|
+
const children = { type: "array", items: { $ref: "#/$defs/node" } } as const;
|
|
11
15
|
const actions = { type: "array", minItems: 1, items: { $ref: "#/$defs/action" } } as const;
|
|
12
16
|
|
|
13
17
|
function node(type: string, fields: Record<string, unknown>, required: string[] = []) {
|
|
@@ -24,6 +24,45 @@ const validPage = {
|
|
|
24
24
|
],
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
type JsonSchema = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Model tool-schema converters accept a recursive `$ref` only when the loop can
|
|
31
|
+
* terminate: it must pass through an optional property or an array that may be
|
|
32
|
+
* empty. Return the first `$defs` path that recurses through required,
|
|
33
|
+
* nonempty values only.
|
|
34
|
+
*/
|
|
35
|
+
function findRequiredRefLoop(defs: Record<string, JsonSchema>): string | null {
|
|
36
|
+
function visit(schema: JsonSchema, path: string, stack: string[]): string | null {
|
|
37
|
+
const ref = typeof schema.$ref === "string" ? schema.$ref.replace("#/$defs/", "") : null;
|
|
38
|
+
if (ref) {
|
|
39
|
+
if (stack.includes(ref)) return path;
|
|
40
|
+
return visit(defs[ref]!, `$defs.${ref}`, [...stack, ref]);
|
|
41
|
+
}
|
|
42
|
+
for (const key of ["oneOf", "anyOf"] as const) {
|
|
43
|
+
const options = schema[key];
|
|
44
|
+
if (Array.isArray(options)) {
|
|
45
|
+
for (const [index, option] of options.entries()) {
|
|
46
|
+
const loop = visit(option as JsonSchema, `${path}.${key}.${index}`, stack);
|
|
47
|
+
if (loop) return loop;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (schema.type === "array" && schema.items && Number(schema.minItems ?? 0) > 0) {
|
|
52
|
+
return visit(schema.items as JsonSchema, `${path}.items`, stack);
|
|
53
|
+
}
|
|
54
|
+
const required = Array.isArray(schema.required) ? schema.required as string[] : [];
|
|
55
|
+
const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;
|
|
56
|
+
for (const name of required) {
|
|
57
|
+
if (!properties[name]) continue;
|
|
58
|
+
const loop = visit(properties[name], `${path}.properties.${name}`, stack);
|
|
59
|
+
if (loop) return loop;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return visit({ $ref: "#/$defs/node" }, "$", []);
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
describe("OpenUI JSON contract", () => {
|
|
28
67
|
it("advertises every renderer node and accepts nested JSON with primitive cells", () => {
|
|
29
68
|
expect(OPENUI_NODE_TYPES).toHaveLength(13);
|
|
@@ -32,6 +71,12 @@ describe("OpenUI JSON contract", () => {
|
|
|
32
71
|
expect(validateOpenUIJsonArtifact([validPage, { type: "text", text: "Afterword" }])).toMatchObject({ ok: true });
|
|
33
72
|
});
|
|
34
73
|
|
|
74
|
+
it("keeps every recursive node path terminable for model tool-schema converters", () => {
|
|
75
|
+
expect(findRequiredRefLoop(OPENUI_NODE_JSON_SCHEMA.$defs as unknown as Record<string, JsonSchema>)).toBeNull();
|
|
76
|
+
expect(validateOpenUIJsonNode({ type: "card", title: "Empty", children: [] }))
|
|
77
|
+
.toMatchObject({ ok: false, issue: { path: "$.children" } });
|
|
78
|
+
});
|
|
79
|
+
|
|
35
80
|
it("rejects an unsupported top-level or nested section before it can be dropped", () => {
|
|
36
81
|
expect(validateOpenUIJsonNode({ type: "section", children: [{ type: "text", text: "hidden" }] }))
|
|
37
82
|
.toMatchObject({ ok: false, issue: { path: "$", message: expect.stringContaining("section") } });
|
package/src/openui/schema.ts
CHANGED
|
@@ -40,6 +40,11 @@ export function findUnsupportedOpenUINode(value: unknown): OpenUISchemaIssue | n
|
|
|
40
40
|
return { path, message: `Unsupported OpenUI node type: ${String(record.type)}.` };
|
|
41
41
|
}
|
|
42
42
|
if (record.type === "stack" || record.type === "grid" || record.type === "card") {
|
|
43
|
+
// The model-facing schema cannot express this minimum without a required
|
|
44
|
+
// recursive loop (see schema-data.ts), so the gate enforces it here.
|
|
45
|
+
if (Array.isArray(record.children) && record.children.length === 0) {
|
|
46
|
+
return { path: `${path}.children`, message: "OpenUI containers need at least one child." };
|
|
47
|
+
}
|
|
43
48
|
if (Array.isArray(record.children)) {
|
|
44
49
|
for (const [index, child] of record.children.entries()) {
|
|
45
50
|
const issue = visit(child, `${path}.children[${index}]`, depth + 1);
|
|
@@ -11,6 +11,9 @@ export interface ExpandableContentProps {
|
|
|
11
11
|
*/
|
|
12
12
|
fadeClassName?: string;
|
|
13
13
|
className?: string;
|
|
14
|
+
/** Classes for the Show more / Show less toggle, e.g. an inset when the
|
|
15
|
+
* content sits flush against its card's edges. */
|
|
16
|
+
toggleClassName?: string;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -23,6 +26,7 @@ export function ExpandableContent({
|
|
|
23
26
|
collapsedMaxPx = 180,
|
|
24
27
|
fadeClassName = "from-muted",
|
|
25
28
|
className,
|
|
29
|
+
toggleClassName,
|
|
26
30
|
}: ExpandableContentProps) {
|
|
27
31
|
const [expanded, setExpanded] = useState(false);
|
|
28
32
|
const [overflowing, setOverflowing] = useState(false);
|
|
@@ -63,7 +67,10 @@ export function ExpandableContent({
|
|
|
63
67
|
<button
|
|
64
68
|
type="button"
|
|
65
69
|
onClick={() => setExpanded((value) => !value)}
|
|
66
|
-
className=
|
|
70
|
+
className={cn(
|
|
71
|
+
"mt-1 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground",
|
|
72
|
+
toggleClassName,
|
|
73
|
+
)}
|
|
67
74
|
>
|
|
68
75
|
{expanded ? "Show less" : "Show more"}
|
|
69
76
|
</button>
|
|
@@ -23,6 +23,10 @@ export interface ExpandedToolDetailProps {
|
|
|
23
23
|
part: ToolPart;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// The clamped output sits flush against its card's edges; the toggle under it
|
|
27
|
+
// takes the card's 12px content inset so it does not touch the border.
|
|
28
|
+
const FLUSH_TOGGLE = "mx-3 mb-2";
|
|
29
|
+
|
|
26
30
|
const EXT_LANG: Record<string, string> = {
|
|
27
31
|
ts: "typescript", tsx: "tsx", js: "javascript", jsx: "jsx",
|
|
28
32
|
rs: "rust", py: "python", go: "go", rb: "ruby",
|
|
@@ -147,7 +151,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
|
|
|
147
151
|
Output
|
|
148
152
|
</span>
|
|
149
153
|
</div>
|
|
150
|
-
<ExpandableContent fadeClassName="from-card">
|
|
154
|
+
<ExpandableContent fadeClassName="from-card" toggleClassName={FLUSH_TOGGLE}>
|
|
151
155
|
<CodeBlock
|
|
152
156
|
code={
|
|
153
157
|
outputStr.length > 2000
|
|
@@ -170,7 +174,7 @@ export const ExpandedToolDetail = memo(({ part }: ExpandedToolDetailProps) => {
|
|
|
170
174
|
Error
|
|
171
175
|
</span>
|
|
172
176
|
</div>
|
|
173
|
-
<ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]">
|
|
177
|
+
<ExpandableContent fadeClassName="from-[var(--surface-danger-bg)]" toggleClassName={FLUSH_TOGGLE}>
|
|
174
178
|
<pre className="bg-[var(--surface-danger-bg)] p-3 text-xs font-mono whitespace-pre-wrap break-all text-[var(--surface-danger-text)]">
|
|
175
179
|
{error}
|
|
176
180
|
</pre>
|