@lotics/ui 7.12.2 → 7.14.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.
Files changed (3) hide show
  1. package/AGENTS.md +19 -13
  2. package/package.json +4 -16
  3. package/src/agent_run.tsx +94 -29
package/AGENTS.md CHANGED
@@ -415,20 +415,26 @@ Compose the surfaces as a loop, and reach for the right one by job:
415
415
  way; while running the host swaps `Composer` for `AgentProgress` (same pill geometry, so it reads as
416
416
  a morph).
417
417
  - **Show the work** — `AgentRun`: a live feed of the agent's work as a TIMELINE. The prop is an
418
- ORDERED `items` array — `{type:"text"}` prose (rendered as `Markdown`, a trailing ▍ while live) interleaved with `{type:"step"}` tool/step calls, in
419
- the order they happened (a real run is text → a burst of calls more text more calls, NOT all
420
- text on top of a flat step list). Consecutive `step` items fold into ONE activity group: while the
421
- agent is mid-tools the tail group is a SINGLE pulsing row whose label swaps in place as each call
422
- fires (no growing stack of dots); once prose resumes the group settles into a persistent "{final
423
- action} · {n} steps" HEADER (a `complete` terminal dot distinct from the filled `done` step dots)
424
- that STAYS PUT and rolls the calls out BELOW it on press (a `step` can carry `peek` content;
425
- `detail` is OPTIONAL real tool I/O, never invented prose). The run ALWAYS ends on the agent's
426
- text (no global terminal node). A `step` with
418
+ ORDERED `items` array — `{type:"text"}` prose (rendered as `Markdown`, a trailing ▍ while live),
419
+ `{type:"reasoning"}` thinking, and `{type:"step"}` tool/step calls, in the order they happened (a
420
+ real run is think → text a burst of calls more text, NOT all text on top of a flat step list).
421
+ **Progressive disclosure** the feed shows the label + state; the detail is revealed on demand:
422
+ `reasoning` renders COLLAPSED (a muted "Thinking" row, press to reveal the Markdown), and a `step`'s
423
+ `input`/`output` are hidden in the row but open in a press-to-reveal **peek** (auto-built Input /
424
+ Output `JsonPanel`s; an `error` status + `errorText` paint the row red and show the reason there).
425
+ Pass an explicit `peek` node to render that reveal yourself (it overrides the auto panel); `detail`
426
+ is OPTIONAL a short human summary (a count), never invented prose or raw I/O (that's `input`/`output`).
427
+ Consecutive `step` items fold into ONE activity group: while the agent is mid-tools the tail group is
428
+ a SINGLE pulsing row whose label swaps in place as each call fires (no growing stack of dots); once
429
+ prose resumes the group settles into a persistent "{final action} · {n} steps" HEADER (a `complete`
430
+ terminal dot — distinct from the filled `done` step dots) that STAYS PUT and rolls the calls out
431
+ BELOW it on press. The run ALWAYS ends on the agent's text (no global terminal node). A `step` with
427
432
  `kind:"tool"` carries the RAW tool name, resolved via a built-in map + an optional `labelForTool`
428
433
  override (localize THERE — the kit stays English); `stepsLabel` localizes the "{n} steps" suffix.
429
- Transparent work, NEVER a bare spinner. On a canvas/composer app reach for `AgentProgress` —
430
- `AgentRun` collapsed into a floating pill (avatar + current step) that EXPANDS on press; the composer
431
- morphs into it while running, and reveals again when done.
434
+ Fed natively by `@lotics/app-sdk` `useAgentRun().items` (reasoning + per-tool I/O + state come for
435
+ free no hand-assembly). Transparent work, NEVER a bare spinner. On a canvas/composer app reach for
436
+ `AgentProgress` — `AgentRun` collapsed into a floating pill (avatar + current step) that EXPANDS on
437
+ press; the composer morphs into it while running, and reveals again when done.
432
438
  - **Review before apply — ONE card + the batch engine, the BODY is compositional.** Every "the agent
433
439
  proposes → the human accepts / edits / dismisses → nothing auto-applies" surface wears the same
434
440
  `reviewCardStyle` + an optional `Confidence`. There are two primitives:
@@ -882,7 +888,7 @@ CalloutText · CalloutActions) · kpi_card · kpi_strip · summary_line (Summary
882
888
  bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
883
889
  funnel (Funnel — conversion funnel: narrowing bars + the step rate as a bold aligned headline row, count below; `orientation` vertical|horizontal, `onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of stacked_progress_bar's whole-split) ·
884
890
  status_grid (StatusGrid + StatusLegend) · heatmap (density cross-tab — colour-only, no numbers) · matrix (Matrix — the PIVOT cross-tab: band-compound `Matrix` root + `Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) + `Matrix.Legend`; the data layer `matrixTotals`/`MatrixAxisItem`/`MatrixCellRef` import from `@lotics/ui/matrix_totals`. Pick over `Heatmap` when the NUMBER and totals matter, not just where it clusters) · legend_item · remainder_meter · allocation_row ·
