@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
package/AGENTS.md CHANGED
@@ -20,6 +20,9 @@ subpath you are editing. This file keeps what holds in every module.
20
20
  | `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) | — |
21
21
  | `src/web/webmcp-ui.ts` | Fixed, low-cardinality WebMCP adapter over the Provider's mounted form-control and data-surface registries (#2521) | — |
22
22
  | `src/web/webmcp.svelte.ts` (`useWebMcpTool`) | Component-owned bespoke WebMCP tool. Routes through `@happyvertical/smrt-web`'s `registerWebMcpBespokeTool`, so it shares the fail-closed effect classification and `effects` exposure policy of generated tools — undeclared `annotations` classify destructive and are excluded by default. `namespace` and `maxTools` never apply to a bespoke tool (#2586) | — |
23
+ | `src/web/{list,link,step}-surface.svelte.ts` (`useListSurface`, `useLinkSurface`, `useStepSurface`) | Lifecycle hooks mounting a rendered list (projected `state.rows` + a `find` text lookup), a menu/tab row (`open` → `navigate`, hrefs never published), and a wizard (`next`/`back`/`go-to`; a writing step is revealed via `showNext`, never pressed) on the Provider's data-surface registry. The Svelte-free halves live in `@happyvertical/smrt-ui/data` | — |
24
+ | `src/web/webmcp-ui-context.ts` (`useWebMcpUi`, `tryUseWebMcpUi`) | Provider mounted-UI registries; `tryUseWebMcpUi` returns `null` instead of throwing when there is no Provider or its WebMCP UI is off (#2915) | — |
25
+ | `src/components/forms/Form.svelte` + `FormScope.svelte` | Rich Form spreads native attributes/attachments, takes `enhance`, and its `*_stage_changes` tool covers smrt-ui controls via `controlProposalProperties`/`stageControlProposals` (smrt-ui); `FormScope` is the same without a `<form>` element | — |
23
26
 
24
27
  The composed WebMCP fixture in
25
28
  `src/web/__tests__/webmcp-composed.integration.svelte.test.ts` mounts a real
package/README.md CHANGED
@@ -182,6 +182,25 @@ default) — except a canonical table-control id (`set-filters`, `reset`,
182
182
  reaches `onControl`, even under a custom label. See
183
183
  `docs/data-surface-conformance.md` for the full contract.
184
184
 
185
+ Three lifecycle hooks cover the common page shapes without any registration
186
+ code; each resolves the nearest Provider's registry (or an explicit one),
187
+ re-mounts on an identity change, and unmounts with the component:
188
+
189
+ - `useListSurface(() => ({ surfaceId, label, description, columns, rows }))`
190
+ publishes the page's rendered rows, projected to the declared columns and
191
+ capped (`maxRows`, default 50), as `state.rows`. Columns marked
192
+ `searchable` enable a `find` control: `{ text }` publishes the rows whose
193
+ searchable columns contain the text (any case, accents ignored) across all
194
+ rows, with `state.find = { text, matches }` — so an agent can look an
195
+ article up by title on the page. It narrows only what the agent reads;
196
+ table commands stay refused and the page's own controls stay in charge.
197
+ - `useLinkSurface(() => ({ surfaceId, label, description, links, navigate }))`
198
+ exposes a menu or tab row as an `open` control (see `registerLinkSurface`
199
+ in smrt-ui).
200
+ - `useStepSurface(() => ({ surfaceId, …, steps, current, next, back,
201
+ nextWrites, showNext }))` exposes a wizard's `next`/`back`/`go-to`; a step
202
+ whose forward button writes is revealed for the person, never pressed.
203
+
185
204
  The default prefix is `smrt_ui_`. Configure `ui.prefix` when multiple Providers
186
205
  must coexist in one document; the same prefix cannot be registered twice. The
187
206
  six derived names are reserved through the document-global tool-name lock, so
@@ -201,6 +220,47 @@ hidden data-surface columns are not serialized. Read responses are marked as
201
220
  untrusted content. Bespoke `useWebMcpTool` and `<Form webmcp>` tools retain their
202
221
  existing lifecycle and submit behavior.
203
222
 
223
+ #### `<Form webmcp>` with SvelteKit `enhance` and smrt-ui fields
224
+
225
+ The rich `Form` renders its own `<form>`, so it forwards every native form
226
+ attribute (`method`, `action`, `enctype`, `novalidate`, `autocomplete`,
227
+ `aria-*`, `data-*`, …) and attachments (`{@attach …}`) to that element. A
228
+ component cannot take `use:`, so pass SvelteKit's `enhance` as a prop — it is
229
+ applied to the rendered `<form>` and torn down with it. Without `onsubmit`
230
+ the Form never prevents submission, so `enhance` (or a native POST) runs as
231
+ usual and an error summary above the fields keeps working:
232
+
233
+ ```svelte
234
+ <script lang="ts">
235
+ import { enhance } from '$app/forms';
236
+ import { Form } from '@happyvertical/smrt-svelte/forms';
237
+ import { ErrorSummary, FormGroup, Input } from '@happyvertical/smrt-ui/forms';
238
+ </script>
239
+
240
+ <Form formId="setup-network" webmcp method="POST" action="?/create" novalidate
241
+ enhance={(form) => enhance(form, submit)}>
242
+ <ErrorSummary errors={errors} />
243
+ <FormGroup label="Network name" id="name"><Input name="name" bind:value={name} /></FormGroup>
244
+ </Form>
245
+ ```
246
+
247
+ Its `<formId>_stage_changes` tool describes rich fields (`TextInput`,
248
+ `MoneyInput`, …) and every other control the registry holds for the form:
249
+ smrt-ui primitives (`FormGroup` + `Input`/`Select`/`Textarea`/`Combobox`/…)
250
+ and composites registered with `useControlRegistration`. Proposals for either
251
+ kind are staged for the person to review and apply; nothing is written or
252
+ submitted by the tool.
253
+
254
+ `FormScope` (`@happyvertical/smrt-svelte/forms`) is the same tool and review
255
+ surface without a `<form>` element — for a fetch-driven wizard, an editor
256
+ saved in parts, or a page that must keep its own `<form>` markup. It joins
257
+ the nearest Provider's control registry (or a local one) and registers no
258
+ tool until at least one proposable control is mounted.
259
+
260
+ `tryUseWebMcpUi()` returns the Provider's mounted-UI registries, or `null`
261
+ when there is no Provider or its WebMCP UI is off — use it to fall back to a
262
+ local registry instead of catching `useWebMcpUi()`'s throw (#2915).
263
+
204
264
  Custom rich fields may continue to call `registerField(field)` and later
205
265
  `unregisterField(name)`. New code should retain and invoke the disposer returned
206
266
  by `registerField`: it is bound to that exact registration, so cleanup cannot
@@ -319,6 +379,112 @@ fetch) and mount `AdminShell` in `+layout.svelte`. The `template-sveltekit`
319
379
  scaffold adopts AdminShell as its default chrome exactly this way; copy its
320
380
  `src/routes/+layout.server.ts` / `+layout.svelte` / `settings/+page.svelte`.
321
381
 
382
+ #### Responsive chrome and resizable edges
383
+
384
+ AdminShell carries the phone/tablet/desktop chrome itself, so hosts do not
385
+ wrap it in a frame. Everything below is opt-in; without these props and config
386
+ keys the shell renders exactly as before.
387
+
388
+ ```svelte
389
+ <script lang="ts">
390
+ import {
391
+ AdminShell,
392
+ createShellState,
393
+ PhoneBottomBar,
394
+ PhoneTopBar,
395
+ ShellNavToggle,
396
+ ShellTitle,
397
+ phoneTopBarFor,
398
+ } from '@happyvertical/smrt-svelte/workspace';
399
+
400
+ const shell = createShellState({
401
+ config: {
402
+ top: false,
403
+ bottom: false,
404
+ left: {
405
+ label: 'Navigation',
406
+ // Drawer on phones, icons on tablets, open on desktop.
407
+ viewportDefaults: { phone: 'collapsed', tablet: 'collapsed', desktop: 'expanded' },
408
+ },
409
+ right: {
410
+ label: 'Assistant',
411
+ expandedSize: '28rem',
412
+ resizable: { min: 320, max: 720 }, // drag, arrows, Home/End, double-click resets
413
+ phone: 'sheet', // or 'hidden' when the host shows it elsewhere on phones
414
+ keepMounted: true, // closing the sheet keeps the chat (and its draft) alive
415
+ overlayMedia: '(max-width: 99.9375rem)', // slide over the page below 1600px
416
+ },
417
+ },
418
+ storageKey: 'my-app-shell',
419
+ });
420
+ </script>
421
+
422
+ <AdminShell state={shell} path={page.url.pathname} phone={{ scrim: true, swipeToClose: true }}>
423
+ {#snippet header()}
424
+ <ShellNavToggle expanded={shell.panels.left === 'expanded'} onclick={() => shell.togglePanel('left')} />
425
+ <ShellTitle title="Acme" href="/" />
426
+ {/snippet}
427
+ {#snippet phoneTopBar()}
428
+ <PhoneTopBar model={phoneTopBarFor({ path, homeHref: '/', homeTitle: 'Acme', navItems })} homeHref="/" />
429
+ {/snippet}
430
+ {#snippet phoneBottomBar()}
431
+ <PhoneBottomBar {items} />
432
+ {/snippet}
433
+ {#snippet overlays({ viewport })}<!-- PhoneSheet, WorkingStrip, … -->{/snippet}
434
+ …
435
+ </AdminShell>
436
+ ```
437
+
438
+ - **`header`** is a full-width row above every edge (`#smrt-admin-shell-header`,
439
+ `--smrt-admin-shell-header-size`, default 3.5rem). On phones it is replaced
440
+ by `phoneTopBar` when one is given.
441
+ - **Viewport classes**: `shell.viewport` is `phone` (≤ 48rem), `tablet`
442
+ (≤ 64rem) or `desktop`; `ADMIN_SHELL_PHONE_QUERY` / `ADMIN_SHELL_DESKTOP_QUERY`.
443
+ `viewportDefaults` apply on first render and whenever the class changes; such
444
+ an edge never persists its open/closed state.
445
+ - **Phone chrome**: `phoneTopBar` overlays the top of the main region and hides
446
+ on scroll (`phone.hideOnScroll`, pinned by `pinChrome` or an open drawer);
447
+ `phoneBottomBar` is a bottom row that a form's `[data-form-action-bar]` (see
448
+ smrt-ui `FormActionBar`) replaces and the on-screen keyboard hides
449
+ (`:root[data-keyboard-open]`). `phone.scrim` dims the page behind a drawer or
450
+ sheet, `phone.swipeToClose` closes drawers by swiping. A `path` change on a
451
+ phone closes open drawers. `overlays` is a layer above the edges (and above
452
+ the bottom bar's row) for `PhoneSheet`s and status strips.
453
+ - **Page contracts** on phones: `data-shell-breadcrumbs` is hidden,
454
+ `data-shell-page-title` is visually hidden while the phone top bar shows
455
+ (the bar carries the title), `data-shell-tabs` sticks under the top bar and
456
+ slides away with it. smrt-ui's `PageHeader` renders the first two.
457
+ - **Page trail**: `shellPageTrailFor({ path, homeHref, homeTitle, navItems,
458
+ parents })` is the one source for a page's ancestors: section homes (home
459
+ and top-level nav pages) have none; other pages get the home, the nav items
460
+ above them, then page-given `parents`, never the page itself. Pass the
461
+ `crumbs` to `PageHeader`; `phoneTopBarFor` (same input plus `pageTitle`)
462
+ sends the phone back arrow to the last crumb.
463
+ - **Resizable edges**: `resizable` on a `push` side edge adds a
464
+ `role="separator"` (with `aria-valuenow/min/max` in px) on its inner border.
465
+ Sizes are stored as `ShellSettingsDelta.sizes` through the settings adapter;
466
+ `persist: false` / `{ state?: false, size?: false }` opts an edge out of
467
+ storage.
468
+ - **Overlay edges**: `overlayMedia` on a side edge is a media query under
469
+ which the expanded edge slides over the page (above a scrim, content keeps
470
+ its width) instead of pushing it, on tablet and desktop; above it the edge
471
+ docks. `shell.presentationFor(edge)` reports the live presentation. While
472
+ an overlay is open the page and the other edge are `inert`, the panel takes
473
+ focus (unless something inside already has it) and returns it on close;
474
+ Escape or a scrim click closes it, sliding it back out as the scrim fades
475
+ (no motion under `prefers-reduced-motion`). Resizing applies only while docked.
476
+ - **Kept panels**: `keepMounted: true` keeps a collapsed edge's panel content
477
+ (`appPanel`, `tenantPanel`, the focus panel, `systemPanel`) mounted with the
478
+ `hidden` attribute instead of unmounting it, so component state (a chat
479
+ draft, scroll position) survives closing. The rail renders as usual; the
480
+ left edge keeps only a `tenantPanel`.
481
+ - **Nav attention**: a `ShellNavItem` with `attention: true` (or a string
482
+ label) shows a dot in `TenantNav`, over the icon when collapsed, and
483
+ announces the label ("Needs attention" by default) inside the link.
484
+ - **Public region ids** (`ADMIN_SHELL_REGION_IDS`): `smrt-admin-shell-header`,
485
+ `smrt-admin-shell-{top,left,right,bottom}-panel`, and
486
+ `smrt-admin-shell-main`, which is the page scroller.
487
+
322
488
  - **Migration guide** (first-generation `WorkspaceShell`/`RoleShell` →
323
489
  `AdminShell`; adoption is additive and non-breaking):
324
490
  [`src/components/workspace/MIGRATION.md`](./src/components/workspace/MIGRATION.md)
@@ -357,7 +523,7 @@ resolve against `smrt-ui`:
357
523
  | `@happyvertical/smrt-ui` | `PermissionCheck`, `permission` / `hasPermission` / `hasAnyPermission` / `hasAllPermissions` |
358
524
  | `@happyvertical/smrt-ui/ui` | UI primitives (Button, Card, Badge, Pagination) |
359
525
  | `@happyvertical/smrt-ui/layout` | Layout (Container, Grid, Header, Footer, Masthead, etc.) |
360
- | `@happyvertical/smrt-ui/calendar` | Calendar and DayView |
526
+ | `@happyvertical/smrt-ui/calendar` | CalendarView (deprecated: Calendar, DayView) |
361
527
  | `@happyvertical/smrt-ui/data` | DataTable, CollectionList/ContentList, CollectionToolbar |
362
528
  | `@happyvertical/smrt-ui/feedback` | Modal, ConfirmDialog, LoadingOverlay, ProgressBar |
363
529
  | `@happyvertical/smrt-ui/chat` | Message bubble, reaction picker, typing indicator |
@@ -447,3 +613,50 @@ public registry interactions. It does not require `document.modelContext` or
447
613
  export mounted closures to remote MCP. Agent proposals retain trusted human
448
614
  staged review. See the [bridge contract](../mcp-apps/README.md) for origin
449
615
  configuration, capabilities, fallback and synthetic-browser evidence limits.
616
+
617
+ AdminShell's optional `homeHref` turns default branding into a named home link.
618
+ Use `logoSrc`/`logoAlt` for a logo or a `brand` snippet receiving `{ compact }`
619
+ for custom marks. A collapsed tenant rail keeps a compact linked logo or initial;
620
+ the default app bar keeps the full brand visible at narrow widths. A custom
621
+ `appBar` continues to own its branding. Without these props, branding stays text.
622
+
623
+ ### Voice mode migration (#3261)
624
+
625
+ **Changed default:** Provider and standalone `createAppState()` now keep voice
626
+ mode off unless explicitly requested. Omitted `autoEnableSmrt` defaults to
627
+ `false`; speech-capable browsers stay in `default` mode. Apps that relied on
628
+ automatic voice activation must opt in with `autoEnableSmrt={true}`. Use
629
+ `mode="smrt"` for an explicit voice-mode choice. An explicit `mode="default"`
630
+ wins over auto-detection from the start of initialization, including when
631
+ `autoEnableSmrt` is true; no transient voice-mode activation occurs.
632
+ Standalone state callers can opt in through initial session preferences
633
+ (`autoEnableSmrt: true`); `initialMode` takes precedence. Runtime mode prop
634
+ updates retain their existing behavior.
635
+
636
+ ### Runtime panel controls (#3246)
637
+
638
+ Use `shell.setPanelState(edge, 'hidden' | 'collapsed' | 'expanded')` to persist
639
+ runtime panel preferences; app-configured hidden edges remain unavailable.
640
+ AdminShell keeps a discoverable tenant collapse control with supplied navigation
641
+ (`showTenantToggle={false}` opts out), a Menu opener in narrow layouts, and the
642
+ system toggle alongside a custom `systemBar`. Closed narrow drawers are inert;
643
+ opening focuses the first control, and closing or Escape restores the opener.
644
+ TenantNav accepts `density="touch"` for canonical 48px link targets, including
645
+ collapsed rail links; omit density to inherit the theme's control density.
646
+
647
+ `AdminShell` keeps a supplied `tenantFooter` in the app bar while the left pane
648
+ is collapsed, hidden, or a closed narrow drawer; it moves back to the expanded
649
+ pane without duplicate account controls. Use the optional `account` snippet for
650
+ an account permanently in the app bar (including with a custom `appBar`).
651
+ `WorkspaceAccountMenu density="touch"` sizes its trigger and menu actions to the
652
+ canonical 48px target; omit density to inherit the theme, or choose
653
+ `"comfortable"`. Account menus in the shell app bar open below the bar so the
654
+ existing footer's default placement stays reachable on narrow screens.
655
+
656
+ `TenantNav` accepts optional `groups: ShellNavGroup[]` (`{ heading, items }`)
657
+ next to its existing flat `items`. Each group has a labelled `role="group"` and
658
+ native disclosure summary; keyboard and touch users can collapse it even in the
659
+ icon rail. Groups start open, preserve nested links and active-route markers,
660
+ and inherit the navigation density. Supply `aria-label="Shop navigation"` for
661
+ an instance-specific landmark name; omission uses the localized default.
662
+ `ShellNavGroup` is exported from `@happyvertical/smrt-svelte/workspace`.
@@ -34,7 +34,14 @@ props, a golden test, and a playground page (`playground/.../primitives`).
34
34
  migrate domain re-rolls *onto* them rather than build new primitives:
35
35
 
36
36
  - **`FileUpload`** (`./forms`) — the canonical upload input; replace ad-hoc
37
- drop zones.
37
+ drop zones. With `name` it posts in a native `multipart/form-data` submit:
38
+ the accepted list (picks, drops, removals, bound resets) is mirrored onto the
39
+ visually-hidden input via `DataTransfer` — never `hidden`, which would break
40
+ `required` validation. Without that constructor the input keeps only a
41
+ native selection that matches the list, else clears (#3260). A reset of the
42
+ owning form (`form.reset()`, a reset button, `enhance`'s `update()`) empties
43
+ the list and reports `onchange([])`, so the list never shows files that no
44
+ longer post.
38
45
  - **`Modal` + forms** (`./feedback` + `./forms`) — compose for dialogs; no
39
46
  bespoke modal shells.
40
47
  - **`ConfirmDialog`** (`./feedback`) — the standard confirm/destructive-action
@@ -45,6 +45,30 @@ App settings, tenant names and environment badges wrap within narrow drawers.
45
45
  system bars never clip wrapped chips. Custom bars must allow this flex child
46
46
  to shrink; use safe alignment if aligning its contents to the end.
47
47
 
48
+ ### Responsive chrome, viewport defaults, resizable edges
49
+
50
+ The root grid has five rows: `header` · top edge · body · bottom edge · phone
51
+ bottom bar. The header and phone-bar tracks are `0rem` unless their snippets
52
+ render, so hosts that pass none of the new props get the original layout; keep
53
+ that invariant (`AdminShell.responsive.test.ts` "backward compatibility").
54
+ Phone drawers and the bottom drawer offset themselves by the header and phone
55
+ bar tracks. `shell.viewport` comes from `ADMIN_SHELL_PHONE_QUERY` (48rem, the
56
+ same boundary as the CSS media query — never make one configurable without the
57
+ other) and `ADMIN_SHELL_DESKTOP_QUERY` (64rem); `installShellViewport` runs on
58
+ mount and the `ShellState` constructor detects the initial class so the first
59
+ paint is right.
60
+
61
+ Edge config: `viewportDefaults` (applied per class change; such an edge never
62
+ stores its state), `phone: 'drawer' | 'sheet' | 'hidden'` (`sheet` is right
63
+ edge only), `resizable` (side edges, `push` only, not on phones) and `persist`.
64
+ `stripUnpersistedSettings` runs on both `hydrate()` and adapter writes;
65
+ runtime `applySettings` is never filtered. Resized widths live in
66
+ `ShellSettingsDelta.sizes` (px; `null` = configured size, pruned on write).
67
+ Drags update with `persist: false` and persist once on pointer up.
68
+
69
+ The resize separator is a focusable `role="separator"`: its svelte-ignore of
70
+ the non-interactive a11y warnings is deliberate (WAI-ARIA window splitter).
71
+
48
72
  Run `pnpm --filter @happyvertical/smrt-svelte test:e2e` after building the
49
73
  package dependencies. The package-local fixture exercises real Chromium bounds
50
74
  and touch input across mobile widths and desktop panel-state combinations.
@@ -26,12 +26,12 @@ import { setWebMcpUiContext } from './web/webmcp-ui-context.js';
26
26
  interface Props {
27
27
  /**
28
28
  * Initial mode: 'default' | 'smrt'
29
- * If not provided, mode is auto-detected based on capabilities
29
+ * Defaults to standard mode; auto-detection requires autoEnableSmrt=true
30
30
  */
31
31
  mode?: AppMode;
32
32
  /**
33
33
  * Whether to auto-enable smrt mode when capabilities are available
34
- * @default true
34
+ * @default false
35
35
  */
36
36
  autoEnableSmrt?: boolean;
37
37
  /**
@@ -98,7 +98,7 @@ interface Props {
98
98
 
99
99
  const {
100
100
  mode,
101
- autoEnableSmrt = true,
101
+ autoEnableSmrt = false,
102
102
  user = null,
103
103
  permissions = [],
104
104
  socket,
@@ -114,18 +114,22 @@ const {
114
114
  // Determine if we should show the loading overlay
115
115
  const showLoadingOverlay = $derived(ai?.showLoadingOverlay ?? true);
116
116
 
117
- // Create app state
118
- const appState = createAppState({
119
- onCapabilitiesDetected: () => {
120
- onReady?.();
121
- },
122
- onModeChange: (newMode) => {
123
- onModeChange?.(newMode);
124
- },
125
- onAILoadingChange: (state) => {
126
- onAILoadingChange?.(state);
127
- },
128
- });
117
+ // Capture initial props once; the effects below own subsequent prop updates.
118
+ const appState = untrack(() =>
119
+ createAppState({
120
+ initialMode: mode,
121
+ session: { preferences: { autoEnableSmrt } },
122
+ onCapabilitiesDetected: () => {
123
+ onReady?.();
124
+ },
125
+ onModeChange: (newMode) => {
126
+ onModeChange?.(newMode);
127
+ },
128
+ onAILoadingChange: (state) => {
129
+ onAILoadingChange?.(state);
130
+ },
131
+ }),
132
+ );
129
133
 
130
134
  const localControlRegistry = createControlInteractionRegistry();
131
135
  const localDataSurfaceRegistry = createDataSurfaceRegistry();
@@ -5,12 +5,12 @@ import type { WebMcpProviderConfig } from './web/webmcp-provider.js';
5
5
  interface Props {
6
6
  /**
7
7
  * Initial mode: 'default' | 'smrt'
8
- * If not provided, mode is auto-detected based on capabilities
8
+ * Defaults to standard mode; auto-detection requires autoEnableSmrt=true
9
9
  */
10
10
  mode?: AppMode;
11
11
  /**
12
12
  * Whether to auto-enable smrt mode when capabilities are available
13
- * @default true
13
+ * @default false
14
14
  */
15
15
  autoEnableSmrt?: boolean;
16
16
  /**
@@ -1 +1 @@
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;AAI9B,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;AA4PD,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;AAI9B,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;AAgQD,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 { AppMode } from '../state/app-state.js';
4
+ import type { SmrtAppStateManager } from '../state/app-state.svelte.js';
5
+ import Probe from './provider-mode-probe.svelte';
6
+
7
+ interface Props {
8
+ mode?: AppMode;
9
+ autoEnableSmrt?: boolean;
10
+ capture: (state: SmrtAppStateManager) => void;
11
+ }
12
+ const { mode, autoEnableSmrt, capture }: Props = $props();
13
+ </script>
14
+ <Provider {mode} {autoEnableSmrt}><Probe {capture} /></Provider>
@@ -0,0 +1,11 @@
1
+ import type { AppMode } from '../state/app-state.js';
2
+ import type { SmrtAppStateManager } from '../state/app-state.svelte.js';
3
+ interface Props {
4
+ mode?: AppMode;
5
+ autoEnableSmrt?: boolean;
6
+ capture: (state: SmrtAppStateManager) => void;
7
+ }
8
+ declare const ProviderModeHarness: import("svelte").Component<Props, {}, "">;
9
+ type ProviderModeHarness = ReturnType<typeof ProviderModeHarness>;
10
+ export default ProviderModeHarness;
11
+ //# sourceMappingURL=provider-mode-harness.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-mode-harness.svelte.d.ts","sourceRoot":"","sources":["../../src/__tests__/provider-mode-harness.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAIxE,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC/C;AAaD,QAAA,MAAM,mBAAmB,2CAAwC,CAAC;AAClE,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAClE,eAAe,mBAAmB,CAAC"}
@@ -0,0 +1,13 @@
1
+ <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import type { SmrtAppStateManager } from '../state/app-state.svelte.js';
4
+ import { getAppStateContext } from '../state/context.js';
5
+
6
+ interface Props {
7
+ capture: (state: SmrtAppStateManager) => void;
8
+ }
9
+ const { capture }: Props = $props();
10
+ const appState = getAppStateContext();
11
+ untrack(() => capture(appState));
12
+ </script>
13
+ <output aria-label="App mode">{appState.state.mode}</output>
@@ -0,0 +1,8 @@
1
+ import type { SmrtAppStateManager } from '../state/app-state.svelte.js';
2
+ interface Props {
3
+ capture: (state: SmrtAppStateManager) => void;
4
+ }
5
+ declare const ProviderModeProbe: import("svelte").Component<Props, {}, "">;
6
+ type ProviderModeProbe = ReturnType<typeof ProviderModeProbe>;
7
+ export default ProviderModeProbe;
8
+ //# sourceMappingURL=provider-mode-probe.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-mode-probe.svelte.d.ts","sourceRoot":"","sources":["../../src/__tests__/provider-mode-probe.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAIxE,UAAU,KAAK;IACb,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC/C;AAcD,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -1,5 +1,14 @@
1
1
  import type { InitState, OnProgress } from '../../core/types.js';
2
2
  import type { BrowserSpeechSTTOptions, STTAdapter, STTCapabilities, STTOptions, STTResult } from './types.js';
3
+ /**
4
+ * An error from the browser's speech recognition. `speechError` is the Web
5
+ * Speech error code (`network`, `not-allowed`, `service-not-allowed`,
6
+ * `no-speech`, `audio-capture`, `aborted`, `language-not-supported`, …).
7
+ */
8
+ export type BrowserSpeechError = Error & {
9
+ speechError?: string;
10
+ speechMessage?: string;
11
+ };
3
12
  /**
4
13
  * Browser Speech Recognition adapter
5
14
  */
@@ -9,6 +18,8 @@ export declare class BrowserSpeechSTTAdapter implements STTAdapter {
9
18
  private recognition;
10
19
  private options;
11
20
  private _isListening;
21
+ /** Set while our own `abort()` runs, so its `aborted` error is not reported. */
22
+ private aborting;
12
23
  private resultListeners;
13
24
  private errorListeners;
14
25
  private startListeners;
@@ -1 +1 @@
1
- {"version":3,"file":"browser-speech.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/browser-speech.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,uBAAuB,EACvB,UAAU,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACV,MAAM,YAAY,CAAC;AAWpB;;GAEG;AACH,qBAAa,uBAAwB,YAAW,UAAU;IACxD,QAAQ,CAAC,IAAI,EAAG,gBAAgB,CAAU;IAE1C,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,YAAY,CAAS;IAG7B,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,YAAY,CAAyB;gBAEjC,OAAO,GAAE,uBAA4B;IAQjD,IAAI,SAAS,IAAI,SAAS,CAEzB;IAEK,iBAAiB,CAAC,WAAW,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAqChE,OAAO,CAAC,wBAAwB;IAwEhC,eAAe,IAAI,eAAe;IA2B5B,KAAK,CAAC,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,KAAK,IAAI,IAAI;IAMb,WAAW,IAAI,OAAO;IAItB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI;IAK3D,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKrD,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKzC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAS/B"}
1
+ {"version":3,"file":"browser-speech.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/stt/browser-speech.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,uBAAuB,EACvB,UAAU,EACV,eAAe,EACf,UAAU,EACV,SAAS,EACV,MAAM,YAAY,CAAC;AAWpB;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;GAEG;AACH,qBAAa,uBAAwB,YAAW,UAAU;IACxD,QAAQ,CAAC,IAAI,EAAG,gBAAgB,CAAU;IAE1C,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,YAAY,CAAS;IAC7B,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,YAAY,CAAyB;gBAEjC,OAAO,GAAE,uBAA4B;IAQjD,IAAI,SAAS,IAAI,SAAS,CAEzB;IAEK,iBAAiB,CAAC,WAAW,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAqChE,OAAO,CAAC,wBAAwB;IAqFhC,eAAe,IAAI,eAAe;IA2B5B,KAAK,CAAC,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAM3B,KAAK,IAAI,IAAI;IAOb,WAAW,IAAI,OAAO;IAItB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,MAAM,IAAI;IAK3D,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKrD,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKzC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAS/B"}
@@ -23,6 +23,8 @@ export class BrowserSpeechSTTAdapter {
23
23
  recognition = null;
24
24
  options;
25
25
  _isListening = false;
26
+ /** Set while our own `abort()` runs, so its `aborted` error is not reported. */
27
+ aborting = false;
26
28
  // Event listeners
27
29
  resultListeners = new Set();
28
30
  errorListeners = new Set();
@@ -94,8 +96,13 @@ export class BrowserSpeechSTTAdapter {
94
96
  }
95
97
  };
96
98
  this.recognition.onerror = (event) => {
99
+ const code = event.error;
100
+ if (code === 'aborted' && this.aborting) {
101
+ // We called abort() ourselves: not an error.
102
+ return;
103
+ }
97
104
  let error;
98
- switch (event.error) {
105
+ switch (code) {
99
106
  case 'not-allowed':
100
107
  case 'service-not-allowed':
101
108
  error = new PermissionDeniedError('microphone', 'browser-speech');
@@ -110,11 +117,18 @@ export class BrowserSpeechSTTAdapter {
110
117
  error = new Error('Network error during speech recognition');
111
118
  break;
112
119
  case 'aborted':
113
- // User aborted, not really an error
114
- return;
120
+ // Not our abort(): the browser (or another tab) cut it off.
121
+ error = new Error('Speech recognition was aborted');
122
+ break;
115
123
  default:
116
- error = new Error(`Speech recognition error: ${event.error}`);
124
+ error = new Error(`Speech recognition error: ${code}`);
117
125
  }
126
+ // The Web Speech error code, so callers can tell `service-not-allowed`
127
+ // (the browser has no speech service, e.g. Brave) from `not-allowed`
128
+ // (the microphone is blocked), and `network` from everything else.
129
+ error.speechError = code;
130
+ if (event.message)
131
+ error.speechMessage = event.message;
118
132
  for (const cb of this.errorListeners) {
119
133
  cb(error);
120
134
  }
@@ -127,6 +141,7 @@ export class BrowserSpeechSTTAdapter {
127
141
  };
128
142
  this.recognition.onend = () => {
129
143
  this._isListening = false;
144
+ this.aborting = false;
130
145
  for (const cb of this.endListeners) {
131
146
  cb();
132
147
  }
@@ -181,6 +196,7 @@ export class BrowserSpeechSTTAdapter {
181
196
  }
182
197
  abort() {
183
198
  if (this.recognition && this._isListening) {
199
+ this.aborting = true;
184
200
  this.recognition.abort();
185
201
  }
186
202
  }