@happyvertical/smrt-svelte 0.43.2 → 0.43.4

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 (44) hide show
  1. package/AGENTS.md +23 -0
  2. package/README.md +82 -0
  3. package/dist/Provider.svelte +83 -26
  4. package/dist/Provider.svelte.d.ts +4 -12
  5. package/dist/Provider.svelte.d.ts.map +1 -1
  6. package/dist/__tests__/provider-webmcp-harness.svelte +14 -0
  7. package/dist/__tests__/provider-webmcp-harness.svelte.d.ts +8 -0
  8. package/dist/__tests__/provider-webmcp-harness.svelte.d.ts.map +1 -0
  9. package/dist/components/forms/Form.svelte +112 -35
  10. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  11. package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -5
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/provider-webmcp.test.js +244 -0
  16. package/dist/web/__tests__/provider-ui-registry.fixture.svelte +43 -0
  17. package/dist/web/__tests__/provider-ui-registry.fixture.svelte.d.ts +13 -0
  18. package/dist/web/__tests__/provider-ui-registry.fixture.svelte.d.ts.map +1 -0
  19. package/dist/web/__tests__/provider-ui-registry.test.js +235 -0
  20. package/dist/web/__tests__/remote-query-harness.svelte +27 -0
  21. package/dist/web/__tests__/remote-query-harness.svelte.d.ts +11 -0
  22. package/dist/web/__tests__/remote-query-harness.svelte.d.ts.map +1 -0
  23. package/dist/web/__tests__/remote-query.svelte.test.js +100 -0
  24. package/dist/web/__tests__/webmcp-ui.test.js +410 -0
  25. package/dist/web/__tests__/webmcp.test.js +1 -1
  26. package/dist/web/index.d.ts +4 -0
  27. package/dist/web/index.d.ts.map +1 -1
  28. package/dist/web/index.js +3 -0
  29. package/dist/web/remote-query.svelte.d.ts +26 -0
  30. package/dist/web/remote-query.svelte.d.ts.map +1 -0
  31. package/dist/web/remote-query.svelte.js +57 -0
  32. package/dist/web/webmcp-provider.d.ts +23 -0
  33. package/dist/web/webmcp-provider.d.ts.map +1 -0
  34. package/dist/web/webmcp-provider.js +1 -0
  35. package/dist/web/webmcp-ui-context.d.ts +12 -0
  36. package/dist/web/webmcp-ui-context.d.ts.map +1 -0
  37. package/dist/web/webmcp-ui-context.js +16 -0
  38. package/dist/web/webmcp-ui.d.ts +21 -0
  39. package/dist/web/webmcp-ui.d.ts.map +1 -0
  40. package/dist/web/webmcp-ui.js +455 -0
  41. package/dist/web/webmcp.d.ts +1 -1
  42. package/dist/web/webmcp.svelte.d.ts +4 -1
  43. package/dist/web/webmcp.svelte.d.ts.map +1 -1
  44. package/package.json +5 -5
package/AGENTS.md CHANGED
@@ -15,6 +15,9 @@ subpath you are editing. This file keeps what holds in every module.
15
15
  | `src/test-support/` + `__tests__/` | the golden-test harness and pattern for Svelte component tests | [agents/testing.md](agents/testing.md) |
16
16
  | `src/components/settings/` (`./settings`) | `SettingsCatalog`, `paginateSettingsCatalog`, and the summary-vs-detail scalability contract | [agents/settings.md](agents/settings.md) |
17
17
  | `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
18
+ | `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
19
+ | `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
20
+ | `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
18
21
 
19
22
  ## The UI split — primitive-adoption contract (#1589)
20
23
 
@@ -63,6 +66,26 @@ Wraps app in `+layout.svelte`. Provides auth state, permissions, WebSocket, and
63
66
  </Provider>
