basalt-ui 1.12.0 → 1.13.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 +21 -21
- package/CHANGELOG.md +17 -0
- package/README.md +38 -35
- package/agent/rules/basalt-agent.md +14 -0
- package/configs/oxlint-plugin.js +7 -3
- package/configs/oxlint-plugin.test.ts +34 -5
- package/configs/oxlint.json +4 -4
- package/dist/agent/adapter.d.ts +5 -1
- package/dist/agent/adapter.d.ts.map +1 -1
- package/dist/agent/adapter.js +22 -13
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +6 -2
- package/dist/agent/id.d.ts +67 -19
- package/dist/agent/id.d.ts.map +1 -1
- package/dist/agent/id.js +21 -7
- package/dist/agent/thread.d.ts +4 -0
- package/dist/agent/thread.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.d.ts.map +1 -1
- package/dist/agent/use-agent-stream.js +2 -2
- package/dist/agent/use-agent-thread-runs.d.ts +24 -3
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +55 -15
- package/dist/agent-chat/index.d.ts +4 -0
- package/dist/agent-chat/index.d.ts.map +1 -1
- package/dist/agent-chat/index.js +2 -0
- package/dist/agent-chat/message-affordances.d.ts +59 -0
- package/dist/agent-chat/message-affordances.d.ts.map +1 -0
- package/dist/agent-chat/message-affordances.js +7 -0
- package/dist/agent-chat/relative-time.d.ts +27 -0
- package/dist/agent-chat/relative-time.d.ts.map +1 -0
- package/dist/agent-chat/relative-time.js +27 -0
- package/dist/agent-chat/relative-time.test.d.ts +2 -0
- package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.d.ts +65 -0
- package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed-row.js +156 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-feed.d.ts +34 -5
- package/dist/agent-chat/thread-feed.d.ts.map +1 -1
- package/dist/agent-chat/thread-feed.js +52 -19
- package/dist/agent-chat/thread-feed.test.d.ts +2 -0
- package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
- package/dist/agent-chat/thread-message.d.ts +183 -2
- package/dist/agent-chat/thread-message.d.ts.map +1 -1
- package/dist/agent-chat/thread-message.js +315 -34
- package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
- package/dist/agent-chat/thread-outcome-card.js +1 -16
- package/dist/agent-chat/thread-workspace.d.ts +14 -2
- package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
- package/dist/agent-chat/thread-workspace.js +29 -3
- package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
- package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.d.ts +123 -0
- package/dist/agent-chat/virtualize.d.ts.map +1 -0
- package/dist/agent-chat/virtualize.js +9 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
- package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/virtual-list.d.ts.map +1 -1
- package/dist/data/virtual.d.ts +2 -1
- package/dist/data/virtual.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/surfaces.d.ts +2 -2
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +3 -2
- package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
- package/llms.txt +6 -6
- package/package.json +2 -2
- package/src/agent/adapter.test.ts +237 -0
- package/src/agent/adapter.ts +72 -14
- package/src/agent/ai-sdk-transport.test.ts +64 -0
- package/src/agent/ai-sdk-transport.ts +8 -2
- package/src/agent/id.test.ts +95 -1
- package/src/agent/id.ts +98 -28
- package/src/agent/thread.ts +4 -0
- package/src/agent/use-agent-stream.test.tsx +22 -0
- package/src/agent/use-agent-stream.ts +6 -3
- package/src/agent/use-agent-thread-runs.test.tsx +326 -0
- package/src/agent/use-agent-thread-runs.ts +159 -23
- package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
- package/src/agent-chat/index.ts +12 -0
- package/src/agent-chat/message-affordances.ts +61 -0
- package/src/agent-chat/relative-time.test.ts +95 -0
- package/src/agent-chat/relative-time.ts +55 -0
- package/src/agent-chat/thread-feed-row.test.tsx +510 -0
- package/src/agent-chat/thread-feed-row.tsx +274 -0
- package/src/agent-chat/thread-feed.test.tsx +241 -0
- package/src/agent-chat/thread-feed.tsx +119 -18
- package/src/agent-chat/thread-message.test.tsx +937 -3
- package/src/agent-chat/thread-message.tsx +766 -38
- package/src/agent-chat/thread-outcome-card.tsx +1 -25
- package/src/agent-chat/thread-workspace.test.tsx +169 -0
- package/src/agent-chat/thread-workspace.tsx +69 -5
- package/src/agent-chat/virtualize.ts +132 -0
- package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
- package/src/data/index.ts +5 -1
- package/src/data/virtual-list.tsx +2 -1
- package/src/data/virtual.ts +2 -1
- package/src/index.ts +5 -0
- package/src/surfaces.ts +2 -1
- package/src/theme/shadow-surfaces.test.ts +9 -0
|
@@ -1,53 +1,158 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ThreadFeed — a scrollable, animated list of
|
|
2
|
+
* ThreadFeed — a scrollable, animated list of thread rows for a multi-thread inbox.
|
|
3
3
|
*
|
|
4
4
|
* Wraps the list in `AnimatePresence` (`popLayout`) so removing/reordering threads reflows the
|
|
5
5
|
* remaining rows smoothly; each row is a `motion.div` keyed by `thread.id` (never index — index
|
|
6
6
|
* keys would corrupt the layout animation identity when threads are prepended/removed). Branches
|
|
7
7
|
* on `useReducedMotion` for a plain, unanimated `Stack`.
|
|
8
8
|
*
|
|
9
|
+
* Row content is picked by `variant` (default `'outcome'`, today's `ThreadOutcomeCard` inbox
|
|
10
|
+
* behaviour, unchanged) or `'inline'` (`ThreadFeedRow`, the Slack shape) — or by `renderRow`, which
|
|
11
|
+
* takes priority over `variant` entirely when supplied. The built-in `'inline'` row sends through
|
|
12
|
+
* `onSend` when supplied; omit it and the row's composer renders visibly disabled rather than
|
|
13
|
+
* silently discarding input — see `onSend`'s own doc.
|
|
14
|
+
*
|
|
9
15
|
* @example
|
|
10
16
|
* import { ThreadFeed } from 'basalt-ui'
|
|
11
17
|
*
|
|
12
18
|
* <ThreadFeed threads={threads} activeId={activeId} onSelect={select} />
|
|
13
19
|
*/
|
|
14
|
-
import { ScrollArea, Stack } from '@mantine/core'
|
|
20
|
+
import { Box, ScrollArea, Stack } from '@mantine/core'
|
|
15
21
|
import { useReducedMotion } from '@mantine/hooks'
|
|
16
22
|
import { AnimatePresence, motion } from 'motion/react'
|
|
17
|
-
import type { JSX } from 'react'
|
|
23
|
+
import type { JSX, ReactNode } from 'react'
|
|
24
|
+
import { useEffect, useState } from 'react'
|
|
18
25
|
import type { AgentThread } from '../agent'
|
|
19
26
|
import { MOTION_SPRING } from '../motion'
|
|
27
|
+
import type { ComposerSubmit } from './composer'
|
|
28
|
+
import { ThreadFeedRow } from './thread-feed-row'
|
|
20
29
|
import { ThreadOutcomeCard } from './thread-outcome-card'
|
|
21
30
|
|
|
31
|
+
/** A submit handler for the built-in `'inline'` row when the consumer supplies no `onSend` — paired
|
|
32
|
+
* with `composerProps={{ disabled: true }}` so it is unreachable, not silently reachable. See
|
|
33
|
+
* `onSend`'s doc for why an inert composer beats a live one that eats input. */
|
|
34
|
+
function noopSend(): void {}
|
|
35
|
+
|
|
22
36
|
export type ThreadFeedProps = {
|
|
23
37
|
readonly threads: AgentThread[]
|
|
24
38
|
/** The currently open thread id, or null when none is selected. */
|
|
25
39
|
readonly activeId: string | null
|
|
26
|
-
/** Called with a thread's id when its row is selected.
|
|
40
|
+
/** Called with a thread's id when its row is selected. Never called to report a collapse — see
|
|
41
|
+
* `variant`'s doc for how the built-in `'inline'` row's expand/collapse is decoupled from this. */
|
|
27
42
|
readonly onSelect: (id: string) => void
|
|
43
|
+
/**
|
|
44
|
+
* Row shape: `'outcome'` (default) renders today's `ThreadOutcomeCard` inbox row, unchanged.
|
|
45
|
+
* `'inline'` renders `ThreadFeedRow`, the Slack-style inline-expanding row, wired to this feed's
|
|
46
|
+
* own `activeId` for which row is open (so at most one row is open at a time, mirroring the
|
|
47
|
+
* inbox's single-selection model) — with its OWN collapse tracking layered on top, not a raw
|
|
48
|
+
* pass-through of `onSelect` as `onToggle`. `onSelect` is documented as a selection event, and
|
|
49
|
+
* collapsing a row that remains the active thread isn't one; wiring `onToggle={onSelect}`
|
|
50
|
+
* directly (the previous shape) meant a plain `onSelect={setActiveId}` consumer could never
|
|
51
|
+
* collapse a row, since re-selecting the same id is a no-op state update. Collapsing here is
|
|
52
|
+
* local, UI-only state, reset whenever `activeId` changes to point at a DIFFERENT thread. Since
|
|
53
|
+
* this reset is a `useEffect` keyed on `activeId`, re-driving it to the SAME id from outside
|
|
54
|
+
* this component is a no-op React dep comparison, so it does not clear the override.
|
|
55
|
+
*/
|
|
56
|
+
readonly variant?: 'outcome' | 'inline'
|
|
57
|
+
/**
|
|
58
|
+
* Wires the built-in `'inline'` row's composer to a real send channel — called with the thread
|
|
59
|
+
* and the composer's submit payload. Omit it and the row's composer renders disabled (visibly
|
|
60
|
+
* inert) instead of a live control that silently discards whatever is typed into it; a consumer
|
|
61
|
+
* that needs the composer live must supply this (or `renderRow`, for full control over the row).
|
|
62
|
+
*/
|
|
63
|
+
readonly onSend?: (thread: AgentThread, payload: ComposerSubmit) => void
|
|
64
|
+
/** Overrides row rendering entirely, for BOTH variants — when supplied, this is called for every
|
|
65
|
+
* thread instead of either built-in row. Gives full control over live-run wiring
|
|
66
|
+
* (`onStop`/`liveParts`/`liveStatus`) the built-in `'inline'` row doesn't expose; see `variant`
|
|
67
|
+
* and `onSend`. */
|
|
68
|
+
readonly renderRow?: (thread: AgentThread) => ReactNode
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function defaultRow(
|
|
72
|
+
thread: AgentThread,
|
|
73
|
+
variant: 'outcome' | 'inline',
|
|
74
|
+
activeId: string | null,
|
|
75
|
+
collapsedId: string | null,
|
|
76
|
+
onToggle: (id: string) => void,
|
|
77
|
+
onSelect: (id: string) => void,
|
|
78
|
+
onSend: ((thread: AgentThread, payload: ComposerSubmit) => void) | undefined,
|
|
79
|
+
): ReactNode {
|
|
80
|
+
if (variant === 'inline') {
|
|
81
|
+
const expanded = thread.id === activeId && thread.id !== collapsedId
|
|
82
|
+
return (
|
|
83
|
+
<ThreadFeedRow
|
|
84
|
+
thread={thread}
|
|
85
|
+
expanded={expanded}
|
|
86
|
+
onToggle={onToggle}
|
|
87
|
+
onSend={onSend !== undefined ? (payload) => onSend(thread, payload) : noopSend}
|
|
88
|
+
{...(onSend === undefined ? { composerProps: { disabled: true } } : {})}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return (
|
|
93
|
+
<ThreadOutcomeCard
|
|
94
|
+
thread={thread}
|
|
95
|
+
selected={thread.id === activeId}
|
|
96
|
+
onSelect={() => onSelect(thread.id)}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
28
99
|
}
|
|
29
100
|
|
|
30
101
|
/**
|
|
31
|
-
* A scrollable feed of thread rows, animated on add/remove/reorder.
|
|
32
|
-
* `
|
|
102
|
+
* A scrollable feed of thread rows, animated on add/remove/reorder. Row content comes from
|
|
103
|
+
* `renderRow` when supplied, else from the built-in `variant` row (`ThreadOutcomeCard` or
|
|
104
|
+
* `ThreadFeedRow`) — see `ThreadFeedProps` for the full contract.
|
|
33
105
|
*
|
|
34
106
|
* @example
|
|
35
107
|
* <ThreadFeed threads={threads} activeId={activeId} onSelect={(id) => select(id)} />
|
|
36
108
|
*/
|
|
37
|
-
export function ThreadFeed({
|
|
109
|
+
export function ThreadFeed({
|
|
110
|
+
threads,
|
|
111
|
+
activeId,
|
|
112
|
+
onSelect,
|
|
113
|
+
variant = 'outcome',
|
|
114
|
+
onSend,
|
|
115
|
+
renderRow,
|
|
116
|
+
}: ThreadFeedProps): JSX.Element {
|
|
38
117
|
const reduceMotion = useReducedMotion()
|
|
39
118
|
|
|
119
|
+
// The inline row's own manual-collapse override — see `ThreadFeedProps.variant`'s doc. Only ever
|
|
120
|
+
// meaningful for the id it names; a stale value left over for a since-deselected thread is inert
|
|
121
|
+
// (the per-row `expanded` check below always requires `thread.id === activeId` too).
|
|
122
|
+
const [collapsedId, setCollapsedId] = useState<string | null>(null)
|
|
123
|
+
// Any externally-driven move of `activeId` to a DIFFERENT thread clears a stale override. React
|
|
124
|
+
// bails on a same-value dep, so re-driving `activeId` to the id it already holds (e.g. the SAME
|
|
125
|
+
// thread re-selected after being manually collapsed) does NOT re-run this effect. A collapse
|
|
126
|
+
// triggered by this component's own header click does NOT touch `activeId` at all (see
|
|
127
|
+
// `handleInlineToggle`), so it does not re-trigger this effect either.
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
setCollapsedId(null)
|
|
130
|
+
}, [activeId])
|
|
131
|
+
|
|
132
|
+
function handleInlineToggle(id: string): void {
|
|
133
|
+
const isOpen = id === activeId && id !== collapsedId
|
|
134
|
+
if (isOpen) {
|
|
135
|
+
// Collapsing the already-selected row is a visual-only action. `onSelect` is documented as a
|
|
136
|
+
// SELECTION event; a row that remains the active thread while visually collapsed hasn't had
|
|
137
|
+
// its selection change, so this does not call it.
|
|
138
|
+
setCollapsedId(id)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
setCollapsedId(null)
|
|
142
|
+
onSelect(id)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rowFor = (thread: AgentThread): ReactNode =>
|
|
146
|
+
renderRow !== undefined
|
|
147
|
+
? renderRow(thread)
|
|
148
|
+
: defaultRow(thread, variant, activeId, collapsedId, handleInlineToggle, onSelect, onSend)
|
|
149
|
+
|
|
40
150
|
if (reduceMotion) {
|
|
41
151
|
return (
|
|
42
152
|
<ScrollArea style={{ height: '100%' }}>
|
|
43
153
|
<Stack gap="sm" p="sm">
|
|
44
154
|
{threads.map((thread) => (
|
|
45
|
-
<
|
|
46
|
-
key={thread.id}
|
|
47
|
-
thread={thread}
|
|
48
|
-
selected={thread.id === activeId}
|
|
49
|
-
onSelect={() => onSelect(thread.id)}
|
|
50
|
-
/>
|
|
155
|
+
<Box key={thread.id}>{rowFor(thread)}</Box>
|
|
51
156
|
))}
|
|
52
157
|
</Stack>
|
|
53
158
|
</ScrollArea>
|
|
@@ -67,11 +172,7 @@ export function ThreadFeed({ threads, activeId, onSelect }: ThreadFeedProps): JS
|
|
|
67
172
|
exit={{ opacity: 0, scale: 0.97 }}
|
|
68
173
|
transition={MOTION_SPRING}
|
|
69
174
|
>
|
|
70
|
-
|
|
71
|
-
thread={thread}
|
|
72
|
-
selected={thread.id === activeId}
|
|
73
|
-
onSelect={() => onSelect(thread.id)}
|
|
74
|
-
/>
|
|
175
|
+
{rowFor(thread)}
|
|
75
176
|
</motion.div>
|
|
76
177
|
))}
|
|
77
178
|
</AnimatePresence>
|