@lotics/ui 13.9.0 → 14.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.
@@ -93,26 +93,27 @@ transition — never leave a live button waiting on a network round-trip:
93
93
  ## Show the work — `AgentRun`
94
94
 
95
95
  `AgentRun` (`@lotics/ui/agent_run`) is a live feed of the agent's work as a TIMELINE. The prop is
96
- an ORDERED `items` array (`AgentRunItem`):
96
+ the agent message's **ai-sdk `parts`** (`UIMessagePart[]`) — the same wire shape chat and app
97
+ agents emit; `@lotics/ui` owns the transform from parts to the render timeline, so there is no
98
+ bespoke transcript type. It renders three part kinds:
97
99
 
98
- | Item | Shape | Renders as |
99
- |---|---|---|
100
- | `{ type: "text", id, text }` | Answer prose | `Markdown`, a trailing caret on the last segment while streaming |
101
- | `{ type: "reasoning", id, text }` | Thinking | COLLAPSED a muted "Thinking" row; press to reveal the Markdown |
102
- | `{ type: "step", id, label, status, kind?, input?, output?, errorText?, detail?, peek? }` | A tool/step call | An activity row with a status dot |
103
-
104
- Items appear in the order they happened — a real run is think → text → a burst of calls → more
105
- text, NOT all text on top of a flat step list. Transparent work, NEVER a bare spinner.
106
-
107
- **Progressive disclosure.** The feed shows the label + state; the detail is revealed on demand.
108
- A `step`'s `input`/`output` are hidden in the row but open in a press-to-reveal **peek**
109
- (auto-built Input / Output `JsonPanel`s). On `status: "error"` the row's dot goes amber, the
110
- `detail` line reads in the danger ink, and `errorText` shows in the peek's Error panel. Pass an
111
- explicit `peek` node to render that reveal yourself (it overrides the auto panels). `detail` is
112
- OPTIONAL a short human summary (a count), never invented prose or raw I/O (that's
113
- `input`/`output`).
114
-
115
- **Activity grouping.** Consecutive `step` items fold into ONE group. While the agent is mid-tools
100
+ | Part | Renders as |
101
+ |---|---|
102
+ | `text` | Answer prose (`Markdown`), grows as it streams |
103
+ | `reasoning` | Thinking COLLAPSED, a muted "Thinking" row; press to reveal the Markdown |
104
+ | a tool part `dynamic-tool` (app agents) or `tool-<name>` (chat) | An activity row with a status dot |
105
+
106
+ Parts appear in the order they happened — a real run is think → text → a burst of calls → more
107
+ text, NOT all text on top of a flat step list. Content parts with no feed representation
108
+ (`source-*`, `file`, `data-*`, `step-start`, `custom`) are skipped. Transparent work, NEVER a bare spinner.
109
+
110
+ **Progressive disclosure.** The feed shows the label + state; the detail is revealed on demand. A
111
+ tool part's `input`/`output` are hidden in the row and **expand in place** on press — auto-built
112
+ Input / Output `JsonPanel`s roll out under the row (the same disclosure pattern as "Thinking",
113
+ never a popover). A tool in an `output-error` state flows like any other call the dot goes
114
+ amber and its `errorText` shows in the expanded Error panel.
115
+
116
+ **Activity grouping.** Consecutive tool parts fold into ONE group. While the agent is mid-tools
116
117
  the tail group is a SINGLE pulsing row whose label swaps in place as each call fires (no growing
117
118
  stack of dots); once prose resumes the group settles: a single call becomes one done row, and a
