@lotics/ui 45.10.0 → 46.0.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/AGENTS.md +70 -137
- package/MIGRATION.md +27 -0
- package/docs/ai_patterns.md +166 -160
- package/docs/catalog.md +223 -287
- package/docs/composition.md +394 -518
- package/docs/data_entry.md +109 -155
- package/docs/reviewing.md +39 -55
- package/docs/templates.md +459 -423
- package/docs/testing.md +3 -7
- package/package.json +1 -1
- package/src/agent_progress.tsx +5 -4
- package/src/agent_run.tsx +218 -44
- package/src/agent_run_pane.tsx +5 -0
- package/src/agent_transform.ts +34 -0
- package/src/locale.tsx +3 -7
- package/src/pressable_row.tsx +7 -5
- package/src/table.tsx +15 -14
package/docs/ai_patterns.md
CHANGED
|
@@ -14,11 +14,9 @@ file). Every component here is in the [catalog](./catalog.md) inventory.
|
|
|
14
14
|
The AI surfaces share ONE law: the agent never commits **invisibly**. The agent owns judgment
|
|
15
15
|
(recognition, estimation, intent→parameters); the app owns geometry, math, and the write. How
|
|
16
16
|
the human stays in charge follows **who supplied the values**, not whether a before-state
|
|
17
|
-
exists.
|
|
18
|
-
they need to see it. Values a HUMAN typed need no gate — they already read them, on the way in.
|
|
17
|
+
exists. Values a HUMAN typed need no gate — they already read them, on the way in.
|
|
19
18
|
Values a MACHINE read off a document need one, and a create is not exempt just because there is
|
|
20
|
-
nothing to compare against
|
|
21
|
-
could have caught a misread reference, and by then it is in the data.
|
|
19
|
+
nothing to compare against.
|
|
22
20
|
|
|
23
21
|
- **Modifying an existing record** — a diff exists, and a wrong value can clobber a right one →
|
|
24
22
|
review BEFORE apply: `DiffValue` + `DiffMark` + `useChangeSet`, composed into the screen; nothing auto-applies.
|
|
@@ -26,20 +24,18 @@ could have caught a misread reference, and by then it is in the data.
|
|
|
26
24
|
so nothing to diff, but the values are still proposed rather than typed → **a full EDITABLE
|
|
27
25
|
preview of every record before the write**, not a summary of it. Lay the records out as they
|
|
28
26
|
will be stored — every field, editable in place, nothing behind a "3 fields" count — and
|
|
29
|
-
commit once.
|
|
30
|
-
earns its place to say which records are new versus matched onto something existing.
|
|
27
|
+
commit once. `DiffMark` still earns its place to say which records are new versus matched onto something existing.
|
|
31
28
|
- **Creating new records a human TYPED** (the manual create form) — they read every value on
|
|
32
29
|
the way in → **apply directly, then end on the RECEIPT**
|
|
33
|
-
(`@lotics/ui/result_header`) — the receipt states the outcome and ROUTES
|
|
34
|
-
|
|
30
|
+
(`@lotics/ui/result_header`) — the receipt states the outcome and ROUTES. Run DETERMINISTIC
|
|
31
|
+
checks over what the save STORED (a checksum, a count
|
|
35
32
|
reconciliation — binary math, never a fabricated score). ONE record: `ResultHeader` (tone,
|
|
36
33
|
outcome title) → the RECEIPT LINES — a `DetailTable` of spread `DetailRow`s (who, how
|
|
37
34
|
much, how many, the schedule) — → **`Confidence`**: the level WITH its basis as
|
|
38
35
|
one block, always (the happy path says what was checked; the flagged path NAMES the exact
|
|
39
|
-
failing values
|
|
36
|
+
failing values). Confidence never floats
|
|
40
37
|
alone as a bare meter on a result surface. NOTHING in the receipt edits: the RECORD is the edit surface (its
|
|
41
|
-
flagged cells carry the same amber marks), one press away
|
|
42
|
-
ordinary verbs. SEVERAL records: a compact attention-first RESULT REGISTER (`ListItem` per
|
|
38
|
+
flagged cells carry the same amber marks), one press away. SEVERAL records: a compact attention-first RESULT REGISTER (`ListItem` per
|
|
43
39
|
record — tone mark, outcome title, key figures, the needs-checking count) whose rows
|
|
44
40
|
press STRAIGHT THROUGH to the record — no intermediate screen. Never tabs (they cap out),
|
|
45
41
|
never stacked collapsed cards, never a field wall duplicating the record page. A
|
|
@@ -51,9 +47,7 @@ could have caught a misread reference, and by then it is in the data.
|
|
|
51
47
|
The closing footer button is dismissal (secondary "Done"), never primary. Correction beyond
|
|
52
48
|
the receipt uses the record's ORDINARY verbs — open it, edit or delete (make the delete
|
|
53
49
|
workflow CASCADE the record's children, so deletion is always safe) — never a special undo
|
|
54
|
-
affordance duplicating the delete verb.
|
|
55
|
-
proofreading a wall of cards the user blind-confirms — and a human cannot eyeball-verify a
|
|
56
|
-
reference number anyway; deterministic post-save checks catch what proofreading cannot.
|
|
50
|
+
affordance duplicating the delete verb.
|
|
57
51
|
|
|
58
52
|
Either way the deterministic app applies the write. Compose the surfaces as a loop, and reach
|
|
59
53
|
for the right one by job.
|
|
@@ -71,14 +65,12 @@ Three shapes:
|
|
|
71
65
|
operator acts on what was found) → **in-app agent** + **`Finding`**. Still bounded and
|
|
72
66
|
prompt-free (an optional instructions brief at most): the brief is fixed, the output is
|
|
73
67
|
structured display-only findings read IN the record's context, and there is no conversation
|
|
74
|
-
to have — the loop closes when the operator acts in the app.
|
|
75
|
-
tear the findings away from the record they judge.
|
|
68
|
+
to have — the loop closes when the operator acts in the app.
|
|
76
69
|
3. **A file or an open-ended answer** (edit this document, draft from context, explain) →
|
|
77
70
|
**hand off to the chat agent** (`askAi` in `@lotics/app-sdk` — see
|
|
78
71
|
[the SDK's fields-vs-file razor](../../app-sdk/docs/ai.md)). The loop is multi-turn with
|
|
79
72
|
no output schema, judged by looking — and the chat harness already owns it: preview beside
|
|
80
|
-
the thread, version chains, branching, session memory.
|
|
81
|
-
re-implement all of that inside every app.
|
|
73
|
+
the thread, version chains, branching, session memory.
|
|
82
74
|
|
|
83
75
|
The Document desk's Use-AI fork IS this table as UI: Extract data (1), Cross-check (2),
|
|
84
76
|
Edit with AI (3) — one entry point, three outcome shapes
|
|
@@ -124,37 +116,32 @@ A CTA that starts an agent run (or any paid/mutating action) must make the PRESS
|
|
|
124
116
|
transition — never leave a live button waiting on a network round-trip:
|
|
125
117
|
|
|
126
118
|
- **Same-tick feedback.** On press, synchronously close the dialog / swap to the run surface
|
|
127
|
-
(`AgentRun` with its streaming state) so there is nothing left to click.
|
|
128
|
-
pressable while an attach/start request round-trips invites double-fires.
|
|
119
|
+
(`AgentRun` with its streaming state) so there is nothing left to click.
|
|
129
120
|
- **Single-flight in app code too.** Guard the handler with a ref (state alone races the
|
|
130
121
|
re-render): first press wins, later presses no-op until the flow settles.
|
|
131
122
|
- **The SDK backstops runs, not workflows.** `useAgentRun().run()` is single-flight by default
|
|
132
123
|
(a duplicate call joins the in-flight run; `replace: true` is the deliberate restart) — but
|
|
133
|
-
`useWorkflow` calls are NOT deduped
|
|
124
|
+
`useWorkflow` calls are NOT deduped, so a workflow CTA
|
|
134
125
|
must disable itself (`loading`) while pending or a double-click writes twice.
|
|
135
126
|
|
|
136
127
|
## Stopping — the exit is part of the trigger
|
|
137
128
|
|
|
138
129
|
The press being irreversible is a property of the BUTTON, not of the run. Every streaming surface
|
|
139
|
-
owes the operator a way out
|
|
140
|
-
steps: it read the wrong document, it is grouping the wrong way, the brief was wrong. Without an
|
|
141
|
-
exit they pay for a result they already know they will discard.
|
|
130
|
+
owes the operator a way out.
|
|
142
131
|
|
|
143
132
|
- **`cancel`, never `abort`.** `useAgentRun` exposes both and they are not synonyms. `abort` stops
|
|
144
133
|
LISTENING — the run keeps executing server-side and lands in the session history. `cancel` stops
|
|
145
134
|
the run itself. A user-facing Stop wires to `cancel`; `abort` is for unmount.
|
|
146
|
-
- **Closing a dialog must CANCEL.**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
nobody. Unmount is not cancellation; the close handler calls `cancel` itself.
|
|
135
|
+
- **Closing a dialog must CANCEL.** The hook aborts on unmount by design — right for a page you
|
|
136
|
+
navigate away from and will come back to, wrong for a dialog just dismissed, where the result is
|
|
137
|
+
abandoned. Unmount is not cancellation; the close handler calls `cancel` itself.
|
|
150
138
|
- **Stop lands where the run STARTED**, not on a dead transcript — the intake, with the files, the
|
|
151
|
-
brief and the answers still there
|
|
152
|
-
It is the same destination the parked wizard's Cancel uses; only the verb differs
|
|
153
|
-
|
|
139
|
+
brief and the answers still there.
|
|
140
|
+
It is the same destination the parked wizard's Cancel uses; only the verb differs — you STOP
|
|
141
|
+
something running, you CANCEL something waiting.
|
|
154
142
|
- **The control is PINNED, in the footer.** A feed that follows its newest part scrolls a control
|
|
155
|
-
placed inside it away from the reader
|
|
156
|
-
|
|
157
|
-
directly ([`tpl_item_list`](../examples/tpl_item_list.tsx) and
|
|
143
|
+
placed inside it away from the reader. `AgentRunActions` renders Stop for you; a surface
|
|
144
|
+
composing its own footers states it directly ([`tpl_item_list`](../examples/tpl_item_list.tsx) and
|
|
158
145
|
[`tpl_record`](../examples/tpl_record.tsx) respectively).
|
|
159
146
|
|
|
160
147
|
## Show the work — `AgentRun`
|
|
@@ -170,12 +157,10 @@ bespoke transcript type. It renders three part kinds:
|
|
|
170
157
|
| `reasoning` | Thinking — COLLAPSED, a muted "Thinking" row; press to reveal the Markdown |
|
|
171
158
|
| a tool part — `dynamic-tool` (app agents) or `tool-<name>` (chat) | An activity row with a status dot |
|
|
172
159
|
|
|
173
|
-
Parts appear in the order they happened
|
|
174
|
-
text, NOT all text on top of a flat step list. Content parts with no feed representation
|
|
160
|
+
Parts appear in the order they happened, NOT all text on top of a flat step list. Content parts with no feed representation
|
|
175
161
|
(`source-*`, `file`, `data-*`, `step-start`, `custom`) are skipped. Transparent work, NEVER a bare spinner.
|
|
176
162
|
|
|
177
|
-
**Progressive disclosure.**
|
|
178
|
-
tool part's `input`/`output` are hidden in the row and **expand in place** on press — auto-built
|
|
163
|
+
**Progressive disclosure.** A tool part's `input`/`output` are hidden in the row and **expand in place** on press — auto-built
|
|
179
164
|
Input / Output `JsonPanel`s roll out under the row (the same disclosure pattern as "Thinking",
|
|
180
165
|
never a popover). A tool in an `output-error` state flows like any other call — the dot goes
|
|
181
166
|
amber and its `errorText` shows in the expanded Error panel. To render a tool's OUTPUT yourself
|
|
@@ -186,39 +171,104 @@ Output panel; `undefined` keeps it. Input + error rendering are untouched.
|
|
|
186
171
|
**Activity grouping.** Consecutive tool parts fold into ONE group. While the agent is mid-tools
|
|
187
172
|
the tail group is a SINGLE pulsing row whose label swaps in place as each call fires (no growing
|
|
188
173
|
stack of dots); once prose resumes the group settles: a single call becomes one done row, and a
|
|
189
|
-
multi-call group becomes a persistent "{final action}
|
|
174
|
+
multi-call group becomes a persistent "{final action}" HEADER (a `complete` terminal
|
|
190
175
|
dot — distinct from the filled `done` step dots; amber `warning` if any call errored) that STAYS
|
|
191
|
-
PUT and rolls the calls out BELOW it on press.
|
|
192
|
-
|
|
176
|
+
PUT and rolls the calls out BELOW it on press.
|
|
177
|
+
|
|
178
|
+
**ONE row for the whole run, and NO prose until it ends.** While the agent works, the entire feed
|
|
179
|
+
is a single row. It names the call in flight — "Searching records" — and says
|
|
180
|
+
**"Thinking…"** whenever the model is writing rather than calling, which is every gap between
|
|
181
|
+
calls. The dot pulses; nothing rolls out unless the reader asks.
|
|
182
|
+
|
|
183
|
+
**No step count.** The row says WHAT is happening and stops there. A tally was a number the reader
|
|
184
|
+
could act on in no way: live, the label already changes on every call, so it added no motion the
|
|
185
|
+
row did not have; settled, it sized work nobody had asked to size. The chevron carries "there is
|
|
186
|
+
more inside".
|
|
187
|
+
|
|
188
|
+
**The agent's between-call text is not shown at all.** It is narration by instruction — a plan the
|
|
189
|
+
prompt tells it to state up front, asides capped at two sentences — and none of it is load-bearing
|
|
190
|
+
in flight. The one message that genuinely needs a response is a bulk-mutation confirmation, and
|
|
191
|
+
there the agent WAITS: the run stops, so that text becomes the trailing text and renders as the
|
|
192
|
+
answer. Showing the rest bought a sentence that appeared, got half-read, and vanished when the
|
|
193
|
+
next call superseded it. The threshold is that the run did SOME work — a reply with no tool calls
|
|
194
|
+
at all is all answer, and hiding *that* behind a row until settle would be the real loss.
|
|
195
|
+
|
|
196
|
+
A reader waiting on an answer is not reading a transcript, and a transcript that scrolls past
|
|
197
|
+
while they wait is still noise — bounding it only makes the noise a fixed size. What they need is
|
|
198
|
+
that something is happening and roughly what; the label and the count carry both, and the press
|
|
199
|
+
carries the rest. That the waits are long (production: **p50 25s, p90 94s**; five-plus tool calls
|
|
200
|
+
p50 52s / p90 144s) is an argument for the label CHANGING, not for showing the feed.
|
|
201
|
+
|
|
202
|
+
The cost, stated plainly: the answer appears at settle rather than streaming. Final responses are
|
|
203
|
+
≤3 sentences by instruction, so what is given up is a second of typing — against never yanking a
|
|
204
|
+
half-read sentence out from under anyone.
|
|
205
|
+
|
|
206
|
+
Grouping alone could never deliver this, because a group breaks on every prose part AND every
|
|
207
|
+
reasoning part. An interleaved-thinking run therefore rebuilt the stack one group at a time — the
|
|
208
|
+
reader watched finished rows pile up while they waited, and was then left with that pile sitting
|
|
209
|
+
above the answer at the answer's weight, every row of it stale.
|
|
210
|
+
|
|
211
|
+
**Expanding is where live and settled differ, and the only place they do.** Opened mid-run the
|
|
212
|
+
timeline is CAPPED and self-pinning (220px, ~5 rows) — someone who opens a run in flight asked to
|
|
213
|
+
see the work, not to hand the page a feed that grows for another minute; `FollowScroll` follows at
|
|
214
|
+
LAYOUT level so each call paints already pinned. Opened once settled it is uncapped: nothing is
|
|
215
|
+
arriving, so there is nothing to cap. Settling never closes what a reader opened; it just stops
|
|
216
|
+
capping it.
|
|
217
|
+
|
|
218
|
+
The split is POSITIONAL (`splitTimeline` in `agent_transform`): what comes after the last tool
|
|
219
|
+
call is the answer, everything before it is the work. Nothing has to decide whether a paragraph
|
|
220
|
+
was a plan or a report — a model emits all of it as `text`, so only position is knowable, and
|
|
221
|
+
while the run is still moving that position is not yet final. Which is why the answer waits for
|
|
222
|
+
settle: at that moment, and only then, the trailing text is known to be the report.
|
|
223
|
+
|
|
224
|
+
The work stays fully open where collapsing would hide something needed: while any call is parked
|
|
225
|
+
`awaiting` a human decision (the approval surface would be asking about work the reader can no
|
|
226
|
+
longer see); when a SETTLED run stopped ON a tool call, leaving no answer to fold behind (mid-run
|
|
227
|
+
that is just "not there yet", not a dead end); when the work is already one row (folding buys
|
|
228
|
+
nothing and costs a second press to reach a step's I/O); and when no tool ran at all (`Thinking`
|
|
229
|
+
is already a collapsed row, and a summary could only name a step that never happened).
|
|
230
|
+
**`collapseProcess={false}`** hands the whole job back to the caller — for a surface that ALREADY
|
|
231
|
+
frames the run (`AgentProgress`'s pill) or one whose subject is a step's own output; such a
|
|
232
|
+
surface owns the `FollowScroll` too, and one on the default must NOT add one or it nests a
|
|
233
|
+
scroller in a scroller. **`summarizeRun(steps)`** renames the row; the default names the current
|
|
234
|
+
or last action and nothing more, because only the HOST knows which of its tools WROTE, and a
|
|
235
|
+
kit-side list of write tools would be silently incomplete the first time one was added —
|
|
236
|
+
reporting a run that changed data as though it had only read. A host that knows should say so
|
|
237
|
+
("Updated 3 records"): that is the one fact worth reading without expanding.
|
|
238
|
+
|
|
239
|
+
An ARTIFACT the run produced — a file, an image, a record diff — belongs in the result zone the
|
|
240
|
+
host renders below the feed, never inside a tool's output panel where the fold puts it two
|
|
241
|
+
presses away.
|
|
193
242
|
|
|
194
243
|
**Tool labels.** A tool part carries the RAW tool name (`update_records` from `tool-update_records`,
|
|
195
|
-
or a `dynamic-tool`'s `toolName`), resolved through the active locale's **`agentRun.tools`** map
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool `state` folds ai's
|
|
244
|
+
or a `dynamic-tool`'s `toolName`), resolved through the active locale's **`agentRun.tools`** map.
|
|
245
|
+
An optional **`labelForCall`** override still wins, for phrasing a call by what it TARGETS rather
|
|
246
|
+
than by which tool ran. It gets the whole call — `{ toolName, input, state }` (`AgentToolCall`);
|
|
247
|
+
return `undefined` to fall back. Unknown names fall back
|
|
248
|
+
to a prettified form. The tool `state` folds ai's
|
|
201
249
|
7-state lifecycle to **running / awaiting / done / error**: `approval-requested` → `awaiting` (the
|
|
202
|
-
call is parked on a human decision — an amber dot + an "awaiting" annotation, and it NEVER
|
|
203
|
-
|
|
250
|
+
call is parked on a human decision — an amber dot + an "awaiting" annotation, and it NEVER
|
|
251
|
+
pulses); `approval-responded` → running; `output-denied` → error carrying the
|
|
204
252
|
denial reason. The whole-run `state` prop (`"streaming" | "done" | "error"`) defaults to `streaming`
|
|
205
253
|
while any tool is running, else `done` (an awaiting call is not running, so it settles).
|
|
206
254
|
|
|
207
255
|
**Errors — two kinds, both surfaced, neither shouting.** An INTERMEDIARY tool failure rides in
|
|
208
256
|
that tool's `output-error` part and flows like a normal call — the amber dot marks it (a settled
|
|
209
|
-
group's header goes amber too), the reason waits in the expanded Error panel
|
|
210
|
-
move on, so the feed doesn't dramatize it. A BREAKING error that terminates the whole run lives
|
|
257
|
+
group's header goes amber too), the reason waits in the expanded Error panel. A BREAKING error that terminates the whole run lives
|
|
211
258
|
OUTSIDE `parts` (chat persists it in the message's `errors`; `useAgentRun().error` carries it) —
|
|
212
259
|
pass it as the `error` prop and it terminates the feed with an error `Callout` — a contained,
|
|
213
|
-
tinted panel rather than another step row,
|
|
214
|
-
carries `role="alert"` so a screen reader announces it. Pass **`onRetry`** alongside it and a Retry
|
|
260
|
+
tinted panel rather than another step row, and it carries `role="alert"` so a screen reader announces it. Pass **`onRetry`** alongside it and a Retry
|
|
215
261
|
action (a secondary `Button`) sits INSIDE that panel so the operator can re-fire the run; omit it
|
|
216
262
|
and the panel carries the message alone.
|
|
217
263
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
the
|
|
221
|
-
|
|
264
|
+
**`FollowScroll` — only when you took the job (`collapseProcess={false}`).** A run opened mid-flight
|
|
265
|
+
already caps itself with exactly this, so wrapping the default AGAIN nests a scroller in a
|
|
266
|
+
scroller: the outer never overflows, the inner holds the content, and the wheel lands on whichever
|
|
267
|
+
the pointer happens to be over. Reach for the wrapper when the run is a raw timeline you are
|
|
268
|
+
framing yourself — a dialog, drawer, or fixed-height panel where the feed grows BELOW the fold and
|
|
269
|
+
a plain scroll container doesn't follow. Wrap it then:
|
|
270
|
+
`<FollowScroll style={{ maxHeight: … }}><AgentRun … collapseProcess={false} /></FollowScroll>` — the same
|
|
271
|
+
inverted-list mechanism as a chat message list: the follow happens at
|
|
222
272
|
LAYOUT level (each growth paints already pinned — no scroll-after-paint flash), scrolling up
|
|
223
273
|
releases it, and the feed stays at the newest content when the run settles (`AgentProgress`'s
|
|
224
274
|
expanded panel does this). When settling REPLACES the feed with different content (a results
|
|
@@ -233,7 +283,7 @@ for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={
|
|
|
233
283
|
install; the imports are type-only (purity-enforced), so no `ai` runtime ever enters a bundle.
|
|
234
284
|
**A run with no parts yet renders the breathing "Starting…" row** (streaming state only) — the
|
|
235
285
|
live dot plus a label that pulses, covering the gap between the CTA press and the first streamed
|
|
236
|
-
part (upload + run creation + first token)
|
|
286
|
+
part (upload + run creation + first token). **The law: pass
|
|
237
287
|
`state="streaming"` from the moment the paid CTA fires — uploads included — and render
|
|
238
288
|
`AgentRun` even with zero parts. Never hand-roll a text/skeleton placeholder in front of the
|
|
239
289
|
feed**; the starting row IS the placeholder, consistent and localized.
|
|
@@ -243,9 +293,9 @@ feed**; the starting row IS the placeholder, consistent and localized.
|
|
|
243
293
|
|
|
244
294
|
**Localization.** The feed's fixed chrome — the "Thinking" label, the "Input"/"Output"/"Error"
|
|
245
295
|
panel titles, the "awaiting" annotation, the "Retry" action, the tool labels (`agentRun.tools`,
|
|
246
|
-
keyed by raw tool name)
|
|
296
|
+
keyed by raw tool name) — resolves through the
|
|
247
297
|
`LoticsLocale` `agentRun` slice (translate once at the provider; a `vi` app gets them for free);
|
|
248
|
-
`labelForCall`
|
|
298
|
+
`labelForCall` remains a per-call-site OVERRIDE.
|
|
249
299
|
|
|
250
300
|
**Answering an awaiting call — `ApprovalPrompt`.** The `awaiting` row inside `AgentRun` is
|
|
251
301
|
READ-ONLY — it only NAMES that a call is parked on a human decision. The surface that ANSWERS it
|
|
@@ -263,7 +313,7 @@ locale slice → English.
|
|
|
263
313
|
On a canvas/composer app reach for `AgentProgress` (`@lotics/ui/agent_progress`) — `AgentRun`
|
|
264
314
|
collapsed into a floating pill (an animated `WaveAvatar` + the current step's label) that EXPANDS
|
|
265
315
|
on press to the full feed in a capped scroll panel. It takes the same `parts`/`state`/`error`, and
|
|
266
|
-
forwards `labelForCall`/`renderToolOutput`/`onRetry
|
|
316
|
+
forwards `labelForCall`/`renderToolOutput`/`onRetry` to the expanded feed, plus `label`
|
|
267
317
|
(override the collapsed text — defaults to the running step's label, else "Working…" / "Done" /
|
|
268
318
|
"Stopped"; pass it to localize — note it's one static string, so it no longer tracks the running
|
|
269
319
|
step) and `defaultExpanded`. Its pill matches `Composer`'s compact geometry, so the composer morphs
|
|
@@ -275,17 +325,15 @@ into it while running and reveals again when done.
|
|
|
275
325
|
## Review before apply — TWO atoms and a hook, composed into your screen
|
|
276
326
|
|
|
277
327
|
**When to reach for it — the one law's boundary.** A review gates values the MACHINE
|
|
278
|
-
supplied, whether or not they replace anything. Modifying an existing record
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
it: they read them on the way in, so the manual form saves direct and ends on the receipt.
|
|
328
|
+
supplied, whether or not they replace anything. Modifying an existing record takes the full
|
|
329
|
+
diff. Creating records FROM A DOCUMENT takes the same gate in a different shape — no diff, but
|
|
330
|
+
the whole record laid out and editable before the write (`examples/tpl_item_list.tsx`). Only
|
|
331
|
+
values a HUMAN TYPED skip it: the manual form saves direct and ends on the receipt.
|
|
283
332
|
|
|
284
333
|
**There is no review CONTAINER, deliberately.** A review is not a place you go; it is something
|
|
285
334
|
that happens to values already on screen. So the diff renders wherever the value renders — a
|
|
286
335
|
table cell, a `DetailRow`, a total, a chip, a sentence — and inherits that container's width,
|
|
287
|
-
alignment and rhythm.
|
|
288
|
-
shape its root draws, and every surface needing a diff somewhere else rebuilds it by hand.
|
|
336
|
+
alignment and rhythm.
|
|
289
337
|
|
|
290
338
|
### The three pieces
|
|
291
339
|
|
|
@@ -294,39 +342,28 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
294
342
|
dense rows; `align`/`tabular` for money so the struck value and its replacement line up;
|
|
295
343
|
`tone` when the change itself is the bad news. **The three ops are the same component:**
|
|
296
344
|
an ADD omits `before`, a REMOVAL omits `after` (the struck value IS the change), an UPDATE
|
|
297
|
-
passes both, and **equal values collapse to one** — agreement is not a change
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
correction is the right SIZE), and **`delta`** prints the movement itself — "+250 pcs",
|
|
301
|
-
"−118.519 ₫ VAT" — so nobody subtracts in their head.
|
|
345
|
+
passes both, and **equal values collapse to one** — agreement is not a change.
|
|
346
|
+
**Both values stay on screen** on a real change, and **`delta`** prints the movement itself — "+250 pcs",
|
|
347
|
+
"−118.519 ₫ VAT".
|
|
302
348
|
|
|
303
349
|
**The struck value is muted, never red.** An old value is not an ERROR, and red is this
|
|
304
|
-
system's error colour
|
|
305
|
-
|
|
306
|
-
colour-blind reader cannot separate, and it is the distinction a review turns on. The
|
|
307
|
-
strikethrough carries "going away" with no colour at all; weight, tone and (stacked) size
|
|
350
|
+
system's error colour. Red-against-green is also the one pair a colour-blind reader cannot
|
|
351
|
+
separate. The strikethrough carries "going away" with no colour at all; weight, tone and (stacked) size
|
|
308
352
|
carry the rest.
|
|
309
353
|
- **`DiffMark`** (`@lotics/ui/diff_mark`) — what happened to the row holding the value, for the
|
|
310
354
|
cases no single value carries: `added` / `changed` / `removed` / `unchanged`. Glyph plus an
|
|
311
|
-
accessible word, never colour alone.
|
|
312
|
-
real review are untouched.
|
|
355
|
+
accessible word, never colour alone.
|
|
313
356
|
|
|
314
357
|
**ONE treatment, every surface: a filled 22px disc.** The form, the line table, the files
|
|
315
|
-
list and the thumbnail grid all draw the same mark
|
|
316
|
-
|
|
317
|
-
two systems saying the same thing. `unchanged` renders as an empty disc-sized HOLE: an
|
|
318
|
-
operator opening a job of twelve fee lines to see where a dropped invoice landed should find
|
|
319
|
-
the one filled shape in a column of blanks without reading anything or separating two hues.
|
|
320
|
-
The hole keeps the column's width and announces nothing.
|
|
358
|
+
list and the thumbnail grid all draw the same mark. `unchanged` renders as an empty
|
|
359
|
+
disc-sized HOLE: it keeps the column's width and announces nothing.
|
|
321
360
|
|
|
322
361
|
**Derive the kind from the same before/after the row's `DiffValue` carries — never from a
|
|
323
362
|
boolean "did this differ".** A field with no prior value (`before` empty or absent) that a run
|
|
324
363
|
now proposes a value for is an ADD, not a CHANGE, and a local review row that only asks
|
|
325
364
|
"does `draft !== before`" answers yes for both and always renders `changed` — the amber pencil
|
|
326
|
-
on a value that was never there to begin with
|
|
327
|
-
|
|
328
|
-
looks most ordinary: the first proposal any fresh record ever gets has an empty `before` on
|
|
329
|
-
every field, so every mark on that one run is wrong until the kind is derived per field
|
|
365
|
+
on a value that was never there to begin with. The first proposal any fresh record ever gets
|
|
366
|
+
has an empty `before` on every field, so every mark on that one run is wrong until the kind is derived per field
|
|
330
367
|
(`before === after` → no mark; `before === ""` → `added`; otherwise → `changed`).
|
|
331
368
|
- **`useChangeSet(ids, { initial })`** (`@lotics/ui/use_change_set`) — the bookkeeping:
|
|
332
369
|
`status`/`accept`/`reject`/`undo`/`acceptAll`/`reset`, `accepted`/`rejected`/`pending`,
|
|
@@ -339,33 +376,28 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
339
376
|
- **A record's fields** — a `DetailTable` of rows, each a label plus a `DiffValue`, with
|
|
340
377
|
Keep/Drop on the row and a collapsed state once decided. That row is ~20 lines and it belongs
|
|
341
378
|
to your screen: `examples/tpl_record.tsx` and `dev/pages/ai.tsx` each write their own and they
|
|
342
|
-
read differently
|
|
379
|
+
read differently.
|
|
343
380
|
- **A record SET** — a `Table`, one row per proposed record, `DiffMark` leading and `DiffValue`
|
|
344
|
-
inside the figure cells.
|
|
381
|
+
inside the figure cells.
|
|
345
382
|
- **A conflict** — two documents disagree, so the value is the read-only OUTCOME of a pick:
|
|
346
383
|
a `DiffValue` showing the placeholder until resolved, over `CardSelectItem` candidates (each
|
|
347
384
|
naming its source document) and a type-another-value option. **Never pre-select**, and gate
|
|
348
|
-
Keep until one is chosen
|
|
349
|
-
attributed to the person.
|
|
385
|
+
Keep until one is chosen.
|
|
350
386
|
- **An ATTACHMENT** — a dropped document's proposal is often not a value at all: it is WHICH
|
|
351
|
-
LINE the file belongs to. That decision
|
|
352
|
-
same treatment — the file rendered on the row it lands on, inside a `DiffValue` whose sides
|
|
387
|
+
LINE the file belongs to. That decision gets the same treatment — the file rendered on the row it lands on, inside a `DiffValue` whose sides
|
|
353
388
|
are NODES (a `FileBadge` plus its name). A file replacing one already filed shows both, and
|
|
354
389
|
the superseded one is struck by a DRAWN rule: `line-through` set on a `Text` does not cross
|
|
355
|
-
a `View` child
|
|
356
|
-
with nothing saying which is going away. Worked in `examples/tpl_record.tsx`.
|
|
390
|
+
a `View` child. Worked in `examples/tpl_record.tsx`.
|
|
357
391
|
- **A DOCUMENT SET** — a files section is not a field. It is a set of objects with identity,
|
|
358
392
|
so what changes is MEMBERSHIP plus per-document properties, and the kinds do not share a
|
|
359
393
|
treatment. **Added** and **removed** are membership: the row's existence and its mark are
|
|
360
394
|
the whole change. **Superseded** is one slot whose bytes changed — the row names the new
|
|
361
395
|
file and its second line names the one it displaces. **Reclassified** is the one a review
|
|
362
396
|
surface gets wrong: the bytes are untouched and only the FILING moved, so the `DiffValue`
|
|
363
|
-
belongs on the property
|
|
364
|
-
changed. `FileRow` takes the mark in `leading` (so the names still form a column) and a node
|
|
397
|
+
belongs on the property. `FileRow` takes the mark in `leading` (so the names still form a column) and a node
|
|
365
398
|
`meta` (so the second line can carry a diff).
|
|
366
399
|
|
|
367
|
-
**In a GRID** the same set reads differently again
|
|
368
|
-
rather than a gap: a grid is scanned whole, so it carries membership (this is arriving, that
|
|
400
|
+
**In a GRID** the same set reads differently again: a grid is scanned whole, so it carries membership (this is arriving, that
|
|
369
401
|
is leaving — `FileThumbnailGrid diffs`, a corner mark plus a fade) at a glance and carries
|
|
370
402
|
pairing not at all. Pick the surface for the change; do not shrink the change to fit a shape.
|
|
371
403
|
|
|
@@ -373,56 +405,45 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
373
405
|
a tile — the badge earns its space and the row is built around it. Where the file is one
|
|
374
406
|
VALUE among several columns, render the filename as a value at the column's own text size:
|
|
375
407
|
a badge shrunk to fit a table cell takes its label off the type scale entirely (5px, at the
|
|
376
|
-
size a cell allows), and sizing it back up makes it set the row's height
|
|
377
|
-
extension already carries.
|
|
408
|
+
size a cell allows), and sizing it back up makes it set the row's height.
|
|
378
409
|
|
|
379
410
|
**The strikethrough has a discriminator.** Strike the superseded file when it shares a CELL
|
|
380
411
|
with its replacement, adjacent — a table's document column, where the pairing is what the
|
|
381
412
|
cell means. Do NOT strike it on a files-section line, where the two sit on different rows:
|
|
382
|
-
|
|
383
|
-
relation and the chip only identifies the file. Both shapes are worked in `dev/pages/ai.tsx`.
|
|
413
|
+
a word names the relation and the chip only identifies the file. Both shapes are worked in `dev/pages/ai.tsx`.
|
|
384
414
|
- **A record being CREATED from a document** — no before-state, so no diff: the whole record
|
|
385
415
|
is the review. Lay every field out as it will be stored, editable in place, and mark only
|
|
386
416
|
the fact that IS relational (matched onto something existing, or new alongside it). One
|
|
387
417
|
commit named for the outcome. Worked in `examples/tpl_item_list.tsx`.
|
|
388
418
|
- **Display-only** — a finding that informs but has nothing to decide simply is not in the
|
|
389
|
-
change set.
|
|
419
|
+
change set.
|
|
390
420
|
|
|
391
421
|
### The laws that survive the container
|
|
392
422
|
|
|
393
423
|
- **Nothing auto-applies.** The commit is one explicit press, named for its outcome
|
|
394
424
|
("Update record (3)", "Add 4 tasks") — never "Apply" alone, never "OK".
|
|
395
425
|
- **Editing IS the review.** A value the operator can correct in place beats accept-or-reject
|
|
396
|
-
on a value they cannot touch
|
|
397
|
-
only reject costs the whole re-entry.
|
|
426
|
+
on a value they cannot touch.
|
|
398
427
|
- **Gate the commit on what will actually be written**, not on the proposal count — disable at
|
|
399
428
|
zero kept.
|
|
400
429
|
- **A decided row's Keep/Drop must be reachable from BOTH directions when `initial: "accepted"`.**
|
|
401
430
|
`undo` returns a row to its DEFAULT, not to `"pending"` — so on the common default
|
|
402
|
-
(`initial: "accepted"
|
|
403
|
-
|
|
404
|
-
there is no path back to a `pending`-style Keep/Drop pair, and the row is stuck accepted
|
|
405
|
-
forever. The collapsed-to-Undo anatomy (value + one "Undo" button, swapping to Keep/Drop only
|
|
431
|
+
(`initial: "accepted"`) a row starts already decided and Undo is a no-op with nothing to
|
|
432
|
+
reach. The collapsed-to-Undo anatomy (value + one "Undo" button, swapping to Keep/Drop only
|
|
406
433
|
while `pending`) is correct ONLY under `initial: "pending"`, where every row genuinely starts
|
|
407
434
|
undecided. Under `initial: "accepted"`, render Keep/Drop as an always-visible TOGGLE instead —
|
|
408
|
-
both buttons present, the one matching the row's current status disabled, the other live
|
|
409
|
-
so either direction is one press away regardless of where the row started.
|
|
435
|
+
both buttons present, the one matching the row's current status disabled, the other live.
|
|
410
436
|
- **The mark rides the LABEL, and the value sits in an `InlineSlot`.** Both are alignment
|
|
411
437
|
before they are semantics. A mark beside the VALUE indents every row it marks by its own
|
|
412
438
|
width plus a gap, so an unmarked row needs a spacer of exactly the glyph's width to keep up —
|
|
413
|
-
and they drift
|
|
439
|
+
and they drift. The label column is a fixed width with one
|
|
414
440
|
left edge, so a mark placed there aligns down the page for free. Give the slot a fixed HEIGHT
|
|
415
441
|
as well as width: it is an inline-flex box inside `DetailRow`'s label `Text`, so an empty one
|
|
416
442
|
baselines differently from one holding a glyph. On the value side, a form mixes 40px editors
|
|
417
|
-
with 20px flat diffs; without the slot the column has two left edges and two baselines
|
|
418
|
-
the label agrees with one kind of row and not the other.
|
|
443
|
+
with 20px flat diffs; without the slot the column has two left edges and two baselines.
|
|
419
444
|
- **The reason goes above the verdict.** A `why` passed as `DetailRow`'s `description` renders
|
|
420
|
-
under the whole cell — which is under the Keep/Drop buttons.
|
|
421
|
-
|
|
422
|
-
- **Show the WHOLE thing, not just the parts that moved.** The operator's question is rarely
|
|
423
|
-
"what did it find" — it is "is this right, and where does it sit". A surface showing only
|
|
424
|
-
the changed rows answers the first and leaves the second to a second screen. Render the
|
|
425
|
-
record entire and let `DiffValue` collapse what agrees (equal values render as one plain
|
|
445
|
+
under the whole cell — which is under the Keep/Drop buttons. Render it inside the value block.
|
|
446
|
+
- **Show the WHOLE thing, not just the parts that moved.** Render the record entire and let `DiffValue` collapse what agrees (equal values render as one plain
|
|
426
447
|
value, so the same component tree serves a record with one change and a record with twenty).
|
|
427
448
|
Emphasis, not omission, is what makes the changes findable.
|
|
428
449
|
- **The fixture must not flatter.** A review demo whose every row carries a change proves
|
|
@@ -435,8 +456,7 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
435
456
|
`Clarify` (`@lotics/ui/clarify`): when the agent is unsure, it asks a question with quick-reply
|
|
436
457
|
options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question` (the
|
|
437
458
|
prompt, rendered as-is — no eyebrow chrome), `options` (`{ label, value, description }` — every
|
|
438
|
-
answer carries a REQUIRED one-line description
|
|
439
|
-
question; a `ChoiceList`), `onAnswer(value)` resumes the run, the controlled `answer` keeps the pick
|
|
459
|
+
answer carries a REQUIRED one-line description; a `ChoiceList`), `onAnswer(value)` resumes the run, the controlled `answer` keeps the pick
|
|
440
460
|
switchable until committed. **`allowCustom`** adds an always-visible, borderless, auto-growing multiline field below the
|
|
441
461
|
options — typing it IS the selection; its text becomes the value (any value not matching an option
|
|
442
462
|
reads as the custom answer).
|
|
@@ -446,7 +466,7 @@ Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebro
|
|
|
446
466
|
question — no progress bar, and no eyebrow at all for a single question. Each step is a `Clarify`; advance only once the
|
|
447
467
|
current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
|
|
448
468
|
`custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
|
|
449
|
-
locale-wired (the `clarify` slice).
|
|
469
|
+
locale-wired (the `clarify` slice). **Placement follows
|
|
450
470
|
the dialog grammar**: standalone (riding a parked run's stream, where no footer exists) the wizard
|
|
451
471
|
renders its own inline action row; as a DIALOG phase, wrap the `Dialog` with `ClarifyWizardScope`
|
|
452
472
|
and put `ClarifyWizardActions` in the `DialogFooter` — the wizard suppresses the inline row and
|
|
@@ -474,29 +494,25 @@ const run = useAgentRun("intake"); // @lotics/app-sdk
|
|
|
474
494
|
after a question it is the only thing holding how the answered leg ended — your own `run()` promise
|
|
475
495
|
already resolved, with `parked`, and never resolves again. Give `onLanding` the same handler you
|
|
476
496
|
give `run()`: it is typed from your run (no cast), and it re-fires if answering leads to a follow-up
|
|
477
|
-
ask. Omit it and an answered run completes with the host none the wiser —
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
handed directly.
|
|
497
|
+
ask. Omit it and an answered run completes with the host none the wiser — your review step never
|
|
498
|
+
opens, and the paid read is discarded. A host that instead reads its result off `status`/`output`
|
|
499
|
+
does not need it.
|
|
481
500
|
|
|
482
501
|
**The parked question REPLACES the feed — that is the contract, not a style.** The run is blocked
|
|
483
502
|
on the answer, so the question is the only thing to act on; it gets the dialog's own scroller and
|
|
484
|
-
its actions sit outside it. Stacked under the transcript in an unscrollable box
|
|
485
|
-
|
|
486
|
-
Submit, leaving a live `awaiting_input` run readable and unanswerable until it expires. The pane
|
|
503
|
+
its actions sit outside it. Stacked under the transcript in an unscrollable box, a multi-question
|
|
504
|
+
ask runs past the dialog's height and clips its own Submit, leaving a live `awaiting_input` run readable and unanswerable until it expires. The pane
|
|
487
505
|
also swaps `FollowScroll` for `DialogScrollArea` on the park, so the question opens at the top
|
|
488
506
|
rather than wherever the feed was scrolled to.
|
|
489
507
|
|
|
490
508
|
**Mount `AgentRunActions` wherever the run drives the surface — never gated on one phase.** It
|
|
491
509
|
holds the wizard's verbs while a question is up, **Stop** while the run streams, and nothing once
|
|
492
|
-
it settles; the RUN decides
|
|
493
|
-
single phase out here duplicates that decision and gets it wrong the moment the bar learns a new
|
|
494
|
-
one.
|
|
510
|
+
it settles; the RUN decides. A host that names a single phase out here duplicates that decision.
|
|
495
511
|
|
|
496
512
|
**`run` is a SHAPE, not an import** (`AgentRunLike`: `status`, `parts`, `pendingChoice`,
|
|
497
513
|
`answerChoice`, `cancel`, `error`; `AgentRunQuestion` names what `pendingChoice` carries, so a template or
|
|
498
514
|
test declares one directly — an option is a label + description and the answer's value IS the
|
|
499
|
-
label, which is the `ask_user_choice` wire shape
|
|
515
|
+
label, which is the `ask_user_choice` wire shape). `useAgentRun()` satisfies it structurally — `@lotics/ui` never depends on
|
|
500
516
|
`@lotics/app-sdk` — which also means a template or test can hand it a plain object and exercise the
|
|
501
517
|
whole pane with no backend ([`tpl_item_list`](../examples/tpl_item_list.tsx) does exactly that).
|
|
502
518
|
|
|
@@ -533,28 +549,24 @@ list renders nothing.
|
|
|
533
549
|
reader is deciding about it. The harder surface is the one they open a week later: a stored
|
|
534
550
|
record whose fields were written by a person, by a chat agent, by an extraction off an upload,
|
|
535
551
|
and by an automation that fired when a file landed — and which carries no memory of which was
|
|
536
|
-
which.
|
|
537
|
-
in. Three rules, each of which a screen built for a single writer gets wrong for free.
|
|
552
|
+
which. Three rules, each of which a screen built for a single writer gets wrong for free.
|
|
538
553
|
|
|
539
554
|
**An unwritten value must not render like a written one.** A row assembled by an automation
|
|
540
555
|
arrives with its enums set and its prose empty, and the natural repair — composing a label out
|
|
541
556
|
of the enums that ARE set — produces a real-looking sentence in body ink. "Nobody has written
|
|
542
|
-
this up" and "this is what happened" then look identical
|
|
543
|
-
reader most needs. Say the value is missing, in the ink an unfilled field already uses
|
|
557
|
+
this up" and "this is what happened" then look identical. Say the value is missing, in the ink an unfilled field already uses
|
|
544
558
|
(`Timeline`'s `placeholder`; the same idea wherever a component takes a `label: string` and
|
|
545
559
|
therefore cannot express absence — that type is the trap, so give it a way to say so).
|
|
546
560
|
|
|
547
|
-
**A machine's prose and a person's must not share a treatment.**
|
|
548
|
-
they earn different trust, so rendering them alike teaches the reader to skim both at one
|
|
549
|
-
speed. Carry the difference with SHAPE, not weight: name the author and the evidence on the
|
|
561
|
+
**A machine's prose and a person's must not share a treatment.** Carry the difference with SHAPE, not weight: name the author and the evidence on the
|
|
550
562
|
line above ("Written by AI from the recording" — the source alone is not authorship), and drop
|
|
551
563
|
the machine's text into a recessed `Inset`, which says it was not written on this page. Leave
|
|
552
564
|
the person's conclusion on the page's own ground; that contrast IS the signal.
|
|
553
565
|
|
|
554
566
|
**Model prose arrives with its own type ladder — take it away at the door.** A model asked for
|
|
555
567
|
a summary emits `##` headings and heavy `**bold**` knowing nothing of what surrounds it, and
|
|
556
|
-
document-scale markdown hands those an `h1` equal to your section heading
|
|
557
|
-
|
|
568
|
+
document-scale markdown hands those an `h1` equal to your section heading. Render every
|
|
569
|
+
embedded value with
|
|
558
570
|
`<Markdown variant="embedded">`.
|
|
559
571
|
|
|
560
572
|
And one that is not about treatment at all: **check that the read path projects every field the
|
|
@@ -568,22 +580,18 @@ Confidence IS a callout (`@lotics/ui/confidence`) — the ONLY form it takes: th
|
|
|
568
580
|
meter + level phrase head the block, and the REQUIRED `children` carry the basis — what was
|
|
569
581
|
checked and what came of it ("all 7 references pass their checksum" / "2 of 7 fail (X, Y) —
|
|
570
582
|
verify against the source"). The box stays white with a hairline border at EVERY level — the
|
|
571
|
-
meter is the only colored element
|
|
572
|
-
tinted fill would say it a third time). Pass `level` or a 0–1 `score` (≥0.8 high,
|
|
583
|
+
meter is the only colored element. Pass `level` or a 0–1 `score` (≥0.8 high,
|
|
573
584
|
≥0.5 medium, else low; `levelFromScore` exported). There is NO standalone meter: a level
|
|
574
|
-
without its reasons cannot be acted on, so it cannot be rendered
|
|
575
|
-
used to decorate a ranked row or a proposal card, remove it (rank, badges, and severity
|
|
576
|
-
already convey standing). Derive levels HONESTLY (deterministic checks: all pass → high, any
|
|
585
|
+
without its reasons cannot be acted on, so it cannot be rendered. Derive levels HONESTLY (deterministic checks: all pass → high, any
|
|
577
586
|
fail → low) — never render a fabricated score.
|
|
578
587
|
|
|
579
588
|
## Findings — evidence, not writes
|
|
580
589
|
|
|
581
590
|
`Finding` (`@lotics/ui/finding`) is one ranked insight from an AI check — a cross-check
|
|
582
591
|
discrepancy, an audit observation, a briefing item — laid out HORIZONTALLY, in the order a reader
|
|
583
|
-
needs it: a severity dot, the `title`, and the `delta` right-aligned on that same line, then
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
right. `detail` is the CONSEQUENCE and is optional — a second sentence restating the title in
|
|
592
|
+
needs it: a severity dot, the `title`, and the `delta` right-aligned on that same line, then the
|
|
593
|
+
`readings` beneath — ONE PER LINE, each a dim `source` plus its `value`, never joined by a
|
|
594
|
+
separator. `detail` is the CONSEQUENCE and is optional — a second sentence restating the title in
|
|
587
595
|
longer words costs a line and adds nothing.
|
|
588
596
|
|
|
589
597
|
BOTH readings stay on screen and NEITHER is struck through or marked wrong: a finding reports
|
|
@@ -602,7 +610,7 @@ persisted check-status goes stale on the next edit). Stack several most-severe f
|
|
|
602
610
|
|
|
603
611
|
The outputs accrue as a history the user can CLEAR ("New session"); the APP owns the evolving
|
|
604
612
|
state, each run is a discrete task. (The agent may re-read the session for "make it a bit less",
|
|
605
|
-
but it's a run LOG, not a conversation transcript
|
|
613
|
+
but it's a run LOG, not a conversation transcript.) The session key
|
|
606
614
|
and history reads are the SDK's: `sessionId` on each run, `useAgentRuns` for the history — see
|
|
607
615
|
[the SDK doc](../../app-sdk/docs/ai.md).
|
|
608
616
|
|
|
@@ -684,13 +692,11 @@ kit does:
|
|
|
684
692
|
(white centre — pulses only when live), `done` a filled accent dot + **white check**,
|
|
685
693
|
`upcoming` a faint **grey** ring, the terminal `complete` a **blackish ring + black check**,
|
|
686
694
|
`warning` **amber**. Default accent is neutral ink (`Stepper`'s `color` prop themes it);
|
|
687
|
-
`AgentRun` keeps the neutral default
|
|
688
|
-
group is a `done`/`warning` row, and there is no global terminal node (the run ends on text).
|
|
695
|
+
`AgentRun` keeps the neutral default.
|
|
689
696
|
- **ChangeBand** — the removed value on the light **red** band with the `−` marker, the incoming
|
|
690
697
|
value on the light **emerald** band with `+` (the GitHub-diff idiom, markers in one aligned
|
|
691
698
|
column); everything else in a review stays neutral — a decided row reads a single emerald
|
|
692
699
|
check, an add/remove record card wears the quiet tone wash (50 body, 100 header, 200 border).
|
|
693
|
-
Colour marks the change, never the chrome.
|
|
694
700
|
- **Finding severity** — the coloured dot badge (red / amber / zinc / emerald), most severe
|
|
695
701
|
first.
|
|
696
702
|
|
|
@@ -698,5 +704,5 @@ Card chrome (borders, microlabels) stays neutral — colour marks the *state*, n
|
|
|
698
704
|
The **composer keeps its icons**: `Composer` compact is a single-row pill with an optional
|
|
699
705
|
circular attach `actionsButton` + a circular send button; expanded it adds a `FileThumbnail`
|
|
700
706
|
attachment row (each `onRemove`-able) above the input; `AgentProgress` is the `WaveAvatar` pill.
|
|
701
|
-
"
|
|
707
|
+
The "no icons" rule covers the review surfaces' sparkles and severity glyphs, not functional
|
|
702
708
|
affordances.
|