@iloveagents/foundry-web-ui 0.3.0 → 0.4.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 (223) hide show
  1. package/README.md +14 -5
  2. package/dist/components/ag-ui-runtime-provider.d.ts +64 -0
  3. package/dist/components/ag-ui-runtime-provider.js +71 -0
  4. package/dist/components/app-brand.d.ts +6 -0
  5. package/dist/components/app-brand.js +13 -0
  6. package/dist/components/assistant-chat.d.ts +26 -0
  7. package/dist/components/assistant-chat.js +93 -0
  8. package/dist/components/chat-attachments.d.ts +25 -0
  9. package/dist/components/chat-attachments.js +20 -0
  10. package/dist/components/chat-bubble.d.ts +1 -0
  11. package/dist/components/chat-bubble.js +103 -0
  12. package/dist/components/chat-header.d.ts +2 -0
  13. package/dist/components/chat-header.js +16 -0
  14. package/dist/components/client-tool-executor.d.ts +12 -0
  15. package/dist/components/client-tool-executor.js +108 -0
  16. package/dist/components/collection-empty-state.d.ts +9 -0
  17. package/dist/components/collection-empty-state.js +6 -0
  18. package/dist/components/collection-filter-bar.d.ts +19 -0
  19. package/dist/components/collection-filter-bar.js +28 -0
  20. package/dist/components/collection-search-input.d.ts +6 -0
  21. package/dist/components/collection-search-input.js +8 -0
  22. package/dist/components/collection-skeleton.d.ts +6 -0
  23. package/dist/components/collection-skeleton.js +10 -0
  24. package/dist/components/collection-sort-menu.d.ts +10 -0
  25. package/dist/components/collection-sort-menu.js +9 -0
  26. package/dist/components/collection-surface.d.ts +8 -0
  27. package/dist/components/collection-surface.js +5 -0
  28. package/dist/components/collection-toolbar.d.ts +9 -0
  29. package/dist/components/collection-toolbar.js +5 -0
  30. package/dist/components/collection-view-toggle.d.ts +7 -0
  31. package/dist/components/collection-view-toggle.js +17 -0
  32. package/dist/components/composer-submit-bridge.d.ts +1 -0
  33. package/dist/components/composer-submit-bridge.js +15 -0
  34. package/dist/components/confirm-dialog.d.ts +11 -0
  35. package/dist/components/confirm-dialog.js +6 -0
  36. package/dist/components/confirmation-card.d.ts +14 -0
  37. package/dist/components/confirmation-card.js +12 -0
  38. package/dist/components/context-badges.d.ts +16 -0
  39. package/dist/components/context-badges.js +56 -0
  40. package/dist/components/context-bar.d.ts +41 -0
  41. package/dist/components/context-bar.js +123 -0
  42. package/dist/components/form-dialog.d.ts +12 -0
  43. package/dist/components/form-dialog.js +13 -0
  44. package/dist/components/global-selection-popover.d.ts +7 -0
  45. package/dist/components/global-selection-popover.js +167 -0
  46. package/dist/components/infinite-scroll-sentinel.d.ts +6 -0
  47. package/dist/components/infinite-scroll-sentinel.js +25 -0
  48. package/dist/components/json-viewer.d.ts +10 -0
  49. package/dist/components/json-viewer.js +80 -0
  50. package/dist/components/loading-indicator.d.ts +4 -0
  51. package/dist/components/loading-indicator.js +22 -0
  52. package/dist/components/markdown-text.d.ts +13 -0
  53. package/dist/components/markdown-text.js +221 -0
  54. package/dist/components/name-dialog.d.ts +16 -0
  55. package/dist/components/name-dialog.js +23 -0
  56. package/dist/components/selection-popover.d.ts +12 -0
  57. package/dist/components/selection-popover.js +111 -0
  58. package/dist/components/show-document-tool-ui.d.ts +1 -0
  59. package/dist/components/show-document-tool-ui.js +53 -0
  60. package/dist/components/sidebar.d.ts +4 -0
  61. package/dist/components/sidebar.js +904 -0
  62. package/dist/components/surface-card.d.ts +9 -0
  63. package/dist/components/surface-card.js +33 -0
  64. package/dist/components/theme-runtime-provider.d.ts +15 -0
  65. package/dist/components/theme-runtime-provider.js +48 -0
  66. package/dist/components/theme-toggle.d.ts +2 -0
  67. package/dist/components/theme-toggle.js +19 -0
  68. package/dist/components/tool-call-card.d.ts +27 -0
  69. package/dist/components/tool-call-card.js +13 -0
  70. package/dist/components/tool-fallback.d.ts +26 -0
  71. package/dist/components/tool-fallback.js +129 -0
  72. package/dist/components/tool-panel-layout.d.ts +10 -0
  73. package/dist/components/tool-panel-layout.js +12 -0
  74. package/dist/components/tool-panel.d.ts +1 -0
  75. package/dist/components/tool-panel.js +89 -0
  76. package/dist/components/tooltip-icon-button.d.ts +7 -0
  77. package/dist/components/tooltip-icon-button.js +9 -0
  78. package/dist/components/user-menu.d.ts +2 -0
  79. package/dist/components/user-menu.js +14 -0
  80. package/dist/index.d.ts +68 -0
  81. package/dist/index.js +83 -0
  82. package/dist/lib/ag-ui-adapter.d.ts +29 -0
  83. package/dist/lib/ag-ui-adapter.js +373 -0
  84. package/dist/lib/app-store.d.ts +115 -0
  85. package/dist/lib/app-store.js +199 -0
  86. package/dist/lib/attachment-adapter.d.ts +20 -0
  87. package/dist/lib/attachment-adapter.js +99 -0
  88. package/dist/lib/auth-provider.d.ts +16 -0
  89. package/dist/lib/auth-provider.js +88 -0
  90. package/dist/lib/chat-bubble-store.d.ts +30 -0
  91. package/dist/lib/chat-bubble-store.js +23 -0
  92. package/dist/lib/chat-lifecycle-store.d.ts +10 -0
  93. package/{src/lib/chat-lifecycle-store.ts → dist/lib/chat-lifecycle-store.js} +3 -13
  94. package/dist/lib/client-tools.d.ts +11 -0
  95. package/dist/lib/client-tools.js +274 -0
  96. package/dist/lib/composer-submit-store.d.ts +12 -0
  97. package/dist/lib/composer-submit-store.js +17 -0
  98. package/dist/lib/dev-store.d.ts +38 -0
  99. package/dist/lib/dev-store.js +32 -0
  100. package/dist/lib/nav-config.d.ts +159 -0
  101. package/dist/lib/nav-config.js +162 -0
  102. package/dist/lib/nav-dnd.d.ts +8 -0
  103. package/dist/lib/nav-dnd.js +9 -0
  104. package/dist/lib/nav-store.d.ts +45 -0
  105. package/dist/lib/nav-store.js +49 -0
  106. package/dist/lib/selection-context.d.ts +21 -0
  107. package/dist/lib/selection-context.js +34 -0
  108. package/dist/lib/sidebar-store.d.ts +23 -0
  109. package/dist/lib/sidebar-store.js +98 -0
  110. package/dist/lib/theme-runtime.d.ts +130 -0
  111. package/dist/lib/theme-runtime.js +479 -0
  112. package/dist/lib/theme-store.d.ts +15 -0
  113. package/dist/lib/theme-store.js +53 -0
  114. package/dist/lib/tool-panel-store.d.ts +48 -0
  115. package/dist/lib/tool-panel-store.js +69 -0
  116. package/dist/lib/use-new-conversation.d.ts +8 -0
  117. package/dist/lib/use-new-conversation.js +62 -0
  118. package/dist/lib/use-page-tools.d.ts +27 -0
  119. package/{src/lib/use-page-tools.ts → dist/lib/use-page-tools.js} +11 -13
  120. package/dist/styles.css +142 -0
  121. package/dist/ui/collapsible.d.ts +5 -0
  122. package/{src/ui/collapsible.tsx → dist/ui/collapsible.js} +0 -2
  123. package/dist/ui/dialog.d.ts +15 -0
  124. package/dist/ui/dialog.js +25 -0
  125. package/dist/ui/dropdown-menu.d.ts +7 -0
  126. package/dist/ui/dropdown-menu.js +19 -0
  127. package/dist/ui/popover.d.ts +7 -0
  128. package/dist/ui/popover.js +10 -0
  129. package/dist/ui/select.d.ts +4 -0
  130. package/dist/ui/select.js +9 -0
  131. package/dist/ui/tooltip.d.ts +7 -0
  132. package/dist/ui/tooltip.js +10 -0
  133. package/package.json +31 -12
  134. package/AGENTS.md +0 -59
  135. package/CHANGELOG.md +0 -203
  136. package/CLAUDE.md +0 -1
  137. package/src/__tests__/no-spaces-imports.test.ts +0 -59
  138. package/src/__tests__/open-core-guards.test.ts +0 -307
  139. package/src/__tests__/theme-runtime.test.ts +0 -81
  140. package/src/__tests__/tool-fallback.test.tsx +0 -109
  141. package/src/components/__tests__/context-bar.test.ts +0 -112
  142. package/src/components/ag-ui-runtime-provider.tsx +0 -170
  143. package/src/components/app-brand.tsx +0 -38
  144. package/src/components/assistant-chat.tsx +0 -461
  145. package/src/components/chat-attachments.tsx +0 -68
  146. package/src/components/chat-bubble.tsx +0 -373
  147. package/src/components/chat-header.tsx +0 -44
  148. package/src/components/client-tool-executor.tsx +0 -230
  149. package/src/components/collection-empty-state.tsx +0 -37
  150. package/src/components/collection-filter-bar.tsx +0 -168
  151. package/src/components/collection-search-input.tsx +0 -41
  152. package/src/components/collection-skeleton.tsx +0 -55
  153. package/src/components/collection-sort-menu.tsx +0 -62
  154. package/src/components/collection-surface.tsx +0 -46
  155. package/src/components/collection-toolbar.tsx +0 -33
  156. package/src/components/collection-view-toggle.tsx +0 -61
  157. package/src/components/composer-submit-bridge.tsx +0 -19
  158. package/src/components/confirm-dialog.tsx +0 -49
  159. package/src/components/confirmation-card.tsx +0 -32
  160. package/src/components/context-badges.tsx +0 -136
  161. package/src/components/context-bar.tsx +0 -238
  162. package/src/components/form-dialog.tsx +0 -56
  163. package/src/components/global-selection-popover.tsx +0 -266
  164. package/src/components/infinite-scroll-sentinel.tsx +0 -48
  165. package/src/components/json-viewer.tsx +0 -232
  166. package/src/components/loading-indicator.tsx +0 -40
  167. package/src/components/markdown-text-citations.test.tsx +0 -108
  168. package/src/components/markdown-text.tsx +0 -508
  169. package/src/components/name-dialog.tsx +0 -87
  170. package/src/components/selection-popover.tsx +0 -156
  171. package/src/components/show-document-tool-ui.tsx +0 -90
  172. package/src/components/sidebar.test.tsx +0 -611
  173. package/src/components/sidebar.tsx +0 -1689
  174. package/src/components/surface-card.tsx +0 -45
  175. package/src/components/theme-runtime-provider.tsx +0 -93
  176. package/src/components/theme-toggle.tsx +0 -27
  177. package/src/components/tool-call-card.tsx +0 -126
  178. package/src/components/tool-fallback.tsx +0 -169
  179. package/src/components/tool-panel-layout.tsx +0 -36
  180. package/src/components/tool-panel.tsx +0 -233
  181. package/src/components/tooltip-icon-button.tsx +0 -28
  182. package/src/components/user-menu.tsx +0 -60
  183. package/src/index.ts +0 -185
  184. package/src/lib/__tests__/ag-ui-adapter.test.ts +0 -273
  185. package/src/lib/__tests__/app-store.test.ts +0 -405
  186. package/src/lib/__tests__/attachment-adapter.test.ts +0 -48
  187. package/src/lib/__tests__/chat-bubble-store.test.ts +0 -67
  188. package/src/lib/__tests__/client-tools.test.ts +0 -124
  189. package/src/lib/__tests__/dev-store.test.ts +0 -78
  190. package/src/lib/__tests__/nav-config.test.ts +0 -135
  191. package/src/lib/__tests__/nav-dnd.test.ts +0 -24
  192. package/src/lib/__tests__/nav-store.test.ts +0 -59
  193. package/src/lib/__tests__/selection-context.test.ts +0 -114
  194. package/src/lib/__tests__/sidebar-store.test.ts +0 -144
  195. package/src/lib/__tests__/theme-store.test.ts +0 -83
  196. package/src/lib/__tests__/tool-panel-store.test.ts +0 -61
  197. package/src/lib/__tests__/use-page-context.test.ts +0 -58
  198. package/src/lib/ag-ui-adapter.ts +0 -412
  199. package/src/lib/app-store.ts +0 -361
  200. package/src/lib/attachment-adapter.ts +0 -92
  201. package/src/lib/auth-provider.tsx +0 -144
  202. package/src/lib/chat-bubble-store.ts +0 -46
  203. package/src/lib/client-tools.ts +0 -293
  204. package/src/lib/composer-submit-store.ts +0 -32
  205. package/src/lib/dev-store.ts +0 -69
  206. package/src/lib/nav-config.ts +0 -329
  207. package/src/lib/nav-dnd.ts +0 -14
  208. package/src/lib/nav-store.ts +0 -89
  209. package/src/lib/selection-context.ts +0 -65
  210. package/src/lib/sidebar-store.ts +0 -113
  211. package/src/lib/theme-runtime.ts +0 -660
  212. package/src/lib/theme-store.ts +0 -67
  213. package/src/lib/tool-panel-store.ts +0 -121
  214. package/src/lib/use-new-conversation.test.tsx +0 -168
  215. package/src/lib/use-new-conversation.ts +0 -64
  216. package/src/ui/dialog.tsx +0 -109
  217. package/src/ui/dropdown-menu.tsx +0 -75
  218. package/src/ui/popover.tsx +0 -34
  219. package/src/ui/select.tsx +0 -15
  220. package/src/ui/tooltip.tsx +0 -30
  221. package/src/vite-env.d.ts +0 -1
  222. package/tsconfig.json +0 -16
  223. package/vitest.config.ts +0 -8