118
119
  multi-call group becomes a persistent "{final action} · {n} steps" HEADER (a `complete` terminal
@@ -120,17 +121,28 @@ dot — distinct from the filled `done` step dots; amber `warning` if any call e
120
121
  PUT and rolls the calls out BELOW it on press. The run ALWAYS ends on the agent's text — there is
121
122
  no global terminal node.
122
123
 
123
- **Tool labels.** A `step` with `kind: "tool"` carries the RAW tool name (`update_records`),
124
- resolved via a built-in map of the platform record/document tools + an optional `labelForTool`
125
- override (localize THERE — the kit stays English); unknown names fall back to a prettified form.
126
- `stepsLabel` localizes the "{n} steps" suffix. `state` (`"streaming" | "done" | "error"`)
127
- defaults to `streaming` while any step is running, else `done`.
128
-
129
- Fed natively by `@lotics/app-sdk`'s `useAgentRun().items` (reasoning + per-tool I/O + state come
130
- for free — no hand-assembly): `<AgentRun items={run.items} state={…} />` — see
131
- [the SDK doc](../../app-sdk/docs/ai.md) for the hook.
132
-
133
- **Limitation:** the collapsed reasoning row's "Thinking" label and the auto peek's
124
+ **Tool labels.** A tool part carries the RAW tool name (`update_records` from `tool-update_records`,
125
+ or a `dynamic-tool`'s `toolName`), resolved via a built-in map of the platform record/document
126
+ tools + an optional `labelForTool` override (localize THERE — the kit stays English); unknown
127
+ names fall back to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool
128
+ `state` collapses ai's lifecycle to running / done / error (an approval-pending state reads as
129
+ running — a read-only feed has no approval control). The whole-run `state` prop
130
+ (`"streaming" | "done" | "error"`) defaults to `streaming` while any tool is running, else `done`.
131
+
132
+ **Errors two kinds, both surfaced, neither shouting.** An INTERMEDIARY tool failure rides in
133
+ that tool's `output-error` part and flows like a normal call — the amber dot marks it (a settled
134
+ group's header goes amber too), the reason waits in the expanded Error panel; real runs retry and
135
+ move on, so the feed doesn't dramatize it. A BREAKING error that terminates the whole run lives
136
+ OUTSIDE `parts` (chat persists it in the message's `errors`; `useAgentRun().error` carries it) —
137
+ pass it as the `error` prop and it renders as a terminal danger row under the transcript.
138
+
139
+ Fed natively by `@lotics/app-sdk`'s `useAgentRun().parts` (reasoning + per-tool I/O + state come
140
+ for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={run.error} />` — see
141
+ [the SDK doc](../../app-sdk/docs/ai.md) for the hook. Requires `ai` as an optional (type-only) peer.
142
+ **Reasoning only shows when the model produces it** — the declared model must support thinking
143
+ (Sonnet 4.6 / Opus 4.8, not Haiku); the renderer shows every reasoning part it's handed.
144
+
145
+ **Limitation:** the collapsed reasoning row's "Thinking" label and the expanded detail's
134
146
  "Input"/"Output"/"Error" panel titles are currently English-only (not on the locale provider and
135
147
  not prop-overridable); `labelForTool`/`stepsLabel` localize everything else in the feed.
136
148
 
@@ -243,15 +255,23 @@ plain `useState` (controlled `status` everywhere); the family owns the mechanics
243
255
  Undo, the counter, Keep-all, apply gating). Never apply a field with no value — gate unresolved
244
256
  conflicts (`keepDisabled`, `applyDisabled`). Editing IS the review.
245
257
 
246
- ## Ask back — `Clarify`
258
+ ## Ask back — `Clarify` / `ClarifyWizard`
247
259
 
248
260
  `Clarify` (`@lotics/ui/clarify`): when the agent is unsure, it asks a question with quick-reply
249
- options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question`,
250
- `options` (`{ label, value, description? }` — a `ChoiceList`), `onAnswer(value)` resumes the run,
251
- and the controlled `answer` keeps the pick switchable until committed.
252
-
253
- **Limitation:** the "Question" eyebrow above the prompt is currently hardcoded English (no label
254
- prop, not on the locale provider).
261
+ options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question` (the
262
+ prompt, rendered as-is — no eyebrow chrome), `options` (`{ label, value, description }` — every
263
+ answer carries a REQUIRED one-line description so the human picks without re-deriving it from the
264
+ question; a `ChoiceList`), `onAnswer(value)` resumes the run, the controlled `answer` keeps the pick
265
+ switchable until committed. **`allowCustom`** adds an always-visible, borderless, auto-growing multiline field below the
266
+ options typing it IS the selection; its text becomes the value (any value not matching an option
267
+ reads as the custom answer).
268
+
269
+ `ClarifyWizard` (`@lotics/ui/clarify_wizard`): a SEQUENCE of clarify questions worked one at a time —
270
+ Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebrow ("1 / 3") above the
271
+ question — no progress bar. Each step is a `Clarify`; advance only once the
272
+ current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
273
+ `custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
274
+ locale-wired (the `clarify` slice). The multi-question form of the ask-back.
255
275
 
256
276
  ## Provenance — `Sources`
257
277
 
package/docs/catalog.md CHANGED
@@ -962,11 +962,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
962
962
  - **`composer`** — `Composer`: the adaptive command/chat composer — a compact pill that
963
963
  expands for long text + attachments (`pills` + attachment slots, controlled or
964
964
  uncontrolled `value`, `onSend`/`onStop`, `sendDisabled` override).
965
- - **`agent_run`** — `AgentRun` + `AgentRunStep`/`AgentRunItem` + `resolveToolMeta`: the live
966
- streaming work feed. AI work ONLY deterministic work (template fill, an export) shows a
967
- `loading` state on its trigger and delivers at once; dressing it as an agent run is theater.
968
- - **`agent_progress`** `AgentProgress`: its compact, floating, expandable form a
969
- composer's working state.
965
+ - **`agent_run`** — `AgentRun` + `resolveToolMeta`: the live streaming work feed. Its prop is the
966
+ agent message's ai-sdk `parts` (`UIMessagePart[]`text, reasoning, and tool parts), fed straight
967
+ from `useAgentRun().parts`; renders prose, a collapsed "Thinking" disclosure, and grouped tool
968
+ steps whose I/O expands in place on press (no popover). A per-tool failure shows amber with the
969
+ reason in its expanded Error panel; a run-level
970
+ breaking `error` (outside `parts`) renders as a terminal danger row. Needs `ai` as an optional
971
+ type-only peer. AI work ONLY —
972
+ deterministic work (template fill, an export) shows a `loading` state on its trigger and delivers
973
+ at once; dressing it as an agent run is theater.
974
+ - **`agent_transform`** — internal (no component): the one place that folds an ai-sdk `UIMessage`'s
975
+ `parts` into the render timeline (`AgentUIPart`, `toSegments`, `lastRunningStep`), shared by
976
+ `AgentRun` and `AgentProgress`. The sole file permitted to `import type` from `ai`.
977
+ - **`agent_progress`** — `AgentProgress`: `AgentRun`'s compact, floating, expandable form (same
978
+ `parts` prop) — a composer's working state.
970
979
  - **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: calibrated
