@artooi/ag-ui-web-component 0.25.1 → 0.26.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/CHANGELOG.md +62 -1
- package/README.md +69 -0
- package/dist/ag-ui-web-component.bundle.js +73 -29
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +19 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +12 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +713 -165
- package/dist/index.js.map +4 -4
- package/dist/tools/client_tool_registry.d.ts +24 -1
- package/dist/tools/client_tool_registry.d.ts.map +1 -1
- package/dist/ui/chart_block.d.ts +39 -0
- package/dist/ui/chart_block.d.ts.map +1 -0
- package/dist/ui/chart_spec_from.d.ts +12 -0
- package/dist/ui/chart_spec_from.d.ts.map +1 -0
- package/dist/ui/chart_tool.d.ts +15 -0
- package/dist/ui/chart_tool.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +11 -0
- package/src/core/ag_ui_chat.ts +159 -3
- package/src/core/agui_client.ts +51 -3
- package/src/index.ts +8 -0
- package/src/tools/client_tool_registry.ts +24 -1
- package/src/ui/chart_block.ts +349 -0
- package/src/ui/chart_spec_from.ts +106 -0
- package/src/ui/chart_tool.ts +64 -0
- package/src/ui/styles.ts +44 -0
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context, Interrupt, Message, Tool } from "@ag-ui/core";
|
|
2
2
|
import {
|
|
3
3
|
ATTACHMENT_EVENT,
|
|
4
|
+
CHART_ACTIVITY_TYPE,
|
|
4
5
|
COMPACTION_ACTIVITY_TYPE,
|
|
5
6
|
DEFAULT_ATTACHMENT_MAX_BYTES,
|
|
6
7
|
ICON_ATTACH,
|
|
@@ -39,6 +40,9 @@ import {
|
|
|
39
40
|
import { attachCopyButtons } from "../ui/attach_copy_buttons.js";
|
|
40
41
|
import { renderAttachmentChips } from "../ui/attachment_chips.js";
|
|
41
42
|
import { AttachmentTray } from "../ui/attachment_tray.js";
|
|
43
|
+
import { renderChart } from "../ui/chart_block.js";
|
|
44
|
+
import { chartSpecFrom } from "../ui/chart_spec_from.js";
|
|
45
|
+
import { createChartTool } from "../ui/chart_tool.js";
|
|
42
46
|
import { CheckpointMenu, type CheckpointVerb } from "../ui/checkpoint_menu.js";
|
|
43
47
|
import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
|
|
44
48
|
import { prettifyToolName } from "../ui/prettify_tool_name.js";
|
|
@@ -392,6 +396,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
392
396
|
* (`TOOL_CALL_RESULT`), so the post-run executeTool sweep doesn't overwrite
|
|
393
397
|
* the real output with the generic "executed on the server" fallback.
|
|
394
398
|
*/
|
|
399
|
+
/** Whether a server-pushed chart activity is drawn. Off unless asked for. */
|
|
400
|
+
#chartActivity = false;
|
|
401
|
+
|
|
402
|
+
/** Card elements by call id, so a rendering handler can find its own card. */
|
|
403
|
+
readonly #cardElements = new Map<string, HTMLElement>();
|
|
404
|
+
|
|
405
|
+
/** Chart blocks by activity message id, so an update redraws in place. */
|
|
406
|
+
readonly #activityBlocks = new Map<string, HTMLElement>();
|
|
407
|
+
|
|
395
408
|
readonly #serverSettled = new Set<string>();
|
|
396
409
|
/**
|
|
397
410
|
* Tool calls made during the current interaction, in the order they started,
|
|
@@ -1662,6 +1675,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
1662
1675
|
this.#hidePending();
|
|
1663
1676
|
this.#toolCards.clear();
|
|
1664
1677
|
this.#serverSettled.clear();
|
|
1678
|
+
this.#cardElements.clear();
|
|
1679
|
+
this.#activityBlocks.clear();
|
|
1665
1680
|
this.#initialMessages = [];
|
|
1666
1681
|
this.#attachTray?.clear();
|
|
1667
1682
|
// Keep the empty-state region; everything else clears.
|
|
@@ -1822,7 +1837,23 @@ export class AgUiChat extends HTMLElement {
|
|
|
1822
1837
|
if (this.#noticeIfSkillLoad(restored)) {
|
|
1823
1838
|
continue;
|
|
1824
1839
|
}
|
|
1825
|
-
this.#cardFor(restored);
|
|
1840
|
+
this.#cardElements.set(restored.id, this.#cardFor(restored).element);
|
|
1841
|
+
// Only `render` is replayed, never `handler`. A restored transcript
|
|
1842
|
+
// redraws what the call drew; it must not re-run what the call *did*.
|
|
1843
|
+
const tool = this.#resolveTool(restored.name);
|
|
1844
|
+
if (tool !== null) {
|
|
1845
|
+
this.#renderToolOutput(tool, restored);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (message.role === "activity") {
|
|
1851
|
+
// The client materialises a pushed activity as a message of its own, so a
|
|
1852
|
+
// chart's data is in the transcript already and survives a reload. Only
|
|
1853
|
+
// the drawing had to be put back.
|
|
1854
|
+
const activity = message as unknown as { activityType?: unknown; content?: unknown };
|
|
1855
|
+
if (activity.activityType === CHART_ACTIVITY_TYPE && this.#chartActivity) {
|
|
1856
|
+
this.#drawActivityChart(message.id, activity.content);
|
|
1826
1857
|
}
|
|
1827
1858
|
return;
|
|
1828
1859
|
}
|
|
@@ -2539,6 +2570,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
2539
2570
|
}
|
|
2540
2571
|
const card = this.#cardFor(call);
|
|
2541
2572
|
this.#toolCards.delete(call.id);
|
|
2573
|
+
// Kept after the card leaves `#toolCards`: a tool that renders into the
|
|
2574
|
+
// transcript places itself against its own card, and by the time it runs the
|
|
2575
|
+
// card is no longer reachable by id.
|
|
2576
|
+
this.#cardElements.set(call.id, card.element);
|
|
2542
2577
|
const tool = this.#resolveTool(call.name);
|
|
2543
2578
|
if (tool === null) {
|
|
2544
2579
|
// Not a client tool. A server-side tool's real output arrives via
|
|
@@ -2613,7 +2648,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
2613
2648
|
this.conversationStore.saveCheckpoint(this.#threadId, { toolCallId: call.id });
|
|
2614
2649
|
}
|
|
2615
2650
|
try {
|
|
2616
|
-
|
|
2651
|
+
// The call id lets a handler that renders into the transcript find its
|
|
2652
|
+
// own card; handlers that only act on the page ignore it.
|
|
2653
|
+
const result = await tool.handler(call.args, call.id);
|
|
2654
|
+
// Drawn from the arguments rather than the result, so the live path and
|
|
2655
|
+
// the replay path render the same thing from the same input.
|
|
2656
|
+
this.#renderToolOutput(tool, call);
|
|
2617
2657
|
if (navigates) {
|
|
2618
2658
|
card.settle(TOOL_CALL_STATUS.DONE, this.#strings.navigating);
|
|
2619
2659
|
return { content: "", halt: true };
|
|
@@ -2775,7 +2815,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
2775
2815
|
this.#runTools.push({ id: call.id, name: call.name });
|
|
2776
2816
|
this.#cardFor(call);
|
|
2777
2817
|
},
|
|
2778
|
-
onActivity: (activityType, content) => {
|
|
2818
|
+
onActivity: (activityType, content, messageId) => {
|
|
2819
|
+
if (activityType === CHART_ACTIVITY_TYPE) {
|
|
2820
|
+
if (this.#chartActivity) {
|
|
2821
|
+
this.#drawActivityChart(messageId, content);
|
|
2822
|
+
}
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2779
2825
|
if (activityType !== COMPACTION_ACTIVITY_TYPE) {
|
|
2780
2826
|
return;
|
|
2781
2827
|
}
|
|
@@ -2796,6 +2842,25 @@ export class AgUiChat extends HTMLElement {
|
|
|
2796
2842
|
}
|
|
2797
2843
|
card.settle(TOOL_CALL_STATUS.DONE, content);
|
|
2798
2844
|
this.#serverSettled.add(toolCallId);
|
|
2845
|
+
// The card stops being the live thing the moment it settles, and the
|
|
2846
|
+
// server goes straight back to the model with the result -- a wait with
|
|
2847
|
+
// nothing on screen to own it, and the longest one in a run when the
|
|
2848
|
+
// result is a large inlined attachment being re-sent with every request.
|
|
2849
|
+
// The dots go back where ``onToolCall`` took them from, after the card,
|
|
2850
|
+
// and whatever comes next clears them: reasoning, the first text delta,
|
|
2851
|
+
// the round ending, or ``onSettled``'s terminal guarantee.
|
|
2852
|
+
//
|
|
2853
|
+
// Not the same case as the one ``#executeTool`` refuses to show them
|
|
2854
|
+
// for. That runs after the run has ended, so there is nothing left to
|
|
2855
|
+
// clear them and they would hang -- which is what happened before 0.2.1
|
|
2856
|
+
// and is why they were removed from here too. The terminal guarantee
|
|
2857
|
+
// that shipped in the same release is what makes showing them safe now.
|
|
2858
|
+
this.#showPending();
|
|
2859
|
+
},
|
|
2860
|
+
onActivityChanged: (messageId, activityType, content) => {
|
|
2861
|
+
if (activityType === CHART_ACTIVITY_TYPE && this.#chartActivity) {
|
|
2862
|
+
this.#drawActivityChart(messageId, content);
|
|
2863
|
+
}
|
|
2799
2864
|
},
|
|
2800
2865
|
onRunEnd: () => {
|
|
2801
2866
|
// Per-round end; the button stays on Stop until the whole interaction
|
|
@@ -2969,6 +3034,97 @@ export class AgUiChat extends HTMLElement {
|
|
|
2969
3034
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
2970
3035
|
}
|
|
2971
3036
|
|
|
3037
|
+
/**
|
|
3038
|
+
* Turn on chart rendering, by whichever route this consumer wants.
|
|
3039
|
+
*
|
|
3040
|
+
* Both routes converge on one renderer deliberately. Built apart they become
|
|
3041
|
+
* two chart implementations with two sets of bugs, and the choice between them
|
|
3042
|
+
* is about *where the data lives* rather than how a bar should look:
|
|
3043
|
+
*
|
|
3044
|
+
* - `"tool"` registers the built-in `render_chart`. The agent decides a chart
|
|
3045
|
+
* helps and calls it, so the numbers are in its context and it can discuss
|
|
3046
|
+
* them afterwards. Costs one model round, and works over any transport.
|
|
3047
|
+
* - `"activity"` draws a server-pushed `ACTIVITY_SNAPSHOT` of type `chart`.
|
|
3048
|
+
* No round trip, and the data never enters the model's context at all —
|
|
3049
|
+
* which is what makes it the one for a large or sensitive dataset. Only this
|
|
3050
|
+
* route can update a chart in place as the server computes.
|
|
3051
|
+
*
|
|
3052
|
+
* Off unless asked for, both of them: a component that renders whatever
|
|
3053
|
+
* arrives is not something to switch on for everybody.
|
|
3054
|
+
*/
|
|
3055
|
+
enableCharts(routes: readonly ("tool" | "activity")[] = ["tool", "activity"]): void {
|
|
3056
|
+
if (routes.includes("activity")) {
|
|
3057
|
+
this.#chartActivity = true;
|
|
3058
|
+
}
|
|
3059
|
+
if (routes.includes("tool")) {
|
|
3060
|
+
this.registerTool(createChartTool());
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/**
|
|
3065
|
+
* Place a tool's rendered node against its own card.
|
|
3066
|
+
*
|
|
3067
|
+
* Anchored rather than appended because a client tool's handler does not run
|
|
3068
|
+
* until the round is over: appending would put the node after everything the
|
|
3069
|
+
* model said next, visibly detached from the call that produced it, and in a
|
|
3070
|
+
* different order than the same transcript takes on reload. The card was
|
|
3071
|
+
* created inline, in the right place, so anchoring makes *when* the handler
|
|
3072
|
+
* runs stop mattering.
|
|
3073
|
+
*/
|
|
3074
|
+
#renderToolOutput(tool: ClientTool, call: AgUiToolCall): void {
|
|
3075
|
+
if (tool.render === undefined) {
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
let node: Node | null;
|
|
3079
|
+
try {
|
|
3080
|
+
node = tool.render(call.args);
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
// `render` is consumer code and this runs inside the history replay, where
|
|
3083
|
+
// a throw abandons the loop and takes every later turn of the transcript
|
|
3084
|
+
// with it -- silently, and again on every reload. A chart that fails to
|
|
3085
|
+
// draw is worth losing; the rest of the conversation is not. Reported so
|
|
3086
|
+
// the failure is findable rather than merely survived.
|
|
3087
|
+
console.warn(`ag-ui-chat: render failed for tool ${call.name}`, error);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
3090
|
+
if (node === null) {
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
// `after` rather than an insert-or-append branch: both callers set the card
|
|
3094
|
+
// element immediately before calling, and a parentless anchor makes `after`
|
|
3095
|
+
// a no-op, so the alternative would be a branch nothing can reach.
|
|
3096
|
+
this.#cardElements.get(call.id)?.after(node);
|
|
3097
|
+
this.#afterTranscriptGrew();
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
/** Draw, or redraw in place, the chart for one activity message. */
|
|
3101
|
+
#drawActivityChart(messageId: string, content: unknown): void {
|
|
3102
|
+
const spec = chartSpecFrom(content);
|
|
3103
|
+
if (spec === null) {
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const block = renderChart(spec);
|
|
3107
|
+
if (block === null) {
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
const existing = this.#activityBlocks.get(messageId);
|
|
3111
|
+
if (existing === undefined) {
|
|
3112
|
+
this.#ensureGroup().appendChild(block);
|
|
3113
|
+
} else {
|
|
3114
|
+
// Replaced rather than appended: a server redrawing a chart under the same
|
|
3115
|
+
// id means *this chart changed*, and a second copy below the first would
|
|
3116
|
+
// read as two measurements instead of one that moved.
|
|
3117
|
+
existing.replaceWith(block);
|
|
3118
|
+
}
|
|
3119
|
+
this.#activityBlocks.set(messageId, block);
|
|
3120
|
+
this.#afterTranscriptGrew();
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
#afterTranscriptGrew(): void {
|
|
3124
|
+
this.#updateEmptyState();
|
|
3125
|
+
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2972
3128
|
#cardFor(call: AgUiToolCall): ToolCallCard {
|
|
2973
3129
|
const existing = this.#toolCards.get(call.id);
|
|
2974
3130
|
if (existing !== undefined) {
|
package/src/core/agui_client.ts
CHANGED
|
@@ -83,7 +83,18 @@ export interface AgUiClientHandlers {
|
|
|
83
83
|
* as opposed to work the agent asked for. `django-ag-ui` emits one with
|
|
84
84
|
* `activityType: "compaction"` when it condensed the history.
|
|
85
85
|
*/
|
|
86
|
-
onActivity(activityType: string, content: unknown): void;
|
|
86
|
+
onActivity(activityType: string, content: unknown, messageId: string): void;
|
|
87
|
+
/**
|
|
88
|
+
* An activity's content changed in place — a snapshot re-sent under the same
|
|
89
|
+
* `messageId` with `replace`, or an `ACTIVITY_DELTA` whose JSON patch
|
|
90
|
+
* `@ag-ui/client` has already applied.
|
|
91
|
+
*
|
|
92
|
+
* Reported after the client has updated its own message, so `content` is the
|
|
93
|
+
* result rather than the instruction. That is the whole reason this is a
|
|
94
|
+
* separate callback: the raw delta event fires *before* the patch lands, and
|
|
95
|
+
* a subscriber acting on it would redraw from stale content.
|
|
96
|
+
*/
|
|
97
|
+
onActivityChanged(messageId: string, activityType: string, content: unknown): void;
|
|
87
98
|
/** Fired when a reasoning model starts emitting its chain-of-thought. */
|
|
88
99
|
onReasoningStart(): void;
|
|
89
100
|
/** Fired on every reasoning token; ``buffer`` is the full reasoning text so far. */
|
|
@@ -397,6 +408,9 @@ export class AgUiClient {
|
|
|
397
408
|
#buildSubscriber(pending: AgUiToolCall[], runState: RunState): AgentSubscriber {
|
|
398
409
|
const h = this.#handlers;
|
|
399
410
|
const closed = this.#closedMessageIds;
|
|
411
|
+
// Charts whose patch has been dispatched but not yet applied. Scoped to the
|
|
412
|
+
// subscriber, so it cannot outlive the run that created it.
|
|
413
|
+
const pendingDeltas = new Set<string>();
|
|
400
414
|
return {
|
|
401
415
|
onRunInitialized() {
|
|
402
416
|
h.onRunStart();
|
|
@@ -434,8 +448,42 @@ export class AgUiClient {
|
|
|
434
448
|
onToolCallResultEvent({ event }) {
|
|
435
449
|
h.onToolResult(event.toolCallId, event.content);
|
|
436
450
|
},
|
|
437
|
-
onActivitySnapshotEvent({ event }) {
|
|
438
|
-
|
|
451
|
+
onActivitySnapshotEvent({ event, messages }) {
|
|
452
|
+
// A snapshot for an id already in the list is a replacement, not a new
|
|
453
|
+
// activity: the client has swapped its content in place, and a second
|
|
454
|
+
// append would leave the superseded one on screen.
|
|
455
|
+
const known = messages.some(
|
|
456
|
+
(message) => message.id === event.messageId && message.role === "activity",
|
|
457
|
+
);
|
|
458
|
+
if (known) {
|
|
459
|
+
h.onActivityChanged(event.messageId, event.activityType, event.content);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
h.onActivity(event.activityType, event.content, event.messageId);
|
|
463
|
+
},
|
|
464
|
+
// Deliberately does *not* read the message here. `@ag-ui/client`
|
|
465
|
+
// dispatches this subscriber **before** applying the patch, so the
|
|
466
|
+
// message still holds its previous content: redrawing from it would leave
|
|
467
|
+
// the chart one revision behind for the life of the run, and disagreeing
|
|
468
|
+
// with what a reload shows. Note which chart moved and read the result on
|
|
469
|
+
// the change that follows.
|
|
470
|
+
onActivityDeltaEvent({ event }) {
|
|
471
|
+
pendingDeltas.add(event.messageId);
|
|
472
|
+
},
|
|
473
|
+
// Emitted after the client has written the patched messages, which is the
|
|
474
|
+
// first moment the result exists. Only the ids marked above are looked at,
|
|
475
|
+
// so an ordinary text delta does not walk the transcript.
|
|
476
|
+
onMessagesChanged({ messages }) {
|
|
477
|
+
if (pendingDeltas.size === 0) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
for (const id of pendingDeltas) {
|
|
481
|
+
const message = messages.find((entry) => entry.id === id);
|
|
482
|
+
if (message !== undefined && message.role === "activity") {
|
|
483
|
+
h.onActivityChanged(id, message.activityType, message.content);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
pendingDeltas.clear();
|
|
439
487
|
},
|
|
440
488
|
// `@ag-ui/client` maps the deprecated THINKING_* events onto these
|
|
441
489
|
// REASONING_* callbacks, so the reasoning family alone covers both
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
export {
|
|
4
4
|
ATTACHMENT_EVENT,
|
|
5
|
+
CHART_ACTIVITY_TYPE,
|
|
5
6
|
COMPACTION_ACTIVITY_TYPE,
|
|
6
7
|
ELEMENT_TAG,
|
|
7
8
|
LOAD_CAPABILITY_TOOL,
|
|
@@ -129,6 +130,13 @@ export {
|
|
|
129
130
|
type ApprovalRequest,
|
|
130
131
|
requestApproval,
|
|
131
132
|
} from "./ui/approval_card.js";
|
|
133
|
+
// Charts. `CHART_ACTIVITY_TYPE` above is the wire name a server sets on an
|
|
134
|
+
// ACTIVITY_SNAPSHOT; these are the shape it carries and the renderer itself,
|
|
135
|
+
// for a host building its own visual on the same seam.
|
|
136
|
+
export type { ChartKind, ChartSeries, ChartSpec } from "./ui/chart_block.js";
|
|
137
|
+
export { renderChart } from "./ui/chart_block.js";
|
|
138
|
+
export { chartSpecFrom } from "./ui/chart_spec_from.js";
|
|
139
|
+
export { CHART_TOOL_NAME } from "./ui/chart_tool.js";
|
|
132
140
|
export { CheckpointMenu, type CheckpointVerb } from "./ui/checkpoint_menu.js";
|
|
133
141
|
export {
|
|
134
142
|
type ConfirmationOptions,
|
|
@@ -11,7 +11,30 @@ export interface ClientTool {
|
|
|
11
11
|
name: string;
|
|
12
12
|
description: string;
|
|
13
13
|
parameters: Record<string, unknown>;
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Run the tool. `callId` identifies the call being executed, for a handler
|
|
16
|
+
* that renders into the transcript and needs to place itself against its own
|
|
17
|
+
* card; handlers that only act on the page ignore it.
|
|
18
|
+
*/
|
|
19
|
+
handler: (args: Record<string, unknown>, callId?: string) => unknown | Promise<unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* Draw this call, from its arguments alone.
|
|
22
|
+
*
|
|
23
|
+
* Optional, and **the only half that is replayed**. A restored transcript
|
|
24
|
+
* redraws by calling `render`; it never calls {@link handler}. That is what
|
|
25
|
+
* keeps a replayable tool apart from an effectful one *structurally* rather
|
|
26
|
+
* than by promise: the restore path holds no reference to the half that acts,
|
|
27
|
+
* so re-running `fill_field` on every reload is not a mistake anyone can make.
|
|
28
|
+
*
|
|
29
|
+
* The contract this must keep, because it runs again on every restore:
|
|
30
|
+
*
|
|
31
|
+
* - a pure function of `args` — no host state, no network, no clock;
|
|
32
|
+
* - deterministic, so a reload reproduces what was there before;
|
|
33
|
+
* - free of effects outside the node it returns, which the component places.
|
|
34
|
+
*
|
|
35
|
+
* Return `null` for arguments that say nothing worth drawing.
|
|
36
|
+
*/
|
|
37
|
+
render?: (args: Record<string, unknown>) => Node | null;
|
|
15
38
|
}
|
|
16
39
|
|
|
17
40
|
/**
|