@lotics/ui 11.4.0 → 11.6.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.
@@ -0,0 +1,374 @@
1
+ # AI patterns — AI proposes, the human decides
2
+
3
+ The AI surfaces of `@lotics/ui`: the command composer, the live run feed, the review-before-apply
4
+ family, findings, clarification, provenance, confidence — how to compose them into a loop, which
5
+ surface fits which outcome, the proven screen shapes, and the visual vocabulary AI output uses.
6
+ Read this before building any AI-driven app screen. **This doc is the UI half of a pair**: the SDK
7
+ half — declaring and running agents (`useAgentRun`, `askAi`, sessions, the authority model) — is
8
+ [`@lotics/app-sdk` docs/ai.md](../../app-sdk/docs/ai.md); that doc feeds the data, this one renders
9
+ it. Exact prop APIs are the shipped sources — `../src/<name>.tsx` (never guess a prop; open the
10
+ file). Every component here is in the [catalog](./catalog.md) inventory.
11
+
12
+ ## The one law
13
+
14
+ The AI surfaces share ONE law: the agent never commits — it **proposes**, the human
15
+ accepts / edits / dismisses, the deterministic app applies. The agent owns judgment
16
+ (recognition, estimation, intent→parameters); the app owns geometry, math, and the write.
17
+ Compose the surfaces as a loop, and reach for the right one by job.
18
+
19
+ ## Which AI surface — decide by the OUTCOME's shape, not the task's topic
20
+
21
+ Three shapes:
22
+
23
+ 1. **Field writes** (extract, match, rank, classify → the record changes) → **in-app agent**
24
+ (`useAgentRun`) + **`ChangeReview`**. The commits must obey the diff law (Keep/Drop per
25
+ change, app-workflow writes, bounded app authority), and the operator wants one button +
26
+ a review — never a prompt box.
27
+ 2. **Evidence for an in-app decision** (cross-check, audit, tie-out → nothing is written; the
28
+ operator acts on what was found) → **in-app agent** + **`Finding`**. Still bounded and
29
+ prompt-free (an optional instructions brief at most): the brief is fixed, the output is
30
+ structured display-only findings read IN the record's context, and there is no conversation
31
+ to have — the loop closes when the operator acts in the app. Chat would add prompting and
32
+ tear the findings away from the record they judge.
33
+ 3. **A file or an open-ended answer** (edit this document, draft from context, explain) →
34
+ **hand off to the chat agent** (`askAi` in `@lotics/app-sdk` — see
35
+ [the SDK's fields-vs-file razor](../../app-sdk/docs/ai.md)). The loop is multi-turn with
36
+ no output schema, judged by looking — and the chat harness already owns it: preview beside
37
+ the thread, version chains, branching, session memory. An in-app "edit chat" would
38
+ re-implement all of that inside every app.
39
+
40
+ The Document desk's Use-AI fork IS this table as UI: Extract data (1) · Cross-check (2) ·
41
+ Edit with AI (3) — one entry point, three outcome shapes
42
+ ([`tpl_documents`](../examples/tpl_documents.tsx)).
43
+
44
+ ## Command / compose — `Composer`
45
+
46
+ `Composer` (`@lotics/ui/composer`) is the adaptive command surface. Empty + minimal (no
47
+ `footerRight` model picker, no `pills`, no `files`, no `children`), it's a COMPACT PILL: an
48
+ optional `actionsButton` (attach) left, the input, a send arrow right (Enter sends, Shift+Enter
49
+ for a newline) — the same geometry `AgentProgress` morphs into. Type past one line, or add
50
+ files/pills/footer chrome, and it EXPANDS: the input lifts onto its own row, the `files` slot
51
+ (`FileThumbnail`s, each `onRemove`-able) and `pills` stack above, and the buttons drop to a
52
+ footer row. The expansion latches until the text is fully cleared — blank lines (Shift+Enter)
53
+ keep it expanded; Send still gates on non-whitespace text.
54
+
55
+ Key props (full API: `../src/composer.tsx`):
56
+
57
+ | Prop | What it does |
58
+ |---|---|
59
+ | `onSend(text)` | Fires on Enter / the send button; clears the input. The host holds any uploaded file id itself |
60
+ | `onStop` | With `disabled`, swaps the send button for a Stop button |
61
+ | `sendDisabled` | Overrides the default block-on-empty-text gate — set it to allow a files-only send, or to require an attachment |
62
+ | `value` / `onChangeText` | Optional controlled text; omit for internal state |
63
+ | `pills` / `files` / `children` | Slots stacked above the input; any of them forces the expanded layout |
64
+ | `actionsButton` | Attach/actions button — inline-left in the pill, footer-left expanded |
65
+ | `footerRight` | Footer-right content before Send (a model picker); forces expanded |
66
+ | `maxLines` | Expanded growth cap before the input scrolls (default 10) |
67
+ | `sendLabel` / `stopLabel` | Accessible/tooltip labels (default English "Send"/"Stop" — pass translations) |
68
+ | `textInputProps` | Extra `TextInput` props; a consumer `onKeyPress` runs before Enter-to-send and can `preventDefault()` to suppress it |
69
+
70
+ For a run that needs a FILE + a prompt together (attach a photo, review/remove it, add a note,
71
+ THEN send), fill `actionsButton` + the `files` slot and set `sendDisabled` so Send fires with an
72
+ attachment and/or text. **Never auto-run an agent on attach** — attaching HOLDS the file for
73
+ review; the human presses Send. The canvas template
74
+ ([`tpl_dieline`](../examples/tpl_dieline.tsx)) uses it this way; while running the host swaps
75
+ `Composer` for `AgentProgress` (same pill geometry, so it reads as a morph).
76
+
77
+ ## Show the work — `AgentRun`
78
+
79
+ `AgentRun` (`@lotics/ui/agent_run`) is a live feed of the agent's work as a TIMELINE. The prop is
80
+ an ORDERED `items` array (`AgentRunItem`):
81
+
82
+ | Item | Shape | Renders as |
83
+ |---|---|---|
84
+ | `{ type: "text", id, text }` | Answer prose | `Markdown`, a trailing ▍ caret on the last segment while streaming |
85
+ | `{ type: "reasoning", id, text }` | Thinking | COLLAPSED — a muted "Thinking" row; press to reveal the Markdown |
86
+ | `{ type: "step", id, label, status, kind?, input?, output?, errorText?, detail?, peek? }` | A tool/step call | An activity row with a status dot |
87
+
88
+ Items appear in the order they happened — a real run is think → text → a burst of calls → more
89
+ text, NOT all text on top of a flat step list. Transparent work, NEVER a bare spinner.
90
+
91
+ **Progressive disclosure.** The feed shows the label + state; the detail is revealed on demand.
92
+ A `step`'s `input`/`output` are hidden in the row but open in a press-to-reveal **peek**
93
+ (auto-built Input / Output `JsonPanel`s). On `status: "error"` the row's dot goes amber, the
94
+ `detail` line reads in the danger ink, and `errorText` shows in the peek's Error panel. Pass an
95
+ explicit `peek` node to render that reveal yourself (it overrides the auto panels). `detail` is
96
+ OPTIONAL — a short human summary (a count), never invented prose or raw I/O (that's
97
+ `input`/`output`).
98
+
99
+ **Activity grouping.** Consecutive `step` items fold into ONE group. While the agent is mid-tools
100
+ the tail group is a SINGLE pulsing row whose label swaps in place as each call fires (no growing
101
+ stack of dots); once prose resumes the group settles: a single call becomes one done row, and a
102
+ multi-call group becomes a persistent "{final action} · {n} steps" HEADER (a `complete` terminal
103
+ dot — distinct from the filled `done` step dots; amber `warning` if any call errored) that STAYS
104
+ PUT and rolls the calls out BELOW it on press. The run ALWAYS ends on the agent's text — there is
105
+ no global terminal node.
106
+
107
+ **Tool labels.** A `step` with `kind: "tool"` carries the RAW tool name (`update_records`),
108
+ resolved via a built-in map of the platform record/document tools + an optional `labelForTool`
109
+ override (localize THERE — the kit stays English); unknown names fall back to a prettified form.
110
+ `stepsLabel` localizes the "{n} steps" suffix. `state` (`"streaming" | "done" | "error"`)
111
+ defaults to `streaming` while any step is running, else `done`.
112
+
113
+ Fed natively by `@lotics/app-sdk`'s `useAgentRun().items` (reasoning + per-tool I/O + state come
114
+ for free — no hand-assembly): `<AgentRun items={run.items} state={…} />` — see
115
+ [the SDK doc](../../app-sdk/docs/ai.md) for the hook.
116
+
117
+ **Limitation:** the collapsed reasoning row's "Thinking" label and the auto peek's
118
+ "Input"/"Output"/"Error" panel titles are currently English-only (not on the locale provider and
119
+ not prop-overridable); `labelForTool`/`stepsLabel` localize everything else in the feed.
120
+
121
+ ### `AgentProgress` — the floating pill
122
+
123
+ On a canvas/composer app reach for `AgentProgress` (`@lotics/ui/agent_progress`) — `AgentRun`
124
+ collapsed into a floating pill (an animated `WaveAvatar` + the current step's label) that EXPANDS
125
+ on press to the full feed in a capped scroll panel. It takes the same `items`/`state`/
126
+ `labelForTool`/`stepsLabel`, plus `label` (override the collapsed text — defaults to the running
127
+ step's label, else "Working…" / "Done" / "Stopped"; pass it to localize — note it's one static
128
+ string, so it no longer tracks the running step) and `defaultExpanded`. Its pill matches
129
+ `Composer`'s compact geometry, so the composer morphs into it while running and reveals again
130
+ when done.
131
+
132
+ **Limitation:** the pill's expand/collapse accessibility labels ("Show the agent's steps" /
133
+ "Hide the agent's steps") are currently hardcoded English.
134
+
135
+ ## Review before apply — ONE surface, the compound `ChangeReview` family
136
+
137
+ Every "the agent proposes → the human accepts / edits / dismisses → nothing auto-applies" surface
138
+ composes from twelve pieces in three groups (all in `@lotics/ui/change_review`; worked example:
139
+ [`tpl_documents`](../examples/tpl_documents.tsx)).
140
+
141
+ **Frame**
142
+
143
+ - `ChangeReview` — context provider + stack; hairline dividers between consecutive `Change`
144
+ sections (and `ChangeFields` rules between fields). Changes are OPEN sections — spacing and
145
+ rules, never card boxes.
146
+ - `ChangeReviewHeader` — the SECTION heading (md semibold; default title localizes to "Suggested
147
+ edits"). Keep it lean: never repeat the record the dialog is already about. The counter does
148
+ NOT live here.
149
+ - `ChangeReviewActions` — the commit bar; pin it in the `DialogFooter`/`DrawerFooter` (or inline
150
+ under the list). **Keep all** sits bottom-left and presses every pending decision's own
151
+ `onAccept` — pass `onAcceptAll` when decisions live in host state the registry can't see
152
+ (field-level Keep/Drop, unresolved conflicts); it then replaces the derived handler entirely.
153
+ The "N of M kept" counter reads HERE beside it, never in the header (the counter renders only
154
+ when at least one registered entry carries `onAccept`; Keep-all renders when one does OR when
155
+ you pass `onAcceptAll`; `showAcceptAll={false}` hides Keep-all). Apply
156
+ disables below `minKept` kept (default 1 when any registered entry carries `onAccept`, else 0)
157
+ or via `applyDisabled` (host-level gate, ORed in); `applyLoading` marks the async write.
158
+ `onDiscard` puts the Discard button beside Apply. Apply is the only outcome-named button
159
+ (`applyLabel`).
160
+
161
+ **Sections**
162
+
163
+ - `Change` — one proposed change: `id` (registers in the review context — the counter, Keep-all
164
+ and the apply gate derive from registered ids), controlled `status`
165
+ (`"pending" | "accepted" | "rejected"`), `onAccept`/`onReject`/`onUndo`, heading + free body +
166
+ optional verbs + collapse. Omit both callbacks for a **display-only** entry (a finding inside a
167
+ review). Decided → collapses to the `ChangeSummary` child (falls back to the body) behind a
168
+ status mark + Undo. `acceptDisabled` gates Keep on a precondition.
169
+ - `ChangeLabel` — the change's SUBJECT heading; a stack of changes skims by subject.
170
+ - `ChangeSummary` — the collapsed row's one-line content after a decision.
171
+ - `ChangeReasoning` — the agent's quiet hairline-ruled aside (muted text behind a left rule) —
172
+ only when the change is not self-explanatory.
173
+
174
+ **The grammar**
175
+
176
+ - **`ChangeFields` + `ChangeField`** — THE field unit. `ChangeFields` is the OPEN form (the
177
+ section IS the record — an extract dialog's one order: no card chrome), stacking `ChangeField`s
178
+ with hairline rules. A diff-form `ChangeField` renders, top to bottom: the label (sm medium,
179
+ **full colour** — it names the subject of a decision, never `DetailRow`'s muted ink) · the
180
+ field's `reasoning` directly under the label · the `−` band when replacing (`before`) · the
181
+ value — `ChangeValueInput` when `onChangeText` is given (the green `+` band; pressing it edits
182
+ IN THE BAND — a borderless input with identical type metrics, so nothing shifts), the read-only
183
+ `+` band when `valueReadOnly` or no `onChangeText` (muted `placeholder` while empty), or any
184
+ input via `children` — compose `<ChangeValueInput unit="pcs" …>` there yourself for a unit
185
+ suffix (`unit` fixes the suffix outside the editable core — type the number, never the unit;
186
+ the built-in default doesn't take it) · conflict `candidates` as full-width decision rows
187
+ (picked via `onPickCandidate`) + the localized "Type another value" third option
188
+ (`customValue`/`customSelected`/`onCustomValue`/`onCustomSelect`; the outcome band is read-only
189
+ via `valueReadOnly` — the decision comes from picking, never from editing the band; gate with
190
+ `keepDisabled`) · per-field **Keep/Drop** bottom-right
191
+ (`status: "pending" | "kept" | "dropped"`, `onKeep`/`onDrop`/`onUndo`). A pure REMOVAL is
192
+ `before` with no value — the `−` band alone. A decided field collapses to the compact one-row
193
+ card (mark · label → `summary` · Undo). A field with no diff and no decision is the plain
194
+ inline label · value row — this form borrows `DetailRow`'s *metrics* (130px label column /
195
+ 40px row) — press-to-edit when `onChangeText` is given.
196
+
197
+ **Limitation:** `ChangeFieldCandidate.source` and `.description` are accepted by the type but
198
+ the built-in candidate row currently renders only the `value` (plus `selected`). Fold a short
199
+ qualifier into the value string, or compose your own rows; provenance belongs in `Sources` at
200
+ the review's bottom anyway.
201
+
202
+ - **`ChangeRecord`** — THE item card for SETS of records (order lines): `id` (registers in the
203
+ review context like a `Change`), `tone` `"add" | "edit" | "remove"`, the tinted header band
204
+ with the localized op word (Add / Edit / Delete — the `changeReview` locale slice) + `title`,
205
+ body = its `ChangeField`s, required `summary` for the collapsed row. **The verb level follows
206
+ the decision level**: add/remove carry ONE card Keep/Drop (fields none — editable parts, one
207
+ write); edit carries NO card verbs (only its changed fields, each deciding for itself —
208
+ dropping a field narrows the update diff).
209
+ - **`ChangeBand`** — the raw diff band (aligned `+`/`−` marker column, light emerald/red, dark
210
+ text) every piece builds from; reach for it directly for custom strokes (a removed record's
211
+ one-line summary). String content is UNCLAMPED by default (`numberOfLines` to clamp) — a
212
+ review must show the whole value being decided.
213
+ - **`ChangeValueInput`** — the proposed value as a proper diff at rest (the green `+` line) that
214
+ becomes a real input on press; rest and edit share one height, so nothing shifts.
215
+
216
+ **The scaling boundary (known, deliberate)**: `ChangeFields` + `ChangeRecord` cards cover one
217
+ record through ~10; a BULK review (30+ imported rows — a bank statement, a spreadsheet import)
218
+ needs a dense form that does not exist yet — design it against the first real migration, not
219
+ speculatively.
220
+
221
+ **The laws**: ONE verb pair everywhere — Keep/Drop, localized through the `changeReview` locale
222
+ slice (en: Keep / Drop / Keep all / Apply / Discard / Undo); never rename per shape (Apply is the
223
+ only outcome-named button; `acceptLabel`/`rejectLabel` exist but are not a license to invent
224
+ verbs). Candidate rows carry the VALUE only; provenance = `Sources` at the VERY bottom of the
225
+ review (after every change; or omit it — lean beats decorated). The HOST owns every decision in
226
+ plain `useState` (controlled `status` everywhere); the family owns the mechanics (collapse +
227
+ Undo, the counter, Keep-all, apply gating). Never apply a field with no value — gate unresolved
228
+ conflicts (`keepDisabled`, `applyDisabled`). Editing IS the review.
229
+
230
+ ## Ask back — `Clarify`
231
+
232
+ `Clarify` (`@lotics/ui/clarify`): when the agent is unsure, it asks a question with quick-reply
233
+ options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question`,
234
+ `options` (`{ label, value, description? }` — a `ChoiceList`), `onAnswer(value)` resumes the run,
235
+ and the controlled `answer` keeps the pick switchable until committed.
236
+
237
+ **Limitation:** the "Question" eyebrow above the prompt is currently hardcoded English (no label
238
+ prop, not on the locale provider).
239
+
240
+ ## Provenance — `Sources`
241
+
242
+ `Sources` (`@lotics/ui/sources`): openable chips saying where the output came FROM, under any
243
+ answer / summary / extracted value. Makes AI output verifiable — show it on anything produced
244
+ from data the agent read. Each `SourceRef` is `{ id, label, kind?, detail? }` — `detail` is a
245
+ locator after the label ("page 2", a record code). Kinds carry recognizable glyph + colour:
246
+
247
+ | `kind` | Glyph | Colour |
248
+ |---|---|---|
249
+ | `document` (the default) | file-text | red |
250
+ | `table` | table-2 | emerald |
251
+ | `record` | box | blue |
252
+ | `knowledge` | book-open | amber |
253
+ | `web` | globe | sky |
254
+
255
+ `label` sets the eyebrow above the chips (default "Sources" — English; pass a translation), or
256
+ `label={null}` for chips only — the form used INSIDE a `Change` or a `Finding`. Pass `onOpen` to
257
+ make each chip pressable (hover wash + an open glyph); the host does the navigation. An empty
258
+ list renders nothing.
259
+
260
+ ## Confidence — `Confidence`
261
+
262
+ `Confidence` (`@lotics/ui/confidence`): how sure the AI is — a 3-tick meter + the full level
263
+ phrase ("High confidence" / "Medium confidence" / "Low confidence"), emerald / amber / zinc by
264
+ level (low is *unsure*, not an error). Pass `level` (`"high" | "medium" | "low"`) or a 0–1
265
+ `score` (≥ 0.8 high · ≥ 0.5 medium · else low; neither given defaults to medium). Phrases resolve
266
+ prop `labels` → the `confidence` locale slice → English — one phrase per level, so it translates
267
+ cleanly. The human weights an AI proposal by it; pair with `ChangeReview`.
268
+
269
+ ## Findings — evidence, not writes
270
+
271
+ `Finding` (`@lotics/ui/finding`) is one ranked insight from an AI check — a cross-check
272
+ discrepancy, an audit observation, a briefing item: severity dot-badge + localized word ·
273
+ `title` · `detail` · the PROMINENT `metric` (lg semibold, never a side note) + `metricCaption` ·
274
+ `Sources` chips at the bottom (`sources`/`onOpenSource`), with a `children` slot between body and
275
+ sources. Severity → colour: `critical` red · `warning` amber · `info` zinc ("Note") · `positive`
276
+ emerald ("On track"); words come from the `finding` locale slice. **Display-only**: a finding
277
+ informs the action the human takes in the app; it decides nothing itself — no phantom "record
278
+ verdict" write (a persisted check-status goes stale on the next edit). Stack several most-severe
279
+ first — inside a `ChangeReview` wrap each in a display-only `Change` (the family's dividers
280
+ apply). `FindingComparison` is the expected-vs-actual body: each disagreeing side a labeled row
281
+ (source · value), a hairline, then the DELTA emphasized — compose it as the finding's children
282
+ for any one-value-disagrees insight (quantities, totals, dates).
283
+
284
+ ## Session, not chat
285
+
286
+ The outputs accrue as a history the user can CLEAR ("New session"); the APP owns the evolving
287
+ state, each run is a discrete task. (The agent may re-read the session for "make it a bit less",
288
+ but it's a run LOG, not a conversation transcript — that's why it's not a chat.) The session key
289
+ and history reads are the SDK's: `sessionId` on each run, `useAgentRuns` for the history — see
290
+ [the SDK doc](../../app-sdk/docs/ai.md).
291
+
292
+ ## Five proven shapes
293
+
294
+ Five shapes prove the range. **One PRODUCES** — a living artifact the user shapes over time.
295
+ (The other producing outcomes — a conversation, generated prose, an edited document — are the
296
+ chat handoff, shape 3 of
297
+ [the decision table](#which-ai-surface--decide-by-the-outcomes-shape-not-the-tasks-topic);
298
+ the chat harness owns them.)
299
+
300
+ - **Canvas** ([`tpl_dieline`](../examples/tpl_dieline.tsx)) — the page IS the design on a
301
+ pannable/zoomable surface: it stays CENTRED at every zoom (a floating zoom pill bottom-left),
302
+ the FLOATING composer at the bottom (a `Composer`: attach a photo → review/remove it → add a
303
+ note → send, with `sendDisabled` allowing a file-and/or-text submit — NOT auto-run on attach)
304
+ morphs into `AgentProgress` while running, and a pinned PARAMS PANEL (a label/value field-card
305
+ in live-edit mode, carrying the single Download; minimizes to a pill) floats centre-right.
306
+ Panel, zoom pill and composer float ON TOP — they never shift the design. (Floating layers use
307
+ a `pointerEvents: "none"` wrapper with the interactive child set `"auto"`; RN-Web ignores
308
+ `"box-none"` in style, so a full-width wrapper would otherwise eat clicks on the canvas behind
309
+ it.) Change the design by prompt ("5 mm taller") OR by editing a param directly — either
310
+ re-flows it in place. For a design/document the user shapes over time.
311
+
312
+ The other four output STRUCTURE — when the answer is a panel of facts, a queue of decisions, or a
313
+ ranked set, don't cram it into chat prose:
314
+
315
+ - **Answer desk** ([`tpl_lookup`](../examples/tpl_lookup.tsx)) — describe the goods on the LEFT →
316
+ the agent RANKS the matching codes (nearest matches, the top one Recommended) → pick one and
317
+ its STRUCTURED answer pins on the RIGHT (a verdict header + an exact breakdown + the policies +
318
+ sources). Input → matches → pick, NOT a single confident verdict: classification is ambiguous,
319
+ so the alternatives are first-class and picking a different one changes the outcome; refining
320
+ the description re-ranks. For look-up-and-explain: tariff/classification codes, fee lookup,
321
+ policy Q&A, a spec/compliance desk. (NOT a chat with the answer in a bubble.)
322
+ - **Document desk** ([`tpl_documents`](../examples/tpl_documents.tsx) — THE go-to for
323
+ document-driven records) — the record's files block feeds ONE "Use AI" entry that FORKS into
324
+ the two document tasks, each a specialized run with a task-pure result: **Extract** (files
325
+ read → fields already matching fold into one quiet line → every add / update / conflict a
326
+ `ChangeField` (the `−` band · the editable value · candidate rows), the record's current value
327
+ a first-class choice — plus proposed new lines as record-body `Change`s → one outcome-named
328
+ `ChangeReviewActions` commit) and **Cross-check** (documents compared against the record and
329
+ each other → ranked `Finding`s — severity · title · the prominent metric · sources — separated
330
+ by hairlines; the findings ARE the outcome the human acts on). The fork carries an OPTIONAL
331
+ instructions field — the user steers what the agent checks or extracts, so `Finding` serves ANY
332
+ file-based AI request, not just the stock cross-check. Every file list in the flow opens the
333
+ full-page `FileGalleryModal`. Plus **Create documents**: a readiness checklist (unchecked by
334
+ default, missing inputs called out) → generate → the files land back on the record. Never merge
335
+ the two AI tasks into one mixed output — the fork is the design.
336
+ - **Triage** — an inbox the agent classified + routed is a `ChangeReview` of `Change`s (body: the
337
+ item + the agent's call; the standard Keep/Drop verbs; Keep-all covers the high-confidence
338
+ sweep). Leads, tickets, documents, emails.
339
+ - **Compare / ranked pick** — one `ChangeField` whose `candidates` carry the ranked options; the
340
+ human picks one. Quotes, carriers, suppliers, plans. (Fold the score/reason into each
341
+ candidate's value string — see the candidate-row limitation above.)
342
+
343
+ ## The visual vocabulary — functional colour, no gimmicks
344
+
345
+ The AI vocabulary has **no purple accent and no gimmick glyphs** (no sparkles) — but it is NOT
346
+ monochrome: **colour is used where it carries meaning, not for decoration.** What the violet
347
+ sparkle used to carry now reads structurally — **provenance** is a quiet sentence-case microlabel
348
+ naming the artifact ("Proposed" · "Match" · "Mismatch" · "Suggested edit" · "Question" — xs,
349
+ muted, medium; all-caps is banned kit-wide), and the agent's **reasoning** is a left-ruled margin
350
+ note (a hairline rule + muted text — `ChangeReasoning`), quoted apart from the facts and the
351
+ human's controls — while **status/severity/diffs use functional colour** the way the rest of the
352
+ kit does:
353
+
354
+ - **Confidence** — the 3-tick meter + the full phrase, emerald / amber / zinc by level.
355
+ - **Stepper / AgentRun nodes** — progress dots on a spine: `current` a **pulsing accent ring**
356
+ (white centre — pulses only when live), `done` a filled accent dot + **white check**,
357
+ `upcoming` a faint **grey** ring, the terminal `complete` a **blackish ring + black check**,
358
+ `warning` **amber**. Default accent is neutral ink (`Stepper`'s `color` prop themes it);
359
+ `AgentRun` keeps the neutral default — an active group's tail pulses (`current`), a settled
360
+ group is a `done`/`warning` row, and there is no global terminal node (the run ends on text).
361
+ - **ChangeBand** — the removed value on the light **red** band with the `−` marker, the incoming
362
+ value on the light **emerald** band with `+` (the GitHub-diff idiom, markers in one aligned
363
+ column); everything else in a review stays neutral — a decided row reads a single emerald
364
+ check, an add/remove record card wears the quiet tone wash (50 body · 100 header · 200 border).
365
+ Colour marks the change, never the chrome.
366
+ - **Finding severity** — the coloured dot badge (red / amber / zinc / emerald), most severe
367
+ first.
368
+
369
+ Card chrome (borders, microlabels) stays neutral — colour marks the *state*, never the container.
370
+ The **composer keeps its icons**: `Composer` compact is a single-row pill with an optional
371
+ circular attach `actionsButton` + a circular send button; expanded it adds a `FileThumbnail`
372
+ attachment row (each `onRemove`-able) above the input; `AgentProgress` is the `WaveAvatar` pill.
373
+ "No icons" was only ever about the review surfaces' sparkles/severity glyphs, not functional
374
+ affordances.