64
67
  ```
65
68
 
69
+ An object `webmcp` config with `definitions` registers generated data/model
70
+ tools through the same registrar as direct smrt-web callers. With no `effects`
71
+ policy, only `read`-effect tools register; `write` and `destructive` require
72
+ explicit opt-in. Use `filter` for legacy collection definitions and `filterTool`
73
+ for canonical definitions; a filter supplied for definitions of the other kind
74
+ fails closed. Namespace, budget, resolver, annotation, and atomic-validation
75
+ behavior must stay identical between Provider and direct registration. The
76
+ authenticated REST route remains the execution authorization boundary.
77
+
78
+ With the fixed mounted-UI adapter enabled, Provider also owns shared
79
+ control/data-surface registries and registers six fixed `smrt_ui_*` tools. This
80
+ adapter is a separate, consent-gated surface and is not governed by the
81
+ generated-model effect policy. Descendant rich Forms use the shared
82
+ control registry unless their explicit `interactionRegistry` prop overrides it.
83
+ Supply `webmcp.ui.dataSurfaceRegistry` when mounted surface components already
84
+ share an application registry. The adapter resolves registry state at execution
85
+ time, never reads the DOM, never accepts agent confirmation, and removes the
86
+ entire tool set with one abort signal. A document may have only one active
87
+ adapter for a prefix; configure distinct prefixes for intentional coexistence.
88
+
66
89
  ## Hooks
67
90
 
68
91
  | Hook | Returns |
package/README.md CHANGED
@@ -60,6 +60,24 @@ state.
60
60
 
61
61
  ## Usage
62
62
 
63
+ ### Query-backed data surfaces
64
+
65
+ Use the web binding when a table should render one remote page instead of
66
+ hydrating its whole collection. It exposes `rows`, `page`, `total`,
67
+ `loading`, `refreshing`, `stale`, `error`, `retry`, and `lastUpdated`.
68
+
69
+ ```svelte
70
+ <script lang="ts">
71
+ import { remoteQuery } from '@happyvertical/smrt-svelte/web';
72
+ const view = remoteQuery(collection, transport);
73
+ // Failures remain available as view.error for reactive rendering.
74
+ void view.execute(request).catch(() => undefined);
75
+ </script>
76
+
77
+ {#if view.loading}<p>Loading…</p>{/if}
78
+ {#each view.rows as row (row.id)}<div>{row.name}</div>{/each}
79
+ ```
80
+
63
81
  ### Provider Setup
64
82
 
65
83
  ```svelte
@@ -74,6 +92,70 @@ state.
74
92
  </Provider>
75
93
  ```
76
94
 
95
+ The Provider can own generated WebMCP tools for the same lifecycle. Its policy
96
+ is identical to `registerWebMcpTools`; omitted `effects` exposes reads only:
97
+
98
+ ```svelte
99
+ <Provider webmcp={{
100
+ definitions,
101
+ effects: ['read', 'write'],
102
+ namespace: 'workspace',
103
+ maxTools: 24
104
+ }}>
105
+ {@render children()}
106
+ </Provider>
107
+ ```
108
+
109
+ This controls capability exposure, not authorization. Tool execution still
110
+ crosses the authenticated REST boundary and must retain its auth and tenancy
111
+ guards. The `effects`, `filter`, and `filterTool` policy applies to generated
112
+ data/model tools only; the fixed mounted-UI adapter has the separate controls
113
+ described below.
114
+
115
+ WebMCP test doubles and polyfills should implement the browser's
116
+ promise-returning `document.modelContext.registerTool()` contract; declare the
117
+ function `async` when migrating older void-returning fixtures.
118
+
119
+ ### Mounted UI through WebMCP
120
+
121
+ `<Provider webmcp>` registers six fixed `smrt_ui_*` tools for the mounted UI:
122
+ list, inspect, and execute for form controls and data surfaces. The tool set does
123
+ not change as components mount and unmount; each call reads the current
124
+ transport-neutral registries instead of inspecting or simulating the DOM.
125
+
126
+ Forms automatically join the Provider's control registry. An explicit Form
127
+ `interactionRegistry` still takes precedence. Pass the same data-surface
128
+ registry used by `DataTable` or `CollectionToolbar` when those mounted surfaces
129
+ should be discoverable:
130
+
131
+ ```svelte
132
+ <script lang="ts">
133
+ import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
134
+ import { Provider } from '@happyvertical/smrt-svelte';
135
+
136
+ const surfaces = createDataSurfaceRegistry();
137
+ </script>
138
+
139
+ <Provider webmcp={{ ui: { dataSurfaceRegistry: surfaces } }}>
140
+ <!-- pass {surfaces} to mounted data-surface components -->
141
+ {@render children()}
142
+ </Provider>
143
+ ```
144
+
145
+ The default prefix is `smrt_ui_`. Configure `ui.prefix` when multiple Providers
146
+ must coexist in one document; the same prefix cannot be registered twice.
147
+ `ui: false` disables only the fixed UI adapter while leaving generated model
148
+ tools enabled. For compatibility, an object config that omits `ui` continues to
149
+ enable only generated model tools; use `webmcp={true}` or provide `ui: {}` to
150
+ enable the mounted-UI adapter.
151
+
152
+ Form commands always run with `source: 'agent'`. WebMCP input cannot assert
153
+ confirmation: staging is allowed by the registry policy, while apply, clear,
154
+ and undo require a separate human-confirmed path. Secret control values and
155
+ hidden data-surface columns are not serialized. Read responses are marked as
156
+ untrusted content. Bespoke `useWebMcpTool` and `<Form webmcp>` tools retain their
157
+ existing lifecycle and submit behavior.
158
+
77
159
  ### Form Components
78
160
 
79
161
  ```svelte
@@ -1,14 +1,11 @@
1
1
  <script lang="ts">
2
+ import { createDataSurfaceRegistry } from '@happyvertical/smrt-ui/data';
3
+ import { createControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
2
4
  import {
3
5
  createI18nContext,
4
6
  type I18nSnapshot,
5
7
  setI18nContext,
6
8
  } from '@happyvertical/smrt-ui/i18n';
7
- import {
8
- type RegisterWebMcpToolsOptions,
9
- type SmrtWebClient,
10
- type SmrtWebCollectionDefinition,
11
- } from '@happyvertical/smrt-web';
12
9
  import type { Snippet } from 'svelte';
13
10
  import { onDestroy, untrack } from 'svelte';
14
11
  import { logger } from './internal/logger.js';
@@ -21,6 +18,9 @@ import type {
21
18
  } from './state/app-state.js';
22
19
  import { createAppState } from './state/app-state.svelte.js';
23
20
  import { setAppStateContext } from './state/context.js';
21
+ import type { WebMcpProviderConfig } from './web/webmcp-provider.js';
22
+ import { registerWebMcpUiTools } from './web/webmcp-ui.js';
23
+ import { setWebMcpUiContext } from './web/webmcp-ui-context.js';
24
24
 
25
25
  interface Props {
26
26
  /**
@@ -84,9 +84,9 @@ interface Props {
84
84
  */
85
85
  i18n?: I18nSnapshot;
86
86
  /**
87
- * Opt in to the generated collection tools for this browser surface.
88
- * WebMCP is feature-detected by smrt-web, so this is safe during SSR and on
89
- * browsers that do not expose `document.modelContext`.
87
+ * Opt in to generated data tools and the fixed mounted-UI tool adapter for
88
+ * this browser surface. WebMCP is feature-detected, so this is safe during
89
+ * SSR and on browsers that do not expose `document.modelContext`.
90
90
  */
91
91
  webmcp?: boolean | WebMcpProviderConfig;
92
92
  /**
@@ -95,15 +95,6 @@ interface Props {
95
95
  children: Snippet;
96
96
  }
97
97
 
98
- export interface WebMcpProviderConfig {
99
- definitions?: SmrtWebCollectionDefinition[];
100
- client?: SmrtWebClient;
101
- basePath?: string;
102
- fetchFn?: typeof fetch;
103
- scope?: string;
104
- filter?: RegisterWebMcpToolsOptions['filter'];
105
- }
106
-
107
98
  const {
108
99
  mode,
109
100
  autoEnableSmrt = true,
@@ -135,6 +126,35 @@ const appState = createAppState({
135
126
  },
136
127
  });
137
128
 
129
+ const localControlRegistry = createControlInteractionRegistry();
130
+ const localDataSurfaceRegistry = createDataSurfaceRegistry();
131
+ const webMcpConfig = $derived(typeof webmcp === 'object' ? webmcp : undefined);
132
+ const webMcpUiConfig = $derived(
133
+ webmcp === true
134
+ ? {}
135
+ : typeof webMcpConfig?.ui === 'object'
136
+ ? webMcpConfig.ui
137
+ : undefined,
138
+ );
139
+ const resolvedControlRegistry = $derived(
140
+ webMcpUiConfig?.controlRegistry ?? localControlRegistry,
141
+ );
142
+ const resolvedDataSurfaceRegistry = $derived(
143
+ webMcpUiConfig?.dataSurfaceRegistry ?? localDataSurfaceRegistry,
144
+ );
145
+
146
+ setWebMcpUiContext({
147
+ get enabled() {
148
+ return webMcpUiConfig !== undefined;
149
+ },
150
+ get controlRegistry() {
151
+ return resolvedControlRegistry;
152
+ },
153
+ get dataSurfaceRegistry() {
154
+ return resolvedDataSurfaceRegistry;
155
+ },
156
+ });
157
+
138
158
  $effect(() => {
139
159
  const currentPreferences = untrack(() => appState.state.session.preferences);
140
160
  if (currentPreferences.autoEnableSmrt === autoEnableSmrt) {
@@ -149,6 +169,21 @@ $effect(() => {
149
169
  });
150
170
  });
151
171
 
172
+ // Register one fixed UI tool set. Mounted components change registry contents,
173
+ // not the document-level WebMCP tool set.
174
+ $effect(() => {
175
+ if (typeof window === 'undefined' || !webmcp || !webMcpUiConfig) return;
176
+ try {
177
+ return registerWebMcpUiTools({
178
+ controlRegistry: resolvedControlRegistry,
179
+ dataSurfaceRegistry: resolvedDataSurfaceRegistry,
180
+ ...(webMcpUiConfig.prefix ? { prefix: webMcpUiConfig.prefix } : {}),
181
+ });
182
+ } catch (error) {
183
+ logger.warn('Provider: WebMCP UI tool registration rejected', { error });
184
+ }
185
+ });
186
+
152
187
  // Keep generated data-plane tools scoped to this Provider. The registrar
153
188
  // feature-detects WebMCP itself; effects do not run during SSR.
154
189
  $effect(() => {
@@ -160,16 +195,38 @@ $effect(() => {
160
195
 
161
196
  // Keep the data-plane engine out of applications that do not opt in. The
162
197
  // dynamic import also means SSR never evaluates browser-only engine code.
163
- void import('@happyvertical/smrt-web').then(({ registerWebMcpTools }) => {
164
- if (cancelled) return;
165
- dispose = registerWebMcpTools(config.definitions ?? [], {
166
- ...(config.client ? { client: config.client } : {}),
167
- ...(config.basePath ? { basePath: config.basePath } : {}),
168
- ...(config.fetchFn ? { fetchFn: config.fetchFn } : {}),
169
- ...(config.scope ? { scope: config.scope } : {}),
170
- ...(config.filter ? { filter: config.filter } : {}),
198
+ void import('@happyvertical/smrt-web')
199
+ .then(({ registerWebMcpTools }) => {
200
+ if (cancelled) return;
201
+ const registration = registerWebMcpTools(config.definitions ?? [], {
202
+ client: config.client,
203
+ basePath: config.basePath,
204
+ fetchFn: config.fetchFn,
205
+ scope: config.scope,
206
+ filter: config.filter,
207
+ filterTool: config.filterTool,
208
+ resolveFetchers: config.resolveFetchers,
209
+ resolveToolFetchers: config.resolveToolFetchers,
210
+ effects: config.effects,
211
+ namespace: config.namespace,
212
+ maxTools: config.maxTools,
213
+ });
214
+ dispose = registration;
215
+ void registration.ready.catch((error) => {
216
+ if (!cancelled) {
217
+ logger.warn('Provider: WebMCP data tool registration rejected', {
218
+ error,
219
+ });
220
+ }
221
+ });
222
+ })
223
+ .catch((error) => {
224
+ if (!cancelled) {
225
+ logger.warn('Provider: WebMCP data tool registration rejected', {
226
+ error,
227
+ });
228
+ }
171
229
  });
172
- });
173
230
 
174
231
  return () => {
175
232
  cancelled = true;
@@ -1,15 +1,7 @@
1
1
  import { type I18nSnapshot } from '@happyvertical/smrt-ui/i18n';
2
- import { type RegisterWebMcpToolsOptions, type SmrtWebClient, type SmrtWebCollectionDefinition } from '@happyvertical/smrt-web';
3
2
  import type { Snippet } from 'svelte';
4
3
  import type { AIConfig, AILoadingState, AppMode, SocketConfig, User } from './state/app-state.js';
5
- export interface WebMcpProviderConfig {
6
- definitions?: SmrtWebCollectionDefinition[];
7
- client?: SmrtWebClient;
8
- basePath?: string;
9
- fetchFn?: typeof fetch;
10
- scope?: string;
11
- filter?: RegisterWebMcpToolsOptions['filter'];
12
- }
4
+ import type { WebMcpProviderConfig } from './web/webmcp-provider.js';
13
5
  interface Props {
14
6
  /**
15
7
  * Initial mode: 'default' | 'smrt'
@@ -72,9 +64,9 @@ interface Props {
72
64
  */
73
65
  i18n?: I18nSnapshot;
74
66
  /**
75
- * Opt in to the generated collection tools for this browser surface.
76
- * WebMCP is feature-detected by smrt-web, so this is safe during SSR and on
77
- * browsers that do not expose `document.modelContext`.
67
+ * Opt in to generated data tools and the fixed mounted-UI tool adapter for
68
+ * this browser surface. WebMCP is feature-detected, so this is safe during
69
+ * SSR and on browsers that do not expose `document.modelContext`.
78
70
  */
79
71
  webmcp?: boolean | WebMcpProviderConfig;
80
72
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"Provider.svelte.d.ts","sourceRoot":"","sources":["../src/Provider.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,aAAa,EAClB,KAAK,2BAA2B,EACjC,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,OAAO,EACP,YAAY,EACZ,IAAI,EACL,MAAM,sBAAsB,CAAC;AAK9B,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,2BAA2B,EAAE,CAAC;IAC5C,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,0BAA0B,CAAC,QAAQ,CAAC,CAAC;CAC/C;AAED,UAAU,KAAK;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;;;;;;;;;;;;OAeG;IACH,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACpD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;IACxC;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AA4KD,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"Provider.svelte.d.ts","sourceRoot":"","sources":["../src/Provider.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EACV,QAAQ,EACR,cAAc,EACd,OAAO,EACP,YAAY,EACZ,IAAI,EACL,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAKrE,UAAU,KAAK;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACnB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;;;;;;;;;;;;OAeG;IACH,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;OAEG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC;;OAEG;IACH,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACpD;;;;;OAKG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;IACxC;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAiPD,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import Provider from '../Provider.svelte';
3
+ import type { WebMcpProviderConfig } from '../web/webmcp-provider.js';
4
+
5
+ interface Props {
6
+ webmcp: WebMcpProviderConfig;
7
+ }
8
+
9
+ const { webmcp }: Props = $props();
10
+ </script>
11
+
12
+ <Provider {webmcp}>
13
+ <span></span>
14
+ </Provider>
@@ -0,0 +1,8 @@
1
+ import type { WebMcpProviderConfig } from '../web/webmcp-provider.js';
2
+ interface Props {
3
+ webmcp: WebMcpProviderConfig;
4
+ }
5
+ declare const ProviderWebmcpHarness: import("svelte").Component<Props, {}, "">;
6
+ type ProviderWebmcpHarness = ReturnType<typeof ProviderWebmcpHarness>;
7
+ export default ProviderWebmcpHarness;
8
+ //# sourceMappingURL=provider-webmcp-harness.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-webmcp-harness.svelte.d.ts","sourceRoot":"","sources":["../../src/__tests__/provider-webmcp-harness.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAGtE,UAAU,KAAK;IACb,MAAM,EAAE,oBAAoB,CAAC;CAC9B;AAeD,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
@@ -19,6 +19,7 @@ import {
19
19
  setFormContext,
20
20
  } from '../../state/form-context.js';
21
21
  import { useWebMcpTool } from '../../web/webmcp.svelte.js';
22
+ import { tryGetWebMcpUiContext } from '../../web/webmcp-ui-context.js';
22
23
  import type { LLMModelId, STTAdapterType } from './types.js';
23
24
 
24
25
  const { t } = useI18n();
@@ -80,11 +81,21 @@ const stt = useSTT();
80
81
  const instanceId = $props.id();
81
82
  const generatedFormId = `smrt-form-${instanceId}`;
82
83
  const localInteractionRegistry = createControlInteractionRegistry();
84
+ const providerWebMcpUi = tryGetWebMcpUiContext();
83
85
  const resolvedFormId = $derived(formId ?? id ?? name ?? generatedFormId);
84
86
  const resolvedInteractionRegistry = $derived(
85
- interactionRegistry ?? localInteractionRegistry,
87
+ interactionRegistry ??
88
+ (providerWebMcpUi?.enabled
89
+ ? providerWebMcpUi.controlRegistry
90
+ : undefined) ??
91
+ localInteractionRegistry,
86
92
  );
87
- const interactionDisposers = new Map<string, () => void>();
93
+ const interactionDisposers = new Map<
94
+ string,
95
+ { field: FieldDefinition; dispose: () => void }
96
+ >();
97
+ let activeInteractionRegistry: ControlInteractionRegistry | undefined;
98
+ let activeInteractionFormId: string | undefined;
88
99
 
89
100
  setControlInteractionContext({
90
101
  get formId() {
@@ -97,7 +108,14 @@ setControlInteractionContext({
97
108
 
98
109
  $effect(() => {
99
110
  if (!oninteraction) return;
100
- return resolvedInteractionRegistry.subscribe(oninteraction);
111
+ return resolvedInteractionRegistry.subscribe((event) => {
112
+ if (event.identity.formId !== resolvedFormId) return;
113
+ try {
114
+ oninteraction(event);
115
+ } catch (error) {
116
+ logger.warn('Form: interaction observer failed', { error });
117
+ }
118
+ });
101
119
  });
102
120
 
103
121
  // Internal state
@@ -132,47 +150,104 @@ function interactionKind(field: FieldDefinition): ControlKind {
132
150
  }
133
151
  }
134
152
 
153
+ function registerInteraction(
154
+ registry: ControlInteractionRegistry,
155
+ currentFormId: string,
156
+ field: FieldDefinition,
157
+ ): (() => void) | undefined {
158
+ const identity = {
159
+ formId: currentFormId,
160
+ controlId: field.controlId ?? field.name,
161
+ };
162
+ if (registry.get(identity)) {
163
+ logger.warn('Form: duplicate interaction identity rejected', { identity });
164
+ return undefined;
165
+ }
166
+ return registry.register({
167
+ identity,
168
+ metadata: {
169
+ kind: interactionKind(field),
170
+ label: field.label,
171
+ description: field.description,
172
+ sensitivity: field.sensitivity ?? 'public',
173
+ readable: field.readable,
174
+ writable: field.writable,
175
+ constraints: field.constraints,
176
+ options: field.options,
177
+ unit: field.unit,
178
+ },
179
+ getValue: field.getValue,
180
+ setValue: field.setValue,
181
+ clear: field.clear ?? (() => field.setValue('')),
182
+ focus: field.focus,
183
+ reveal: field.reveal,
184
+ highlight: field.highlight,
185
+ validate: field.validate,
186
+ getState: field.getState,
187
+ });
188
+ }
189
+
190
+ let interactionRegistryRevision = $state(0);
191
+
192
+ $effect(() => {
193
+ const registry = resolvedInteractionRegistry;
194
+ return registry.subscribe((event) => {
195
+ if (event.type === 'registered' || event.type === 'unregistered') {
196
+ interactionRegistryRevision += 1;
197
+ }
198
+ });
199
+ });
200
+
201
+ $effect(() => {
202
+ const registry = resolvedInteractionRegistry;
203
+ const currentFormId = resolvedFormId;
204
+ const currentFields = new Map(fields);
205
+ void interactionRegistryRevision;
206
+ if (
207
+ activeInteractionRegistry !== registry ||
208
+ activeInteractionFormId !== currentFormId
209
+ ) {
210
+ for (const registration of interactionDisposers.values()) {
211
+ registration.dispose();
212
+ }
213
+ interactionDisposers.clear();
214
+ activeInteractionRegistry = registry;
215
+ activeInteractionFormId = currentFormId;
216
+ }
217
+ for (const [name, registration] of interactionDisposers) {
218
+ const identity = {
219
+ formId: currentFormId,
220
+ controlId: registration.field.controlId ?? registration.field.name,
221
+ };
222
+ if (
223
+ currentFields.get(name) !== registration.field ||
224
+ !registry.get(identity)
225
+ ) {
226
+ registration.dispose();
227
+ interactionDisposers.delete(name);
228
+ }
229
+ }
230
+ for (const [name, field] of currentFields) {
231
+ if (interactionDisposers.has(name)) continue;
232
+ const dispose = registerInteraction(registry, currentFormId, field);
233
+ if (!dispose) continue;
234
+ interactionDisposers.set(name, {
235
+ field,
236
+ dispose,
237
+ });
238
+ }
239
+ });
240
+
135
241
  // Create form context
136
242
  const formContext: SMRTFormContext = {
137
243
  get mode() {
138
244
  return app.state.mode === 'smrt' ? 'smrt' : 'default';
139
245
  },
140
246
  registerField(field: FieldDefinition) {
141
- interactionDisposers.get(field.name)?.();
142
247
  fields.set(field.name, field);
143
248
  fields = new Map(fields); // Trigger reactivity
144
- interactionDisposers.set(
145
- field.name,
146
- resolvedInteractionRegistry.register({
147
- identity: {
148
- formId: resolvedFormId,
149
- controlId: field.controlId ?? field.name,
150
- },
151
- metadata: {
152
- kind: interactionKind(field),
153
- label: field.label,
154
- description: field.description,
155
- sensitivity: field.sensitivity ?? 'public',
156
- readable: field.readable,
157
- writable: field.writable,
158
- constraints: field.constraints,
159
- options: field.options,
160
- unit: field.unit,
161
- },
162
- getValue: field.getValue,
163
- setValue: field.setValue,
164
- clear: field.clear ?? (() => field.setValue('')),
165
- focus: field.focus,
166
- reveal: field.reveal,
167
- highlight: field.highlight,
168
- validate: field.validate,
169
- getState: field.getState,
170
- }),
171
- );
172
249
  },
173
250
  unregisterField(name: string) {
174
- interactionDisposers.get(name)?.();
175
- interactionDisposers.delete(name);
176
251
  fields.delete(name);
177
252
  fields = new Map(fields);
178
253
  },
@@ -762,7 +837,9 @@ onDestroy(() => {
762
837
  clearTimeout(silenceTimer);
763
838
  }
764
839
  stopAudioLevelMonitoring();
765
- for (const dispose of interactionDisposers.values()) dispose();
840
+ for (const registration of interactionDisposers.values()) {
841
+ registration.dispose();
842
+ }
766
843
  interactionDisposers.clear();
767
844
  });
768
845
 
@@ -1 +1 @@
1
- {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAIhC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAYtC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAyzBD,QAAA,MAAM,IAAI;uBAhFe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kCAQZ,0BAA0B;MAwEZ,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Form.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,0BAA0B,EAIhC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAatC,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG7D,MAAM,WAAW,KAAK;IACpB,oBAAoB;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,8BAA8B;IAC9B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB;IACvB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnE,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3D,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACxB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,0BAA0B,CAAC;IACjD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACzD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAs4BD,QAAA,MAAM,IAAI;uBAhFe,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;kCAQZ,0BAA0B;MAwEZ,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -27,7 +27,7 @@ describe('Form WebMCP submit intent', () => {
27
27
  it('registers a field-derived tool and dispatches validation + submit', async () => {
28
28
  const registered = [];
29
29
  document.modelContext = {
30
- registerTool(tool) {
30
+ async registerTool(tool) {
31
31
  registered.push(tool);
32
32
  },
33
33
  };
@@ -54,7 +54,7 @@ describe('Form WebMCP submit intent', () => {
54
54
  it('publishes required and numeric constraints and validates them before submit', async () => {
55
55
  const registered = [];
56
56
  document.modelContext = {
57
- registerTool(tool) {
57
+ async registerTool(tool) {
58
58
  registered.push(tool);
59
59
  },
60
60
  };
@@ -94,7 +94,7 @@ describe('Form WebMCP submit intent', () => {
94
94
  it('publishes and accepts structured measurement, date range, and address fields', async () => {
95
95
  const registered = [];
96
96
  document.modelContext = {
97
- registerTool(tool) {
97
+ async registerTool(tool) {
98
98
  registered.push(tool);
99
99
  },
100
100
  };
@@ -154,7 +154,7 @@ describe('Form WebMCP submit intent', () => {
154
154
  it('limits the address schema and payload to configured fields', async () => {
155
155
  const registered = [];
156
156
  document.modelContext = {
157
- registerTool(tool) {
157
+ async registerTool(tool) {
158
158
  registered.push(tool);
159
159
  },
160
160
  };
@@ -190,7 +190,7 @@ describe('Form WebMCP submit intent', () => {
190
190
  it('allows partial payloads for optional structured fields', async () => {
191
191
  const registered = [];
192
192
  document.modelContext = {
193
- registerTool(tool) {
193
+ async registerTool(tool) {
194
194
  registered.push(tool);
195
195
  },
196
196
  };
package/dist/index.d.ts CHANGED
@@ -18,4 +18,7 @@ export * from './hooks/index.js';
18
18
  export { default as Provider } from './Provider.svelte';
19
19
  export * from './state/index.js';
20
20
  export { useWebMcpTool, type WebMcpToolSpec } from './web/webmcp.svelte.js';
21
+ export type { WebMcpProviderConfig, WebMcpUiProviderConfig, } from './web/webmcp-provider.js';
22
+ export { type RegisterWebMcpUiToolsOptions, registerWebMcpUiTools, } from './web/webmcp-ui.js';
23
+ export { useWebMcpUi, type WebMcpUiContext, } from './web/webmcp-ui-context.js';
21
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAGH,cAAc,6BAA6B,CAAC;AAE5C,cAAc,8BAA8B,CAAC;AAE7C,cAAc,mBAAmB,CAAC;AAElC,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExD,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAGH,cAAc,6BAA6B,CAAC;AAE5C,cAAc,8BAA8B,CAAC;AAE7C,cAAc,mBAAmB,CAAC;AAElC,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAExD,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC5E,YAAY,EACV,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,WAAW,EACX,KAAK,eAAe,GACrB,MAAM,4BAA4B,CAAC"}
package/dist/index.js CHANGED
@@ -26,3 +26,5 @@ export { default as Provider } from './Provider.svelte';
26
26
  export * from './state/index.js';
27
27
  // Opt-in browser WebMCP lifecycle primitive.
28
28
  export { useWebMcpTool } from './web/webmcp.svelte.js';
29
+ export { registerWebMcpUiTools, } from './web/webmcp-ui.js';
30
+ export { useWebMcpUi, } from './web/webmcp-ui-context.js';