971
980
  high/med/low; localized via the provider.
972
981
  - **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
@@ -980,11 +989,19 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
980
989
  `ChangeRecord` (THE item card: registers like a Change; tone wash + localized op word;
981
990
  verb level follows the decision level) · `ChangeBand` (the raw ± band) ·
982
991
  `ChangeValueInput` (the diff-at-rest editor); the `changeReview` locale slice.
983
- - **`clarify`** — `Clarify`: the agent asks back — a borderless block (question + a
984
- `ChoiceList`), no card wrapper.
992
+ - **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
993
+ (the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
994
+ the question — e.g. a wizard's "1 / 3"). `ClarifyOption` requires
995
+ `{ value, label, description }` — the description is MANDATORY (every answer explains itself).
996
+ `allowCustom` adds an always-visible borderless multiline custom-answer field below the options.
997
+ - **`clarify_wizard`** — `ClarifyWizard` + `ClarifyWizardQuestion`/`ClarifyWizardAnswer`: a SEQUENCE
998
+ of `Clarify` questions with Back/Next/Cancel/Submit + a `n / total` position indicator; advance
999
+ only when the current is answered, `onSubmit` returns one `{ value, custom }` per question.
1000
+ Navigation chrome is locale-wired (the `clarify` locale slice).
985
1001
  - **`choice_list`** — `ChoiceList` + `ChoiceOption`: selectable answer options as
986
1002
  divider-separated rows (no bordered cards) with a per-row focus ring + hover wash; the
987
- agent's quick-reply surface.
1003
+ agent's quick-reply surface. `allowCustom` appends an always-visible borderless multiline field whose
1004
+ text IS the value (a value not in `options` reads as the custom answer).
988
1005
  - **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
989
1006
  knowledge): provenance chips, per-kind glyphs.
990
1007
  - **`finding`** — `Finding` + `FindingComparison` + `FindingSeverity`/`FindingLabels`: one
@@ -1023,4 +1040,5 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1023
1040
  a programmatic focus restore inside the suppression window doesn't re-open the editor it
1024
1041
  just closed.
1025
1042
  - **`json_panel`** — `JsonPanel` (`{title, value}`) + `stringifyData`: a labeled monospace
1026
- panel for raw/JSON payloads (debug & developer surfaces).
1043
+ panel for raw/JSON payloads (debug & developer surfaces). Height-capped (240) and scrolls
1044
+ inside, so a huge dump (a streamed tool input) can never blow the surface open.
@@ -10,7 +10,9 @@ import { InlineTextInput } from "@lotics/ui/inline_text_input";
10
10
  import { AgentProgress } from "@lotics/ui/agent_progress";
11
11
  import { Composer } from "@lotics/ui/composer";
12
12
  import { FileThumbnail, type DisplayFile } from "@lotics/ui/file_thumbnail";
13
- import { type AgentRunStep, type AgentRunItem } from "@lotics/ui/agent_run";
13
+ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
14
+
15
+ type Part = UIMessagePart<UIDataTypes, UITools>;
14
16
 
15
17
  // A real app fills the attachment from `useFileUpload` (the uploaded file's URL);
16
18
  // this mock uses an inline placeholder so the example needs no network image.
