basalt-ui 1.10.0 → 1.11.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 (88) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +10 -0
  3. package/dist/agent/ai-sdk-transport.d.ts +3 -52
  4. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  5. package/dist/agent/ai-sdk-transport.js +108 -24
  6. package/dist/agent/coalesce.d.ts +26 -0
  7. package/dist/agent/coalesce.d.ts.map +1 -0
  8. package/dist/agent/coalesce.js +69 -0
  9. package/dist/agent/foreign.d.ts +97 -0
  10. package/dist/agent/foreign.d.ts.map +1 -0
  11. package/dist/agent/foreign.js +18 -0
  12. package/dist/agent/history.d.ts +2 -0
  13. package/dist/agent/history.d.ts.map +1 -1
  14. package/dist/agent/id.d.ts +35 -0
  15. package/dist/agent/id.d.ts.map +1 -0
  16. package/dist/agent/id.js +14 -0
  17. package/dist/agent/index.d.ts +9 -4
  18. package/dist/agent/index.d.ts.map +1 -1
  19. package/dist/agent/index.js +15 -3
  20. package/dist/agent/merge.d.ts +8 -0
  21. package/dist/agent/merge.d.ts.map +1 -0
  22. package/dist/agent/merge.js +23 -0
  23. package/dist/agent/part-list.d.ts.map +1 -1
  24. package/dist/agent/part-list.js +16 -6
  25. package/dist/agent/parts.d.ts +100 -21
  26. package/dist/agent/parts.d.ts.map +1 -1
  27. package/dist/agent/parts.js +128 -11
  28. package/dist/agent/transport.d.ts +39 -3
  29. package/dist/agent/transport.d.ts.map +1 -1
  30. package/dist/agent/transport.js +20 -17
  31. package/dist/agent/use-agent-stream.d.ts +2 -1
  32. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  33. package/dist/agent/use-agent-stream.js +5 -2
  34. package/dist/agent/use-agent-thread-runs.d.ts +9 -3
  35. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  36. package/dist/agent/use-agent-thread-runs.js +75 -23
  37. package/dist/agent-chat/index.d.ts +2 -0
  38. package/dist/agent-chat/index.d.ts.map +1 -1
  39. package/dist/agent-chat/index.js +2 -0
  40. package/dist/agent-chat/thread-message.d.ts +24 -7
  41. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  42. package/dist/agent-chat/thread-message.js +102 -43
  43. package/dist/agent-chat/tool-chip.d.ts +21 -0
  44. package/dist/agent-chat/tool-chip.d.ts.map +1 -0
  45. package/dist/agent-chat/tool-chip.js +120 -0
  46. package/dist/guard/index.d.ts.map +1 -1
  47. package/dist/guard/index.js +1 -3
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -1
  51. package/dist/surfaces.d.ts +2 -2
  52. package/dist/surfaces.js +2 -2
  53. package/llms.txt +4 -4
  54. package/package.json +1 -1
  55. package/src/agent/ai-sdk-transport.test.ts +338 -40
  56. package/src/agent/ai-sdk-transport.ts +206 -53
  57. package/src/agent/coalesce.test.ts +194 -0
  58. package/src/agent/coalesce.ts +118 -0
  59. package/src/agent/foreign.test.ts +47 -0
  60. package/src/agent/foreign.ts +140 -0
  61. package/src/agent/history.ts +2 -0
  62. package/src/agent/id.test.ts +71 -0
  63. package/src/agent/id.ts +43 -0
  64. package/src/agent/index.ts +26 -3
  65. package/src/agent/merge.test.ts +119 -0
  66. package/src/agent/merge.ts +67 -0
  67. package/src/agent/part-list.test.tsx +82 -0
  68. package/src/agent/part-list.tsx +31 -6
  69. package/src/agent/parts.test.ts +401 -59
  70. package/src/agent/parts.ts +262 -32
  71. package/src/agent/transport.test.ts +70 -0
  72. package/src/agent/transport.ts +72 -20
  73. package/src/agent/use-agent-stream.test.tsx +47 -17
  74. package/src/agent/use-agent-stream.ts +13 -3
  75. package/src/agent/use-agent-thread-runs.foreign.test.tsx +133 -0
  76. package/src/agent/use-agent-thread-runs.resume.test.tsx +34 -6
  77. package/src/agent/use-agent-thread-runs.test.tsx +397 -14
  78. package/src/agent/use-agent-thread-runs.ts +170 -26
  79. package/src/agent/use-agent-thread-runs.wedge.test.tsx +4 -2
  80. package/src/agent-chat/index.ts +4 -0
  81. package/src/agent-chat/thread-message.test.tsx +211 -0
  82. package/src/agent-chat/thread-message.tsx +235 -62
  83. package/src/agent-chat/tool-chip.test.tsx +139 -0
  84. package/src/agent-chat/tool-chip.tsx +219 -0
  85. package/src/guard/check-source.test.ts +8 -4
  86. package/src/guard/index.ts +1 -3
  87. package/src/index.ts +2 -0
  88. package/src/surfaces.ts +2 -2
