@adecore/intelligent-ui 0.0.0 → 0.21.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/README.md +2 -2
- package/dist/compiler.d.ts +17 -0
- package/dist/compiler.js +130 -34
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/links.d.ts +47 -0
- package/dist/links.js +39 -0
- package/dist/protocol.js +2 -1
- package/dist/query.d.ts +6 -0
- package/dist/query.js +115 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +105 -0
- package/dist/stream.d.ts +28 -0
- package/dist/stream.js +56 -0
- package/dist/text.js +15 -4
- package/package.json +91 -76
- package/src/compiler.ts +148 -33
- package/src/index.ts +27 -2
- package/src/links.ts +49 -0
- package/src/protocol.ts +2 -1
- package/src/query.ts +121 -0
- package/src/runtime.ts +119 -0
- package/src/stream.ts +78 -0
- package/src/text.ts +16 -4
package/dist/stream.d.ts
ADDED
|
@@ -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
|
+
}
|
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.js
CHANGED
|
@@ -6,15 +6,25 @@ export function uiCompactCatalog() {
|
|
|
6
6
|
const props = Object.entries(entry.schema.shape)
|
|
7
7
|
.map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
|
|
8
8
|
.join(',');
|
|
9
|
-
return `${name}
|
|
9
|
+
return `${name}${props ? `: ${props}` : ''}`;
|
|
10
10
|
})
|
|
11
11
|
.join(' · ');
|
|
12
12
|
}
|
|
13
|
+
const UI_SYNTAX = `Write XML-like component tags inside a fenced ${UI_FENCE_LANGUAGE} block. Put text between opening and closing tags; empty components end with />. Quote string props; put numbers, booleans and expressions in braces.`;
|
|
14
|
+
const UI_EXAMPLE = [
|
|
15
|
+
'```' + UI_FENCE_LANGUAGE,
|
|
16
|
+
'$enabled = false',
|
|
17
|
+
'<Summary badge="Draft">Release overview</Summary>',
|
|
18
|
+
'<Stats><Stat label="Tests" value={42}/></Stats>',
|
|
19
|
+
'<Switch value={$enabled}>Notify me</Switch>',
|
|
20
|
+
'<Choices><Choice context="Review the release overview">Continue</Choice></Choices>',
|
|
21
|
+
'```'
|
|
22
|
+
].join('\n');
|
|
13
23
|
export function uiSessionNote() {
|
|
14
|
-
return
|
|
24
|
+
return `${UI_SYNTAX} Prose outside. Only named catalog props, no styling. $name = literal JSON; input value={$name}; 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${UI_EXAMPLE}\n`;
|
|
15
25
|
}
|
|
16
26
|
export function uiReferenceText() {
|
|
17
|
-
|
|
27
|
+
const groups = Object.entries(UI_GROUPS)
|
|
18
28
|
.map(([group, definition]) => {
|
|
19
29
|
const components = Object.entries(UI_CATALOG)
|
|
20
30
|
.filter(([, entry]) => entry.group === group)
|
|
@@ -22,11 +32,12 @@ export function uiReferenceText() {
|
|
|
22
32
|
const props = Object.entries(entry.schema.shape)
|
|
23
33
|
.map(([key, schema]) => `${key}${schema.isOptional() ? '?' : ''}`)
|
|
24
34
|
.join(', ');
|
|
25
|
-
return `${name}
|
|
35
|
+
return `${name}: ${entry.description}${props ? ` Props: ${props}.` : ''}`;
|
|
26
36
|
});
|
|
27
37
|
return [definition.description, definition.rules, ...components].join('\n');
|
|
28
38
|
})
|
|
29
39
|
.join('\n\n');
|
|
40
|
+
return `${UI_SYNTAX}\nExample:\n${UI_EXAMPLE}\n\n${groups}`;
|
|
30
41
|
}
|
|
31
42
|
export function uiFallbackText(text, blocks) {
|
|
32
43
|
let result = '';
|
package/package.json
CHANGED
|
@@ -1,84 +1,99 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
"name": "@adecore/intelligent-ui",
|
|
3
|
+
"version": "0.21.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"
|
|
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
|
-
"
|
|
13
|
-
|
|
30
|
+
"./links": {
|
|
31
|
+
"source": "./src/links.ts",
|
|
32
|
+
"types": "./dist/links.d.ts",
|
|
33
|
+
"default": "./dist/links.js"
|
|
14
34
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
40
|
+
"./catalog": {
|
|
41
|
+
"source": "./src/catalog.ts",
|
|
42
|
+
"types": "./dist/catalog.d.ts",
|
|
43
|
+
"default": "./dist/catalog.js"
|
|
75
44
|
},
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
45
|
+
"./budget": {
|
|
46
|
+
"source": "./src/budget.ts",
|
|
47
|
+
"types": "./dist/budget.d.ts",
|
|
48
|
+
"default": "./dist/budget.js"
|
|
80
49
|
},
|
|
81
|
-
"
|
|
82
|
-
|
|
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/compiler.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { type z } from 'zod';
|
|
|
6
6
|
|
|
7
7
|
export const UI_FENCE_LANGUAGE = 'ruimte-ui';
|
|
8
8
|
|
|
9
|
+
export const UI_REPLY_LIMITS = { blocks: 16, characters: 262144, nodes: 2048, milliseconds: 60 } as const;
|
|
10
|
+
|
|
9
11
|
export interface UiNode {
|
|
10
12
|
id: string;
|
|
11
13
|
type: string;
|
|
@@ -23,11 +25,13 @@ export interface UiNode {
|
|
|
23
25
|
export interface UiQuery {
|
|
24
26
|
source: string;
|
|
25
27
|
args: Record<string, UiValue>;
|
|
28
|
+
expression?: UiExpression;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
export interface UiBlock {
|
|
29
32
|
id: string;
|
|
30
33
|
catalogVersion: number;
|
|
34
|
+
revision?: string;
|
|
31
35
|
start: number;
|
|
32
36
|
end: number;
|
|
33
37
|
complete: boolean;
|
|
@@ -217,14 +221,29 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
|
|
|
217
221
|
try {
|
|
218
222
|
const syntax = parseUiSyntax(source, options.id, options.final, budget);
|
|
219
223
|
block.diagnostics = syntax.diagnostics;
|
|
220
|
-
for (const [name, expression] of Object.entries(syntax.declarations)
|
|
224
|
+
for (const [name, expression] of Object.entries(syntax.declarations).sort(
|
|
225
|
+
([, a], [, b]) => Number(a.kind === 'call' && a.name === 'Query') - Number(b.kind === 'call' && b.name === 'Query')
|
|
226
|
+
)) {
|
|
221
227
|
try {
|
|
222
228
|
if (expression.kind === 'call' && expression.name === 'Query') {
|
|
223
|
-
if (expression.args.length !== 2 || !expression.args.
|
|
224
|
-
throw new UiFailure('invalid_query', '@Query needs a literal source and
|
|
229
|
+
if (expression.args.length !== 2 || !isConstant(expression.args[0]) || expression.args[1].kind !== 'object') {
|
|
230
|
+
throw new UiFailure('invalid_query', '@Query needs a literal source and an argument record.');
|
|
225
231
|
}
|
|
226
232
|
const querySource = evaluateUiExpression(expression.args[0], {}, budget);
|
|
227
|
-
const
|
|
233
|
+
const argument = expression.args[1];
|
|
234
|
+
const references = (part: unknown): void => {
|
|
235
|
+
if (!part || typeof part !== 'object') {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if ('kind' in part && part.kind === 'reference' && 'name' in part && !Object.hasOwn(block.defaults, String(part.name))) {
|
|
239
|
+
throw new UiFailure('invalid_query', 'Query arguments may reference only declared local state.');
|
|
240
|
+
}
|
|
241
|
+
for (const value of Object.values(part)) {
|
|
242
|
+
references(value);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
references(argument);
|
|
246
|
+
const args = evaluateUiExpression(argument, block.defaults, budget);
|
|
228
247
|
if (typeof querySource !== 'string' || !args || Array.isArray(args) || typeof args !== 'object') {
|
|
229
248
|
throw new UiFailure('invalid_query', 'Invalid query source or argument record.');
|
|
230
249
|
}
|
|
@@ -235,7 +254,10 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
|
|
|
235
254
|
if (!checked.success) {
|
|
236
255
|
throw new UiFailure('invalid_query', 'Query arguments do not match the host schema.');
|
|
237
256
|
}
|
|
238
|
-
block.queries
|
|
257
|
+
if (Object.keys(block.queries).length >= 8) {
|
|
258
|
+
throw new UiFailure('budget_exceeded', 'A block may declare at most eight queries.');
|
|
259
|
+
}
|
|
260
|
+
block.queries[name] = { source: querySource, args, ...(!isConstant(argument) ? { expression: argument } : {}) };
|
|
239
261
|
} else {
|
|
240
262
|
if (!isConstant(expression)) {
|
|
241
263
|
throw new UiFailure('invalid_default', 'State defaults must be literal JSON values.');
|
|
@@ -267,36 +289,129 @@ export function compileUiBlock(source: string, options: UiCompileOptions): UiBlo
|
|
|
267
289
|
return block;
|
|
268
290
|
}
|
|
269
291
|
|
|
270
|
-
export
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
export type UiCompilerOptions = Omit<UiCompileOptions, 'final' | 'latestAttachment'>;
|
|
293
|
+
export type UiCompileUpdate = Pick<UiCompileOptions, 'final' | 'latestAttachment'>;
|
|
294
|
+
|
|
295
|
+
interface CachedUiBlock {
|
|
296
|
+
source: string;
|
|
297
|
+
block: UiBlock;
|
|
298
|
+
latestAttachment?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export class UiCompiler {
|
|
302
|
+
private readonly options: UiCompilerOptions;
|
|
303
|
+
private cache = new Map<number, CachedUiBlock>();
|
|
304
|
+
|
|
305
|
+
constructor(options: UiCompilerOptions) {
|
|
306
|
+
this.options = options;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
compile(text: string, update: UiCompileUpdate = {}): UiBlock[] {
|
|
310
|
+
const blocks: UiBlock[] = [];
|
|
311
|
+
const next = new Map<number, CachedUiBlock>();
|
|
312
|
+
let fence: { marker: string; length: number; start: number; codeStart: number; ui: boolean } | null = null;
|
|
313
|
+
let position = 0;
|
|
314
|
+
let characters = 0;
|
|
315
|
+
let nodes = 0;
|
|
316
|
+
let exhausted = false;
|
|
317
|
+
const now = this.options.now ?? Date.now;
|
|
318
|
+
const started = now();
|
|
319
|
+
const nodeCount = (items: readonly UiNode[]): number => items.reduce((count, node) => count + 1 + nodeCount(node.children), 0);
|
|
320
|
+
const rejected = (start: number, codeStart: number, codeEnd: number, end: number, complete: boolean): UiBlock => ({
|
|
321
|
+
id: `${this.options.id}:ui:${start}`,
|
|
322
|
+
catalogVersion: UI_CATALOG_VERSION,
|
|
323
|
+
start,
|
|
324
|
+
end,
|
|
325
|
+
complete,
|
|
326
|
+
defaults: {},
|
|
327
|
+
queries: {},
|
|
328
|
+
nodes: [],
|
|
329
|
+
fallback: text.slice(codeStart, Math.min(codeEnd, codeStart + 4096)),
|
|
330
|
+
diagnostics: [uiDiagnostic(new UiFailure('budget_exceeded', 'This reply exceeded its UI budget; remaining fences stay text.'), start, end)]
|
|
331
|
+
});
|
|
332
|
+
const append = (start: number, codeStart: number, codeEnd: number, end: number, closed: boolean) => {
|
|
333
|
+
if (exhausted) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (blocks.length >= UI_REPLY_LIMITS.blocks) {
|
|
337
|
+
const last = blocks.at(-1)!;
|
|
338
|
+
blocks[blocks.length - 1] = {
|
|
339
|
+
...last,
|
|
340
|
+
diagnostics: [
|
|
341
|
+
...last.diagnostics.slice(0, 19),
|
|
342
|
+
uiDiagnostic(new UiFailure('budget_exceeded', 'Further UI fences stay text because this reply has too many blocks.'), start, end)
|
|
343
|
+
]
|
|
344
|
+
};
|
|
345
|
+
exhausted = true;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const complete = closed || update.final === true;
|
|
349
|
+
characters += codeEnd - codeStart;
|
|
350
|
+
if (characters > UI_REPLY_LIMITS.characters || now() - started > UI_REPLY_LIMITS.milliseconds) {
|
|
351
|
+
blocks.push(rejected(start, codeStart, codeEnd, end, complete));
|
|
352
|
+
exhausted = true;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const source = text.slice(codeStart, codeEnd);
|
|
356
|
+
const cached = this.cache.get(start);
|
|
357
|
+
const unchanged =
|
|
358
|
+
!update.final &&
|
|
359
|
+
cached?.source === source &&
|
|
360
|
+
cached.block.end === end &&
|
|
361
|
+
cached.block.complete === complete &&
|
|
362
|
+
cached.latestAttachment === update.latestAttachment;
|
|
363
|
+
const block = unchanged
|
|
364
|
+
? cached.block
|
|
365
|
+
: compileUiBlock(source, {
|
|
366
|
+
...this.options,
|
|
367
|
+
...update,
|
|
368
|
+
id: `${this.options.id}:ui:${start}`,
|
|
369
|
+
final: complete
|
|
370
|
+
});
|
|
371
|
+
nodes += nodeCount(block.nodes);
|
|
372
|
+
if (nodes > UI_REPLY_LIMITS.nodes) {
|
|
373
|
+
blocks.push(rejected(start, codeStart, codeEnd, end, complete));
|
|
374
|
+
exhausted = true;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
block.start = start;
|
|
378
|
+
block.end = end;
|
|
379
|
+
blocks.push(block);
|
|
380
|
+
next.set(start, { source, block, latestAttachment: update.latestAttachment });
|
|
381
|
+
};
|
|
382
|
+
while (position < text.length && !exhausted) {
|
|
383
|
+
const newline = text.indexOf('\n', position);
|
|
384
|
+
const end = newline === -1 ? text.length : newline + 1;
|
|
385
|
+
const line = text.slice(position, end);
|
|
386
|
+
const opening = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)/.exec(line);
|
|
387
|
+
if (!fence && opening) {
|
|
388
|
+
fence = {
|
|
389
|
+
marker: opening[1][0],
|
|
390
|
+
length: opening[1].length,
|
|
391
|
+
start: position,
|
|
392
|
+
codeStart: position + line.length,
|
|
393
|
+
ui: opening[2].trim() === UI_FENCE_LANGUAGE
|
|
394
|
+
};
|
|
395
|
+
} else if (fence && opening && opening[1][0] === fence.marker && opening[1].length >= fence.length && !opening[2].trim()) {
|
|
396
|
+
if (fence.ui) {
|
|
397
|
+
append(fence.start, fence.codeStart, position, position + line.length, true);
|
|
398
|
+
}
|
|
399
|
+
fence = null;
|
|
290
400
|
}
|
|
291
|
-
|
|
401
|
+
position = end;
|
|
402
|
+
}
|
|
403
|
+
if (fence?.ui) {
|
|
404
|
+
append(fence.start, fence.codeStart, text.length, text.length, false);
|
|
292
405
|
}
|
|
293
|
-
|
|
406
|
+
this.cache = next;
|
|
407
|
+
return blocks;
|
|
294
408
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
block.end = text.length;
|
|
299
|
-
blocks.push(block);
|
|
409
|
+
|
|
410
|
+
clear(): void {
|
|
411
|
+
this.cache.clear();
|
|
300
412
|
}
|
|
301
|
-
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function compileUi(text: string, options: UiCompileOptions): UiBlock[] {
|
|
416
|
+
return new UiCompiler(options).compile(text, options);
|
|
302
417
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,32 @@ export {
|
|
|
12
12
|
export { UI_LIMITS, UiBudget, UiFailure, safeKey, type UiLimits } from './budget.ts';
|
|
13
13
|
export { copyUiValue, evaluateUiExpression, parseUiExpression, type UiExpression, type UiValue } from './expression.ts';
|
|
14
14
|
export { parseUiSyntax, uiDiagnostic, type UiDiagnostic, type UiSyntax, type UiSyntaxNode } from './syntax.ts';
|
|
15
|
-
export {
|
|
16
|
-
|
|
15
|
+
export {
|
|
16
|
+
UI_FENCE_LANGUAGE,
|
|
17
|
+
UI_REPLY_LIMITS,
|
|
18
|
+
UiCompiler,
|
|
19
|
+
compileUi,
|
|
20
|
+
compileUiBlock,
|
|
21
|
+
type UiCompilerOptions,
|
|
22
|
+
type UiCompileUpdate,
|
|
23
|
+
type UiBlock,
|
|
24
|
+
type UiCompileOptions,
|
|
25
|
+
type UiNode,
|
|
26
|
+
type UiQuery
|
|
27
|
+
} from './compiler.ts';
|
|
28
|
+
export {
|
|
29
|
+
evaluateUiBlock,
|
|
30
|
+
UiState,
|
|
31
|
+
uiInputValues,
|
|
32
|
+
resolveUiChoice,
|
|
33
|
+
type UiChoiceSelection,
|
|
34
|
+
type UiBinding,
|
|
35
|
+
type UiEvaluation,
|
|
36
|
+
type UiViewNode
|
|
37
|
+
} from './runtime.ts';
|
|
38
|
+
export { uiValidatedState, uiQueryArguments, uiQueryFallback } from './query.ts';
|
|
17
39
|
export { UiNodeSchema, UiDiagnosticSchema, UiBlockSchema, UiBlocksSchema } from './protocol.ts';
|
|
18
40
|
export { uiCompactCatalog, uiSessionNote, uiReferenceText, uiFallbackText } from './text.ts';
|
|
41
|
+
export { UI_STREAM_INTERVAL_MS, UiStream, type UiStreamClock, type UiStreamOptions, type UiStreamPreview } from './stream.ts';
|
|
42
|
+
|
|
43
|
+
export { UiLinkTargetSchema, UiLinkResolutionSchema, uiLinkTargets, type UiLinkTarget, type UiLinkResolution } from './links.ts';
|
package/src/links.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { UiBlock } from './compiler.ts';
|
|
3
|
+
import { evaluateUiBlock, type UiViewNode } from './runtime.ts';
|
|
4
|
+
import { uiValidatedState } from './query.ts';
|
|
5
|
+
|
|
6
|
+
export const UiLinkTargetSchema = z.discriminatedUnion('type', [
|
|
7
|
+
z.object({ type: z.literal('File'), path: z.string().min(1).max(4096), line: z.number().int().positive().optional() }).strict(),
|
|
8
|
+
z.object({ type: z.literal('Diff'), path: z.string().min(1).max(4096) }).strict(),
|
|
9
|
+
z.object({ type: z.literal('Commit'), sha: z.string().regex(/^[a-f\d]{7,64}$/i) }).strict(),
|
|
10
|
+
z.object({ type: z.literal('Node'), id: z.string().min(1).max(256) }).strict()
|
|
11
|
+
]);
|
|
12
|
+
export type UiLinkTarget = z.infer<typeof UiLinkTargetSchema>;
|
|
13
|
+
|
|
14
|
+
export const UiLinkResolutionSchema = z.object({
|
|
15
|
+
state: z.enum(['chip', 'plain']),
|
|
16
|
+
target: UiLinkTargetSchema.optional(),
|
|
17
|
+
cwd: z.string().optional(),
|
|
18
|
+
viewId: z.string().optional(),
|
|
19
|
+
projectId: z.string().optional(),
|
|
20
|
+
staged: z.boolean().optional(),
|
|
21
|
+
conflicted: z.boolean().optional(),
|
|
22
|
+
relativePath: z.string().optional(),
|
|
23
|
+
label: z.string().optional(),
|
|
24
|
+
reason: z.string().optional()
|
|
25
|
+
});
|
|
26
|
+
export type UiLinkResolution = z.infer<typeof UiLinkResolutionSchema>;
|
|
27
|
+
|
|
28
|
+
export function uiLinkTargets(
|
|
29
|
+
block: UiBlock,
|
|
30
|
+
input: Readonly<Record<string, unknown>> = {},
|
|
31
|
+
queries: Readonly<Record<string, unknown>> = {}
|
|
32
|
+
): Record<string, UiLinkTarget> {
|
|
33
|
+
const state = uiValidatedState(block, input, queries);
|
|
34
|
+
const targets: Record<string, UiLinkTarget> = Object.create(null);
|
|
35
|
+
const visit = (nodes: readonly UiViewNode[]) => {
|
|
36
|
+
for (const node of nodes) {
|
|
37
|
+
if (node.error || !node.complete) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const checked = UiLinkTargetSchema.safeParse({ type: node.type, ...node.props });
|
|
41
|
+
if (checked.success) {
|
|
42
|
+
targets[node.id] = checked.data;
|
|
43
|
+
}
|
|
44
|
+
visit(node.children);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
visit(evaluateUiBlock(block, state).nodes);
|
|
48
|
+
return targets;
|
|
49
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -32,11 +32,12 @@ export const UiDiagnosticSchema = z.object({
|
|
|
32
32
|
export const UiBlockSchema: z.ZodType<UiBlock> = z.object({
|
|
33
33
|
id: z.string(),
|
|
34
34
|
catalogVersion: position,
|
|
35
|
+
revision: z.string().min(1).optional(),
|
|
35
36
|
start: position,
|
|
36
37
|
end: position,
|
|
37
38
|
complete: z.boolean(),
|
|
38
39
|
defaults: values,
|
|
39
|
-
queries: z.record(z.string(), z.object({ source: z.string(), args: values })),
|
|
40
|
+
queries: z.record(z.string(), z.object({ source: z.string(), args: values, expression: z.unknown().optional() })),
|
|
40
41
|
nodes: z.array(UiNodeSchema),
|
|
41
42
|
diagnostics: z.array(UiDiagnosticSchema),
|
|
42
43
|
fallback: z.string()
|