@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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A streaming compiler and bounded expression interpreter for UI blocks in agent replies. The catalog defines props once with Zod. Renderers consume evaluated props and input bindings; they do not evaluate model text.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Chat wire integration and scheduling are provided by `@adecore/agents`; React renderers are provided by `@adecore/agents-react`. Hosts supply actions, query authorization and platform renderers. The compiler and interpreter do not authorize a query or an action.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import { compileUi, evaluateUiBlock, UiState } from '@adecore/intelligent-ui';
|
|
@@ -29,7 +29,7 @@ The parser recovers at the next tag or line after malformed input. Open tags rem
|
|
|
29
29
|
|
|
30
30
|
## Catalog
|
|
31
31
|
|
|
32
|
-
`UI_CATALOG` contains each component's schema, description, group and applicable parent, child or binding rule. `UI_GROUPS` contains the instructions shared by each group. `isUiComponent` recognizes a catalog name; `uiCatalogText` lists names, props and descriptions for tooling. `uiCompactCatalog` derives the short prop list from those schemas, `uiSessionNote` adds the
|
|
32
|
+
`UI_CATALOG` contains each component's schema, description, group and applicable parent, child or binding rule. `UI_GROUPS` contains the instructions shared by each group. `isUiComponent` recognizes a catalog name; `uiCatalogText` lists names, props and descriptions for tooling. `uiCompactCatalog` derives the short prop list from those schemas, `uiSessionNote` adds explicit tag syntax and a fenced example checked by the compiler, and `uiReferenceText` combines full descriptions with group rules. `uiFallbackText` replaces UI fences with their compiled fallback while retaining surrounding prose.
|
|
33
33
|
|
|
34
34
|
`UiComponentName` is the key union. `UiProps<Name>` is inferred from that component's schema. `UiToneSchema` and `UiTone` provide neutral, info, success, warning and danger. `UI_CATALOG_VERSION` identifies this catalog.
|
|
35
35
|
|
package/dist/compiler.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { type UiExpression, type UiValue } from './expression.ts';
|
|
|
3
3
|
import { type UiDiagnostic } from './syntax.ts';
|
|
4
4
|
import { type z } from 'zod';
|
|
5
5
|
export declare const UI_FENCE_LANGUAGE = "ruimte-ui";
|
|
6
|
+
export declare const UI_REPLY_LIMITS: {
|
|
7
|
+
readonly blocks: 16;
|
|
8
|
+
readonly characters: 262144;
|
|
9
|
+
readonly nodes: 2048;
|
|
10
|
+
readonly milliseconds: 60;
|
|
11
|
+
};
|
|
6
12
|
export interface UiNode {
|
|
7
13
|
id: string;
|
|
8
14
|
type: string;
|
|
@@ -19,10 +25,12 @@ export interface UiNode {
|
|
|
19
25
|
export interface UiQuery {
|
|
20
26
|
source: string;
|
|
21
27
|
args: Record<string, UiValue>;
|
|
28
|
+
expression?: UiExpression;
|
|
22
29
|
}
|
|
23
30
|
export interface UiBlock {
|
|
24
31
|
id: string;
|
|
25
32
|
catalogVersion: number;
|
|
33
|
+
revision?: string;
|
|
26
34
|
start: number;
|
|
27
35
|
end: number;
|
|
28
36
|
complete: boolean;
|
|
@@ -41,4 +49,13 @@ export interface UiCompileOptions {
|
|
|
41
49
|
now?: () => number;
|
|
42
50
|
}
|
|
43
51
|
export declare function compileUiBlock(source: string, options: UiCompileOptions): UiBlock;
|
|
52
|
+
export type UiCompilerOptions = Omit<UiCompileOptions, 'final' | 'latestAttachment'>;
|
|
53
|
+
export type UiCompileUpdate = Pick<UiCompileOptions, 'final' | 'latestAttachment'>;
|
|
54
|
+
export declare class UiCompiler {
|
|
55
|
+
private readonly options;
|
|
56
|
+
private cache;
|
|
57
|
+
constructor(options: UiCompilerOptions);
|
|
58
|
+
compile(text: string, update?: UiCompileUpdate): UiBlock[];
|
|
59
|
+
clear(): void;
|
|
60
|
+
}
|
|
44
61
|
export declare function compileUi(text: string, options: UiCompileOptions): UiBlock[];
|
package/dist/compiler.js
CHANGED
|
@@ -4,6 +4,7 @@ import { evaluateUiExpression } from './expression.js';
|
|
|
4
4
|
import { parseUiSyntax, uiDiagnostic } from './syntax.js';
|
|
5
5
|
import {} from 'zod';
|
|
6
6
|
export const UI_FENCE_LANGUAGE = 'ruimte-ui';
|
|
7
|
+
export const UI_REPLY_LIMITS = { blocks: 16, characters: 262144, nodes: 2048, milliseconds: 60 };
|
|
7
8
|
function isConstant(expression) {
|
|
8
9
|
switch (expression.kind) {
|
|
9
10
|
case 'literal':
|
|
@@ -178,14 +179,27 @@ export function compileUiBlock(source, options) {
|
|
|
178
179
|
try {
|
|
179
180
|
const syntax = parseUiSyntax(source, options.id, options.final, budget);
|
|
180
181
|
block.diagnostics = syntax.diagnostics;
|
|
181
|
-
for (const [name, expression] of Object.entries(syntax.declarations)) {
|
|
182
|
+
for (const [name, expression] of Object.entries(syntax.declarations).sort(([, a], [, b]) => Number(a.kind === 'call' && a.name === 'Query') - Number(b.kind === 'call' && b.name === 'Query'))) {
|
|
182
183
|
try {
|
|
183
184
|
if (expression.kind === 'call' && expression.name === 'Query') {
|
|
184
|
-
if (expression.args.length !== 2 || !expression.args.
|
|
185
|
-
throw new UiFailure('invalid_query', '@Query needs a literal source and
|
|
185
|
+
if (expression.args.length !== 2 || !isConstant(expression.args[0]) || expression.args[1].kind !== 'object') {
|
|
186
|
+
throw new UiFailure('invalid_query', '@Query needs a literal source and an argument record.');
|
|
186
187
|
}
|
|
187
188
|
const querySource = evaluateUiExpression(expression.args[0], {}, budget);
|
|
188
|
-
const
|
|
189
|
+
const argument = expression.args[1];
|
|
190
|
+
const references = (part) => {
|
|
191
|
+
if (!part || typeof part !== 'object') {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if ('kind' in part && part.kind === 'reference' && 'name' in part && !Object.hasOwn(block.defaults, String(part.name))) {
|
|
195
|
+
throw new UiFailure('invalid_query', 'Query arguments may reference only declared local state.');
|
|
196
|
+
}
|
|
197
|
+
for (const value of Object.values(part)) {
|
|
198
|
+
references(value);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
references(argument);
|
|
202
|
+
const args = evaluateUiExpression(argument, block.defaults, budget);
|
|
189
203
|
if (typeof querySource !== 'string' || !args || Array.isArray(args) || typeof args !== 'object') {
|
|
190
204
|
throw new UiFailure('invalid_query', 'Invalid query source or argument record.');
|
|
191
205
|
}
|
|
@@ -196,7 +210,10 @@ export function compileUiBlock(source, options) {
|
|
|
196
210
|
if (!checked.success) {
|
|
197
211
|
throw new UiFailure('invalid_query', 'Query arguments do not match the host schema.');
|
|
198
212
|
}
|
|
199
|
-
block.queries
|
|
213
|
+
if (Object.keys(block.queries).length >= 8) {
|
|
214
|
+
throw new UiFailure('budget_exceeded', 'A block may declare at most eight queries.');
|
|
215
|
+
}
|
|
216
|
+
block.queries[name] = { source: querySource, args, ...(!isConstant(argument) ? { expression: argument } : {}) };
|
|
200
217
|
}
|
|
201
218
|
else {
|
|
202
219
|
if (!isConstant(expression)) {
|
|
@@ -230,37 +247,116 @@ export function compileUiBlock(source, options) {
|
|
|
230
247
|
}
|
|
231
248
|
return block;
|
|
232
249
|
}
|
|
233
|
-
export
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
export class UiCompiler {
|
|
251
|
+
options;
|
|
252
|
+
cache = new Map();
|
|
253
|
+
constructor(options) {
|
|
254
|
+
this.options = options;
|
|
255
|
+
}
|
|
256
|
+
compile(text, update = {}) {
|
|
257
|
+
const blocks = [];
|
|
258
|
+
const next = new Map();
|
|
259
|
+
let fence = null;
|
|
260
|
+
let position = 0;
|
|
261
|
+
let characters = 0;
|
|
262
|
+
let nodes = 0;
|
|
263
|
+
let exhausted = false;
|
|
264
|
+
const now = this.options.now ?? Date.now;
|
|
265
|
+
const started = now();
|
|
266
|
+
const nodeCount = (items) => items.reduce((count, node) => count + 1 + nodeCount(node.children), 0);
|
|
267
|
+
const rejected = (start, codeStart, codeEnd, end, complete) => ({
|
|
268
|
+
id: `${this.options.id}:ui:${start}`,
|
|
269
|
+
catalogVersion: UI_CATALOG_VERSION,
|
|
270
|
+
start,
|
|
271
|
+
end,
|
|
272
|
+
complete,
|
|
273
|
+
defaults: {},
|
|
274
|
+
queries: {},
|
|
275
|
+
nodes: [],
|
|
276
|
+
fallback: text.slice(codeStart, Math.min(codeEnd, codeStart + 4096)),
|
|
277
|
+
diagnostics: [uiDiagnostic(new UiFailure('budget_exceeded', 'This reply exceeded its UI budget; remaining fences stay text.'), start, end)]
|
|
278
|
+
});
|
|
279
|
+
const append = (start, codeStart, codeEnd, end, closed) => {
|
|
280
|
+
if (exhausted) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (blocks.length >= UI_REPLY_LIMITS.blocks) {
|
|
284
|
+
const last = blocks.at(-1);
|
|
285
|
+
blocks[blocks.length - 1] = {
|
|
286
|
+
...last,
|
|
287
|
+
diagnostics: [
|
|
288
|
+
...last.diagnostics.slice(0, 19),
|
|
289
|
+
uiDiagnostic(new UiFailure('budget_exceeded', 'Further UI fences stay text because this reply has too many blocks.'), start, end)
|
|
290
|
+
]
|
|
291
|
+
};
|
|
292
|
+
exhausted = true;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const complete = closed || update.final === true;
|
|
296
|
+
characters += codeEnd - codeStart;
|
|
297
|
+
if (characters > UI_REPLY_LIMITS.characters || now() - started > UI_REPLY_LIMITS.milliseconds) {
|
|
298
|
+
blocks.push(rejected(start, codeStart, codeEnd, end, complete));
|
|
299
|
+
exhausted = true;
|
|
300
|
+
return;
|
|
254
301
|
}
|
|
255
|
-
|
|
302
|
+
const source = text.slice(codeStart, codeEnd);
|
|
303
|
+
const cached = this.cache.get(start);
|
|
304
|
+
const unchanged = !update.final &&
|
|
305
|
+
cached?.source === source &&
|
|
306
|
+
cached.block.end === end &&
|
|
307
|
+
cached.block.complete === complete &&
|
|
308
|
+
cached.latestAttachment === update.latestAttachment;
|
|
309
|
+
const block = unchanged
|
|
310
|
+
? cached.block
|
|
311
|
+
: compileUiBlock(source, {
|
|
312
|
+
...this.options,
|
|
313
|
+
...update,
|
|
314
|
+
id: `${this.options.id}:ui:${start}`,
|
|
315
|
+
final: complete
|
|
316
|
+
});
|
|
317
|
+
nodes += nodeCount(block.nodes);
|
|
318
|
+
if (nodes > UI_REPLY_LIMITS.nodes) {
|
|
319
|
+
blocks.push(rejected(start, codeStart, codeEnd, end, complete));
|
|
320
|
+
exhausted = true;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
block.start = start;
|
|
324
|
+
block.end = end;
|
|
325
|
+
blocks.push(block);
|
|
326
|
+
next.set(start, { source, block, latestAttachment: update.latestAttachment });
|
|
327
|
+
};
|
|
328
|
+
while (position < text.length && !exhausted) {
|
|
329
|
+
const newline = text.indexOf('\n', position);
|
|
330
|
+
const end = newline === -1 ? text.length : newline + 1;
|
|
331
|
+
const line = text.slice(position, end);
|
|
332
|
+
const opening = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)/.exec(line);
|
|
333
|
+
if (!fence && opening) {
|
|
334
|
+
fence = {
|
|
335
|
+
marker: opening[1][0],
|
|
336
|
+
length: opening[1].length,
|
|
337
|
+
start: position,
|
|
338
|
+
codeStart: position + line.length,
|
|
339
|
+
ui: opening[2].trim() === UI_FENCE_LANGUAGE
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
else if (fence && opening && opening[1][0] === fence.marker && opening[1].length >= fence.length && !opening[2].trim()) {
|
|
343
|
+
if (fence.ui) {
|
|
344
|
+
append(fence.start, fence.codeStart, position, position + line.length, true);
|
|
345
|
+
}
|
|
346
|
+
fence = null;
|
|
347
|
+
}
|
|
348
|
+
position = end;
|
|
349
|
+
}
|
|
350
|
+
if (fence?.ui) {
|
|
351
|
+
append(fence.start, fence.codeStart, text.length, text.length, false);
|
|
256
352
|
}
|
|
257
|
-
|
|
353
|
+
this.cache = next;
|
|
354
|
+
return blocks;
|
|
258
355
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
block.start = fence.start;
|
|
262
|
-
block.end = text.length;
|
|
263
|
-
blocks.push(block);
|
|
356
|
+
clear() {
|
|
357
|
+
this.cache.clear();
|
|
264
358
|
}
|
|
265
|
-
|
|
359
|
+
}
|
|
360
|
+
export function compileUi(text, options) {
|
|
361
|
+
return new UiCompiler(options).compile(text, options);
|
|
266
362
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ export { UI_CATALOG, UI_CATALOG_VERSION, UI_GROUPS, UiToneSchema, isUiComponent,
|
|
|
2
2
|
export { UI_LIMITS, UiBudget, UiFailure, safeKey, type UiLimits } from './budget.ts';
|
|
3
3
|
export { copyUiValue, evaluateUiExpression, parseUiExpression, 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_REPLY_LIMITS, UiCompiler, compileUi, compileUiBlock, 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
9
|
export { uiCompactCatalog, uiSessionNote, uiReferenceText, uiFallbackText } 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
|
@@ -2,7 +2,10 @@ export { UI_CATALOG, UI_CATALOG_VERSION, UI_GROUPS, UiToneSchema, isUiComponent,
|
|
|
2
2
|
export { UI_LIMITS, UiBudget, UiFailure, safeKey } from './budget.js';
|
|
3
3
|
export { copyUiValue, evaluateUiExpression, parseUiExpression } 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_REPLY_LIMITS, UiCompiler, compileUi, compileUiBlock } 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';
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { UiBlock } from './compiler.ts';
|
|
3
|
+
export declare const UiLinkTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
4
|
+
type: z.ZodLiteral<"File">;
|
|
5
|
+
path: z.ZodString;
|
|
6
|
+
line: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
8
|
+
type: z.ZodLiteral<"Diff">;
|
|
9
|
+
path: z.ZodString;
|
|
10
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
11
|
+
type: z.ZodLiteral<"Commit">;
|
|
12
|
+
sha: z.ZodString;
|
|
13
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14
|
+
type: z.ZodLiteral<"Node">;
|
|
15
|
+
id: z.ZodString;
|
|
16
|
+
}, z.core.$strict>], "type">;
|
|
17
|
+
export type UiLinkTarget = z.infer<typeof UiLinkTargetSchema>;
|
|
18
|
+
export declare const UiLinkResolutionSchema: z.ZodObject<{
|
|
19
|
+
state: z.ZodEnum<{
|
|
20
|
+
chip: "chip";
|
|
21
|
+
plain: "plain";
|
|
22
|
+
}>;
|
|
23
|
+
target: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
24
|
+
type: z.ZodLiteral<"File">;
|
|
25
|
+
path: z.ZodString;
|
|
26
|
+
line: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
28
|
+
type: z.ZodLiteral<"Diff">;
|
|
29
|
+
path: z.ZodString;
|
|
30
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"Commit">;
|
|
32
|
+
sha: z.ZodString;
|
|
33
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
|
+
type: z.ZodLiteral<"Node">;
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
}, z.core.$strict>], "type">>;
|
|
37
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
38
|
+
viewId: z.ZodOptional<z.ZodString>;
|
|
39
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
40
|
+
staged: z.ZodOptional<z.ZodBoolean>;
|
|
41
|
+
conflicted: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
relativePath: z.ZodOptional<z.ZodString>;
|
|
43
|
+
label: z.ZodOptional<z.ZodString>;
|
|
44
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
export type UiLinkResolution = z.infer<typeof UiLinkResolutionSchema>;
|
|
47
|
+
export declare function uiLinkTargets(block: UiBlock, input?: Readonly<Record<string, unknown>>, queries?: Readonly<Record<string, unknown>>): Record<string, UiLinkTarget>;
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
export const UiLinkResolutionSchema = z.object({
|
|
11
|
+
state: z.enum(['chip', 'plain']),
|
|
12
|
+
target: UiLinkTargetSchema.optional(),
|
|
13
|
+
cwd: z.string().optional(),
|
|
14
|
+
viewId: z.string().optional(),
|
|
15
|
+
projectId: z.string().optional(),
|
|
16
|
+
staged: z.boolean().optional(),
|
|
17
|
+
conflicted: z.boolean().optional(),
|
|
18
|
+
relativePath: z.string().optional(),
|
|
19
|
+
label: z.string().optional(),
|
|
20
|
+
reason: z.string().optional()
|
|
21
|
+
});
|
|
22
|
+
export function uiLinkTargets(block, input = {}, queries = {}) {
|
|
23
|
+
const state = uiValidatedState(block, input, queries);
|
|
24
|
+
const targets = Object.create(null);
|
|
25
|
+
const visit = (nodes) => {
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
if (node.error || !node.complete) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const checked = UiLinkTargetSchema.safeParse({ type: node.type, ...node.props });
|
|
31
|
+
if (checked.success) {
|
|
32
|
+
targets[node.id] = checked.data;
|
|
33
|
+
}
|
|
34
|
+
visit(node.children);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
visit(evaluateUiBlock(block, state).nodes);
|
|
38
|
+
return targets;
|
|
39
|
+
}
|
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()
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { UiBlock } from './compiler.ts';
|
|
2
|
+
import { type UiValue } from './expression.ts';
|
|
3
|
+
import { UiState } from './runtime.ts';
|
|
4
|
+
export declare function uiValidatedState(block: UiBlock, input?: Readonly<Record<string, unknown>>, queries?: Readonly<Record<string, unknown>>): UiState;
|
|
5
|
+
export declare function uiQueryArguments(block: UiBlock, name: string, input?: Readonly<Record<string, unknown>>): Record<string, UiValue>;
|
|
6
|
+
export declare function uiQueryFallback(block: UiBlock, queries: Readonly<Record<string, unknown>>): string;
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { UI_CATALOG_VERSION } from './catalog.js';
|
|
2
|
+
import { UiBudget, UiFailure } from './budget.js';
|
|
3
|
+
import { copyUiValue, evaluateUiExpression } from './expression.js';
|
|
4
|
+
import { evaluateUiBlock, uiInputValues, UiState } from './runtime.js';
|
|
5
|
+
function findBinding(nodes, id, prop) {
|
|
6
|
+
for (const node of nodes) {
|
|
7
|
+
if (node.id === id) {
|
|
8
|
+
return node.bindings[prop];
|
|
9
|
+
}
|
|
10
|
+
const found = findBinding(node.children, id, prop);
|
|
11
|
+
if (found) {
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
export function uiValidatedState(block, input = {}, queries = {}) {
|
|
18
|
+
if (!block.complete || block.catalogVersion !== UI_CATALOG_VERSION) {
|
|
19
|
+
throw new UiFailure('invalid_block', 'Only completed supported blocks can resolve input.');
|
|
20
|
+
}
|
|
21
|
+
const values = copyUiValue(input, new UiBudget());
|
|
22
|
+
const state = new UiState(block);
|
|
23
|
+
for (const [name, value] of Object.entries(queries)) {
|
|
24
|
+
state.setQuery(name, value, block);
|
|
25
|
+
}
|
|
26
|
+
const allowed = uiInputValues(block, state);
|
|
27
|
+
for (const [key, value] of Object.entries(values)) {
|
|
28
|
+
if (!Object.hasOwn(allowed, key)) {
|
|
29
|
+
throw new UiFailure('refused_binding', 'Only declared input bindings can be submitted.');
|
|
30
|
+
}
|
|
31
|
+
state.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
const visible = new Set();
|
|
34
|
+
const visit = (nodes) => {
|
|
35
|
+
for (const node of nodes) {
|
|
36
|
+
if (node.error || !node.complete) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
let valid = true;
|
|
40
|
+
if (node.type === 'Segmented') {
|
|
41
|
+
valid = node.children.some((option) => option.type === 'Option' && !option.error && option.props.value === node.props.value);
|
|
42
|
+
}
|
|
43
|
+
if (node.type === 'Checklist') {
|
|
44
|
+
const options = node.children.filter((item) => item.type === 'Item' && !item.error).map((item) => item.props.value);
|
|
45
|
+
valid = node.props.value.every((value) => options.includes(value));
|
|
46
|
+
}
|
|
47
|
+
if (valid) {
|
|
48
|
+
for (const [key, binding] of Object.entries(node.bindings)) {
|
|
49
|
+
const original = findBinding(block.nodes, node.sourceId ?? node.id, key);
|
|
50
|
+
if (original && binding) {
|
|
51
|
+
visible.add(original);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
visit(node.children);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const evaluated = evaluateUiBlock(block, state);
|
|
59
|
+
if (evaluated.diagnostics.some((diagnostic) => diagnostic.code === 'budget_exceeded')) {
|
|
60
|
+
throw new UiFailure('budget_exceeded', 'The query inputs exceeded their evaluation budget.');
|
|
61
|
+
}
|
|
62
|
+
visit(evaluated.nodes);
|
|
63
|
+
for (const [key, value] of Object.entries(values)) {
|
|
64
|
+
if (JSON.stringify(value) !== JSON.stringify(allowed[key]) && !visible.has(key)) {
|
|
65
|
+
throw new UiFailure('invalid_value', 'An input value is outside the visible control’s allowed values.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
export function uiQueryArguments(block, name, input = {}) {
|
|
71
|
+
if (!Object.hasOwn(block.queries, name)) {
|
|
72
|
+
throw new UiFailure('invalid_query', 'This block does not declare that query.');
|
|
73
|
+
}
|
|
74
|
+
const state = uiValidatedState(block, input);
|
|
75
|
+
const query = block.queries[name];
|
|
76
|
+
return query.expression
|
|
77
|
+
? evaluateUiExpression(query.expression, state.scope(), new UiBudget())
|
|
78
|
+
: copyUiValue(query.args, new UiBudget());
|
|
79
|
+
}
|
|
80
|
+
export function uiQueryFallback(block, queries) {
|
|
81
|
+
const state = new UiState(block);
|
|
82
|
+
for (const [name, value] of Object.entries(queries)) {
|
|
83
|
+
state.setQuery(name, value, block);
|
|
84
|
+
}
|
|
85
|
+
const plain = (value) => (value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
86
|
+
const text = (nodes) => nodes
|
|
87
|
+
.map((node) => {
|
|
88
|
+
if (node.error) {
|
|
89
|
+
return node.fallback;
|
|
90
|
+
}
|
|
91
|
+
if (node.type === '$text') {
|
|
92
|
+
return plain(node.props.text);
|
|
93
|
+
}
|
|
94
|
+
const props = node.props;
|
|
95
|
+
const children = text(node.children);
|
|
96
|
+
if (node.type === 'Stat') {
|
|
97
|
+
return `${plain(props.label)}: ${plain(props.value)}${props.unit ? ` ${plain(props.unit)}` : ''}\n`;
|
|
98
|
+
}
|
|
99
|
+
if (node.type === 'Table') {
|
|
100
|
+
return `${plain(props.rows)}\n`;
|
|
101
|
+
}
|
|
102
|
+
if (node.type === 'Chart') {
|
|
103
|
+
return `${plain(props.data)}\n`;
|
|
104
|
+
}
|
|
105
|
+
if (node.type === 'Source') {
|
|
106
|
+
return `${plain(props.label)}: ${plain(props.url)}\n`;
|
|
107
|
+
}
|
|
108
|
+
if (node.type === 'Image') {
|
|
109
|
+
return node.fallback;
|
|
110
|
+
}
|
|
111
|
+
return `${(props.label ?? props.title) ? `${plain(props.label ?? props.title)}: ` : ''}${plain(props.path ?? props.sha ?? (node.type === 'Node' ? props.id : null))}${children}${props.context ? `\n${plain(props.context)}` : ''}${node.type === 'Tag' ? '' : '\n'}`;
|
|
112
|
+
})
|
|
113
|
+
.join('');
|
|
114
|
+
return text(evaluateUiBlock(block, state).nodes).trim() || block.fallback;
|
|
115
|
+
}
|
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>>>;
|
|
@@ -39,3 +40,10 @@ export declare class UiState {
|
|
|
39
40
|
private changed;
|
|
40
41
|
}
|
|
41
42
|
export declare function evaluateUiBlock(block: UiBlock, state?: UiState, limits?: Partial<UiLimits>): UiEvaluation;
|
|
43
|
+
export interface UiChoiceSelection {
|
|
44
|
+
label: string;
|
|
45
|
+
context: string;
|
|
46
|
+
values: Record<string, UiValue>;
|
|
47
|
+
}
|
|
48
|
+
export declare function uiInputValues(block: UiBlock, state: UiState): Record<string, UiValue>;
|
|
49
|
+
export declare function resolveUiChoice(block: UiBlock, choiceId: string, input?: Readonly<Record<string, unknown>>, queries?: Readonly<Record<string, unknown>>): UiChoiceSelection;
|
package/dist/runtime.js
CHANGED
|
@@ -119,6 +119,7 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
|
|
|
119
119
|
const visit = (node, variables, depth, suffix = '') => {
|
|
120
120
|
const view = {
|
|
121
121
|
id: `${node.id}${suffix}`,
|
|
122
|
+
sourceId: node.id,
|
|
122
123
|
type: node.type,
|
|
123
124
|
props: {},
|
|
124
125
|
bindings: {},
|
|
@@ -212,3 +213,107 @@ export function evaluateUiBlock(block, state = new UiState(block), limits = {})
|
|
|
212
213
|
}
|
|
213
214
|
return result;
|
|
214
215
|
}
|
|
216
|
+
export function uiInputValues(block, state) {
|
|
217
|
+
const scope = state.scope();
|
|
218
|
+
const values = Object.create(null);
|
|
219
|
+
const visit = (nodes) => {
|
|
220
|
+
for (const node of nodes) {
|
|
221
|
+
if (isUiComponent(node.type) && !node.error && node.complete) {
|
|
222
|
+
const entry = UI_CATALOG[node.type];
|
|
223
|
+
for (const [prop, name] of Object.entries(node.bindings)) {
|
|
224
|
+
if ('binding' in entry && entry.binding === prop && Object.hasOwn(block.defaults, name) && !Object.hasOwn(block.queries, name)) {
|
|
225
|
+
values[name] = scope[name];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
visit(node.children);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
visit(block.nodes);
|
|
233
|
+
return values;
|
|
234
|
+
}
|
|
235
|
+
/* The host supplies its stored block; client data can change only declared input bindings. */
|
|
236
|
+
export function resolveUiChoice(block, choiceId, input = {}, queries = {}) {
|
|
237
|
+
if (!block.complete || block.catalogVersion !== UI_CATALOG_VERSION) {
|
|
238
|
+
throw new UiFailure('refused_choice', 'Only a completed supported block can send a choice.');
|
|
239
|
+
}
|
|
240
|
+
const values = copyUiValue(input, new UiBudget());
|
|
241
|
+
const state = new UiState(block);
|
|
242
|
+
for (const [name, value] of Object.entries(queries)) {
|
|
243
|
+
state.setQuery(name, value, block);
|
|
244
|
+
}
|
|
245
|
+
const allowed = uiInputValues(block, state);
|
|
246
|
+
const changed = new Set();
|
|
247
|
+
for (const [name, value] of Object.entries(values)) {
|
|
248
|
+
if (!Object.hasOwn(allowed, name)) {
|
|
249
|
+
throw new UiFailure('refused_binding', 'Only declared input bindings can be submitted.');
|
|
250
|
+
}
|
|
251
|
+
if (JSON.stringify(value) !== JSON.stringify(allowed[name])) {
|
|
252
|
+
state.set(name, value);
|
|
253
|
+
changed.add(name);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const evaluated = evaluateUiBlock(block, state);
|
|
257
|
+
if (evaluated.diagnostics.some((diagnostic) => diagnostic.code === 'budget_exceeded')) {
|
|
258
|
+
throw new UiFailure('budget_exceeded', 'The choice exceeded its evaluation budget.');
|
|
259
|
+
}
|
|
260
|
+
const validInputs = new Set();
|
|
261
|
+
let choice;
|
|
262
|
+
const written = new Map();
|
|
263
|
+
const remember = (nodes) => {
|
|
264
|
+
for (const node of nodes) {
|
|
265
|
+
written.set(node.id, node);
|
|
266
|
+
remember(node.children);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
remember(block.nodes);
|
|
270
|
+
const visit = (nodes, parent) => {
|
|
271
|
+
for (const node of nodes) {
|
|
272
|
+
const source = written.get(node.sourceId ?? node.id);
|
|
273
|
+
if (node.error && source && Object.values(source.bindings).some((name) => changed.has(name))) {
|
|
274
|
+
throw new UiFailure('invalid_value', 'An input value does not match its control.');
|
|
275
|
+
}
|
|
276
|
+
if (node.error || !node.complete) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
let valid = true;
|
|
280
|
+
if (node.type === 'Segmented') {
|
|
281
|
+
valid = node.children.some((option) => option.type === 'Option' && !option.error && option.props.value === node.props.value);
|
|
282
|
+
}
|
|
283
|
+
else if (node.type === 'Checklist') {
|
|
284
|
+
const options = node.children.filter((item) => item.type === 'Item' && !item.error).map((item) => item.props.value);
|
|
285
|
+
valid = node.props.value.every((value) => options.includes(value));
|
|
286
|
+
}
|
|
287
|
+
if (valid && source) {
|
|
288
|
+
for (const prop of Object.keys(node.bindings)) {
|
|
289
|
+
validInputs.add(source.bindings[prop]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (node.id === choiceId && node.type === 'Choice' && parent === 'Choices' && node.props.disabled !== true) {
|
|
293
|
+
choice = node;
|
|
294
|
+
}
|
|
295
|
+
visit(node.children, node.type);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
visit(evaluated.nodes);
|
|
299
|
+
if ([...changed].some((name) => !validInputs.has(name))) {
|
|
300
|
+
throw new UiFailure('invalid_value', 'An input value is outside the visible control’s allowed values.');
|
|
301
|
+
}
|
|
302
|
+
if (!choice) {
|
|
303
|
+
throw new UiFailure('refused_choice', 'This choice is not available in the current block.');
|
|
304
|
+
}
|
|
305
|
+
const text = (nodes) => nodes.map((node) => (node.type === '$text' ? String(node.props.text ?? '') : text(node.children))).join('');
|
|
306
|
+
const hasError = (nodes) => nodes.some((node) => node.error !== undefined || hasError(node.children));
|
|
307
|
+
if (hasError(choice.children)) {
|
|
308
|
+
throw new UiFailure('refused_choice', 'The choice label could not be fully read.');
|
|
309
|
+
}
|
|
310
|
+
const label = text(choice.children).replace(/\s+/g, ' ').trim();
|
|
311
|
+
if (!label) {
|
|
312
|
+
throw new UiFailure('refused_choice', 'A choice needs a visible label.');
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
label,
|
|
316
|
+
values: uiInputValues(block, state),
|
|
317
|
+
context: typeof choice.props.context === 'string' && choice.props.context.trim() ? choice.props.context : label
|
|
318
|
+
};
|
|
319
|
+
}
|