@terreno/mcp 57.9.0 → 57.10.1

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 (95) hide show
  1. package/dist/docs/upgrades/57.10.1.md +54 -0
  2. package/dist/docs/versioned/next/README.md +1 -0
  3. package/dist/docs/versioned/next/explanation/README.md +4 -0
  4. package/dist/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  5. package/dist/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  6. package/dist/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  7. package/dist/docs/versioned/next/explanation/ai-observability.md +101 -0
  8. package/dist/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
  9. package/dist/docs/versioned/next/explanation/example-coverage.md +3 -1
  10. package/dist/docs/versioned/next/explanation/loop-engineering.md +1 -1
  11. package/dist/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
  12. package/dist/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  13. package/dist/docs/versioned/next/explanation/versioning-policy.md +1 -1
  14. package/dist/docs/versioned/next/how-to/README.md +6 -0
  15. package/dist/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  16. package/dist/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  17. package/dist/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  18. package/dist/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  19. package/dist/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
  20. package/dist/docs/versioned/next/how-to/circleci.md +5 -3
  21. package/dist/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  22. package/dist/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  23. package/dist/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  24. package/dist/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  25. package/dist/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  26. package/dist/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  27. package/dist/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  28. package/dist/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  29. package/dist/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  30. package/dist/docs/versioned/next/reference/README.md +3 -0
  31. package/dist/docs/versioned/next/reference/admin-config.md +2 -0
  32. package/dist/docs/versioned/next/reference/admin-frontend.md +216 -0
  33. package/dist/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  34. package/dist/docs/versioned/next/reference/ai-harness.md +1881 -0
  35. package/dist/docs/versioned/next/reference/ai.md +251 -25
  36. package/dist/docs/versioned/next/reference/api.md +46 -0
  37. package/dist/docs/versioned/next/reference/blocks.md +178 -0
  38. package/dist/docs/versioned/next/reference/environment-variables.md +1 -0
  39. package/dist/docs/versioned/next/reference/mcp-server.md +17 -0
  40. package/dist/docs/versioned/next/reference/test.md +11 -2
  41. package/dist/docs/versioned/next/reference/ui.md +325 -1
  42. package/dist/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  43. package/dist/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  44. package/dist/docs/versioned/next/tasks/ai-observability.md +233 -104
  45. package/dist/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
  46. package/dist/tools.d.ts +4 -0
  47. package/dist/tools.d.ts.map +1 -1
  48. package/dist/tools.js +37 -0
  49. package/dist/tools.js.map +1 -1
  50. package/package.json +3 -2
  51. package/src/docs/upgrades/57.10.1.md +54 -0
  52. package/src/docs/versioned/next/README.md +1 -0
  53. package/src/docs/versioned/next/explanation/README.md +4 -0
  54. package/src/docs/versioned/next/explanation/admin-plugin-frontend.md +2 -2
  55. package/src/docs/versioned/next/explanation/agent-ui-asks.md +299 -0
  56. package/src/docs/versioned/next/explanation/agent-ui-blocks.md +67 -0
  57. package/src/docs/versioned/next/explanation/ai-observability.md +101 -0
  58. package/src/docs/versioned/next/explanation/durable-agent-harness.md +531 -0
  59. package/src/docs/versioned/next/explanation/example-coverage.md +3 -1
  60. package/src/docs/versioned/next/explanation/loop-engineering.md +1 -1
  61. package/src/docs/versioned/next/explanation/roadmap-seed-issues.md +29 -1
  62. package/src/docs/versioned/next/explanation/typescript-configuration.md +1 -1
  63. package/src/docs/versioned/next/explanation/versioning-policy.md +1 -1
  64. package/src/docs/versioned/next/how-to/README.md +6 -0
  65. package/src/docs/versioned/next/how-to/agent-ui-asks.md +730 -0
  66. package/src/docs/versioned/next/how-to/agent-ui-blocks.md +104 -0
  67. package/src/docs/versioned/next/how-to/ai-feature-development.md +156 -0
  68. package/src/docs/versioned/next/how-to/build-a-durable-workflow.md +232 -0
  69. package/src/docs/versioned/next/how-to/charts-and-dashboards.md +2 -0
  70. package/src/docs/versioned/next/how-to/circleci.md +5 -3
  71. package/src/docs/versioned/next/how-to/observe-llm-calls.md +197 -0
  72. package/src/docs/versioned/next/how-to/run-tests-locally.md +2 -1
  73. package/src/docs/versioned/next/how-to/ship-a-new-task-version.md +76 -0
  74. package/src/docs/versioned/next/how-to/upgrade-terreno.md +1 -1
  75. package/src/docs/versioned/next/implementationPlans/agent-ui-asks.md +688 -0
  76. package/src/docs/versioned/next/implementationPlans/agent-ui-blocks.md +10 -6
  77. package/src/docs/versioned/next/implementationPlans/ai-observability.md +475 -157
  78. package/src/docs/versioned/next/implementationPlans/durable-agent-harness.md +340 -0
  79. package/src/docs/versioned/next/implementationPlans/terreno-langfuse-integration.md +1 -1
  80. package/src/docs/versioned/next/reference/README.md +3 -0
  81. package/src/docs/versioned/next/reference/admin-config.md +2 -0
  82. package/src/docs/versioned/next/reference/admin-frontend.md +216 -0
  83. package/src/docs/versioned/next/reference/agent-ui-asks.md +1059 -0
  84. package/src/docs/versioned/next/reference/ai-harness.md +1881 -0
  85. package/src/docs/versioned/next/reference/ai.md +251 -25
  86. package/src/docs/versioned/next/reference/api.md +46 -0
  87. package/src/docs/versioned/next/reference/blocks.md +178 -0
  88. package/src/docs/versioned/next/reference/environment-variables.md +1 -0
  89. package/src/docs/versioned/next/reference/mcp-server.md +17 -0
  90. package/src/docs/versioned/next/reference/test.md +11 -2
  91. package/src/docs/versioned/next/reference/ui.md +325 -1
  92. package/src/docs/versioned/next/tasks/agent-ui-asks.md +213 -0
  93. package/src/docs/versioned/next/tasks/agent-ui-blocks.md +2 -1
  94. package/src/docs/versioned/next/tasks/ai-observability.md +233 -104
  95. package/src/docs/versioned/next/tasks/durable-agent-harness.md +183 -0
