@agentable-ui/genui 0.1.0 → 0.2.0
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/dist/index.d.ts +59 -3
- package/dist/index.js +230 -13
- package/package.json +6 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ComponentType, ReactNode } from "react";
|
|
2
2
|
import { JSONSchema, SchemaInput, Tier } from "@agentable-ui/core";
|
|
3
|
+
import { Library } from "@agentable-ui/lang";
|
|
3
4
|
//#region src/state.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* Surface state. Two roots: `data` is the app's, read-only to generated UI;
|
|
@@ -61,7 +62,12 @@ interface ToolAction {
|
|
|
61
62
|
args?: Record<string, unknown>;
|
|
62
63
|
into?: string;
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
/** Send text to the assistant (the surface's `onMessage`). */
|
|
66
|
+
interface MessageAction {
|
|
67
|
+
type: "message";
|
|
68
|
+
text: string;
|
|
69
|
+
}
|
|
70
|
+
type UIAction = StateAction | ToolAction | MessageAction;
|
|
65
71
|
interface UISpec {
|
|
66
72
|
component: string;
|
|
67
73
|
/** Stable id, needed to target the node with patch ops. */
|
|
@@ -113,6 +119,11 @@ interface ValidateOptions {
|
|
|
113
119
|
state?: SurfaceState;
|
|
114
120
|
/** Tool names that `tool` actions may call. None when absent. */
|
|
115
121
|
tools?: ReadonlySet<string>;
|
|
122
|
+
/**
|
|
123
|
+
* Drop nodes with issues instead of failing the whole spec. Used while a
|
|
124
|
+
* program streams in, where a half-formed node must not blank the rest.
|
|
125
|
+
*/
|
|
126
|
+
prune?: boolean;
|
|
116
127
|
}
|
|
117
128
|
export declare function isVisible(node: Pick<UISpec, "when">, state: SurfaceState): boolean;
|
|
118
129
|
/** Static props overlaid with bound values that currently resolve. */
|
|
@@ -127,6 +138,7 @@ export declare function validateSpec(spec: UISpec, catalog: Record<string, Valid
|
|
|
127
138
|
ok: true;
|
|
128
139
|
spec: UISpec;
|
|
129
140
|
nodes: number;
|
|
141
|
+
issues: SpecIssue[];
|
|
130
142
|
} | {
|
|
131
143
|
ok: false;
|
|
132
144
|
issues: SpecIssue[];
|
|
@@ -141,6 +153,46 @@ export declare function applyOps(root: UISpec, ops: SurfaceOp[]): {
|
|
|
141
153
|
state: StateAction[];
|
|
142
154
|
};
|
|
143
155
|
//#endregion
|
|
156
|
+
//#region src/renderer.d.ts
|
|
157
|
+
interface RendererError {
|
|
158
|
+
code: string;
|
|
159
|
+
message: string;
|
|
160
|
+
line?: number;
|
|
161
|
+
path?: string;
|
|
162
|
+
}
|
|
163
|
+
interface RendererProps {
|
|
164
|
+
/** Components the program may use. The library (signatures, prompt) is derived from it. */
|
|
165
|
+
components: Catalog;
|
|
166
|
+
/** Pass a prebuilt library to share it with prompt generation. */
|
|
167
|
+
library?: Library;
|
|
168
|
+
/** The program text received so far. */
|
|
169
|
+
response: string;
|
|
170
|
+
/** True while tokens are still arriving: partial lines are held back and the UI is inert. */
|
|
171
|
+
isStreaming?: boolean;
|
|
172
|
+
/** App data the program may bind to as $data.*. */
|
|
173
|
+
data?: Record<string, unknown>;
|
|
174
|
+
/** Functions @Tool may call by name. Names not here fall back to the Agentable registry when a provider is mounted. */
|
|
175
|
+
tools?: Record<string, (args: Record<string, unknown>) => unknown | Promise<unknown>>;
|
|
176
|
+
/** Text sent by @Say. */
|
|
177
|
+
onMessage?: (text: string, node: UISpec) => void;
|
|
178
|
+
/** Structured parse and validation errors; `[]` once clean. Warnings are left out. */
|
|
179
|
+
onError?: (errors: RendererError[]) => void;
|
|
180
|
+
/** An action failed or was declined. */
|
|
181
|
+
onActionError?: (message: string, node: UISpec) => void;
|
|
182
|
+
/** $ui.* state to start from. */
|
|
183
|
+
initialUi?: Record<string, unknown>;
|
|
184
|
+
onUiChange?: (ui: Record<string, unknown>) => void;
|
|
185
|
+
placeholder?: ReactNode;
|
|
186
|
+
className?: string;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Renders an Agentable Lang program as it streams. Each complete line is
|
|
190
|
+
* parsed, the tree is validated against the catalog with bad nodes pruned,
|
|
191
|
+
* and the rest draws immediately. Bindings, `when` and actions work exactly
|
|
192
|
+
* as in <AgentSurface>; tool actions go to `tools` or the registry.
|
|
193
|
+
*/
|
|
194
|
+
export declare function Renderer({ components, library, response, isStreaming, data, tools, onMessage, onError, onActionError, initialUi, onUiChange, placeholder, className }: RendererProps): import("react").JSX.Element;
|
|
195
|
+
//#endregion
|
|
144
196
|
//#region src/index.d.ts
|
|
145
197
|
export interface CatalogComponent<P = Record<string, unknown>> {
|
|
146
198
|
component: ComponentType<P>;
|
|
@@ -161,6 +213,8 @@ export type Catalog = Record<string, CatalogComponent<never>>;
|
|
|
161
213
|
export interface PresentArgs {
|
|
162
214
|
/** The tree to render. Replaces the current one and resets ui state. null clears the surface. */
|
|
163
215
|
root?: UISpec | null;
|
|
216
|
+
/** The tree as an Agentable Lang program (about half the tokens of `root`). */
|
|
217
|
+
program?: string;
|
|
164
218
|
/** Patch the current tree (applied after `root` when both are given). */
|
|
165
219
|
ops?: SurfaceOp[];
|
|
166
220
|
/** ui.* state to start from when sending `root`. */
|
|
@@ -205,6 +259,8 @@ export interface AgentSurfaceProps {
|
|
|
205
259
|
stateTool?: string | false;
|
|
206
260
|
/** Called when an action in the generated UI fails or is declined. */
|
|
207
261
|
onActionError?: (message: string, node: UISpec) => void;
|
|
262
|
+
/** Receives text a `@Say` / message action sends to the assistant. Without it the action fails. */
|
|
263
|
+
onMessage?: (text: string, node: UISpec) => void;
|
|
208
264
|
}
|
|
209
265
|
/**
|
|
210
266
|
* Renders whatever the agent last called `present` with. Every catalog
|
|
@@ -216,6 +272,6 @@ export interface AgentSurfaceProps {
|
|
|
216
272
|
* generated UI and an agent reach a tool through the same registry path, so
|
|
217
273
|
* confirmation, caching and invalidation apply to both.
|
|
218
274
|
*/
|
|
219
|
-
export declare function AgentSurface({ components, name, id, group, tier, priority, maxDepth, maxNodes, placeholder, onPresent, className, clearable, data, tools, stateTool, onActionError }: AgentSurfaceProps): import("react").JSX.Element;
|
|
275
|
+
export declare function AgentSurface({ components, name, id, group, tier, priority, maxDepth, maxNodes, placeholder, onPresent, className, clearable, data, tools, stateTool, onActionError, onMessage }: AgentSurfaceProps): import("react").JSX.Element;
|
|
220
276
|
//#endregion
|
|
221
|
-
export type { SpecIssue, StateAction, SurfaceOp, SurfaceState, ToolAction, UIAction, UISpec, UiStore, ValidateOptions };
|
|
277
|
+
export type { MessageAction, RendererError, RendererProps, SpecIssue, StateAction, SurfaceOp, SurfaceState, ToolAction, UIAction, UISpec, UiStore, ValidateOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Component, createElement, useCallback, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
1
|
+
import { Component, createElement, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
2
|
import { toJsonSchema, validateInput } from "@agentable-ui/core";
|
|
3
3
|
import { useAgentable, useAgentableSnapshot, useCapability } from "@agentable-ui/react";
|
|
4
|
+
import { createLibrary } from "@agentable-ui/lang";
|
|
4
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/state.ts
|
|
6
7
|
/** Keys that reach an object's prototype. Paths come from agents and user input, so they are never followed. */
|
|
@@ -192,9 +193,13 @@ function actionIssues(a, path, tools) {
|
|
|
192
193
|
});
|
|
193
194
|
return out;
|
|
194
195
|
}
|
|
196
|
+
if (a.type === "message") return typeof a.text === "string" ? [] : [{
|
|
197
|
+
path: `${path}.text`,
|
|
198
|
+
message: "message needs a text string."
|
|
199
|
+
}];
|
|
195
200
|
if (!STATE_ACTIONS.has(a.type)) return [{
|
|
196
201
|
path: `${path}.type`,
|
|
197
|
-
message: `Unknown action "${a.type}". Use set, merge, push, remove, increment, toggle or
|
|
202
|
+
message: `Unknown action "${a.type}". Use set, merge, push, remove, increment, toggle, tool or message.`
|
|
198
203
|
}];
|
|
199
204
|
if (!isStatePath(a.path, ["ui"])) return [{
|
|
200
205
|
path: `${path}.path`,
|
|
@@ -364,6 +369,7 @@ async function validateSpec(spec, catalog, allowed, opts = {}) {
|
|
|
364
369
|
if (Object.keys(on).length) out.on = on;
|
|
365
370
|
}
|
|
366
371
|
}
|
|
372
|
+
const ownBad = issues.length > before;
|
|
367
373
|
if (Array.isArray(node.children) && node.children.length) {
|
|
368
374
|
if (!entry.childrenProp) issues.push({
|
|
369
375
|
path: `${path}.children`,
|
|
@@ -378,17 +384,22 @@ async function validateSpec(spec, catalog, allowed, opts = {}) {
|
|
|
378
384
|
out.children = kids;
|
|
379
385
|
}
|
|
380
386
|
}
|
|
381
|
-
|
|
387
|
+
if (ownBad) {
|
|
388
|
+
nodes--;
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
382
392
|
}
|
|
383
393
|
const cleaned = await walk(spec, "root", 1, false);
|
|
384
|
-
if (issues.length
|
|
394
|
+
if (!cleaned || issues.length && !opts.prune) return {
|
|
385
395
|
ok: false,
|
|
386
396
|
issues
|
|
387
397
|
};
|
|
388
398
|
return {
|
|
389
399
|
ok: true,
|
|
390
400
|
spec: cleaned,
|
|
391
|
-
nodes
|
|
401
|
+
nodes,
|
|
402
|
+
issues
|
|
392
403
|
};
|
|
393
404
|
}
|
|
394
405
|
/**
|
|
@@ -465,6 +476,185 @@ function replaceIn(node, target, next) {
|
|
|
465
476
|
return kids.some((c) => replaceIn(c, target, next));
|
|
466
477
|
}
|
|
467
478
|
//#endregion
|
|
479
|
+
//#region src/renderer.tsx
|
|
480
|
+
/**
|
|
481
|
+
* Renders an Agentable Lang program as it streams. Each complete line is
|
|
482
|
+
* parsed, the tree is validated against the catalog with bad nodes pruned,
|
|
483
|
+
* and the rest draws immediately. Bindings, `when` and actions work exactly
|
|
484
|
+
* as in <AgentSurface>; tool actions go to `tools` or the registry.
|
|
485
|
+
*/
|
|
486
|
+
function Renderer({ components, library, response, isStreaming = false, data, tools, onMessage, onError, onActionError, initialUi, onUiChange, placeholder, className }) {
|
|
487
|
+
const lib = useMemo(() => library ?? createLibrary(components), [library, components]);
|
|
488
|
+
const [store] = useState(() => createUiStore(initialUi ?? {}));
|
|
489
|
+
const ui = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
490
|
+
const agentable = useAgentable();
|
|
491
|
+
const snapshot = useAgentableSnapshot();
|
|
492
|
+
const [spec, setSpec] = useState(null);
|
|
493
|
+
const [actionError, setActionError] = useState(null);
|
|
494
|
+
const dataRef = useRef(data ?? {});
|
|
495
|
+
dataRef.current = data ?? {};
|
|
496
|
+
const toolsRef = useRef(tools);
|
|
497
|
+
toolsRef.current = tools;
|
|
498
|
+
const onMessageRef = useRef(onMessage);
|
|
499
|
+
onMessageRef.current = onMessage;
|
|
500
|
+
const onErrorRef = useRef(onError);
|
|
501
|
+
onErrorRef.current = onError;
|
|
502
|
+
const onActionErrorRef = useRef(onActionError);
|
|
503
|
+
onActionErrorRef.current = onActionError;
|
|
504
|
+
const onUiChangeRef = useRef(onUiChange);
|
|
505
|
+
onUiChangeRef.current = onUiChange;
|
|
506
|
+
useEffect(() => store.subscribe(() => onUiChangeRef.current?.(store.get())), [store]);
|
|
507
|
+
const registryTools = useMemo(() => {
|
|
508
|
+
const out = /* @__PURE__ */ new Map();
|
|
509
|
+
for (const c of snapshot.capabilities) if (c.toolName && c.declaredTier <= 1 && !out.has(c.toolName)) out.set(c.toolName, c.id);
|
|
510
|
+
return out;
|
|
511
|
+
}, [snapshot.capabilities]);
|
|
512
|
+
const registryRef = useRef(registryTools);
|
|
513
|
+
registryRef.current = registryTools;
|
|
514
|
+
const callable = useMemo(() => /* @__PURE__ */ new Set([...Object.keys(tools ?? {}), ...registryTools.keys()]), [tools, registryTools]);
|
|
515
|
+
const parsed = useMemo(() => lib.parse(isStreaming ? heldBack(response) : response), [
|
|
516
|
+
lib,
|
|
517
|
+
response,
|
|
518
|
+
isStreaming
|
|
519
|
+
]);
|
|
520
|
+
useEffect(() => {
|
|
521
|
+
let live = true;
|
|
522
|
+
const allowed = new Set(Object.keys(components));
|
|
523
|
+
const errors = parsed.errors.filter((e) => e.severity === "error" && (e.code !== "no-root" || !isStreaming)).map(fromLang);
|
|
524
|
+
if (!parsed.root) {
|
|
525
|
+
setSpec(null);
|
|
526
|
+
onErrorRef.current?.(errors);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
validateSpec(parsed.root, components, allowed, {
|
|
530
|
+
state: {
|
|
531
|
+
data: dataRef.current,
|
|
532
|
+
ui: store.get()
|
|
533
|
+
},
|
|
534
|
+
tools: callable,
|
|
535
|
+
prune: true,
|
|
536
|
+
maxNodes: 200
|
|
537
|
+
}).then((r) => {
|
|
538
|
+
if (!live) return;
|
|
539
|
+
if (r.ok) {
|
|
540
|
+
setSpec(r.spec);
|
|
541
|
+
onErrorRef.current?.([...errors, ...r.issues.map(fromIssue)]);
|
|
542
|
+
} else {
|
|
543
|
+
setSpec(null);
|
|
544
|
+
onErrorRef.current?.([...errors, ...r.issues.map(fromIssue)]);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
return () => {
|
|
548
|
+
live = false;
|
|
549
|
+
};
|
|
550
|
+
}, [
|
|
551
|
+
parsed,
|
|
552
|
+
components,
|
|
553
|
+
callable,
|
|
554
|
+
isStreaming
|
|
555
|
+
]);
|
|
556
|
+
const fail = useCallback((message, node) => {
|
|
557
|
+
setActionError(message);
|
|
558
|
+
onActionErrorRef.current?.(message, node);
|
|
559
|
+
}, []);
|
|
560
|
+
const emit = useCallback((node, event, value) => {
|
|
561
|
+
const actions = node.on?.[event];
|
|
562
|
+
if (!actions?.length) return;
|
|
563
|
+
(async () => {
|
|
564
|
+
setActionError(null);
|
|
565
|
+
for (const raw of actions) {
|
|
566
|
+
const action = interpolate(raw, { value }, {
|
|
567
|
+
data: dataRef.current,
|
|
568
|
+
ui: store.get()
|
|
569
|
+
});
|
|
570
|
+
if (action.type === "message") {
|
|
571
|
+
if (!onMessageRef.current) return fail("Nothing is listening for messages here.", node);
|
|
572
|
+
onMessageRef.current(action.text, node);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (action.type !== "tool") {
|
|
576
|
+
try {
|
|
577
|
+
store.dispatch(action);
|
|
578
|
+
} catch (e) {
|
|
579
|
+
return fail(e instanceof Error ? e.message : String(e), node);
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
let result;
|
|
584
|
+
const fn = toolsRef.current?.[action.name];
|
|
585
|
+
try {
|
|
586
|
+
if (fn) result = await fn(action.args ?? {});
|
|
587
|
+
else {
|
|
588
|
+
const capId = registryRef.current.get(action.name);
|
|
589
|
+
if (!agentable || !capId) return fail(`Tool "${action.name}" is not available.`, node);
|
|
590
|
+
result = parseResult$1(await agentable.invokeCapability(capId, action.args ?? {}));
|
|
591
|
+
}
|
|
592
|
+
} catch (e) {
|
|
593
|
+
return fail(e instanceof Error ? e.message : String(e), node);
|
|
594
|
+
}
|
|
595
|
+
if (result && typeof result === "object" && "error" in result) return fail(String(result.error), node);
|
|
596
|
+
if (action.into) store.dispatch({
|
|
597
|
+
type: "set",
|
|
598
|
+
path: action.into,
|
|
599
|
+
value: result
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
})();
|
|
603
|
+
}, [
|
|
604
|
+
agentable,
|
|
605
|
+
store,
|
|
606
|
+
fail
|
|
607
|
+
]);
|
|
608
|
+
const ctx = useMemo(() => ({
|
|
609
|
+
state: {
|
|
610
|
+
data: data ?? {},
|
|
611
|
+
ui
|
|
612
|
+
},
|
|
613
|
+
emit
|
|
614
|
+
}), [
|
|
615
|
+
data,
|
|
616
|
+
ui,
|
|
617
|
+
emit
|
|
618
|
+
]);
|
|
619
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
620
|
+
className,
|
|
621
|
+
"data-agentable-surface": "",
|
|
622
|
+
"data-streaming": isStreaming ? "" : void 0,
|
|
623
|
+
"data-empty": spec ? void 0 : "",
|
|
624
|
+
children: [actionError ? /* @__PURE__ */ jsx("p", {
|
|
625
|
+
role: "alert",
|
|
626
|
+
"data-agentable-surface-error": "",
|
|
627
|
+
children: actionError
|
|
628
|
+
}) : null, spec ? /* @__PURE__ */ jsx("div", {
|
|
629
|
+
"data-agentable-surface-body": "",
|
|
630
|
+
inert: isStreaming || void 0,
|
|
631
|
+
children: renderSpec(spec, components, "0", ctx)
|
|
632
|
+
}) : placeholder ?? null]
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
/** While streaming, the last unterminated line is not parsed. */
|
|
636
|
+
function heldBack(text) {
|
|
637
|
+
const nl = text.lastIndexOf("\n");
|
|
638
|
+
return nl < 0 ? "" : text.slice(0, nl + 1);
|
|
639
|
+
}
|
|
640
|
+
const fromLang = (e) => ({
|
|
641
|
+
code: e.code,
|
|
642
|
+
message: e.message,
|
|
643
|
+
line: e.line
|
|
644
|
+
});
|
|
645
|
+
const fromIssue = (i) => ({
|
|
646
|
+
code: "invalid-node",
|
|
647
|
+
message: i.message,
|
|
648
|
+
path: i.path
|
|
649
|
+
});
|
|
650
|
+
function parseResult$1(text) {
|
|
651
|
+
try {
|
|
652
|
+
return JSON.parse(text);
|
|
653
|
+
} catch {
|
|
654
|
+
return text;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
468
658
|
//#region src/index.tsx
|
|
469
659
|
const ACTION_TYPES = [
|
|
470
660
|
"set",
|
|
@@ -583,6 +773,10 @@ function catalogSchema(catalog, allowed) {
|
|
|
583
773
|
anyOf: [{ $ref: "#/$defs/node" }, { type: "null" }],
|
|
584
774
|
description: "Tree to render; replaces the current one. null clears."
|
|
585
775
|
},
|
|
776
|
+
program: {
|
|
777
|
+
type: "string",
|
|
778
|
+
description: "The tree as a program, one node per line: `root = Stack([a, b])`, `a = Stat(\"Revenue\", \"$48k\")`. Same components; fewer tokens than root."
|
|
779
|
+
},
|
|
586
780
|
ops: {
|
|
587
781
|
type: "array",
|
|
588
782
|
items: { $ref: "#/$defs/op" },
|
|
@@ -630,7 +824,7 @@ function renderSpec(spec, catalog, key = "0", ctx) {
|
|
|
630
824
|
* generated UI and an agent reach a tool through the same registry path, so
|
|
631
825
|
* confirmation, caching and invalidation apply to both.
|
|
632
826
|
*/
|
|
633
|
-
function AgentSurface({ components, name = "present", id, group, tier, priority, maxDepth = 6, maxNodes = 60, placeholder, onPresent, className, clearable = true, data, tools, stateTool, onActionError }) {
|
|
827
|
+
function AgentSurface({ components, name = "present", id, group, tier, priority, maxDepth = 6, maxNodes = 60, placeholder, onPresent, className, clearable = true, data, tools, stateTool, onActionError, onMessage }) {
|
|
634
828
|
const [state, setState] = useState({
|
|
635
829
|
spec: null,
|
|
636
830
|
at: 0
|
|
@@ -644,8 +838,13 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
|
|
|
644
838
|
onPresentRef.current = onPresent;
|
|
645
839
|
const onActionErrorRef = useRef(onActionError);
|
|
646
840
|
onActionErrorRef.current = onActionError;
|
|
841
|
+
const onMessageRef = useRef(onMessage);
|
|
842
|
+
onMessageRef.current = onMessage;
|
|
647
843
|
const catalogRef = useRef(components);
|
|
648
844
|
catalogRef.current = components;
|
|
845
|
+
const library = useMemo(() => createLibrary(components), [components]);
|
|
846
|
+
const libraryRef = useRef(library);
|
|
847
|
+
libraryRef.current = library;
|
|
649
848
|
const specRef = useRef(state.spec);
|
|
650
849
|
specRef.current = state.spec;
|
|
651
850
|
const dataRef = useRef(data ?? {});
|
|
@@ -704,7 +903,7 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
|
|
|
704
903
|
const events = Object.keys(e.events ?? {});
|
|
705
904
|
return `${n}{${propSummary(schema)}}${e.childrenProp ? "+children" : ""}${events.length ? ` on:${events.join("|")}` : ""}`;
|
|
706
905
|
});
|
|
707
|
-
let text = "Render UI with this app's components: a
|
|
906
|
+
let text = "Render UI with this app's components: a program (one `name = Component(args)` line per node) or a {component, props, children} tree. Nodes bind props to state, run actions on events, and can be patched by id via ops.";
|
|
708
907
|
if (readName) text += ` Call ${readName} for bindable data.`;
|
|
709
908
|
text += ` Components: ${lines.join(" ")}`;
|
|
710
909
|
if (text.length > 500) text = text.slice(0, 497) + "...";
|
|
@@ -729,10 +928,23 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
|
|
|
729
928
|
onPresentRef.current?.(null);
|
|
730
929
|
return { cleared: true };
|
|
731
930
|
}
|
|
732
|
-
|
|
733
|
-
|
|
931
|
+
let root = args.root;
|
|
932
|
+
if (typeof args.program === "string" && root === void 0) {
|
|
933
|
+
const parsed = libraryRef.current.parse(args.program);
|
|
934
|
+
const errors = parsed.errors.filter((e) => e.severity === "error");
|
|
935
|
+
if (!parsed.root || errors.length) return {
|
|
936
|
+
error: "Invalid program.",
|
|
937
|
+
issues: parsed.errors.map((e) => ({
|
|
938
|
+
path: `line ${e.line}`,
|
|
939
|
+
message: e.message
|
|
940
|
+
}))
|
|
941
|
+
};
|
|
942
|
+
root = parsed.root;
|
|
943
|
+
}
|
|
944
|
+
if (root === void 0 && !args.ops?.length) return { error: "Send program or root to render, ops to patch, or root: null to clear." };
|
|
945
|
+
let tree = root ?? specRef.current;
|
|
734
946
|
if (!tree) return { error: "Nothing is presented yet; send root first." };
|
|
735
|
-
const draft = createUiStore(
|
|
947
|
+
const draft = createUiStore(root ? args.ui ?? {} : store.get());
|
|
736
948
|
try {
|
|
737
949
|
if (args.ops?.length) {
|
|
738
950
|
const patched = applyOps(tree, args.ops);
|
|
@@ -755,13 +967,13 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
|
|
|
755
967
|
error: "Invalid UI spec.",
|
|
756
968
|
issues: result.issues
|
|
757
969
|
};
|
|
758
|
-
const title = args.title ?? (
|
|
970
|
+
const title = args.title ?? (root ? void 0 : state.title);
|
|
759
971
|
store.replace(draft.get());
|
|
760
972
|
setError(null);
|
|
761
973
|
setState({
|
|
762
974
|
spec: result.spec,
|
|
763
975
|
title,
|
|
764
|
-
at:
|
|
976
|
+
at: root ? Date.now() : state.at
|
|
765
977
|
});
|
|
766
978
|
onPresentRef.current?.(result.spec, title);
|
|
767
979
|
return {
|
|
@@ -840,6 +1052,11 @@ function AgentSurface({ components, name = "present", id, group, tier, priority,
|
|
|
840
1052
|
data: dataRef.current,
|
|
841
1053
|
ui: store.get()
|
|
842
1054
|
});
|
|
1055
|
+
if (action.type === "message") {
|
|
1056
|
+
if (!onMessageRef.current) return fail("Nothing is listening for messages here.", node);
|
|
1057
|
+
onMessageRef.current(action.text, node);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
843
1060
|
if (action.type !== "tool") {
|
|
844
1061
|
try {
|
|
845
1062
|
store.dispatch(action);
|
|
@@ -944,4 +1161,4 @@ function shapeOf(value, depth = 0) {
|
|
|
944
1161
|
return typeof value;
|
|
945
1162
|
}
|
|
946
1163
|
//#endregion
|
|
947
|
-
export { AgentSurface, applyOps, catalogSchema, createUiStore, getPath, interpolate, isVisible, parsePath, renderSpec, resolveProps, validateSpec };
|
|
1164
|
+
export { AgentSurface, Renderer, applyOps, catalogSchema, createUiStore, getPath, interpolate, isVisible, parsePath, renderSpec, resolveProps, validateSpec };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentable-ui/genui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Generative UI for Agentable:
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Generative UI for Agentable: a streaming Renderer for Agentable Lang programs, AgentSurface with a single validated present tool, catalog schema generation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -34,8 +34,9 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@agentable-ui/core": "^0.1.
|
|
38
|
-
"@agentable-ui/react": "^0.1.0"
|
|
37
|
+
"@agentable-ui/core": "^0.1.1",
|
|
38
|
+
"@agentable-ui/react": "^0.1.0",
|
|
39
|
+
"@agentable-ui/lang": "^0.1.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"react": "^18.2.0 || ^19.0.0"
|
|
@@ -46,7 +47,7 @@
|
|
|
46
47
|
"@types/react": "^19.2.18",
|
|
47
48
|
"@types/react-dom": "^19.2.7",
|
|
48
49
|
"@testing-library/react": "^16.3.3",
|
|
49
|
-
"@agentable-ui/ui": "^0.1.
|
|
50
|
+
"@agentable-ui/ui": "^0.1.1"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"build": "tsdown",
|