885
- scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view; `selectTileRemove={false}` drops even that so delete is menu-only) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines are LAZY (dynamic-imported, ~free until a doc of that type is opened) so custom-code APPS can use it too they just add `@lotics/docx` + `@lotics/xlsx` to their deps. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
891
+ scan_field · file_dropzone · files_editor (FilesEditor — THE all-in-one attachment field: FileGrid + a toolbar (Upload primary · Select · Download all) that swaps into a batch SELECT mode (Select all · a Menu of Download/Share/Delete · Done; the per-tile ✕ is select-mode-only, never in the default view; `selectTileRemove={false}` drops even that so delete is menu-only) + built-in gallery (Download + inline preview; no "open in new tab") + Alert-confirmed remove; host wires `files` + `onAdd`/`onRemove` (+ optional `uploads`, `onShareSelected`, `readOnly`, `selectTileRemove`, `labels`, `galleryLabels`, `gridMaxHeight` — cap the grid height so it scrolls and the toolbar pins, for a popover/drawer); mirrors the frontend cell_files_editor. Use FileGrid/FileRows bare only when you own the chrome) · file_grid (FileGrid — the upload-aware grid: completed files + a live upload queue in one surface; FileUpload/PendingUpload types; the add-files default) · uploading_thumbnail (UploadingThumbnail — the single in-flight upload tile FileGrid renders; reach for it only when hand-rolling a non-grid upload layout) · file_thumbnail · file_thumbnail_grid · file_row · file_rows (FileRows — batteries-included file list: row press → built-in gallery + a ⋯ Download/Open-external/Remove menu; composes FileRow + ActionMenu + FileGalleryModal) · file_preview (FilePreview — the universal inline preview: image/PDF/video/audio + Word via `@lotics/docx` + Excel/CSV via `@lotics/xlsx`; the heavy engines (pdf.js · `@lotics/docx` · `@lotics/xlsx`) are LAZY (dynamic-imported, ~free until a doc of that type is opened) and SHIP AS `@lotics/ui` deps (7.14.0+)custom-code apps get PDF/Word/Excel preview with ZERO extra install. Renders to canvas/DOM, never a nested iframe — works in the sandboxed app iframe) ·
886
892
  file_gallery_modal · image_gallery · use_selection (useSelection — always-on multi-select state for a register/list: the `selected` Set + `toggle`/`setAll`/`allSelected`/`indeterminate`/`count`/`clear`; selectability gating stays with the caller. The checkbox-always-visible counterpart to `use_selection_mode`) · use_selection_mode · share_or_download · rotate_image · avatar · skeleton · activity_indicator · loading · divider ·
887
893
  spacer · stack · section_card · page_header · page_content · calendar (calendar/index.ts) · gantt ·
888
894
  comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "7.12.2",
3
+ "version": "7.14.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -240,17 +240,17 @@
240
240
  },
241
241
  "license": "MIT",
