basalt-ui 1.9.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.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +18 -0
- package/README.md +64 -34
- package/agent/rules/basalt-agent.md +49 -6
- package/configs/oxlint-plugin.js +279 -0
- package/configs/oxlint-plugin.test.ts +216 -1
- package/configs/oxlint.json +5 -2
- package/dist/agent/ai-sdk-transport.d.ts +3 -52
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +108 -24
- package/dist/agent/coalesce.d.ts +26 -0
- package/dist/agent/coalesce.d.ts.map +1 -0
- package/dist/agent/coalesce.js +69 -0
- package/dist/agent/foreign.d.ts +97 -0
- package/dist/agent/foreign.d.ts.map +1 -0
- package/dist/agent/foreign.js +18 -0
- package/dist/agent/history.d.ts +2 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/id.d.ts +35 -0
- package/dist/agent/id.d.ts.map +1 -0
- package/dist/agent/id.js +14 -0
- package/dist/agent/index.d.ts +9 -4
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +15 -3
- package/dist/agent/merge.d.ts +8 -0
- package/dist/agent/merge.d.ts.map +1 -0
- package/dist/agent/merge.js +23 -0
- package/dist/agent/part-list.d.ts.map +1 -1
- package/dist/agent/part-list.js +16 -6
- package/dist/agent/parts.d.ts +100 -21
- package/dist/agent/parts.d.ts.map +1 -1
- package/dist/agent/parts.js +128 -11
- package/dist/agent/transport.d.ts +39 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/transport.js +20 -17
- package/dist/agent/use-agent-stream.d.ts +2 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +5 -2
- package/dist/agent/use-agent-thread-runs.d.ts +9 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +76 -23
- package/dist/agent-chat/index.d.ts +6 -3
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/thread-message.d.ts +24 -7
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +102 -43
- package/dist/agent-chat/tool-chip.d.ts +21 -0
- package/dist/agent-chat/tool-chip.d.ts.map +1 -0
- package/dist/agent-chat/tool-chip.js +120 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +131 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/surfaces.d.ts +11 -1
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +22 -1
- package/llms.txt +10 -3
- package/package.json +6 -1
- package/src/agent/ai-sdk-transport.test.ts +567 -0
- package/src/agent/ai-sdk-transport.ts +206 -53
- package/src/agent/coalesce.test.ts +194 -0
- package/src/agent/coalesce.ts +118 -0
- package/src/agent/foreign.test.ts +47 -0
- package/src/agent/foreign.ts +140 -0
- package/src/agent/history.ts +2 -0
- package/src/agent/id.test.ts +71 -0
- package/src/agent/id.ts +43 -0
- package/src/agent/index.ts +26 -3
- package/src/agent/merge.test.ts +119 -0
- package/src/agent/merge.ts +67 -0
- package/src/agent/part-list.test.tsx +82 -0
- package/src/agent/part-list.tsx +31 -6
- package/src/agent/parts.test.ts +603 -0
- package/src/agent/parts.ts +262 -32
- package/src/agent/thread.test.ts +147 -0
- package/src/agent/transport.test.ts +70 -0
- package/src/agent/transport.ts +72 -20
- package/src/agent/use-agent-stream.test.tsx +261 -0
- package/src/agent/use-agent-stream.ts +13 -3
- package/src/agent/use-agent-thread-runs.foreign.test.tsx +133 -0
- package/src/agent/use-agent-thread-runs.resume.test.tsx +289 -0
- package/src/agent/use-agent-thread-runs.test.tsx +781 -0
- package/src/agent/use-agent-thread-runs.ts +177 -27
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +301 -0
- package/src/agent-chat/index.ts +8 -3
- package/src/agent-chat/thread-message.test.tsx +211 -0
- package/src/agent-chat/thread-message.tsx +235 -62
- package/src/agent-chat/tool-chip.test.tsx +139 -0
- package/src/agent-chat/tool-chip.tsx +219 -0
- package/src/charts/primitives/ChartFrame.test.tsx +8 -5
- package/src/cli/index.ts +285 -0
- package/src/dashboard/stat-card.test.tsx +5 -3
- package/src/guard/check-source.test.ts +8 -4
- package/src/guard/index.ts +1 -3
- package/src/index.ts +2 -0
- package/src/provider/index.test.tsx +6 -5
- package/src/surfaces.ts +23 -1
- package/src/theme/use-basalt-spacing.test.tsx +7 -4
- package/src/tokens/build-fonts-css.test.ts +3 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolChip — one test per wire state proving it renders without throwing, plus the
|
|
3
|
+
* approve/deny-visibility contract: rendered ONLY in 'approval-requested', and only for whichever
|
|
4
|
+
* of onApprove/onDeny is actually supplied.
|
|
5
|
+
*/
|
|
6
|
+
import { MantineProvider } from '@mantine/core'
|
|
7
|
+
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
|
8
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
9
|
+
import { ToolChip } from './tool-chip'
|
|
10
|
+
import type { ToolCallPart } from '../agent'
|
|
11
|
+
|
|
12
|
+
afterEach(cleanup)
|
|
13
|
+
|
|
14
|
+
function renderChip(part: ToolCallPart, extra: Partial<Parameters<typeof ToolChip>[0]> = {}) {
|
|
15
|
+
return render(
|
|
16
|
+
<MantineProvider>
|
|
17
|
+
<ToolChip part={part} defaultExpanded {...extra} />
|
|
18
|
+
</MantineProvider>,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BASE = { id: 'p1', type: 'tool' as const, toolCallId: 'c1', toolName: 'search' }
|
|
23
|
+
|
|
24
|
+
describe('every wire state renders without throwing', () => {
|
|
25
|
+
const CASES: { name: string; part: ToolCallPart }[] = [
|
|
26
|
+
{ name: 'input-streaming', part: { ...BASE, state: 'input-streaming' } },
|
|
27
|
+
{ name: 'input-available', part: { ...BASE, state: 'input-available', input: { q: 'x' } } },
|
|
28
|
+
{
|
|
29
|
+
name: 'approval-requested',
|
|
30
|
+
part: { ...BASE, state: 'approval-requested', input: { q: 'x' }, approval: { id: 'a1' } },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'approval-responded (approved)',
|
|
34
|
+
part: {
|
|
35
|
+
...BASE,
|
|
36
|
+
state: 'approval-responded',
|
|
37
|
+
input: { q: 'x' },
|
|
38
|
+
approval: { id: 'a1', approved: true },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'approval-responded (declined)',
|
|
43
|
+
part: {
|
|
44
|
+
...BASE,
|
|
45
|
+
state: 'approval-responded',
|
|
46
|
+
input: { q: 'x' },
|
|
47
|
+
approval: { id: 'a1', approved: false },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'output-available',
|
|
52
|
+
part: { ...BASE, state: 'output-available', input: { q: 'x' }, output: { hits: 3 } },
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'output-available (preliminary)',
|
|
56
|
+
part: {
|
|
57
|
+
...BASE,
|
|
58
|
+
state: 'output-available',
|
|
59
|
+
input: { q: 'x' },
|
|
60
|
+
output: { hits: 1 },
|
|
61
|
+
preliminary: true,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'output-error (with rawInput)',
|
|
66
|
+
part: { ...BASE, state: 'output-error', errorText: 'boom', rawInput: { q: 'x' } },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'output-denied (with reason)',
|
|
70
|
+
part: {
|
|
71
|
+
...BASE,
|
|
72
|
+
state: 'output-denied',
|
|
73
|
+
input: { q: 'x' },
|
|
74
|
+
approval: { id: 'a1', approved: false, reason: 'not now' },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
for (const { name, part } of CASES) {
|
|
80
|
+
test(name, () => {
|
|
81
|
+
expect(() => renderChip(part)).not.toThrow()
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('approve/deny affordances', () => {
|
|
87
|
+
const REQUESTED: ToolCallPart = {
|
|
88
|
+
...BASE,
|
|
89
|
+
state: 'approval-requested',
|
|
90
|
+
input: { q: 'x' },
|
|
91
|
+
approval: { id: 'a1' },
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test('read-only when both handlers are omitted', () => {
|
|
95
|
+
renderChip(REQUESTED)
|
|
96
|
+
expect(screen.queryByText('Approve')).toBeNull()
|
|
97
|
+
expect(screen.queryByText('Deny')).toBeNull()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('renders only the supplied handler', () => {
|
|
101
|
+
renderChip(REQUESTED, { onApprove: () => {} })
|
|
102
|
+
expect(screen.queryByText('Approve')).not.toBeNull()
|
|
103
|
+
expect(screen.queryByText('Deny')).toBeNull()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('calls onApprove with the approval id', () => {
|
|
107
|
+
let approvedId: string | undefined
|
|
108
|
+
renderChip(REQUESTED, { onApprove: (id) => (approvedId = id) })
|
|
109
|
+
fireEvent.click(screen.getByText('Approve'))
|
|
110
|
+
expect(approvedId).toBe('a1')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('never renders for a non-approval-requested state even with both handlers supplied', () => {
|
|
114
|
+
renderChip(
|
|
115
|
+
{ ...BASE, state: 'output-available', input: { q: 'x' }, output: {} },
|
|
116
|
+
{ onApprove: () => {}, onDeny: () => {} },
|
|
117
|
+
)
|
|
118
|
+
expect(screen.queryByText('Approve')).toBeNull()
|
|
119
|
+
expect(screen.queryByText('Deny')).toBeNull()
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// F3: a runtime value the type system believes is exhaustively handled — but isn't, once
|
|
124
|
+
// persisted (localStorage, zero shape validation) or wire (edenTransport, zero validation) data
|
|
125
|
+
// is involved — must render an inert 'unknown' chip, never throw via assertNever and blank the
|
|
126
|
+
// transcript.
|
|
127
|
+
describe('a malformed/unrecognized state never throws (F3)', () => {
|
|
128
|
+
test('state undefined (the pre-1.11.0 flat persisted shape) renders "unknown" instead of throwing', () => {
|
|
129
|
+
const malformed = { ...BASE, output: { hits: 1 } } as unknown as ToolCallPart
|
|
130
|
+
expect(() => renderChip(malformed)).not.toThrow()
|
|
131
|
+
expect(screen.getByText('unknown')).not.toBeNull()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('an unrecognized state string renders "unknown" instead of throwing', () => {
|
|
135
|
+
const malformed = { ...BASE, state: 'bogus-future-state' } as unknown as ToolCallPart
|
|
136
|
+
expect(() => renderChip(malformed)).not.toThrow()
|
|
137
|
+
expect(screen.getByText('unknown')).not.toBeNull()
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -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
|
+
}
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* shape back on screen exactly where `ChartPending` is supposed to reserve a static, contentless
|
|
5
5
|
* placeholder (see `ChartPending`'s JSDoc for the three-state "nothing to draw" rationale).
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* A DOM harness now exists (`tests/setup/dom.ts`, preloaded via the root `bunfig.toml`; see
|
|
8
|
+
* `theme/use-basalt-spacing.test.tsx`'s doc) — `renderToStaticMarkup` is used deliberately here
|
|
9
|
+
* instead: `ChartFrame`/`ChartPending` live in `src/charts/**`, which is Mantine-free, so no
|
|
10
|
+
* `MantineProvider` wrapper is needed either. `useChartSize` never measures under SSR (no
|
|
11
|
+
* `ResizeObserver`), so the plot rect falls back to `minWidth` × the resolved fixed height —
|
|
12
|
+
* non-zero, which is what lets `children`/`ChartPending` render at all in this harness. Converting
|
|
13
|
+
* to the DOM harness would only be worth it if a future assertion here needed a real measured size
|
|
14
|
+
* (a live `ResizeObserver` reading) rather than this SSR fallback rect.
|
|
12
15
|
*/
|
|
13
16
|
import { describe, expect, test } from 'bun:test'
|
|
14
17
|
import { renderToStaticMarkup } from 'react-dom/server'
|