@terreno/mcp 57.12.0 → 57.13.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 (43) hide show
  1. package/dist/docs/upgrades/57.13.0.md +75 -0
  2. package/dist/docs/versioned/next/explanation/README.md +1 -1
  3. package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +69 -9
  4. package/dist/docs/versioned/next/explanation/example-coverage.md +3 -3
  5. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +12 -0
  6. package/dist/docs/versioned/next/how-to/README.md +2 -0
  7. package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +187 -3
  8. package/dist/docs/versioned/next/how-to/circleci.md +2 -0
  9. package/dist/docs/versioned/next/how-to/preview-demo.md +5 -5
  10. package/dist/docs/versioned/next/how-to/rate-limiting.md +17 -0
  11. package/dist/docs/versioned/next/how-to/test-ui-accessibility.md +35 -0
  12. package/dist/docs/versioned/next/implementationPlans/agent-ui-rich-replies.md +359 -0
  13. package/dist/docs/versioned/next/reference/README.md +1 -1
  14. package/dist/docs/versioned/next/reference/admin-backend.md +26 -0
  15. package/dist/docs/versioned/next/reference/admin-frontend.md +15 -0
  16. package/dist/docs/versioned/next/reference/ai.md +55 -3
  17. package/dist/docs/versioned/next/reference/api.md +4 -0
  18. package/dist/docs/versioned/next/reference/blocks.md +243 -11
  19. package/dist/docs/versioned/next/reference/environment-variables.md +3 -0
  20. package/dist/docs/versioned/next/reference/ui.md +212 -10
  21. package/dist/docs/versioned/next/tasks/agent-ui-rich-replies.md +188 -0
  22. package/package.json +3 -3
  23. package/src/docs/upgrades/57.13.0.md +75 -0
  24. package/src/docs/versioned/next/explanation/README.md +1 -1
  25. package/src/docs/versioned/next/explanation/agent-ui-blocks.md +69 -9
  26. package/src/docs/versioned/next/explanation/example-coverage.md +3 -3
  27. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +12 -0
  28. package/src/docs/versioned/next/how-to/README.md +2 -0
  29. package/src/docs/versioned/next/how-to/agent-ui-blocks.md +187 -3
  30. package/src/docs/versioned/next/how-to/circleci.md +2 -0
  31. package/src/docs/versioned/next/how-to/preview-demo.md +5 -5
  32. package/src/docs/versioned/next/how-to/rate-limiting.md +17 -0
  33. package/src/docs/versioned/next/how-to/test-ui-accessibility.md +35 -0
  34. package/src/docs/versioned/next/implementationPlans/agent-ui-rich-replies.md +359 -0
  35. package/src/docs/versioned/next/reference/README.md +1 -1
  36. package/src/docs/versioned/next/reference/admin-backend.md +26 -0
  37. package/src/docs/versioned/next/reference/admin-frontend.md +15 -0
  38. package/src/docs/versioned/next/reference/ai.md +55 -3
  39. package/src/docs/versioned/next/reference/api.md +4 -0
  40. package/src/docs/versioned/next/reference/blocks.md +243 -11
  41. package/src/docs/versioned/next/reference/environment-variables.md +3 -0
  42. package/src/docs/versioned/next/reference/ui.md +212 -10
  43. package/src/docs/versioned/next/tasks/agent-ui-rich-replies.md +188 -0
@@ -14,9 +14,17 @@ The same check is `validateBlocks` in `@terreno/blocks`. Field tables are in the
14
14
  ## Preview a document in the playground
15
15
 
16
16
  1. Open the component demo and choose **BlocksPlayground**.
17
- 2. Start from Layout or Invalid, or paste your own document into the text area.
17
+ 2. Start from a preset: **All blocks** (one of each block type), **Block type** (one preset per
18
+ block type, with every chart kind, callout status, and action kind), Layout, Invalid, or
19
+ Sunday roast. Or paste your own document into the text area.
18
20
  3. The preview updates as you type. An invalid document shows the error banner and keeps
19
21
  the raw text collapsed.
