@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.
package/dist/stream.js ADDED
@@ -0,0 +1,56 @@
1
+ import { UiCompiler } from './compiler.js';
2
+ export const UI_STREAM_INTERVAL_MS = 250;
3
+ export class UiStream {
4
+ compiler;
5
+ options;
6
+ pending = null;
7
+ timer = null;
8
+ lastCompiledAt = -Infinity;
9
+ closed = false;
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.compiler = new UiCompiler(options);
13
+ }
14
+ update(text, latestAttachment) {
15
+ if (this.closed) {
16
+ return;
17
+ }
18
+ this.pending = { text, latestAttachment };
19
+ if (this.timer !== null) {
20
+ return;
21
+ }
22
+ const delay = Math.max(0, UI_STREAM_INTERVAL_MS - (this.options.clock.now() - this.lastCompiledAt));
23
+ if (delay === 0) {
24
+ this.flush();
25
+ }
26
+ else {
27
+ this.timer = { handle: this.options.clock.setTimeout(() => this.flush(), delay) };
28
+ }
29
+ }
30
+ finish(text, latestAttachment) {
31
+ this.dispose();
32
+ const blocks = this.compiler.compile(text, { final: true, latestAttachment });
33
+ this.compiler.clear();
34
+ return blocks;
35
+ }
36
+ dispose() {
37
+ this.closed = true;
38
+ if (this.timer !== null) {
39
+ this.options.clock.clearTimeout(this.timer.handle);
40
+ this.timer = null;
41
+ }
42
+ this.pending = null;
43
+ this.compiler.clear();
44
+ }
45
+ flush() {
46
+ this.timer = null;
47
+ const pending = this.pending;
48
+ if (pending === null || this.closed) {
49
+ return;
50
+ }
51
+ this.pending = null;
52
+ this.lastCompiledAt = this.options.clock.now();
53
+ const blocks = this.compiler.compile(pending.text, { latestAttachment: pending.latestAttachment });
54
+ this.options.emit({ textLength: pending.text.length, blocks });
55
+ }
56
+ }
package/dist/text.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { type UiBlock } from './compiler.ts';
2
2
  export declare function uiCompactCatalog(): string;
3
- export declare function uiSessionNote(): string;
4
- export declare function uiReferenceText(): string;
3
+ export interface UiTextOptions {
4
+ fenceLanguage?: string;
5
+ }
6
+ export declare function uiSessionNote({ fenceLanguage }?: UiTextOptions): string;
7
+ export declare function uiReferenceText({ fenceLanguage }?: UiTextOptions): string;
5
8
  export declare function uiFallbackText(text: string, blocks: readonly UiBlock[]): string;
package/dist/text.js CHANGED
@@ -1,32 +1,44 @@
1
1
  import { UI_CATALOG, UI_GROUPS } from './catalog.js';
2
+ import { uiPropList } from './prop-list.js';
2
3
  import { UI_FENCE_LANGUAGE } from './compiler.js';
3
4
  export function uiCompactCatalog() {
4
5
  return Object.entries(UI_CATALOG)
5
6
  .map(([name, entry]) => {
6
- const props = Object.entries(entry.schema.shape)
7
- .map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
8
- .join(',');
9
- return `${name}(${props})`;
7
+ const props = uiPropList(entry).join(',');
8
+ return `${name}${props ? `: ${props}` : ''}`;
10
9
  })
11
10
  .join(' · ');
12
11
  }
