@urbicon-ui/blocks 6.40.4 → 6.43.2
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 +1 -1
- package/dist/components/Calendar/calendar.variants.d.ts +63 -63
- package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
- package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
- package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
- package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
- package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
- package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
- package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
- package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
- package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
- package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
- package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
- package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
- package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
- package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
- package/dist/components/Chat/A2UIView/index.d.ts +35 -2
- package/dist/components/Chat/A2UIView/index.js +13 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +93 -67
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +26 -19
- package/dist/components/Chat/ChatMessage/chat-message.variants.js +45 -12
- package/dist/components/Chat/ChatMessage/index.d.ts +20 -5
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte +59 -33
- package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +7 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.js +28 -13
- package/dist/components/Chat/CodeBlock/index.d.ts +39 -4
- package/dist/components/Chat/PromptInput/PromptInput.svelte +9 -16
- package/dist/components/Chat/PromptInput/prompt-input.variants.js +16 -15
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +9 -4
- package/dist/components/Chat/ToolCallCard/index.d.ts +4 -3
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +0 -3
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +4 -4
- package/dist/components/CopyButton/CopyButton.svelte +9 -19
- package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
- package/dist/components/FileUpload/fileUpload.variants.js +15 -4
- package/dist/components/PinInput/PinInput.svelte +1 -0
- package/dist/components/PinInput/pin-input.variants.js +14 -1
- package/dist/components/TimeInput/time-input.variants.js +16 -1
- package/dist/i18n/index.d.ts +398 -2
- package/dist/internal/copy-state.svelte.d.ts +60 -0
- package/dist/internal/copy-state.svelte.js +76 -0
- package/dist/internal/field-chrome.d.ts +18 -1
- package/dist/internal/field-chrome.js +19 -2
- package/dist/primitives/Alert/alert.variants.d.ts +8 -8
- package/dist/primitives/Avatar/avatar.variants.js +4 -1
- package/dist/primitives/Badge/badge.variants.js +11 -2
- package/dist/primitives/Button/button.variants.js +4 -1
- package/dist/primitives/Card/card.variants.js +4 -1
- package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
- package/dist/primitives/Combobox/Combobox.svelte +9 -1
- package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
- package/dist/primitives/Combobox/combobox.variants.js +43 -7
- package/dist/primitives/Combobox/index.d.ts +8 -2
- package/dist/primitives/Dialog/dialog.variants.js +4 -1
- package/dist/primitives/Drawer/drawer.variants.js +4 -1
- package/dist/primitives/Input/input.variants.js +17 -1
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
- package/dist/primitives/Menu/menu.variants.js +5 -1
- package/dist/primitives/Progress/progress.variants.js +5 -2
- package/dist/primitives/RadioGroup/radioGroup.variants.js +74 -10
- package/dist/primitives/Select/select.variants.js +14 -5
- package/dist/primitives/Slider/Slider.svelte +4 -1
- package/dist/primitives/Slider/slider.variants.js +9 -2
- package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
- package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
- package/dist/primitives/Stepper/stepper.variants.js +2 -1
- package/dist/primitives/Tab/tab.variants.js +4 -1
- package/dist/primitives/Textarea/textarea.variants.js +16 -2
- package/dist/primitives/Toast/toast.variants.d.ts +12 -12
- package/dist/primitives/Toast/toast.variants.js +5 -1
- package/dist/primitives/Toggle/toggle.variants.js +94 -18
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
- package/dist/style/foundation.css +19 -9
- package/dist/style/semantic.css +49 -2
- package/dist/style/themes/forest.css +2 -2
- package/dist/style/themes/neutral.css +2 -2
- package/dist/style/themes/ocean.css +2 -2
- package/dist/style/themes/rose.css +2 -2
- package/dist/style/themes/sunset.css +2 -2
- package/dist/utils/figma-token-export.js +7 -3
- package/dist/utils/persistent-state.svelte.d.ts +25 -1
- package/dist/utils/persistent-state.svelte.js +98 -10
- package/docs/MIGRATION-v5.md +29 -0
- package/package.json +3 -3
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// A2UI stream splitter — the piece between a model's token stream and A2UIView.
|
|
2
|
+
//
|
|
3
|
+
// Pair it with `a2uiFencedTransportSection()` (below), which tells the agent to
|
|
4
|
+
// emit exactly the format this parses. The two are one contract: change the
|
|
5
|
+
// fence tag here and the prompt changes with it, so a consumer can never end up
|
|
6
|
+
// with a parser and a prompt that disagree.
|
|
7
|
+
//
|
|
8
|
+
// The agent writes ordinary Markdown text and emits UI as a fenced block:
|
|
9
|
+
//
|
|
10
|
+
// Here is your form:
|
|
11
|
+
// ```a2ui
|
|
12
|
+
// {"version":"v0.9.1","createSurface":{"surfaceId":"s1","catalogId":"…"}}
|
|
13
|
+
// {"version":"v0.9.1","updateComponents":{"surfaceId":"s1","components":[…]}}
|
|
14
|
+
// ```
|
|
15
|
+
// Anything else you'd like?
|
|
16
|
+
//
|
|
17
|
+
// This module turns a *token stream* of that text into an ordered list of
|
|
18
|
+
// message parts — text parts and `a2ui` parts — that ChatMessage renders in
|
|
19
|
+
// order (the `a2ui` part flowing into A2UIView). It is a small state machine:
|
|
20
|
+
//
|
|
21
|
+
// • text mode → append incoming text to the current text part.
|
|
22
|
+
// • a fenced line ` ```a2ui ` opens UI mode and starts a fresh a2ui part.
|
|
23
|
+
// • in UI mode, a COMPLETE line that parses as JSON is appended to the a2ui
|
|
24
|
+
// part's payload array (immutably: `[...prev, env]`, so A2UIView consumes
|
|
25
|
+
// it incrementally and keeps prior envelope object identity).
|
|
26
|
+
// • a line that does NOT parse but opens a JSON object (models like to
|
|
27
|
+
// pretty-print a large updateComponents across lines) starts a multi-line
|
|
28
|
+
// envelope buffer; a string-aware brace/bracket depth scanner decides when
|
|
29
|
+
// the object is complete, and only then is the whole buffer parsed.
|
|
30
|
+
// • a fenced line ` ``` ` closes UI mode, back to text.
|
|
31
|
+
// • a line/buffer that can never become valid JSON falls the rest of the
|
|
32
|
+
// fence back to text (re-emitted inside a synthetic ``` code block so the
|
|
33
|
+
// fence's REAL closing ``` closes that block in the rendered Markdown
|
|
34
|
+
// instead of opening a stray one) and records an issue.
|
|
35
|
+
//
|
|
36
|
+
// The splitter is CHUNK-DECOMPOSITION-INVARIANT: it buffers a partial trailing
|
|
37
|
+
// line and only ever acts on complete lines (plus a final flush at `end()`), so
|
|
38
|
+
// the committed result is identical no matter how the input was sliced into
|
|
39
|
+
// chunks — fence markers may straddle chunk boundaries, text may sit before /
|
|
40
|
+
// between / after fences, and the last line may be incomplete.
|
|
41
|
+
/**
|
|
42
|
+
* Parse a line as a CommonMark code-fence marker, or `null` if it is not one.
|
|
43
|
+
* A fence is ` ``` `+ or `~~~`+ with 0–3 leading spaces (≥4 = indented code); a
|
|
44
|
+
* backtick fence's info string may not contain a backtick.
|
|
45
|
+
*/
|
|
46
|
+
function parseFence(line) {
|
|
47
|
+
const m = /^( {0,3})(`{3,}|~{3,})[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
48
|
+
if (!m)
|
|
49
|
+
return null;
|
|
50
|
+
const char = m[2][0];
|
|
51
|
+
const info = m[3];
|
|
52
|
+
if (char === '`' && info.includes('`'))
|
|
53
|
+
return null;
|
|
54
|
+
return { char, len: m[2].length, info, indent: m[1].length };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The info string that marks a fence as A2UI. Shared by the parser and by
|
|
58
|
+
* `a2uiFencedTransportSection()` so the prompt can never name a different tag
|
|
59
|
+
* than the splitter accepts.
|
|
60
|
+
*/
|
|
61
|
+
export const A2UI_FENCE_TAG = 'a2ui';
|
|
62
|
+
const FENCE = '```';
|
|
63
|
+
/** Our special opener: a column-0 backtick fence whose info string is exactly `a2ui`. */
|
|
64
|
+
function isA2uiOpen(f) {
|
|
65
|
+
return f !== null && f.char === '`' && f.indent === 0 && f.info === A2UI_FENCE_TAG;
|
|
66
|
+
}
|
|
67
|
+
/** A bare closing fence that terminates a fence opened with `open`. */
|
|
68
|
+
function isCloseOf(f, open) {
|
|
69
|
+
return f !== null && f.char === open.char && f.len >= open.len && f.info === '';
|
|
70
|
+
}
|
|
71
|
+
/** Could this partial (newline-free) line still grow into our ` ```a2ui ` fence marker? */
|
|
72
|
+
function couldStartFence(s) {
|
|
73
|
+
const t = s.replace(/^[ \t]+/, '');
|
|
74
|
+
if (t === '')
|
|
75
|
+
return false;
|
|
76
|
+
return `${FENCE}${A2UI_FENCE_TAG}`.startsWith(t) || t.startsWith(FENCE) || t.startsWith('~~~');
|
|
77
|
+
}
|
|
78
|
+
// ── Pure, immutable part-array transforms ───────────────────────────────────
|
|
79
|
+
// Each returns a NEW array; existing part objects (and, crucially, the parsed
|
|
80
|
+
// envelope objects inside a2ui payloads) are carried by reference.
|
|
81
|
+
function withAppendedText(parts, s) {
|
|
82
|
+
if (s === '')
|
|
83
|
+
return parts;
|
|
84
|
+
const last = parts[parts.length - 1];
|
|
85
|
+
if (last && last.type === 'text') {
|
|
86
|
+
return [...parts.slice(0, -1), { type: 'text', text: last.text + s }];
|
|
87
|
+
}
|
|
88
|
+
return [...parts, { type: 'text', text: s }];
|
|
89
|
+
}
|
|
90
|
+
function withStartedA2ui(parts) {
|
|
91
|
+
return [...parts, { type: 'a2ui', payload: [] }];
|
|
92
|
+
}
|
|
93
|
+
function withAppendedEnvelope(parts, env) {
|
|
94
|
+
const last = parts[parts.length - 1];
|
|
95
|
+
if (last?.type !== 'a2ui')
|
|
96
|
+
return parts;
|
|
97
|
+
return [...parts.slice(0, -1), { type: 'a2ui', payload: [...last.payload, env] }];
|
|
98
|
+
}
|
|
99
|
+
/** Drop a trailing a2ui part that never received an envelope (empty fence). */
|
|
100
|
+
function withoutEmptyTrailingA2ui(parts) {
|
|
101
|
+
const last = parts[parts.length - 1];
|
|
102
|
+
if (last && last.type === 'a2ui' && last.payload.length === 0)
|
|
103
|
+
return parts.slice(0, -1);
|
|
104
|
+
return parts;
|
|
105
|
+
}
|
|
106
|
+
export class A2uiStreamSplitter {
|
|
107
|
+
/** Committed parts (immutable; replaced wholesale on every mutation). */
|
|
108
|
+
committed = [];
|
|
109
|
+
mode = 'text';
|
|
110
|
+
/**
|
|
111
|
+
* When in text mode inside a *regular* (non-a2ui) markdown code fence, the
|
|
112
|
+
* marker that opened it. A `` ```a2ui `` line inside such a block is literal
|
|
113
|
+
* code, NOT a surface — this is what stops a quoted example from executing.
|
|
114
|
+
*/
|
|
115
|
+
codeFence = null;
|
|
116
|
+
/** Buffered trailing line that has not yet seen its newline. */
|
|
117
|
+
pending = '';
|
|
118
|
+
rawText = '';
|
|
119
|
+
issueList = [];
|
|
120
|
+
/**
|
|
121
|
+
* Raw lines of an in-progress multi-line envelope (a pretty-printed object
|
|
122
|
+
* spanning several fence lines), plus the depth-scanner state that tracks
|
|
123
|
+
* whether the object has closed. Empty/zeroed while envelopes arrive as
|
|
124
|
+
* proper single-line JSONL.
|
|
125
|
+
*/
|
|
126
|
+
objLines = [];
|
|
127
|
+
objDepth = 0;
|
|
128
|
+
objInString = false;
|
|
129
|
+
objEscape = false;
|
|
130
|
+
/** Feed the next token / chunk of model output. */
|
|
131
|
+
push(chunk) {
|
|
132
|
+
if (chunk === '')
|
|
133
|
+
return;
|
|
134
|
+
this.rawText += chunk;
|
|
135
|
+
this.pending += chunk;
|
|
136
|
+
for (;;) {
|
|
137
|
+
const nl = this.pending.indexOf('\n');
|
|
138
|
+
if (nl === -1)
|
|
139
|
+
break;
|
|
140
|
+
const line = this.pending.slice(0, nl);
|
|
141
|
+
this.pending = this.pending.slice(nl + 1);
|
|
142
|
+
this.processLine(line, true);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Signal end of stream: flush the trailing partial line and settle state. */
|
|
146
|
+
end() {
|
|
147
|
+
if (this.pending !== '') {
|
|
148
|
+
const line = this.pending;
|
|
149
|
+
this.pending = '';
|
|
150
|
+
this.processLine(line, false);
|
|
151
|
+
}
|
|
152
|
+
if (this.mode === 'fence') {
|
|
153
|
+
// Stream ended inside a fence — settle to text and drop an empty surface.
|
|
154
|
+
this.committed = withoutEmptyTrailingA2ui(this.committed);
|
|
155
|
+
this.mode = 'text';
|
|
156
|
+
// An envelope still buffered mid-object is re-emitted as text (inside a
|
|
157
|
+
// synthetic code block) so a truncated stream never swallows it silently.
|
|
158
|
+
if (this.objLines.length > 0) {
|
|
159
|
+
this.asText('```', true);
|
|
160
|
+
for (const buffered of this.objLines)
|
|
161
|
+
this.asText(buffered, true);
|
|
162
|
+
this.resetObjBuffer();
|
|
163
|
+
}
|
|
164
|
+
this.issueList.push({
|
|
165
|
+
code: 'UNTERMINATED_FENCE',
|
|
166
|
+
message: 'The a2ui fenced block was not closed before the stream ended.'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
asText(line, terminated) {
|
|
171
|
+
this.committed = withAppendedText(this.committed, terminated ? `${line}\n` : line);
|
|
172
|
+
}
|
|
173
|
+
resetObjBuffer() {
|
|
174
|
+
this.objLines = [];
|
|
175
|
+
this.objDepth = 0;
|
|
176
|
+
this.objInString = false;
|
|
177
|
+
this.objEscape = false;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Advance the depth scanner across one line: brace/bracket depth outside of
|
|
181
|
+
* strings, with `\"`-escape handling inside them. Returns `false` when the
|
|
182
|
+
* buffer can never become valid JSON — depth underflow, or a string left
|
|
183
|
+
* open at end-of-line (raw newlines are illegal inside JSON strings).
|
|
184
|
+
*/
|
|
185
|
+
scanDepth(line) {
|
|
186
|
+
for (const ch of line) {
|
|
187
|
+
if (this.objEscape) {
|
|
188
|
+
this.objEscape = false;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (this.objInString) {
|
|
192
|
+
if (ch === '\\')
|
|
193
|
+
this.objEscape = true;
|
|
194
|
+
else if (ch === '"')
|
|
195
|
+
this.objInString = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === '"')
|
|
199
|
+
this.objInString = true;
|
|
200
|
+
else if (ch === '{' || ch === '[')
|
|
201
|
+
this.objDepth++;
|
|
202
|
+
else if (ch === '}' || ch === ']') {
|
|
203
|
+
this.objDepth--;
|
|
204
|
+
if (this.objDepth < 0)
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return !this.objInString;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Abandon the current a2ui fence: drop an empty surface, record an issue and
|
|
212
|
+
* re-emit the buffered lines (plus the offending one, if any) as text inside
|
|
213
|
+
* a synthetic ``` code block. `codeFence` is armed so the fence's real
|
|
214
|
+
* closing ``` closes that block instead of opening a stray one downstream.
|
|
215
|
+
*/
|
|
216
|
+
fenceFallback(message, badLine, terminated = true) {
|
|
217
|
+
this.issueList.push({
|
|
218
|
+
code: 'UNPARSEABLE_LINE',
|
|
219
|
+
message,
|
|
220
|
+
line: badLine ?? this.objLines.join('\n')
|
|
221
|
+
});
|
|
222
|
+
this.committed = withoutEmptyTrailingA2ui(this.committed);
|
|
223
|
+
this.mode = 'text';
|
|
224
|
+
this.codeFence = { char: '`', len: 3 };
|
|
225
|
+
this.asText('```', true);
|
|
226
|
+
const lines = badLine === undefined ? this.objLines : [...this.objLines, badLine];
|
|
227
|
+
for (let i = 0; i < lines.length; i++) {
|
|
228
|
+
this.asText(lines[i], i < lines.length - 1 ? true : terminated);
|
|
229
|
+
}
|
|
230
|
+
this.resetObjBuffer();
|
|
231
|
+
}
|
|
232
|
+
processLine(rawLine, terminated) {
|
|
233
|
+
// Normalize CRLF: a trailing \r would otherwise defeat fence detection
|
|
234
|
+
// (the marker regex ends at whitespace, not \r) and leak into JSONL lines.
|
|
235
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
236
|
+
const fence = parseFence(line);
|
|
237
|
+
if (this.mode === 'fence') {
|
|
238
|
+
// Inside OUR a2ui block (opened by ```a2ui). Only a bare ``` closes it;
|
|
239
|
+
// every other line feeds the JSONL parser (with a multi-line object
|
|
240
|
+
// buffer for pretty-printed envelopes).
|
|
241
|
+
if (isCloseOf(fence, { char: '`', len: 3 })) {
|
|
242
|
+
if (this.objLines.length > 0) {
|
|
243
|
+
// The fence closed on a still-open envelope — fall it back to text,
|
|
244
|
+
// then re-process this ``` so it closes the synthetic code block.
|
|
245
|
+
this.fenceFallback('An a2ui envelope was still incomplete when its fence closed; falling back to text.');
|
|
246
|
+
this.processLine(rawLine, terminated);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.committed = withoutEmptyTrailingA2ui(this.committed);
|
|
250
|
+
this.mode = 'text';
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (this.objLines.length > 0) {
|
|
254
|
+
// Continuation of a multi-line envelope.
|
|
255
|
+
const viable = this.scanDepth(line);
|
|
256
|
+
this.objLines.push(line);
|
|
257
|
+
if (!viable) {
|
|
258
|
+
this.fenceFallback('An a2ui envelope was not valid JSON; falling back to text.', undefined, terminated);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (this.objDepth === 0) {
|
|
262
|
+
const joined = this.objLines.join('\n');
|
|
263
|
+
try {
|
|
264
|
+
const env = JSON.parse(joined);
|
|
265
|
+
this.committed = withAppendedEnvelope(this.committed, env);
|
|
266
|
+
this.resetObjBuffer();
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
this.fenceFallback('An a2ui envelope was not valid JSON; falling back to text.', undefined, terminated);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (line.trim() === '')
|
|
275
|
+
return; // ignore blank lines between envelopes
|
|
276
|
+
try {
|
|
277
|
+
const env = JSON.parse(line);
|
|
278
|
+
this.committed = withAppendedEnvelope(this.committed, env);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Not a complete envelope. If the line opens a JSON object, buffer it
|
|
282
|
+
// as the start of a multi-line envelope; anything else is garbage and
|
|
283
|
+
// falls the rest of the fence back to text.
|
|
284
|
+
if (line.trimStart().startsWith('{')) {
|
|
285
|
+
const viable = this.scanDepth(line);
|
|
286
|
+
this.objLines.push(line);
|
|
287
|
+
if (!viable || this.objDepth === 0) {
|
|
288
|
+
// Underflow, an unterminated string, or a braces-balanced line
|
|
289
|
+
// that still failed to parse — it can never become valid JSON.
|
|
290
|
+
this.fenceFallback('An a2ui line was not valid JSON; falling back to text.', undefined, terminated);
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.fenceFallback('An a2ui line was not valid JSON; falling back to text.', line, terminated);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// text mode, inside a regular (non-a2ui) code block: everything is verbatim
|
|
299
|
+
// text — a ```a2ui here is quoted code, never a surface — until the matching
|
|
300
|
+
// closing fence, which is itself emitted as text.
|
|
301
|
+
if (this.codeFence) {
|
|
302
|
+
if (isCloseOf(fence, this.codeFence))
|
|
303
|
+
this.codeFence = null;
|
|
304
|
+
this.asText(line, terminated);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// plain text mode.
|
|
308
|
+
if (isA2uiOpen(fence)) {
|
|
309
|
+
this.mode = 'fence';
|
|
310
|
+
this.committed = withStartedA2ui(this.committed);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (fence) {
|
|
314
|
+
// A regular code fence opens: track it so a nested ```a2ui stays literal.
|
|
315
|
+
this.codeFence = { char: fence.char, len: fence.len };
|
|
316
|
+
}
|
|
317
|
+
this.asText(line, terminated);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The current parts, including the buffered trailing text tail for a
|
|
321
|
+
* responsive stream. A partial line that might still become a fence marker is
|
|
322
|
+
* held back (never flashed). a2ui parts are carried by reference so A2UIView
|
|
323
|
+
* consumes them incrementally.
|
|
324
|
+
*/
|
|
325
|
+
snapshot() {
|
|
326
|
+
if (this.mode === 'text' && this.pending !== '' && !couldStartFence(this.pending)) {
|
|
327
|
+
return withAppendedText(this.committed, this.pending);
|
|
328
|
+
}
|
|
329
|
+
return this.committed;
|
|
330
|
+
}
|
|
331
|
+
/** The raw model output fed so far (fences included) — the faithful wire form. */
|
|
332
|
+
get raw() {
|
|
333
|
+
return this.rawText;
|
|
334
|
+
}
|
|
335
|
+
/** Accumulated non-fatal issues (malformed JSONL, unterminated fence). */
|
|
336
|
+
get issues() {
|
|
337
|
+
return this.issueList;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The transport half of the fenced-JSONL contract: the prompt section that makes
|
|
342
|
+
* an agent emit exactly what {@link A2uiStreamSplitter} parses.
|
|
343
|
+
*
|
|
344
|
+
* `a2uiSystemPrompt()` deliberately says nothing about transport, because how
|
|
345
|
+
* envelopes travel is the app's business. This is the transport for apps that
|
|
346
|
+
* take the model's ordinary text stream and let it open a ` ```a2ui ` fence —
|
|
347
|
+
* the setup the splitter exists for. Append it after `a2uiSystemPrompt()` (and
|
|
348
|
+
* after `a2uiDataSchemaSection()`, if you use one).
|
|
349
|
+
*
|
|
350
|
+
* Anything domain-specific — which tools to call, what never to invent — stays
|
|
351
|
+
* yours to append; this covers only the wire format and the return path.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* const system = [
|
|
356
|
+
* a2uiSystemPrompt({ catalog: urbiconA2uiCatalogSpec }),
|
|
357
|
+
* a2uiDataSchemaSection(MY_SCHEMA),
|
|
358
|
+
* a2uiFencedTransportSection(),
|
|
359
|
+
* MY_DOMAIN_RULES
|
|
360
|
+
* ].join('\n\n');
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
export function a2uiFencedTransportSection(options) {
|
|
364
|
+
const actionPrefix = options?.actionPrefix ?? '[ui-action]';
|
|
365
|
+
const errorPrefix = options?.errorPrefix ?? '[ui-error]';
|
|
366
|
+
const open = `${FENCE}${A2UI_FENCE_TAG}`;
|
|
367
|
+
const lines = [
|
|
368
|
+
'## Transport — how your UI reaches this client',
|
|
369
|
+
'',
|
|
370
|
+
'Write normal Markdown prose. When (and only when) a form, a chooser, or a',
|
|
371
|
+
'structured surface would genuinely help the user more than prose, emit the UI',
|
|
372
|
+
`as a fenced code block tagged ${open} containing A2UI envelopes as JSONL —`,
|
|
373
|
+
'one complete JSON envelope per line, no blank lines, no trailing commentary',
|
|
374
|
+
'inside the fence. Example:',
|
|
375
|
+
'',
|
|
376
|
+
open,
|
|
377
|
+
'{"version":"v0.9.1","createSurface":{"surfaceId":"form-1","catalogId":"…"}}',
|
|
378
|
+
'{"version":"v0.9.1","updateComponents":{"surfaceId":"form-1","components":[ … ]}}',
|
|
379
|
+
FENCE,
|
|
380
|
+
'',
|
|
381
|
+
'Rules for the fence:',
|
|
382
|
+
`- Open the block with a line that is exactly ${open} and close it with ${FENCE}.`,
|
|
383
|
+
'- Emit createSurface FIRST, then the updateComponents / updateDataModel',
|
|
384
|
+
' envelopes. Every envelope has "version":"v0.9.1".',
|
|
385
|
+
'- One envelope per line, as compact single-line JSON — never pretty-print an',
|
|
386
|
+
' envelope across multiple lines (a large updateComponents stays on ONE line).',
|
|
387
|
+
'- You may put prose before and/or after the fence. Do not nest fences.',
|
|
388
|
+
'- Patching an EARLIER surface works the same way: open a fence and send the',
|
|
389
|
+
' updateComponents / updateDataModel envelopes for its surfaceId (no',
|
|
390
|
+
' createSurface). The client routes them to the message that shows that',
|
|
391
|
+
' surface, so it updates where it already stands — say in prose what you',
|
|
392
|
+
' changed, since it may be scrolled out of view.'
|
|
393
|
+
];
|
|
394
|
+
if (actionPrefix || errorPrefix) {
|
|
395
|
+
lines.push('', '## Interaction round-channel', '');
|
|
396
|
+
}
|
|
397
|
+
if (actionPrefix) {
|
|
398
|
+
lines.push('When the user interacts with a surface you sent, the client sends you a new', `user turn whose text begins with \`${actionPrefix} \` followed by a compact JSON`, 'object: { name, surfaceId, sourceComponentId, timestamp, context }, plus', "`dataModel` (the surface's full state) when you created it with", '"sendDataModel": true. Treat it as the user activating a control on your', 'surface — usually the right answer is to patch THAT surface, not to send a new', 'one.');
|
|
399
|
+
}
|
|
400
|
+
if (errorPrefix) {
|
|
401
|
+
if (actionPrefix)
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push('If a surface you sent failed validation, the next user turn is prefixed with a', `\`${errorPrefix} \` line carrying the validation issues as JSON. Read it, correct`, 'the offending envelopes, and re-emit a valid surface.');
|
|
404
|
+
}
|
|
405
|
+
return lines.join('\n');
|
|
406
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript-side wiring for {@link A2uiSurfaceRouter}.
|
|
3
|
+
*
|
|
4
|
+
* The router decides WHERE an envelope belongs; these functions carry it there.
|
|
5
|
+
* They are pure over `ChatMessageData[]` — every one returns a new message list
|
|
6
|
+
* and never touches component state — so the whole cross-message patch flow is
|
|
7
|
+
* testable without mounting the chat.
|
|
8
|
+
*
|
|
9
|
+
* Addressing: a source key is `` `${messageId}#${n}` ``, where `n` counts the
|
|
10
|
+
* a2ui parts of that message in render order. Message ids may contain `#`, so
|
|
11
|
+
* the key is always split at the LAST one.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChatMessageData, ChatMessagePart } from '../chat.types.js';
|
|
14
|
+
import type { A2uiValidationIssue } from './a2ui.types.js';
|
|
15
|
+
import type { A2uiSurfaceRouter } from './a2ui-router.js';
|
|
16
|
+
/** A surface that a later message patched, and the message it lives in. */
|
|
17
|
+
export interface PatchedSurface {
|
|
18
|
+
surfaceId: string;
|
|
19
|
+
targetMessageId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RouteMessageResult {
|
|
22
|
+
/** The transcript with every delivered/withdrawn patch applied. */
|
|
23
|
+
messages: ChatMessageData[];
|
|
24
|
+
/** The routed message's own parts — foreign envelopes removed. */
|
|
25
|
+
parts: ChatMessagePart[];
|
|
26
|
+
/** Router findings (e.g. a re-created surfaceId) for the agent's error channel. */
|
|
27
|
+
issues: A2uiValidationIssue[];
|
|
28
|
+
/** Surfaces this call promoted to long-lived, with their home message. */
|
|
29
|
+
promoted: PatchedSurface[];
|
|
30
|
+
/**
|
|
31
|
+
* Ids of the messages that received envelopes in this call — every target,
|
|
32
|
+
* not just newly promoted ones.
|
|
33
|
+
*
|
|
34
|
+
* A patched message must render as if it were streaming for as long as the
|
|
35
|
+
* patch is still arriving: envelopes land one by one, and a container that
|
|
36
|
+
* names children from the NEXT envelope is normal mid-stream (the agent is
|
|
37
|
+
* even told to send containers first). Without that grace the settled message
|
|
38
|
+
* flags each intermediate state as a dangling reference, the user watches
|
|
39
|
+
* error chips appear and vanish, and the agent gets told to repair UI that was
|
|
40
|
+
* never broken.
|
|
41
|
+
*/
|
|
42
|
+
targets: string[];
|
|
43
|
+
}
|
|
44
|
+
export declare const sourceKey: (messageId: string, partIndex: number) => string;
|
|
45
|
+
export declare const messageIdOf: (key: string) => string;
|
|
46
|
+
/** Rewrite the payload of the one a2ui part a source key addresses. */
|
|
47
|
+
export declare function editA2uiPayload(messages: ChatMessageData[], key: string, edit: (payload: unknown[]) => unknown[]): ChatMessageData[];
|
|
48
|
+
/**
|
|
49
|
+
* Route one message's a2ui parts: keep what belongs to it, deliver the rest to
|
|
50
|
+
* the payloads that own those surfaces.
|
|
51
|
+
*
|
|
52
|
+
* A part whose envelopes ALL travelled elsewhere is dropped — it would render
|
|
53
|
+
* as an empty surface. A part that is merely still empty (fence open, no
|
|
54
|
+
* complete envelope yet) is kept, so the streaming placeholder survives.
|
|
55
|
+
*/
|
|
56
|
+
export declare function routeMessageParts(router: A2uiSurfaceRouter, messages: ChatMessageData[], messageId: string, parts: ChatMessagePart[]): RouteMessageResult;
|
|
57
|
+
/**
|
|
58
|
+
* Drop a message from the router's books and take back everything it had
|
|
59
|
+
* patched into other messages — for regenerate, retry and deletion.
|
|
60
|
+
*
|
|
61
|
+
* The sources come from the router, not from counting the message's parts: a
|
|
62
|
+
* message whose envelopes all travelled elsewhere keeps no a2ui part, and its
|
|
63
|
+
* patches would otherwise outlive it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function revokeMessage(router: A2uiSurfaceRouter, messages: ChatMessageData[], messageId: string): ChatMessageData[];
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript-side wiring for {@link A2uiSurfaceRouter}.
|
|
3
|
+
*
|
|
4
|
+
* The router decides WHERE an envelope belongs; these functions carry it there.
|
|
5
|
+
* They are pure over `ChatMessageData[]` — every one returns a new message list
|
|
6
|
+
* and never touches component state — so the whole cross-message patch flow is
|
|
7
|
+
* testable without mounting the chat.
|
|
8
|
+
*
|
|
9
|
+
* Addressing: a source key is `` `${messageId}#${n}` ``, where `n` counts the
|
|
10
|
+
* a2ui parts of that message in render order. Message ids may contain `#`, so
|
|
11
|
+
* the key is always split at the LAST one.
|
|
12
|
+
*/
|
|
13
|
+
export const sourceKey = (messageId, partIndex) => `${messageId}#${partIndex}`;
|
|
14
|
+
export const messageIdOf = (key) => key.slice(0, key.lastIndexOf('#'));
|
|
15
|
+
const partIndexOf = (key) => Number(key.slice(key.lastIndexOf('#') + 1));
|
|
16
|
+
/** Rewrite the payload of the one a2ui part a source key addresses. */
|
|
17
|
+
export function editA2uiPayload(messages, key, edit) {
|
|
18
|
+
const messageId = messageIdOf(key);
|
|
19
|
+
const partIndex = partIndexOf(key);
|
|
20
|
+
return messages.map((message) => {
|
|
21
|
+
if (message.id !== messageId)
|
|
22
|
+
return message;
|
|
23
|
+
let seen = -1;
|
|
24
|
+
return {
|
|
25
|
+
...message,
|
|
26
|
+
parts: message.parts.map((part) => {
|
|
27
|
+
if (part.type !== 'a2ui')
|
|
28
|
+
return part;
|
|
29
|
+
seen += 1;
|
|
30
|
+
if (seen !== partIndex)
|
|
31
|
+
return part;
|
|
32
|
+
return { ...part, payload: edit(part.payload ?? []) };
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const deliver = (messages, patch) => editA2uiPayload(messages, patch.targetKey, (payload) => [...payload, ...patch.envelopes]);
|
|
38
|
+
// Removal is by reference: the router hands back the very objects it delivered,
|
|
39
|
+
// so a withdrawn patch leaves the surviving envelopes (and their order) alone.
|
|
40
|
+
const withdraw = (messages, patch) => editA2uiPayload(messages, patch.targetKey, (payload) => payload.filter((envelope) => !patch.envelopes.includes(envelope)));
|
|
41
|
+
/**
|
|
42
|
+
* Route one message's a2ui parts: keep what belongs to it, deliver the rest to
|
|
43
|
+
* the payloads that own those surfaces.
|
|
44
|
+
*
|
|
45
|
+
* A part whose envelopes ALL travelled elsewhere is dropped — it would render
|
|
46
|
+
* as an empty surface. A part that is merely still empty (fence open, no
|
|
47
|
+
* complete envelope yet) is kept, so the streaming placeholder survives.
|
|
48
|
+
*/
|
|
49
|
+
export function routeMessageParts(router, messages, messageId, parts) {
|
|
50
|
+
let next = messages;
|
|
51
|
+
let partIndex = -1;
|
|
52
|
+
const issues = [];
|
|
53
|
+
const promoted = [];
|
|
54
|
+
const targets = new Set();
|
|
55
|
+
const routed = [];
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
if (part.type !== 'a2ui') {
|
|
58
|
+
routed.push(part);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
partIndex += 1;
|
|
62
|
+
const payload = part.payload ?? [];
|
|
63
|
+
const result = router.route(sourceKey(messageId, partIndex), payload);
|
|
64
|
+
for (const patch of result.revoked)
|
|
65
|
+
next = withdraw(next, patch);
|
|
66
|
+
for (const patch of result.patches) {
|
|
67
|
+
next = deliver(next, patch);
|
|
68
|
+
targets.add(messageIdOf(patch.targetKey));
|
|
69
|
+
}
|
|
70
|
+
issues.push(...result.issues);
|
|
71
|
+
// Every patch in this call went to a target; a promoted surface is hosted by
|
|
72
|
+
// the target its own envelopes travelled to.
|
|
73
|
+
for (const surfaceId of result.promoted) {
|
|
74
|
+
const owner = router.ownerOf(surfaceId);
|
|
75
|
+
if (owner)
|
|
76
|
+
promoted.push({ surfaceId, targetMessageId: messageIdOf(owner) });
|
|
77
|
+
}
|
|
78
|
+
if (payload.length > 0 && result.own.length === 0)
|
|
79
|
+
continue;
|
|
80
|
+
routed.push({ ...part, payload: result.own });
|
|
81
|
+
}
|
|
82
|
+
return { messages: next, parts: routed, issues, promoted, targets: [...targets] };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Drop a message from the router's books and take back everything it had
|
|
86
|
+
* patched into other messages — for regenerate, retry and deletion.
|
|
87
|
+
*
|
|
88
|
+
* The sources come from the router, not from counting the message's parts: a
|
|
89
|
+
* message whose envelopes all travelled elsewhere keeps no a2ui part, and its
|
|
90
|
+
* patches would otherwise outlive it.
|
|
91
|
+
*/
|
|
92
|
+
export function revokeMessage(router, messages, messageId) {
|
|
93
|
+
let next = messages;
|
|
94
|
+
for (const key of router.sourceKeys()) {
|
|
95
|
+
if (messageIdOf(key) !== messageId)
|
|
96
|
+
continue;
|
|
97
|
+
for (const patch of router.revoke(key))
|
|
98
|
+
next = withdraw(next, patch);
|
|
99
|
+
}
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* because "dangling" is a warning mid-stream and an error once settled.
|
|
22
22
|
*/
|
|
23
23
|
import { type A2uiValidationIssue } from './a2ui.types.js';
|
|
24
|
+
import { type A2uiCatalogSpec } from './a2ui-catalog.js';
|
|
25
|
+
import { type A2uiDataSchema } from './a2ui-schema.js';
|
|
24
26
|
export interface A2uiComponentInstance {
|
|
25
27
|
id: string;
|
|
26
28
|
component: string;
|
|
@@ -35,6 +37,19 @@ export interface A2uiSurfaceState {
|
|
|
35
37
|
/** Root of the surface data model (mutated in place by two-way edits). */
|
|
36
38
|
dataModel: unknown;
|
|
37
39
|
issues: A2uiValidationIssue[];
|
|
40
|
+
/**
|
|
41
|
+
* The catalog resolved from `createSurface.catalogId` (or the default when the
|
|
42
|
+
* id names no configured catalog). Every downstream check — registry lookup,
|
|
43
|
+
* icon set, flex containers, per-component checks — reads it, so surfaces on
|
|
44
|
+
* different catalogs validate and render independently.
|
|
45
|
+
*/
|
|
46
|
+
catalog: A2uiCatalogSpec;
|
|
47
|
+
/**
|
|
48
|
+
* `createSurface.sendDataModel` — when true, every action dispatched from this
|
|
49
|
+
* surface carries the full data model (see `A2uiActionEvent.dataModel`), so
|
|
50
|
+
* the agent sees the user's input even for fields it left out of `context`.
|
|
51
|
+
*/
|
|
52
|
+
sendDataModel: boolean;
|
|
38
53
|
}
|
|
39
54
|
export interface A2uiProcessor {
|
|
40
55
|
surfaces: Map<string, A2uiSurfaceState>;
|
|
@@ -42,7 +57,24 @@ export interface A2uiProcessor {
|
|
|
42
57
|
/** Validate + apply one envelope. `index` positions it as `/messages/<index>` in issue paths. */
|
|
43
58
|
apply(envelope: unknown, index: number): void;
|
|
44
59
|
}
|
|
45
|
-
|
|
60
|
+
/** Options for {@link createA2uiProcessor}. Omitting them yields the Basic-only default. */
|
|
61
|
+
export interface A2uiProcessorOptions {
|
|
62
|
+
/**
|
|
63
|
+
* The catalogs this processor understands, in priority order. `catalogs[0]`
|
|
64
|
+
* is the default/fallback. Defaults to `[basicA2uiCatalogSpec]` — a
|
|
65
|
+
* single-catalog processor accepts any `catalogId` string silently
|
|
66
|
+
* (back-compat); an unknown id only warns once there are ≥ 2 catalogs.
|
|
67
|
+
*/
|
|
68
|
+
catalogs?: readonly A2uiCatalogSpec[];
|
|
69
|
+
/**
|
|
70
|
+
* Optional data schema. When set, every `updateDataModel` write is validated
|
|
71
|
+
* against it — a type mismatch on a declared pointer is a `SCHEMA_TYPE_MISMATCH`
|
|
72
|
+
* error, a write to an undeclared top-level branch a `SCHEMA_UNDECLARED_PATH`
|
|
73
|
+
* warning. Omitting it disables schema validation entirely (back-compat).
|
|
74
|
+
*/
|
|
75
|
+
dataSchema?: A2uiDataSchema;
|
|
76
|
+
}
|
|
77
|
+
export declare function createA2uiProcessor(options?: A2uiProcessorOptions): A2uiProcessor;
|
|
46
78
|
/**
|
|
47
79
|
* Normalize a render payload into an envelope list. Accepts an envelope array,
|
|
48
80
|
* a single envelope, or the golden-file `{ messages: [...] }` wrapper. Returns
|