package/CHANGELOG.md DELETED
@@ -1,203 +0,0 @@
1
- # @iloveagents/foundry-web-ui
2
-
3
- ## 0.3.0
4
-
5
- ### Patch Changes
6
-
7
- - @iloveagents/foundry-agent@0.3.0
8
- - @iloveagents/foundry-web-primitives@0.3.0
9
-
10
- ## 0.2.2
11
-
12
- ### Patch Changes
13
-
14
- - 395f0cd: web-ui: popover New Thread didn't actually start a new thread
15
-
16
- When the user clicks the compose / SquarePen "New Thread" button in
17
- the floating chat popover (`ChatBubble` on workspace/entity pages with
18
- `navigateToChat: false`), the runtime was supposed to discard the
19
- current thread and start fresh. In practice the popover header
20
- subtitle flipped to "New conversation" but the message list stayed
21
- populated with the prior chat's messages — the button looked like a
22
- no-op.
23
-
24
- Root cause: `useNewConversation` bumped the AGUIRuntime inner key to
25
- remount, but the parent `RuntimeBody`'s `effectiveThreadId`
26
- (`urlMatch ?? sticky ?? freshIdRef.current`) stayed pinned to the
27
- sticky active-chat id. The history adapter on the fresh inner mount
28
- immediately loaded the same chat's messages from the server →
29
- nothing visibly changed.
30
-
31
- The sidebar "New Thread" button avoided this only by side-effect —
32
- it navigates to `/`, which triggers `useTrackActiveChatFromUrl` to
33
- clear sticky. The popover preserves the user's workspace context and
34
- doesn't navigate, so sticky never cleared.
35
-
36
- Fix: add a tiny DI store `useChatLifecycleStore` exposing an
37
- `onNewConversation` callback. `useNewConversation` invokes it BEFORE
38
- `resetThread` so the host (spaces-web-ui in lastspace) can clear its
39
- sticky id; the next render's `effectiveThreadId` has already advanced
40
- to a freshly-minted UUID, and the history adapter binds to that
41
- instead of re-hydrating the prior chat.
42
-
43
- Customers that don't track sticky ids leave `onNewConversation`
44
- unset; the call site no-ops gracefully.
45
-
46
- - 1ba01ef: web-ui(sidebar): align group-header labels (CHATS, WORKSPACES, ADMIN, …)
47
- with the leftmost edge of their item labels
48
-
49
- Previously the collapsible group header used `px-3` on the outer wrapper
50
- and a `size-6` chevron, while item rows used `pl-5` (depth-0 indent) +
51
- `size-3.5` icon + `gap-2`. Result: group labels landed at ~36px from the
52
- sidebar edge while item labels landed at 42px — group labels drifted
53
- right of the children they introduced and the visual rhythm broke.
54
-
55
- Make the header's leading column a shared `SIDEBAR_ITEM_ICON_CLASS` +
56
- `SIDEBAR_ITEM_GAP_CLASS` cell — the same constants item icons use — so
57
- every group label (collapsible, link-style, or plain) lands at the same
58
- x-coordinate as item labels. The chevron now sits in the icon column;
59
- its hit-area is preserved at 28px via `-m-2 p-2` (negative-margin
60
- absorbs the extra padding so the visual cell stays 14px and label
61
- geometry doesn't shift).
62
-
63
- Non-collapsible header branches get an explicit invisible spacer of the
64
- same width so their labels align too.
65
-
66
- - 8184a8c: shell: URL is authoritative for the runtime threadId — fixes
67
- resume-creates-new-conversation race
68
-
69
- `ChatConversationAwareRuntime` previously computed
70
- `effectiveThreadId = sticky ?? urlMatch ?? freshIdRef.current`.
71
- When the user navigated from one chat to another via a Recents
72
- click, the synchronous render that followed the URL change had:
73
- - `urlMatch = B` (read from the now-updated `useLocation`)
74
- - `sticky = A` (the active-chat-store hadn't been updated by
75
- `useTrackActiveChatFromUrl`'s `useEffect` yet — effects run
76
- AFTER the commit phase)
77
-
78
- `sticky ?? urlMatch` picked `A` (the previous chat). The AG-UI
79
- adapter was constructed with `threadId = A`. The first message the
80
- user sent went to `/api/agent` with `thread_id = A`, the middleware
81
- created a new row keyed by `A`... wait actually no, here's what
82
- happens — the runtime mints a fresh runner UUID when given a
83
- mismatched threadId; that fresh UUID became a new conversation row,
84
- appearing as "Untitled chat" at the top of the sidebar while the
85
- URL still said `/chat/B`. Two active-looking dots: one for `B`
86
- (NavLink URL match), one for the new row (statusDot from the
87
- updated active-chat-store after the lazy ensure ran).
88
-
89
- Swap the priority: `urlMatch ?? sticky ?? freshIdRef.current`. The
90
- URL is authoritative whenever it's set (i.e. on `/chat/<id>`
91
- routes), eliminating the stale-sticky race entirely. `sticky` is
92
- still consulted as the second-priority fallback for non-chat
93
- routes (`/spaces`, `/tasks`) so the popout chat stays "live" while
94
- the user browses workspaces — that's the original purpose of
95
- `useStickyConversationId` and it's preserved.
96
-
97
- - Updated dependencies [8184a8c]
98
- - @iloveagents/foundry-agent@0.2.2
99
- - @iloveagents/foundry-web-primitives@0.2.2
100
-
101
- ## 0.2.1
102
-
103
- ### Patch Changes
104
-
105
- - No source changes. Bumped in lock-step with the fixed group so the
106
- whole group can publish at a version number that's free across all
107
- four packages. (`ui@0.2.0` already published cleanly; this is a
108
- follow-the-group patch bump.)
109
-
110
- ## 0.2.0
111
-
112
- ### Minor Changes
113
-
114
- - UI: collapsible nav groups in `sidebar.tsx` with persisted state via
115
- `useSyncExternalStore` + localStorage, plus stable group ordering via
116
- new `priority` field in `nav-config.ts`.
117
-
118
- `ag-ui-runtime-provider.tsx` accepts an external `threadId` and
119
- `historyAdapterFactory` so the consuming app can wire its own
120
- persistence (e.g. `ThreadHistoryAdapter` → REST `/api/conversations`).
121
-
122
- ### Patch Changes
123
-
124
- - Move `@iloveagents/foundry-agent`, `-web-primitives` from
125
- `peerDependencies` to `dependencies`. They always ship together as a
126
- coordinated fixed group from this monorepo — they were never
127
- independently-versioned peers. Declaring them as peerDeps caused
128
- changesets' `shouldBumpMajor` cascade to promote the entire group to
129
- a major version on every minor changeset. See the root `foundry-agent`
130
- CHANGELOG entry for details.
131
-
132
- - Updated dependencies
133
- - @iloveagents/foundry-agent@0.2.0
134
- - @iloveagents/foundry-web-primitives@0.2.0
135
-
136
- ### NOTE: 1.0.1 was accidental
137
-
138
- Version 1.0.1 was published briefly on 2026-05-27 due to the peerDep
139
- cascade bug described above (1.0.0 was reserved but not published —
140
- 1.0.1 was the retry). It has been unpublished. Both 1.0.0 and 1.0.1
141
- are now permanently reserved on npm and CANNOT be re-published. Do
142
- not depend on 1.0.x.
143
-
144
- ## 0.1.5
145
-
146
- ### Patch Changes
147
-
148
- - f9144ca: Add `NavItem.statusIcon` slot for sidebar warning / status hints.
149
-
150
- Renders a small tone-coloured icon next to the existing `statusDot`. Use for "this item needs attention" annotations (failed background jobs, permission drift, etc.) where a coloured dot isn't legible enough. Tones map to `warning` / `danger` / `info` / `neutral`. The `label` lands on both `aria-label` and the wrapping span's `title` attribute so screen-reader announcement + hover tooltip work without consumer wiring.
151
- - @iloveagents/foundry-agent@0.1.5
152
- - @iloveagents/foundry-web-primitives@0.1.5
153
-
154
- ## 0.1.4
155
-
156
- ### Patch Changes
157
-
158
- - Updated dependencies [07ace7e]
159
- - @iloveagents/foundry-agent@0.1.4
160
- - @iloveagents/foundry-web-primitives@0.1.4
161
-
162
- ## 0.1.3
163
-
164
- ### Patch Changes
165
-
166
- - Updated dependencies [d9b0460]
167
- - @iloveagents/foundry-agent@0.1.3
168
- - @iloveagents/foundry-web-primitives@0.1.3
169
-
170
- ## 0.1.2
171
-
172
- ### Patch Changes
173
-
174
- - Updated dependencies [30a4346]
175
- - @iloveagents/foundry-agent@0.1.2
176
- - @iloveagents/foundry-web-primitives@0.1.2
177
-
178
- ## 0.1.1
179
-
180
- ### Patch Changes
181
-
182
- - 689d3e9: feat(sidebar): distinct file-drop affordance for nav containers
183
-
184
- `useNavItemDnd` now exposes `isFileDragOver` separately from `isDragOver`
185
- so consuming nav items can render a prominent file-drop visual (dashed
186
- primary outline + soft primary background + Upload icon) when the user
187
- is dragging native files over the container. Previously file drags
188
- shared the same subtle ring as entity-move drags, so users couldn't
189
- tell that a folder accepted external files. Applies to all five nav-
190
- item shapes (action-row leaf, button leaf, `NestedFolderItem`,
191
- `CollapsibleNavItem` with children/actions, `NavLink` fallthrough) and
192
- updates `ContainerDropZone` for visual parity.
193
-
194
- Also fixes a related regression: the capture-phase
195
- `onDragEnterCapture` / `onDragOverCapture` handlers were calling
196
- `e.stopPropagation()`, which short-circuits React's synthetic dispatch
197
- and prevented the bubble-phase `onDragEnter` (where state actually
198
- mutates) from running. Removed — `preventDefault()` alone is enough to
199
- mark the element as droppable.
200
-
201
- - Updated dependencies [689d3e9]
202
- - @iloveagents/foundry-agent@0.1.1
203
- - @iloveagents/foundry-web-primitives@0.1.1
package/CLAUDE.md DELETED
@@ -1 +0,0 @@
1
- @AGENTS.md
@@ -1,59 +0,0 @@
1
- /**
2
- * Guard test: @iloveagents/foundry-web-ui must never import from @lastspace/spaces-web-ui.
3
- * This prevents SPACES code from leaking back into the React UI core package.
4
- */
5
- import { describe, expect, it } from "vitest";
6
- import { readFileSync, readdirSync, statSync } from "node:fs";
7
- import { join } from "node:path";
8
-
9
- function findFiles(dir: string, ext: string[]): string[] {
10
- const results: string[] = [];
11
- for (const entry of readdirSync(dir)) {
12
- const full = join(dir, entry);
13
- if (entry === "node_modules" || entry === "__tests__") continue;
14
- if (statSync(full).isDirectory()) {
15
- results.push(...findFiles(full, ext));
16
- } else if (ext.some((e) => full.endsWith(e))) {
17
- results.push(full);
18
- }
19
- }
20
- return results;
21
- }
22
-
23
- describe("@iloveagents/foundry-web-ui import boundaries", () => {
24
- const srcDir = join(import.meta.dirname!, "..");
25
- const files = findFiles(srcDir, [".ts", ".tsx"]);
26
-
27
- it("must not import from @lastspace/spaces-web-ui", () => {
28
- const violations: string[] = [];
29
- for (const file of files) {
30
- const content = readFileSync(file, "utf-8");
31
- if (content.includes("@lastspace/spaces-web-ui")) {
32
- violations.push(file.replace(srcDir + "/", ""));
33
- }
34
- }
35
- expect(violations).toEqual([]);
36
- });
37
-
38
- it("must not import from @content-editor/react (legacy name)", () => {
39
- const violations: string[] = [];
40
- for (const file of files) {
41
- const content = readFileSync(file, "utf-8");
42
- if (content.includes("@content-editor/react")) {
43
- violations.push(file.replace(srcDir + "/", ""));
44
- }
45
- }
46
- expect(violations).toEqual([]);
47
- });
48
-
49
- it("must not use @/ path aliases (uses relative imports)", () => {
50
- const violations: string[] = [];
51
- for (const file of files) {
52
- const content = readFileSync(file, "utf-8");
53
- if (/from ["']@\//.test(content)) {
54
- violations.push(file.replace(srcDir + "/", ""));
55
- }
56
- }
57
- expect(violations).toEqual([]);
58
- });
59
- });
@@ -1,307 +0,0 @@
1
- /**
2
- * Open-core packaging guards.
3
- *
4
- * Three data-driven guards (the customer-isolation + Python guards from the
5
- * lastspace-origin version were dropped — foundry-ui is TS-only, no
6
- * customer-specific code lives here):
7
- *
8
- * 1. Banned imports inside `packages/agent/src/` — no React, no DOM, no
9
- * `@assistant-ui/*`, no bare `"zustand"`. The MSAL subpath is the
10
- * single exception that may import `@azure/msal-browser`.
11
- * 2. Open-core import-direction graph — each `@iloveagents/foundry-*`
12
- * package may only import from a declared subset of siblings.
13
- * 3. File-count ceiling on `packages/web-shell/src` — the shell stays
14
- * thin (composition layer, not application surface).
15
- * 4. Version-pin parity — shared deps in `pnpm-workspace.yaml` overrides
16
- * stay in sync across workspace `package.json` files.
17
- */
18
- import { describe, expect, it } from "vitest";
19
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
20
- import { join, relative } from "node:path";
21
-
22
- const ROOT = join(import.meta.dirname!, "../../../..");
23
-
24
- // ----------------------------------------------------------------------------
25
- // Configs
26
- // ----------------------------------------------------------------------------
27
-
28
- /**
29
- * Open-core package import graph. For each package, list which `@iloveagents/*`
30
- * packages it MAY import. Empty array = none.
31
- *
32
- * Packages that do not yet exist on disk are skipped at runtime by
33
- * `findPackageDir` so future packages can be wired in here ahead of their
34
- * extraction.
35
- */
36
- const OPEN_CORE_GRAPH: Record<string, string[]> = {
37
- "@iloveagents/foundry-agent": [],
38
- "@iloveagents/foundry-web-primitives": [],
39
- "@iloveagents/foundry-web-ui": [
40
- "@iloveagents/foundry-web-primitives",
41
- "@iloveagents/foundry-agent",
42
- ],
43
- "@iloveagents/foundry-web-shell": [
44
- "@iloveagents/foundry-web-primitives",
45
- "@iloveagents/foundry-web-ui",
46
- "@iloveagents/foundry-agent",
47
- ],
48
- };
49
-
50
- /**
51
- * Banned bare-package imports inside `packages/agent/src/`.
52
- *
53
- * `@iloveagents/foundry-agent` is the cross-runtime transport — pure TS, zero React,
54
- * zero `@assistant-ui/*`. The MSAL subpath (`packages/agent/src/msal/`) may
55
- * pull in `@azure/msal-browser` (dynamic import keeps it optional). Bare
56
- * `"zustand"` is banned everywhere — must use `"zustand/vanilla"`.
57
- */
58
- const AGENT_BANNED_IMPORTS = [
59
- { specifier: "react", scope: "all" as const },
60
- { specifier: "react-dom", scope: "all" as const },
61
- { specifier: "react/jsx-runtime", scope: "all" as const },
62
- { specifierPrefix: "@assistant-ui/", scope: "all" as const },
63
- { specifier: "@azure/msal-react", scope: "all" as const },
64
- // bare "zustand" is the React entry — agent must use "zustand/vanilla".
65
- { specifierExact: "zustand", scope: "all" as const },
66
- // msal-browser is allowed only under src/msal/.
67
- { specifier: "@azure/msal-browser", scope: "non-msal" as const },
68
- ];
69
-
70
- /** File-count ceiling: `packages/web-shell/src` is hard-capped at 15 — the
71
- * shell is a thin composition layer (bootstrapShell + ChatModule wiring +
72
- * auth provider seam), not application surface. Growth indicates leakage.
73
- */
74
- const FILE_COUNT_CEILINGS = [
75
- { path: "packages/web-shell/src", ceiling: 15, hardFail: true },
76
- ];
77
-
78
- /**
79
- * Shared deps where all consumers must agree on the major version.
80
- * Rooted in `pnpm-workspace.yaml` overrides — once a customer's package.json
81
- * declares an incompatible major, pnpm overrides hide the drift at install
82
- * time, but the public package metadata is wrong.
83
- */
84
- const SHARED_DEPS_REQUIRED_MAJOR = [
85
- { name: "react-router", expectedMajor: "7" },
86
- { name: "@assistant-ui/react", expectedMajor: "0" },
87
- { name: "@ag-ui/client", expectedMajor: "0" },
88
- { name: "@ag-ui/core", expectedMajor: "0" },
89
- ];
90
-
91
- // ----------------------------------------------------------------------------
92
- // Helpers
93
- // ----------------------------------------------------------------------------
94
-
95
- function findFiles(dir: string, exts: string[], skip: Set<string>): string[] {
96
- if (!existsSync(dir)) return [];
97
- const out: string[] = [];
98
- for (const entry of readdirSync(dir)) {
99
- if (skip.has(entry)) continue;
100
- const full = join(dir, entry);
101
- const stat = statSync(full);
102
- if (stat.isDirectory()) out.push(...findFiles(full, exts, skip));
103
- else if (exts.some((e) => full.endsWith(e))) out.push(full);
104
- }
105
- return out;
106
- }
107
-
108
- function findPackageDir(name: string): string | null {
109
- const pkgDir = join(ROOT, "packages");
110
- if (!existsSync(pkgDir)) return null;
111
- for (const entry of readdirSync(pkgDir)) {
112
- const candidate = join(pkgDir, entry);
113
- const pkgJsonPath = join(candidate, "package.json");
114
- if (existsSync(pkgJsonPath)) {
115
- try {
116
- const json = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
117
- if (json.name === name) return candidate;
118
- } catch {
119
- /* ignore malformed */
120
- }
121
- }
122
- }
123
- return null;
124
- }
125
-
126
- function importsOf(content: string): string[] {
127
- const out: string[] = [];
128
- // ES imports + dynamic imports + re-exports
129
- const re = /(?:import|export|require)\s*[^"';]*?["']([^"']+)["']/g;
130
- let m: RegExpExecArray | null;
131
- while ((m = re.exec(content)) !== null) out.push(m[1]);
132
- return out;
133
- }
134
-
135
- // ----------------------------------------------------------------------------
136
- // Tests
137
- // ----------------------------------------------------------------------------
138
-
139
- describe("open-core guards", () => {
140
- // 1. Agent package: no React / no assistant-ui / no bare zustand
141
- describe("@iloveagents/foundry-agent banned-import guard", () => {
142
- const agentRoot = findPackageDir("@iloveagents/foundry-agent");
143
-
144
- if (!agentRoot) {
145
- it.skip("agent package not yet extracted", () => undefined);
146
- } else {
147
- const files = findFiles(
148
- join(agentRoot, "src"),
149
- [".ts", ".tsx"],
150
- new Set(["node_modules", "dist", "__tests__"]),
151
- );
152
-
153
- function isUnderMsal(filePath: string): boolean {
154
- const rel = relative(agentRoot!, filePath);
155
- return rel.startsWith(`src/msal/`) || rel.startsWith(`src${"/"}msal${"/"}`);
156
- }
157
-
158
- for (const rule of AGENT_BANNED_IMPORTS) {
159
- const label =
160
- (rule as { specifier?: string }).specifier ??
161
- (rule as { specifierPrefix?: string }).specifierPrefix ??
162
- (rule as { specifierExact?: string }).specifierExact!;
163
-
164
- it(`no source file imports "${label}" (scope: ${rule.scope})`, () => {
165
- const violations: string[] = [];
166
- for (const file of files) {
167
- if (rule.scope === "non-msal" && isUnderMsal(file)) continue;
168
- const content = readFileSync(file, "utf-8");
169
- for (const imp of importsOf(content)) {
170
- const matches =
171
- ("specifierExact" in rule && imp === rule.specifierExact) ||
172
- ("specifier" in rule && imp === rule.specifier) ||
173
- ("specifierPrefix" in rule &&
174
- rule.specifierPrefix !== undefined &&
175
- imp.startsWith(rule.specifierPrefix));
176
- if (matches) violations.push(`${relative(ROOT, file)} -> ${imp}`);
177
- }
178
- }
179
- expect(
180
- violations,
181
- `${label} import leaked into packages/agent/src/${
182
- rule.scope === "non-msal" ? " (outside msal/)" : ""
183
- }`,
184
- ).toEqual([]);
185
- });
186
- }
187
- }
188
- });
189
-
190
- // 2. Open-core import graph
191
- describe("open-core package import graph", () => {
192
- for (const [pkg, allowed] of Object.entries(OPEN_CORE_GRAPH)) {
193
- it(`${pkg} only imports from [${allowed.join(", ") || "none"}]`, () => {
194
- const dir = findPackageDir(pkg);
195
- if (!dir) return; // package not yet extracted — skip
196
- const files = findFiles(
197
- join(dir, "src"),
198
- [".ts", ".tsx"],
199
- new Set(["node_modules", "dist", "__tests__"]),
200
- );
201
- const violations: string[] = [];
202
- for (const file of files) {
203
- const content = readFileSync(file, "utf-8");
204
- for (const imp of importsOf(content)) {
205
- // Foundry-ui packages must never import from @lastspace/* (commercial
206
- // downstream); allow-listing is enforced for sibling @iloveagents/* deps.
207
- if (imp.startsWith("@lastspace/")) {
208
- violations.push(`${relative(ROOT, file)} -> ${imp} (lastspace import forbidden)`);
209
- continue;
210
- }
211
- if (!imp.startsWith("@iloveagents/")) continue;
212
- // Self-imports allowed (subpath exports etc).
213
- if (imp === pkg || imp.startsWith(`${pkg}/`)) continue;
214
- // Strip subpath off the imported package.
215
- const importedPkg = imp.split("/").slice(0, 2).join("/");
216
- if (!allowed.includes(importedPkg)) {
217
- violations.push(`${relative(ROOT, file)} -> ${imp}`);
218
- }
219
- }
220
- }
221
- expect(violations, `${pkg} imported a forbidden package`).toEqual([]);
222
- });
223
- }
224
- });
225
-
226
- // 3. File-count ceiling (warning + optional hard-fail)
227
- describe("file-count ceilings on host directories", () => {
228
- for (const { path, ceiling, hardFail } of FILE_COUNT_CEILINGS) {
229
- it(`${path} ${hardFail ? "must" : "should"} stay ≤ ${ceiling} files`, () => {
230
- const dir = join(ROOT, path);
231
- if (!existsSync(dir)) return;
232
- const files = findFiles(
233
- dir,
234
- [".ts", ".tsx", ".py"],
235
- new Set(["node_modules", ".venv", "__pycache__", "dist", "__tests__"]),
236
- );
237
- if (hardFail) {
238
- expect(
239
- files.length,
240
- `${path} has ${files.length} files; ceiling is ${ceiling}. Move features into packages.`,
241
- ).toBeLessThanOrEqual(ceiling);
242
- } else {
243
- // Warning-mode: log if over ceiling but don't fail. Flip to hardFail
244
- // once host-shell extraction (#98) lands.
245
- if (files.length > ceiling) {
246
- // eslint-disable-next-line no-console
247
- console.warn(
248
- `[file-count guard] ${path} has ${files.length} files (ceiling ${ceiling}). Hard-fail enabled once #98 lands.`,
249
- );
250
- }
251
- expect(files.length).toBeGreaterThanOrEqual(0); // always passes; just records
252
- }
253
- });
254
- }
255
- });
256
-
257
- // 4. Version-pin parity
258
- describe("shared deps stay in sync across workspace package.jsons", () => {
259
- function workspacePackageJsons(): { dir: string; json: Record<string, unknown> }[] {
260
- const out: { dir: string; json: Record<string, unknown> }[] = [];
261
- for (const base of ["packages"]) {
262
- const baseDir = join(ROOT, base);
263
- if (!existsSync(baseDir)) continue;
264
- for (const entry of readdirSync(baseDir)) {
265
- const dir = join(baseDir, entry);
266
- const pkgJson = join(dir, "package.json");
267
- if (existsSync(pkgJson)) {
268
- try {
269
- out.push({ dir, json: JSON.parse(readFileSync(pkgJson, "utf-8")) });
270
- } catch {
271
- /* ignore */
272
- }
273
- }
274
- }
275
- }
276
- return out;
277
- }
278
-
279
- function depMajor(spec: string): string | null {
280
- // Strip leading ^, ~, >=, =, etc.
281
- const m = spec.match(/(\d+)/);
282
- return m ? m[1] : null;
283
- }
284
-
285
- for (const { name, expectedMajor } of SHARED_DEPS_REQUIRED_MAJOR) {
286
- it(`${name} declared at major ${expectedMajor} in every package.json that declares it`, () => {
287
- const violations: string[] = [];
288
- for (const { dir, json } of workspacePackageJsons()) {
289
- const buckets = ["dependencies", "peerDependencies", "devDependencies"] as const;
290
- for (const bucket of buckets) {
291
- const deps = (json[bucket] ?? {}) as Record<string, string>;
292
- const spec = deps[name];
293
- if (!spec) continue;
294
- const major = depMajor(spec);
295
- if (major !== expectedMajor) {
296
- violations.push(`${relative(ROOT, dir)} ${bucket}.${name}=${spec}`);
297
- }
298
- }
299
- }
300
- expect(
301
- violations,
302
- `${name} version drift detected; pnpm-workspace.yaml override hides it at install time but published metadata is wrong`,
303
- ).toEqual([]);
304
- });
305
- }
306
- });
307
- });
@@ -1,81 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- getThemePreset,
4
- mergeThemeDefinitions,
5
- resolveThemeRuntime,
6
- } from "../lib/theme-runtime.ts";
7
-
8
- describe("theme runtime", () => {
9
- it("merges sparse definitions in precedence order", () => {
10
- const merged = mergeThemeDefinitions(
11
- {
12
- light: {
13
- primary: "oklch(0.4 0.2 280)",
14
- foreground: "oklch(0.2 0.01 280)",
15
- },
16
- },
17
- {
18
- light: {
19
- primary: "oklch(0.5 0.19 260)",
20
- },
21
- status: {
22
- light: {
23
- success: "oklch(0.6 0.15 150)",
24
- },
25
- },
26
- },
27
- );
28
-
29
- expect(merged.light?.primary).toBe("oklch(0.5 0.19 260)");
30
- expect(merged.light?.foreground).toBe("oklch(0.2 0.01 280)");
31
- expect(merged.status?.light?.success).toBe("oklch(0.6 0.15 150)");
32
- });
33
-
34
- it("resolves scoped layers on top of the preset", () => {
35
- const runtime = resolveThemeRuntime(
36
- [
37
- {
38
- id: "global-theme",
39
- basePreset: "foundry-violet",
40
- definition: {
41
- light: {
42
- primary: "oklch(0.45 0.18 300)",
43
- },
44
- sidebar: {
45
- light: {
46
- sidebar: "oklch(0.98 0.01 300)",
47
- },
48
- },
49
- },
50
- },
51
- {
52
- id: "workspace-theme",
53
- definition: {
54
- light: {
55
- primary: "oklch(0.52 0.16 240)",
56
- },
57
- },
58
- },
59
- ],
60
- "light",
61
- );
62
-
63
- expect(runtime.mode).toBe("light");
64
- expect(runtime.sources.map((layer) => layer.id)).toEqual([
65
- "global-theme",
66
- "workspace-theme",
67
- ]);
68
- expect(runtime.definition.light?.primary).toBe("oklch(0.52 0.16 240)");
69
- expect(runtime.variables["--primary" as keyof typeof runtime.variables]).toBe(
70
- "oklch(0.52 0.16 240)",
71
- );
72
- expect(runtime.variables["--sidebar" as keyof typeof runtime.variables]).toBe(
73
- "oklch(0.98 0.01 300)",
74
- );
75
- expect(runtime.variables["--success" as keyof typeof runtime.variables]).toBeTruthy();
76
- });
77
-
78
- it("falls back to the default preset for unknown names", () => {
79
- expect(getThemePreset("unknown-preset")).toEqual(getThemePreset("foundry-violet"));
80
- });
81
- });