13
- export function uiSessionNote() {
14
- return `For a result, comparison or choice that benefits from an interface, put a fenced ${UI_FENCE_LANGUAGE} block in your final reply, with explanation outside it. Use catalog tags with named props; no layout, style or color props. State: $name = literal JSON on a line above the tags. Bind local inputs with value={$name}; use braced expressions and @Count, @Filter(list,row,predicate), @Sum, @Join, @Round. Show(when) and Each(items,as) control children. End decisions with Choices; each Choice has a short label and precise context shown to the person. Image takes a chat attachment or generated="latest", never a URL or path. No executable code. Local state resets on reload. ${uiCompactCatalog()}`;
12
+ function uiSyntax(language) {
13
+ return `Write XML-like component tags inside a fenced ${language} block. Put text between opening and closing tags; empty components end with />. Quote string props; put numbers, booleans and expressions in braces.`;
15
14
  }
16
- export function uiReferenceText() {
17
- return Object.entries(UI_GROUPS)
15
+ function uiExample(language) {
16
+ return [
17
+ '```' + language,
18
+ '$enabled = false',
19
+ '<Summary badge="Draft">Release overview</Summary>',
20
+ '<Stats><Stat label="Tests" value={42}/></Stats>',
21
+ '<Switch value={$enabled}>Notify me</Switch>',
22
+ '<Choices><Choice context="Review the release overview">Continue</Choice></Choices>',
23
+ '```'
24
+ ].join('\n');
25
+ }
26
+ export function uiSessionNote({ fenceLanguage = UI_FENCE_LANGUAGE } = {}) {
27
+ return `${uiSyntax(fenceLanguage)} Prose outside. Only named catalog props, no styling. $name = literal JSON; input value={$name}; Button action={@Set($name, literal)}, {@Reset($name)} or {@Reset()} changes local state only; braced expressions: @Count, @Filter(list,row,predicate), @Sum, @Join, @Round. Show/Each control children. Choice labels and precise context stay visible and send once. Image: attachment or generated="latest", never URL/path. No code execution. State resets on reload. Catalog (? means optional props): ${uiCompactCatalog()}\nExample:\n${uiExample(fenceLanguage)}\n`;
28
+ }
29
+ export function uiReferenceText({ fenceLanguage = UI_FENCE_LANGUAGE } = {}) {
30
+ const groups = Object.entries(UI_GROUPS)
18
31
  .map(([group, definition]) => {
19
32
  const components = Object.entries(UI_CATALOG)
20
33
  .filter(([, entry]) => entry.group === group)
21
34
  .map(([name, entry]) => {
22
- const props = Object.entries(entry.schema.shape)
23
- .map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
24
- .join(', ');
25
- return `${name}(${props}): ${entry.description}`;
35
+ const props = uiPropList(entry).join(', ');
36
+ return `${name}: ${entry.description}${props ? ` Props: ${props}.` : ''}`;
26
37
  });
27
38
  return [definition.description, definition.rules, ...components].join('\n');
28
39
  })
29
40
  .join('\n\n');
41
+ return `${uiSyntax(fenceLanguage)}\nExample:\n${uiExample(fenceLanguage)}\n\n${groups}`;
30
42
  }
31
43
  export function uiFallbackText(text, blocks) {
32
44
  let result = '';
package/package.json CHANGED
@@ -1,84 +1,99 @@
1
1
  {
2
- "name": "@adecore/intelligent-ui",
3
- "version": "0.0.0",
4
- "description": "A streaming UI catalog, compiler and bounded expression interpreter without a framework or DOM.",
5
- "license": "FSL-1.1-MIT",
6
- "author": "Bas Milius",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/basmilius/adecore.git",
10
- "directory": "packages/intelligent-ui"
2
+ "name": "@adecore/intelligent-ui",
3
+ "version": "0.21.1",
4
+ "description": "A streaming UI catalog, compiler and bounded expression interpreter without a framework or DOM.",
5
+ "license": "FSL-1.1-MIT",
6
+ "author": "Bas Milius",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/basmilius/adecore.git",
10
+ "directory": "packages/intelligent-ui"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/basmilius/adecore/issues"
14
+ },
15
+ "homepage": "https://adecore.dev/intelligent-ui/",
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/fixtures"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "source": "./src/index.ts",
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
11
29
  },
12
- "bugs": {
13
- "url": "https://github.com/basmilius/adecore/issues"
30
+ "./links": {
31
+ "source": "./src/links.ts",
32
+ "types": "./dist/links.d.ts",
33
+ "default": "./dist/links.js"
14
34
  },
15
- "homepage": "https://adecore.dev/intelligent-ui/",
16
- "type": "module",
17
- "sideEffects": false,
18
- "files": [
19
- "dist",
20
- "src",
21
- "!src/**/*.test.ts",
22
- "!src/fixtures"
23
- ],
24
- "exports": {
25
- ".": {
26
- "source": "./src/index.ts",
27
- "types": "./dist/index.d.ts",
28
- "default": "./dist/index.js"
29
- },
30
- "./catalog": {
31
- "source": "./src/catalog.ts",
32
- "types": "./dist/catalog.d.ts",
33
- "default": "./dist/catalog.js"
34
- },
35
- "./budget": {
36
- "source": "./src/budget.ts",
37
- "types": "./dist/budget.d.ts",
38
- "default": "./dist/budget.js"
39
- },
40
- "./expression": {
41
- "source": "./src/expression.ts",
42
- "types": "./dist/expression.d.ts",
43
- "default": "./dist/expression.js"
44
- },
45
- "./syntax": {
46
- "source": "./src/syntax.ts",
47
- "types": "./dist/syntax.d.ts",
48
- "default": "./dist/syntax.js"
49
- },
50
- "./compiler": {
51
- "source": "./src/compiler.ts",
52
- "types": "./dist/compiler.d.ts",
53
- "default": "./dist/compiler.js"
54
- },
55
- "./runtime": {
56
- "source": "./src/runtime.ts",
57
- "types": "./dist/runtime.d.ts",
58
- "default": "./dist/runtime.js"
59
- },
60
- "./protocol": {
61
- "source": "./src/protocol.ts",
62
- "types": "./dist/protocol.d.ts",
63
- "default": "./dist/protocol.js"
64
- },
65
- "./text": {
66
- "source": "./src/text.ts",
67
- "types": "./dist/text.d.ts",
68
- "default": "./dist/text.js"
69
- }
35
+ "./query": {
36
+ "source": "./src/query.ts",
37
+ "types": "./dist/query.d.ts",
38
+ "default": "./dist/query.js"
70
39
  },
71
- "scripts": {
72
- "build": "bun scripts/build.ts",
73
- "typecheck": "tsc --noEmit",
74
- "test": "bun test --conditions=source"
40
+ "./catalog": {
41
+ "source": "./src/catalog.ts",
42
+ "types": "./dist/catalog.d.ts",
43
+ "default": "./dist/catalog.js"
75
44
  },
76
- "devDependencies": {
77
- "@types/bun": "^1.4.2",
78
- "@types/node": "^24.13.6",
79
- "typescript": "^7.0.2"
45
+ "./budget": {
46
+ "source": "./src/budget.ts",
47
+ "types": "./dist/budget.d.ts",
48
+ "default": "./dist/budget.js"
80
49
  },
81
- "dependencies": {
82
- "zod": "^4.6.5"
50
+ "./expression": {
51
+ "source": "./src/expression.ts",
52
+ "types": "./dist/expression.d.ts",
53
+ "default": "./dist/expression.js"
54
+ },
55
+ "./syntax": {
56
+ "source": "./src/syntax.ts",
57
+ "types": "./dist/syntax.d.ts",
58
+ "default": "./dist/syntax.js"
59
+ },
60
+ "./compiler": {
61
+ "source": "./src/compiler.ts",
62
+ "types": "./dist/compiler.d.ts",
63
+ "default": "./dist/compiler.js"
64
+ },
65
+ "./runtime": {
66
+ "source": "./src/runtime.ts",
67
+ "types": "./dist/runtime.d.ts",
68
+ "default": "./dist/runtime.js"
69
+ },
70
+ "./protocol": {
71
+ "source": "./src/protocol.ts",
72
+ "types": "./dist/protocol.d.ts",
73
+ "default": "./dist/protocol.js"
74
+ },
75
+ "./text": {
76
+ "source": "./src/text.ts",
77
+ "types": "./dist/text.d.ts",
78
+ "default": "./dist/text.js"
79
+ },
80
+ "./stream": {
81
+ "source": "./src/stream.ts",
82
+ "types": "./dist/stream.d.ts",
83
+ "default": "./dist/stream.js"
83
84
  }
85
+ },
86
+ "scripts": {
87
+ "build": "bun scripts/build.ts",
88
+ "typecheck": "tsc --noEmit",
89
+ "test": "bun test --conditions=source"
90
+ },
91
+ "devDependencies": {
92
+ "@types/bun": "^1.4.2",
93
+ "@types/node": "^24.13.6",
94
+ "typescript": "^7.0.2"
95
+ },
96
+ "dependencies": {
97
+ "zod": "^4.6.5"
98
+ }
84
99
  }
package/src/actions.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { UiFailure } from './budget.ts';
2
+ import { isUiComponent, UI_CATALOG } from './catalog.ts';
3
+ import type { UiBlock } from './compiler.ts';
4
+ import type { UiExpression } from './expression.ts';
5
+
6
+ /* The prop of a component that holds a state action, kept unevaluated, instead of a value. */
7
+ export function uiActionProp(type: string): string | undefined {
8
+ if (!isUiComponent(type)) {
9
+ return undefined;
10
+ }
11
+ const entry = UI_CATALOG[type];
12
+ return 'action' in entry ? entry.action : undefined;
13
+ }
14
+
15
+ /*
16
+ * The variable an action writes, or null for `@Reset()`, which restores them all. Throws for anything
17
+ * but `@Set($name, value)`, `@Reset($name)` and `@Reset()` on a variable that `local` accepts.
18
+ */
19
+ export function uiActionTarget(action: UiExpression, local: (name: string) => boolean): string | null {
20
+ if (action.kind !== 'call' || (action.name !== 'Set' && action.name !== 'Reset')) {
21
+ throw new UiFailure('refused_action', 'Expected @Set or @Reset.');
22
+ }
23
+ if (action.name === 'Reset' && action.args.length === 0) {
24
+ return null;
25
+ }
26
+ if (action.args.length !== (action.name === 'Set' ? 2 : 1)) {
27
+ throw new UiFailure('refused_action', 'Invalid state action arguments.');
28
+ }
29
+ const reference = action.args[0];
30
+ if (reference.kind !== 'reference' || !local(reference.name)) {
31
+ throw new UiFailure('refused_binding', 'The action needs a local state variable.');
32
+ }
33
+ return reference.name;
34
+ }
35
+
36
+ /* Whether `name` is local state of the block: declared with a default and never a query. */
37
+ export function uiLocalState(block: Pick<UiBlock, 'defaults' | 'queries'>): (name: string) => boolean {
38
+ return (name) => Object.hasOwn(block.defaults, name) && !Object.hasOwn(block.queries, name);
39
+ }
package/src/catalog.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { uiPropList } from './prop-list.ts';
2
3
 
3
4
  export const UI_CATALOG_VERSION = 1;
4
5
  export const UiToneSchema = z.enum(['neutral', 'info', 'success', 'warning', 'danger']);
@@ -21,7 +22,10 @@ export const UI_GROUPS = {
21
22
  rules: 'Image accepts a chat attachment or generated="latest", never a URL or a local path.'
22
23
  },
23
24
  links: { description: 'Refer to a host-owned resource.', rules: 'Targets are checked by the host. No arbitrary navigation or file reads.' },
24
- inputs: { description: 'Change local block state.', rules: 'Bind value={$name}. State stays local until a Choice sends visible context.' },
25
+ inputs: {
26
+ description: 'Change local block state.',
27
+ rules: 'Bind value={$name}. A Button sets a literal with action={@Set($name, value)} or restores a default with {@Reset($name)} or {@Reset()}. State stays local until a Choice sends visible context.'
28
+ },
25
29
  choices: {
26
30
  description: 'Ask the person to pick a next step.',
27
31
  rules: 'Give a short label and precise visible context. A Choice sends once after streaming ends.'
@@ -120,6 +124,12 @@ export const UI_CATALOG = {
120
124
  },
121
125
  Segmented: { group: 'inputs', description: 'Choose one local value.', schema: z.strictObject({ value: scalar }), children: ['Option'], binding: 'value' },
122
126
  Option: { group: 'inputs', description: 'One local alternative.', schema: z.strictObject({ value: scalar }), parent: 'Segmented' },
127
+ Button: {
128
+ group: 'inputs',
129
+ description: 'Run one local state action when pressed; children are the label.',
130
+ schema: z.strictObject({ disabled: z.boolean().optional() }),
131
+ action: 'action'
132
+ },
123
133
  Show: { group: 'inputs', description: 'Show children when the condition is true.', schema: z.strictObject({ when: z.boolean() }) },
124
134
  Each: {
125
135
  group: 'inputs',
@@ -144,12 +154,6 @@ export function isUiComponent(name: string): name is UiComponentName {
144
154
 
145
155
  export function uiCatalogText(): string {
146
156
  return Object.entries(UI_CATALOG)
147
- .map(([name, entry]) => {
148
- const shape = entry.schema.shape;
149
- const props = Object.entries(shape)
150
- .map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
151
- .join(', ');
152
- return `${name}(${props}): ${entry.description}`;
153
- })
157
+ .map(([name, entry]) => `${name}(${uiPropList(entry).join(', ')}): ${entry.description}`)
154
158
  .join('\n');
155
159
  }