@@ -0,0 +1,178 @@
1
+ # UI blocks
2
+
3
+ `@terreno/blocks` checks the YAML (or JSON) document an agent uses when a chat reply is
4
+ components instead of prose. Ask schemas are in [Agent UI Asks](agent-ui-asks.md). Validate a
5
+ file locally with [the blocks how-to](../how-to/agent-ui-blocks.md).
6
+
7
+ ## Document
8
+
9
+ A reply is one mapping. Keys, when present, are in this order: `v`, `datasets`, `blocks`.
10
+
11
+ ```yaml
12
+ v: 1
13
+ blocks:
14
+ - type: text
15
+ markdown: I could not find any signups for that range.
16
+ ```
17
+
18
+ `parseBlocks` strips one surrounding ` ```yaml ` fence, parses YAML 1.2 core schema or JSON,
19
+ and rejects anchors, aliases, and explicit tags (`YAML_FEATURE_DISALLOWED`). A value that is
20
+ not a mapping with `v` is `NOT_A_DOCUMENT`. Callers display that reply with
21
+ `wrapAsTextDocument`, which builds `{v: 1, blocks: [{type: text, markdown}]}`.
22
+
23
+ ## Datasets
24
+
25
+ A dataset name matches `^[a-z][a-z0-9_]{0,63}$`. A document has at most 8 datasets.
26
+
27
+ | Source | Fields | Rule |
28
+ | --- | --- | --- |
29
+ | inline | `columns`: `{name, type}` where `type` is `string`, `number`, or `date`; `rows`: arrays | At most 500 rows and 12 columns. Each row has one value per column. `source: inline` is optional. |
30
+ | ref | `source: ref`, `id`, optional `grain` (`hour`, `day`, `week`, `month`), optional `limit` | `limit` above 1,000 is `TOO_MANY_POINTS`. Set `grain` to a coarser bucket. Column checks run when `knownDatasets` is passed. |
31
+
32
+ ## Charts and tables
33
+
34
+ | Block | Required | Optional |
35
+ | --- | --- | --- |
36
+ | `chart` | `kind`: `line`, `bar`, `area`, `donut`, and either `data` + `x` + `y` or `points` (`{label, value}`) | `id`, `title`, `legend`, `emptyText`, `height`: `sm`, `md`, `lg` |
37
+ | `table` | `data` (dataset name) | `id`, `columns` (names), `title` |
38
+
39
+ `x` is a string or date column. `y` is a number column. A table lists at most 12 columns.
40
+
41
+ ## HTML
42
+
43
+ | Block | Required | Optional |
44
+ | --- | --- | --- |
45
+ | `html` | `html` (at most 100,000 bytes) | `id`, `title`, `height`: `sm`, `md`, `lg` |
46
+
47
+ `validateBlocks` returns `HTML_DISABLED` unless `options.allowHtml` is true. A host turns that on with `uiBlocks.html`. The server sanitizes the HTML before it is stored. The client renders it only when `allowHtml` is set.
48
+
49
+ ## Callout, image, and details
50
+
51
+ | Block | Required | Optional |
52
+ | --- | --- | --- |
53
+ | `callout` | `text` | `id`, `status`: `info`, `warning`, `alert` |
54
+ | `image` | `alt`, `src` | `id` |
55
+ | `details` | `title`, `text` | `id` |
56
+
57
+ `src` is a `data:image` URL, a `file:` ref (`file:` plus an id), or an `https` URL. `https` is allowed only when the hostname is in `validateBlocks` `imageHosts` (the server option is `uiBlocks.imageHosts`). Anything else is `IMAGE_HOST_NOT_ALLOWED`. An empty list rejects every `https` image. A ref dataset `id` uses 1–80 letters, digits, underscores, or hyphens. A block or action-element `id` stays lowercase and starts with a letter. `limit` of `0` is `INVALID_TYPE` (an integer of at least 1), not `TOO_SHORT`.
58
+
59
+ Warnings do not block rendering:
60
+
61
+ | Code | When |
62
+ | --- | --- |
63
+ | `BAR_TOO_MANY_CATEGORIES` | A bar chart would draw more than 60 categories. |
64
+ | `DONUT_TOO_MANY_SLICES` | A donut chart would draw more than 8 slices. |
65
+ | `LINE_SINGLE_POINT` | A line chart has one point. |
66
+
67
+ ## Blocks
68
+
69
+ | Block | Required | Optional | Renders as |
70
+ | --- | --- | --- | --- |
71
+ | `heading` | `text` (1–200) | `size`: `sm`, `md`, `lg`, `xl`, `2xl`; `id` | `Heading` |
72
+ | `text` | `markdown` (1–4,000) | `id` | `MarkdownView` |
73
+ | `metric` | `label` (1–80), `value` (1–80) | `delta` (1–40), `trend`: `up`, `down`, `flat`, `helper` (1–120), `id` | `Card` |
74
+ | `badge` | `text` (1–80) | `status`: `info`, `error`, `warning`, `success`, `neutral`, `active`; `id` | `Badge` |
75
+ | `divider` | — | `id` | `SectionDivider` |
76
+ | `context` | `text` (1–280) | `id` | `Text` |
77
+ | `columns` | `children`: 2–4 blocks | `id` | `Box` row |
78
+ | `card` | `children`: at least 1 block | `title` (1–120), `id` | `Card` |
79
+
80
+ `columns` and `card` sit at the top level. Their children are leaf blocks. A layout block
81
+ inside another layout block is `DEPTH_EXCEEDED`. Every block counts toward the 50-block
82
+ cap, including the `card` or `columns` block itself, so a card cannot hold 50 children.
83
+
84
+ `id` matches `^[a-z][a-z0-9_]{0,63}$`.
85
+
86
+ ## Actions
87
+
88
+ An `actions` block requires `id` and `elements` (1–25). An element is a `button` or a
89
+ `segmented` control.
90
+
91
+ | Element | Required | Optional |
92
+ | --- | --- | --- |
93
+ | `button` | `id`, `text` (1–80), `action` | `variant`: `primary`, `secondary`, `outline`, `ghost`, `destructive`; `iconName` |
94
+ | `segmented` | `id`, `target` (a chart or table `id`), `options` (2–8 of `{label, data}`) | — |
95
+
96
+ `action.kind` is `reply` (`text`), `open` (`url` or `route`, exactly one), `select`
97
+ (`target` plus `data`), or `callback` (`name` plus optional `payload`). A `select` target
98
+ that is not a chart or table `id` is `SELECT_TARGET_INVALID`. When `validateBlocks` is
99
+ called with `hostActions`, a callback `name` outside that list is `UNKNOWN_HOST_ACTION`.
100
+ Omitting `hostActions` skips that check.
101
+
102
+ ## Partial parsing
103
+
104
+ `parseBlocksPartial(text)` never throws. A document that `validateBlocks` accepts returns
105
+ `{datasets, blocks, pending: false}`. A cut reply returns only the top-level blocks whose
106
+ next `- ` already arrived, drops the unfinished tail, and sets `pending: true`. `datasets`
107
+ is present only after the `blocks:` key has started and the prefix parses.
108
+
109
+ ## CLI
110
+
111
+ `terreno-blocks validate <file | ->` prints each problem as `path CODE message — fix` and
112
+ exits 1. A valid document exits 0. Warnings are printed and still exit 0.
113
+
114
+ Unknown fields fail with `UNKNOWN_KEY`. `v` must be `1` (`UNSUPPORTED_VERSION`).
115
+
116
+ ## Limits
117
+
118
+ | Limit | Value |
119
+ | --- | --- |
120
+ | Blocks in one document, including nested blocks | 50 |
121
+ | Nesting | 2 (`blocks` → `columns` or `card` → leaf) |
122
+ | `columns` children | 2–4 |
123
+ | Elements in one `actions` block | 25 |
124
+ | Text in one `text` block | 4,000 characters |
125
+ | Document version | 1 |
126
+
127
+ ## Errors
128
+
129
+ `validateBlocks` returns `{ok: true, doc, warnings}` or `{ok: false, errors, warnings}`.
130
+ Each error is `{path, code, message, fix}`, sorted by path. `warnings` lists chart
131
+ heuristics and does not fail `ok`.
132
+
133
+ | Code | Meaning |
134
+ | --- | --- |
135
+ | `COLUMN_NOT_FOUND` | A chart or table names a column the dataset does not have. |
136
+ | `COLUMN_TYPE_MISMATCH` | A column value, or a chart axis, does not match the column type. |
137
+ | `DATASET_NOT_FOUND` | A chart or table names a dataset the document does not define. |
138
+ | `DATASET_TOO_LARGE` | A dataset has more than 500 rows or 12 columns. |
139
+ | `DEPTH_EXCEEDED` | A `columns` or `card` block is nested inside another layout block. |
140
+ | `DUPLICATE_ID` | A block id, an action-element id, or a column name is used more than once. |
141
+ | `HTML_DISABLED` | An html block is present and this host has not turned HTML on. |
142
+ | `HTML_TOO_LARGE` | An html block is larger than 100,000 bytes. |
143
+ | `IMAGE_HOST_NOT_ALLOWED` | An image URL is not a `data:image` URL, a `file:` ref, or an `https` URL on an allowed host. |
144
+ | `INVALID_ENUM` | A value is not one of the allowed values. |
145
+ | `INVALID_FORMAT` | A string does not match its required format. |
146
+ | `INVALID_TYPE` | A value has the wrong type, including a numeric field below its minimum. |
147
+ | `KEY_ORDER` | Top-level keys are not in the order `v`, `datasets`, `blocks`. |
148
+ | `MISSING_REQUIRED` | A required field is missing. |
149
+ | `NOT_A_DOCUMENT` | The reply is not one YAML or JSON mapping with a `v` field. |
150
+ | `SELECT_TARGET_INVALID` | A select action names a block that is not a chart or table. |
151
+ | `ROW_ARITY_MISMATCH` | A dataset row does not have one value per column. |
152
+ | `TABLE_TOO_WIDE` | A table lists more than 12 columns. |
153
+ | `TOO_FEW` | A list has fewer items than allowed. |
154
+ | `TOO_LONG` | A string is longer than allowed. |
155
+ | `TOO_MANY` | A list has more items than allowed. |
156
+ | `TOO_MANY_BLOCKS` | The document has more than 50 blocks. |
157
+ | `TOO_MANY_POINTS` | A ref dataset `limit` is above 1,000. |
158
+ | `TOO_SHORT` | A string is empty or only whitespace. |
159
+ | `UNKNOWN_HOST_ACTION` | A callback names a host action that is not registered. |
160
+ | `UNKNOWN_KEY` | An object has a field that its schema does not define. |
161
+ | `UNSUPPORTED_VERSION` | `v` is not 1. |
162
+ | `YAML_FEATURE_DISALLOWED` | The document uses a YAML anchor, alias, or explicit tag. |
163
+
164
+ ## Exports
165
+
166
+ | Export | Role |
167
+ | --- | --- |
168
+ | `parseBlocks(text)` | Fence strip, YAML or JSON parse |
169
+ | `parseBlocksPartial(text)` | Completed top-level blocks while a reply is still streaming |
170
+ | `validateBlocks(doc, options?)` | Structure, then dataset, chart, table, action, and html lint. `options.knownDatasets` checks `ref` columns. `options.hostActions` checks callback names. `options.allowHtml` allows `html` blocks. |
171
+ | `wrapAsTextDocument(text)` | Display fallback for a non-document |
172
+ | `blocksSchema` | Zod schema |
173
+ | `blocksJsonSchema` | JSON Schema for the same structure |
174
+ | `blocksPromptSection({hostActions, allowHtml})` | System-prompt section. Limits come from `BLOCK_LIMITS`. `allowHtml` adds the `html` block. |
175
+ | `BLOCK_LIMITS` | The numbers in the table above |
176
+ | `BLOCK_ERROR_CODES` | The codes in the table above |
177
+ | `BLOCK_WARNING_CODES` | `BAR_TOO_MANY_CATEGORIES`, `DONUT_TOO_MANY_SLICES`, `LINE_SINGLE_POINT` |
178
+ | `HTML_HEIGHTS`, `CALLOUT_STATUSES` | Allowed `html` heights and `callout` statuses. `HtmlBlock`, `CalloutBlock`, `ImageBlock`, and `DetailsBlock` are type exports. |
@@ -23,6 +23,7 @@ Comprehensive guide to environment variables used across Terreno packages and ex
23
23
  | `TERRENO_TEST_MONGODB_URI` | `@terreno/test` | ❌ | auto | Yes | tooling |
24
24
  | `TERRENO_TEST_USE_MEMORY_MONGO` | `@terreno/test` | ❌ | — | No | tooling |
25
25
  | `TERRENO_TEST_USE_REPLSET` | `@terreno/test` | ❌ | — | No | tooling |
26
+ | `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` | `@terreno/test` | ❌ | `60000` | No | tooling |
26
27
  | `TERRENO_TEST_USE_FIXTURE_CACHE` | `@terreno/test` | ❌ | — | No | tooling |
27
28
  | `TERRENO_TEST_CACHE_DIR` | `@terreno/test` | ❌ | — | No | tooling |
28
29
  | `BUN_TEST_DISABLE_DB` | test harness | ❌ | — | No | tooling |
@@ -391,6 +391,23 @@ Validate a Mongoose schema against Terreno conventions.
391
391
  - Recommendations for fixes
392
392
  - Severity levels (error, warning, info)
393
393
 
394
+ ### terreno_validate_ui_blocks
395
+
396
+ Check one whole-reply YAML or JSON document. The text is the same report `terreno-blocks validate` prints: each problem is `path CODE message — fix`. A valid document with no warnings returns an empty string.
397
+
398
+ **Parameters:**
399
+
400
+ ``````typescript
401
+ {
402
+ document: string; // Required — the YAML or JSON document
403
+ }
404
+ ``````
405
+
406
+ **Returns:**
407
+
408
+ - The CLI report, including a trailing newline when there is at least one line
409
+ - An empty string when the document is valid and has no warnings
410
+
394
411
  ### terreno_bootstrap_ai_rules
395
412
 
396
413
  Scaffold AI coding assistant rules (AGENTS.md, Cursor/Windsurf rules, Copilot instructions, rulesync config).
@@ -79,10 +79,14 @@ Defaults: `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRE
79
79
  |----------|--------|
