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
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `navExtra` — the seam that lets a consumer put arbitrary content (a tree, a filter panel,
3
+ * a project list) inside the sidebar's nav scroll region instead of rendering a second column
4
+ * beside the shell. Four invariants:
5
+ *
6
+ * 1. It renders INSIDE the nav `ScrollArea`'s content (Mantine's own `.mantine-ScrollArea-content`
7
+ * class, stable across runs — CSS-module class hashes are not available under `bun test`, see
8
+ * `stat-card.test.tsx`/`use-basalt-spacing.test.tsx` for the same constraint elsewhere), as the
9
+ * LAST child, so a long tree scrolls with the nav instead of fighting it for height.
10
+ * 2. It STAYS MOUNTED when `collapsed` — `collapsed` is one value shared by the desktop rail AND
11
+ * the mobile drawer (`AppShell.Navbar`'s `collapsed: { mobile: !mobileOpened }` never touches
12
+ * the desktop side), and the rail-vs-drawer split lives entirely in a `min-width: sm` CSS media
13
+ * query, never in JS. A JS gate on `collapsed` would silently drop `navExtra` from the mobile
14
+ * drawer too, which opens at full width regardless of the persisted rail state. jsdom does not
15
+ * evaluate the media query, so this asserts presence in the DOM, not computed visibility — the
16
+ * CSS half (`.root[data-collapsed] .navExtra { display: none }`) is exercised by the guard rules
17
+ * it shares with `.childList`/`.sectionBand`, not by this test.
18
+ * 3. `sections={[]}` plus only `navExtra` renders cleanly: the nav Stack holds exactly the
19
+ * extra content, no orphan section wrapper.
20
+ * 4. Omitting the prop reproduces today's DOM exactly — the nav Stack's child count matches
21
+ * `sections.length`, with no stray wrapper appended for the (absent) slot. This is the
22
+ * regression guard for every existing consumer.
23
+ */
24
+ import { MantineProvider } from '@mantine/core'
25
+ import { render, screen } from '@testing-library/react'
26
+ import { describe, expect, test } from 'bun:test'
27
+ import type { ReactNode } from 'react'
28
+ import { AppSidebar } from './app-sidebar'
29
+ import type { AppSidebarProps } from './app-sidebar'
30
+
31
+ const BRAND = { name: 'Argo' }
32
+ const ONE_SECTION: AppSidebarProps['sections'] = [
33
+ { label: 'Main', items: [{ key: 'home', label: 'Home', icon: null }] },
34
+ ]
35
+
36
+ function renderSidebar(props: Partial<AppSidebarProps> & { navExtra?: ReactNode }) {
37
+ return render(
38
+ <MantineProvider>
39
+ <AppSidebar
40
+ brand={BRAND}
41
+ sections={ONE_SECTION}
42
+ collapsed={false}
43
+ onToggleCollapse={() => {}}
44
+ onClose={() => {}}
45
+ {...props}
46
+ />
47
+ </MantineProvider>,
48
+ )
49
+ }
50
+
51
+ /** The nav `ScrollArea`'s content Stack — the direct parent of `sections` and `navExtra`. */
52
+ function navStack(container: HTMLElement): Element {
53
+ const stack = container.querySelector('.mantine-ScrollArea-content > .mantine-Stack-root')
54
+ if (!stack) throw new Error('expected the nav ScrollArea content Stack to render')
55
+ return stack
56
+ }
57
+
58
+ describe('navExtra', () => {
59
+ test('renders inside the nav scroll region, as the last child, when expanded', () => {
60
+ const { container } = renderSidebar({
61
+ navExtra: <div data-testid="nav-extra">Extra</div>,
62
+ })
63
+ const extra = screen.getByTestId('nav-extra')
64
+ const stack = navStack(container)
65
+ expect(stack.contains(extra)).toBe(true)
66
+ expect(stack.lastElementChild?.contains(extra)).toBe(true)
67
+ })
68
+
69
+ test('stays mounted in the DOM when collapsed — hiding it is CSS-only, not a JS gate', () => {
70
+ const { container } = renderSidebar({
71
+ collapsed: true,
72
+ navExtra: <div data-testid="nav-extra">Extra</div>,
73
+ })
74
+ const extra = screen.getByTestId('nav-extra')
75
+ const stack = navStack(container)
76
+ // Presence, not visibility — jsdom never evaluates the `min-width: sm` media query that hides
77
+ // this in the rail. A regression here (reintroducing `!collapsed &&` in app-sidebar.tsx) would
78
+ // fail this assertion instead of only breaking silently in a real mobile-drawer browser.
79
+ expect(stack.contains(extra)).toBe(true)
80
+ })
81
+
82
+ test('sections={[]} plus only navExtra renders cleanly — no orphan section wrapper', () => {
83
+ const { container } = renderSidebar({
84
+ sections: [],
85
+ navExtra: <div data-testid="nav-extra">Extra</div>,
86
+ })
87
+ const stack = navStack(container)
88
+ expect(screen.getByTestId('nav-extra')).toBeTruthy()
89
+ expect(stack.children.length).toBe(1)
90
+ })
91
+
92
+ test("omitting the prop reproduces today's DOM — no stray wrapper for the unused slot", () => {
93
+ const { container } = renderSidebar({})
94
+ expect(screen.queryByTestId('nav-extra')).toBeNull()
95
+ const stack = navStack(container)
96
+ expect(stack.children.length).toBe(ONE_SECTION.length)
97
+ })
98
+ })
@@ -92,6 +92,24 @@ export type AppSidebarProps = {
92
92
  * fixed, non-scrolling row. Pair with basalt-ui/commands' openSpotlight.
93
93
  */
94
94
  search?: SidebarSearchConfig
95
+ /**
96
+ * Arbitrary content appended after `sections` inside the nav `ScrollArea` — a tree, a filter
97
+ * panel, a project list, anything a set of `SidebarItem`s can't express. Renders as the last
98
+ * child of the scrolling nav column, so a long list scrolls with the rest of the nav instead of
99
+ * fighting it for height. Pass `sections={[]}` to use this slot exclusively; the section-spacing
100
+ * rule only fires between adjacent children, so an empty `sections` produces no orphan divider or
101
+ * dead padding above it.
102
+ *
103
+ * Hidden on the collapsed desktop rail; still present in the mobile drawer, which opens at full
104
+ * width. The rail is ~56px of icon buttons with no sensible representation for arbitrary
105
+ * consumer content, so the CSS media query that drives the rail hides this slot the same way it
106
+ * hides `.childList` — never a JS check on `collapsed`, since that one value is shared by the
107
+ * rail AND the drawer (only the media query tells them apart). `SidebarSearch` gets to adapt
108
+ * itself to the rail (it takes `collapsed` and renders its own icon-only form) because the shell
109
+ * owns that control; it cannot adapt content it knows nothing about, so this slot hides instead
110
+ * of squashing it.
111
+ */
112
+ navExtra?: ReactNode
95
113
  }