242
242
  "dependencies": {
243
+ "@lotics/docx": "^0.1.0",
244
+ "@lotics/xlsx": "^0.1.0",
245
+ "pdfjs-dist": "^6.0.227",
243
246
  "react-markdown": "^10.0.0",
244
247
  "remark-gfm": "^4.0.0"
245
248
  },
246
249
  "peerDependencies": {
247
- "@lotics/docx": "^0.1.0",
248
- "@lotics/xlsx": "^0.1.0",
249
250
  "@react-native-picker/picker": ">=2.0.0",
250
251
  "expo-image": ">=3.0.0",
251
252
  "lucide-react": ">=0.460.0",
252
253
  "lucide-react-native": ">=0.460.0",
253
- "pdfjs-dist": ">=6.0.0",
254
254
  "react": "^19.2.0",
255
255
  "react-dom": "^19.2.0",
256
256
  "react-native": ">=0.85.0",
@@ -258,15 +258,6 @@
258
258
  "react-native-web": ">=0.20.0"
259
259
  },
260
260
  "peerDependenciesMeta": {
261
- "@lotics/docx": {
262
- "optional": true
263
- },
264
- "@lotics/xlsx": {
265
- "optional": true
266
- },
267
- "pdfjs-dist": {
268
- "optional": true
269
- },
270
261
  "expo-image": {
271
262
  "optional": true
272
263
  },
@@ -287,12 +278,9 @@
287
278
  "test": "vitest run"
288
279
  },