22
+ 4. Press the controls. The playground has no server: a stepper's − and + and a checklist tick
23
+ run on the device with `scaleStepperBlock` and `applyChecklistState` from `@terreno/blocks`
24
+ (the helpers `scaleStepperHostAction` and `toggleChecklistHostAction` use), and a toast names
25
+ the callback and its result, such as "scaleStepper → 6 People". A reply, open, select, or
26
+ other callback shows a toast describing the event instead. Copy writes the clipboard.
27
+ Editing the text drops the local results.
20
28
 
21
29
  ## Require documents from the model
22
30
 
@@ -72,6 +80,133 @@ addGptRoutes(router, {
72
80
 
73
81
  The path is `/gpt/actions`, so a limiter on `/gpt` covers it. Each call is stored as an `AIRequest` with `requestType: "ui_action"`.
74
82
 
83
+ ## Add a stepper callback
84
+
85
+ A `stepper` is a − value + control. Each tap goes to a host action, and the action returns the
86
+ replacement stepper. Use it when a number changes other numbers, such as guests changing a
87
+ shopping list.
88
+
89
+ 1. Register a stepper action. `scaleStepperHostAction` from `@terreno/ai` scales every item
90
+ linearly from the stepper the agent wrote:
91
+
92
+ ```ts
93
+ import {addGptRoutes, scaleStepperHostAction} from "@terreno/ai";
94
+
95
+ addGptRoutes(router, {
96
+ aiService,
97
+ uiBlocks: {hostActions: {scaleStepper: scaleStepperHostAction}},
98
+ });
99
+ ```
100
+
101
+ 2. Leave `uiBlocks.richBlocks` unset (it defaults to `true`). The prompt offers `stepper` only
102
+ when some host action has `handles: "stepper"`, and names those actions as its callbacks.
103
+ 3. List the same name in `GPTChat` `hostActions` (`["scaleStepper"]`). `onBlockCallback` posts
104
+ to `POST /gpt/actions` and returns the result, as in [Wire the example chat](#wire-the-example-chat).
105
+ 4. The agent writes a stepper whose `callback.name` is that action:
106
+
107
+ ```yaml
108
+ - type: stepper
109
+ id: guests
110
+ label: Number of people
111
+ unit: People
112
+ value: 5
113
+ min: 1
114
+ max: 20
115
+ callback: {name: scaleStepper}
116
+ items:
117
+ - {label: Bone-in leg of lamb, amount: 2, unit: kg, decimals: 1}
118
+ - {label: Carrots, amount: 8, round: up}
119
+ ```
120
+
121
+ 5. Press +. The client sends `payload: {...callback.payload, value: 6}`, with `blockId: guests`
122
+ and `elementId: guests_increase`. The action returns `{replace: "block", blocks}` and
123
+ `GPTChat` swaps the stepper: lamb reads "2.4 kg", carrots "10". Both buttons are disabled
124
+ until the reply lands, so a fast second tap is dropped.
125
+
126
+ A stepper whose `callback.name` is not a `handles: "stepper"` action fails `UNKNOWN_HOST_ACTION`.
127
+ `value` outside `min`–`max`, or off the `step` grid, returns 400.
128
+
129
+ **Stored prompts are owner-writable.** The history owner can change any stored prompt with
130
+ `PATCH /gpt/histories/:id`, including the stepper `scaleStepperHostAction` reads. When the numbers
131
+ matter (prices, stock, dosage), write your own action and compute them from your own data:
132
+
133
+ ```ts
134
+ import {findAgentBlock, type HostAction} from "@terreno/ai";
135
+ import {z} from "@terreno/api";
136
+
137
+ const priceGuests: HostAction = {
138
+ handles: "stepper",
139
+ logResponse: false, // keep the returned block out of the AIRequest log
140
+ payload: z.object({value: z.number().int().min(1).max(20)}).passthrough(),
141
+ handler: async ({blockId, history, messageId, payload, user}) => {
142
+ const stepper = findAgentBlock({blockId, history, messageId, type: "stepper"});
143
+ const {value} = payload as {value: number};
144
+ const items = await quoteItems({guests: value, userId: user._id}); // your data, not the stored block
145
+ return {replace: "block", blocks: {v: 1, blocks: [{...stepper, items, value}]}};
146
+ },
147
+ };
148
+ ```
149
+
150
+ `findAgentBlock` reads `history.prompts[n]` for `messageId: msg-<n>`, otherwise the only assistant
151
+ prompt holding that block: several matches throw 409, none 404. Field rules are in
152
+ [Stepper](../reference/blocks.md#stepper) and [Host actions](../reference/ai.md#host-actions).
153
+
154
+ ## Add a checklist callback
155
+
156
+ A `checklist` without a `callback` ticks on the device only. To send ticks to the server:
157
+
158
+ 1. Register `toggleChecklistHostAction` (or your own `handles: "checklist"` action) under a name,
159
+ such as `toggleChecklist`, and list it in `GPTChat` `hostActions`.
160
+ 2. The prompt then tells the model to set `callback: {name: toggleChecklist}` on a checklist.
161
+ 3. A tick sends `{...callback.payload, itemId, checked, state}`, where `state` holds every item's
162
+ tick after this one. The whole checklist is locked until the reply lands, and the "n of m"
163
+ counter changes when the returned checklist arrives.
164
+
165
+ `toggleChecklistHostAction` returns the stored checklist with `checked` from `state` and saves
166
+ nothing. An unknown item id returns 400. An app that records progress writes its own action.
167
+
168
+ ## Copy a block's text
169
+
170
+ Add a button with `action: {kind: copy, target: <block id>}` (or `text` for a literal). The
171
+ target is a `stepper`, `checklist`, `list`, `table`, or `text` block. `BlocksView` copies the
172
+ block as it is shown now, so a stepper at 6 copies the scaled amounts. Nothing reaches the
173
+ server, so `hostActions` does not apply. See [Copy](../reference/blocks.md#copy).
174
+
175
+ ## Keep the old catalog for shipped native builds
176
+
177
+ Rich blocks (`stepper`, `checklist`, `gallery`, `list`, `card.eyebrow`, `copy`) are in the prompt
178
+ by default. A native build released before them shows an error banner for a reply that uses
179
+ them. Until those builds update, turn them off:
180
+
181
+ ```ts
182
+ addGptRoutes(router, {aiService, uiBlocks: {hostActions, richBlocks: false}});
183
+ ```
184
+
185
+ The prompt is then the same as before rich blocks, even with a stepper action registered.
186
+ Validation still accepts the new blocks.
187
+
188
+ ## Smoke-test a real model
189
+
190
+ `bun run blocks:smoke` (from the repo root) sends "Give me a plan for a sunday lamb roast, I'm
191
+ having friends over still figuring out numbers tbh" through one real chat turn with the example
192
+ backend's `uiBlocks` options and the `findPhotos` tool.
193
+
194
+ | Check | Passes when |
195
+ | --- | --- |
196
+ | Document | The reply parses and passes `validateBlocks` |
197
+ | Blocks | It holds a `gallery`, `list`, `stepper`, and `checklist`, and a `copy` action |
198
+ | Photos | Every image `src` is a `file:` id that `findPhotos` returned in that turn |
199
+
200
+ | Variable | Use |
201
+ | --- | --- |
202
+ | `GOOGLE_VERTEX_PROJECT` (with Application Default Credentials) or `GEMINI_API_KEY` | The model. With neither, the script prints that it skipped and exits 0 |
203
+ | `BLOCKS_SMOKE_MODEL` | Optional model id instead of the server default |
204
+ | `MONGO_URI`, `MONGO_DB_NAME` | The database holding the photo library ([Give the agent photos](#give-the-agent-photos)) |
205
+ | `BLOCKS_SMOKE_OUT_DIR` or `--out <dir>` | Where `reply.yaml` and `result.json` go (default: a new temp directory) |
206
+
207
+ It exits 1 when any check fails. The turn runs as `blocks-smoke@system.invalid`, and its history
208
+ is deleted afterwards.
209
+
75
210
  ## Wire the example chat
76
211
 
77
212
  1. Turn `uiBlocks` on in `addGptRoutes` so `GET /gpt/datasets/:id` and `POST /gpt/actions` are in the OpenAPI spec.
@@ -84,11 +219,57 @@ The path is `/gpt/actions`, so a limiter on `/gpt` covers it. Each call is store
84
219
 
85
220
  ## Turn blocks on in the example backend
86
221
 
87
- `example-backend` passes `uiBlocks` with `repair: true` and one host callback, `exportDataset`. Its payload is `{dataset: string}`. The handler returns `{replace: "block", blocks}` with a badge that names that dataset. Dataset TTL stays at the default, so stored rows are kept.
222
+ `example-backend` passes `uiBlocks` (`exampleUiBlocksOptions` in `src/ai/hostActions.ts`) with
223
+ `repair: true`, `html: true`, and three host callbacks:
224
+
225
+ | Name | Handles | Does |
226
+ | --- | --- | --- |
227
+ | `exportDataset` | buttons | Payload `{dataset: string}`. Returns a badge that names that dataset |
228
+ | `scaleStepper` | `stepper` | `scaleStepperHostAction` |
229
+ | `toggleChecklist` | `checklist` | `toggleChecklistHostAction` |
230
+
231
+ `richBlocks` is left at its default (`true`). Dataset TTL stays at the default, so stored rows are kept.
88
232
 
89
233
  The per-request tool `todoStats` counts the signed-in user's open and completed todos, calls `registerAiDataset`, and returns `datasetId`. It stores the rows on `req.body.historyId` when that id belongs to the caller. With no history id it uses the caller's newest history, and with no history it returns `{datasetId: null, rowCount: 0}`. A chart dataset uses `source: ref` and that id. Columns are `status` and `count`.
90
234
 
91
- `GPTChat` lists `exportDataset` in `hostActions`, so that callback stays enabled. The e2e mock also allows `export_csv`.
235
+ `GPTChat` lists `exportDataset`, `scaleStepper`, and `toggleChecklist` in `hostActions`, so those callbacks stay enabled. The e2e mock also allows `export_csv`.
236
+
237
+ ## Give the agent photos
238
+
239
+ The model cannot make photos on the fly, and an `https` image needs `imageHosts`. The example
240
+ app generates a photo library once, and the agent cites its photos as `file:` ids.
241
+
242
+ 1. Generate the library, once per environment. From `example-backend`, with `GOOGLE_VERTEX_PROJECT`,
243
+ Application Default Credentials for Vertex, and `GCS_BUCKET` set, run `bun run photos:generate`.
244
+ Each prompt in `src/scripts/photoPrompts.ts` becomes one `PhotoLibraryEntry`. A re-run skips
245
+ prompts that already have an entry; pass `--force` to regenerate them. `PHOTO_IMAGE_MODEL`
246
+ (default `imagen-4.0-fast-generate-001`) and `GOOGLE_VERTEX_LOCATION` (default `us-central1`)
247
+ are optional.
248
+ 2. Register the `findPhotos` tool. `createFindPhotosTool()` from `src/ai/tools.ts` is in the
249
+ example's per-request tools. `findPhotos({query, count})` matches the query words against each
250
+ entry's `tags` and `alt`. `count` is 1–6. It returns `{photos: [{src: "file:<entry id>", alt}], note}`.
251
+ Entries that match more words come first; ties keep entry id order. Soft-deleted entries are
252
+ left out. When nothing matches, `photos` is empty and `note` tells the model not to invent an
253
+ id. The tool description tells the model to put each `src` into a `gallery`, `list`, or
254
+ `image` block exactly as returned.
255
+ 3. Serve the URLs. `photoLibraryRouter` (`src/api/photoLibrary.ts`) mounts `/photoLibrary`.
256
+ List and read need a signed-in user; create, update, and delete return 405.
257
+ `GET /photoLibrary/:id/url` returns `{url}`, a signed read URL that lasts one hour, to any
258
+ signed-in user. The library is shared, so the per-user `GET /files/*` route does not fit.
259
+ An unknown or soft-deleted id is 404. Without a bucket (`GCS_BUCKET` unset and none saved in
260
+ Profile) it returns 503 `Photo storage is not configured`.
261
+ 4. Regenerate the frontend SDK: `cd example-frontend && bun run sdk`. This adds
262
+ `useLazyPhotoLibraryUrlQuery`.
263
+ 5. Pass `resolveImage` to `GPTChat`. The example builds it with `createPhotoImageResolver`
264
+ (`example-frontend/lib/photoLibraryImages.ts`) around `openapi.useLazyPhotoLibraryUrlQuery`.
265
+ It fetches each id once while the URL is fresh (50 minutes). A failed lookup returns
266
+ `undefined`, so the block keeps its labelled placeholder, and a later render tries again.
267
+
268
+ In the example app, every `file:` image id in a block is a photo library entry id. Nothing else
269
+ in the example writes `file:` image ids, so the ids carry no extra prefix. A host that also cites
270
+ its own files must tell the two apart in its `resolveImage`. An id that is not in the library,
271
+ such as `file:roast-lamb` in the reference examples, resolves to `undefined` and shows the
272
+ placeholder.
92
273
 
93
274
  ## Render documents in chat
94
275
 
@@ -100,5 +281,8 @@ is true, finished top-level blocks render and a spinner marks the block still ar
100
281
  - `callback` calls `onBlockCallback`. Return `{replace: "block", blocks}` to swap that
101
282
  block, or `{text}` to append an assistant message. The button shows loading until the
102
283
  promise settles.
284
+ - Stepper taps and checklist ticks with a callback also call `onBlockCallback`. The block is
285
+ disabled until the promise settles.
286
+ - `copy` writes the clipboard on the device and calls nothing.
103
287
  - Pass `hostActions` to disable callbacks the host does not run, `resolveDataset` to
104
288
  load `ref` datasets, and `resolveImage` to turn a `file:` image id into a URL.
@@ -179,6 +179,8 @@ an isolated `pr-N` backend: `deploy-frontend-preview` always points
179
179
  up to 20 minutes for its `/health` before publishing, because `gcp-cd-preview`
180
180
  runs in a separate workflow.
181
181
 
182
+ Those backend deploys set `GCS_BUCKET` to `flourish-terreno-example-documents` (override with `GCP_DOCUMENTS_BUCKET`). Preview revisions also set `GCS_FOLDER_PREFIX=pr-<number>/` so each PR keeps its own folder. Without `GCS_BUCKET`, `GET /documents/` returns 503 and the Files section shows “Storage is not configured.”
183
+
182
184
  Backend previews prune not-Ready tagged revisions from traffic
183
185
  (`rebuild-cloud-run-ready-traffic.sh`), deploy untagged with
184
186
  `--revision-suffix`, then point the `pr-N` tag at that revision. Terraform
@@ -1,10 +1,10 @@
1
1
  # Preview a component demo
2
2
 
3
- Open a component demo and change the shareable query. The preview controls sit in the
4
- navigation header on demo and dev component routes: dropdowns for theme, viewport, and
3
+ Open a demo or dev route and change the shareable query. The preview controls sit in the
4
+ navigation header on every demo and dev screen, including the catalogs: dropdowns for theme, viewport, and
5
5
  background, plus a settings button (sliders icon) that opens a modal for right-to-left,
6
6
  reduced motion, locale, and the share query. In windows narrower than 768px every
7
- preview control moves into that modal; the Dev/Demo mode link stays in the header. `?embed=1` applies the same query and hides
7
+ preview control moves into that modal; the Dev/Demo mode link stays in the header. The header chrome uses the same `theme` query. `?embed=1` applies the same query and hides
8
8
  the controls.
9
9
 
10
10
  ```text
@@ -13,11 +13,11 @@ the controls.
13
13
 
14
14
  | Query | Values |
15
15
  | --- | --- |
16
- | `theme` | `light` (default) or `dark` (canonical dark role remap) |
16
+ | `theme` | `light` (default), `dark`, or `system` (follow the device color scheme) |
17
17
  | `viewport` | `full` (default), `320`, `375`, `1024`, `1280` |
18
18
  | `background` | `default`, `inverse`, `transparent` |
19
19
  | `locale` | Passed to components that already read a locale, such as consent |
20
20
  | `rtl` | `1` sets document direction to `rtl` on web |
21
21
  | `reducedMotion` | `1` sets `data-reduced-motion="1"` on the document element |
22
22
 
23
- Reload keeps the query. The settings modal shows the current query when any value is not the default. Choosing a control's default removes that key from the URL. Leaving the component route restores the app theme and, on web, document direction and reduced motion. Custom palettes stay on `/palette`.
23
+ Reload keeps the query. The settings modal shows the current query when any value is not the default. Choosing a control's default removes that key from the URL. Opening a component from the catalog keeps the current preview query. Leaving a component route restores document direction and reduced motion on web. Custom palettes stay on `/palette`.
@@ -52,6 +52,23 @@ Pass `{store: "mongo"}`. Hits live in `rateLimitHits` on the same mongoose conne
52
52
 
53
53
  Unauthenticated keys use `req.ip`. Express `trust proxy` defaults to **off** so a client cannot rotate `X-Forwarded-For` to bypass the auth bucket. On Cloud Run (one hop via GFE), pass `trustProxy: 1`. Extra proxies: hop count or a subnet list. `trustProxy: false` is the same as the default.
54
54
 
55
+ ## Limit one route
56
+
57
+ Use `createRouteRateLimitMiddleware` for a stricter limit on one expensive route. It works with or without the global limiter.
58
+
59
+ ```typescript
60
+ import {createRouteRateLimitMiddleware} from "@terreno/api";
61
+
62
+ const uploadLimit = createRouteRateLimitMiddleware({
63
+ name: "uploads",
64
+ max: 1,
65
+ windowMs: 60_000,
66
+ store: "mongo",
67
+ });
68
+ ```
69
+
70
+ `DocumentStorageApp` takes the same options as `uploadRateLimit`. The example backend allows one upload per IP per minute and sets `trust proxy` to `1` on Cloud Run so the key is the real client IP.
71
+
55
72
  ## Auth vs API buckets
56
73
 
57
74
  | Path | Bucket |
@@ -0,0 +1,35 @@
1
+ # Test UI accessibility
2
+
3
+ Run the WCAG scanner against every changed rendered state:
4
+
5
+ ```bash
6
+ bun run accessibility:scan \
7
+ "http://localhost:8085/dev/Theme?story=ThemeComponent&theme=light" \
8
+ "http://localhost:8085/dev/Theme?story=ThemeComponent&theme=dark"
9
+ ```
10
+
11
+ The scanner applies axe rules tagged WCAG 2.0 A/AA, WCAG 2.1 A/AA, and WCAG
12
+ 2.2 AA. It writes a JSON report and full-page screenshots under
13
+ `/opt/cursor/artifacts/accessibility/` and exits nonzero when any violation is
14
+ found.
15
+
16
+ ## Verify what automation cannot
17
+
18
+ 1. Use only the keyboard to reach and operate each changed control.
19
+ 2. Confirm focus order and the visible focus indicator match the visual order.
20
+ 3. Inspect each control's accessible name, role, state, and error relationship.
21
+ 4. Check light and dark modes, plus loading, empty, disabled, and error states
22
+ affected by the change.
23
+ 5. At 200% browser zoom, confirm content remains usable without clipping or
24
+ losing actions.
25
+
26
+ Automated scans do not prove keyboard behavior, announcement quality, reading
27
+ order, or whether alternative text conveys the intended meaning. Record those
28
+ manual checks with the automated report in the pull request Verification table.
29
+
30
+ ## Fixing findings
31
+
32
+ Fix violations in the changed feature before submission. If a finding is
33
+ pre-existing and outside the changed path, capture a baseline on an unaffected
34
+ route and name both routes in the pull request. Do not suppress an axe rule
35
+ without a linked issue and a documented reason.