96
114
 
97
115
  /** Inline collapse/expand chevrons — keeps the shell icon-dependency-free. */
@@ -457,6 +475,7 @@ export function AppSidebar({
457
475
  footerExtra,
458
476
  account,
459
477
  search,
478
+ navExtra,
460
479
  }: AppSidebarProps) {
461
480
  // Density-tracking Menu dropdown width (`SPACE_STEP.sidebarSettingsMenuWidth`) — read the ACTIVE
462
481
  // resolved level, not the frozen level-0 constant (see that constant's own doc in
@@ -622,6 +641,9 @@ export function AppSidebar({
622
641
  </div>
623
642
  )
624
643
  })}
644
+ {navExtra !== undefined && navExtra !== null && (
645
+ <div className={classes.navExtra}>{navExtra}</div>
646
+ )}
625
647
  </Stack>
626
648
  </ScrollArea>
627
649
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `BasaltShell`'s `sidebarNavExtra` forwarding path — `AppSidebar` (exercised directly in
3
+ * `app-sidebar.test.tsx`) is not the only place this prop can break. `BasaltShell` destructures
4
+ * `sidebarNavExtra` and hands it to `AppSidebar` as `navExtra`; a typo'd destructure key, a dropped
5
+ * prop, or a `sidebarFooterExtra`/`sidebarNavExtra` mix-up in that hand-off would ship silently
6
+ * without a test that renders `BasaltShell` itself.
7
+ */
8
+ import { MantineProvider } from '@mantine/core'
9
+ import { render, screen } from '@testing-library/react'
10
+ import { describe, expect, test } from 'bun:test'
11
+ import { BasaltShell } from './index'
12
+ import type { SidebarSection } from './index'
13
+
14
+ const BRAND = { name: 'Argo' }
15
+ const ONE_SECTION: SidebarSection[] = [
16
+ { label: 'Main', items: [{ key: 'home', label: 'Home', icon: null }] },
17
+ ]
18
+
19
+ describe('BasaltShell sidebarNavExtra', () => {
20
+ test("reaches AppSidebar's nav scroll region through the BasaltShell -> AppSidebar hand-off", () => {
21
+ const { container } = render(
22
+ <MantineProvider>
23
+ <BasaltShell
24
+ brand={BRAND}
25
+ sections={ONE_SECTION}
26
+ sidebarNavExtra={<div data-testid="nav-extra">Extra</div>}
27
+ />
28
+ </MantineProvider>,
29
+ )
30
+ const extra = screen.getByTestId('nav-extra')
31
+ const stack = container.querySelector('.mantine-ScrollArea-content > .mantine-Stack-root')
32
+ expect(stack).not.toBeNull()
33
+ expect(stack?.contains(extra)).toBe(true)
34
+ })
35
+ })
@@ -115,6 +115,15 @@ export type BasaltShellProps = {
115
115
  globalActions?: ReactNode
116
116
  /** Extra content appended to the sidebar footer, beside the settings menu (mobile close, etc.). */
117
117
  sidebarFooterExtra?: ReactNode
118
+ /**
119
+ * Arbitrary content appended after `sections` inside the sidebar's nav scroll region (a tree, a
120
+ * filter panel, a project list, …). Hidden on the collapsed desktop rail; still present in the
121
+ * mobile drawer, which opens at full width — see `AppSidebarProps.navExtra`'s JSDoc for why.
122
+ * `AppSidebar` renders once and serves both the desktop rail and the mobile drawer (collapse
123
+ * styling is gated behind a `min-width: sm` media query, not a second render), so this slot
124
+ * reaches both.
125
+ */
126
+ sidebarNavExtra?: ReactNode
118
127
  /** Entries appended to the sidebar settings menu. */
119
128
  settingsMenuItems?: SettingsMenuItem[]
120
129
  /**
@@ -262,6 +271,7 @@ export function BasaltShell({
262
271
  renderNavLink,
263
272
  globalActions,
264
273
  sidebarFooterExtra,
274
+ sidebarNavExtra,
265
275
  settingsMenuItems,
266
276
  storageKey = 'basalt-sidebar-collapsed',
267
277
  collapsed: collapsedProp,
@@ -332,6 +342,7 @@ export function BasaltShell({
332
342
  onToggleCollapse={toggleCollapse}
333
343
  onClose={closeMobile}
334
344
  footerExtra={sidebarFooterExtra}
345
+ navExtra={sidebarNavExtra}
335
346
  {...(renderNavLink !== undefined && { renderNavLink })}
336
347
  {...(settingsMenuItems !== undefined && { settingsMenuItems })}
337
348
  {...(account !== undefined && { account })}
package/src/surfaces.ts CHANGED
@@ -385,7 +385,7 @@ export const SURFACES = {
385
385
  skill: ['basalt-app'],
386
386
  guardKinds: [],
387
387
  description:
388
- '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). motion is required, not optional — ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without it installed even though peerDependenciesMeta marks it optional (npm has no per-subpath optionality). remend is genuinely optional here — ThreadTranscript reaches it only through the lazy dynamic import() inside content/markdown.tsx.',
388
+ 'Mantine-styled thread-chat components over basalt-ui/agent: ThreadWorkspace, ThreadFeed (variant/renderRow), ThreadFeedRow (inline-expanding Slack row, lazily mounted + kept mounted), ThreadOutcomeCard, ThreadDetailPanel, Composer, ThreadTranscript (open part-renderer registry via its renderers/fallbackRenderer props, per-message MessageAffordances, groupConsecutive, and an optional virtualize/height windowing mode whose VirtualizeOptions carry overscan/estimateSize/initialScroll — a virtualized transcript opens scrolled to the newest message unless initialScroll is "start"), threadPartRenderers, ToolChip (Mantine-coupled). motion is required, not optional — ThreadFeed/ThreadDetailPanel import motion/react eagerly, so this subpath fails to resolve without it installed even though peerDependenciesMeta marks it optional (npm has no per-subpath optionality). remend is genuinely optional here — ThreadTranscript reaches it only through the lazy dynamic import() inside content/markdown.tsx, and @tanstack/react-virtual the same way through the lazy import() behind ThreadTranscript virtualize (absent peer degrades to an unwindowed, height-bound pane).',
389
389
  optionalPeers: [
390
390
  'ai',
391
391
  'motion',
@@ -397,6 +397,7 @@ export const SURFACES = {
397
397
  '@shikijs/langs',
398
398
  '@shikijs/themes',
399
399
  'beautiful-mermaid',
400
+ '@tanstack/react-virtual',
400
401
  ],
401
402
  forbiddenImports: [],
402
403
  },
@@ -386,6 +386,15 @@ export const SHADOW_SURFACES: readonly ShadowSurfaceEntry[] = [
386
386
  site: 'default',
387
387
  roundedBy: 'borderRadius: VX.radiusCard (co-declared in the same inline style object).',
388
388
  },
389
+ {
390
+ file: 'agent-chat/thread-feed-row.tsx',
391
+ site: 'default',
392
+ roundedBy:
393
+ 'borderRadius: VX.radiusCard (co-declared in the same inline style object as the shadow). ' +
394
+ 'This box also carries overflow: hidden, which is what lets the header button and the ' +
395
+ 'expanded body square off against the row’s rounded corners; an element’s own overflow ' +
396
+ 'never clips its own ring, so the shadow is unaffected.',
397
+ },
389
398
  // ── agent ────────────────────────────────────────────────────────────────────────────────────
390
399
  {
391
400
  file: 'agent/stick-to-bottom.tsx',