basalt-ui 1.12.0 → 1.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 (123) hide show
  1. package/AGENTS.md +21 -21
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +38 -35
  4. package/agent/rules/basalt-agent.md +14 -0
  5. package/configs/oxlint-plugin.js +7 -3
  6. package/configs/oxlint-plugin.test.ts +34 -5
  7. package/configs/oxlint.json +4 -4
  8. package/dist/agent/adapter.d.ts +5 -1
  9. package/dist/agent/adapter.d.ts.map +1 -1
  10. package/dist/agent/adapter.js +22 -13
  11. package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
  12. package/dist/agent/ai-sdk-transport.js +6 -2
  13. package/dist/agent/id.d.ts +67 -19
  14. package/dist/agent/id.d.ts.map +1 -1
  15. package/dist/agent/id.js +21 -7
  16. package/dist/agent/thread.d.ts +4 -0
  17. package/dist/agent/thread.d.ts.map +1 -1
  18. package/dist/agent/use-agent-stream.d.ts.map +1 -1
  19. package/dist/agent/use-agent-stream.js +2 -2
  20. package/dist/agent/use-agent-thread-runs.d.ts +24 -3
  21. package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
  22. package/dist/agent/use-agent-thread-runs.js +55 -15
  23. package/dist/agent-chat/index.d.ts +4 -0
  24. package/dist/agent-chat/index.d.ts.map +1 -1
  25. package/dist/agent-chat/index.js +2 -0
  26. package/dist/agent-chat/message-affordances.d.ts +59 -0
  27. package/dist/agent-chat/message-affordances.d.ts.map +1 -0
  28. package/dist/agent-chat/message-affordances.js +7 -0
  29. package/dist/agent-chat/relative-time.d.ts +27 -0
  30. package/dist/agent-chat/relative-time.d.ts.map +1 -0
  31. package/dist/agent-chat/relative-time.js +27 -0
  32. package/dist/agent-chat/relative-time.test.d.ts +2 -0
  33. package/dist/agent-chat/relative-time.test.d.ts.map +1 -0
  34. package/dist/agent-chat/thread-feed-row.d.ts +65 -0
  35. package/dist/agent-chat/thread-feed-row.d.ts.map +1 -0
  36. package/dist/agent-chat/thread-feed-row.js +156 -0
  37. package/dist/agent-chat/thread-feed-row.test.d.ts +2 -0
  38. package/dist/agent-chat/thread-feed-row.test.d.ts.map +1 -0
  39. package/dist/agent-chat/thread-feed.d.ts +34 -5
  40. package/dist/agent-chat/thread-feed.d.ts.map +1 -1
  41. package/dist/agent-chat/thread-feed.js +52 -19
  42. package/dist/agent-chat/thread-feed.test.d.ts +2 -0
  43. package/dist/agent-chat/thread-feed.test.d.ts.map +1 -0
  44. package/dist/agent-chat/thread-message.d.ts +183 -2
  45. package/dist/agent-chat/thread-message.d.ts.map +1 -1
  46. package/dist/agent-chat/thread-message.js +315 -34
  47. package/dist/agent-chat/thread-outcome-card.d.ts.map +1 -1
  48. package/dist/agent-chat/thread-outcome-card.js +1 -16
  49. package/dist/agent-chat/thread-workspace.d.ts +14 -2
  50. package/dist/agent-chat/thread-workspace.d.ts.map +1 -1
  51. package/dist/agent-chat/thread-workspace.js +29 -3
  52. package/dist/agent-chat/thread-workspace.test.d.ts +2 -0
  53. package/dist/agent-chat/thread-workspace.test.d.ts.map +1 -0
  54. package/dist/agent-chat/virtualize.d.ts +123 -0
  55. package/dist/agent-chat/virtualize.d.ts.map +1 -0
  56. package/dist/agent-chat/virtualize.js +9 -0
  57. package/dist/agent-chat/virtualize.type-guard.test.d.ts +2 -0
  58. package/dist/agent-chat/virtualize.type-guard.test.d.ts.map +1 -0
  59. package/dist/data/index.d.ts +5 -1
  60. package/dist/data/index.d.ts.map +1 -1
  61. package/dist/data/virtual-list.d.ts.map +1 -1
  62. package/dist/data/virtual.d.ts +2 -1
  63. package/dist/data/virtual.d.ts.map +1 -1
  64. package/dist/index.d.ts +2 -2
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +2 -0
  67. package/dist/shell/app-sidebar.d.ts +19 -1
  68. package/dist/shell/app-sidebar.d.ts.map +1 -1
  69. package/dist/shell/app-sidebar.js +30 -26
  70. package/dist/shell/app-sidebar.module.css +11 -0
  71. package/dist/shell/app-sidebar.module.css.d.ts +1 -0
  72. package/dist/shell/app-sidebar.test.d.ts +2 -0
  73. package/dist/shell/app-sidebar.test.d.ts.map +1 -0
  74. package/dist/shell/index.d.ts +10 -1
  75. package/dist/shell/index.d.ts.map +1 -1
  76. package/dist/shell/index.js +2 -0
  77. package/dist/shell/index.test.d.ts +2 -0
  78. package/dist/shell/index.test.d.ts.map +1 -0
  79. package/dist/surfaces.d.ts +2 -2
  80. package/dist/surfaces.d.ts.map +1 -1
  81. package/dist/surfaces.js +3 -2
  82. package/dist/theme/shadow-surfaces.test.d.ts.map +1 -1
  83. package/llms.txt +6 -6
  84. package/package.json +2 -2
  85. package/src/agent/adapter.test.ts +237 -0
  86. package/src/agent/adapter.ts +72 -14
  87. package/src/agent/ai-sdk-transport.test.ts +64 -0
  88. package/src/agent/ai-sdk-transport.ts +8 -2
  89. package/src/agent/id.test.ts +95 -1
  90. package/src/agent/id.ts +98 -28
  91. package/src/agent/thread.ts +4 -0
  92. package/src/agent/use-agent-stream.test.tsx +22 -0
  93. package/src/agent/use-agent-stream.ts +6 -3
  94. package/src/agent/use-agent-thread-runs.test.tsx +326 -0
  95. package/src/agent/use-agent-thread-runs.ts +159 -23
  96. package/src/agent/use-agent-thread-runs.wedge.test.tsx +80 -0
  97. package/src/agent-chat/index.ts +12 -0
  98. package/src/agent-chat/message-affordances.ts +61 -0
  99. package/src/agent-chat/relative-time.test.ts +95 -0
  100. package/src/agent-chat/relative-time.ts +55 -0
  101. package/src/agent-chat/thread-feed-row.test.tsx +510 -0
  102. package/src/agent-chat/thread-feed-row.tsx +274 -0
  103. package/src/agent-chat/thread-feed.test.tsx +241 -0
  104. package/src/agent-chat/thread-feed.tsx +119 -18
  105. package/src/agent-chat/thread-message.test.tsx +937 -3
  106. package/src/agent-chat/thread-message.tsx +766 -38
  107. package/src/agent-chat/thread-outcome-card.tsx +1 -25
  108. package/src/agent-chat/thread-workspace.test.tsx +169 -0
  109. package/src/agent-chat/thread-workspace.tsx +69 -5
  110. package/src/agent-chat/virtualize.ts +132 -0
  111. package/src/agent-chat/virtualize.type-guard.test.ts +49 -0
  112. package/src/data/index.ts +5 -1
  113. package/src/data/virtual-list.tsx +2 -1
  114. package/src/data/virtual.ts +2 -1
  115. package/src/index.ts +5 -0
  116. package/src/shell/app-sidebar.module.css +11 -0
  117. package/src/shell/app-sidebar.module.css.d.ts +1 -0
  118. package/src/shell/app-sidebar.test.tsx +98 -0
  119. package/src/shell/app-sidebar.tsx +22 -0
  120. package/src/shell/index.test.tsx +35 -0
  121. package/src/shell/index.tsx +11 -0
  122. package/src/surfaces.ts +2 -1
  123. package/src/theme/shadow-surfaces.test.ts +9 -0
@@ -1,53 +1,158 @@
1
1
  /**
2
- * ThreadFeed — a scrollable, animated list of `ThreadOutcomeCard` rows for a multi-thread inbox.
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. Each row renders via
32
- * `ThreadOutcomeCard`; selection highlighting is derived from `activeId`.
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({ threads, activeId, onSelect }: ThreadFeedProps): JSX.Element {
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
- <ThreadOutcomeCard
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
- <ThreadOutcomeCard
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>