289
280
  "devDependencies": {
290
- "@lotics/docx": "^0.1.0",
291
- "@lotics/xlsx": "^0.1.0",
292
281
  "@types/react-dom": "~19.2.2",
293
282
  "@vitejs/plugin-react": "^4.3.4",
294
283
  "lucide-react": "^0.562.0",
295
- "pdfjs-dist": "^6.0.227",
296
284
  "react-dom": "^19.2.0",
297
285
  "react-native-web": "^0.21.0",
298
286
  "vite": "^7.2.4"
package/src/agent_run.tsx CHANGED
@@ -4,6 +4,7 @@ import { colors } from "./colors";
4
4
  import { Text } from "./text";
5
5
  import { Icon, type IconName } from "./icon";
6
6
  import { Markdown } from "./markdown";
7
+ import { JsonPanel, stringifyData } from "./json_panel";
7
8
  import { Peek } from "./peek";
8
9
  import { PressableHighlight } from "./pressable_highlight";
9
10
  import { Marker, type StepStatus } from "./stepper";
@@ -18,25 +19,35 @@ export interface AgentRunStep {
18
19
  * it to a human label via the default map / `labelForTool`. A `step` carries
19
20
  * an already-human label. */
20
21
  label: string;
21
- /** A note under the label — should be REAL tool I/O (an arg, a count, a result
22
- * summary), never invented prose. Muted. */
23
- detail?: string;
24
22
  status: AgentStepStatus;
25
23
  kind?: "step" | "tool";
26
- /** Optional content shown in a `Peek` popover when the (expanded) step is
27
- * pressed glance into what the step did (the tool input, a result summary). */
24
+ /** The tool call's input (arguments) and result. When present (and no explicit
25
+ * `peek`), the feed reveals them ON DEMAND a press-to-open peek with an Input /
26
+ * Output panel — instead of cluttering the row; the row itself shows only the
27
+ * label + state. */
28
+ input?: unknown;
29
+ output?: unknown;
30
+ /** The tool failure message (shown in the peek when `status` is `"error"`). */
31
+ errorText?: string;
32
+ /** A note under the label — a short, human summary (a count, a result summary),
33
+ * never invented prose. Muted. For raw tool I/O prefer `input`/`output`. */
34
+ detail?: string;
35
+ /** Custom content for the press-to-open peek. Overrides the auto-built Input /
36
+ * Output panel — pass this to render the reveal yourself. */
28
37
  peek?: ReactNode;
29
38
  }
30
39
 
31
40
  /**
32
- * The ordered transcript of a run, in the order it happened: prose the agent
33
- * streams (`text`) interleaved with the tools/steps it runs between prose
34
- * (`step`). A real run is a timeline — text, a burst of tool calls, more text,
35
- * more calls — so the feed is an ordered array, NOT a text block over a flat
36
- * step list. Consecutive `step` items collapse into ONE activity group.
41
+ * The ordered transcript of a run, in the order it happened: the agent's answer
42
+ * prose (`text`), its thinking (`reasoning`, shown collapsed / revealed on demand),
43
+ * and the tools it runs between prose (`step`). A real run is a timeline — text, a
44
+ * burst of tool calls, more text — so the feed is an ordered array, NOT a text
45
+ * block over a flat step list. Consecutive `step` items collapse into ONE activity
46
+ * group; a `step`'s `input`/`output` reveal in a peek on press.
37
47
  */
38
48
  export type AgentRunItem =
39
49
  | { type: "text"; id: string; text: string }
50
+ | { type: "reasoning"; id: string; text: string }
40
51
  | ({ type: "step" } & AgentRunStep);
41
52
 
42
53
  export interface AgentRunProps {
@@ -101,18 +112,19 @@ function stepLabel(s: AgentRunStep, labelForTool?: (toolName: string) => string
101
112
  return s.kind === "tool" ? resolveToolMeta(s.label, labelForTool).label : s.label;
102
113
  }
103
114
 
104
- // A run is a timeline of segments: prose, then a group of consecutive tool/step
105
- // calls, then more prose. Folding the flat item list into segments is what lets
106
- // the feed interleave instead of forcing all text to the top.
115
+ // A run is a timeline of segments: prose, thinking, then a group of consecutive
116
+ // tool calls, then more prose. Folding the flat item list into segments is what
117
+ // lets the feed interleave instead of forcing all text to the top.
107
118
  type Segment =
108
119
  | { kind: "text"; id: string; text: string }
120
+ | { kind: "reasoning"; id: string; text: string }
109
121
  | { kind: "group"; id: string; steps: AgentRunStep[] };
110
122
 
111
123
  function toSegments(items: AgentRunItem[]): Segment[] {
112
124
  const segments: Segment[] = [];
113
125
  for (const item of items) {
114
- if (item.type === "text") {
115
- segments.push({ kind: "text", id: item.id, text: item.text });
126
+ if (item.type === "text" || item.type === "reasoning") {
127
+ segments.push({ kind: item.type, id: item.id, text: item.text });
116
128
  continue;
117
129
  }
118
130
  const { type: _t, ...step } = item;
@@ -158,6 +170,9 @@ export function AgentRun(props: AgentRunProps) {
158
170
  </View>
159
171
  );
160
172
  }
173
+ if (seg.kind === "reasoning") {
174
+ return <ReasoningDisclosure key={seg.id} text={seg.text} streaming={streaming && i === lastIndex} expanded={!!expanded[seg.id]} onToggle={() => toggle(seg.id)} />;
175
+ }
161
176
  return (
162
177
  <ToolGroup
163
178
  key={seg.id}
@@ -218,6 +233,66 @@ function StepBody({ label, detail, status }: { label: string; detail?: string; s
218
233
  );
219
234
  }
220
235
 
236
+ // The press-to-open reveal for a tool step: the caller's `peek` wins; else Input /
237
+ // Output / Error panels auto-built from the carried I/O; else nothing (no reveal).
238
+ // Raw I/O stays OUT of the row — only shown here, on demand.
239
+ function stepPeek(s: AgentRunStep): ReactNode {
240
+ if (s.peek) return s.peek;
241
+ const hasInput = s.input !== undefined;
242
+ const hasOutput = s.output !== undefined;
243
+ if (!hasInput && !hasOutput && !s.errorText) return null;
244
+ return (
245
+ <View style={{ gap: 8 }}>
246
+ {hasInput ? <JsonPanel title="Input" value={stringifyData(s.input)} /> : null}
247
+ {s.errorText ? (
248
+ <JsonPanel title="Error" value={s.errorText} />
249
+ ) : hasOutput ? (
250
+ <JsonPanel title="Output" value={stringifyData(s.output)} />
251
+ ) : null}
252
+ </View>
253
+ );
254
+ }
255
+
256
+ // A step's body, wrapped in a Peek when there's something to reveal (I/O or a
257
+ // caller peek), plain otherwise.
258
+ function StepContent({ s, label }: { s: AgentRunStep; label: string }) {
259
+ const peek = stepPeek(s);
260
+ const body = <StepBody label={label} detail={s.detail} status={s.status} />;
261
+ return peek ? (
262
+ <Peek accessibilityLabel={label} content={peek}>
263
+ {body}
264
+ </Peek>
265
+ ) : (
266
+ body
267
+ );
268
+ }
269
+
270
+ // Thinking — a muted, COLLAPSED "Thinking" disclosure (revealed on press), so the
271
+ // agent's reasoning never crowds the answer. Pulses while it's still streaming.
272
+ function ReasoningDisclosure(props: { text: string; streaming?: boolean; expanded: boolean; onToggle: () => void }) {
273
+ const { text, streaming, expanded, onToggle } = props;
274
+ return (
275
+ <View style={styles.group}>
276
+ <PressableHighlight focusRing onPress={onToggle} accessibilityRole="button" accessibilityLabel="Thinking" style={styles.row}>
277
+ <View style={styles.dotCol}>
278
+ <Marker status={streaming ? "current" : "done"} color={colors.zinc[400]} live={!!streaming} />
279
+ </View>
280
+ <View style={styles.rowBody}>
281
+ <Text size="sm" color="muted">
282
+ {streaming ? "Thinking…" : "Thinking"}
283
+ </Text>
284
+ </View>
285
+ {chevron(expanded ? "up" : "down")}
286
+ </PressableHighlight>
287
+ {expanded ? (
288
+ <View style={styles.reasoning}>
289
+ <Markdown>{text}</Markdown>
290
+ </View>
291
+ ) : null}
292
+ </View>
293
+ );
294
+ }
295
+
221
296
  function SummaryLabel({ label, count, stepsLabel }: { label: string; count: number; stepsLabel: (n: number) => string }) {
222
297
  return (
223
298
  <Text size="sm" weight="medium" numberOfLines={1}>
@@ -269,13 +344,7 @@ function ToolGroup(props: {
269
344
  return (
270
345
  <View style={styles.group}>
271
346
  <ActivityRow markerStatus={final.status === "error" ? "warning" : "done"}>
272
- {final.peek ? (
273
- <Peek accessibilityLabel={resolve(final)} content={final.peek}>
274
- <StepBody label={resolve(final)} detail={final.detail} status={final.status} />
275
- </Peek>
276
- ) : (
277
- <StepBody label={resolve(final)} detail={final.detail} status={final.status} />
278
- )}
347
+ <StepContent s={final} label={resolve(final)} />
279
348
  </ActivityRow>
280
349
  </View>
281
350
  );
@@ -300,13 +369,7 @@ function ToolGroup(props: {
300
369
  ? steps.map((s) => (
301
370
  <AnimationFadeIn key={s.id} translateY={4}>
302
371
  <ActivityRow markerStatus={s.status === "error" ? "warning" : "done"}>
303
- {s.peek ? (
304
- <Peek accessibilityLabel={resolve(s)} content={s.peek}>
305
- <StepBody label={resolve(s)} detail={s.detail} status={s.status} />
306
- </Peek>
307
- ) : (
308
- <StepBody label={resolve(s)} detail={s.detail} status={s.status} />
309
- )}
372
+ <StepContent s={s} label={resolve(s)} />
310
373
  </ActivityRow>
311
374
  </AnimationFadeIn>
312
375
  ))
@@ -328,4 +391,6 @@ const styles = StyleSheet.create({
328
391
  },
329
392
  dotCol: { width: 18, alignItems: "center" },
330
393
  rowBody: { flex: 1 },
394
+ // Reasoning body aligns under the "Thinking" label (past the dot column + gap).
395
+ reasoning: { paddingLeft: 36, paddingRight: 8, paddingBottom: 4 },
331
396
  });