@happyvertical/smrt-svelte 0.55.9 → 0.55.11

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 (119) hide show
  1. package/AGENTS.md +2 -1
  2. package/agents/cookbook-shell.md +176 -0
  3. package/agents/overview-presentation.md +8 -0
  4. package/agents/overview-surfaces.md +133 -17
  5. package/agents/screens.md +5 -0
  6. package/agents/workspace.md +6 -0
  7. package/dist/components/activity/ActivityList.svelte +35 -0
  8. package/dist/components/activity/ActivityList.svelte.d.ts +11 -0
  9. package/dist/components/activity/ActivityList.svelte.d.ts.map +1 -0
  10. package/dist/components/activity/index.d.ts +3 -0
  11. package/dist/components/activity/index.d.ts.map +1 -0
  12. package/dist/components/activity/index.js +1 -0
  13. package/dist/components/activity/safe-href.d.ts +3 -0
  14. package/dist/components/activity/safe-href.d.ts.map +1 -0
  15. package/dist/components/activity/safe-href.js +12 -0
  16. package/dist/components/activity/types.d.ts +9 -0
  17. package/dist/components/activity/types.d.ts.map +1 -0
  18. package/dist/components/activity/types.js +1 -0
  19. package/dist/components/cookbook/CookbookApp.svelte +236 -0
  20. package/dist/components/cookbook/CookbookApp.svelte.d.ts +5 -0
  21. package/dist/components/cookbook/CookbookApp.svelte.d.ts.map +1 -0
  22. package/dist/components/cookbook/CookbookSectionOverview.svelte +71 -0
  23. package/dist/components/cookbook/CookbookSectionOverview.svelte.d.ts +20 -0
  24. package/dist/components/cookbook/CookbookSectionOverview.svelte.d.ts.map +1 -0
  25. package/dist/components/cookbook/CookbookSectionPage.svelte +133 -0
  26. package/dist/components/cookbook/CookbookSectionPage.svelte.d.ts +5 -0
  27. package/dist/components/cookbook/CookbookSectionPage.svelte.d.ts.map +1 -0
  28. package/dist/components/cookbook/brand-theme.d.ts +11 -0
  29. package/dist/components/cookbook/brand-theme.d.ts.map +1 -0
  30. package/dist/components/cookbook/brand-theme.js +24 -0
  31. package/dist/components/cookbook/build.d.ts +8 -0
  32. package/dist/components/cookbook/build.d.ts.map +1 -0
  33. package/dist/components/cookbook/build.js +277 -0
  34. package/dist/components/cookbook/catalog.d.ts +37 -0
  35. package/dist/components/cookbook/catalog.d.ts.map +1 -0
  36. package/dist/components/cookbook/catalog.js +99 -0
  37. package/dist/components/cookbook/context.d.ts +23 -0
  38. package/dist/components/cookbook/context.d.ts.map +1 -0
  39. package/dist/components/cookbook/context.js +15 -0
  40. package/dist/components/cookbook/index.d.ts +22 -0
  41. package/dist/components/cookbook/index.d.ts.map +1 -0
  42. package/dist/components/cookbook/index.js +20 -0
  43. package/dist/components/cookbook/page.d.ts +23 -0
  44. package/dist/components/cookbook/page.d.ts.map +1 -0
  45. package/dist/components/cookbook/page.js +32 -0
  46. package/dist/components/cookbook/registry.d.ts +37 -0
  47. package/dist/components/cookbook/registry.d.ts.map +1 -0
  48. package/dist/components/cookbook/registry.js +38 -0
  49. package/dist/components/cookbook/sections.d.ts +60 -0
  50. package/dist/components/cookbook/sections.d.ts.map +1 -0
  51. package/dist/components/cookbook/sections.js +88 -0
  52. package/dist/components/cookbook/server.d.ts +61 -0
  53. package/dist/components/cookbook/server.d.ts.map +1 -0
  54. package/dist/components/cookbook/server.js +108 -0
  55. package/dist/components/cookbook/theme.d.ts +21 -0
  56. package/dist/components/cookbook/theme.d.ts.map +1 -0
  57. package/dist/components/cookbook/theme.js +53 -0
  58. package/dist/components/cookbook/types.d.ts +204 -0
  59. package/dist/components/cookbook/types.d.ts.map +1 -0
  60. package/dist/components/cookbook/types.js +1 -0
  61. package/dist/components/notifications/LocalizedNotifications.fixture.svelte +16 -0
  62. package/dist/components/notifications/LocalizedNotifications.fixture.svelte.d.ts +11 -0
  63. package/dist/components/notifications/LocalizedNotifications.fixture.svelte.d.ts.map +1 -0
  64. package/dist/components/notifications/NotificationBell.svelte +136 -0
  65. package/dist/components/notifications/NotificationBell.svelte.d.ts +13 -0
  66. package/dist/components/notifications/NotificationBell.svelte.d.ts.map +1 -0
  67. package/dist/components/notifications/index.d.ts +3 -0
  68. package/dist/components/notifications/index.d.ts.map +1 -0
  69. package/dist/components/notifications/index.js +1 -0
  70. package/dist/components/notifications/types.d.ts +27 -0
  71. package/dist/components/notifications/types.d.ts.map +1 -0
  72. package/dist/components/notifications/types.js +1 -0
  73. package/dist/components/overview/OverviewAssistantUndo.svelte +77 -0
  74. package/dist/components/overview/OverviewAssistantUndo.svelte.d.ts +13 -0
  75. package/dist/components/overview/OverviewAssistantUndo.svelte.d.ts.map +1 -0
  76. package/dist/components/overview/OverviewGrid.svelte +5 -2
  77. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
  78. package/dist/components/overview/assistant-surface.d.ts +90 -0
  79. package/dist/components/overview/assistant-surface.d.ts.map +1 -0
  80. package/dist/components/overview/assistant-surface.js +65 -0
  81. package/dist/components/overview/assistant.svelte.d.ts +115 -0
  82. package/dist/components/overview/assistant.svelte.d.ts.map +1 -0
  83. package/dist/components/overview/assistant.svelte.js +260 -0
  84. package/dist/components/overview/controller.svelte.d.ts +8 -0
  85. package/dist/components/overview/controller.svelte.d.ts.map +1 -1
  86. package/dist/components/overview/controller.svelte.js +76 -13
  87. package/dist/components/overview/grid-types.d.ts +6 -2
  88. package/dist/components/overview/grid-types.d.ts.map +1 -1
  89. package/dist/components/overview/index.d.ts +2 -0
  90. package/dist/components/overview/index.d.ts.map +1 -1
  91. package/dist/components/overview/index.js +2 -0
  92. package/dist/components/overview/operations.d.ts +169 -0
  93. package/dist/components/overview/operations.d.ts.map +1 -0
  94. package/dist/components/overview/operations.js +358 -0
  95. package/dist/components/overview/server.d.ts +2 -0
  96. package/dist/components/overview/server.d.ts.map +1 -1
  97. package/dist/components/overview/server.js +2 -0
  98. package/dist/components/workspace/admin-shell/settings-delta.d.ts +21 -0
  99. package/dist/components/workspace/admin-shell/settings-delta.d.ts.map +1 -0
  100. package/dist/components/workspace/admin-shell/settings-delta.js +142 -0
  101. package/dist/components/workspace/index.d.ts +1 -0
  102. package/dist/components/workspace/index.d.ts.map +1 -1
  103. package/dist/components/workspace/index.js +1 -0
  104. package/dist/components/workspace/server/index.d.ts +6 -2
  105. package/dist/components/workspace/server/index.d.ts.map +1 -1
  106. package/dist/components/workspace/server/index.js +5 -2
  107. package/dist/i18n/server.d.ts +1 -0
  108. package/dist/i18n/server.d.ts.map +1 -1
  109. package/dist/i18n/server.js +1 -0
  110. package/dist/i18n/strings.notifications.d.ts +12 -0
  111. package/dist/i18n/strings.notifications.d.ts.map +1 -0
  112. package/dist/i18n/strings.notifications.js +13 -0
  113. package/dist/i18n/strings.overview.d.ts +9 -0
  114. package/dist/i18n/strings.overview.d.ts.map +1 -1
  115. package/dist/i18n/strings.overview.js +11 -0
  116. package/dist/index.d.ts +2 -0
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +2 -0
  119. package/package.json +31 -14
