@happyvertical/smrt-svelte 0.51.39 → 0.53.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.
Files changed (143) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +214 -1
  3. package/agents/components.md +8 -1
  4. package/agents/workspace.md +24 -0
  5. package/dist/Provider.svelte +19 -15
  6. package/dist/Provider.svelte.d.ts +2 -2
  7. package/dist/Provider.svelte.d.ts.map +1 -1
  8. package/dist/__tests__/provider-mode-harness.svelte +14 -0
  9. package/dist/__tests__/provider-mode-harness.svelte.d.ts +11 -0
  10. package/dist/__tests__/provider-mode-harness.svelte.d.ts.map +1 -0
  11. package/dist/__tests__/provider-mode-probe.svelte +13 -0
  12. package/dist/__tests__/provider-mode-probe.svelte.d.ts +8 -0
  13. package/dist/__tests__/provider-mode-probe.svelte.d.ts.map +1 -0
  14. package/dist/browser-ai/adapters/stt/browser-speech.d.ts +11 -0
  15. package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -1
  16. package/dist/browser-ai/adapters/stt/browser-speech.js +20 -4
  17. package/dist/browser-ai/adapters/stt/browser-speech.test.js +113 -0
  18. package/dist/browser-ai/adapters/stt/dictation-source.d.ts +3 -0
  19. package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -0
  20. package/dist/browser-ai/adapters/stt/dictation-source.js +38 -0
  21. package/dist/browser-ai/adapters/stt/dictation-source.test.js +52 -0
  22. package/dist/browser-ai/adapters/stt/index.d.ts +2 -1
  23. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
  24. package/dist/browser-ai/adapters/stt/index.js +2 -1
  25. package/dist/components/forms/FileUpload.svelte +120 -16
  26. package/dist/components/forms/FileUpload.svelte.d.ts +7 -1
  27. package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -1
  28. package/dist/components/forms/Form.svelte +118 -8
  29. package/dist/components/forms/Form.svelte.d.ts +18 -2
  30. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  31. package/dist/components/forms/FormScope.svelte +202 -0
  32. package/dist/components/forms/FormScope.svelte.d.ts +22 -0
  33. package/dist/components/forms/FormScope.svelte.d.ts.map +1 -0
  34. package/dist/components/forms/__tests__/FileUpload.native-form.test.js +249 -0
  35. package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte +24 -0
  36. package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts +10 -0
  37. package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts.map +1 -0
  38. package/dist/components/forms/__tests__/Form.ui-controls.test.js +119 -0
  39. package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -3
  40. package/dist/components/forms/__tests__/FormScope.test.js +39 -0
  41. package/dist/components/forms/__tests__/form-scope.fixture.svelte +30 -0
  42. package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts +10 -0
  43. package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts.map +1 -0
  44. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte +84 -0
  45. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts +14 -0
  46. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts.map +1 -0
  47. package/dist/components/forms/index.d.ts +1 -0
  48. package/dist/components/forms/index.d.ts.map +1 -1
  49. package/dist/components/forms/index.js +1 -0
  50. package/dist/components/workspace/README.md +11 -0
  51. package/dist/components/workspace/__tests__/AdminShell.responsive.test.js +597 -0
  52. package/dist/components/workspace/__tests__/AdminShell.test.js +257 -1
  53. package/dist/components/workspace/__tests__/TenantNav.test.js +115 -0
  54. package/dist/components/workspace/__tests__/WorkspaceAccountMenu.test.js +6 -0
  55. package/dist/components/workspace/__tests__/admin-shell-header-overflow.test.js +25 -0
  56. package/dist/components/workspace/__tests__/admin-shell-mobile.test.js +217 -0
  57. package/dist/components/workspace/__tests__/admin-shell-page-contracts.test.js +30 -0
  58. package/dist/components/workspace/__tests__/admin-shell-responsive-state.test.js +184 -0
  59. package/dist/components/workspace/__tests__/admin-shell-state.test.js +25 -0
  60. package/dist/components/workspace/__tests__/admin-shell-theme-hooks.test.js +36 -0
  61. package/dist/components/workspace/__tests__/shell-chrome-components.test.js +108 -0
  62. package/dist/components/workspace/admin-shell/AdminShell.svelte +1099 -59
  63. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +44 -1
  64. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  65. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte +170 -0
  66. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts +11 -0
  67. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts.map +1 -0
  68. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +93 -0
  69. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts +11 -0
  70. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts.map +1 -0
  71. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte +88 -0
  72. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts +14 -0
  73. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts.map +1 -0
  74. package/dist/components/workspace/admin-shell/ShellTitle.svelte +42 -0
  75. package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts +11 -0
  76. package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts.map +1 -0
  77. package/dist/components/workspace/admin-shell/TenantNav.svelte +116 -13
  78. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -2
  79. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  80. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +16 -2
  81. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts +2 -0
  82. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts.map +1 -1
  83. package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts +37 -0
  84. package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts.map +1 -0
  85. package/dist/components/workspace/admin-shell/mobile-shell-dom.js +150 -0
  86. package/dist/components/workspace/admin-shell/mobile-shell.d.ts +191 -0
  87. package/dist/components/workspace/admin-shell/mobile-shell.d.ts.map +1 -0
  88. package/dist/components/workspace/admin-shell/mobile-shell.js +165 -0
  89. package/dist/components/workspace/admin-shell/settings.d.ts +27 -2
  90. package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
  91. package/dist/components/workspace/admin-shell/settings.js +84 -2
  92. package/dist/components/workspace/admin-shell/state.svelte.d.ts +57 -1
  93. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  94. package/dist/components/workspace/admin-shell/state.svelte.js +156 -6
  95. package/dist/components/workspace/admin-shell/types.d.ts +115 -1
  96. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  97. package/dist/components/workspace/admin-shell/types.js +13 -0
  98. package/dist/components/workspace/index.d.ts +11 -4
  99. package/dist/components/workspace/index.d.ts.map +1 -1
  100. package/dist/components/workspace/index.js +12 -3
  101. package/dist/i18n/strings.forms.d.ts +3 -0
  102. package/dist/i18n/strings.forms.d.ts.map +1 -1
  103. package/dist/i18n/strings.forms.js +3 -0
  104. package/dist/i18n/strings.workspace.d.ts +14 -0
  105. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  106. package/dist/i18n/strings.workspace.js +14 -0
  107. package/dist/index.d.ts +1 -1
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +1 -1
  110. package/dist/provider-mode.test.js +86 -0
  111. package/dist/state/app-state.svelte.d.ts.map +1 -1
  112. package/dist/state/app-state.svelte.js +8 -8
  113. package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +2 -2
  114. package/dist/web/__tests__/list-sort-surface.fixture.svelte +41 -0
  115. package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts +15 -0
  116. package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts.map +1 -0
  117. package/dist/web/__tests__/list-sort-surface.svelte.test.js +59 -0
  118. package/dist/web/__tests__/surfaces.child.fixture.svelte +69 -0
  119. package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts +16 -0
  120. package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts.map +1 -0
  121. package/dist/web/__tests__/surfaces.fixture.svelte +26 -0
  122. package/dist/web/__tests__/surfaces.fixture.svelte.d.ts +18 -0
  123. package/dist/web/__tests__/surfaces.fixture.svelte.d.ts.map +1 -0
  124. package/dist/web/__tests__/surfaces.svelte.test.js +140 -0
  125. package/dist/web/__tests__/webmcp-ui.test.js +18 -0
  126. package/dist/web/index.d.ts +4 -1
  127. package/dist/web/index.d.ts.map +1 -1
  128. package/dist/web/index.js +4 -1
  129. package/dist/web/link-surface.svelte.d.ts +14 -0
  130. package/dist/web/link-surface.svelte.d.ts.map +1 -0
  131. package/dist/web/link-surface.svelte.js +49 -0
  132. package/dist/web/list-surface.svelte.d.ts +80 -0
  133. package/dist/web/list-surface.svelte.d.ts.map +1 -0
  134. package/dist/web/list-surface.svelte.js +309 -0
  135. package/dist/web/step-surface.svelte.d.ts +17 -0
  136. package/dist/web/step-surface.svelte.d.ts.map +1 -0
  137. package/dist/web/step-surface.svelte.js +56 -0
  138. package/dist/web/webmcp-ui-context.d.ts +7 -0
  139. package/dist/web/webmcp-ui-context.d.ts.map +1 -1
  140. package/dist/web/webmcp-ui-context.js +10 -0
  141. package/dist/web/webmcp-ui.d.ts.map +1 -1
  142. package/dist/web/webmcp-ui.js +16 -3
  143. package/package.json +17 -17
