@adecore/intelligent-ui 0.0.0 → 0.21.1

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.
@@ -230,6 +230,22 @@ class ExpressionParser {
230
230
  export function parseUiExpression(source, budget = new UiBudget()) {
231
231
  return new ExpressionParser(source, budget).parse();
232
232
  }
233
+ /* Deep equality of two values, regardless of the order their record keys were written in. */
234
+ export function sameUiValue(first, second) {
235
+ if (first === second) {
236
+ return true;
237
+ }
238
+ if (first === null || second === null || typeof first !== 'object' || typeof second !== 'object' || Array.isArray(first) !== Array.isArray(second)) {
239
+ return false;
240
+ }
241
+ if (Array.isArray(first) && Array.isArray(second)) {
242
+ return first.length === second.length && first.every((item, i) => sameUiValue(item, second[i]));
243
+ }
244
+ const left = first;
245
+ const right = second;
246
+ const keys = Object.keys(left);
247
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameUiValue(left[key], right[key]));
248
+ }
233
249
  export function copyUiValue(value, budget = new UiBudget(), depth = 0) {
234
250
  budget.depth(depth);
235
251
  if (value === null || typeof value === 'boolean') {
@@ -0,0 +1,2 @@
1
+ export declare function uiPlainText(value: unknown): string;
2
+ export declare function uiNodeFallback(type: string, props: Readonly<Record<string, unknown>>, children: string): string;
@@ -0,0 +1,25 @@
1
+ export function uiPlainText(value) {
2
+ return value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
3
+ }
4
+ /* The readable text a node leaves behind where it cannot be drawn, from its props and its children's text. */
5
+ export function uiNodeFallback(type, props, children) {
6
+ const label = props.label ?? props.title;
7
+ switch (type) {
8
+ case '$text':
9
+ return uiPlainText(props.text);
10
+ case 'Stat':
11
+ return `${uiPlainText(label)}: ${uiPlainText(props.value)}${props.unit ? ` ${uiPlainText(props.unit)}` : ''}\n`;
12
+ case 'Source':
13
+ return `${uiPlainText(label)}: ${uiPlainText(props.url)}\n`;
14
+ case 'Table':
15
+ return `${uiPlainText(props.rows ?? [])}\n`;
16
+ case 'Chart':
17
+ return `${uiPlainText(props.data ?? [])}\n`;
18
+ case 'Image':
19
+ return `[Image: ${uiPlainText(props.alt ?? props.attachment ?? props.generated ?? 'unavailable')}]${children}\n`;
20
+ }
21
+ const target = props.path ?? props.sha ?? (type === 'Node' ? props.id : undefined);
22
+ const context = props.context ? `\n${uiPlainText(props.context)}` : '';
23
+ const text = `${label !== undefined ? `${uiPlainText(label)}: ` : ''}${target !== undefined ? `${uiPlainText(target)} ` : ''}${children}${context}`;
24
+ return type === 'Tag' || text.endsWith('\n') ? text : `${text}\n`;
25
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  export { UI_CATALOG, UI_CATALOG_VERSION, UI_GROUPS, UiToneSchema, isUiComponent, uiCatalogText, type UiComponentName, type UiProps, type UiTone } from './catalog.ts';
2
2
  export { UI_LIMITS, UiBudget, UiFailure, safeKey, type UiLimits } from './budget.ts';
3
- export { copyUiValue, evaluateUiExpression, parseUiExpression, type UiExpression, type UiValue } from './expression.ts';
3
+ export { copyUiValue, evaluateUiExpression, parseUiExpression, sameUiValue, type UiExpression, type UiValue } from './expression.ts';
4
4
  export { parseUiSyntax, uiDiagnostic, type UiDiagnostic, type UiSyntax, type UiSyntaxNode } from './syntax.ts';
5
- export { UI_FENCE_LANGUAGE, compileUi, compileUiBlock, type UiBlock, type UiCompileOptions, type UiNode, type UiQuery } from './compiler.ts';
6
- export { evaluateUiBlock, UiState, type UiBinding, type UiEvaluation, type UiViewNode } from './runtime.ts';
5
+ export { UI_FENCE_LANGUAGE, UI_HOST_LIMITS, UI_REPLY_LIMITS, UiCompiler, compileUi, compileUiBlock, uiHasFence, uiMayReferenceHost, type UiCompilerOptions, type UiCompileUpdate, type UiBlock, type UiCompileOptions, type UiNode, type UiQuery } from './compiler.ts';
6
+ export { evaluateUiBlock, UiState, uiInputValues, resolveUiChoice, type UiChoiceSelection, type UiBinding, type UiEvaluation, type UiViewNode } from './runtime.ts';
7
+ export { uiValidatedState, uiQueryArguments, uiQueryFallback } from './query.ts';
7
8
  export { UiNodeSchema, UiDiagnosticSchema, UiBlockSchema, UiBlocksSchema } from './protocol.ts';
8
- export { uiCompactCatalog, uiSessionNote, uiReferenceText, uiFallbackText } from './text.ts';
9
+ export { uiCompactCatalog, uiSessionNote, uiReferenceText, uiFallbackText, type UiTextOptions } from './text.ts';
10
+ export { UI_STREAM_INTERVAL_MS, UiStream, type UiStreamClock, type UiStreamOptions, type UiStreamPreview } from './stream.ts';
11
+ export { UiLinkTargetSchema, UiLinkResolutionSchema, uiLinkTargets, type UiLinkTarget, type UiLinkResolution } from './links.ts';
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export { UI_CATALOG, UI_CATALOG_VERSION, UI_GROUPS, UiToneSchema, isUiComponent, uiCatalogText } from './catalog.js';
2
2
  export { UI_LIMITS, UiBudget, UiFailure, safeKey } from './budget.js';
3
- export { copyUiValue, evaluateUiExpression, parseUiExpression } from './expression.js';
3
+ export { copyUiValue, evaluateUiExpression, parseUiExpression, sameUiValue } from './expression.js';
4
4
  export { parseUiSyntax, uiDiagnostic } from './syntax.js';
5
- export { UI_FENCE_LANGUAGE, compileUi, compileUiBlock } from './compiler.js';
6
- export { evaluateUiBlock, UiState } from './runtime.js';
5
+ export { UI_FENCE_LANGUAGE, UI_HOST_LIMITS, UI_REPLY_LIMITS, UiCompiler, compileUi, compileUiBlock, uiHasFence, uiMayReferenceHost } from './compiler.js';
6
+ export { evaluateUiBlock, UiState, uiInputValues, resolveUiChoice } from './runtime.js';
7
+ export { uiValidatedState, uiQueryArguments, uiQueryFallback } from './query.js';
7
8
  export { UiNodeSchema, UiDiagnosticSchema, UiBlockSchema, UiBlocksSchema } from './protocol.js';
8
9
  export { uiCompactCatalog, uiSessionNote, uiReferenceText, uiFallbackText } from './text.js';
10
+ export { UI_STREAM_INTERVAL_MS, UiStream } from './stream.js';
11
+ export { UiLinkTargetSchema, UiLinkResolutionSchema, uiLinkTargets } from './links.js';
@@ -0,0 +1,5 @@
1
+ import type { UiBlock, UiNode } from './compiler.ts';
2
+ import { type UiValue } from './expression.ts';
3
+ import type { UiViewNode } from './runtime.ts';
4
+ export declare function uiCompiledNodes(nodes: readonly UiNode[], into?: Map<string, UiNode>): Map<string, UiNode>;
5
+ export declare function uiVisibleInputs(block: UiBlock, nodes: readonly UiViewNode[], scope: Readonly<Record<string, UiValue>>): Set<string>;
package/dist/inputs.js ADDED
@@ -0,0 +1,69 @@
1
+ import { uiActionProp, uiActionTarget, uiLocalState } from './actions.js';
2
+ import { evaluateUiExpression, sameUiValue } from './expression.js';
3
+ export function uiCompiledNodes(nodes, into = new Map()) {
4
+ for (const node of nodes) {
5
+ into.set(node.id, node);
6
+ uiCompiledNodes(node.children, into);
7
+ }
8
+ return into;
9
+ }
10
+ /* Whether a control's value is one its own visible options offer. */
11
+ function offered(node) {
12
+ if (node.type === 'Segmented') {
13
+ return node.children.some((option) => option.type === 'Option' && !option.error && option.props.value === node.props.value);
14
+ }
15
+ if (node.type === 'Checklist') {
16
+ const options = node.children.filter((item) => item.type === 'Item' && !item.error).map((item) => item.props.value);
17
+ return node.props.value.every((value) => options.includes(value));
18
+ }
19
+ return true;
20
+ }
21
+ /*
22
+ * The value pressing a Button sets its target to, or undefined when it sets none. A Button's @Set takes
23
+ * a literal only, so the value never depends on the state it was pressed in.
24
+ */
25
+ function actionValue(action, block) {
26
+ try {
27
+ const target = uiActionTarget(action, uiLocalState(block));
28
+ if (target === null || action.kind !== 'call') {
29
+ return undefined;
30
+ }
31
+ return { target, value: action.name === 'Set' ? evaluateUiExpression(action.args[1], {}) : block.defaults[target] };
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ /*
38
+ * The variables whose current value a visible, complete control of the evaluated tree accounts for: a bound
39
+ * control holding it within what it offers, or an enabled Button that sets exactly that value. A submitted
40
+ * value outside this set, and unequal to its default, is one no person could have produced on the screen.
41
+ */
42
+ export function uiVisibleInputs(block, nodes, scope) {
43
+ const written = uiCompiledNodes(block.nodes);
44
+ const inputs = new Set();
45
+ const visit = (views) => {
46
+ for (const node of views) {
47
+ if (node.error || !node.complete) {
48
+ continue;
49
+ }
50
+ const source = written.get(node.sourceId ?? node.id);
51
+ if (source && offered(node)) {
52
+ for (const prop of Object.keys(node.bindings)) {
53
+ if (source.bindings[prop]) {
54
+ inputs.add(source.bindings[prop]);
55
+ }
56
+ }
57
+ }
58
+ const actionProp = uiActionProp(node.type);
59
+ const action = source && actionProp ? source.expressions[actionProp] : undefined;
60
+ const pressed = action && node.props.disabled !== true ? actionValue(action, block) : undefined;
61
+ if (pressed && sameUiValue(pressed.value, scope[pressed.target])) {
62
+ inputs.add(pressed.target);
63
+ }
64
+ visit(node.children);
65
+ }
66
+ };
67
+ visit(nodes);
68
+ return inputs;
69
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ import type { UiLimits } from './budget.ts';
3
+ import type { UiBlock } from './compiler.ts';
4
+ export declare const UiLinkTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
5
+ type: z.ZodLiteral<"File">;
6
+ path: z.ZodString;
7
+ line: z.ZodOptional<z.ZodNumber>;
8
+ }, z.core.$strict>, z.ZodObject<{
9
+ type: z.ZodLiteral<"Diff">;
10
+ path: z.ZodString;
11
+ }, z.core.$strict>, z.ZodObject<{
12
+ type: z.ZodLiteral<"Commit">;
13
+ sha: z.ZodString;
14
+ }, z.core.$strict>, z.ZodObject<{
15
+ type: z.ZodLiteral<"Node">;
16
+ id: z.ZodString;
17
+ }, z.core.$strict>], "type">;
18
+ export type UiLinkTarget = z.infer<typeof UiLinkTargetSchema>;
19
+ export declare const UiLinkResolutionSchema: z.ZodObject<{
20
+ state: z.ZodEnum<{
21
+ chip: "chip";
22
+ plain: "plain";
23
+ }>;
24
+ target: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
25
+ type: z.ZodLiteral<"File">;
26
+ path: z.ZodString;
27
+ line: z.ZodOptional<z.ZodNumber>;
28
+ }, z.core.$strict>, z.ZodObject<{
29
+ type: z.ZodLiteral<"Diff">;
30
+ path: z.ZodString;
31
+ }, z.core.$strict>, z.ZodObject<{
32
+ type: z.ZodLiteral<"Commit">;
33
+ sha: z.ZodString;
34
+ }, z.core.$strict>, z.ZodObject<{
35
+ type: z.ZodLiteral<"Node">;
36
+ id: z.ZodString;
37
+ }, z.core.$strict>], "type">>;
38
+ cwd: z.ZodOptional<z.ZodString>;
39
+ viewId: z.ZodOptional<z.ZodString>;
40
+ projectId: z.ZodOptional<z.ZodString>;
41
+ staged: z.ZodOptional<z.ZodBoolean>;
42
+ conflicted: z.ZodOptional<z.ZodBoolean>;
43
+ relativePath: z.ZodOptional<z.ZodString>;
44
+ label: z.ZodOptional<z.ZodString>;
45
+ code: z.ZodOptional<z.ZodString>;
46
+ reason: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export type UiLinkResolution = z.infer<typeof UiLinkResolutionSchema>;
49
+ export declare function uiLinkTargets(block: UiBlock, input?: Readonly<Record<string, unknown>>, queries?: Readonly<Record<string, unknown>>, limits?: Partial<UiLimits>): Record<string, UiLinkTarget>;
package/dist/links.js ADDED
@@ -0,0 +1,42 @@
1
+ import { z } from 'zod';
2
+ import { evaluateUiBlock } from './runtime.js';
3
+ import { uiValidatedState } from './query.js';
4
+ export const UiLinkTargetSchema = z.discriminatedUnion('type', [
5
+ z.object({ type: z.literal('File'), path: z.string().min(1).max(4096), line: z.number().int().positive().optional() }).strict(),
6
+ z.object({ type: z.literal('Diff'), path: z.string().min(1).max(4096) }).strict(),
7
+ z.object({ type: z.literal('Commit'), sha: z.string().regex(/^[a-f\d]{7,64}$/i) }).strict(),
8
+ z.object({ type: z.literal('Node'), id: z.string().min(1).max(256) }).strict()
9
+ ]);
10
+ // `state` is a frozen set: native clients validate these payloads whole, so a new state would fail them.
11
+ export const UiLinkResolutionSchema = z.object({
12
+ state: z.enum(['chip', 'plain']),
13
+ target: UiLinkTargetSchema.optional(),
14
+ cwd: z.string().optional(),
15
+ viewId: z.string().optional(),
16
+ projectId: z.string().optional(),
17
+ staged: z.boolean().optional(),
18
+ conflicted: z.boolean().optional(),
19
+ relativePath: z.string().optional(),
20
+ label: z.string().optional(),
21
+ // A code that names the reason across versions; open, so a new one fails nobody.
22
+ code: z.string().optional(),
23
+ reason: z.string().optional()
24
+ });
25
+ export function uiLinkTargets(block, input = {}, queries = {}, limits = {}) {
26
+ const state = uiValidatedState(block, input, queries, limits);
27
+ const targets = Object.create(null);
28
+ const visit = (nodes) => {
29
+ for (const node of nodes) {
30
+ if (node.error || !node.complete) {
31
+ continue;
32
+ }
33
+ const checked = UiLinkTargetSchema.safeParse({ type: node.type, ...node.props });
34
+ if (checked.success) {
35
+ targets[node.id] = checked.data;
36
+ }
37
+ visit(node.children);
38
+ }
39
+ };
40
+ visit(evaluateUiBlock(block, state, limits).nodes);
41
+ return targets;
42
+ }
@@ -0,0 +1,7 @@
1
+ import type { z } from 'zod';
2
+ export declare function uiPropList(entry: {
3
+ readonly schema: {
4
+ readonly shape: Readonly<Record<string, z.ZodType>>;
5
+ };
6
+ readonly action?: string;
7
+ }): string[];
@@ -0,0 +1,5 @@
1
+ /* The props a catalog entry accepts, its action first and an optional one marked with `?`. */
2
+ export function uiPropList(entry) {
3
+ const props = Object.entries(entry.schema.shape).map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`);
4
+ return entry.action ? [entry.action, ...props] : props;
5
+ }
package/dist/protocol.js CHANGED
@@ -25,11 +25,12 @@ export const UiDiagnosticSchema = z.object({
25
25
  export const UiBlockSchema = z.object({
26
26
  id: z.string(),
27
27
  catalogVersion: position,
28
+ revision: z.string().min(1).optional(),
28
29
  start: position,
29
30
  end: position,
30
31
  complete: z.boolean(),
31
32
  defaults: values,
32
- queries: z.record(z.string(), z.object({ source: z.string(), args: values })),
33
+ queries: z.record(z.string(), z.object({ source: z.string(), args: values, expression: z.unknown().optional() })),
33
34
  nodes: z.array(UiNodeSchema),
34
35
  diagnostics: z.array(UiDiagnosticSchema),
35
36
  fallback: z.string()
@@ -0,0 +1,7 @@
1
+ import { type UiLimits } from './budget.ts';
2
+ import type { UiBlock } from './compiler.ts';
3
+ import { type UiValue } from './expression.ts';
4
+ import { UiState } from './runtime.ts';
5
+ export declare function uiValidatedState(block: UiBlock, input?: Readonly<Record<string, unknown>>, queries?: Readonly<Record<string, unknown>>, limits?: Partial<UiLimits>): UiState;
6
+ export declare function uiQueryArguments(block: UiBlock, name: string, input?: Readonly<Record<string, unknown>>, limits?: Partial<UiLimits>): Record<string, UiValue>;
7
+ export declare function uiQueryFallback(block: UiBlock, queries: Readonly<Record<string, unknown>>): string;
package/dist/query.js ADDED
@@ -0,0 +1,52 @@
1
+ import { UI_CATALOG_VERSION } from './catalog.js';
2
+ import { UiBudget, UiFailure } from './budget.js';
3
+ import { uiVisibleInputs } from './inputs.js';
4
+ import { uiNodeFallback } from './fallback.js';
5
+ import { copyUiValue, evaluateUiExpression, sameUiValue } from './expression.js';
6
+ import { evaluateUiBlock, uiInputValues, UiState } from './runtime.js';
7
+ export function uiValidatedState(block, input = {}, queries = {}, limits = {}) {
8
+ if (!block.complete || block.catalogVersion !== UI_CATALOG_VERSION) {
9
+ throw new UiFailure('invalid_block', 'Only completed supported blocks can resolve input.');
10
+ }
11
+ const values = copyUiValue(input, new UiBudget(limits));
12
+ const state = new UiState(block, limits);
13
+ for (const [name, value] of Object.entries(queries)) {
14
+ state.setQuery(name, value, block);
15
+ }
16
+ const allowed = uiInputValues(block, state);
17
+ for (const [key, value] of Object.entries(values)) {
18
+ if (!Object.hasOwn(allowed, key)) {
19
+ throw new UiFailure('refused_binding', 'Only declared input bindings can be submitted.');
20
+ }
21
+ state.set(key, value);
22
+ }
23
+ const evaluated = evaluateUiBlock(block, state, limits);
24
+ if (evaluated.diagnostics.some((diagnostic) => diagnostic.code === 'budget_exceeded')) {
25
+ throw new UiFailure('budget_exceeded', 'The query inputs exceeded their evaluation budget.');
26
+ }
27
+ const visible = uiVisibleInputs(block, evaluated.nodes, state.scope());
28
+ for (const [key, value] of Object.entries(values)) {
29
+ if (!sameUiValue(value, allowed[key]) && !visible.has(key)) {
30
+ throw new UiFailure('invalid_value', 'An input value is outside the visible control’s allowed values.');
31
+ }
32
+ }
33
+ return state;
34
+ }
35
+ export function uiQueryArguments(block, name, input = {}, limits = {}) {
36
+ if (!Object.hasOwn(block.queries, name)) {
37
+ throw new UiFailure('invalid_query', 'This block does not declare that query.');
38
+ }
39
+ const state = uiValidatedState(block, input, {}, limits);
40
+ const query = block.queries[name];
41
+ return query.expression
42
+ ? evaluateUiExpression(query.expression, state.scope(), new UiBudget(limits))
43
+ : copyUiValue(query.args, new UiBudget(limits));
44
+ }
45
+ export function uiQueryFallback(block, queries) {
46
+ const state = new UiState(block);
47
+ for (const [name, value] of Object.entries(queries)) {
48
+ state.setQuery(name, value, block);
49
+ }
50
+ const text = (nodes) => nodes.map((node) => (node.error ? node.fallback : uiNodeFallback(node.type, node.props, text(node.children)))).join('');
51
+ return text(evaluateUiBlock(block, state).nodes).trim() || block.fallback;
52
+ }
package/dist/runtime.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface UiBinding<Value> {
8
8
  }
9
9
  export interface UiViewNode<Props = Record<string, unknown>> {
10
10
  id: string;
11
+ sourceId?: string;
11
12
  type: string;
12
13
  props: Props;
13
14
  bindings: Readonly<Record<string, UiBinding<unknown>>>;
@@ -15,6 +16,7 @@ export interface UiViewNode<Props = Record<string, unknown>> {
15
16
  complete: boolean;
16
17
  fallback: string;
17
18
  error?: string;
19
+ onAction?(): void;
18
20
  }
19
21
  export interface UiEvaluation {
20
22
  nodes: UiViewNode[];
@@ -28,7 +30,8 @@ export declare class UiState {
28
30
  private querySignatures;
29
31
  private readonly listeners;
30
32
  private revision;
31
- constructor(block: UiBlock);
33
+ private readonly limits;
34
+ constructor(block: UiBlock, limits?: Partial<UiLimits>);
32
35
  sync(block: UiBlock): void;
33
36
  snapshot(): number;
34
37
  subscribe(listener: () => void): () => void;
@@ -39,3 +42,10 @@ export declare class UiState {
39
42
  private changed;
40
43
  }
41
44
  export declare function evaluateUiBlock(block: UiBlock, state?: UiState, limits?: Partial<UiLimits>): UiEvaluation;
45
+ export interface UiChoiceSelection {
46
+ label: string;
47
+ context: string;
48
+ values: Record<string, UiValue>;
49
+ }
50
+ export declare function uiInputValues(block: UiBlock, state: UiState): Record<string, UiValue>;
51
+ export declare function resolveUiChoice(block: UiBlock, choiceId: string, input?: Readonly<Record<string, unknown>>, queries?: Readonly<Record<string, unknown>>, limits?: Partial<UiLimits>): UiChoiceSelection;
package/dist/runtime.js CHANGED
@@ -1,7 +1,9 @@
1
- import { copyUiValue, evaluateUiExpression } from './expression.js';
1
+ import { copyUiValue, evaluateUiExpression, sameUiValue } from './expression.js';
2
2
  import { UiBudget, UiFailure } from './budget.js';
3
3
  import { isUiComponent, UI_CATALOG, UI_CATALOG_VERSION } from './catalog.js';
4
4
  import {} from './compiler.js';
5
+ import { uiActionProp, uiActionTarget, uiLocalState } from './actions.js';
6
+ import { uiCompiledNodes, uiVisibleInputs } from './inputs.js';
5
7
  import { uiDiagnostic } from './syntax.js';
6
8
  export class UiState {
7
9
  id = '';
@@ -11,17 +13,19 @@ export class UiState {
11
13
  querySignatures = Object.create(null);
12
14
  listeners = new Set();
13
15
  revision = 0;
14
- constructor(block) {
16
+ limits;
17
+ constructor(block, limits = {}) {
18
+ this.limits = limits;
15
19
  this.sync(block);
16
20
  }
17
21
  sync(block) {
18
- const defaults = copyUiValue(block.defaults);
22
+ const defaults = copyUiValue(block.defaults, new UiBudget(this.limits));
19
23
  if (this.id !== block.id) {
20
24
  this.values = Object.create(null);
21
25
  this.queries = Object.create(null);
22
26
  }
23
27
  for (const [name, value] of Object.entries(defaults)) {
24
- if (this.id !== block.id || !Object.hasOwn(this.defaults, name) || JSON.stringify(this.defaults[name]) !== JSON.stringify(value)) {
28
+ if (this.id !== block.id || !Object.hasOwn(this.defaults, name) || !sameUiValue(this.defaults[name], value)) {
25
29
  this.values[name] = value;
26
30
  }
27
31
  }
@@ -50,13 +54,13 @@ export class UiState {
50
54
  return () => this.listeners.delete(listener);
51
55
  }
52
56
  scope() {
53
- return copyUiValue({ ...this.values, ...this.queries });
57
+ return copyUiValue({ ...this.values, ...this.queries }, new UiBudget(this.limits));
54
58
  }
55
59
  set(name, value) {
56
60
  if (!Object.hasOwn(this.defaults, name)) {
57
61
  throw new UiFailure('refused_binding', 'Only declared local state can be changed.');
58
62
  }
59
- const next = copyUiValue(value);
63
+ const next = copyUiValue(value, new UiBudget(this.limits));
60
64
  const initial = this.defaults[name];
61
65
  if (Array.isArray(next) !== Array.isArray(initial) || typeof next !== typeof initial || (next === null) !== (initial === null)) {
62
66
  throw new UiFailure('invalid_value', 'The state type cannot change.');
@@ -68,30 +72,20 @@ export class UiState {
68
72
  if (block.id !== this.id || !Object.hasOwn(block.queries, name) || this.querySignatures[name] !== JSON.stringify(block.queries[name])) {
69
73
  throw new UiFailure('refused_query', 'The query must belong to this block.');
70
74
  }
71
- this.queries[name] = copyUiValue(value);
75
+ this.queries[name] = copyUiValue(value, new UiBudget(this.limits));
72
76
  this.changed();
73
77
  }
74
78
  run(action) {
75
- if (action.kind !== 'call' || !['Set', 'Reset'].includes(action.name)) {
76
- throw new UiFailure('refused_action', 'Expected @Set or @Reset.');
77
- }
78
- if (action.name === 'Reset' && action.args.length === 0) {
79
- this.values = copyUiValue(this.defaults);
79
+ const target = uiActionTarget(action, (name) => Object.hasOwn(this.defaults, name));
80
+ if (target === null) {
81
+ this.values = copyUiValue(this.defaults, new UiBudget(this.limits));
80
82
  this.changed();
81
- return;
82
- }
83
- const reference = action.args[0];
84
- if (!reference || reference.kind !== 'reference' || !Object.hasOwn(this.defaults, reference.name)) {
85
- throw new UiFailure('refused_binding', 'The action needs a local state variable.');
86
- }
87
- if (action.name === 'Set' && action.args.length === 2) {
88
- this.set(reference.name, evaluateUiExpression(action.args[1], this.scope()));
89
83
  }
90
- else if (action.name === 'Reset' && action.args.length === 1) {
91
- this.set(reference.name, this.defaults[reference.name]);
84
+ else if (action.kind === 'call' && action.name === 'Set') {
85
+ this.set(target, evaluateUiExpression(action.args[1], this.scope(), new UiBudget(this.limits)));
92
86
  }
93
87
  else {
94
- throw new UiFailure('refused_action', 'Invalid state action arguments.');
88
+ this.set(target, this.defaults[target]);
95
89
  }
96
90
  }
97
91
  changed() {
@@ -119,6 +113,7 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
119
113
  const visit = (node, variables, depth, suffix = '') => {
120
114
  const view = {
121
115
  id: `${node.id}${suffix}`,
116
+ sourceId: node.id,
122
117
  type: node.type,
123
118
  props: {},
124
119
  bindings: {},
@@ -133,8 +128,11 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
133
128
  throw new UiFailure(node.error ?? 'unknown_catalog', 'This part uses an unsupported UI definition.');
134
129
  }
135
130
  const props = copyUiValue(node.props, budget);
131
+ const actionProp = uiActionProp(node.type);
136
132
  for (const [key, expression] of Object.entries(node.expressions)) {
137
- props[key] = evaluateUiExpression(expression, variables, budget);
133
+ if (key !== actionProp) {
134
+ props[key] = evaluateUiExpression(expression, variables, budget);
135
+ }
138
136
  }
139
137
  if (node.type === '$text') {
140
138
  if (typeof props.text !== 'string' && typeof props.text !== 'number' && typeof props.text !== 'boolean' && props.text !== null) {
@@ -173,7 +171,7 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
173
171
  }
174
172
  const bindings = Object.create(null);
175
173
  for (const [key, name] of Object.entries(node.bindings)) {
176
- if (!('binding' in entry) || entry.binding !== key || !Object.hasOwn(block.defaults, name) || Object.hasOwn(block.queries, name)) {
174
+ if (!('binding' in entry) || entry.binding !== key || !uiLocalState(block)(name)) {
177
175
  throw new UiFailure('refused_binding', 'The binding must target declared local input state.');
178
176
  }
179
177
  bindings[key] = {
@@ -190,6 +188,24 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
190
188
  };
191
189
  }
192
190
  view.bindings = bindings;
191
+ const action = actionProp === undefined ? undefined : node.expressions[actionProp];
192
+ if (action) {
193
+ uiActionTarget(action, uiLocalState(block));
194
+ view.onAction = () => {
195
+ if (!node.complete || props.disabled === true) {
196
+ return;
197
+ }
198
+ try {
199
+ state.run(action);
200
+ }
201
+ catch (error) {
202
+ // An action that cannot apply to the current state leaves it as it was.
203
+ if (!(error instanceof UiFailure)) {
204
+ throw error;
205
+ }
206
+ }
207
+ };
208
+ }
193
209
  view.children = children(node.children, variables, depth + 1, suffix);
194
210
  return [view];
195
211
  }
@@ -212,3 +228,101 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
212
228
  }
213
229
  return result;
214
230
  }
231
+ export function uiInputValues(block, state) {
232
+ const scope = state.scope();
233
+ const values = Object.create(null);
234
+ const local = uiLocalState(block);
235
+ const actionTarget = (action) => {
236
+ try {
237
+ return uiActionTarget(action, local);
238
+ }
239
+ catch {
240
+ return null;
241
+ }
242
+ };
243
+ const visit = (nodes) => {
244
+ for (const node of nodes) {
245
+ if (isUiComponent(node.type) && !node.error && node.complete) {
246
+ const entry = UI_CATALOG[node.type];
247
+ for (const [prop, name] of Object.entries(node.bindings)) {
248
+ if ('binding' in entry && entry.binding === prop && local(name)) {
249
+ values[name] = scope[name];
250
+ }
251
+ }
252
+ const actionProp = uiActionProp(node.type);
253
+ const target = actionProp && node.expressions[actionProp] ? actionTarget(node.expressions[actionProp]) : null;
254
+ if (target !== null) {
255
+ values[target] = scope[target];
256
+ }
257
+ }
258
+ visit(node.children);
259
+ }
260
+ };
261
+ visit(block.nodes);
262
+ return values;
263
+ }
264
+ /* The host supplies its stored block; client data can change only declared inputs and Button targets. */
265
+ export function resolveUiChoice(block, choiceId, input = {}, queries = {}, limits = {}) {
266
+ if (!block.complete || block.catalogVersion !== UI_CATALOG_VERSION) {
267
+ throw new UiFailure('refused_choice', 'Only a completed supported block can send a choice.');
268
+ }
269
+ const values = copyUiValue(input, new UiBudget(limits));
270
+ const state = new UiState(block, limits);
271
+ for (const [name, value] of Object.entries(queries)) {
272
+ state.setQuery(name, value, block);
273
+ }
274
+ const allowed = uiInputValues(block, state);
275
+ const changed = new Set();
276
+ for (const [name, value] of Object.entries(values)) {
277
+ if (!Object.hasOwn(allowed, name)) {
278
+ throw new UiFailure('refused_binding', 'Only declared input bindings can be submitted.');
279
+ }
280
+ if (!sameUiValue(value, allowed[name])) {
281
+ state.set(name, value);
282
+ changed.add(name);
283
+ }
284
+ }
285
+ const evaluated = evaluateUiBlock(block, state, limits);
286
+ if (evaluated.diagnostics.some((diagnostic) => diagnostic.code === 'budget_exceeded')) {
287
+ throw new UiFailure('budget_exceeded', 'The choice exceeded its evaluation budget.');
288
+ }
289
+ let choice;
290
+ const written = uiCompiledNodes(block.nodes);
291
+ const visit = (nodes, parent) => {
292
+ for (const node of nodes) {
293
+ const source = written.get(node.sourceId ?? node.id);
294
+ if (node.error && source && Object.values(source.bindings).some((name) => changed.has(name))) {
295
+ throw new UiFailure('invalid_value', 'An input value does not match its control.');
296
+ }
297
+ if (node.error || !node.complete) {
298
+ continue;
299
+ }
300
+ if (node.id === choiceId && node.type === 'Choice' && parent === 'Choices' && node.props.disabled !== true) {
301
+ choice = node;
302
+ }
303
+ visit(node.children, node.type);
304
+ }
305
+ };
306
+ visit(evaluated.nodes);
307
+ const validInputs = uiVisibleInputs(block, evaluated.nodes, state.scope());
308
+ if ([...changed].some((name) => !validInputs.has(name))) {
309
+ throw new UiFailure('invalid_value', 'An input value is outside the visible control’s allowed values.');
310
+ }
311
+ if (!choice) {
312
+ throw new UiFailure('refused_choice', 'This choice is not available in the current block.');
313
+ }
314
+ const text = (nodes) => nodes.map((node) => (node.type === '$text' ? String(node.props.text ?? '') : text(node.children))).join('');
315
+ const hasError = (nodes) => nodes.some((node) => node.error !== undefined || hasError(node.children));
316
+ if (hasError(choice.children)) {
317
+ throw new UiFailure('refused_choice', 'The choice label could not be fully read.');
318
+ }
319
+ const label = text(choice.children).replace(/\s+/g, ' ').trim();
320
+ if (!label) {
321
+ throw new UiFailure('refused_choice', 'A choice needs a visible label.');
322
+ }
323
+ return {
324
+ label,
325
+ values: uiInputValues(block, state),
326
+ context: typeof choice.props.context === 'string' && choice.props.context.trim() ? choice.props.context : label
327
+ };
328
+ }
@@ -0,0 +1,28 @@
1
+ import { type UiBlock, type UiCompilerOptions } from './compiler.ts';
2
+ export declare const UI_STREAM_INTERVAL_MS = 250;
3
+ export interface UiStreamClock {
4
+ now(): number;
5
+ setTimeout(run: () => void, ms: number): unknown;
6
+ clearTimeout(handle: unknown): void;
7
+ }
8
+ export interface UiStreamPreview {
9
+ textLength: number;
10
+ blocks: UiBlock[];
11
+ }
12
+ export interface UiStreamOptions extends UiCompilerOptions {
13
+ clock: UiStreamClock;
14
+ emit(preview: UiStreamPreview): void;
15
+ }
16
+ export declare class UiStream {
17
+ private readonly compiler;
18
+ private readonly options;
19
+ private pending;
20
+ private timer;
21
+ private lastCompiledAt;
22
+ private closed;
23
+ constructor(options: UiStreamOptions);
24
+ update(text: string, latestAttachment?: string): void;
25
+ finish(text: string, latestAttachment?: string): UiBlock[];
26
+ dispose(): void;
27
+ private flush;
28
+ }