@@ -34,20 +36,20 @@ const MOCK_PHOTO: DisplayFile = {
34
36
  // carries the single Download. All mock — the recipe, not a CAD engine.
35
37
  // ─────────────────────────────────────────────────────────────────────────────
36
38
 
37
- type ScriptStep = Omit<AgentRunStep, "status">;
39
+ type ScriptStep = { id: string; label: string };
38
40
  type Dims = { L: number; W: number; H: number };
39
41
 
40
42
  const RECOGNIZE: ScriptStep[] = [
41
- { id: "r1", label: "Reading the photo", detail: "A single brown carton on a white sweep, card left for scale." },
42
- { id: "r2", label: "reference_dielines.search", kind: "tool" },
43
- { id: "r3", label: "Identifying the box style", detail: "Four flaps meet at the centre seam → Regular Slotted Container, FEFCO 0201." },
44
- { id: "r4", label: "Estimating dimensions", detail: "Scaling against the reference card — about 320 × 230 × 150 mm." },
45
- { id: "r5", label: "Drafting the dieline", kind: "tool" },
43
+ { id: "r1", label: "Reading the photo" },
44
+ { id: "r2", label: "reference_dielines.search" },
45
+ { id: "r3", label: "Identifying the box style" },
46
+ { id: "r4", label: "Estimating dimensions" },
47
+ { id: "r5", label: "Drafting the dieline" },
46
48
  ];
47
49
 
48
- const editScript = (prompt: string): ScriptStep[] => [
49
- { id: "e1", label: "Reading the request", detail: prompt },
50
- { id: "e2", label: "Recomputing the dieline", kind: "tool" },
50
+ const EDIT_SCRIPT: ScriptStep[] = [
51
+ { id: "e1", label: "Reading the request" },
52
+ { id: "e2", label: "Recomputing the dieline" },
51
53
  ];
52
54
 
53
55
  const ZOOM_MIN = 0.4;
@@ -105,12 +107,13 @@ export function TplDieline() {
105
107
  return () => clearTimeout(t);
106
108
  }, [run, revealed]);
107
109
 
108
- const liveItems: AgentRunItem[] = run
109
- ? run.script.slice(0, revealed).map((s, i) => ({
110
- type: "step",
111
- ...s,
112
- status: revealed < run.script.length && i === revealed - 1 ? "running" : "done",
113
- }))
110
+ const liveItems: Part[] = run
111
+ ? run.script.slice(0, revealed).map((s, i): Part => {
112
+ const running = revealed < run.script.length && i === revealed - 1;
113
+ return running
114
+ ? { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "input-available", input: undefined }
115
+ : { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "output-available", input: undefined, output: undefined };
116
+ })
114
117
  : [];
115
118
 
116
119
  const startRecognize = () => {
@@ -135,7 +138,7 @@ export function TplDieline() {
135
138
  setPhase("working");
136
139
  setRevealed(0);
137
140
  setRun({
138
- script: editScript(text),
141
+ script: EDIT_SCRIPT,
139
142
  onDone: () => {
140
143
  const m = /(\d+)\s*mm/i.exec(text);
141
144
  const delta = m ? Number(m[1]) : 5;
@@ -209,7 +212,7 @@ export function TplDieline() {
209
212
  <View style={{ position: "absolute", left: 0, right: 0, bottom: 24, alignItems: "center", paddingHorizontal: 16, pointerEvents: "none" }}>
210
213
  <View style={{ width: "100%", maxWidth: 620, pointerEvents: "auto" }}>
211
214
  {phase === "working" ? (
212
- <AgentProgress items={liveItems} state="streaming" />
215
+ <AgentProgress parts={liveItems} state="streaming" />
213
216
  ) : (
214
217
  <Composer
215
218
  value={prompt}
@@ -22,11 +22,14 @@ import { useSelection } from "@lotics/ui/use_selection";
22
22
  import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
23
23
  import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
24
24
  import { CardSelectItem } from "@lotics/ui/card_select_item";
25
- import { AgentRun, type AgentRunItem, type AgentRunStep } from "@lotics/ui/agent_run";
25
+ import { AgentRun } from "@lotics/ui/agent_run";
26
26
  import { type SourceRef } from "@lotics/ui/sources";
27
27
  import { ChangeValueInput, Change, ChangeField, ChangeFields, ChangeReasoning, ChangeRecord, ChangeReview, ChangeReviewActions, ChangeReviewHeader, type ChangeStatus } from "@lotics/ui/change_review";
28
28
 
29
29
  import { CompletionState } from "@lotics/ui/completion_state";
30
+ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
31
+
32
+ type Part = UIMessagePart<UIDataTypes, UITools>;
30
33
 
31
34
  // ─────────────────────────────────────────────────────────────────────────────
32
35
  // Template · Document desk — THE worked example for everything document-driven
@@ -50,7 +53,7 @@ import { CompletionState } from "@lotics/ui/completion_state";
50
53
  // split) and drops the toolbar button.
51
54
  // ─────────────────────────────────────────────────────────────────────────────
52
55
 
53
- type ScriptStep = Omit<AgentRunStep, "status">;
56
+ type ScriptStep = { id: string; label: string; detail?: string; kind?: "tool" };
54
57
  type Task = "extract" | "check";
55
58
  type Phase = "fork" | "running" | "review" | "done";
56
59
 
@@ -276,14 +279,16 @@ export function TplDocuments() {
276
279
  return () => clearTimeout(t);
277
280
  }, [phase, revealed, script.length]);
278
281
 
279
- const runItems: AgentRunItem[] = [
280
- { type: "text", id: "in", text: (task === "check" ? "Comparing the documents against the record and each other." + (brief.trim() ? ` Also: ${brief.trim()}` : "") : "Reading the documents and comparing them with the record.") },
281
- ...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i): AgentRunItem => ({
282
- type: "step", ...s, detail: i < revealed ? s.detail : undefined, status: i < revealed ? "done" : "running",
283
- })),
282
+ const runItems: Part[] = [
283
+ { type: "text", text: (task === "check" ? "Comparing the documents against the record and each other." + (brief.trim() ? ` Also: ${brief.trim()}` : "") : "Reading the documents and comparing them with the record.") },
284
+ ...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i): Part =>
285
+ i < revealed
286
+ ? { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "output-available", input: undefined, output: undefined }
287
+ : { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "input-available", input: undefined },
288
+ ),
284
289
  ];
285
290
  if (revealed >= script.length) {
286
- runItems.push({ type: "text", id: "out", text: task === "check" ? "Checked 18 fields — 2 disagree, 1 worth noting. The findings are below." : "3 fields already match; 3 need a decision, and the invoice carries 2 lines the order doesn't have yet." });
291
+ runItems.push({ type: "text", text: task === "check" ? "Checked 18 fields — 2 disagree, 1 worth noting. The findings are below." : "3 fields already match; 3 need a decision, and the invoice carries 2 lines the order doesn't have yet." });
287
292
  }
288
293
 
289
294
  // Apply counts what actually commits: the kept fields.
@@ -498,7 +503,7 @@ export function TplDocuments() {
498
503
  </View>
499
504
  ) : null}
500
505
 
501
- {phase === "running" ? <AgentRun items={runItems} state={revealed >= script.length ? "done" : "streaming"} /> : null}
506
+ {phase === "running" ? <AgentRun parts={runItems} state={revealed >= script.length ? "done" : "streaming"} /> : null}
502
507
 
503
508
  {phase === "review" && task === "extract" ? (
504
509
  <View style={{ gap: 16 }}>
@@ -695,9 +700,11 @@ export function TplDocuments() {
695
700
  </View>
696
701
  ) : genRunning ? (
697
702
  <AgentRun
698
- items={genSteps.slice(0, Math.min(genRevealed + 1, genSteps.length)).map((st, i) => ({
699
- type: "step", id: st.id, label: st.label, status: i < genRevealed ? "done" : "running",
700
- }))}
703
+ parts={genSteps.slice(0, Math.min(genRevealed + 1, genSteps.length)).map((st, i): Part =>
704
+ i < genRevealed
705
+ ? { type: "dynamic-tool", toolName: st.label, toolCallId: st.id, state: "output-available", input: undefined, output: undefined }
706
+ : { type: "dynamic-tool", toolName: st.label, toolCallId: st.id, state: "input-available", input: undefined },
707
+ )}
701
708
  state={genRevealed >= genSteps.length ? "done" : "streaming"}
702
709
  />
703
710
  ) : !genDone ? (
@@ -6,10 +6,13 @@ import { Divider } from "@lotics/ui/divider";
6
6
  import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
7
7
  import { PressableHighlight } from "@lotics/ui/pressable_highlight";
8
8
  import { Composer } from "@lotics/ui/composer";
9
- import { AgentRun, type AgentRunStep, type AgentRunItem } from "@lotics/ui/agent_run";
9
+ import { AgentRun } from "@lotics/ui/agent_run";
10
10
  import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
11
11
  import { Badge } from "@lotics/ui/badge";
12
12
  import { Sources, type SourceRef } from "@lotics/ui/sources";
13
+ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
14
+
15
+ type Part = UIMessagePart<UIDataTypes, UITools>;
13
16
 
14
17
  // ─────────────────────────────────────────────────────────────────────────────
15
18
  // Template · Answer desk — describe the goods on the LEFT, the agent RANKS the
@@ -134,13 +137,13 @@ const CANDIDATES: Candidate[] = [
134
137
  },
135
138
  ];
136
139
 
137
- type ScriptStep = Omit<AgentRunStep, "status">;
140
+ type ScriptStep = { id: string; toolName: string; input?: unknown; output?: unknown };
138
141
  const SCRIPT: ScriptStep[] = [
139
- { id: "r1", label: "Reading the description", detail: "Form, material, processing and presentation extracted." },
140
- { id: "r2", label: "tariff_schedule.search", kind: "tool" },
141
- { id: "r3", label: "Narrowing the heading", detail: "Matching the goods against chapter notes and exclusions." },
142
- { id: "r4", label: "duty_rates.read", kind: "tool" },
143
- { id: "r5", label: "Checking import policies", detail: "Permits, certificates and trade-remedy orders for the line." },
142
+ { id: "r1", toolName: "Reading the description", input: undefined },
143
+ { id: "r2", toolName: "tariff_schedule.search" },
144
+ { id: "r3", toolName: "Narrowing the heading", input: undefined },
145
+ { id: "r4", toolName: "duty_rates.read" },
146
+ { id: "r5", toolName: "Checking import policies", input: undefined },
144
147
  ];
145
148
 
146
149
  export function TplLookup() {
@@ -182,11 +185,13 @@ export function TplLookup() {
182
185
  };
183
186
 
184
187
  const streaming = phase === "running" && revealed < SCRIPT.length;
185
- const items: AgentRunItem[] = SCRIPT.slice(0, revealed).map((s, i) => ({
186
- type: "step",
187
- ...s,
188
- status: streaming && i === revealed - 1 ? "running" : "done",
189
- }));
188
+ const items: Part[] = SCRIPT.slice(0, revealed).map((s, i): Part => {
189
+ const running = streaming && i === revealed - 1;
190
+ if (running) {
191
+ return { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "input-available", input: s.input };
192
+ }
193
+ return { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "output-available", input: s.input, output: s.output };
194
+ });
190
195
 
191
196
  return (
192
197
  <View style={{ flex: 1, flexDirection: "row", backgroundColor: colors.white }}>
@@ -200,7 +205,7 @@ export function TplLookup() {
200
205
 
201
206
  <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, gap: 16 }}>
202
207
  {/* current run */}
203
- <AgentRun items={items} state={streaming ? "streaming" : "done"} />
208
+ <AgentRun parts={items} state={streaming ? "streaming" : "done"} />
204
209
 
205
210
  {/* Matches surface only once the run settles — the agent's output,
206
211
  never a pre-filled list; they sit at the bottom, under the run. */}
@@ -65,10 +65,13 @@ import { SearchInput } from "@lotics/ui/search_input";
65
65
  import { useSelection } from "@lotics/ui/use_selection";
66
66
  import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
67
67
  import { CardSelectItem } from "@lotics/ui/card_select_item";
68
- import { AgentRun, type AgentRunItem, type AgentRunStep } from "@lotics/ui/agent_run";
68
+ import { AgentRun } from "@lotics/ui/agent_run";
69
69
  import { type SourceRef } from "@lotics/ui/sources";
70
70
  import { ChangeValueInput, Change, ChangeField, ChangeFields, ChangeReasoning, ChangeRecord, ChangeReview, ChangeReviewActions, ChangeReviewHeader, type ChangeStatus } from "@lotics/ui/change_review";
71
71
  import { CompletionState } from "@lotics/ui/completion_state";
72
+ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
73
+
74
+ type Part = UIMessagePart<UIDataTypes, UITools>;
72
75
 
73
76
  // ─────────────────────────────────────────────────────────────────────────────
74
77
  // Template · Record — THE record surface: the surface IS the editor, every
@@ -431,7 +434,7 @@ const CONTENT_MAX = 720;
431
434
  // that forks into extract / cross-check / edit-with-AI. The two templates
432
435
  // share this desk — change it in BOTH (the one divergence: generation lives
433
436
  // in this page's output sections, in tpl_documents' toolbar dialog).
434
- type ScriptStep = Omit<AgentRunStep, "status">;
437
+ type ScriptStep = { id: string; toolName: string; input?: unknown; output?: unknown };
435
438
  type Task = "extract" | "check";
436
439
  type Phase = "fork" | "running" | "review" | "done";
437
440
 
@@ -455,15 +458,15 @@ const DOCS: Doc[] = [
455
458
  ];
456
459
 
457
460
  const EXTRACT_STEPS: ScriptStep[] = [
458
- { id: "e1", label: "Reading the selected documents", detail: "Page by page, tables included" },
459
- { id: "e2", label: "get_record", kind: "tool" },
460
- { id: "e3", label: "Extracting field values", detail: "9 fields · 2 order lines" },
461
- { id: "e4", label: "Comparing against the record", detail: "3 match · 1 new · 1 change · 1 conflict" },
461
+ { id: "e1", toolName: "Reading the selected documents" },
462
+ { id: "e2", toolName: "get_record" },
463
+ { id: "e3", toolName: "Extracting field values" },
464
+ { id: "e4", toolName: "Comparing against the record" },
462
465
  ];
463
466
  const CHECK_STEPS: ScriptStep[] = [
464
- { id: "c1", label: "Reading the selected documents", detail: "Quantities, parties, dates and terms" },
465
- { id: "c2", label: "get_record", kind: "tool" },
466
- { id: "c3", label: "Cross-checking documents and record", detail: "18 fields compared across the sources" },
467
+ { id: "c1", toolName: "Reading the selected documents" },
468
+ { id: "c2", toolName: "get_record" },
469
+ { id: "c3", toolName: "Cross-checking documents and record" },
467
470
  ];
468
471
 
469
472
  // The shapes of an extract decision — an ADD, an UPDATE, a REMOVAL, a source
@@ -959,14 +962,16 @@ export function TplRecord() {
959
962
  return () => clearTimeout(t);
960
963
  }, [phase, revealed, script.length]);
961
964
 
962
- const runItems: AgentRunItem[] = [
963
- { type: "text", id: "in", text: (task === "check" ? "Comparing the documents against the record and each other." + (brief.trim() ? ` Also: ${brief.trim()}` : "") : "Reading the documents and comparing them with the record.") },
964
- ...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i): AgentRunItem => ({
965
- type: "step", ...s, detail: i < revealed ? s.detail : undefined, status: i < revealed ? "done" : "running",
966
- })),
965
+ const runItems: Part[] = [
966
+ { type: "text", text: (task === "check" ? "Comparing the documents against the record and each other." + (brief.trim() ? ` Also: ${brief.trim()}` : "") : "Reading the documents and comparing them with the record.") },
967
+ ...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i): Part => (
968
+ i < revealed
969
+ ? { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "output-available", input: s.input, output: s.output }
970
+ : { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "input-available", input: s.input }
971
+ )),
967
972
  ];
968
973
  if (revealed >= script.length) {
969
- runItems.push({ type: "text", id: "out", text: task === "check" ? "Checked 18 fields — 2 disagree, 1 worth noting. The findings are below." : "3 fields already match; 3 need a decision, and the invoice carries 2 lines the order doesn't have yet." });
974
+ runItems.push({ type: "text", text: task === "check" ? "Checked 18 fields — 2 disagree, 1 worth noting. The findings are below." : "3 fields already match; 3 need a decision, and the invoice carries 2 lines the order doesn't have yet." });
970
975
  }
971
976
 
972
977
  // Apply counts what actually commits: the kept fields.
@@ -2771,7 +2776,7 @@ export function TplRecord() {
2771
2776
  </View>
2772
2777
  ) : null}
2773
2778
 
2774
- {phase === "running" ? <AgentRun items={runItems} state={revealed >= script.length ? "done" : "streaming"} /> : null}
2779
+ {phase === "running" ? <AgentRun parts={runItems} state={revealed >= script.length ? "done" : "streaming"} /> : null}
2775
2780
 
2776
2781
  {phase === "review" && task === "extract" ? (
2777
2782
  <View style={{ gap: 16 }}>
@@ -27,12 +27,15 @@ import { DataGrid, gridRowStyle, type DataGridColumn, type DataGridGroup } from
27
27
  import { CONTROL_RADIUS } from "@lotics/ui/control_surface";
28
28
  import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
29
29
  import { Composer } from "@lotics/ui/composer";
30
- import { AgentRun, type AgentRunItem } from "@lotics/ui/agent_run";
30
+ import { AgentRun } from "@lotics/ui/agent_run";
31
31
  import { Change, ChangeReview, ChangeReviewActions, ChangeReviewHeader, ChangeSummary, type ChangeStatus } from "@lotics/ui/change_review";
32
32
  import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
33
33
  import { CompletionState } from "@lotics/ui/completion_state";
34
34
  import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
35
35
  import { toISODate } from "@lotics/ui/format_date";
36
+ import type { UIMessagePart, UIDataTypes, UITools } from "ai";
37
+
38
+ type Part = UIMessagePart<UIDataTypes, UITools>;
36
39
 
37
40
  type TagOption = { value: string; label: string };
38
41
 
@@ -253,14 +256,16 @@ export function TplTaskBoard() {
253
256
  return () => clearTimeout(t);
254
257
  }, [phase, revealed]);
255
258
 
256
- const runItems: AgentRunItem[] = [
257
- { type: "text", id: "intro", text: "I read your notes and pulled out the work." },
258
- ...DRAFT_STEPS.slice(0, Math.min(revealed + 1, DRAFT_STEPS.length)).map((s, i): AgentRunItem => ({
259
- type: "step", id: `s${i}`, label: s.label, detail: i < revealed ? s.detail : undefined, status: i < revealed ? "done" : "running",
260
- })),
259
+ const runItems: Part[] = [
260
+ { type: "text", text: "I read your notes and pulled out the work." },
261
+ ...DRAFT_STEPS.slice(0, Math.min(revealed + 1, DRAFT_STEPS.length)).map((s, i): Part =>
262
+ i < revealed
263
+ ? { type: "dynamic-tool", toolName: s.label, toolCallId: `s${i}`, state: "output-available", input: undefined, output: undefined }
264
+ : { type: "dynamic-tool", toolName: s.label, toolCallId: `s${i}`, state: "input-available", input: undefined },
265
+ ),
261
266
  ];
262
267
  if (revealed >= DRAFT_STEPS.length) {
263
- runItems.push({ type: "text", id: "outro", text: `Drafted ${DRAFTED.length} tasks — keep the ones that look right, edit any before adding.` });
268
+ runItems.push({ type: "text", text: `Drafted ${DRAFTED.length} tasks — keep the ones that look right, edit any before adding.` });
264
269
  }
265
270
 
266
271
  const propertyCols: DataGridColumn<Task>[] = [
@@ -384,7 +389,7 @@ export function TplTaskBoard() {
384
389
  <Composer onSend={startDraft} placeholder="Paste your notes…" autoFocus sendLabel="Draft tasks" />
385
390
  </View>
386
391
  ) : null}
387
- {phase === "reading" ? <AgentRun items={runItems} state={revealed >= DRAFT_STEPS.length ? "done" : "streaming"} /> : null}
392
+ {phase === "reading" ? <AgentRun parts={runItems} state={revealed >= DRAFT_STEPS.length ? "done" : "streaming"} /> : null}
388
393
  {phase === "review" ? (
389
394
  <View style={{ gap: 8 }}>
390
395
  <ChangeReviewHeader title="Drafted tasks" />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "13.9.0",
3
+ "version": "14.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -89,6 +89,7 @@
89
89
  "./change_review": "./src/change_review.tsx",
90
90
  "./finding": "./src/finding.tsx",
91
91
  "./clarify": "./src/clarify.tsx",
92
+ "./clarify_wizard": "./src/clarify_wizard.tsx",
92
93
  "./choice_list": "./src/choice_list.tsx",
93
94
  "./sources": "./src/sources.tsx",
94
95
  "./icon": "./src/icon.tsx",
@@ -263,9 +264,13 @@
263
264
  "react-dom": "^19.2.0",
264
265
  "react-native": ">=0.85.0",
265
266
  "react-native-svg": ">=15.0.0",
266
- "react-native-web": ">=0.20.0"
267
+ "react-native-web": ">=0.20.0",
268
+ "ai": ">=7.0.0"
267
269
  },
268
270
  "peerDependenciesMeta": {
271
+ "ai": {
272
+ "optional": true
273
+ },
269
274
  "expo-image": {
270
275
  "optional": true
271
276
  },
@@ -4,12 +4,16 @@ import { colors } from "./colors";
4
4
  import { Text } from "./text";
5
5
  import { WaveAvatar } from "./wave_avatar";
6
6
  import { PressableHighlight } from "./pressable_highlight";
7
- import { AgentRun, resolveToolMeta, type AgentRunItem } from "./agent_run";
7
+ import { AgentRun, resolveToolMeta } from "./agent_run";
8
+ import { toSegments, lastRunningStep, type AgentUIPart } from "./agent_transform";
8
9
 
9
10
  export interface AgentProgressProps {
10
- /** The ordered transcript — same shape `AgentRun` takes. */
11
- items: AgentRunItem[];
11
+ /** The agent message's ai-sdk `parts` — same shape `AgentRun` takes. */
12
+ parts: readonly AgentUIPart[];
12
13
  state?: "streaming" | "done" | "error";
14
+ /** A run-level breaking error, forwarded to the expanded `AgentRun` (a terminal
15
+ * danger row). Lives outside `parts` — `useAgentRun().error`. */
16
+ error?: string;
13
17
  /** The collapsed-pill label. Defaults to the running step's label (or
14
18
  * "Working…" / "Done"). */
15
19
  label?: string;
@@ -30,16 +34,11 @@ export interface AgentProgressProps {
30
34
  * label, so the surface stays calm until you ask to see the steps.
31
35
  */
32
36
  export function AgentProgress(props: AgentProgressProps) {
33
- const { items, state = "streaming", label, labelForTool, stepsLabel, defaultExpanded } = props;
37
+ const { parts, state = "streaming", error, label, labelForTool, stepsLabel, defaultExpanded } = props;
34
38
  const [expanded, setExpanded] = useState(defaultExpanded ?? false);
35
39
 
36
- const running = [...items].reverse().find((it) => it.type === "step" && it.status === "running");
37
- const runningLabel =
38
- running && running.type === "step"
39
- ? running.kind === "tool"
40
- ? resolveToolMeta(running.label, labelForTool).label
41
- : running.label
42
- : undefined;
40
+ const running = lastRunningStep(toSegments(parts));
41
+ const runningLabel = running ? resolveToolMeta(running.toolName, labelForTool).label : undefined;
43
42
  const compact = label ?? runningLabel ?? (state === "done" ? "Done" : state === "error" ? "Stopped" : "Working…");
44
43
  const streaming = state === "streaming";
45
44
 
@@ -48,7 +47,7 @@ export function AgentProgress(props: AgentProgressProps) {
48
47
  {expanded ? (
49
48
  <View style={styles.panel}>
50
49
  <ScrollView style={{ maxHeight: 260 }} contentContainerStyle={{ padding: 16 }}>
51
- <AgentRun items={items} state={state} labelForTool={labelForTool} stepsLabel={stepsLabel} />
50
+ <AgentRun parts={parts} state={state} error={error} labelForTool={labelForTool} stepsLabel={stepsLabel} />
52
51
  </ScrollView>
53
52
  </View>
54
53
  ) : null}