package/AGENTS.md CHANGED
@@ -19,7 +19,8 @@ subpath you are editing. This file keeps what holds in every module.
19
19
  | `src/components/app/` (`./app`) | `AppShell` (Provider + ThemeProvider + theme CSS + AdminShell + nav/dock slots), `OwnerSetupForm` (props-driven first-run owner form), `ShellSettingsPage`, `RuntimeDiagnosticsWebMcp`. Browser-only; no `$app/*` or server imports | [agents/workspace.md](agents/workspace.md#app-shell-and-owner-setup-app) |
20
20
  | `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) |
21
21
  | `src/components/command-palette/` (`./command-palette`) | `CommandPalette` trigger + dialog, `PaletteProvider` contract, nav and manifest providers (#3713) | [agents/command-palette.md](agents/command-palette.md) |
22
- | `src/components/overview/` (`./overview` + `./overview/server`) | customizable overview surfaces: `OverviewGrid` flow grid (span 1-4, keyboard reorder/resize) joined to the shell's layout edit mode, host-owned `createOverview` controller, widget registry with versioned option schemas validated on save and load, the server `load(options, ctx)` contract (`loadOverview`), the metric/chart/records/shortcuts/note core widgets, and `registerRecipeWidgets` (recipe `widget` surfaces to registry registrations, injected module resolver) (#3727) | [agents/overview-surfaces.md](agents/overview-surfaces.md) |
22
+ | `src/components/cookbook/` (`./cookbook` + `./cookbook/server`) | cookbook-driven app shell: pure `buildCookbookShell` (cookbook + recipe catalog to `AppShell` sections, entries, layout, theme, section overviews, recipe surfaces), `CookbookApp` / `CookbookSectionPage`, and the server `loadCookbookApp` (file + installed manifests + validation); ported from smrt-planner (#3749) | [agents/cookbook-shell.md](agents/cookbook-shell.md) |
23
+ | `src/components/overview/` (`./overview` + `./overview/server`) | customizable overview surfaces: `OverviewGrid` flow grid (span 1-4, keyboard reorder/resize) joined to the shell's layout edit mode, host-owned `createOverview` controller, widget registry with versioned option schemas validated on save and load, the server `load(options, ctx)` contract (`loadOverview`), the metric/chart/records/shortcuts/note core widgets, and `registerRecipeWidgets` (recipe `widget` surfaces to registry registrations, injected module resolver) (#3727); production persistence is `@happyvertical/smrt-preferences` (kind `overview`) | [agents/overview-surfaces.md](agents/overview-surfaces.md) |
23
24
  | `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) | — |