@@ -0,0 +1,309 @@
1
+ /**
2
+ * `useListSurface` — mount a page's rendered list on the nearest
3
+ * `<Provider webmcp>` data-surface registry in one call.
4
+ *
5
+ * A thin convenience over {@link mountListDataSurface} that owns the whole
6
+ * lifecycle (descriptor, controller, identity-keyed re-mount, context
7
+ * updates, destroy) and adds what a bare mount does not:
8
+ *
9
+ * - the visible rows, projected to the declared columns and capped, are
10
+ * published as `state.rows`, so an agent reading the surface sees what the
11
+ * person sees even on a page with no server catalog surface;
12
+ * - a `find` control (when any column is `searchable`) looks rows up by text:
13
+ * payload `{ text }` (or a bare string) keeps the rows whose searchable
14
+ * columns contain it, case-insensitively, across ALL rows — not only the
15
+ * published first page — and publishes them as `state.rows` with
16
+ * `state.find = { text, matches }`. An empty text clears it. It narrows
17
+ * only what the agent reads; the page's own list is unchanged.
18
+ *
19
+ * - sorting (when `sort` and `onSort` are given): the current order is
20
+ * published as `state.sort` and in the table snapshot, `sortable` columns
21
+ * are declared sortable, and a `set-sorting` control
22
+ * (`{ sorting: [{ columnId, direction }] }`, one rule) calls `onSort` — the
23
+ * same path the page's own column headers take, so a server-paginated
24
+ * list re-queries in that order (see `list-sort` in
25
+ * `@happyvertical/smrt-ui/data`).
26
+ *
27
+ * Otherwise a read-only mirror: no remote filter/page/selection commands are
28
+ * accepted (the page's own controls stay the source of truth); `refresh` is
29
+ * honoured when supplied. Only declared columns leave the page: never declare
30
+ * a secret (tokens, keys), and mark names/emails the person can see as
31
+ * `personal`. No-op without a WebMCP Provider. Call during component init.
32
+ */
33
+ import { createDataTableController, } from '@happyvertical/smrt-ui/data';
34
+ import { onDestroy } from 'svelte';
35
+ import { mountListDataSurface, } from './list-data-surface.svelte.js';
36
+ import { tryGetWebMcpUiContext } from './webmcp-ui-context.js';
37
+ function sortEnabled(options) {
38
+ return (typeof options.onSort === 'function' &&
39
+ options.columns.some((column) => column.sortable));
40
+ }
41
+ const DEFAULT_MAX_ROWS = 50;
42
+ const MAX_QUERY_ROWS = 300;
43
+ const MAX_FIND_TEXT = 200;
44
+ function toJsonPrimitive(value) {
45
+ if (value === null || value === undefined)
46
+ return null;
47
+ if (typeof value === 'string' || typeof value === 'boolean')
48
+ return value;
49
+ if (typeof value === 'number')
50
+ return Number.isFinite(value) ? value : null;
51
+ if (value instanceof Date)
52
+ return Number.isNaN(value.getTime()) ? null : value.toISOString();
53
+ return null;
54
+ }
55
+ /** Project rows to the declared columns only; everything else is dropped. */
56
+ export function projectListRows(rows, columns, maxRows = DEFAULT_MAX_ROWS) {
57
+ return rows.slice(0, maxRows).map((row) => {
58
+ const projected = {};
59
+ for (const column of columns) {
60
+ projected[column.id] = toJsonPrimitive(row[column.id]);
61
+ }
62
+ return projected;
63
+ });
64
+ }
65
+ function normalizeText(value) {
66
+ return value.normalize('NFKD').toLowerCase().replace(/\s+/g, ' ').trim();
67
+ }
68
+ /** Rows whose searchable columns contain `text` (case- and accent-insensitive). */
69
+ export function findListRows(rows, columns, text) {
70
+ const wanted = normalizeText(text);
71
+ if (!wanted)
72
+ return [...rows];
73
+ const searchable = columns.filter((column) => column.searchable);
74
+ return rows.filter((row) => searchable.some((column) => {
75
+ const value = row[column.id];
76
+ return ((typeof value === 'string' || typeof value === 'number') &&
77
+ normalizeText(String(value))
78
+ .replace(/\p{M}/gu, '')
79
+ .includes(wanted.replace(/\p{M}/gu, '')));
80
+ }));
81
+ }
82
+ /** The descriptor {@link useListSurface} mounts. */
83
+ export function listSurfaceDescriptor(options) {
84
+ const rowKey = options.columns.find((column) => column.rowKey)?.id ??
85
+ options.columns[0]?.id ??
86
+ 'id';
87
+ const searchableIds = options.columns
88
+ .filter((column) => column.searchable)
89
+ .map((column) => column.id);
90
+ const sortableIds = sortEnabled(options)
91
+ ? options.columns
92
+ .filter((column) => column.sortable)
93
+ .map((column) => column.id)
94
+ : [];
95
+ const columns = options.columns.map((column) => ({
96
+ id: column.id,
97
+ label: column.label,
98
+ ...(column.description ? { description: column.description } : {}),
99
+ ...(column.sensitivity ? { sensitivity: column.sensitivity } : {}),
100
+ capabilities: [
101
+ 'read',
102
+ ...(column.searchable ? ['search'] : []),
103
+ ...(sortableIds.includes(column.id) ? ['sort'] : []),
104
+ ],
105
+ ...(sortableIds.includes(column.id)
106
+ ? { operators: { sort: ['asc', 'desc'] } }
107
+ : {}),
108
+ ...(column.id === rowKey
109
+ ? { role: 'row-key' }
110
+ : column.status
111
+ ? { role: 'status' }
112
+ : {}),
113
+ }));
114
+ const searchLabels = options.columns
115
+ .filter((column) => column.searchable)
116
+ .map((column) => column.label.toLowerCase());
117
+ return {
118
+ version: 1,
119
+ identity: {
120
+ kind: options.kind ?? 'list',
121
+ surfaceId: options.surfaceId,
122
+ ...(options.subject ? { subject: options.subject } : {}),
123
+ },
124
+ schemaVersion: 1,
125
+ label: options.label,
126
+ description: options.description,
127
+ rowKey,
128
+ columns,
129
+ query: {
130
+ modes: ['rows'],
131
+ projectableColumnIds: columns.map((column) => column.id),
132
+ ...(searchableIds.length > 0
133
+ ? { searchableColumnIds: searchableIds }
134
+ : {}),
135
+ filterableColumnIds: [],
136
+ sortableColumnIds: sortableIds,
137
+ },
138
+ controls: [
139
+ ...(options.refresh ? [{ id: 'refresh', label: 'Refresh' }] : []),
140
+ ...(sortableIds.length > 0
141
+ ? [
142
+ {
143
+ id: 'set-sorting',
144
+ label: 'Sort',
145
+ description: `Re-order the list the way its column headers do. Payload: { sorting: [{ columnId, direction: 'asc' | 'desc' }] } with exactly one rule; sortable columns: ${sortableIds.join(', ')}. The current order is state.sort.`,
146
+ },
147
+ ]
148
+ : []),
149
+ ...(searchableIds.length > 0
150
+ ? [
151
+ {
152
+ id: 'find',
153
+ label: 'Find',
154
+ description: `Look rows up by ${searchLabels.join(' or ')}. Payload: { text } — rows containing it (any case) are published in state.rows; empty text clears.`,
155
+ },
156
+ ]
157
+ : []),
158
+ ],
159
+ actions: [],
160
+ // Constant: a mount publishes its descriptor once, so a row count
161
+ // captured here would cap the surface for the life of the mount.
162
+ limits: {
163
+ maxQueryRows: MAX_QUERY_ROWS,
164
+ maxQueryBytes: 1_000_000,
165
+ maxSelectionSize: MAX_QUERY_ROWS,
166
+ },
167
+ };
168
+ }
169
+ function findTextFromPayload(payload) {
170
+ const raw = typeof payload === 'string'
171
+ ? payload
172
+ : payload && typeof payload === 'object' && !Array.isArray(payload)
173
+ ? ['text', 'q', 'query', 'search', 'title']
174
+ .map((key) => payload[key])
175
+ .find((value) => typeof value === 'string')
176
+ : undefined;
177
+ if (typeof raw !== 'string' || raw.length > MAX_FIND_TEXT)
178
+ return null;
179
+ return raw;
180
+ }
181
+ function surfaceContext(options, findText) {
182
+ const visible = findText
183
+ ? findListRows(options.rows, options.columns, findText)
184
+ : options.rows;
185
+ const rows = projectListRows(visible, options.columns, options.maxRows);
186
+ return {
187
+ ...(options.context ?? {}),
188
+ ...(options.sort
189
+ ? {
190
+ sort: {
191
+ columnId: options.sort.columnId,
192
+ direction: options.sort.direction,
193
+ },
194
+ }
195
+ : {}),
196
+ rowCount: options.rows.length,
197
+ rows,
198
+ rowsTruncated: visible.length > rows.length,
199
+ find: findText ? { text: findText, matches: visible.length } : undefined,
200
+ };
201
+ }
202
+ export function useListSurface(getOptions, registry) {
203
+ const uiContext = tryGetWebMcpUiContext();
204
+ let handle = null;
205
+ let mountedKey = null;
206
+ let mountedRegistry = null;
207
+ let latest = null;
208
+ let controller = null;
209
+ let findText = '';
210
+ const sortingOf = (options) => options.sort
211
+ ? [{ columnId: options.sort.columnId, direction: options.sort.direction }]
212
+ : [];
213
+ const teardown = () => {
214
+ handle?.destroy();
215
+ handle = null;
216
+ controller = null;
217
+ mountedKey = null;
218
+ mountedRegistry = null;
219
+ findText = '';
220
+ };
221
+ $effect(() => {
222
+ const options = getOptions();
223
+ const resolved = registry ??
224
+ (uiContext?.enabled ? uiContext.dataSurfaceRegistry : undefined);
225
+ latest = options;
226
+ if (!resolved || !options) {
227
+ teardown();
228
+ return;
229
+ }
230
+ // Re-mount on identity change: SvelteKit reuses a page component across
231
+ // a param-only navigation, and a mount keeps its first subject.
232
+ const key = JSON.stringify([
233
+ options.surfaceId,
234
+ options.kind ?? 'list',
235
+ options.subject ?? null,
236
+ options.columns.map((column) => [
237
+ column.id,
238
+ column.searchable === true,
239
+ column.sortable === true,
240
+ ]),
241
+ Boolean(options.refresh),
242
+ sortEnabled(options),
243
+ ]);
244
+ if (key !== mountedKey || resolved !== mountedRegistry) {
245
+ teardown();
246
+ const context = surfaceContext(options, '');
247
+ delete context.find;
248
+ const sortable = sortEnabled(options);
249
+ const columnIds = options.columns.map((column) => column.id);
250
+ const mountedController = sortable
251
+ ? createDataTableController({
252
+ columnIds,
253
+ // Controlled: the page's `sort` is the source of truth; an agent's
254
+ // command is only a proposal until `onSort` applies it.
255
+ state: {
256
+ ...createDataTableController({ columnIds }).getState(),
257
+ sorting: sortingOf(options),
258
+ },
259
+ })
260
+ : createDataTableController({ columnIds });
261
+ controller = mountedController;
262
+ handle = mountListDataSurface({
263
+ registry: resolved,
264
+ descriptor: listSurfaceDescriptor(options),
265
+ controller: mountedController,
266
+ context: context,
267
+ acceptsTableCommand: (command) => sortable &&
268
+ command.type === 'setSorting' &&
269
+ command.sorting.length === 1,
270
+ ...(sortable
271
+ ? {
272
+ applyControlledState: async (next) => {
273
+ const rule = next.sorting[0];
274
+ if (!rule || !latest?.onSort)
275
+ return undefined;
276
+ const result = await latest.onSort({
277
+ columnId: rule.columnId,
278
+ direction: rule.direction,
279
+ });
280
+ return result === false ? undefined : next;
281
+ },
282
+ }
283
+ : {}),
284
+ ...(options.refresh ? { refresh: options.refresh } : {}),
285
+ onControl: (controlId, payload) => {
286
+ if (controlId !== 'find' || !latest || !handle)
287
+ return false;
288
+ const text = findTextFromPayload(payload);
289
+ if (text === null)
290
+ return false;
291
+ findText = text.trim();
292
+ handle.update(surfaceContext(latest, findText));
293
+ return true;
294
+ },
295
+ });
296
+ mountedKey = key;
297
+ mountedRegistry = resolved;
298
+ return;
299
+ }
300
+ if (controller?.isControlled()) {
301
+ controller.replaceState({
302
+ ...controller.getState(),
303
+ sorting: sortingOf(options),
304
+ });
305
+ }
306
+ handle?.update(surfaceContext(options, findText));
307
+ });
308
+ onDestroy(teardown);
309
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `useStepSurface` — mount a wizard or other multi-step flow on the nearest
3
+ * `<Provider webmcp>` data-surface registry, so an agent can read where the
4
+ * person is and move the flow with `next` / `back` / `go-to`. A step whose
5
+ * forward button saves or creates something (`nextWrites`) is never pressed
6
+ * by an agent: `next` calls `showNext` (reveal + highlight the button) and
7
+ * the person presses it. See `registerStepSurface` in
8
+ * `@happyvertical/smrt-ui/data`.
9
+ *
10
+ * Handlers are read at command time, so pass the page's own functions — the
11
+ * ones its buttons call. A no-op without a WebMCP Provider. Call during
12
+ * component init.
13
+ */
14
+ import { type DataSurfaceRegistry, type StepSurfaceOptions } from '@happyvertical/smrt-ui/data';
15
+ export type UseStepSurfaceOptions = Omit<StepSurfaceOptions, 'registry'>;
16
+ export declare function useStepSurface(getOptions: () => UseStepSurfaceOptions | null, registry?: DataSurfaceRegistry): void;
17
+ //# sourceMappingURL=step-surface.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-surface.svelte.d.ts","sourceRoot":"","sources":["../../src/web/step-surface.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,KAAK,mBAAmB,EAGxB,KAAK,kBAAkB,EACxB,MAAM,6BAA6B,CAAC;AAIrC,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,kBAAkB,EAAE,UAAU,CAAC,CAAC;AAEzE,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,qBAAqB,GAAG,IAAI,EAC9C,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,IAAI,CA2CN"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `useStepSurface` — mount a wizard or other multi-step flow on the nearest
3
+ * `<Provider webmcp>` data-surface registry, so an agent can read where the
4
+ * person is and move the flow with `next` / `back` / `go-to`. A step whose
5
+ * forward button saves or creates something (`nextWrites`) is never pressed
6
+ * by an agent: `next` calls `showNext` (reveal + highlight the button) and
7
+ * the person presses it. See `registerStepSurface` in
8
+ * `@happyvertical/smrt-ui/data`.
9
+ *
10
+ * Handlers are read at command time, so pass the page's own functions — the
11
+ * ones its buttons call. A no-op without a WebMCP Provider. Call during
12
+ * component init.
13
+ */
14
+ import { registerStepSurface, } from '@happyvertical/smrt-ui/data';
15
+ import { onDestroy } from 'svelte';
16
+ import { tryGetWebMcpUiContext } from './webmcp-ui-context.js';
17
+ export function useStepSurface(getOptions, registry) {
18
+ const uiContext = tryGetWebMcpUiContext();
19
+ let handle = null;
20
+ let mountedKey = null;
21
+ let mountedRegistry = null;
22
+ const teardown = () => {
23
+ handle?.destroy();
24
+ handle = null;
25
+ mountedKey = null;
26
+ mountedRegistry = null;
27
+ };
28
+ $effect(() => {
29
+ const options = getOptions();
30
+ const resolved = registry ??
31
+ (uiContext?.enabled ? uiContext.dataSurfaceRegistry : undefined);
32
+ if (!resolved || !options) {
33
+ teardown();
34
+ return;
35
+ }
36
+ const key = JSON.stringify([options.surfaceId, options.subject ?? null]);
37
+ if (key !== mountedKey || resolved !== mountedRegistry) {
38
+ teardown();
39
+ handle = registerStepSurface({ ...options, registry: resolved });
40
+ mountedKey = key;
41
+ mountedRegistry = resolved;
42
+ return;
43
+ }
44
+ handle?.update({
45
+ steps: options.steps,
46
+ current: options.current,
47
+ next: options.next,
48
+ nextWrites: options.nextWrites ?? false,
49
+ showNext: options.showNext,
50
+ back: options.back,
51
+ goTo: options.goTo,
52
+ state: options.state,
53
+ });
54
+ });
55
+ onDestroy(teardown);
56
+ }
@@ -7,6 +7,13 @@ export interface WebMcpUiContext {
7
7
  }
8
8
  export declare function setWebMcpUiContext(context: WebMcpUiContext): void;
9
9
  export declare function tryGetWebMcpUiContext(): WebMcpUiContext | null;
10
+ /**
11
+ * The nearest Provider's mounted-UI registries, or `null` when there is no
12
+ * Provider or its WebMCP UI is off (#2915). Use this to fall back to a local
13
+ * registry instead of catching {@link useWebMcpUi}'s throw. Call during
14
+ * component initialization, like any context read.
15
+ */
16
+ export declare function tryUseWebMcpUi(): WebMcpUiContext | null;
10
17
  /** Return the mounted-UI registries owned by the nearest SMRT Provider. */
11
18
  export declare function useWebMcpUi(): WebMcpUiContext;
12
19
  //# sourceMappingURL=webmcp-ui-context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webmcp-ui-context.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAK/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,0BAA0B,CAAC;IACrD,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;CACnD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEjE;AAED,wBAAgB,qBAAqB,IAAI,eAAe,GAAG,IAAI,CAE9D;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,IAAI,eAAe,CAQ7C"}
1
+ {"version":3,"file":"webmcp-ui-context.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAK/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,0BAA0B,CAAC;IACrD,QAAQ,CAAC,mBAAmB,EAAE,mBAAmB,CAAC;CACnD;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAEjE;AAED,wBAAgB,qBAAqB,IAAI,eAAe,GAAG,IAAI,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,eAAe,GAAG,IAAI,CAGvD;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,IAAI,eAAe,CAQ7C"}
@@ -6,6 +6,16 @@ export function setWebMcpUiContext(context) {
6
6
  export function tryGetWebMcpUiContext() {
7
7
  return getContext(WEBMCP_UI_CONTEXT_KEY) ?? null;
8
8
  }
9
+ /**
10
+ * The nearest Provider's mounted-UI registries, or `null` when there is no
11
+ * Provider or its WebMCP UI is off (#2915). Use this to fall back to a local
12
+ * registry instead of catching {@link useWebMcpUi}'s throw. Call during
13
+ * component initialization, like any context read.
14
+ */
15
+ export function tryUseWebMcpUi() {
16
+ const context = tryGetWebMcpUiContext();
17
+ return context?.enabled ? context : null;
18
+ }
9
19
  /** Return the mounted-UI registries owned by the nearest SMRT Provider. */
10
20
  export function useWebMcpUi() {
11
21
  const context = tryGetWebMcpUiContext();
@@ -1 +1 @@
1
- {"version":3,"file":"webmcp-ui.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,mBAAmB,EAGzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAKV,0BAA0B,EAE3B,MAAM,8BAA8B,CAAC;AAKtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA0DzD,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,0BAA0B,CAAC;IAC5C,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC;CACtD;AAED,UAAU,sBAAsB;IAC9B,YAAY,CACV,IAAI,EAAE,cAAc,EACpB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AA6fD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,MAAM,IAAI,CA6EZ"}
1
+ {"version":3,"file":"webmcp-ui.d.ts","sourceRoot":"","sources":["../../src/web/webmcp-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,mBAAmB,EAGzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAKV,0BAA0B,EAE3B,MAAM,8BAA8B,CAAC;AAOtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AA0DzD,MAAM,WAAW,4BAA4B;IAC3C,eAAe,EAAE,0BAA0B,CAAC;IAC5C,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC;CACtD;AAED,UAAU,sBAAsB;IAC9B,YAAY,CACV,IAAI,EAAE,cAAc,EACpB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AA6fD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,MAAM,IAAI,CA6FZ"}
@@ -1,5 +1,6 @@
1
1
  import { DATA_SURFACE_IDENTIFIER_MAX_LENGTH, DATA_SURFACE_MAX_REQUEST_BYTES, } from '@happyvertical/smrt-ui/data';
2
- import { reserveWebMcpToolNames, } from '@happyvertical/smrt-web/webmcp-tool-names';
2
+ import { WEBMCP_TOOL_EFFECT } from '@happyvertical/smrt-web/webmcp-page-tools';
3
+ import { markWebMcpProposalTool, reserveWebMcpToolNames, } from '@happyvertical/smrt-web/webmcp-tool-names';
3
4
  const CONTROL_ACTIONS = new Set([
4
5
  'focus',
5
6
  'reveal',
@@ -315,7 +316,7 @@ function tools(prefix, controlRegistry, dataSurfaceRegistry) {
315
316
  })),
316
317
  {
317
318
  name: `${prefix}execute_form_control`,
318
- description: 'Execute an allowed command on a mounted SMRT form control. Agent mutations are consent-gated.',
319
+ description: 'Execute an allowed command on a mounted SMRT form control. Agent mutations are consent-gated. An agent never moves keyboard focus: `focus` scrolls the control into view and highlights it.',
319
320
  inputSchema: {
320
321
  type: 'object',
321
322
  required: ['action', 'identity'],
@@ -490,7 +491,19 @@ export function registerWebMcpUiTools(options) {
490
491
  };
491
492
  try {
492
493
  for (const tool of specs) {
493
- const registration = modelContext.registerTool(tool, {
494
+ // The in-page tool registry (#2908) reads the resolved effect from this
495
+ // stamp. The two execute tools are `write`, not destructive: an agent
496
+ // can only stage values or change visible state; apply, clear, and undo
497
+ // need a separate human-confirmed path, so their mutations are
498
+ // proposals. Their browser annotations are unchanged. The proposal
499
+ // brand is what lets the in-page assistant run those writes without a
500
+ // confirmation step; the tool-name lock's `ui` label never does.
501
+ const stamped = Object.assign({
502
+ [WEBMCP_TOOL_EFFECT]: tool.annotations?.readOnlyHint
503
+ ? 'read'
504
+ : 'write',
505
+ }, tool, { execute: markWebMcpProposalTool(tool.execute) });
506
+ const registration = modelContext.registerTool(stamped, {
494
507
  signal: controller.signal,
495
508
  });
496
509
  if (registration) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-svelte",
3
- "version": "0.51.39",
3
+ "version": "0.53.0",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -126,12 +126,12 @@
126
126
  "access": "public"
127
127
  },
128
128
  "dependencies": {
129
- "@happyvertical/logger": "^0.96.0",
130
- "@happyvertical/smrt-languages": "0.51.39",
131
- "@happyvertical/smrt-mcp-apps": "0.51.39",
132
- "@happyvertical/smrt-types": "0.51.39",
133
- "@happyvertical/smrt-ui": "0.51.39",
134
- "@happyvertical/smrt-web": "0.51.39",
129
+ "@happyvertical/logger": "^0.98.0",
130
+ "@happyvertical/smrt-languages": "0.53.0",
131
+ "@happyvertical/smrt-mcp-apps": "0.53.0",
132
+ "@happyvertical/smrt-types": "0.53.0",
133
+ "@happyvertical/smrt-ui": "0.53.0",
134
+ "@happyvertical/smrt-web": "0.53.0",
135
135
  "@tanstack/db": "^0.6.14",
136
136
  "@tanstack/svelte-db": "^0.1.91",
137
137
  "esm-env": "^1.2.2"
@@ -143,7 +143,7 @@
143
143
  "@xenova/transformers": "^2.17.2",
144
144
  "bitgpu": "^0.19.1",
145
145
  "chrono-node": "^2.10.1",
146
- "svelte": "^5.57.1"
146
+ "svelte": "^5.57.0"
147
147
  },
148
148
  "peerDependenciesMeta": {
149
149
  "@huggingface/transformers": {
@@ -166,14 +166,14 @@
166
166
  }
167
167
  },
168
168
  "devDependencies": {
169
- "@happyvertical/smrt-agents": "0.51.39",
170
- "@happyvertical/smrt-chat": "0.51.39",
171
- "@happyvertical/smrt-content": "0.51.39",
172
- "@happyvertical/smrt-core": "0.51.39",
173
- "@happyvertical/smrt-reports": "0.51.39",
174
- "@happyvertical/smrt-scanner": "0.51.39",
175
- "@happyvertical/smrt-tenancy": "0.51.39",
176
- "@happyvertical/smrt-users": "0.51.39",
169
+ "@happyvertical/smrt-agents": "0.53.0",
170
+ "@happyvertical/smrt-chat": "0.53.0",
171
+ "@happyvertical/smrt-content": "0.53.0",
172
+ "@happyvertical/smrt-core": "0.53.0",
173
+ "@happyvertical/smrt-reports": "0.53.0",
174
+ "@happyvertical/smrt-scanner": "0.53.0",
175
+ "@happyvertical/smrt-tenancy": "0.53.0",
176
+ "@happyvertical/smrt-users": "0.53.0",
177
177
  "@sveltejs/package": "^2.5.8",
178
178
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
179
179
  "@testing-library/jest-dom": "^7.0.1",
@@ -192,7 +192,7 @@
192
192
  "dev": "svelte-package -i src --watch",
193
193
  "check": "pnpm exec svelte-check --tsconfig ./tsconfig.svelte.json",
194
194
  "test": "pnpm run test:unit && pnpm run test:conformance",
195
- "test:unit": "vitest run --exclude src/web/__tests__/data-surface-conformance.integration.svelte.test.ts && node --test scripts/ensure-conformance-build.test.mjs",
195
+ "test:unit": "vitest run --exclude src/web/__tests__/data-surface-conformance.integration.svelte.test.ts && node --test scripts/*.test.mjs",
196
196
  "test:conformance": "node scripts/ensure-conformance-build.mjs && SMRT_SVELTE_PUBLISHED_ENTRY=1 vitest run src/web/__tests__/data-surface-conformance.integration.svelte.test.ts",
197
197
  "typecheck:conformance": "tsc --noEmit -p ./tsconfig.conformance.json",
198
198
  "typecheck": "tsc --noEmit && pnpm run typecheck:conformance && node ../../scripts/svelte-check-a11y.mjs --tsconfig ./tsconfig.svelte.json",