80
80
  | `TERRENO_TEST_USE_MEMORY_MONGO` | When `"true"`, `registerSimpleMongoPreload` starts `mongodb-memory-server` if `TERRENO_TEST_MONGODB_URI` is unset. |
81
81
  | `TERRENO_TEST_MONGODB_URI` | External MongoDB URI. Takes priority over in-memory server in `startMongoServer` and `registerSimpleMongoPreload`. |
82
+ | `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` | Per-mongod start timeout for in-memory servers started by `startMongoServer`. Default `60000` (mongodb-memory-server's own default is 10s). Startup is tried twice. |
82
83
  | `BUN_TEST_DISABLE_DB` | When `"true"`, `registerBackendPreload` skips all Mongo `beforeAll`/`afterAll` hooks (no DB startup). |
83
84
 
84
85
  `startMongoServer` also publishes the resolved URI to `TERRENO_TEST_MONGO_URI` and honors `TERRENO_TEST_USE_REPLSET=true` for replica-set memory servers.
85
86
 
87
+ If `registerBackendPreload` cannot start Mongo, every later test fails at once with
88
+ `MongoDB test server failed to start` instead of waiting on mongoose command buffering.
89
+
86
90
  ## Exported helpers
87
91
 
88
92
  | Export | Description |
@@ -92,6 +96,7 @@ Defaults: `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRE
92
96
  | `setTerrenoTestEnv` | Apply and validate auth test env vars. |
93
97
  | `startMongoServer` / `stopMongoServer` | Start or stop shared in-memory Mongo and connect mongoose. |
94
98
  | `getMongoServerUri` | Resolved URI after `startMongoServer`. |
99
+ | `resolveMongoLaunchTimeoutMs` | In-memory mongod launch timeout from `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` (integer ≥ 1000), default 60000. |
95
100
  | `getBaseServer` | Build an Express app for supertest from route registrars. |
96
101
  | `authAsUser` | Login helper returning auth headers for supertest. |
97
102
  | `createMongoTestCache` | Fixture-cache controller for fast `@terreno/api` tests. |
@@ -112,8 +117,12 @@ Defaults: `TOKEN_SECRET`, `TOKEN_ISSUER`, `REFRESH_TOKEN_SECRET`, `SESSION_SECRE
112
117
  Package CI uses `scripts/check-coverage.ts` (`bun run test:coverage`, default 95%
113
118
  functions and lines) as the live gate. Bun 1.4.2+ may exit 1 on bunfig
114
119
  `coverageThreshold` before isolated LCOV merges; the script continues when tests
115
- reported `0 fail` and then enforces 95% on the merged report. Dedicated CircleCI
116
- jobs (`api-ci`, `ai-ci`,
120
+ reported `0 fail` and then enforces 95% on the merged report. The blocks
121
+ coverage pass sets `--max-concurrency=1`, because Bun's default 20-way run
122
+ exits 0 on a 2-vCPU worker before it prints the `All files` row. When that
123
+ row is still missing after one retry, the script uses `coverage/lcov.info`
124
+ only if it lists every non-test source file under `src/`. Dedicated CircleCI
125
+ jobs (`api-ci`, `ai-ci`, `blocks-ci`,
117
126
  `rtk-ci`, `ui-ci`, `syncdb-ci`, `comms-ci`, `mcp-server-ci`, `admin-spa-ci`) run
118
127
  that script. Published packages without a dedicated workflow
119
128
  (`admin-backend`, `admin-frontend`, `api-health`, `feature-flags`, `@terreno/test`)
@@ -10,6 +10,7 @@ React Native UI component library (a large component library). Layout (Box, Page
10
10
  - Actions: `Button`, `IconButton`, `Link`
11
11
  - Feedback: `Spinner`, `Modal`, `Toast`
12
12
  - Notifications: `NotificationBell`, `NotificationInbox`, `NotificationPreferences`
13
+ - AI chat: `GPTChat`, `AskCard` (agent asks in the transcript), `SimpleAskCard` (an ask's simple card for narrow layouts), `BlocksView` (a whole-reply YAML document)
13
14
  - Authentication: `SocialLoginButton`, `LoginScreen`, `SignUpScreen`
14
15
  - Theming: `TerrenoProvider`, `useTheme`, custom icon registry (`icons` prop)
15
16
  - **Type re-exports:** `StyleProp`, `ViewStyle` (re-exported from react-native to avoid version conflicts)
@@ -32,7 +33,7 @@ supported and is convenient when startup cost is not material:
32
33
  import {Box, DataTable, Icon} from "@terreno/ui";
33
34
  ```
34
35
 
35
- Heavy optional widgets (`GPTChat`, `EmojiSelector`, `MarkdownEditor`, consent flows, `ChartCard`, `Scorecard`, `SparklineChart`, `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, and related admin tools) are
36
+ Heavy optional widgets (`GPTChat`, `AskCard`, `BlocksView`, `EmojiSelector`, `MarkdownEditor`, consent flows, `ChartCard`, `Scorecard`, `SparklineChart`, `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, and related admin tools) are
36
37
  re-exported from the root entry through lazy boundaries. Importing them from `@terreno/ui` stays type-compatible, but
37
38
  their implementation modules load on first render instead of during the initial root import. `DashboardGrid` stays eager.
38
39
  `MarkdownView` and `DataTable` header info defer `react-native-markdown-display`;
@@ -250,6 +251,18 @@ Buttons automatically size to their content unless `fullWidth` is specified:
250
251
 
251
252
  Internally, Button sets `alignSelf: 'flex-start'` when `fullWidth={false}` to prevent stretching in column layouts.
252
253
 
254
+ A label stays on one line by default, so a long label can make the button wider than its container.
255
+ Set `wrapText` to keep the button inside its container instead: the label wraps onto centered lines
256
+ and the button grows taller. A `size="sm"` button grows from its 28px height instead of clipping the
257
+ second line. `SimpleAskCard` and `AskCard` set `wrapText` on their answer buttons, because their
258
+ labels come from the agent.
259
+
260
+ ```tsx
261
+ <Box direction="row" width={160}>
262
+ <Button onClick={handleSave} text="Save all changes now" wrapText />
263
+ </Box>
264
+ ```
265
+
253
266
  ### TextField grow and maxHeight
254
267
 
255
268
  `grow` makes a multiline field expand with its content. Pair it with `maxHeight` (pixels) to cap the expansion; past the cap the content scrolls.
@@ -390,10 +403,321 @@ The attach control (`FilePickerButton`) opens an anchored dropdown with **Photo
390
403
  />
391
404
  ```
392
405
 
406
+ Pass `uiBlocks` to render each assistant message through `BlocksView`. A streaming message
407
+ shows each finished top-level block and a spinner for the block still arriving. `reply`
408
+ calls `onSubmit` with the button text. `open` and `select` call `onBlockAction`. `callback`
409
+ calls `onBlockCallback` and the button stays loading until that promise settles. Return
410
+ `{replace: "block", blocks}` to swap that block, or `{text}` to place an assistant message
411
+ immediately after the message that started the callback. A result that arrives after
412
+ `currentHistoryId` changes is dropped. `hostActions` disables callback names the host did
413
+ not list. `resolveDataset` loads `ref` datasets. `resolveImage` turns a `file:` image id
414
+ into a URL.
415
+
416
+ ```tsx
417
+ <GPTChat
418
+ currentMessages={messages}
419
+ histories={histories}
420
+ hostActions={["export_csv"]}
421
+ onBlockAction={handleBlockAction}
422
+ onBlockCallback={handleBlockCallback}
423
+ onCreateHistory={onCreateHistory}
424
+ onDeleteHistory={onDeleteHistory}
425
+ onSelectHistory={onSelectHistory}
426
+ onSubmit={onSubmit}
427
+ uiBlocks
428
+ />
429
+ ```
430
+
393
431
  Operator steps: [Add a GPT chat mascot](../how-to/add-gpt-chat-mascot.md). Demo story: `GPTChat` → `Mascot`.
432
+ Blocks playground: `BlocksPlayground`.
394
433
  The example AI screen demonstrates a consumer selecting one of four bundled mascot
395
434
  images once per mount.
396
435
 
436
+ #### Asks
437
+
438
+ `GPTChat` shows an [agent ask](agent-ui-asks.md) as an `AskCard` in the transcript. Set `ask` on
439
+ the ask's `tool-call` message and pass `onAskSubmit`:
440
+
441
+ ```tsx
442
+ <GPTChat
443
+ askErrors={askErrors}
444
+ currentMessages={currentMessages}
445
+ histories={histories}
446
+ onAskSubmit={handleAskSubmit}
447
+ onCreateHistory={onCreateHistory}
448
+ onDeleteHistory={onDeleteHistory}
449
+ onSelectHistory={onSelectHistory}
450
+ onSubmit={onSubmit}
451
+ />
452
+ ```
453
+
454
+ | Prop or field | Type | Description |
455
+ | --- | --- | --- |
456
+ | `GPTChatMessage.ask` | `ChatAsk` | On a `tool-call` message: the ask (`kind`, `input`), its `toolCallId`, `status` (`pending`, `answered`, or `cancelled`), and optional `response` and `simple` card. The chat shows an `AskCard` instead of the tool call. |
457
+ | `onAskSubmit` | `(submission: {toolCallId, response}) => void \| Promise<void>` | Called when the user answers a pending ask. `response` is the answer envelope. The pressed control shows a loading state until the promise settles. Without it, asks show but cannot be answered. |
458
+ | `askErrors` | `Record<string, AskValidationError[]>` | Errors for the last answer to each ask, keyed by tool call id, such as the `fields` of a 400 `Invalid askResponse`. Shown inside the card. |
459
+ | `resolveAskFiles` | `AskFilesResolver` | Turns the files picked for a `files` ask into the refs its answer sends. Defaults to `resolveAskFilesAsDataUrls`. Pass an uploader to send `{fileId}` refs; see [Accept uploads with or without GCS](../how-to/agent-ui-asks.md#accept-uploads-with-or-without-gcs). |
460
+
461
+ - The ask's `tool-result` message stays in `currentMessages` but is not shown. Keep it: message
462
+ indexes must match the stored `prompts` rows that ratings use. When `ask.response` is unset, the
463
+ card reads the answer from that message.
464
+ - A pending ask takes focus when it appears: DOM focus on the card, labelled with the ask's
465
+ `title`, on web; accessibility focus on the question, an accessibility header, on native.
466
+ - Answered and cancelled asks collapse to a one-line summary.
467
+
468
+ The example AI screen, `example-frontend/app/(tabs)/ai.tsx`, handles the stream events, saved
469
+ rows, answers, and errors. Steps: [Add agent asks to a chat](../how-to/agent-ui-asks.md).
470
+
471
+ ### BlocksView
472
+
473
+ `BlocksView` paints a whole-reply document from `@terreno/blocks`. Pass the assistant text
474
+ as `document`. Leaf blocks in this slice are `heading`, `text`, `metric`, `badge`,
475
+ `divider`, and `context`. `columns` is a row from the `md` breakpoint and a stack on `sm`.
476
+ `card` groups children under an optional title. A `callout` draws `Banner` and is not dismissible. An `image` draws `Image` and requires `alt`; pass `imageHosts` to allow `https` hosts. A `details` block draws `Accordion`. A `chart` draws `LineChart`, `BarChart`,
477
+ `AreaChart`, or `DonutChart` from an inline dataset or from `points`. The series is only
478
+ `{label, value}` — a point color in the document is not passed through. A `table` draws
479
+ `DataTable`. A `ref` dataset stays empty until `resolveDataset` returns its rows, and the
480
+ chart shows `loading` while that promise is in flight. A rejected fetch clears that
481
+ loading state. A segmented control highlights the option whose `data` matches the target
482
+ chart or table. A `select` action switches a table the same way it switches a chart.
483
+ An image `file:` ref is not passed to `Image` until `resolveImage` returns a URL; the
484
+ alt text still shows.
485
+
486
+ A string that is not a document becomes one `text` block. A document that fails
487
+ `validateBlocks` shows a `Banner` with the first three errors and the raw text inside a
488
+ collapsed `Accordion`.
489
+
490
+ An `actions` block draws `Button` and `SegmentedControl`. `onAction` receives
491
+ `{action, blockId, elementId}` for reply, open, select, and callback. A `select`
492
+ action also stores the chosen dataset on the target chart for this view. Pass
493
+ `hostActions` to disable callback buttons whose names are not in that list. Omit
494
+ `hostActions` and every callback stays enabled. `pendingElementIds` shows those
495
+ buttons as loading. `overrides` replaces a block by id.
496
+
497
+ ```tsx
498
+ <BlocksView
499
+ document={reply}
500
+ hostActions={["export_csv"]}
501
+ onAction={handleAction}
502
+ testID="assistant-blocks"
503
+ />
504
+ ```
505
+
506
+ Demo story: `BlocksView`.
507
+
508
+ ### HtmlFrame
509
+
510
+ `HtmlFrame` paints one `html` block. On web it is an `<iframe sandbox="" referrerpolicy="no-referrer">` whose `srcdoc` starts with a Content-Security-Policy meta tag (`default-src 'none'`). On native it is a WebView with JavaScript off, and navigation after the first load is rejected. `BlocksView` renders that frame only when `allowHtml` is true and `streaming` is false. Otherwise the block is a card that says the preview is off or still arriving. `GPTChat` passes `allowHtml` through.
511
+
512
+ ```tsx
513
+ <HtmlFrame height="md" html="<h1>Invoice</h1>" title="Invoice preview" />
514
+ ```
515
+
516
+ ### AskCard
517
+
518
+ One agent ask in a chat transcript: controls while it is pending, a summary line after. `GPTChat`
519
+ renders it for messages with `ask`. Render it directly in a custom transcript. A saved ask whose
520
+ input no longer validates, or whose `accept` answer has no `content` object, shows the generic
521
+ line ("You answered this question.") and no sent values.
522
+
523
+ ```tsx
524
+ <AskCard ask={ask} errors={errors} onSubmit={handleAskSubmit} testID="plan-ask" />
525
+ ```
526
+
527
+ | Prop | Type | Description |
528
+ | --- | --- | --- |
529
+ | `ask` | `ChatAsk` | The ask. Pending asks are interactive. Answered and cancelled asks show a summary. |
530
+ | `errors` | `AskValidationError[]` | Errors for the last answer, shown under the controls |
531
+ | `onSubmit` | `AskSubmitHandler` | Called with `{toolCallId, response}`. Without it, the card cannot be answered: buttons and the select are disabled, and radio and checkbox options show as plain text. When the promise it returns rejects, the ask stays open and the card shows "Your answer could not be sent. Try again." until the next answer. |
532
+ | `promptRef` | `React.Ref<Text>` (React Native) | Receives the question's native text, which is an accessibility header. `GPTChat` uses it to move screen reader focus to a pending ask on native. |
533
+ | `resolveAskFiles` | `AskFilesResolver` | For a `files` ask: turns the picked files into refs on Submit. Defaults to data URLs. `GPTChat` passes its own `resolveAskFiles`. |
534
+ | `testID` | string | Defaults to `ask-card`. `GPTChat` passes `gpt-ask-<toolCallId>`. |
535
+
536
+ A saved ask whose input no longer passes `validateAskInput` shows "This question cannot be shown.
537
+ Send a message to continue." while pending, and "You answered this question." once answered.
538
+
539
+ `choice` controls:
540
+
541
+ | Ask | Controls |
542
+ | --- | --- |
543
+ | The simple card has a button for every option (`handoff: false`), and every option label is at most 20 characters | The card's buttons as quick replies, then Skip unless `allowDecline` is `false`. A tap answers. |
544
+ | Up to 8 options | `RadioField`, then Submit (`submitLabel`) and Skip |
545
+ | More than 8 options | Searchable `SelectField`, then Submit and Skip |
546
+ | `select: "many"` | `MultiselectField` with a hint such as "Choose 1 to 3.", then a `TextField` titled `otherLabel` (default "Other") when `allowOther` is `true`, then Submit and Skip |
547
+
548
+ The selection starts on the ask's `default`. Submit is enabled only when `validateAskResponse`
549
+ accepts the selection. For `select: "many"`, the answer lists the checked ids in option order and
550
+ sends the Other text trimmed, leaving `other` out when the field is blank. When the checked
551
+ options and the Other text add up to more than `maxSelected`, the checkboxes say so ("You chose
552
+ 4. Choose at most 3."). An Other text over 500 characters, or a server error at `content.other`,
553
+ shows on the Other field. When `ask.simple` is absent, the card derives it with `toSimpleCard`. An
554
+ ask whose input fails `validateAskInput` shows "This question cannot be shown. Send a message to
555
+ continue." instead of controls.
556
+
557
+ `confirm` controls: the simple card's two buttons in its order, the approve button (`confirmLabel`,
558
+ default "Confirm") first and the deny button (`denyLabel`, default "Cancel") last. The approve
559
+ button uses the `destructive` variant when the ask sets `destructive: true`, else `primary`; the
560
+ deny button uses `ghost`. Skip follows only when `allowDecline` is `true`. A tap answers
561
+ `{"confirmed": true}` or `{"confirmed": false}`. Without `onSubmit`, both buttons are disabled.
562
+
563
+ `markdown` controls: a `MarkdownEditorField` (edit and preview, at most 320 pt tall) that starts
564
+ on the ask's `initial` draft and shows `placeholder` while empty, then Submit (`submitLabel`) and
565
+ Skip unless `allowDecline` is `false`. Under the editor a hint gives the length and the bounds,
566
+ such as "1,240 / 2,000 characters. At least 20." Submit is enabled only when
567
+ `validateAskResponse` accepts the text, and it sends `{markdown, changed}` with `changed` true
568
+ when the text differs from `initial`. Text over `maxLength`, or a server error at
569
+ `content.markdown`, shows on the editor. Without `onSubmit`, the editor and both buttons are
570
+ disabled. An answered markdown ask shows the sent text under its summary; text over 280
571
+ characters shows a preview cut at a word, with Show all and Show less.
572
+
573
+ `form` controls: one field per entry in `fields`, in order, titled with the field's `label`
574
+ ("(required)" appended for required fields) and its `helperText`, then Submit (`submitLabel`) and
575
+ Skip unless `allowDecline` is `false`. Each field uses the `@terreno/ui` control for its type:
576
+
577
+ | Field `type` | Control | Sends |
578
+ | --- | --- | --- |
579
+ | `text`, `email`, `url` | `TextField` (`text`, `email`, `url`) | The trimmed text |
580
+ | `phone` | `TextField` (`phoneNumber`) | E.164, such as `+14155552671`, when the number parses (as a US number without a country code); else the trimmed text |
581
+ | `number` | `TextField` | A number when the text is plain decimal notation; else the text, so validation says why |
582
+ | `textarea` | `TextArea` | The trimmed text |
583
+ | `date` | `DateTimeField` (`date`) | `YYYY-MM-DD` |
584
+ | `time` | `DateTimeField` (`time`) with its time zone picker (the device's zone to start) | 24-hour `HH:mm` as shown; the zone is not sent |
585
+ | `datetime` | `DateTimeField` (`datetime`) with a time zone picker | An ISO datetime with the chosen zone's offset, such as `2026-10-01T09:30:00-07:00` |
586
+ | `boolean` | `BooleanField` | `true` or `false`, always |
587
+ | `select` | `SelectField` | The option id |
588
+ | `multiselect` | `MultiselectField` | The checked option ids |
589
+
590
+ Fields start on their `default`. Blank fields are left out of `values`. Submit is enabled only
591
+ when `validateAskResponse` accepts the values and no date, time, or datetime field holds an
592
+ unfinished entry. `DateTimeField` reports that through `onEntryStatusChange`, such as
593
+ "0 / 5 / 026": the field says "Enter a complete date, or clear it." until the user finishes it,
594
+ and clearing every part leaves the field out. After the user edits a field, it says what is
595
+ wrong in plain words, such as "Enter a number from 1 to 500." or "This field is required." A
596
+ server error whose path is `content.values.<id>` (or an item under it) shows on that field until
597
+ the user edits it; other errors show under the form. Without `onSubmit`, every field and both
598
+ buttons are disabled. An answered form lists each sent field as its label and a readable value
599
+ (Yes or No, option labels, "Oct 1, 2026", "9:30 AM", text shortened to one line of 80
600
+ characters) under its summary.
601
+
602
+ `files` controls: a hint built from the counts and types, such as "Up to 3 files: images, PDFs or
603
+ text files.", then a "Choose files" `FilePickerButton` ("Add files" once some are picked), the
604
+ picked files as an `AttachmentPreview` with a remove control on each, then Submit (`submitLabel`,
605
+ default "Submit") and Skip unless `allowDecline` is `false`. The picker offers Photo Library only
606
+ when `accept` has `image`, and its document picker offers only the accepted MIME types. It allows
607
+ several files when `maxFiles` is more than 1 and is disabled once `maxFiles` files are picked.
608
+ Submit is enabled only when `validateAskResponse` accepts the picked names, types, and sizes. On
609
+ Submit, the card calls `resolveAskFiles` with the picked files and sends `{files: refs}`; Submit
610
+ shows a spinner and Skip is disabled until the answer is sent. When the ask ends another way while
611
+ the resolver runs, such as an answer from another tab, its refs are not sent. If the resolver throws, the ask
612
+ stays open and the card shows "The files could not be sent. Try again, or pick them again." Server
613
+ errors, such as `MIME_MISMATCH`, show under the picked files. Each file's type is
614
+ `selectedFileMimeType(file)`, so a CSV a picker reports as `application/vnd.ms-excel` is sent as
615
+ `text/csv`.
616
+
617
+ | How the ask ended | Summary |
618
+ | --- | --- |
619
+ | `files` `accept` | You sent `<n>` files: `<filenames>`, or You sent 1 file: `<filename>` |
620
+ | `form` `accept` | You sent the form (`<n>` fields) |
621
+ | `markdown` `accept` | You approved the draft as is (`changed: false`), or You edited the draft (`<n>` characters) |
622
+ | `confirm` `accept` | You confirmed: `<confirmLabel>`, or You declined: `<denyLabel>`, with the default labels when the ask sets none |
623
+ | `choice` `accept` | You chose: `<option labels>`. With Other text, it adds "`<otherLabel>`: `<text>`" (the label defaults to "Other"), or shows only that when no option was checked. An empty `select: "many"` answer shows "You chose none of the options." |
624
+ | `decline` | You skipped this question. |
625
+ | `cancel` with `user_sent_message` | Not answered: you sent a message instead. |
626
+ | `cancel` with `one_ask_at_a_time` | Not asked: the assistant asked another question first. |
627
+ | Any other `cancel`, or status `cancelled` without a response | This question was cancelled. |
628
+ | Status `answered` without a response | You answered this question. |
629
+
630
+ | Element | testID |
631
+ | --- | --- |
632
+ | Card | `{testID}` |
633
+ | Quick reply row | `{testID}-quick-replies` |
634
+ | Quick reply or Skip button | `{testID}-button-<button id>`, such as `{testID}-button-option:team` or `{testID}-button-skip` |
635
+ | Confirm button row | `{testID}-confirm-buttons` |
636
+ | Confirm approve or deny button | `{testID}-button-approve`, `{testID}-button-deny` |
637
+ | Radio options (up to 8) | `{testID}-radio` |
638
+ | Select | `{testID}-select` |
639
+ | Checkboxes (`select: "many"`) | `{testID}-multiselect` |
640
+ | Other text field | `{testID}-other` |
641
+ | Markdown editor, and its text input | `{testID}-editor`, `{testID}-editor-input` |
642
+ | Sent markdown under an answered ask, and its Show all toggle | `{testID}-answer`, `{testID}-answer-toggle` |
643
+ | Form field, and its wrapper | `{testID}-field-<field id>` (a `BooleanField` switch is `{testID}-field-<field id>.switch`), `{testID}-form-field-<field id>` |
644
+ | Sent form values under an answered ask | `{testID}-answer` |
645
+ | Files hint, picker, and picked files | `{testID}-hint`, `{testID}-picker`, `{testID}-selected` |
646
+ | Files that could not be sent | `{testID}-resolve-error` |
647
+ | Radio or checkbox options as plain text, without `onSubmit` | `{testID}-options` |
648
+ | Submit | `{testID}-submit` |
649
+ | Answer errors | `{testID}-errors` |
650
+ | Summary | `{testID}-summary` |
651
+ | Ask that cannot be shown | `{testID}-invalid` |
652
+ | Answer that could not be sent | `{testID}-submit-error` |
653
+
654
+ Types: `AskCardProps`, `ChatAsk`, `ChatAskState`, `ChatAskStatus`, `AskSubmission`,
655
+ `AskSubmitHandler`, `AskFilesResolver`. Demo story: `AskCard`.
656
+
657
+ File ref helpers:
658
+
659
+ | Export | Description |
660
+ | --- | --- |
661
+ | `AskFilesResolver` | `(files: SelectedFile[]) => Promise<AskFileRef[]>`. Throw to keep the ask open. |
662
+ | `resolveAskFilesAsDataUrls` | The default resolver: every file as a `{url}` data URL |
663
+ | `selectedFileToDataUrlRef(file)` | One picked file as `{filename, mimeType, size, url}`. The data URL's media type is `selectedFileMimeType(file)`, and `size` is the decoded byte count. |
664
+ | `selectedFileMimeType(file)` | The picked file's normalized type. When the picker reports none, `application/octet-stream`, or `application/vnd.ms-excel` (Windows reports CSV files so), the type comes from the extension: `.csv`, `.gif`, `.jpeg`, `.jpg`, `.json`, `.pdf`, `.png`, `.txt`, `.webp`. Otherwise the reported type is kept. |
665
+ | `normalizeMimeType(mimeType)` | Drops parameters and lowercases: `Text/CSV; charset=utf-8` becomes `text/csv` |
666
+
667
+ `FilePickerButton` props used by the files card, also available to any caller:
668
+
669
+ | Prop | Type | Description |
670
+ | --- | --- | --- |
671
+ | `documentTypes` | `string[]` | MIME types the document picker offers. Defaults to PDF, text, CSV, and JSON. An empty list hides Document. |
672
+ | `includeImages` | boolean | Offer Photo Library. Defaults to `true`. When `false`, the button opens the document picker directly. |
673
+ | `text` | string | Shows an outline button with this label instead of the paperclip icon |
674
+
675
+ `SelectedFile.size` is the file's size in bytes when the picker reports it.
676
+
677
+ ### SimpleAskCard
678
+
679
+ Any agent ask as its [simple card](agent-ui-asks.md#simple-cards): the title, the question, and up
680
+ to three full-width buttons that each send an exact answer. Use it in narrow layouts, such as a
681
+ watch-sized preview. It reads the card's `kind` only to word the handoff line.
682
+
683
+ ```tsx
684
+ const [runTurn] = useGpthistoriesTurnMutation();
685
+
686
+ <SimpleAskCard
687
+ card={pendingAsk.simple}
688
+ onPress={(button) =>
689
+ runTurn({
690
+ body: {buttonId: button.id, surface: "compact", toolCallId: pendingAsk.toolCallId},
691
+ id: historyId,
692
+ })
693
+ }
694
+ pendingButtonId={sendingButtonId}
695
+ />
696
+ ```
697
+
698
+ | Prop | Type | Description |
699
+ | --- | --- | --- |
700
+ | `card` | `SimpleCard` | `pendingAsk.simple` from the server, or `simple` on an `{ask}` event |
701
+ | `onPress` | `(button: SimpleCardButton) => void \| Promise<void>` | Called with the pressed button. Send `{toolCallId: card.toolCallId, buttonId: button.id}` to the history's [`turn` action](agent-ui-asks.md#headless-endpoints), or send `button.response` as the answer. |
702
+ | `pendingButtonId` | string? | The button whose answer is still sending. It shows a spinner, the other buttons are disabled, and presses are ignored. |
703
+ | `testID` | string? | Defaults to `simple-ask-card` |
704
+
705
+ - A card with `handoff: true` shows "Continue on your phone" under the question, because its
706
+ buttons cannot give every answer. A `markdown` card shows "Edit on your phone" instead, a
707
+ `form` card "Fill it in on your phone", and a `files` card "Upload on your phone".
708
+ - Button styles map to `Button` variants the same way as `AskCard` quick replies: `primary` to
709
+ `primary`, `default` to `outline`, `destructive` to `destructive`, and `cancel` to `ghost`.
710
+
711
+ | Element | testID |
712
+ | --- | --- |
713
+ | Card | `{testID}` |
714
+ | Continue on your phone, or Edit on your phone | `{testID}-handoff` |
715
+ | Button | `{testID}-button-<button id>`, such as `{testID}-button-option:team` |
716
+
717
+ Types: `SimpleAskCardProps`, and `SimpleCard` and `SimpleCardButton` from `@terreno/blocks`. Demo
718
+ story: `SimpleAskCard`. Its demo answers a plan card on a watch-sized 198×242 pt screen, and its
719
+ "Every fixture" story draws every valid fixture's card at that size.
720
+
397
721
  ### SplitPage
398
722
 
399
723
  Master-detail layout. Pass `listViewData` plus `renderListViewItem` for the list, and