24
25
  | `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | Registers an existing non-`DataTable` list (mirroring a `DataTableController`) as a mounted `DataSurfaceDescriptor`; a same-behavior port of `registerContentListDataSurface`, not yet unified behind smrt-ui (#2917). The module doc comment is the authoritative list of divergences (#2906) | — |
25
26
  | `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) | — |
@@ -0,0 +1,176 @@
1
+ # smrt-svelte/cookbook
2
+
3
+ Module semantics for `src/components/cookbook/` (`./cookbook` and
4
+ `./cookbook/server`, #3749). Package orientation and cross-module invariants are
5
+ in [../AGENTS.md](../AGENTS.md); the document it reads is the cookbook v1
6
+ contract ([core cookbook.md](../../core/agents/cookbook.md)).
7
+
8
+ A **cookbook-driven app shell**: given `smrt.cookbook.json` and the installed
9
+ packages' recipes, render the navigation sections, recipe entries, layout, theme
10
+ and section overview pages that smrt-planner showed, so a generated app and the
11
+ planner preview draw the same shell. Logic ported from smrt-planner
12
+ (`+layout.svelte`, `recipes/sections.ts`, `sections/*`, `overviews/*`,
13
+ `theme/*`); the planner-only editor, option/help pages, package browser and
14
+ sample data source are not part of it.
15
+
16
+ ## Entries
17
+
18
+ | Entry | Contents |
19
+ |---|---|
20
+ | `./cookbook/server` | Svelte-free and Node-safe. `loadCookbookApp` (file + manifests + validation), `readInstalledManifests`, `hasCookbook`, and the pure builder/catalog/theme functions and types. Use it in `+layout.server.ts`. |
21
+ | `./cookbook` | Everything above that is browser-safe, plus `CookbookApp`, `CookbookSectionPage`, `applyCookbookTheme`, `createCookbookRegistry`, `createSectionOverview`. |
22
+
23
+ ## Pure core (no DOM, Node-testable)
24
+
25
+ - **`catalogFromManifests(manifests)`** turns package `manifest.json` objects
26
+ into a `CookbookCatalog` (`recipes` trimmed to what the shell reads, `models`
27
+ with package id, description and `exposed`). Recipe order is manifest order;
28
+ a repeated id keeps the first. `narrowCatalog(catalog, cookbook)` keeps only
29
+ the selected recipes and the models they or the cookbook's `features` name, so
30
+ a load serializes a few KB instead of the whole workspace.
31
+ - **`buildCookbookShell({ cookbook, catalog, ... })`** returns a `CookbookShell`:
32
+ `navGroups` for `AppShell` (`navMode: 'sections'`, one group per suggested
33
+ section, icon and section-page `href`), `layout` (the cookbook's, passed to
34
+ the shell, which applies order/hidden/labels itself), `sections` and `entries`
35
+ (every entry by stable id), `theme`, `overviews`, recipe surfaces
36
+ (`shellWidgets`, `routes`, `settingsPanels`, `widgets`), `policies`, and
37
+ `issues`. It never throws for cookbook content: an unknown recipe, a nav model
38
+ the catalog lacks or a feature that is not exposed becomes an `issues` entry
39
+ and is left out.
40
+ - **Ids are the contract.** Section layout ids are `section:<id>` (a recipe's
41
+ `section`, else its `group`, else the recipe) or `custom:<slug>` (the layout's
42
+ own); entries are `item:<package>:<Model>[:<key>]`. They never name labels or
43
+ hrefs, so renames and route changes keep a saved layout valid. Do not change
44
+ `navItemId`/`buildNavSections` without the planner: both read the same
45
+ cookbooks.
46
+ - **Section order** follows the first recipe of each section in **catalog
47
+ order** (not the cookbook's sorted list), then the cookbook layout's
48
+ `sectionOrder`. Pass `recipeOrder` to pin it (the planner's overlay order).
49
+ - **Entries**: label, icon (`fileText` when the recipe names none), description
50
+ (recipe text, else the model description, else a plain fallback), `noun` and
51
+ `filter` carried through. Feature models share the `More` section after the
52
+ recipes' sections.
53
+ - **Paths** are options: `entryPath` (default `/m/<package>/<Model>/[<key>/]`),
54
+ `sectionPath` (default `/s/<slug>/`, slug = layout id with the first `:`
55
+ turned into `-`), and `href` (a prefix or query helper applied to both).
56
+ - **Overviews**: one definition per suggested section and per layout custom
57
+ section (`defineOverview`), id = the section's layout id, so a cookbook's
58
+ `overviews` key survives a section rename. Defaults are a `shortcuts` widget;
59
+ with `dataWidgets: true` they add a `metric` count and `records` list of the
60
+ section's lead model (first nav model of its first recipe), which need the
61
+ host's loaders. The cookbook's stored override is carried untouched
62
+ (untrusted JSON, `null` when absent or empty) for `resolveOverview` /
63
+ `createOverview` to validate; an override for an unknown page, or a malformed
64
+ one, is an `issues` entry.
65
+ - **Theme**: `resolveCookbookTheme` is lenient (a bad colour or font is ignored;
66
+ `validateCookbook` is where it is rejected). A brand colour becomes the preset
67
+ id `cookbook-brand-<hex>[-<font>]`; the colour/font pair is encoded in the id
68
+ because `ThemeProvider` looks a registered theme up once per id.
69
+ - **Recipe surfaces**: `shell-widget` surfaces are returned with their slot (a
70
+ smrt-svelte `ShellSlot`; the equality with `RecipeShellSlot` is a typechecked
71
+ assertion), `route` and `settings-panel` as data, `widget` surfaces grouped by
72
+ recipe for `registerRecipeWidgets`. The builder never imports a component.
73
+
74
+ ## `CookbookApp`
75
+
76
+ `AppShell` with the builder's output. Props: `cookbook`, `catalog`, any
77
+ `AppShell` prop (title, logo, `storageKey`, `user`, `permissions`, `dock`,
78
+ `slotItems`, `settingsHref`, ...), and:
79
+
80
+ - `loaders` (`metric` / `records` / `chart` data loaders; with `metric` and
81
+ `records` the section pages add the count and latest records),
82
+ - `widgetContext` (host capabilities for browser-side widget loads),
83
+ - `resolveExport` (a static map of dynamic imports; turns a recipe export
84
+ reference into a component, used for `shell-widget` slot items and recipe
85
+ `widget` registration; without it those surfaces are ignored),
86
+ - `oncookbookchange(cookbook)` called with the full cookbook after a layout or
87
+ overview edit (layout edits come from the shell's pencil toggle, enabled with
88
+ `layoutEditing`). Without it edits live in memory. Persisting them is the
89
+ host's job; feeding the result back as `cookbook` is safe.
90
+
91
+ Layout and overview edits are kept in local `$state.raw` copies that follow the
92
+ `cookbook` prop whenever it changes. The widget registry is rebuilt only when
93
+ the recipes' widget surfaces change by content, not on every edit.
94
+
95
+ Theme: `applyCookbookTheme` registers a brand colour with smrt-ui
96
+ (`createThemeFromColor` + `registerTheme`) and returns the preset for
97
+ `ThemeProvider`; an unknown preset name falls back to `smrt`.
98
+
99
+ **SSR**: the shell, nav, title and theme render on the server. The widget
100
+ registry, recipe widgets and slot items are resolved in effects (a bundled
101
+ `resolveExport` is async), so a section page's overview appears after hydration,
102
+ not in the server HTML.
103
+
104
+ ## `CookbookSectionPage`
105
+
106
+ A section's own page: title, icon and description (the user's renames win over
107
+ the suggestion), the overview grid, and, in the shell's layout edit mode, the
108
+ section's entries as a sortable menu. A section without an overview (a custom id
109
+ outside the overview id pattern) shows its menu as cards. Mount it from one
110
+ route: `s/[slug]/+page.svelte` renders
111
+ `<CookbookSectionPage slug={page.params.slug} />`. It must be inside
112
+ `CookbookApp`.
113
+
114
+ ## Using it in an app
115
+
116
+ ```ts
117
+ // +layout.server.ts
118
+ import { loadCookbookApp } from '@happyvertical/smrt-svelte/cookbook/server';
119
+ export const load = async () => loadCookbookApp(); // { cookbook, catalog, warnings }
120
+ ```
121
+
122
+ ```svelte
123
+ <!-- +layout.svelte -->
124
+ <script lang="ts">
125
+ import { CookbookApp } from '@happyvertical/smrt-svelte/cookbook';
126
+ let { data, children } = $props();
127
+ </script>
128
+ <CookbookApp cookbook={data.cookbook} catalog={data.catalog} layoutEditing>
129
+ {@render children()}
130
+ </CookbookApp>
131
+ ```
132
+
133
+ `loadCookbookApp` reads `smrt.cookbook.json`, reads the `manifest.json` of the
134
+ project's dependencies that export one, validates with `validateCookbook` from
135
+ `@happyvertical/smrt-core/cookbook` (resolved at call time: the app depends on
136
+ core; smrt-svelte does not, `validate` can be injected), and throws one error
137
+ listing every problem, including a recipe no installed package declares.
138
+
139
+ ## Not done here
140
+
141
+ - **Field policies** (`cookbook.policies`) are returned as `policies` but not
142
+ applied; applying them to the runtime is `smrt cookbook apply` and the
143
+ runtime's job. The shell does not render a settings page; pass `settingsHref`.
144
+ - **Entry pages** (`/m/<package>/<Model>/`) are the app's routes; `./screens`
145
+ (`RecipeScreens`) is the default content for them. Their data comes from the
146
+ package model's generated REST route and its `@smrt/web` definition (#3749).
147
+ The app does nothing beyond listing the package in `smrt.config.ts`
148
+ `consumer.packages`: the `smrt()` Vite preset hosts the models of the
149
+ cookbook's recipes and `features` (core
150
+ [build-knowledge.md](../../core/agents/build-knowledge.md), "Package models
151
+ over REST"), skipping `api: false` models and any the cookbook's `exposure`
152
+ turns off for `api`. In the entry route, take
153
+ `collectionDefinitions[<collection>]` from `@smrt/web` (not
154
+ `@happyvertical/smrt-virt-web`, which holds only the app's own models) as the
155
+ `definition`, and build the `RecipeScreensSource` over
156
+ `GET /api/<collection>` and `/api/<collection>/<id>` (offer writes only when
157
+ the definition's `actions` include them). A model with no definition is not
158
+ exposed over the API; show the notice.
159
+ - **Options/Help gear** on a recipe's first entry (the planner's `action`) is
160
+ planner-only and not emitted.
161
+ - **Legacy id migrations** (`migrateLegacySections`/`NavItemIds`) stay in the
162
+ planner; they depend on its recipe catalog.
163
+
164
+ ## Tests
165
+
166
+ `src/components/cookbook/__tests__/`: `build.test.ts` builds the four fixture
167
+ cookbooks in `packages/core/src/cookbook/__fixtures__/` against the real
168
+ workspace recipes (every package that declares a `SmrtRecipe`, scanned and
169
+ converted to manifests the way the build does, in `workspace-catalog.ts`, so no
170
+ sibling `dist/` is needed), asserts no issues, that every layout id resolves,
171
+ and the exact bakery sections and labels. `contract-types.ts` (not a `*.test.ts`:
172
+ tsc excludes test files) holds the type assertions that smrt-types'
173
+ `CookbookLayout`/`CookbookOverviewOverride` equal `ShellLayout`/
174
+ `OverviewOverride`. `CookbookApp.test.ts` mounts the shell with the bakery
175
+ cookbook (nav, section page, brand theme, edit callback); `server.test.ts` runs
176
+ the loader over a temp project with a fake installed package.
@@ -0,0 +1,8 @@
1
+ # SMRT #3759 test design
2
+ | Behavior | Trigger | Positive | Negative/default | Actor/context | Executor | Runtime | Edge | Level | Command |
3
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
4
+ | Accessible host heading | heading snippet | linked h3 names article | omitted snippet retains title | browser reader | N/A presentation | Svelte5/browser+SSR | optional snippet | component+a11y | pnpm --filter @happyvertical/smrt-svelte exec vitest run src/components/overview/__tests__/OverviewGrid.test.ts |
5
+ | Plain view, editable frame | presentation plain/editing toggle | plain attribute, toolbar and inert edit preview | default card unchanged | authorized editor | N/A presentation | Svelte5/browser | optional enum typed | component | same |
6
+ No persistence/auth/executor changes. Existing grid suite covers denied editor, widget failures, keyboard edits. New behavior is additive, no bug-fix base comparison required.
7
+
8
+ Validation: OverviewGrid component suite passed on this change (includes linked article headings, a11y, edit chrome and existing default regressions). Package typecheck and aggregate release checks are recorded in release evidence.
@@ -11,15 +11,16 @@ its default arrangement; admins (or users, per role) add, reorder, resize,
11
11
  configure and remove widgets inside that confined set, and reset returns to the
12
12
  default. Phase 1 (this module) is the surface, the registry, the data model, the
13
13
  server load contract and five core widgets; phase 2 adds package widgets from
14
- recipes ("Recipe widgets"). Persistence and assistant operations are later
15
- phases (see "Extension points").
14
+ recipes ("Recipe widgets"); phase 3 is production persistence in
15
+ `@happyvertical/smrt-preferences` ("Production persistence"); phase 4 adds
16
+ assistant operations with Undo ("Assistant operations").
16
17
 
17
18
  ## Two entries
18
19
 
19
20
  | Entry | Contents |
20
21
  |---|---|
21
- | `./overview/server` (`server.ts`) | Svelte-free: types, `validateWidgetOptions`, the model (`resolveOverview`, `checkOverviewOverride`, `applyOverviewOverride`, `diffOverview`, `sanitizeOverview`, ...), `WidgetRegistry` / `registerWidget`, `loadOverview`. Safe in a `+page.server.ts` or a save endpoint. |
22
- | `./overview` (`index.ts`) | Everything above plus `OverviewGrid`, `createOverview` (controller), `WidgetOptionsForm`, the core widgets and `registerCoreWidgets`, `shortcutsFromNav`, `formatWidgetValue`, `parseMarkdown` / `safeHref`. |
22
+ | `./overview/server` (`server.ts`) | Svelte-free: types, `validateWidgetOptions`, the model (`resolveOverview`, `checkOverviewOverride`, `applyOverviewOverride`, `diffOverview`, `sanitizeOverview`, ...), `WidgetRegistry` / `registerWidget`, `loadOverview`, the operations (`planOverviewOperations`, `describeOverview`) and `createOverviewAssistantSurface`. Safe in a `+page.server.ts` or a save endpoint. |
23
+ | `./overview` (`index.ts`) | Everything above plus `OverviewGrid`, `createOverview` (controller), `WidgetOptionsForm`, the core widgets and `registerCoreWidgets`, `shortcutsFromNav`, `formatWidgetValue`, `parseMarkdown` / `safeHref`, `createOverviewAssistant` / `useOverviewAssistantTools` / `OverviewAssistantUndo`. |
23
24
 
24
25
  ## Data model (`types.ts`, `model.ts`)
25
26
 
@@ -50,7 +51,8 @@ against (absent means 1).
50
51
  (they would stop incrementing), and an exhausted high end restarts at `w1`.
51
52
  - **Tiers.** `resolveOverview` merges one override. A host with tenant defaults
52
53
  and user overrides merges the tenant tier into the definition's `defaults`
53
- first (phase 3); the user override then applies to that.
54
+ first; the user override then applies to that (smrt-preferences'
55
+ `withTenantDefaults`).
54
56
  - **Reset** is `controller.reset()` (store `null`) or `resetWidget(id)`.
55
57
 
56
58
  ## Validation: what keeps a stored overview from running anything
@@ -136,7 +138,11 @@ return { overview }; // LoadedOverview: JSON, serializable
136
138
  loaded?, loadWidget? })`, the `ShellLayout` pattern: **persistence is host-owned**.
137
139
  With an `override` getter the host is the source of truth and feeds each edit back
138
140
  (`onchange(override | null)`); without it the value is kept in memory. Reactive
139
- reads: `document`, `base`, `override`, `issues`, `customized`, `canCustomize`,
141
+ reads: `document`, `base`, `override`, `committedOverride` (with a host-owned
142
+ getter: the newest value handed to `onchange` that the getter has not fed back
143
+ yet, else `override`; emissions are acknowledged in order, so several saves in
144
+ flight never regress it, and a getter value matching none of them and not the
145
+ value before them is a change from elsewhere that drops the queue), `issues`, `customized`, `canCustomize`,
140
146
  `addable`, `full`, `entry(widget)`. Operations, each returning
141
147
  `{ ok: true, id } | { ok: false, reason, issues? }` and refused with
142
148
  `not_allowed` when `canCustomize()` is false (the role gate; the server must check
@@ -239,17 +245,127 @@ turns the manifest's widget surfaces into `registry.register(...)` calls:
239
245
  and a type-level assertion here keeps `RecipeWidgetOptionType` equal to
240
246
  `WidgetOptionType`.
241
247
 
248
+ ## Production persistence (phase 3, `@happyvertical/smrt-preferences`)
249
+
250
+ Overrides are stored as the `overview` kind of the generic user-interface
251
+ preference table `_smrt_ui_preferences` in
252
+ [smrt-preferences](../../preferences/AGENTS.md) (one row per tenant, kind,
253
+ surface id = `OverviewDefinition.id`, and tier; the kind contract is in its
254
+ [preference-kinds.md](../../preferences/agents/preference-kinds.md)). This
255
+ package stays free of database code: its overview entries import nothing from
256
+ core, tenancy or users. The `overview` kind validates with this package's
257
+ `checkOverviewOverride` on save (only the canonical override is stored) and
258
+ `resolveOverview` on load (bad entries dropped and reported per tier).
259
+ `createOverviewStore({ db })` is the overview-shaped layer over the generic
260
+ store and acts as the ambient principal:
261
+
262
+ | Call | Does |
263
+ | --- | --- |
264
+ | `load(definition, registry)` | Both tiers resolved page defaults < tenant < user; each tier's `override`, `revision`, `issues`, the merged `document` and `canCustomize: { tenant, user }` |
265
+ | `save(definition, registry, { scope, override, revision })` | Strict validation against the tier's definition (user: tenant-merged); `invalid` / `conflict` / `not_allowed` otherwise |
266
+ | `reset(definition, { scope, revision? })` | Deletes that tier's row |
267
+ | `loadPage(definition, registry, ctx, { scope? })` | `load` plus `loadOverview` of the tier's document, for `+page.server.ts` |
268
+ | `loadWidget(definition, registry, widget, ctx)` | One widget's data for the controller's `loadWidget` remote function (throw on `ok: false`) |
269
+
270
+ Host wiring for the personal layer: `createOverview({ definition:
271
+ withTenantDefaults(definition, data.overview.tenant.document), override: () =>
272
+ data.overview.user?.override ?? null, onchange: (next) => save({ scope:
273
+ 'user', override: next, revision }), canCustomize: () =>
274
+ data.overview.canCustomize.user, loaded: data.overview.loaded, loadWidget })`;
275
+ the organization layer uses the page definition, `tenant.override` and
276
+ `canCustomize.tenant`. Keep the returned `revision` for the next save; on
277
+ `conflict` reload the page data. Permissions are `overviews.customize` (tenant
278
+ default) and `overviews.personalize` (own layout).
279
+
280
+ Known model limit: a tenant default that later adds a widget whose id equals
281
+ an id a user override already added hides the user's widget (an added widget
282
+ colliding with a base id is skipped). `nextWidgetId` avoids it within one tier
283
+ only.
284
+
285
+ ## Assistant operations (`operations.ts`, `assistant-surface.ts`, `assistant.svelte.ts`)
286
+
287
+ Phase 4: the assistant adds and configures widgets with structured operations,
288
+ and the person can undo the change.
289
+
290
+ - **Operations** are data: `add { type, span?, options?, index? }`,
291
+ `configure { id, options }` (merged onto the current options; `null` clears a
292
+ key to its default), `move { id, index }`, `resize { id, span }`,
293
+ `remove { id }`. `OVERVIEW_OPERATION_SCHEMA` is their JSON schema.
294
+ - **`planOverviewOperations({ definition, registry, override, operations })`**
295
+ plans a batch (at most 20) against the current override. Strict: unknown ops
296
+ or fields, unregistered or disallowed types (`allowed`, `allowedIn`), invalid
297
+ options (including a `model` outside `definition.models`), spans outside the
298
+ type's range, unknown widget ids and the cap are `OverviewOperationIssue`s
299
+ (`index`, `op`, `widgetId`, `code`, `message`, `options`). The result is then
300
+ run through `checkOverviewOverride`, so it is always an override a save
301
+ accepts. **Atomic**: any issue rejects the batch and returns nothing to
302
+ apply. Later operations see earlier ones; an `add` takes the next free id
303
+ (`nextWidgetId`, never a removed default's). Total, never throws.
304
+ - **`describeOverview(...)`** is what a model reads first: allowed types with
305
+ option fields (type, required, default, range, `choices`, the page
306
+ `models`), span ranges, the current widgets with their index and title.
307
+ `translate` resolves i18n-key titles and labels.
308
+ - **Server: `createOverviewAssistantSurface({ definition, registry, override,
309
+ revision, canCustomize, persist })`** is one page for one principal:
310
+ `describe`, `current`, `plan`, `check` and `persist`. Writes are
311
+ conditional: `persist(override, { revision })` must write only if the
312
+ stored row is still the loaded `revision` and answer
313
+ `{ ok: false, reason: 'conflict' }` otherwise
314
+ (`OverviewAssistantPersistResult`, structurally `OverviewSaveResult` of
315
+ `@happyvertical/smrt-preferences`); on success the surface follows the
316
+ returned `revision`. With phase 3:
317
+ `persist: (override, { revision }) => store.save(definition, registry, { scope: 'user', override, revision })`
318
+ where `store = createOverviewStore({ db })` from `@happyvertical/smrt-preferences`,
319
+ `override` / `revision` come from `store.load(...)`'s `state.user`, and the
320
+ surface's `definition` is `withTenantDefaults(definition, state.tenant.document)`. It is the adapter
321
+ `@happyvertical/smrt-chat`'s `createOverviewTools()` drives (typed
322
+ structurally there; see
323
+ [chat overview-tools.md](../../chat/agents/overview-tools.md) for the tool
324
+ contract, error codes and principal/page-scoped Undo). Phase 3 supplies the
325
+ stored override, the role and `persist`.
326
+ - **Browser: `createOverviewAssistant(controller, { toolPrefix?, toaster?,
327
+ translate? })`** applies a batch through `controller.restore(plan.override)`,
328
+ so the grid updates live and the host's `onchange` persists it like any edit.
329
+ It refuses with `not_allowed` when `canCustomize` is false. `lastBatch`
330
+ (reactive) keeps `{ id, results, counts, before, after }`; `undo(id?)`
331
+ restores `before` exactly, once, and refuses with `changed_since` when the
332
+ override is no longer `after` (the person edited since). Both read
333
+ `controller.committedOverride`, so in host-owned mode a batch whose save the
334
+ host has not fed back yet is not mistaken for a change made elsewhere.
335
+ `useOverviewAssistantTools(assistant)` registers `<prefix>_describe` (read),
336
+ `<prefix>_apply` and `<prefix>_undo` (write) through `useWebMcpTool`, so the
337
+ AssistantDock offers them as page tools: the Provider's `webmcp.effects` must
338
+ allow `write`, the server's `clientToolAllowList` must list `overview_*`, and
339
+ the dock asks the person to Allow each write unless the host policy says
340
+ otherwise. Results are JSON strings (`{ ok: false, error, issues }` on a
341
+ rejected batch).
342
+ - **Undo affordance.** `OverviewAssistantUndo` (`assistant`, optional
343
+ `message`) shows the last batch with Undo and Dismiss buttons and announces
344
+ the outcome in its own polite live region; or pass a smrt-ui `toaster` and
345
+ each batch shows as a toast with an Undo action (the host mounts
346
+ `ToastViewport`). Strings are `ui.overview.assistant_*`.
347
+ - Tests: `__tests__/operations.test.ts`, `__tests__/assistant.test.ts` (live
348
+ grid update, Undo, the component with axe) and
349
+ `__tests__/assistant-tools.integration.test.ts` (chat's tools over this
350
+ model).
351
+
242
352
  ## Extension points left for later phases
243
353
 
244
- - **Phase 2, package widgets (#3727, done).** See "Recipe widgets" below.
245
- - **Phase 3, production persistence.** Implement the `override` getter /
246
- `onchange` pair against the `_smrt_` table (tenant tier merged into
247
- `definition.defaults`, user tier as the override), validate saves with
248
- `checkOverviewOverride` and persist its canonical `override`, run
249
- `loadOverview` in the page load, and supply `loadWidget` as a remote function
250
- that calls the same `load`. Role gating = `canCustomize` plus the same check in
251
- the save endpoint. No migration or table lives in this package.
252
- - **Phase 4, assistant.** The controller methods are the structured operations;
253
- `restore(override)` plus the `override` getter give Undo (snapshot before,
254
- restore after). `OverviewOpResult.issues` is the validation feedback to return.
354
+ - **Phase 2, package widgets (#3727, done).** See "Recipe widgets" above.
355
+ - **Phase 3, production persistence (#3727, done).** See "Production
356
+ persistence" above.
357
+ - **Phase 4, assistant (#3727, done).** See "Assistant operations" above;
358
+ `createOverviewAssistantSurface` is wired to the stored user tier through
359
+ `createOverviewStore(...).save` as shown there.
255
360
  - **Not built:** the activity widget (Track B, #3710) and agenda.
361
+
362
+ ## Host section presentation
363
+
364
+ `OverviewGrid` defaults to framed cards. Pass `presentation="plain"` for
365
+ unframed page sections; editing restores the visible tile frame and controls.
366
+ The optional `heading(widget, title)` snippet supplies heading content, such as
367
+ an application-owned link. The grid still renders its `headingLevel` element
368
+ and generated id used by the article's accessible name; the snippet must not
369
+ supply another heading. Use trusted application routes for links. Widget bodies
370
+ should omit duplicate section headings. Default titles remain unchanged when
371
+ no snippet is supplied.
package/agents/screens.md CHANGED
@@ -55,3 +55,8 @@ stays below the domain layer, and the app passes both in as props.
55
55
  default and does not replace it. Not wired into recipe surfaces yet (#3708).
56
56
  - Component props live in `types.ts`, not in the `.svelte` files, so the barrel
57
57
  never re-exports types from a component.
58
+ - **Where a package model's definition and data come from (#3749).** A cookbook
59
+ app's `smrt()` preset hosts the cookbook's package models; the definition is
60
+ `collectionDefinitions[<collection>]` from `@smrt/web` and the source is the
61
+ model's `/api/<collection>` route (see [cookbook-shell.md](cookbook-shell.md)).
62
+ A model the API does not expose has neither, so the entry page shows its notice.
@@ -32,6 +32,12 @@ them from the published artifact versus exporting them is tracked in #2286.
32
32
  - App-owned configuration for hidden edges, push/overlay presentation, and
33
33
  exclusivity groups
34
34
  - User-owned preferences persist as sparse `ShellSettingsDelta` values
35
+ through a `ShellSettingsAdapter` (`LocalStorageShellSettingsAdapter` by
36
+ default). Server-side, per tenant and user, they can persist as the
37
+ `shell-layout` kind of `@happyvertical/smrt-preferences`
38
+ (`createShellSettingsPreferences`, the server half of an adapter); it
39
+ validates with `sanitizeShellSettingsDelta`, the untrusted-input reader on
40
+ the Node-safe `./workspace/server` entry (with `mergeShellSettingsDelta`)
35
41
  - Focus tools may register imperatively through `ShellState` or declaratively
36
42
  through Svelte helpers
37
43
  - Shell activities are client-side records; server jobs, polling, WebSockets,
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import WorkspaceActivityList from '../workspace/admin-shell/ActivityList.svelte';
3
+ import { tryUseAdminShell } from '../workspace/admin-shell/context.js';
4
+ import { safeActivityHref } from './safe-href.js';
5
+ import type { ActivityListEntry } from './types.js';
6
+
7
+ interface Props {
8
+ /** Already-authorized activity rows to display. */
9
+ entries: ActivityListEntry[];
10
+ /** Message displayed by the shell list when no activities match. */
11
+ emptyLabel?: string;
12
+ }
13
+ let { entries, emptyLabel = 'No activity.' }: Props = $props();
14
+ const shell = tryUseAdminShell();
15
+ const prefix = `audit-feed:${crypto.randomUUID()}:`;
16
+ $effect(() => {
17
+ if (!shell) return;
18
+ const ids = new Set(entries.map((entry) => `${prefix}${entry.id}`));
19
+ for (const entry of entries)
20
+ shell.upsertActivity({
21
+ id: `${prefix}${entry.id}`,
22
+ label: entry.title,
23
+ message: entry.detail,
24
+ detailHref: safeActivityHref(entry.href),
25
+ kind: prefix,
26
+ scope: 'system',
27
+ status: 'completed',
28
+ createdAt: entry.occurredAt,
29
+ });
30
+ return () => {
31
+ for (const id of ids) shell.removeActivity(id);
32
+ };
33
+ });
34
+ </script>
35
+ {#if shell}<WorkspaceActivityList filter={{ kind: prefix }} {emptyLabel} />{:else}<p>{emptyLabel}</p>{/if}
@@ -0,0 +1,11 @@
1
+ import type { ActivityListEntry } from './types.js';
2
+ interface Props {
3
+ /** Already-authorized activity rows to display. */
4
+ entries: ActivityListEntry[];
5
+ /** Message displayed by the shell list when no activities match. */
6
+ emptyLabel?: string;
7
+ }
8
+ declare const ActivityList: import("svelte").Component<Props, {}, "">;
9
+ type ActivityList = ReturnType<typeof ActivityList>;
10
+ export default ActivityList;
11
+ //# sourceMappingURL=ActivityList.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivityList.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/activity/ActivityList.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,UAAU,KAAK;IACb,mDAAmD;IACnD,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAiCD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { default as ActivityList } from './ActivityList.svelte';
2
+ export type { ActivityListEntry } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/activity/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1 @@
1
+ export { default as ActivityList } from './ActivityList.svelte';
@@ -0,0 +1,3 @@
1
+ /** Accept ordinary web links, never executable schemes or protocol-relative URLs. */
2
+ export declare function safeActivityHref(value: string | null | undefined): string | undefined;
3
+ //# sourceMappingURL=safe-href.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-href.d.ts","sourceRoot":"","sources":["../../../src/components/activity/safe-href.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC/B,MAAM,GAAG,SAAS,CASpB"}
@@ -0,0 +1,12 @@
1
+ /** Accept ordinary web links, never executable schemes or protocol-relative URLs. */
2
+ export function safeActivityHref(value) {
3
+ if (!value || value.startsWith('//') || value.includes('\\'))
4
+ return undefined;
5
+ try {
6
+ const url = new URL(value, 'https://smrt.invalid');
7
+ return ['https:', 'http:'].includes(url.protocol) ? value : undefined;
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
@@ -0,0 +1,9 @@
1
+ /** Authorized activity projected by an owning domain package. */
2
+ export interface ActivityListEntry {
3
+ id: string;
4
+ title: string;
5
+ detail?: string;
6
+ occurredAt: string;
7
+ href?: string | null;
8
+ }
9
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/activity/types.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB"}
@@ -0,0 +1 @@
1
+ export {};