@nxisai/agent-harness 0.4.1 → 0.5.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 +3 -1
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +31 -9
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/tools/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +7 -5
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/bridge.d.ts.map +1 -1
- package/dist/harness/tools/bridge.js +33 -12
- package/dist/harness/tools/bridge.js.map +1 -1
- package/dist/harness/tools/local-context.d.ts +49 -9
- package/dist/harness/tools/local-context.d.ts.map +1 -1
- package/dist/harness/tools/local-context.js +377 -22
- package/dist/harness/tools/local-context.js.map +1 -1
- package/dist/harness/tools/open-page.d.ts +16 -0
- package/dist/harness/tools/open-page.d.ts.map +1 -1
- package/dist/harness/tools/open-page.js +45 -9
- package/dist/harness/tools/open-page.js.map +1 -1
- package/dist/harness/tools/retry.d.ts +53 -0
- package/dist/harness/tools/retry.d.ts.map +1 -0
- package/dist/harness/tools/retry.js +96 -0
- package/dist/harness/tools/retry.js.map +1 -0
- package/dist/harness/tools/schema.d.ts +18 -0
- package/dist/harness/tools/schema.d.ts.map +1 -1
- package/dist/harness/tools/schema.js +38 -0
- package/dist/harness/tools/schema.js.map +1 -1
- package/dist/harness/tools/site-actions.d.ts.map +1 -1
- package/dist/harness/tools/site-actions.js +3 -2
- package/dist/harness/tools/site-actions.js.map +1 -1
- package/dist/harness/tools/site-nav.d.ts +168 -0
- package/dist/harness/tools/site-nav.d.ts.map +1 -0
- package/dist/harness/tools/site-nav.js +316 -0
- package/dist/harness/tools/site-nav.js.map +1 -0
- package/dist/harness/tools/site-pages.d.ts +4 -0
- package/dist/harness/tools/site-pages.d.ts.map +1 -1
- package/dist/harness/tools/site-pages.js +19 -3
- package/dist/harness/tools/site-pages.js.map +1 -1
- package/dist/harness/tools/source.d.ts +9 -0
- package/dist/harness/tools/source.d.ts.map +1 -1
- package/dist/harness/tools/source.js +54 -14
- package/dist/harness/tools/source.js.map +1 -1
- package/dist/harness/tools/webmcp.d.ts +39 -3
- package/dist/harness/tools/webmcp.d.ts.map +1 -1
- package/dist/harness/tools/webmcp.js +93 -14
- package/dist/harness/tools/webmcp.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +12 -1
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/dist/ui/tailwind.generated.d.ts +1 -1
- package/dist/ui/tailwind.generated.d.ts.map +1 -1
- package/dist/ui/tailwind.generated.js +1 -1
- package/dist/ui/tailwind.generated.js.map +1 -1
- package/package.json +2 -1
- package/src/a2ui/cards.ts +177 -0
- package/src/a2ui/emit.ts +147 -0
- package/src/a2ui/form.ts +117 -0
- package/src/a2ui/host.ts +73 -0
- package/src/a2ui/index.ts +22 -0
- package/src/a2ui/stepper.ts +211 -0
- package/src/a2ui/theme.ts +134 -0
- package/src/a2ui/to-v09.ts +118 -0
- package/src/engine.ts +701 -0
- package/src/harness/harness.ts +956 -0
- package/src/harness/plan.ts +91 -0
- package/src/harness/policy.ts +59 -0
- package/src/harness/renderer.ts +208 -0
- package/src/harness/router/coordinator.ts +220 -0
- package/src/harness/router/rules.ts +65 -0
- package/src/harness/slots.ts +117 -0
- package/src/harness/steps.ts +156 -0
- package/src/harness/tools/action-tools.ts +870 -0
- package/src/harness/tools/bridge.ts +393 -0
- package/src/harness/tools/client.ts +26 -0
- package/src/harness/tools/discovery.ts +55 -0
- package/src/harness/tools/local-context.ts +480 -0
- package/src/harness/tools/open-page.ts +273 -0
- package/src/harness/tools/pending-action.ts +232 -0
- package/src/harness/tools/resume.ts +134 -0
- package/src/harness/tools/retry.ts +117 -0
- package/src/harness/tools/schema.ts +245 -0
- package/src/harness/tools/site-actions.ts +356 -0
- package/src/harness/tools/site-nav.ts +432 -0
- package/src/harness/tools/site-pages.ts +288 -0
- package/src/harness/tools/source.ts +353 -0
- package/src/harness/tools/webmcp.ts +200 -0
- package/src/index.ts +193 -0
- package/src/providers/capability.ts +167 -0
- package/src/providers/proxy.ts +191 -0
- package/src/providers/types.ts +150 -0
- package/src/types.ts +258 -0
- package/src/ui/agent-fullscreen.ts +3984 -0
- package/src/ui/agent.tw.css +49 -0
- package/src/ui/config.ts +138 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/journey.ts +57 -0
- package/src/ui/tailwind.generated.ts +2 -0
- package/src/webmcp-contract.ts +65 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two prompt surfaces: asking for missing values, and confirming a write.
|
|
3
|
+
*
|
|
4
|
+
* The stepper never recreates its surface. Steps arrive as component updates
|
|
5
|
+
* against a live surface whose data model already holds every answer, so moving
|
|
6
|
+
* between steps changes the components without touching the data, and nothing
|
|
7
|
+
* already entered is lost.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { JsonSchema } from '../types.js';
|
|
13
|
+
import type { A2uiMessage } from './emit.js';
|
|
14
|
+
import type { ClarificationPlan, PlanStep } from '../harness/plan.js';
|
|
15
|
+
import type { Slot } from '../harness/slots.js';
|
|
16
|
+
|
|
17
|
+
const CATALOG = 'https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json';
|
|
18
|
+
|
|
19
|
+
interface Component { id: string; component: string; [prop: string]: unknown }
|
|
20
|
+
|
|
21
|
+
const humanize = (s: string): string =>
|
|
22
|
+
s.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ')
|
|
23
|
+
.replace(/^\w/, c => c.toUpperCase());
|
|
24
|
+
|
|
25
|
+
function labelFor(slot: Slot): string {
|
|
26
|
+
return slot.schema.description || humanize(slot.name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function enumOptions(values: unknown[]): { label: string; value: string }[] {
|
|
30
|
+
return values
|
|
31
|
+
.map(v => String(v))
|
|
32
|
+
// A leading placeholder option is a markup convention rather than a
|
|
33
|
+
// choice, and offering it would allow it to be submitted as an answer.
|
|
34
|
+
.filter(v => !/[…:]\s*$/.test(v))
|
|
35
|
+
.map(v => ({ label: humanize(v), value: v }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One input bound to /form/<name>. */
|
|
39
|
+
function inputFor(slot: Slot): Component {
|
|
40
|
+
const id = `f_${slot.name}`;
|
|
41
|
+
const value = { path: `/form/${slot.name}` };
|
|
42
|
+
const label = labelFor(slot);
|
|
43
|
+
const spec: JsonSchema = slot.schema;
|
|
44
|
+
|
|
45
|
+
const values = spec.enum ?? spec.items?.enum;
|
|
46
|
+
if (Array.isArray(values) && values.length) {
|
|
47
|
+
return { id, component: 'ChoicePicker', label, variant: 'mutuallyExclusive',
|
|
48
|
+
options: enumOptions(values), value };
|
|
49
|
+
}
|
|
50
|
+
if (spec.type === 'boolean') return { id, component: 'CheckBox', label, value };
|
|
51
|
+
|
|
52
|
+
const variant =
|
|
53
|
+
spec.type === 'number' || spec.type === 'integer' ? 'number'
|
|
54
|
+
: /description|message|notes|details/.test(slot.name) ? 'longText'
|
|
55
|
+
: 'shortText';
|
|
56
|
+
return { id, component: 'TextField', label, variant, value };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stepComponents(
|
|
60
|
+
step: PlanStep,
|
|
61
|
+
plan: ClarificationPlan,
|
|
62
|
+
toolName: string,
|
|
63
|
+
): Component[] {
|
|
64
|
+
const fields = step.slots.map(inputFor);
|
|
65
|
+
const isLast = step.index === plan.steps.length - 1;
|
|
66
|
+
const isFirst = step.index === 0;
|
|
67
|
+
|
|
68
|
+
const actionIds = [
|
|
69
|
+
...(plan.stepped && !isFirst ? ['back'] : []),
|
|
70
|
+
'cancel',
|
|
71
|
+
isLast ? 'submit' : 'next',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// Same header shape as the review and submitted cards: an uppercase eyebrow
|
|
75
|
+
// then the detail. Uppercased in the string itself because no CSS of
|
|
76
|
+
// ours reaches inside these components.
|
|
77
|
+
//
|
|
78
|
+
// The headline is NOT Manrope. --a2ui-font-family-title applies only to
|
|
79
|
+
// markdown headings, and Text falls back to rendering the raw string unless
|
|
80
|
+
// a MarkdownRenderer is provided through Lit context, so "### Foo" renders as
|
|
81
|
+
// literal hashes. Reaching the display face here needs @a2ui/markdown-it
|
|
82
|
+
// wired into the surface, not a formatting trick.
|
|
83
|
+
const heading: Component[] = [
|
|
84
|
+
{ id: 'eyebrow', component: 'Text',
|
|
85
|
+
text: plan.stepped
|
|
86
|
+
? `STEP ${step.index + 1} OF ${plan.steps.length}`
|
|
87
|
+
: 'ONE MORE DETAIL',
|
|
88
|
+
variant: 'caption' },
|
|
89
|
+
{ id: 'headline', component: 'Text', text: step.title, variant: 'body' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
// Every inline component is a card: the surface is a thing placed in the
|
|
94
|
+
// conversation, and the card edge is what separates it from the message
|
|
95
|
+
// bubbles either side. Card takes one child, so the content column sits
|
|
96
|
+
// inside it rather than being the root itself.
|
|
97
|
+
{ id: 'root', component: 'Card', child: 'body' },
|
|
98
|
+
{ id: 'body', component: 'Column',
|
|
99
|
+
children: [...heading.map(h => h.id), ...fields.map(f => f.id), 'actions'] },
|
|
100
|
+
...heading,
|
|
101
|
+
...fields,
|
|
102
|
+
{ id: 'actions', component: 'Row', children: actionIds, justify: 'end' },
|
|
103
|
+
|
|
104
|
+
{ id: 'back_label', component: 'Text', text: 'Back' },
|
|
105
|
+
{ id: 'back', component: 'Button', child: 'back_label',
|
|
106
|
+
action: { event: { name: 'nxis.back', context: { tool: toolName, step: step.index } } } },
|
|
107
|
+
|
|
108
|
+
{ id: 'cancel_label', component: 'Text', text: 'Cancel' },
|
|
109
|
+
{ id: 'cancel', component: 'Button', child: 'cancel_label',
|
|
110
|
+
action: { event: { name: 'nxis.cancel', context: { tool: toolName } } } },
|
|
111
|
+
|
|
112
|
+
{ id: 'next_label', component: 'Text', text: 'Next' },
|
|
113
|
+
{ id: 'next', component: 'Button', child: 'next_label', variant: 'primary',
|
|
114
|
+
action: { event: { name: 'nxis.next', context: { tool: toolName, step: step.index } } } },
|
|
115
|
+
|
|
116
|
+
{ id: 'submit_label', component: 'Text', text: 'Submit' },
|
|
117
|
+
{ id: 'submit', component: 'Button', child: 'submit_label', variant: 'primary',
|
|
118
|
+
action: { event: { name: 'nxis.submit', context: { tool: toolName } } } },
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Create the surface, showing the first step. */
|
|
123
|
+
export function emitAsk(
|
|
124
|
+
toolName: string,
|
|
125
|
+
plan: ClarificationPlan,
|
|
126
|
+
known: Record<string, unknown>,
|
|
127
|
+
surfaceId = 'ask',
|
|
128
|
+
): A2uiMessage[] {
|
|
129
|
+
// Seed every gap so its input has a value to bind to, and carry the known
|
|
130
|
+
// values along: they are submitted with the answers even though no step
|
|
131
|
+
// ever displays them.
|
|
132
|
+
const form: Record<string, unknown> = { ...known };
|
|
133
|
+
for (const step of plan.steps) {
|
|
134
|
+
for (const slot of step.slots) {
|
|
135
|
+
if (form[slot.name] === undefined) {
|
|
136
|
+
form[slot.name] = slot.schema.type === 'boolean' ? false : '';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [{
|
|
142
|
+
version: 'v1.0',
|
|
143
|
+
createSurface: {
|
|
144
|
+
surfaceId, catalogId: CATALOG,
|
|
145
|
+
components: stepComponents(plan.steps[0], plan, toolName),
|
|
146
|
+
dataModel: { form, step: 0 },
|
|
147
|
+
},
|
|
148
|
+
}];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Move to another step: components only, so answers already typed survive. */
|
|
152
|
+
export function emitStep(
|
|
153
|
+
surfaceId: string,
|
|
154
|
+
toolName: string,
|
|
155
|
+
plan: ClarificationPlan,
|
|
156
|
+
index: number,
|
|
157
|
+
): A2uiMessage[] {
|
|
158
|
+
return [
|
|
159
|
+
{ version: 'v1.0',
|
|
160
|
+
updateComponents: { surfaceId, components: stepComponents(plan.steps[index], plan, toolName) } },
|
|
161
|
+
{ version: 'v1.0', updateDataModel: { surfaceId, path: '/step', value: index } },
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The write confirmation: what is about to be sent, read-only.
|
|
167
|
+
*
|
|
168
|
+
* Not a form: nothing is missing, so there is nothing to fill in,
|
|
169
|
+
* showing inputs would imply otherwise and invite editing that was not asked
|
|
170
|
+
* for. Edit is available, as a button, for when it genuinely was.
|
|
171
|
+
*/
|
|
172
|
+
export function emitConfirm(
|
|
173
|
+
toolName: string,
|
|
174
|
+
slots: Slot[],
|
|
175
|
+
surfaceId = 'confirm',
|
|
176
|
+
submitLabel = 'Submit',
|
|
177
|
+
): A2uiMessage[] {
|
|
178
|
+
const rows = slots.map(s => ({ label: labelFor(s), value: String(s.value ?? '') }));
|
|
179
|
+
|
|
180
|
+
const components: Component[] = [
|
|
181
|
+
// The review is a card for the same reason the inputs are: it is one
|
|
182
|
+
// reviewable object, and its edge is what says "this is what will be sent"
|
|
183
|
+
// rather than "this is more conversation".
|
|
184
|
+
{ id: 'root', component: 'Card', child: 'body' },
|
|
185
|
+
{ id: 'body', component: 'Column', children: ['eyebrow', 'headline', 'rows', 'actions'] },
|
|
186
|
+
{ id: 'eyebrow', component: 'Text', text: 'READY TO SEND', variant: 'caption' },
|
|
187
|
+
{ id: 'headline', component: 'Text', text: humanize(toolName), variant: 'body' },
|
|
188
|
+
|
|
189
|
+
{ id: 'rows', component: 'List',
|
|
190
|
+
children: { path: '/rows', componentId: 'row_tpl' }, direction: 'vertical' },
|
|
191
|
+
{ id: 'row_tpl', component: 'Row', children: ['row_label', 'row_value'] },
|
|
192
|
+
{ id: 'row_label', component: 'Text', text: '${label}', variant: 'caption' },
|
|
193
|
+
{ id: 'row_value', component: 'Text', text: '${value}' },
|
|
194
|
+
|
|
195
|
+
{ id: 'actions', component: 'Row', children: ['edit', 'cancel', 'send'], justify: 'end' },
|
|
196
|
+
{ id: 'edit_label', component: 'Text', text: 'Edit' },
|
|
197
|
+
{ id: 'edit', component: 'Button', child: 'edit_label',
|
|
198
|
+
action: { event: { name: 'nxis.edit', context: { tool: toolName } } } },
|
|
199
|
+
{ id: 'cancel_label', component: 'Text', text: 'Cancel' },
|
|
200
|
+
{ id: 'cancel', component: 'Button', child: 'cancel_label',
|
|
201
|
+
action: { event: { name: 'nxis.cancel', context: { tool: toolName } } } },
|
|
202
|
+
{ id: 'send_label', component: 'Text', text: submitLabel },
|
|
203
|
+
{ id: 'send', component: 'Button', child: 'send_label', variant: 'primary',
|
|
204
|
+
action: { event: { name: 'nxis.submit', context: { tool: toolName } } } },
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
return [{
|
|
208
|
+
version: 'v1.0',
|
|
209
|
+
createSurface: { surfaceId, catalogId: CATALOG, components, dataModel: { rows } },
|
|
210
|
+
}];
|
|
211
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme tokens for A2UI surfaces.
|
|
3
|
+
*
|
|
4
|
+
* The Lit renderer styles its components entirely through `--a2ui-*` custom
|
|
5
|
+
* properties. Those inherit across shadow boundaries, so setting them on the
|
|
6
|
+
* element containing `<a2ui-surface>` restyles everything inside it without
|
|
7
|
+
* `::part()`, a forked catalog, or a patched package.
|
|
8
|
+
*
|
|
9
|
+
* Values map onto the tokens the packaged element already declares, so surfaces
|
|
10
|
+
* follow the same palette rather than carrying a second copy of it.
|
|
11
|
+
*
|
|
12
|
+
* The rules encoded here:
|
|
13
|
+
* - sans first: Manrope for the title, Inter for everything readable
|
|
14
|
+
* - mono for labels only: field labels and tags, never body copy
|
|
15
|
+
* - blue for trust and action: the primary button and focus ring
|
|
16
|
+
* - light by default
|
|
17
|
+
*/
|
|
18
|
+
export const A2UI_THEME_CSS = `
|
|
19
|
+
.a2ui-scope {
|
|
20
|
+
/* Core roles */
|
|
21
|
+
--a2ui-color-primary: var(--blue);
|
|
22
|
+
--a2ui-color-primary-hover: var(--blue-strong);
|
|
23
|
+
--a2ui-color-primary-dark: var(--blue-strong);
|
|
24
|
+
--a2ui-color-primary-light: var(--blue-tint);
|
|
25
|
+
--a2ui-color-on-primary: #FFFFFF;
|
|
26
|
+
--a2ui-color-secondary: var(--surface);
|
|
27
|
+
--a2ui-color-secondary-hover: var(--surface-2);
|
|
28
|
+
--a2ui-color-on-secondary: var(--ink);
|
|
29
|
+
--a2ui-color-surface: var(--bg);
|
|
30
|
+
--a2ui-color-on-surface: var(--ink);
|
|
31
|
+
--a2ui-color-on-background: var(--ink);
|
|
32
|
+
--a2ui-color-border: var(--border);
|
|
33
|
+
--a2ui-color-input: var(--bg);
|
|
34
|
+
--a2ui-color-on-input: var(--ink);
|
|
35
|
+
|
|
36
|
+
/* Type — Manrope displays, Inter reads (design.md §1) */
|
|
37
|
+
--a2ui-font-family-title: var(--display);
|
|
38
|
+
--a2ui-font-size-xs: 11px; /* eyebrow / label size, matching .dc-row dt */
|
|
39
|
+
--a2ui-font-size-s: 13px;
|
|
40
|
+
--a2ui-font-size-m: 14px;
|
|
41
|
+
--a2ui-font-size-l: 18px; /* markdown ### → .dc-title size */
|
|
42
|
+
--a2ui-font-size-xl: 22px;
|
|
43
|
+
--a2ui-font-size-2xl: 31px;
|
|
44
|
+
--a2ui-line-height-body: 1.65;
|
|
45
|
+
--a2ui-line-height-headings: 1.25;
|
|
46
|
+
--a2ui-text-color-text: var(--ink);
|
|
47
|
+
--a2ui-text-caption-color: var(--ink-3); /* same muted label ink as .dc-row dt */
|
|
48
|
+
--a2ui-text-a-color: var(--blue);
|
|
49
|
+
--a2ui-text-a-font-weight: 600;
|
|
50
|
+
|
|
51
|
+
/* Radii — 14px cards, 10px buttons, 999px pills (design.md §1) */
|
|
52
|
+
--a2ui-border-radius: var(--radius-sm);
|
|
53
|
+
--a2ui-border: 1px solid var(--border);
|
|
54
|
+
--a2ui-border-width: 1px;
|
|
55
|
+
|
|
56
|
+
--a2ui-card-background: var(--bg);
|
|
57
|
+
--a2ui-card-border: 1px solid var(--border);
|
|
58
|
+
--a2ui-card-border-radius: var(--radius);
|
|
59
|
+
--a2ui-card-box-shadow: var(--shadow-sm);
|
|
60
|
+
/* 22px matches .detailcard, so an input card and a review card sitting in
|
|
61
|
+
the same thread share one rhythm rather than three inherited paddings. */
|
|
62
|
+
--a2ui-card-padding: 22px;
|
|
63
|
+
--a2ui-card-margin: 0;
|
|
64
|
+
|
|
65
|
+
/* Buttons: radius 10, weight 600, one primary per view (design.md §4) */
|
|
66
|
+
--a2ui-button-border-radius: var(--radius-sm);
|
|
67
|
+
--a2ui-button-font-weight: 600;
|
|
68
|
+
--a2ui-button-padding: 11px 20px;
|
|
69
|
+
--a2ui-button-border: 1px solid var(--border-strong);
|
|
70
|
+
--a2ui-button-background: var(--bg);
|
|
71
|
+
--a2ui-button-box-shadow: none;
|
|
72
|
+
/* Buttons sit in a Row with its own gap; a margin here would double it. */
|
|
73
|
+
--a2ui-button-margin: 0;
|
|
74
|
+
/* The label is a Text child, so the button's own colour comes from the
|
|
75
|
+
secondary text token — this is what makes a default A2UI button read as
|
|
76
|
+
.btn.ghost rather than as unstyled chrome. */
|
|
77
|
+
--a2ui-color-on-secondary: var(--ink);
|
|
78
|
+
--a2ui-color-secondary-hover: var(--surface-2);
|
|
79
|
+
|
|
80
|
+
/* Inputs */
|
|
81
|
+
--a2ui-textfield-border: 1px solid var(--border);
|
|
82
|
+
--a2ui-textfield-border-radius: var(--radius-sm);
|
|
83
|
+
--a2ui-textfield-padding: 10px 12px;
|
|
84
|
+
--a2ui-textfield-color-border-focus: var(--blue);
|
|
85
|
+
--a2ui-textfield-color-error: var(--red);
|
|
86
|
+
--a2ui-textfield-label-font-size: 11px;
|
|
87
|
+
--a2ui-textfield-label-font-weight: 500;
|
|
88
|
+
|
|
89
|
+
--a2ui-checkbox-border: 1px solid var(--border-strong);
|
|
90
|
+
--a2ui-checkbox-border-radius: 6px;
|
|
91
|
+
--a2ui-checkbox-background: var(--bg);
|
|
92
|
+
--a2ui-checkbox-color-error: var(--red);
|
|
93
|
+
--a2ui-checkbox-size: 18px;
|
|
94
|
+
--a2ui-checkbox-gap: 10px;
|
|
95
|
+
--a2ui-checkbox-label-font-size: 14px;
|
|
96
|
+
--a2ui-checkbox-label-font-weight: 400;
|
|
97
|
+
|
|
98
|
+
/* Tags render as ChoicePicker chips / caption Text — pill radius */
|
|
99
|
+
--a2ui-choicepicker-chip-border-radius: var(--radius-pill);
|
|
100
|
+
--a2ui-choicepicker-chip-padding: 6px 12px;
|
|
101
|
+
--a2ui-choicepicker-label-color: var(--ink-3);
|
|
102
|
+
--a2ui-choicepicker-label-font-size: 11px;
|
|
103
|
+
--a2ui-choicepicker-label-font-weight: 500;
|
|
104
|
+
|
|
105
|
+
/* Rhythm */
|
|
106
|
+
--a2ui-spacing-xs: 4px;
|
|
107
|
+
--a2ui-spacing-s: 8px;
|
|
108
|
+
--a2ui-spacing-m: 12px;
|
|
109
|
+
--a2ui-spacing-l: 18px;
|
|
110
|
+
--a2ui-column-gap: 12px;
|
|
111
|
+
--a2ui-row-gap: 10px;
|
|
112
|
+
--a2ui-list-gap: 12px;
|
|
113
|
+
--a2ui-list-padding: 0;
|
|
114
|
+
|
|
115
|
+
font-family: var(--sans);
|
|
116
|
+
color: var(--ink);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* Mono for labels only (design.md §3.2). The renderer has no token for label
|
|
121
|
+
* *family*, only size and weight, so the eyebrow treatment is applied to the
|
|
122
|
+
* label elements directly. Scoped to .a2ui-scope so it cannot leak.
|
|
123
|
+
*/
|
|
124
|
+
.a2ui-scope a2ui-surface::part(label),
|
|
125
|
+
.a2ui-scope label {
|
|
126
|
+
font-family: var(--mono);
|
|
127
|
+
letter-spacing: .14em;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
color: var(--ink-3);
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
/** Signal Blue — the one theme value A2UI itself carries on createSurface. */
|
|
134
|
+
export const NXIS_PRIMARY = '#2563EB';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Downgrades A2UI v1.0 messages to v0.9.
|
|
3
|
+
*
|
|
4
|
+
* The emitters target v1.0, which is the documented spec and the one with
|
|
5
|
+
* published JSON Schemas, but available renderers support v0.9 and earlier, so
|
|
6
|
+
* messages are downgraded before rendering.
|
|
7
|
+
*
|
|
8
|
+
* Two mechanical differences:
|
|
9
|
+
*
|
|
10
|
+
* 1. `version` is the string "v0.9", not "v1.0".
|
|
11
|
+
* 2. v0.9's `createSurface` carries only surfaceId/catalogId: it rejects
|
|
12
|
+
* inline `components` and `dataModel` as additional properties. They have
|
|
13
|
+
* to follow as separate `updateComponents` / `updateDataModel` messages for
|
|
14
|
+
* the same surfaceId. So one v1.0 message becomes up to three.
|
|
15
|
+
*
|
|
16
|
+
* No longer needed once a v1.0 renderer is available.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { A2uiMessage } from './emit.js';
|
|
22
|
+
import { NXIS_PRIMARY } from './theme.js';
|
|
23
|
+
|
|
24
|
+
type Msg = Record<string, any>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The catalog is versioned alongside the protocol, and the renderer looks it up
|
|
28
|
+
* by exact id, so a v1.0 id against a v0.9 catalog registry throws
|
|
29
|
+
* "Catalog not found" rather than falling back.
|
|
30
|
+
*/
|
|
31
|
+
const CATALOG_V1 = 'https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json';
|
|
32
|
+
const CATALOG_V09 = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json';
|
|
33
|
+
|
|
34
|
+
const downgradeCatalog = (id: string | undefined): string | undefined =>
|
|
35
|
+
id === CATALOG_V1 ? CATALOG_V09 : id;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* v1.0 interpolates with `${expr}` inside a string. v0.9 has no interpolation
|
|
39
|
+
* at all: a dynamic value is a DataBinding object, `{ path }`. A `${title}`
|
|
40
|
+
* left as-is is not an error: it renders as the literal characters, which is
|
|
41
|
+
* how it first showed up on the verification page.
|
|
42
|
+
*
|
|
43
|
+
* Only a string that is *entirely* one expression can convert. "Hi ${name}"
|
|
44
|
+
* has no v0.9 equivalent, so it is left alone and will render literally; the
|
|
45
|
+
* emitters avoid mixed interpolation for that reason.
|
|
46
|
+
*/
|
|
47
|
+
const INTERPOLATION = /^\$\{([^}]+)\}$/;
|
|
48
|
+
|
|
49
|
+
function bindStrings(value: unknown): unknown {
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
const m = INTERPOLATION.exec(value);
|
|
52
|
+
return m ? { path: m[1] } : value;
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(value)) return value.map(bindStrings);
|
|
55
|
+
if (value && typeof value === 'object') {
|
|
56
|
+
const out: Msg = {};
|
|
57
|
+
for (const [k, v] of Object.entries(value as Msg)) {
|
|
58
|
+
// Identity and structure keys are never data-bound.
|
|
59
|
+
out[k] = k === 'id' || k === 'component' || k === 'componentId' ? v : bindStrings(v);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Downgrade v1.0 messages to v0.9.
|
|
68
|
+
*
|
|
69
|
+
* @param messages - The messages as authored by the emitters.
|
|
70
|
+
* @returns The equivalent v0.9 messages, which may be more numerous because a
|
|
71
|
+
* v1.0 surface creation splits into separate component and data updates.
|
|
72
|
+
*/
|
|
73
|
+
export function toV09(messages: A2uiMessage[]): Msg[] {
|
|
74
|
+
const out: Msg[] = [];
|
|
75
|
+
|
|
76
|
+
for (const msg of messages as unknown as Msg[]) {
|
|
77
|
+
if (msg.createSurface) {
|
|
78
|
+
const { surfaceId, catalogId, components, dataModel } = msg.createSurface;
|
|
79
|
+
|
|
80
|
+
// `theme` exists in v0.9 and was dropped in v1.0, so it is set here
|
|
81
|
+
// rather than in the emitters, which must stay v1.0-valid. The CSS
|
|
82
|
+
// custom properties do the real styling; this carries the accent for any
|
|
83
|
+
// renderer that never sees our stylesheet.
|
|
84
|
+
out.push({
|
|
85
|
+
version: 'v0.9',
|
|
86
|
+
createSurface: {
|
|
87
|
+
surfaceId,
|
|
88
|
+
catalogId: downgradeCatalog(catalogId),
|
|
89
|
+
theme: { primaryColor: NXIS_PRIMARY },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (components) {
|
|
94
|
+
out.push({ version: 'v0.9', updateComponents: { surfaceId, components: bindStrings(components) } });
|
|
95
|
+
}
|
|
96
|
+
// v0.9 patches one path at a time, so a whole-model seed becomes one
|
|
97
|
+
// message per top-level key rather than a single root write. The payload
|
|
98
|
+
// key is `value` and the object is additionalProperties:false, so a
|
|
99
|
+
// wrong name here is rejected outright rather than ignored.
|
|
100
|
+
if (dataModel) {
|
|
101
|
+
for (const [key, value] of Object.entries(dataModel)) {
|
|
102
|
+
out.push({ version: 'v0.9', updateDataModel: { surfaceId, path: `/${key}`, value } });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (msg.updateDataModel) {
|
|
109
|
+
const { surfaceId, path, value } = msg.updateDataModel;
|
|
110
|
+
out.push({ version: 'v0.9', updateDataModel: { surfaceId, path, value } });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
out.push({ ...msg, version: 'v0.9' });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return out;
|
|
118
|
+
}
|