@@ -0,0 +1,219 @@
1
+ /**
2
+ * ToolChip — a one-line collapsed row for a tool-call part, expanding to its input/output JSON.
3
+ *
4
+ * Replaces `thread-message.tsx`'s old always-expanded `ToolRenderer`, which unconditionally
5
+ * rendered both input and output `<pre>` blocks. Collapsed, it shows a mono micro-label, the tool
6
+ * name, a state dot, and `durationMs` when present; expanded, it shows whatever the part's state
7
+ * actually carries — input, output, the surviving `rawInput` for a call whose input never
8
+ * validated, the error text, the denial reason, or the approve/deny affordances.
9
+ *
10
+ * @example
11
+ * import { ToolChip } from 'basalt-ui/agent-chat'
12
+ *
13
+ * <ToolChip
14
+ * part={part}
15
+ * onApprove={(id) => api.runs.approve.post({ approvalId: id })}
16
+ * onDeny={(id, reason) => api.runs.deny.post({ approvalId: id, reason })}
17
+ * />
18
+ */
19
+ import { Box, Button, Collapse, Group, Stack, Text, UnstyledButton } from '@mantine/core'
20
+ import { useDisclosure } from '@mantine/hooks'
21
+ import type { JSX } from 'react'
22
+ import { isToolCallSettled } from '../agent'
23
+ import type { ToolCallPart } from '../agent'
24
+ import { alpha, VX } from '../tokens'
25
+
26
+ // Mirrors thread-message.tsx's micro-label/rail/code-block idiom (docs/DESIGN-SPEC.md §3/§5) —
27
+ // duplicated locally rather than imported, matching the existing part-list.tsx/thread-message.tsx
28
+ // precedent of each Mantine-facing renderer file owning its own copy of these style objects.
29
+ const MICRO_LABEL_STYLE = {
30
+ fontFamily: 'var(--basalt-font-mono)',
31
+ fontSize: VX.text.micro,
32
+ fontWeight: 500,
33
+ letterSpacing: '0.06em',
34
+ textTransform: 'uppercase',
35
+ color: VX.faint,
36
+ } as const
37
+ const RAIL_STYLE = {
38
+ borderLeft: `2px solid ${VX.divider}`,
39
+ paddingLeft: 'var(--vx-space-agent-rail-inset-x)',
40
+ } as const
41
+ const CODE_BLOCK_STYLE = {
42
+ margin: 0,
43
+ marginTop: 'var(--vx-space-agent-part-gap-top)',
44
+ fontFamily: 'var(--basalt-font-mono)',
45
+ backgroundColor: alpha(VX.ink, 0.06),
46
+ borderRadius: 'var(--vx-radius-card)',
47
+ padding: 'var(--vx-space-agent-code-inset)',
48
+ whiteSpace: 'pre-wrap',
49
+ overflowWrap: 'anywhere',
50
+ } as const
51
+
52
+ /** Per-state (label, dot color) — the exhaustive switch that drives the collapsed row. */
53
+ function stateMeta(part: ToolCallPart): { readonly label: string; readonly dotToken: string } {
54
+ switch (part.state) {
55
+ case 'input-streaming':
56
+ return { label: 'streaming', dotToken: VX.status.neutral }
57
+ case 'input-available':
58
+ // No 'running' state on the wire — 'input-available' IS the running state (input complete,
59
+ // no output yet). This label is a UI-only gloss on that wire state, not a new state.
60
+ return { label: 'running', dotToken: VX.status.neutral }
61
+ case 'approval-requested':
62
+ return { label: 'needs approval', dotToken: VX.status.warn }
63
+ case 'approval-responded':
64
+ return part.approval.approved
65
+ ? { label: 'approved', dotToken: VX.status.neutral }
66
+ : { label: 'declined', dotToken: VX.status.bad }
67
+ case 'output-available':
68
+ // preliminary vs final must be visually distinguishable — the label carries that here; the
69
+ // expanded output body below is identical either way.
70
+ return part.preliminary === true
71
+ ? { label: 'preliminary', dotToken: VX.status.warn }
72
+ : { label: 'done', dotToken: VX.status.good }
73
+ case 'output-error':
74
+ return { label: 'error', dotToken: VX.status.bad }
75
+ case 'output-denied':
76
+ return { label: 'denied', dotToken: VX.status.bad }
77
+ default: {
78
+ // `part` is `never` here ONLY at compile time — this switch is genuinely exhaustive today,
79
+ // so adding an eighth ToolCallPart state without a case above makes this assignment a tsc
80
+ // error, the same gate `assertNever` provides. But `part` at RUNTIME can be a value this
81
+ // build never declared: `createThreadsStore` persists to localStorage with no shape
82
+ // validation, and `edenTransport` trusts the wire with zero validation, so a 1.10.0-shaped
83
+ // flat tool part (`state` undefined) or a future/foreign `state` string both reach here.
84
+ // That must never THROW during render — mirrors ThreadTranscript's fallbackRenderer
85
+ // doctrine (unknown data renders inert, it doesn't blank the transcript) — so, unlike a
86
+ // genuinely-exhaustive switch elsewhere, this default returns a neutral descriptor instead
87
+ // of calling assertNever.
88
+ const exhaustive: never = part
89
+ void exhaustive
90
+ return { label: 'unknown', dotToken: VX.status.neutral }
91
+ }
92
+ }
93
+ }
94
+
95
+ function formatDuration(ms: number): string {
96
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`
97
+ }
98
+
99
+ export type ToolChipProps = {
100
+ readonly part: ToolCallPart
101
+ readonly defaultExpanded?: boolean
102
+ /** Rendered only in 'approval-requested'. Omit both to render a read-only chip. */
103
+ readonly onApprove?: (approvalId: string) => void
104
+ readonly onDeny?: (approvalId: string, reason?: string) => void
105
+ }
106
+
107
+ /**
108
+ * A collapsed-by-default row for a single tool-call part. All seven wire states render something
109
+ * sane; the switch above still gates an eighth (compile-time-unhandled) state as a tsc error via a
110
+ * `const _: never = part` assignment, but unlike a truly-closed switch, its `default` branch never
111
+ * throws at RUNTIME — a value from persisted/wire data that reaches here with a `state` this build
112
+ * doesn't recognize renders an inert 'unknown' chip instead of blanking the transcript.
113
+ *
114
+ * @example
115
+ * <ToolChip part={part} />
116
+ */
117
+ export function ToolChip({
118
+ part,
119
+ defaultExpanded = false,
120
+ onApprove,
121
+ onDeny,
122
+ }: ToolChipProps): JSX.Element {
123
+ const [expanded, { toggle }] = useDisclosure(defaultExpanded)
124
+ const { label, dotToken } = stateMeta(part)
125
+ const settled = isToolCallSettled(part)
126
+
127
+ const input = 'input' in part ? part.input : undefined
128
+ const rawInput = part.state === 'output-error' ? part.rawInput : undefined
129
+
130
+ return (
131
+ <Box style={RAIL_STYLE}>
132
+ <Stack gap={4}>
133
+ <UnstyledButton onClick={toggle}>
134
+ <Group gap={6} align="center" wrap="nowrap">
135
+ <Box
136
+ aria-hidden
137
+ style={{
138
+ width: 6,
139
+ height: 6,
140
+ flexShrink: 0,
141
+ borderRadius: '50%', // theme-allow: a 6px state dot is a circle, not a surface corner — no radius token expresses it
142
+ backgroundColor: dotToken,
143
+ }}
144
+ />
145
+ <Text style={MICRO_LABEL_STYLE}>{part.toolName}</Text>
146
+ <Text size="xs" c="dimmed">
147
+ {label}
148
+ </Text>
149
+ {part.durationMs !== undefined && (
150
+ <Text
151
+ style={{
152
+ fontFamily: 'var(--basalt-font-mono)',
153
+ fontSize: VX.text.micro,
154
+ color: VX.faint,
155
+ }}
156
+ >
157
+ {formatDuration(part.durationMs)}
158
+ </Text>
159
+ )}
160
+ {!settled && (
161
+ <Text size="xs" c="dimmed">
162
+ …
163
+ </Text>
164
+ )}
165
+ </Group>
166
+ </UnstyledButton>
167
+ <Collapse expanded={expanded}>
168
+ <Stack gap={6}>
169
+ {input !== undefined && (
170
+ <Text component="pre" size="xs" style={CODE_BLOCK_STYLE}>
171
+ {JSON.stringify(input, null, 2)}
172
+ </Text>
173
+ )}
174
+ {rawInput !== undefined && (
175
+ <Text component="pre" size="xs" style={CODE_BLOCK_STYLE}>
176
+ {JSON.stringify(rawInput, null, 2)}
177
+ </Text>
178
+ )}
179
+ {part.state === 'output-available' && (
180
+ <Text component="pre" size="xs" style={CODE_BLOCK_STYLE}>
181
+ {JSON.stringify(part.output, null, 2)}
182
+ </Text>
183
+ )}
184
+ {part.state === 'output-error' && (
185
+ <Text size="xs" style={{ color: VX.status.bad }}>
186
+ {part.errorText}
187
+ </Text>
188
+ )}
189
+ {part.state === 'output-denied' && part.approval.reason !== undefined && (
190
+ <Text size="xs" c="dimmed">
191
+ {part.approval.reason}
192
+ </Text>
193
+ )}
194
+ {part.state === 'approval-requested' &&
195
+ (onApprove !== undefined || onDeny !== undefined) && (
196
+ <Group gap={6}>
197
+ {onApprove !== undefined && (
198
+ <Button size="compact-sm" onClick={() => onApprove(part.approval.id)}>
199
+ Approve
200
+ </Button>
201
+ )}
202
+ {onDeny !== undefined && (
203
+ <Button
204
+ size="compact-sm"
205
+ variant="light"
206
+ color="red"
207
+ onClick={() => onDeny(part.approval.id)}
208
+ >
209
+ Deny
210
+ </Button>
211
+ )}
212
+ </Group>
213
+ )}
214
+ </Stack>
215
+ </Collapse>
216
+ </Stack>
217
+ </Box>
218
+ )
219
+ }
@@ -209,11 +209,15 @@ describe('mantine-shade-index', () => {
209
209
  })
210
210
 
211
211
  // The grace-minor doctrine (package CLAUDE.md): a kind that rejects previously-passing code
212
- // lands as `warn` for one minor. Deleting its GRACE_PERIOD_KINDS entry flips this — scheduled
213
- // for 1.9.0, deferred one minor because 1.8.0 shipped the same day as 1.7.0 (see the entry).
214
- it('lands as a warning, not an error, for its grace minor', () => {
212
+ // lands as `warn` for one minor, then its GRACE_PERIOD_KINDS entry is deleted and it becomes an
213
+ // error. This one ran its grace across FOUR minors — introduced 1.7.0, then deferred by 1.8.0
214
+ // (shipped the same day as 1.7.0), 1.9.0 (carried the chart-layer batch the same consumer was
215
+ // waiting on) and 1.10.0 (shipped without the promotion at all). Promoted in 1.11.0, verified
216
+ // against the only consumer first: argo's `check-theme` reports zero violations of any kind, so
217
+ // the promotion breaks nothing that was passing.
218
+ it('is an error now that its grace period has ended', () => {
215
219
  const f = find(`<Text c="yellow.7" />`)
216
- expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('warn')
220
+ expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('error')
217
221
  })
218
222
  })
219
223
 
@@ -296,9 +296,7 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
296
296
  * 'raw-font-family': 'introduced 1.4.0 — promote to error in 1.5.0',
297
297
  * }
298
298
  */
299
- const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
300
- 'mantine-shade-index': 'introduced 1.7.0 — promote to error in 1.10.0',
301
- }
299
+ const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {}
302
300
 
303
301
  /** A kind's effective severity: consumer override first, then the grace table, then `error`. */
304
302
  function severityOf(kind: GuardKind, cfg: GuardConfig): GuardSeverity {
package/src/index.ts CHANGED
@@ -118,6 +118,7 @@ export {
118
118
  Composer,
119
119
  threadPartRenderers,
120
120
  ThreadTranscript,
121
+ ToolChip,
121
122
  } from './agent-chat'
122
123
  export type {
123
124
  ThreadWorkspaceProps,
@@ -126,6 +127,7 @@ export type {
126
127
  ThreadDetailPanelProps,
127
128
  ComposerProps,
128
129
  ThreadTranscriptProps,
130
+ ToolChipProps,
129
131
  } from './agent-chat'
130
132
  export { createThreadsStore, heuristicOutcome, useAgentThreadRuns } from './agent'
131
133
  export type {
package/src/surfaces.ts CHANGED
@@ -362,7 +362,7 @@ export const SURFACES = {
362
362
  skill: ['basalt-app'],
363
363
  guardKinds: [],
364
364
  description:
365
- 'Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, PartList, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)',
365
+ 'Headless streaming-chat layer: useAgentStream, aiSdkTransport (recommended default) + edenTransport, isResumable/ResumableAgentTransport (stream-resumption seam), PartList, coalesceParts, the ForeignPart/definePartRenderers/narrowAgentPart open part-registry seam, plus the multi-thread createThreadsStore + useAgentThreadRuns + outcome-resolver seam (Mantine-free)',
366
366
  optionalPeers: ['ai', 'use-stick-to-bottom'],
367
367
  globs: {
368
368
  shipped: [],
@@ -378,7 +378,7 @@ export const SURFACES = {
378
378
  skill: ['basalt-app'],
379
379
  guardKinds: [],
380
380
  description:
381
- 'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript, threadPartRenderers (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality).',
381
+ 'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed, ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props), threadPartRenderers, ToolChip (Mantine-coupled). remend and motion are required, not optional — ThreadTranscript imports content/markdown.tsx (remend eagerly), and ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without both installed even though peerDependenciesMeta marks them optional (npm has no per-subpath optionality).',
382
382
  optionalPeers: [
383
383
  'ai',
384
384
  'motion',