@lotics/ui 45.10.0 → 45.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.
- package/AGENTS.md +70 -137
- package/docs/ai_patterns.md +88 -150
- package/docs/catalog.md +206 -284
- 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/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
|
|
@@ -192,33 +177,30 @@ PUT and rolls the calls out BELOW it on press. The run ALWAYS ends on the agent'
|
|
|
192
177
|
no global terminal node.
|
|
193
178
|
|
|
194
179
|
**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
|
-
call targets or by whether it's awaiting; return `undefined` to fall back. Unknown names fall back
|
|
180
|
+
or a `dynamic-tool`'s `toolName`), resolved through the active locale's **`agentRun.tools`** map.
|
|
181
|
+
An optional **`labelForCall`** override still wins, for phrasing a call by what it TARGETS rather
|
|
182
|
+
than by which tool ran. It gets the whole call — `{ toolName, input, state }` (`AgentToolCall`);
|
|
183
|
+
return `undefined` to fall back. Unknown names fall back
|
|
200
184
|
to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool `state` folds ai's
|
|
201
185
|
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
|
-
|
|
186
|
+
call is parked on a human decision — an amber dot + an "awaiting" annotation, and it NEVER
|
|
187
|
+
pulses); `approval-responded` → running; `output-denied` → error carrying the
|
|
204
188
|
denial reason. The whole-run `state` prop (`"streaming" | "done" | "error"`) defaults to `streaming`
|
|
205
189
|
while any tool is running, else `done` (an awaiting call is not running, so it settles).
|
|
206
190
|
|
|
207
191
|
**Errors — two kinds, both surfaced, neither shouting.** An INTERMEDIARY tool failure rides in
|
|
208
192
|
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
|
|
193
|
+
group's header goes amber too), the reason waits in the expanded Error panel. A BREAKING error that terminates the whole run lives
|
|
211
194
|
OUTSIDE `parts` (chat persists it in the message's `errors`; `useAgentRun().error` carries it) —
|
|
212
195
|
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
|
|
196
|
+
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
197
|
action (a secondary `Button`) sits INSIDE that panel so the operator can re-fire the run; omit it
|
|
216
198
|
and the panel carries the message alone.
|
|
217
199
|
|
|
218
200
|
**In a bounded container, wrap it in `FollowScroll`.** An `AgentRun` streaming inside a dialog,
|
|
219
201
|
drawer, or fixed-height panel grows BELOW the fold — a plain scroll container doesn't follow. Wrap
|
|
220
202
|
the feed: `<FollowScroll style={{ maxHeight: … }}><AgentRun … /></FollowScroll>` — the same
|
|
221
|
-
inverted-list mechanism as a chat message list
|
|
203
|
+
inverted-list mechanism as a chat message list: the follow happens at
|
|
222
204
|
LAYOUT level (each growth paints already pinned — no scroll-after-paint flash), scrolling up
|
|
223
205
|
releases it, and the feed stays at the newest content when the run settles (`AgentProgress`'s
|
|
224
206
|
expanded panel does this). When settling REPLACES the feed with different content (a results
|
|
@@ -233,7 +215,7 @@ for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={
|
|
|
233
215
|
install; the imports are type-only (purity-enforced), so no `ai` runtime ever enters a bundle.
|
|
234
216
|
**A run with no parts yet renders the breathing "Starting…" row** (streaming state only) — the
|
|
235
217
|
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)
|
|
218
|
+
part (upload + run creation + first token). **The law: pass
|
|
237
219
|
`state="streaming"` from the moment the paid CTA fires — uploads included — and render
|
|
238
220
|
`AgentRun` even with zero parts. Never hand-roll a text/skeleton placeholder in front of the
|
|
239
221
|
feed**; the starting row IS the placeholder, consistent and localized.
|
|
@@ -275,17 +257,15 @@ into it while running and reveals again when done.
|
|
|
275
257
|
## Review before apply — TWO atoms and a hook, composed into your screen
|
|
276
258
|
|
|
277
259
|
**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.
|
|
260
|
+
supplied, whether or not they replace anything. Modifying an existing record takes the full
|
|
261
|
+
diff. Creating records FROM A DOCUMENT takes the same gate in a different shape — no diff, but
|
|
262
|
+
the whole record laid out and editable before the write (`examples/tpl_item_list.tsx`). Only
|
|
263
|
+
values a HUMAN TYPED skip it: the manual form saves direct and ends on the receipt.
|
|
283
264
|
|
|
284
265
|
**There is no review CONTAINER, deliberately.** A review is not a place you go; it is something
|
|
285
266
|
that happens to values already on screen. So the diff renders wherever the value renders — a
|
|
286
267
|
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.
|
|
268
|
+
alignment and rhythm.
|
|
289
269
|
|
|
290
270
|
### The three pieces
|
|
291
271
|
|
|
@@ -294,39 +274,28 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
294
274
|
dense rows; `align`/`tabular` for money so the struck value and its replacement line up;
|
|
295
275
|
`tone` when the change itself is the bad news. **The three ops are the same component:**
|
|
296
276
|
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.
|
|
277
|
+
passes both, and **equal values collapse to one** — agreement is not a change.
|
|
278
|
+
**Both values stay on screen** on a real change, and **`delta`** prints the movement itself — "+250 pcs",
|
|
279
|
+
"−118.519 ₫ VAT".
|
|
302
280
|
|
|
303
281
|
**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
|
|
282
|
+
system's error colour. Red-against-green is also the one pair a colour-blind reader cannot
|
|
283
|
+
separate. The strikethrough carries "going away" with no colour at all; weight, tone and (stacked) size
|
|
308
284
|
carry the rest.
|
|
309
285
|
- **`DiffMark`** (`@lotics/ui/diff_mark`) — what happened to the row holding the value, for the
|
|
310
286
|
cases no single value carries: `added` / `changed` / `removed` / `unchanged`. Glyph plus an
|
|
311
|
-
accessible word, never colour alone.
|
|
312
|
-
real review are untouched.
|
|
287
|
+
accessible word, never colour alone.
|
|
313
288
|
|
|
314
289
|
**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.
|
|
290
|
+
list and the thumbnail grid all draw the same mark. `unchanged` renders as an empty
|
|
291
|
+
disc-sized HOLE: it keeps the column's width and announces nothing.
|
|
321
292
|
|
|
322
293
|
**Derive the kind from the same before/after the row's `DiffValue` carries — never from a
|
|
323
294
|
boolean "did this differ".** A field with no prior value (`before` empty or absent) that a run
|
|
324
295
|
now proposes a value for is an ADD, not a CHANGE, and a local review row that only asks
|
|
325
296
|
"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
|
|
297
|
+
on a value that was never there to begin with. The first proposal any fresh record ever gets
|
|
298
|
+
has an empty `before` on every field, so every mark on that one run is wrong until the kind is derived per field
|
|
330
299
|
(`before === after` → no mark; `before === ""` → `added`; otherwise → `changed`).
|
|
331
300
|
- **`useChangeSet(ids, { initial })`** (`@lotics/ui/use_change_set`) — the bookkeeping:
|
|
332
301
|
`status`/`accept`/`reject`/`undo`/`acceptAll`/`reset`, `accepted`/`rejected`/`pending`,
|
|
@@ -339,33 +308,28 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
339
308
|
- **A record's fields** — a `DetailTable` of rows, each a label plus a `DiffValue`, with
|
|
340
309
|
Keep/Drop on the row and a collapsed state once decided. That row is ~20 lines and it belongs
|
|
341
310
|
to your screen: `examples/tpl_record.tsx` and `dev/pages/ai.tsx` each write their own and they
|
|
342
|
-
read differently
|
|
311
|
+
read differently.
|
|
343
312
|
- **A record SET** — a `Table`, one row per proposed record, `DiffMark` leading and `DiffValue`
|
|
344
|
-
inside the figure cells.
|
|
313
|
+
inside the figure cells.
|
|
345
314
|
- **A conflict** — two documents disagree, so the value is the read-only OUTCOME of a pick:
|
|
346
315
|
a `DiffValue` showing the placeholder until resolved, over `CardSelectItem` candidates (each
|
|
347
316
|
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.
|
|
317
|
+
Keep until one is chosen.
|
|
350
318
|
- **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
|
|
319
|
+
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
320
|
are NODES (a `FileBadge` plus its name). A file replacing one already filed shows both, and
|
|
354
321
|
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`.
|
|
322
|
+
a `View` child. Worked in `examples/tpl_record.tsx`.
|
|
357
323
|
- **A DOCUMENT SET** — a files section is not a field. It is a set of objects with identity,
|
|
358
324
|
so what changes is MEMBERSHIP plus per-document properties, and the kinds do not share a
|
|
359
325
|
treatment. **Added** and **removed** are membership: the row's existence and its mark are
|
|
360
326
|
the whole change. **Superseded** is one slot whose bytes changed — the row names the new
|
|
361
327
|
file and its second line names the one it displaces. **Reclassified** is the one a review
|
|
362
328
|
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
|
|
329
|
+
belongs on the property. `FileRow` takes the mark in `leading` (so the names still form a column) and a node
|
|
365
330
|
`meta` (so the second line can carry a diff).
|
|
366
331
|
|
|
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
|
|
332
|
+
**In a GRID** the same set reads differently again: a grid is scanned whole, so it carries membership (this is arriving, that
|
|
369
333
|
is leaving — `FileThumbnailGrid diffs`, a corner mark plus a fade) at a glance and carries
|
|
370
334
|
pairing not at all. Pick the surface for the change; do not shrink the change to fit a shape.
|
|
371
335
|
|
|
@@ -373,56 +337,45 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
373
337
|
a tile — the badge earns its space and the row is built around it. Where the file is one
|
|
374
338
|
VALUE among several columns, render the filename as a value at the column's own text size:
|
|
375
339
|
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.
|
|
340
|
+
size a cell allows), and sizing it back up makes it set the row's height.
|
|
378
341
|
|
|
379
342
|
**The strikethrough has a discriminator.** Strike the superseded file when it shares a CELL
|
|
380
343
|
with its replacement, adjacent — a table's document column, where the pairing is what the
|
|
381
344
|
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`.
|
|
345
|
+
a word names the relation and the chip only identifies the file. Both shapes are worked in `dev/pages/ai.tsx`.
|
|
384
346
|
- **A record being CREATED from a document** — no before-state, so no diff: the whole record
|
|
385
347
|
is the review. Lay every field out as it will be stored, editable in place, and mark only
|
|
386
348
|
the fact that IS relational (matched onto something existing, or new alongside it). One
|
|
387
349
|
commit named for the outcome. Worked in `examples/tpl_item_list.tsx`.
|
|
388
350
|
- **Display-only** — a finding that informs but has nothing to decide simply is not in the
|
|
389
|
-
change set.
|
|
351
|
+
change set.
|
|
390
352
|
|
|
391
353
|
### The laws that survive the container
|
|
392
354
|
|
|
393
355
|
- **Nothing auto-applies.** The commit is one explicit press, named for its outcome
|
|
394
356
|
("Update record (3)", "Add 4 tasks") — never "Apply" alone, never "OK".
|
|
395
357
|
- **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.
|
|
358
|
+
on a value they cannot touch.
|
|
398
359
|
- **Gate the commit on what will actually be written**, not on the proposal count — disable at
|
|
399
360
|
zero kept.
|
|
400
361
|
- **A decided row's Keep/Drop must be reachable from BOTH directions when `initial: "accepted"`.**
|
|
401
362
|
`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
|
|
363
|
+
(`initial: "accepted"`) a row starts already decided and Undo is a no-op with nothing to
|
|
364
|
+
reach. The collapsed-to-Undo anatomy (value + one "Undo" button, swapping to Keep/Drop only
|
|
406
365
|
while `pending`) is correct ONLY under `initial: "pending"`, where every row genuinely starts
|
|
407
366
|
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.
|
|
367
|
+
both buttons present, the one matching the row's current status disabled, the other live.
|
|
410
368
|
- **The mark rides the LABEL, and the value sits in an `InlineSlot`.** Both are alignment
|
|
411
369
|
before they are semantics. A mark beside the VALUE indents every row it marks by its own
|
|
412
370
|
width plus a gap, so an unmarked row needs a spacer of exactly the glyph's width to keep up —
|
|
413
|
-
and they drift
|
|
371
|
+
and they drift. The label column is a fixed width with one
|
|
414
372
|
left edge, so a mark placed there aligns down the page for free. Give the slot a fixed HEIGHT
|
|
415
373
|
as well as width: it is an inline-flex box inside `DetailRow`'s label `Text`, so an empty one
|
|
416
374
|
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.
|
|
375
|
+
with 20px flat diffs; without the slot the column has two left edges and two baselines.
|
|
419
376
|
- **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
|
|
377
|
+
under the whole cell — which is under the Keep/Drop buttons. Render it inside the value block.
|
|
378
|
+
- **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
379
|
value, so the same component tree serves a record with one change and a record with twenty).
|
|
427
380
|
Emphasis, not omission, is what makes the changes findable.
|
|
428
381
|
- **The fixture must not flatter.** A review demo whose every row carries a change proves
|
|
@@ -435,8 +388,7 @@ shape its root draws, and every surface needing a diff somewhere else rebuilds i
|
|
|
435
388
|
`Clarify` (`@lotics/ui/clarify`): when the agent is unsure, it asks a question with quick-reply
|
|
436
389
|
options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question` (the
|
|
437
390
|
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
|
|
391
|
+
answer carries a REQUIRED one-line description; a `ChoiceList`), `onAnswer(value)` resumes the run, the controlled `answer` keeps the pick
|
|
440
392
|
switchable until committed. **`allowCustom`** adds an always-visible, borderless, auto-growing multiline field below the
|
|
441
393
|
options — typing it IS the selection; its text becomes the value (any value not matching an option
|
|
442
394
|
reads as the custom answer).
|
|
@@ -446,7 +398,7 @@ Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebro
|
|
|
446
398
|
question — no progress bar, and no eyebrow at all for a single question. Each step is a `Clarify`; advance only once the
|
|
447
399
|
current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
|
|
448
400
|
`custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
|
|
449
|
-
locale-wired (the `clarify` slice).
|
|
401
|
+
locale-wired (the `clarify` slice). **Placement follows
|
|
450
402
|
the dialog grammar**: standalone (riding a parked run's stream, where no footer exists) the wizard
|
|
451
403
|
renders its own inline action row; as a DIALOG phase, wrap the `Dialog` with `ClarifyWizardScope`
|
|
452
404
|
and put `ClarifyWizardActions` in the `DialogFooter` — the wizard suppresses the inline row and
|
|
@@ -474,29 +426,25 @@ const run = useAgentRun("intake"); // @lotics/app-sdk
|
|
|
474
426
|
after a question it is the only thing holding how the answered leg ended — your own `run()` promise
|
|
475
427
|
already resolved, with `parked`, and never resolves again. Give `onLanding` the same handler you
|
|
476
428
|
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.
|
|
429
|
+
ask. Omit it and an answered run completes with the host none the wiser — your review step never
|
|
430
|
+
opens, and the paid read is discarded. A host that instead reads its result off `status`/`output`
|
|
431
|
+
does not need it.
|
|
481
432
|
|
|
482
433
|
**The parked question REPLACES the feed — that is the contract, not a style.** The run is blocked
|
|
483
434
|
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
|
|
435
|
+
its actions sit outside it. Stacked under the transcript in an unscrollable box, a multi-question
|
|
436
|
+
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
437
|
also swaps `FollowScroll` for `DialogScrollArea` on the park, so the question opens at the top
|
|
488
438
|
rather than wherever the feed was scrolled to.
|
|
489
439
|
|
|
490
440
|
**Mount `AgentRunActions` wherever the run drives the surface — never gated on one phase.** It
|
|
491
441
|
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.
|
|
442
|
+
it settles; the RUN decides. A host that names a single phase out here duplicates that decision.
|
|
495
443
|
|
|
496
444
|
**`run` is a SHAPE, not an import** (`AgentRunLike`: `status`, `parts`, `pendingChoice`,
|
|
497
445
|
`answerChoice`, `cancel`, `error`; `AgentRunQuestion` names what `pendingChoice` carries, so a template or
|
|
498
446
|
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
|
|
447
|
+
label, which is the `ask_user_choice` wire shape). `useAgentRun()` satisfies it structurally — `@lotics/ui` never depends on
|
|
500
448
|
`@lotics/app-sdk` — which also means a template or test can hand it a plain object and exercise the
|
|
501
449
|
whole pane with no backend ([`tpl_item_list`](../examples/tpl_item_list.tsx) does exactly that).
|
|
502
450
|
|
|
@@ -533,28 +481,24 @@ list renders nothing.
|
|
|
533
481
|
reader is deciding about it. The harder surface is the one they open a week later: a stored
|
|
534
482
|
record whose fields were written by a person, by a chat agent, by an extraction off an upload,
|
|
535
483
|
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.
|
|
484
|
+
which. Three rules, each of which a screen built for a single writer gets wrong for free.
|
|
538
485
|
|
|
539
486
|
**An unwritten value must not render like a written one.** A row assembled by an automation
|
|
540
487
|
arrives with its enums set and its prose empty, and the natural repair — composing a label out
|
|
541
488
|
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
|
|
489
|
+
this up" and "this is what happened" then look identical. Say the value is missing, in the ink an unfilled field already uses
|
|
544
490
|
(`Timeline`'s `placeholder`; the same idea wherever a component takes a `label: string` and
|
|
545
491
|
therefore cannot express absence — that type is the trap, so give it a way to say so).
|
|
546
492
|
|
|
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
|
|
493
|
+
**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
494
|
line above ("Written by AI from the recording" — the source alone is not authorship), and drop
|
|
551
495
|
the machine's text into a recessed `Inset`, which says it was not written on this page. Leave
|
|
552
496
|
the person's conclusion on the page's own ground; that contrast IS the signal.
|
|
553
497
|
|
|
554
498
|
**Model prose arrives with its own type ladder — take it away at the door.** A model asked for
|
|
555
499
|
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
|
-
|
|
500
|
+
document-scale markdown hands those an `h1` equal to your section heading. Render every
|
|
501
|
+
embedded value with
|
|
558
502
|
`<Markdown variant="embedded">`.
|
|
559
503
|
|
|
560
504
|
And one that is not about treatment at all: **check that the read path projects every field the
|
|
@@ -568,22 +512,18 @@ Confidence IS a callout (`@lotics/ui/confidence`) — the ONLY form it takes: th
|
|
|
568
512
|
meter + level phrase head the block, and the REQUIRED `children` carry the basis — what was
|
|
569
513
|
checked and what came of it ("all 7 references pass their checksum" / "2 of 7 fail (X, Y) —
|
|
570
514
|
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,
|
|
515
|
+
meter is the only colored element. Pass `level` or a 0–1 `score` (≥0.8 high,
|
|
573
516
|
≥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
|
|
517
|
+
without its reasons cannot be acted on, so it cannot be rendered. Derive levels HONESTLY (deterministic checks: all pass → high, any
|
|
577
518
|
fail → low) — never render a fabricated score.
|
|
578
519
|
|
|
579
520
|
## Findings — evidence, not writes
|
|
580
521
|
|
|
581
522
|
`Finding` (`@lotics/ui/finding`) is one ranked insight from an AI check — a cross-check
|
|
582
523
|
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
|
|
524
|
+
needs it: a severity dot, the `title`, and the `delta` right-aligned on that same line, then the
|
|
525
|
+
`readings` beneath — ONE PER LINE, each a dim `source` plus its `value`, never joined by a
|
|
526
|
+
separator. `detail` is the CONSEQUENCE and is optional — a second sentence restating the title in
|
|
587
527
|
longer words costs a line and adds nothing.
|
|
588
528
|
|
|
589
529
|
BOTH readings stay on screen and NEITHER is struck through or marked wrong: a finding reports
|
|
@@ -602,7 +542,7 @@ persisted check-status goes stale on the next edit). Stack several most-severe f
|
|
|
602
542
|
|
|
603
543
|
The outputs accrue as a history the user can CLEAR ("New session"); the APP owns the evolving
|
|
604
544
|
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
|
|
545
|
+
but it's a run LOG, not a conversation transcript.) The session key
|
|
606
546
|
and history reads are the SDK's: `sessionId` on each run, `useAgentRuns` for the history — see
|
|
607
547
|
[the SDK doc](../../app-sdk/docs/ai.md).
|
|
608
548
|
|
|
@@ -684,13 +624,11 @@ kit does:
|
|
|
684
624
|
(white centre — pulses only when live), `done` a filled accent dot + **white check**,
|
|
685
625
|
`upcoming` a faint **grey** ring, the terminal `complete` a **blackish ring + black check**,
|
|
686
626
|
`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).
|
|
627
|
+
`AgentRun` keeps the neutral default.
|
|
689
628
|
- **ChangeBand** — the removed value on the light **red** band with the `−` marker, the incoming
|
|
690
629
|
value on the light **emerald** band with `+` (the GitHub-diff idiom, markers in one aligned
|
|
691
630
|
column); everything else in a review stays neutral — a decided row reads a single emerald
|
|
692
631
|
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
632
|
- **Finding severity** — the coloured dot badge (red / amber / zinc / emerald), most severe
|
|
695
633
|
first.
|
|
696
634
|
|
|
@@ -698,5 +636,5 @@ Card chrome (borders, microlabels) stays neutral — colour marks the *state*, n
|
|
|
698
636
|
The **composer keeps its icons**: `Composer` compact is a single-row pill with an optional
|
|
699
637
|
circular attach `actionsButton` + a circular send button; expanded it adds a `FileThumbnail`
|
|
700
638
|
attachment row (each `onRemove`-able) above the input; `AgentProgress` is the `WaveAvatar` pill.
|
|
701
|
-
"
|
|
639
|
+
The "no icons" rule covers the review surfaces' sparkles and severity glyphs, not functional
|
|
702
640
|
affordances.
|