@terreno/mcp 57.11.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.
- package/dist/docs/upgrades/57.12.0.md +89 -0
- package/dist/docs/upgrades/57.13.0.md +75 -0
- package/dist/docs/versioned/next/explanation/README.md +1 -1
- package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +69 -9
- package/dist/docs/versioned/next/explanation/ai-workflows.md +4 -3
- package/dist/docs/versioned/next/explanation/example-coverage.md +3 -3
- package/dist/docs/versioned/next/explanation/roadmap-process.md +4 -2
- package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +12 -0
- package/dist/docs/versioned/next/how-to/README.md +2 -0
- package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +187 -3
- package/dist/docs/versioned/next/how-to/circleci.md +8 -2
- package/dist/docs/versioned/next/how-to/github-actions-ci.md +5 -6
- package/dist/docs/versioned/next/how-to/preview-demo.md +5 -5
- package/dist/docs/versioned/next/how-to/rate-limiting.md +17 -0
- package/dist/docs/versioned/next/how-to/test-ui-accessibility.md +35 -0
- package/dist/docs/versioned/next/implementationPlans/agent-ui-rich-replies.md +359 -0
- package/dist/docs/versioned/next/reference/README.md +1 -1
- package/dist/docs/versioned/next/reference/admin-backend.md +26 -0
- package/dist/docs/versioned/next/reference/admin-frontend.md +15 -0
- package/dist/docs/versioned/next/reference/ai-harness.md +33 -5
- package/dist/docs/versioned/next/reference/ai.md +88 -7
- package/dist/docs/versioned/next/reference/api.md +4 -0
- package/dist/docs/versioned/next/reference/blocks.md +243 -11
- package/dist/docs/versioned/next/reference/environment-variables.md +3 -0
- package/dist/docs/versioned/next/reference/ui.md +222 -12
- package/dist/docs/versioned/next/tasks/agent-ui-rich-replies.md +188 -0
- package/package.json +3 -3
- package/src/docs/upgrades/57.12.0.md +89 -0
- package/src/docs/upgrades/57.13.0.md +75 -0
- package/src/docs/versioned/next/explanation/README.md +1 -1
- package/src/docs/versioned/next/explanation/agent-ui-blocks.md +69 -9
- package/src/docs/versioned/next/explanation/ai-workflows.md +4 -3
- package/src/docs/versioned/next/explanation/example-coverage.md +3 -3
- package/src/docs/versioned/next/explanation/roadmap-process.md +4 -2
- package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +12 -0
- package/src/docs/versioned/next/how-to/README.md +2 -0
- package/src/docs/versioned/next/how-to/agent-ui-blocks.md +187 -3
- package/src/docs/versioned/next/how-to/circleci.md +8 -2
- package/src/docs/versioned/next/how-to/github-actions-ci.md +5 -6
- package/src/docs/versioned/next/how-to/preview-demo.md +5 -5
- package/src/docs/versioned/next/how-to/rate-limiting.md +17 -0
- package/src/docs/versioned/next/how-to/test-ui-accessibility.md +35 -0
- package/src/docs/versioned/next/implementationPlans/agent-ui-rich-replies.md +359 -0
- package/src/docs/versioned/next/reference/README.md +1 -1
- package/src/docs/versioned/next/reference/admin-backend.md +26 -0
- package/src/docs/versioned/next/reference/admin-frontend.md +15 -0
- package/src/docs/versioned/next/reference/ai-harness.md +33 -5
- package/src/docs/versioned/next/reference/ai.md +88 -7
- package/src/docs/versioned/next/reference/api.md +4 -0
- package/src/docs/versioned/next/reference/blocks.md +243 -11
- package/src/docs/versioned/next/reference/environment-variables.md +3 -0
- package/src/docs/versioned/next/reference/ui.md +222 -12
- package/src/docs/versioned/next/tasks/agent-ui-rich-replies.md +188 -0
|
@@ -7,24 +7,23 @@ describes how they behaved if one is ever re-enabled.
|
|
|
7
7
|
|
|
8
8
|
## Docs site (`docs-deploy.yml`)
|
|
9
9
|
|
|
10
|
-
`Build docs site` is often the longest PR job because
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
`Build docs site` is often the longest PR job because TypeDoc regenerates API
|
|
11
|
+
pages. The site builds only the current docs tree; `website/versioned_docs/` is
|
|
12
|
+
markdown for the MCP server.
|
|
13
13
|
|
|
14
14
|
PR builds set `DOCS_PREVIEW=true` and pass `--no-minify`. Both PR and
|
|
15
15
|
`master` use Docusaurus Faster (Rspack/SWC via `@docusaurus/faster`).
|
|
16
16
|
|
|
17
17
|
| Behavior | PR preview | `master` production |
|
|
18
18
|
| --- | --- | --- |
|
|
19
|
-
| Versioned docs (`57.1.0`, `0.30.0`, …) | Omitted
|
|
19
|
+
| Versioned docs (`57.1.0`, `0.30.0`, …) | Omitted | Prebuilt archives unpacked under `/<version>/` |
|
|
20
20
|
| JS minify | Off | On |
|
|
21
21
|
| Local search index | Skipped | Built |
|
|
22
22
|
| Generated API + component MDX | Restored from cache when `api`/`rtk`/`ui` hashes match | Same cache, then full generate on miss |
|
|
23
23
|
| TypeDoc workspace `tsc` | Once per generate (api+rtk deps share a process) | Same |
|
|
24
24
|
| Rspack cache (`node_modules/.cache/rspack`) | Restored per `pull_request` vs `push` | Separate production key |
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
PR deploy preview.
|
|
26
|
+
Do not rely on `/57.1.0/…` URLs in a PR deploy preview.
|
|
28
27
|
|
|
29
28
|
## Other high-cost jobs
|
|
30
29
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Preview a component demo
|
|
2
2
|
|
|
3
|
-
Open a
|
|
4
|
-
navigation header on demo and dev
|
|
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 `
|
|
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.
|
|
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.
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Agent UI Rich Replies: steppers, checklists, galleries, and copy
|
|
2
|
+
|
|
3
|
+
**Status:** approved 2026-10-07
|
|
4
|
+
**Task list:** [`docs/tasks/agent-ui-rich-replies.md`](../tasks/agent-ui-rich-replies.md)
|
|
5
|
+
**Supporting skills:** `terreno-ui`, `terreno-backend-api`, `ai-prompt-governance`, `update-docs`, `verify-ui-changes`, `backend-test-env`
|
|
6
|
+
**Source:** OpenAI, "GPT-6 and Intelligent UI for everyone", the "Planning a Sunday roast" GPT-5.6 vs GPT-6 Instant comparison (fetched 2026-10-07).
|
|
7
|
+
|
|
8
|
+
Terreno agents can already reply with whole-reply YAML block documents: headings, markdown,
|
|
9
|
+
metrics, cards, charts, dataset tables, images, callouts, and buttons that reply, open,
|
|
10
|
+
select, or call a host callback. OpenAI's GPT-6 launch shows a meal-plan reply that goes
|
|
11
|
+
further: a summary card at the top, a three-photo collage, a photo menu list, a guest-count
|
|
12
|
+
stepper that recalculates the shopping list, a "Copy shopping list" button, and a timed
|
|
13
|
+
cooking checklist with a "1 of 8" counter. The GPT-5.6 version beside it uses a markdown
|
|
14
|
+
"How much lamb?" table. This plan adds the missing pieces, so a Terreno agent can
|
|
15
|
+
return both replies. The new blocks are on by default (D6); a host whose shipped clients cannot render them yet sets `uiBlocks.richBlocks: false`.
|
|
16
|
+
|
|
17
|
+
## The idea
|
|
18
|
+
|
|
19
|
+
Six additions to the closed block catalog in `@terreno/blocks`, each rendered by
|
|
20
|
+
`BlocksView` with `@terreno/ui` components:
|
|
21
|
+
|
|
22
|
+
| Demo element | Terreno block | Interaction |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| "Your dinner plan" summary with a small label above the title | `card.eyebrow` (new optional field) | none |
|
|
25
|
+
| Three grouped hero photos | `gallery` (2–6 images, optional captions) | none |
|
|
26
|
+
| Menu items: photo, bold title, description | `list` (items with optional image) | none |
|
|
27
|
+
| Guest count − / + with recalculated quantities | `stepper` | **host callback round trip** |
|
|
28
|
+
| "Copy shopping list" | `copy` action kind on a button | on the device (clipboard) |
|
|
29
|
+
| Cooking checklist with times and "n of m" | `checklist` | **host callback per tick** (local when the host has no checklist action) |
|
|
30
|
+
| "How much lamb?" guests table | `table` renders typed columns; markdown tables in `text` get theme styling | none |
|
|
31
|
+
|
|
32
|
+
The stepper follows the decision made in chat: **− and + are callback buttons like the
|
|
33
|
+
existing ones.** A tap sends `POST /gpt/actions` with the stepper's callback name and
|
|
34
|
+
`{...callback.payload, value: <next>}`. The host action returns
|
|
35
|
+
`{replace: "block", blocks: {v: 1, blocks: [<new stepper>]}}`, and `GPTChat` swaps the block by
|
|
36
|
+
id through `overrides`, as it does today. `@terreno/ai` ships an opt-in host action,
|
|
37
|
+
`scaleStepperHostAction`. It reads the stepper the agent wrote from the stored history, not
|
|
38
|
+
from the tap request, and scales every item linearly from it. Scaling from the agent's
|
|
39
|
+
original keeps rounding from drifting tap after tap. The owner of a history can still edit
|
|
40
|
+
its stored prompts through `PATCH /gpt/histories/:id`, so an app whose numbers matter
|
|
41
|
+
(prices, stock) must compute them from its own data, not from the stored block.
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
- type: card
|
|
45
|
+
eyebrow: How much should you buy?
|
|
46
|
+
children:
|
|
47
|
+
- type: stepper
|
|
48
|
+
id: guests
|
|
49
|
+
label: Number of people
|
|
50
|
+
unit: People
|
|
51
|
+
value: 5
|
|
52
|
+
min: 1
|
|
53
|
+
max: 20
|
|
54
|
+
callback: {name: scaleStepper}
|
|
55
|
+
items:
|
|
56
|
+
- {label: Bone-in leg of lamb, amount: 2, unit: kg, decimals: 1}
|
|
57
|
+
- {label: Carrots, amount: 8, round: up}
|
|
58
|
+
note: Generous portions, with a little extra.
|
|
59
|
+
- type: actions
|
|
60
|
+
id: copy_list
|
|
61
|
+
elements:
|
|
62
|
+
- {type: button, id: copy, text: Copy shopping list, action: {kind: copy, target: guests}}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The checklist works the same way (D2). Each tick sends a callback with the full set of ticks,
|
|
66
|
+
and `toggleChecklistHostAction` returns the checklist with those ticks. Photos (D3) come from
|
|
67
|
+
a library of images generated once with the Vertex image model and stored through
|
|
68
|
+
`FileStorageService`. The agent picks from it with a `findPhotos` tool and writes `file:` ids.
|
|
69
|
+
|
|
70
|
+
Rejected: a shared document `state` with `{{formula}}` bindings computed on the device.
|
|
71
|
+
It needs an expression language, and the human chose a server round trip.
|
|
72
|
+
|
|
73
|
+
## The plan
|
|
74
|
+
|
|
75
|
+
| # | Task | Depends on | Lands in | Proves it |
|
|
76
|
+
| --- | --- | --- | --- | --- |
|
|
77
|
+
| T1 | Stepper block round trips through a host callback (tracer) | none | blocks, ui, ai | fixture, renderer, handler, and route tests |
|
|
78
|
+
| T2 | Prompt offers the new blocks only when the host opts in | T1 | blocks, ai | prompt + chat-turn tests |
|
|
79
|
+
| T3 | Table block renders typed columns | T1 | ui | `DataTable` + `BlocksView` tests |
|
|
80
|
+
| T4 | Markdown tables in `text` get theme styling | T3 | ui | `MarkdownView` test |
|
|
81
|
+
| T5 | `checklist` contract | T2 | blocks | fixtures |
|
|
82
|
+
| T6 | `gallery` contract | T5 | blocks | fixtures + image-host lint |
|
|
83
|
+
| T7 | `list` contract | T6 | blocks | fixtures + image-host lint |
|
|
84
|
+
| T8 | `card.eyebrow` contract | T7 | blocks | fixtures |
|
|
85
|
+
| T9 | `copy` action contract + `blockPlainText` | T8 | blocks | fixtures + serializer tests |
|
|
86
|
+
| T10 | Checklist renders and toggles | T3, T4, T5 | ui | `BlocksView` test |
|
|
87
|
+
| T11 | Gallery renders | T6, T10 | ui | `BlocksView` test |
|
|
88
|
+
| T12 | List renders | T7, T11 | ui | `BlocksView` test |
|
|
89
|
+
| T13 | Card eyebrow renders | T8, T12 | ui | `BlocksView` test |
|
|
90
|
+
| T14 | Copy action writes the clipboard | T9, T13 | ui | `BlocksView` test |
|
|
91
|
+
| T15 | Sunday roast golden document + playground story | T14 | blocks, demo | fixture + story screenshot |
|
|
92
|
+
| T16 | Example app: the roast reply against the real callback routes | T15, T20, T21 | example-backend, example-frontend | Playwright + recording |
|
|
93
|
+
| T17 | Model smoke: a real model writes the roast reply | T15, T20 | example-backend | smoke script output |
|
|
94
|
+
| T18 | Docs sweep, changelog, rules | T15, T20, T21 | docs, rules | `website:build`, `rules:check` |
|
|
95
|
+
| T19 | Generated photo library: generate images once and store them | none | example-backend | model + script tests |
|
|
96
|
+
| T20 | `findPhotos` tool and photo URLs in the example app | T19 | example-backend, example-frontend | tool + action tests |
|
|
97
|
+
| T21 | Checklist ticks round trip through a host callback | T10 | ai | handler + route tests |
|
|
98
|
+
|
|
99
|
+
Tracer: T1, from the agent's YAML through `validateBlocks`, `BlocksView`, the callback
|
|
100
|
+
event, `POST /gpt/actions`, and `scaleStepperHostAction`, back to a replaced block.
|
|
101
|
+
Out of scope: formulas or shared state, scripted html, persisting overrides, maps, tabs,
|
|
102
|
+
interactive diagrams, generated tools (see Expansions).
|
|
103
|
+
Open risks:
|
|
104
|
+
- Each tap is one round trip. `pendingElementIds` disables both buttons until the reply
|
|
105
|
+
lands, so fast taps are dropped, not queued (Q4).
|
|
106
|
+
- `scaleStepperHostAction` finds the agent's block by `messageId`, which is `msg-<index>`
|
|
107
|
+
when the host sets no message id. When that misses, it uses the only assistant prompt that
|
|
108
|
+
holds a stepper with that `blockId`. If more than one does, it returns 409 rather than
|
|
109
|
+
guess.
|
|
110
|
+
- Stored history prompts are writable by their owner, so the stored stepper is not a trust
|
|
111
|
+
boundary (see the idea above; the how-to says so).
|
|
112
|
+
|
|
113
|
+
## Contracts
|
|
114
|
+
|
|
115
|
+
Field names are the contract. Limits go in `BLOCK_LIMITS` and are printed in the prompt
|
|
116
|
+
from there. Lint paths use the existing `blocks[0].images[2].src` form.
|
|
117
|
+
|
|
118
|
+
### `stepper` (T1)
|
|
119
|
+
|
|
120
|
+
| Field | Required | Rule |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `id` | yes | block id pattern, at most 54 characters (room for the `_increase` suffix) |
|
|
123
|
+
| `label` | yes | 1–80 |
|
|
124
|
+
| `value`, `min`, `max` | yes | numbers; `min` < `max`; `min` ≤ `value` ≤ `max` (`OUT_OF_RANGE`) |
|
|
125
|
+
| `step` | no | > 0, default 1 |
|
|
126
|
+
| `unit` | no | 1–40, shown under the value ("People") |
|
|
127
|
+
| `callback` | yes | `{name, payload?}`; `name` is checked by `UNKNOWN_HOST_ACTION` when `hostActions` is passed |
|
|
128
|
+
| `itemsTitle` | no | 1–80, a small heading above the items ("Your shopping quantities") |
|
|
129
|
+
| `items` | no | 0–12 of `{label (1–80), amount (number), unit? (1–20), decimals? (0–3, default 0), round?: nearest \| up (default nearest)}` |
|
|
130
|
+
| `note` | no | 1–280, muted under the items |
|
|
131
|
+
|
|
132
|
+
The renderer reserves the element ids `<id>_decrease` and `<id>_increase`. Lint reports
|
|
133
|
+
`DUPLICATE_ID` when any other block or element uses them.
|
|
134
|
+
|
|
135
|
+
Rendering: the label as an eyebrow, `IconButton` − and +, then the value with its unit,
|
|
136
|
+
`itemsTitle`, the items as a two-column label / amount grid formatted with `decimals` ("2.0 kg"), then the
|
|
137
|
+
note. − is disabled when one more step would go below `min`, + when it would go above `max`.
|
|
138
|
+
Both are disabled while either element is pending,
|
|
139
|
+
and when `callback.name` is not in `hostActions`. Accessible labels: "Decrease <label>",
|
|
140
|
+
"Increase <label>".
|
|
141
|
+
|
|
142
|
+
Callback event: `blockId: <id>`, `elementId: <id>_increase` or `<id>_decrease`, action
|
|
143
|
+
`{kind: "callback", name, payload: {...payload, value: value ± step}}`.
|
|
144
|
+
|
|
145
|
+
`scaleStepperHostAction` (in `@terreno/ai`) is `{handler, payload, handles: "stepper", logResponse: false}`. Its `payload` schema is `z.object({value: z.number()}).passthrough()`, and the handler reads only `value`.
|
|
146
|
+
|
|
147
|
+
1. Loads the stepper the agent wrote with `findAgentBlock({history, messageId, blockId, type})`,
|
|
148
|
+
a shared helper (`ai/src/service/agentBlocks.ts`). It reads `history.prompts[n]` when
|
|
149
|
+
`messageId` is `msg-<n>` (stored prompts have no ids), parses it with `parseBlocks`, and
|
|
150
|
+
finds the block of that type whose id is `blockId`. When that misses, it uses the only
|
|
151
|
+
assistant prompt holding such a block. With more than one, it returns 409. With none, 404.
|
|
152
|
+
2. Checks that `value` is a number within `[min, max]` on the `step` grid. Anything else
|
|
153
|
+
returns 400.
|
|
154
|
+
3. Returns `{replace: "block", blocks: {v: 1, blocks: [stepper]}}`. The returned stepper has
|
|
155
|
+
the new `value`, and each item's `amount` is `original amount × value / original value`,
|
|
156
|
+
rounded by that item's `decimals` and `round`.
|
|
157
|
+
|
|
158
|
+
`logResponse: false` is a new optional host-action field. With it, the `AIRequest` row for
|
|
159
|
+
`POST /gpt/actions` keeps today's ids-only prompt, adds the numeric `value`, and leaves out
|
|
160
|
+
the returned block. Every other host action keeps logging the full response, as today.
|
|
161
|
+
|
|
162
|
+
### Default on, with an opt-out (T2)
|
|
163
|
+
|
|
164
|
+
| Option | Effect |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `uiBlocks.richBlocks` (default `true`) | The prompt offers `checklist`, `gallery`, `list`, `card.eyebrow`, and `copy`. `false` removes them. |
|
|
167
|
+
| `richBlocks` on **and** a host action with `handles: "stepper"` | The prompt offers `stepper` and names those actions as its callbacks. Without a stepper action, the prompt never mentions `stepper`, because its buttons would be dead. |
|
|
168
|
+
| `richBlocks` on **and** a host action with `handles: "checklist"` | The checklist prompt line tells the model to set `callback` to that action. Without one, it says to leave `callback` out (ticks stay local). |
|
|
169
|
+
|
|
170
|
+
`blocksPromptSection` gains `richBlocks` (default `true`, matching the server), `stepperActions`, and `checklistActions`.
|
|
171
|
+
`validateBlocks` gains the same two lists: a stepper or checklist whose `callback.name` is not
|
|
172
|
+
in its list fails `UNKNOWN_HOST_ACTION`. `chatTurn` derives both lists from the host actions'
|
|
173
|
+
`handles` field. The server always accepts the new blocks when it validates. The
|
|
174
|
+
opt-out only stops the model from writing them for clients that cannot render them yet.
|
|
175
|
+
|
|
176
|
+
Upgrading changes behaviour: every host with `uiBlocks` on gets the new blocks in its prompt
|
|
177
|
+
after the bump. The changelog entry says so under a "Behaviour change" heading and names
|
|
178
|
+
`richBlocks: false` as the opt-out.
|
|
179
|
+
|
|
180
|
+
### `checklist` (T5)
|
|
181
|
+
|
|
182
|
+
`{type: checklist, id (at most 31 characters), title? (1–120), callback?: {name, payload?}, items: 1–30 of {id (at most 32 characters), text (1–120), detail? (1–280), meta? (1–40), checked?: boolean}}`.
|
|
183
|
+
Item ids are unique. The element id of a tick is `<id>_<item id>`, reserved like the stepper's
|
|
184
|
+
(`DUPLICATE_ID`). Rendering: the title on the left and "n of m" on the right, then one
|
|
185
|
+
`CheckBox` row per item: the bold `text` first, then `meta` (the time, small and muted), then a muted
|
|
186
|
+
`detail` (D7).
|
|
187
|
+
|
|
188
|
+
With a `callback`, an `onAction` handler, and either no `hostActions` list or one that names the
|
|
189
|
+
callback (matching the stepper), a tick sends
|
|
190
|
+
`{kind: "callback", name, payload: {...payload, itemId, checked, state: {<item id>: boolean, ...}}}`
|
|
191
|
+
with `blockId: <id>` and `elementId: <id>_<item id>`. `state` is the full set of ticks after
|
|
192
|
+
this one, because overrides live only in the client (D4). The whole checklist is disabled until
|
|
193
|
+
the reply lands, so a second tick cannot send a `state` that misses the first, and the checklist
|
|
194
|
+
then shows the returned block. Ticks are not applied ahead of
|
|
195
|
+
the reply. Without a `callback`, without `onAction`, or when a `hostActions` list leaves the name
|
|
196
|
+
out, ticks are local.
|
|
197
|
+
|
|
198
|
+
`toggleChecklistHostAction` (in `@terreno/ai`, T21) is `{handler, payload, handles: "checklist", logResponse: false}`
|
|
199
|
+
with payload `z.object({itemId: z.string(), checked: z.boolean(), state: z.record(z.string(), z.boolean())}).passthrough()`.
|
|
200
|
+
It loads the agent's checklist with `findAgentBlock`, rejects item ids the checklist does not
|
|
201
|
+
have (400), and returns `{replace: "block", blocks: {v: 1, blocks: [checklist]}}` with
|
|
202
|
+
`checked` set from `state`. Apps that record progress register their own `handles: "checklist"`
|
|
203
|
+
action.
|
|
204
|
+
|
|
205
|
+
### `gallery` (T6)
|
|
206
|
+
|
|
207
|
+
`{type: gallery, id?, images: 2–6 of {src, alt (1–200), caption? (1–120)}}`. Each `src` follows
|
|
208
|
+
the `image` rules, including `IMAGE_HOST_NOT_ALLOWED`. Rendering: up to 3 images sit in one
|
|
209
|
+
row of equal 4:3 tiles; more than 3 wrap into a 3-column grid. Narrow screens scroll the row
|
|
210
|
+
sideways (Q5).
|
|
211
|
+
|
|
212
|
+
### `list` (T7)
|
|
213
|
+
|
|
214
|
+
`{type: list, id?, items: 1–12 of {title (1–120), text? (1–500, plain text), meta? (1–40), image?: {src, alt}}}`.
|
|
215
|
+
Image sources follow the `image` rules. Rendering: a 3:4 portrait thumbnail on the left,
|
|
216
|
+
then the bold title and muted text.
|
|
217
|
+
|
|
218
|
+
### `card.eyebrow` (T8)
|
|
219
|
+
|
|
220
|
+
Optional `eyebrow` (1–60) on `card`, rendered small and muted above `title`.
|
|
221
|
+
|
|
222
|
+
### Generated photo library (T19, T20)
|
|
223
|
+
|
|
224
|
+
The agent cannot make photos on the fly, and `https` images need an allowlist. So the example
|
|
225
|
+
app generates a library once and stores it.
|
|
226
|
+
|
|
227
|
+
| Piece | Contract |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `PhotoLibraryEntry` model (example-backend) | `{fileAttachmentId, gcsKey, alt (1–200), tags: string[] (1–12), prompt}`, every field with a `description`; `createdUpdatedPlugin`, `isDeletedPlugin`; `prompt` is unique. A soft-deleted entry still holds its prompt, and `--force` leaves the old stored object and its `FileAttachment` behind |
|
|
230
|
+
| `bun run photos:generate` (example-backend script) | Reads `src/scripts/photoPrompts.ts` (about 12 entries: roast lamb, crispy potatoes, honey carrots and parsnips, lemony greens, apple crumble, a laid table, and so on). It runs each through AI SDK `generateImage` with the Vertex image model (`imagen-4.0-fast-generate-001` unless `PHOTO_IMAGE_MODEL` is set), uploads it with `FileStorageService.upload` as a system user, and upserts a `PhotoLibraryEntry` by `prompt`. Re-runs skip prompts that already exist unless `--force` is passed. Requires `GOOGLE_VERTEX_PROJECT`, Application Default Credentials for Vertex, and `GCS_BUCKET`, validated up front; `GOOGLE_VERTEX_LOCATION` (default `us-central1`) picks a regional endpoint for Imagen. Uploads are owned by a find-or-create system user `photo-library@system.invalid`. A failed prompt is logged, the run continues, and the exit code is 1 if any failed |
|
|
231
|
+
| `findPhotos({query, count})` tool | Matches `query` words against `tags` and `alt`, and returns up to `count` (1–6) `{src: "file:<entry id>", alt}` |
|
|
232
|
+
| `photoLibrary` `modelRouter` with an instance action `url` | `GET /photoLibrary/:id/url` returns `{url}` (a `FileStorageService.getSignedUrl`) to any authenticated user. The library is shared, so the per-user `GET /files/*` route does not fit. List and read are `IsAuthenticated`; create, update, and delete are disabled |
|
|
233
|
+
| example-frontend `resolveImage` | Resolves `file:<id>` through the generated SDK hook for `url` |
|
|
234
|
+
|
|
235
|
+
The library holds only generated food photos, no user data.
|
|
236
|
+
|
|
237
|
+
### `copy` action (T9)
|
|
238
|
+
|
|
239
|
+
A new `action.kind`: `{kind: copy, text?, target?}`. Lint requires exactly one of the two,
|
|
240
|
+
reporting `MISSING_REQUIRED` as it does for `open`. `target` must name a `stepper`,
|
|
241
|
+
`checklist`, `list`, `table`, or `text` block (`COPY_TARGET_INVALID`). `text` holds at most
|
|
242
|
+
4,000 characters (`copyTextMaxLength`); `target` is a block id.
|
|
243
|
+
`blockPlainText(block, {datasets?, checked?})` in `@terreno/blocks` turns a block into the
|
|
244
|
+
copied text:
|
|
245
|
+
|
|
246
|
+
| Block | Text |
|
|
247
|
+
| --- | --- |
|
|
248
|
+
| `stepper` | `<label>: <value> <unit>`, then one `<item label>: <amount> <unit>` line per item |
|
|
249
|
+
| `checklist` | one `[x] text` or `[ ] text` line per item, using the current toggles |
|
|
250
|
+
| `list` | one `- title: text` line per item |
|
|
251
|
+
| `table` | a header row, then the rows, tab-separated |
|
|
252
|
+
| `text` | the markdown as written |
|
|
253
|
+
|
|
254
|
+
`BlocksView` copies the text from the **current** block, with the override and toggles applied,
|
|
255
|
+
using `expo-clipboard`. It then shows "Copied" in a polite live region next to the button for
|
|
256
|
+
2 s. It does not call `onAction` or the server.
|
|
257
|
+
|
|
258
|
+
### Tables (T3, T4)
|
|
259
|
+
|
|
260
|
+
T3: `DataTable` gets real `number` and `date` cells. `number` is right-aligned. `date` is
|
|
261
|
+
formatted with Luxon (`DateTime.fromISO(...).toLocaleString(DateTime.DATE_MED)`). The
|
|
262
|
+
`table` block renderer stops forcing every column to `text` and `width: 120`. It maps the
|
|
263
|
+
dataset's column types to these cells. It measures the container with `onLayout` and splits
|
|
264
|
+
the width evenly, at least 96 per column, scrolling sideways past that. Number headers
|
|
265
|
+
right-align to match their cells. Headers show column names with underscores as spaces, and the
|
|
266
|
+
copied header row does the same. The table sits in a frame sized to its header plus rows (54
|
|
267
|
+
each, up to 10 rows), so it never spills over the next block; longer tables scroll inside it.
|
|
268
|
+
|
|
269
|
+
T4: `react-native-markdown-display` already lays GFM tables out as views. T4 adds theme
|
|
270
|
+
styling: `border.default` cell borders, a bold header row on `surface.secondaryLight`, and
|
|
271
|
+
horizontal scroll when the table is wider than its container. Its test fails on `master`
|
|
272
|
+
first.
|
|
273
|
+
|
|
274
|
+
## Acceptance criteria
|
|
275
|
+
|
|
276
|
+
| ID | Criterion | Verification |
|
|
277
|
+
| --- | --- | --- |
|
|
278
|
+
| AC1 | Each new block and the `copy` action has at least one valid and one invalid fixture, and `jsonSchema.test.ts` agrees with Zod on all of them | `bun test blocks/` |
|
|
279
|
+
| AC2 | Every new error code (`OUT_OF_RANGE`, `COPY_TARGET_INVALID`) appears in `docs/reference/blocks.md` | `blocksDocParity.test.ts` |
|
|
280
|
+
| AC3 | With `richBlocks: false`, `blocksPromptSection` output is byte-identical to `master`, even when a stepper action is registered. With `richBlocks` unset, `addGptRoutes` and `blocksPromptSection` both behave as `true`. On, the prompt has one line per new block (added as each contract lands; the full snapshot is taken in T9), limits come from `BLOCK_LIMITS`, and the stepper line names the stepper actions | prompt snapshot tests |
|
|
281
|
+
| AC4 | Tapping + on a 5-person stepper emits a callback with `payload.value: 6`. An item `{amount: 2, unit: kg, decimals: 1}` renders "2.0 kg". Rendering an override with `value: 6` shows 6 and the new amounts | `ui/src/blocks/BlocksView.test.tsx` |
|
|
282
|
+
| AC5 | Through `POST /gpt/actions`, with a stored stepper (5 people; lamb `2` kg / 1 dp; parsnips `7` / up; broccoli `625` g / nearest) and `value: 6`: the result is `{replace: "block", blocks: {v: 1, blocks: [stepper]}}` with lamb 2.4, parsnips 9 (nearest would give 8), and broccoli 750. An extra agent `callback.payload` key is accepted. `value: 21` (max 20) returns 400. An unknown `blockId` returns 404. Two stored prompts with the same stepper id and a missed `msg-<n>` return 409. The `AIRequest` row has the ids and `value`, and no `response` | `ai/src/service/scaleStepper.test.ts`, `ai/src/routes/gptActions.test.ts` |
|
|
283
|
+
| AC6 | Without a callback, toggling an item changes "1 of 8" to "2 of 8" on the device. With a callback in `hostActions`, the tick emits `payload: {itemId, checked: true, state}` with all 8 items, disables the whole checklist while the tick is pending, and the counter shows "2 of 8" only after the override arrives | `BlocksView.test.tsx` |
|
|
284
|
+
| AC6b | Through `POST /gpt/actions`, `toggleChecklistHostAction` returns the stored checklist with `checked` from `state`. An unknown item id returns 400. The `AIRequest` row has no `response` | `ai/src/service/toggleChecklist.test.ts`, `ai/src/routes/gptActions.test.ts` |
|
|
285
|
+
| AC7 | Gallery, list, and eyebrow render with `@terreno/ui` components only, with `alt` as the accessible label | `BlocksView.test.tsx` |
|
|
286
|
+
| AC8 | Pressing a copy button whose target is a stepper at value 6 writes the scaled list to the clipboard (mocked `expo-clipboard`) and shows "Copied" | `BlocksView.test.tsx` |
|
|
287
|
+
| AC9 | A `number` column renders right-aligned and a `date` column renders Luxon `DATE_MED`. A GFM table in `text` has themed borders, a bold header, and scrolls sideways when wide. Both tests fail on `master` | `DataTable.test.tsx`, `BlocksView.test.tsx`, `MarkdownView.test.tsx` |
|
|
288
|
+
| AC10 | `blocks/src/fixtures/golden/sunday-roast.yaml` reproduces the GPT-6 demo reply (title, gallery, summary card, menu list, stepper + copy card with `itemsTitle`, checklist, note, tips, follow-up buttons) plus the GPT-5.6 "How much lamb?" table. It validates with `hostActions: ["scaleStepper", "toggleChecklist"]`, `stepperActions: ["scaleStepper"]`, `checklistActions: ["toggleChecklist"]`, and `imageHosts: ["images.example.com"]` in its own test, outside the option-less `valid/` loop | `bun test blocks/` |
|
|
289
|
+
| AC11 | In example-frontend, the roast reply is stored in a real `GptHistory` and streamed by the mocked model. Pressing + changes 5 to 6 through the real `POST /gpt/actions` on example-backend. Copy writes the list. A checklist tick goes through `POST /gpt/actions` and updates the counter. Gallery and list images load through `GET /photoLibrary/:id/url`, with only the storage download stubbed (`page.route` on the signed-URL host serves a fixture image) | `bun run frontend:e2e` (`e2e/ai-chat.spec.ts`), with screenshots and a recording attached to the PR (`/opt/cursor/artifacts/` is read-only in this sandbox) |
|
|
290
|
+
| AC12 | With a model key set, a generated photo library in the database, and the `findPhotos` tool registered, `bun run blocks:smoke` sends "Give me a plan for a sunday lamb roast, I'm having friends over still figuring out numbers tbh" through `AIService` with the opted-in prompt. It asserts that the reply validates and contains `gallery`, `list`, `stepper`, `checklist`, and a `copy` action, and that every image `src` is a `file:` id that `findPhotos` returned. Without a key it exits 0 and says it skipped | script exit code; output saved to `--out`. **Partly met:** the checks are unit-tested with a mock model, but no real model has run it (no key in the build sandbox) |
|
|
291
|
+
| AC14 | `photos:generate` with a fake image model and a fake storage service creates one entry per prompt, and a second run creates none. `findPhotos({query: "roast lamb", count: 3})` returns 3 `file:` ids. `GET /photoLibrary/:id/url` returns a URL to a non-admin user, and create returns 405 | `example-backend` tests |
|
|
292
|
+
| AC13 | Docs: reference rows, a how-to "Add a stepper callback", the explanation catalog fixed (it lacks `callout`, `image`, `details`, and `html` today), and a changelog entry | `bun run website:build`, `bun run rules:check` |
|
|
293
|
+
|
|
294
|
+
## Assumptions
|
|
295
|
+
|
|
296
|
+
- "Serving layout" means the stepper card: the label, − value +, the unit, the quantities grid,
|
|
297
|
+
and the note (Q6).
|
|
298
|
+
- "Summary in an outline at the top" means the summary card: an eyebrow, a title, and a
|
|
299
|
+
bold-label bullet list written in markdown inside `text`. No new outline block.
|
|
300
|
+
- Generating the library is an operator step run once per environment (`bun run photos:generate`). Tests use a fake image model and fake storage, so CI never calls Vertex.
|
|
301
|
+
- "The tables" means the GPT-5.6 "How much lamb?" guests table, plus tables in general (Q7).
|
|
302
|
+
- Copy feedback is inline "Copied" text in a live region, as in the demo, not a toast.
|
|
303
|
+
- iOS and Android render through the same `BlocksView`. Builds already shipped do not know
|
|
304
|
+
the new blocks and would show an error banner, with the raw YAML collapsed, for the whole reply
|
|
305
|
+
that uses them. With the default on
|
|
306
|
+
(D6), a host with such builds must set `richBlocks: false` until they update. X10 (a
|
|
307
|
+
client-declared catalog version) would remove that manual step.
|
|
308
|
+
- The `copy` action never reaches the server, so `hostActions` lint does not apply to it.
|
|
309
|
+
Local checklist ticks (no callback) don't either.
|
|
310
|
+
- Image tiles reuse `useResolvedImages` for `file:` ids.
|
|
311
|
+
- Storage: no new collection. Each stepper tap adds one `AIRequest` row. Like every host
|
|
312
|
+
action today, its prompt holds only ids; this adds the numeric `value`. Unlike other host
|
|
313
|
+
actions, the returned block is left out (`logResponse: false`), because an agent can put
|
|
314
|
+
anything in a stepper label.
|
|
315
|
+
- Fixtures under `fixtures/valid/` use `data:image` or `file:` sources, because that loop
|
|
316
|
+
validates without `imageHosts`. Invalid cases that need options go in `lint.test.ts`.
|
|
317
|
+
|
|
318
|
+
## Open questions (recommendation assumed)
|
|
319
|
+
|
|
320
|
+
| ID | Question | Recommendation | Why | If answered differently | Tasks affected |
|
|
321
|
+
| --- | --- | --- | --- | --- | --- |
|
|
322
|
+
| Q3 | Should `@terreno/ai` ship `scaleStepperHostAction`, or should each app write its own handler? | Ship it, opt-in, exported from `@terreno/ai` | Any app gets the roast reply in one line; apps with real logic (prices, stock) register their own `handles: "stepper"` action | Drop the export; example-backend owns the handler | T1, T16 |
|
|
323
|
+
| Q4 | Should + / − send one callback per tap, or batch taps (300 ms debounce, then one call)? | One per tap, with the buttons disabled while pending | Matches the existing callback buttons; simplest to prove | Add a debounce and send the final value | T1 |
|
|
324
|
+
| Q5 | Should `gallery` take a `layout` (`collage`, `grid`, `carousel`)? | No: one adaptive layout | Fewer choices for the model; the demo uses one row | Add the enum and two more renderer branches | T6, T11 |
|
|
325
|
+
| Q6 | Does "serving layout" mean the stepper card (− 5 +, People, quantities grid)? | Yes | It sits between the stepper and the copy button in the demo | Name what it is; likely one more block or field | T1 |
|
|
326
|
+
| Q7 | Does "the tables" mean the GPT-5.6 "How much lamb?" table (guests → lamb, potatoes)? | Yes: as a `table` block with typed columns, plus themed markdown tables | It is the only table in the post; the GPT-6 reply shows quantities as a grid | Name the table and what it should do (sort, scale with the stepper) | T3, T4, T15 |
|
|
327
|
+
|
|
328
|
+
## Expansions (follow up later)
|
|
329
|
+
|
|
330
|
+
| ID | Idea | Why it came up | Rough size | Depends on |
|
|
331
|
+
| --- | --- | --- | --- | --- |
|
|
332
|
+
| X1 | Generated tools in chat (savings calculator, bill splitter, game) | The post's "Create interactive experiences" section | L | a state + formulas design, or more host callbacks |
|
|
333
|
+
| X2 | Interactive diagrams ("change an input to see what happens") | The post's central limit theorem example | L | X1 |
|
|
334
|
+
| X3 | `map` block with stops and notes | The post's road-trip example | M | none |
|
|
335
|
+
| X4 | Full eval that the model picks these blocks for planning questions (beyond the T17 smoke) | The post trains the model's design judgment | M | T17 |
|
|
336
|
+
| X5 | Copy button on markdown code fences | A common chat copy target; not in the demo | S | T14 |
|
|
337
|
+
| X6 | Sortable table columns | `DataTable` supports `sortable` already | S | T3 |
|
|
338
|
+
| X7 | Write `blocks.json` to `blocks/schemas/` for native clients | `blocksJsonSchema` exists but is not published there | S | T9 |
|
|
339
|
+
| X8 | Streaming inside a block (partial lists) | The post's progressive compiler | M | none |
|
|
340
|
+
| X9 | Persist overrides (if Q2 stays no) | A reload resets the stepper | M | Q2 |
|
|
341
|
+
| X11 | Generate images per reply (a `generatePhoto` tool) | D3 picked a stored library instead | M | T19 |
|
|
342
|
+
| X10 | A client-declared catalog version, so the server picks blocks per client | Shipped native builds lag behind the server; more pressing now that D6 turns the blocks on by default | M | T2 |
|
|
343
|
+
|
|
344
|
+
## Decisions
|
|
345
|
+
|
|
346
|
+
| ID | Question asked | Answer | What it changes |
|
|
347
|
+
| --- | --- | --- | --- |
|
|
348
|
+
| D1 | How should the agent express the +/− servings interaction: fixed block types computed on the device, shared state with formulas, or scripted html? | "When they press the button it should round trip to the server for it to process the function callback. That's a callback button like the others we have." | `stepper` − / + are host callbacks through `POST /gpt/actions`; the server returns the replaced block. No formulas, no scripted html. |
|
|
349
|
+
| D2 | Q1: Should checklist ticks also round-trip to the server, like the stepper? | "Callback per tick" | `checklist.callback`, `toggleChecklistHostAction`, and T21. Ticks wait for the server; without a registered checklist action they stay local. |
|
|
350
|
+
| D3 | Q8: Where should a real agent get photos for the gallery and menu list? | First "Generated images", then: "change q8 to generate some images and store them" | A library generated once with the Vertex image model and stored through `FileStorageService` (T19). The agent picks with `findPhotos` and writes `file:` ids, resolved by `GET /photoLibrary/:id/url` (T20). No per-reply generation and no `imageHosts`. |
|
|
351
|
+
| D4 | Q2: Should stepper and checklist changes survive a page reload? | "No (Recommended)" | Overrides stay in client memory. X9 stays parked. |
|
|
352
|
+
| D5 | Approve the plan? | "Approve (Recommended)", given with D2–D4 | Status set to approved with these answers applied. |
|
|
353
|
+
| D6 | (unprompted) | "enable rich blocks by default" | `uiBlocks.richBlocks` and `blocksPromptSection` default to `true`; `false` opts out. AC3 now pins the `false` case to `master` and checks the default. The changelog flags the behaviour change. The stepper still needs a `handles: "stepper"` action. |
|
|
354
|
+
| D7 | (review of the built PR) | "in blocks playground, ensure every kind of block is available. also the serving callback doesn't work. if it cant work, wire up toasts or something. flip the checkboxes to be bold title, then time, then description. none of the images render." | The playground offers a preset per block type and an "All blocks" preset. The stepper and checklist callbacks run locally there through pure `scaleStepperBlock` / `applyChecklistState` helpers moved into `@terreno/blocks` (the host actions call the same helpers); other actions show a toast. Checklist rows read bold title, time, description. Demo stories and presets use bundled public-domain food photos through `file:` ids instead of colour swatches. Tasks T22–T25. |
|
|
355
|
+
|
|
356
|
+
## Sign-off
|
|
357
|
+
|
|
358
|
+
Status: approved 2026-10-07 (chat; answers in D2–D6). Built 2026-10-08; AC12 partly met (see AC table).
|
|
359
|
+
Cut: 2 rounds, 23 findings: 21 fixed, 2 moved to questions or expansions (round 1 F4 → Q7, round 2 F2 → Q8, since answered as D3), 0 rebutted. Sign-off answers changed T5, T10, T16–T21; no cut re-run, since less than half the tasks changed and the tracer did not move
|
|
@@ -10,7 +10,7 @@ Technical reference for Terreno packages and APIs. Information-oriented, precise
|
|
|
10
10
|
- [@terreno/ai](ai.md) — AI service, GPT routes, Langfuse integration
|
|
11
11
|
- [@terreno/ai/harness](ai-harness.md) — Durable multi-phase tasks with transactional checkpoints and audit spans
|
|
12
12
|
- [Agent UI Asks](agent-ui-asks.md) — `@terreno/blocks` ask schemas, simple cards, the compact surface, limits, error codes, SSE events, the headless `pendingAsks` and `turn` endpoints, and JSON Schemas for native clients
|
|
13
|
-
- [UI blocks](blocks.md) — whole-reply YAML for leaf and
|
|
13
|
+
- [UI blocks](blocks.md) — whole-reply YAML for leaf, layout, and interactive blocks (stepper, checklist, gallery, list, copy): grammar, limits, and error codes
|
|
14
14
|
- [@terreno/admin-backend](admin-backend.md) — Auto-generated admin CRUD endpoints
|
|
15
15
|
- [@terreno/admin-frontend](admin-frontend.md) — Admin panel UI components
|
|
16
16
|
- [@terreno/admin-spa](admin-spa.md) — Standalone admin SPA + Express serve plugin
|
|
@@ -259,6 +259,32 @@ status. `AuditEvent` itself is never audited.
|
|
|
259
259
|
|
|
260
260
|
**Important:** Only expose models that should be editable via admin panel. Avoid sensitive internal models.
|
|
261
261
|
|
|
262
|
+
## Document storage
|
|
263
|
+
|
|
264
|
+
`DocumentStorageApp` mounts a GCS file browser at `basePath` (default `/documents`): list, upload, download, create folder, and delete.
|
|
265
|
+
|
|
266
|
+
| Option | Default | Notes |
|
|
267
|
+
| --- | --- | --- |
|
|
268
|
+
| `bucketName` | `GCS_BUCKET` | 503 `Storage not configured` when neither is set |
|
|
269
|
+
| `folderPrefix` | `""` | Root inside the bucket. A non-empty value without a trailing `/` gets one, so `pr-5` and `pr-5/` both scope to `pr-5/` |
|
|
270
|
+
| `access` | `"admin"` | `"authenticated"` also admits signed-in non-admins, confined to `{folderPrefix}users/<userId>/`. Admins always see the whole `folderPrefix` |
|
|
271
|
+
| `uploadRateLimit` | off | `{max, windowMs, store?, keyBy?}` on `POST basePath/` only. Keys by IP by default. See [Rate limiting](../how-to/rate-limiting.md#limit-one-route) |
|
|
272
|
+
| `allowedMimeTypes`, `maxFileSize` | images, PDF, text, Office; 10 MB | Upload filter |
|
|
273
|
+
| `fileUploadsEnabled` | `true` | `false` or `(req) => boolean` rejects uploads; list, download, delete stay on |
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
new DocumentStorageApp({
|
|
277
|
+
access: "authenticated",
|
|
278
|
+
basePath: "/documents",
|
|
279
|
+
bucketName: process.env.GCS_BUCKET ?? "",
|
|
280
|
+
uploadRateLimit: {max: 1, store: "mongo", windowMs: 60_000},
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Pair it with `DocumentStorageBrowser` from `@terreno/admin-frontend`. Pass `backButton={false}` when the browser is embedded outside admin. Upload failures (including 429) show inline.
|
|
285
|
+
|
|
286
|
+
Example-backend deploys set `GCS_BUCKET` to the example documents bucket. PR previews also set `GCS_FOLDER_PREFIX` to `pr-<number>/`. Unset `GCS_BUCKET` is the only path that returns 503 `Storage not configured`. A bucket GCS reports as missing lists as an empty folder.
|
|
287
|
+
|
|
262
288
|
## Best Practices
|
|
263
289
|
|
|
264
290
|
- Add `description` to all model fields — flows through to admin UI
|
|
@@ -240,6 +240,21 @@ const syncConflicts = useConflicts();
|
|
|
240
240
|
|
|
241
241
|
### AdminRolesList
|
|
242
242
|
|
|
243
|
+
Role cards and the **Available permissions** list split permissions into two groups:
|
|
244
|
+
|
|
245
|
+
1. **Custom permissions**: actions an app or plugin declared, such as `admin:runScripts` or
|
|
246
|
+
`user:impersonate`.
|
|
247
|
+
2. **Model permissions**: the actions `modelRouter` and admin model access check: `create`,
|
|
248
|
+
`list`, `read`, `update`, `delete`, `write`, and `writeOwned`.
|
|
249
|
+
|
|
250
|
+
A resource with both kinds of action appears in both groups. Each group shows one row per
|
|
251
|
+
resource, with that resource's actions as badges. Badge color shows the kind of access:
|
|
252
|
+
|
|
253
|
+
- Green: read-only actions (`read`, `list`, `access`, `view*`)
|
|
254
|
+
- Red: destructive actions (`delete`, `destroy`, `remove`)
|
|
255
|
+
- Amber: every other action. Custom actions (such as screen gates like `adminScreen:syncLab`)
|
|
256
|
+
fall here because the screen cannot tell from the name whether they only read data
|
|
257
|
+
|
|
243
258
|
Role editing starts with a dedicated **Admin page** toggle for `admin:access`. That is the only
|
|
244
259
|
permission that opens the admin panel. Grant it first; model and tool permissions do nothing until
|
|
245
260
|
the role can enter.
|