@iloveagents/foundry-web-shell 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.
@@ -0,0 +1,174 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import type { ThemeLayer } from "@iloveagents/foundry-web-ui";
3
+ /**
4
+ * Per-mount inputs the shell hands the module's history-adapter
5
+ * factory. Either ``urlMatch`` (resumed chat — the user clicked a
6
+ * Recents entry) or ``aguiThreadId`` alone (fresh chat — assistant-ui
7
+ * just minted a UUID) drives the canonical conversation id, depending
8
+ * on what the module's backend treats as authoritative.
9
+ */
10
+ export interface ChatConversationFactoryArgs {
11
+ /**
12
+ * The conversation id captured from the URL via
13
+ * :attr:`ChatConversationConfig.pathPattern`. Set when the user
14
+ * resumed a known chat; ``undefined`` on a fresh thread.
15
+ */
16
+ urlMatch?: string;
17
+ /**
18
+ * The assistant-ui adapter's stable thread id (a client-minted
19
+ * UUID, unique per "New Thread" click). Always set. Use this as the
20
+ * key for a lazy ``create_or_get`` when ``urlMatch`` is missing so
21
+ * fresh chats also persist.
22
+ */
23
+ aguiThreadId: string;
24
+ }
25
+ /**
26
+ * URL-driven chat persistence wiring. Modules declare this so the
27
+ * shell can stand up an assistant-ui :type:`ThreadHistoryAdapter` on
28
+ * every mount of :type:`AGUIRuntimeProvider` — both resumed chats
29
+ * (matched URL) and fresh chats.
30
+ *
31
+ * The shell stays free of feature-specific knowledge: it captures
32
+ * the URL match (if any) and the assistant-ui-minted thread id, and
33
+ * hands both to :attr:`buildHistoryAdapter`. The module owns the
34
+ * actual backend API shape and "fresh chat vs. resume" semantics.
35
+ */
36
+ export interface ChatConversationConfig {
37
+ /**
38
+ * Regex with exactly one capture group that yields the conversation
39
+ * id from ``window.location.pathname``. Example:
40
+ * ``/^\/chat\/([^/]+)$/``.
41
+ */
42
+ pathPattern: RegExp;
43
+ /**
44
+ * Optional hook giving the module's view of "what conversation is
45
+ * the user currently engaged with" — separate from the URL.
46
+ *
47
+ * Why this exists: ChatGPT-style UX wants the chat runtime to stay
48
+ * alive while the user browses Workspaces, Jobs, etc. If we drove
49
+ * the runtime's ``threadId`` from the URL only, every non-chat
50
+ * navigation would remount the runtime and wipe in-flight messages.
51
+ *
52
+ * When provided, the shell prefers this value over the URL match
53
+ * for the ``AGUIRuntimeProvider.threadId`` prop. The module is
54
+ * expected to update its sticky state on URL transitions itself
55
+ * (e.g. via :type:`useTrackActiveChatFromUrl` in Spaces).
56
+ *
57
+ * Return ``null`` when the user has no active chat (initial app
58
+ * load, or just clicked "New Thread"). The shell then lets the
59
+ * AG-UI adapter mint a fresh UUID, same as before this hook.
60
+ */
61
+ useStickyConversationId?: () => string | null;
62
+ /**
63
+ * Build an assistant-ui :type:`ThreadHistoryAdapter` for the
64
+ * current mount. Called inside the runtime provider once the
65
+ * AG-UI adapter has minted (or accepted) its thread id, so both
66
+ * ``urlMatch`` and ``aguiThreadId`` are available.
67
+ *
68
+ * The returned adapter is wired into ``useLocalRuntime``'s
69
+ * ``adapters.history`` slot — assistant-ui calls ``load()`` on
70
+ * mount and ``append()`` after every completed turn.
71
+ *
72
+ * Modules typically:
73
+ * - On resume (``urlMatch`` set): use it directly as the
74
+ * conversation id for load/append.
75
+ * - On fresh (``urlMatch`` undefined): lazily ``create_or_get``
76
+ * a server row keyed by ``aguiThreadId``, cache the resulting
77
+ * conversation id, then load/append against that.
78
+ */
79
+ buildHistoryAdapter: (args: ChatConversationFactoryArgs) => import("@assistant-ui/react").ThreadHistoryAdapter;
80
+ }
81
+ /** Route entry contributed by a module or the host app. */
82
+ export interface ShellPage {
83
+ path: string;
84
+ element: ReactNode;
85
+ }
86
+ /**
87
+ * A ChatModule is the unit of composition for `bootstrapShell`. Each field is
88
+ * optional — modules contribute what they need:
89
+ *
90
+ * - `useInit`: React hook body called once per layout render (rules-of-hooks
91
+ * apply). Use for cross-store sync, registry registration, etc.
92
+ * Modules wanting to mutate `useNavStore` config (sidebar nav items)
93
+ * do it inside `useInit` — see Spaces' `useSpacesNavSync()` for the
94
+ * canonical pattern.
95
+ * - `toolUIs`: ReactNode rendered inside `<AGUIRuntimeProvider>`. Use to
96
+ * register `makeAssistantToolUI` instances.
97
+ * - `layoutExtras`: ReactNode rendered inside the shell layout (banners,
98
+ * global dialogs, popovers).
99
+ * - `wrappers`: ComponentType<{children}>[] applied outer→inner from the
100
+ * array. Wrap providers like SpacesQueryProvider here.
101
+ * - `useThemeLayers`: hook returning ThemeLayer[]; merged on top of the
102
+ * static `theme` prop in module-array order.
103
+ * - `pages`: ShellPage[] appended to Routes; customer-supplied `pages` win
104
+ * on path collision.
105
+ * - `fetchInterceptor`: zero-arg installer called once before `createRoot`.
106
+ * Use to install `window.fetch` wrappers.
107
+ */
108
+ export interface ChatModule {
109
+ name: string;
110
+ useInit?: () => void;
111
+ toolUIs?: ReactNode;
112
+ layoutExtras?: ReactNode;
113
+ wrappers?: ComponentType<{
114
+ children: ReactNode;
115
+ }>[];
116
+ useThemeLayers?: () => ThemeLayer[];
117
+ pages?: ShellPage[];
118
+ fetchInterceptor?: () => void;
119
+ /**
120
+ * URL-driven chat persistence.
121
+ *
122
+ * Selection order (see ``ChatConversationAwareRuntime`` in
123
+ * ``shell-app.tsx``):
124
+ *
125
+ * 1. URL match. If a module's ``pathPattern`` matches the current
126
+ * pathname and captures a non-empty group, that module's config
127
+ * wins and its captured id is fed to the history adapter as
128
+ * ``urlMatch``.
129
+ * 2. Fallback. If nothing matches the URL, the shell falls back to
130
+ * the FIRST registered config (``configs[0]``) so a fresh-chat
131
+ * runtime still gets a history adapter and the chat persists
132
+ * from the very first message. ``urlMatch`` is ``undefined``
133
+ * in this case.
134
+ * 3. None. With zero configs registered, the runtime stays in
135
+ * legacy in-memory mode (no persistence).
136
+ *
137
+ * Modules without a config don't participate in selection.
138
+ */
139
+ chatConversation?: ChatConversationConfig;
140
+ }
141
+ /**
142
+ * Adapter for plugging in an auth Provider. The Provider renders children
143
+ * once authenticated and is responsible for resolving its own config
144
+ * (the default reads MSAL settings from `import.meta.env`). Tests and
145
+ * non-MSAL deployments override with a different Provider.
146
+ */
147
+ export interface AuthAdapter {
148
+ Provider: ComponentType<{
149
+ children: ReactNode;
150
+ }>;
151
+ }
152
+ export interface BootstrapShellOptions {
153
+ modules?: ChatModule[];
154
+ /** Customer-supplied pages — win over module pages on `path` collision. */
155
+ pages?: ShellPage[];
156
+ /** Static base theme layers; modules' useThemeLayers stack on top. */
157
+ theme?: ThemeLayer[];
158
+ /** Override default MSAL-backed auth provider (e.g. for tests). */
159
+ authProvider?: AuthAdapter;
160
+ /**
161
+ * Override the agent transport used by the AG-UI runtime.
162
+ *
163
+ * Defaults to the MSAL-backed `defaultAgentFetch`. Apps can pass a custom
164
+ * fetch to point at a local demo agent, a mock AG-UI transport, or a
165
+ * production backend without patching global `window.fetch`.
166
+ */
167
+ agentFetch?: typeof fetch;
168
+ /** Wrap the tree in <StrictMode>. Default: true. */
169
+ strictMode?: boolean;
170
+ /** DOM element to mount into. Default: document.getElementById("root"). */
171
+ rootElement?: HTMLElement;
172
+ }
173
+ /** Pass-through helper that adds nothing at runtime — pure type marker. */
174
+ export declare function defineChatModule(m: ChatModule): ChatModule;
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /** Pass-through helper that adds nothing at runtime — pure type marker. */
2
+ export function defineChatModule(m) {
3
+ return m;
4
+ }
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-shell",
3
- "version": "0.3.0",
4
- "license": "SEE LICENSE IN LICENSE",
3
+ "version": "0.4.0",
4
+ "license": "MIT",
5
5
  "type": "module",
6
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.ts"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
9
13
  },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
10
19
  "publishConfig": {
11
20
  "access": "public"
12
21
  },
@@ -19,20 +28,27 @@
19
28
  "zustand": "^5.0.0"
20
29
  },
21
30
  "dependencies": {
22
- "@iloveagents/foundry-agent": "0.3.0",
23
- "@iloveagents/foundry-web-primitives": "0.3.0",
24
- "@iloveagents/foundry-web-ui": "0.3.0"
31
+ "@iloveagents/foundry-agent": "^0.4.0",
32
+ "@iloveagents/foundry-web-primitives": "^0.4.0",
33
+ "@iloveagents/foundry-web-ui": "^0.4.0"
25
34
  },
26
35
  "devDependencies": {
27
- "typescript": "~5.9.3",
36
+ "@assistant-ui/react": "^0.12.25",
37
+ "@testing-library/react": "^16.0.0",
28
38
  "@types/react": "^19.2.2",
29
39
  "@types/react-dom": "^19.2.2",
40
+ "jsdom": "^28.1.0",
41
+ "lucide-react": ">=0.400.0",
42
+ "react": "^19.0.0",
43
+ "react-dom": "^19.0.0",
44
+ "react-router": "^7.0.0",
45
+ "typescript": "~5.9.3",
30
46
  "vite": "^7.2.2",
31
47
  "vitest": "^4.1.4",
32
- "jsdom": "^28.1.0",
33
- "@testing-library/react": "^16.0.0"
48
+ "zustand": "^5.0.0"
34
49
  },
35
50
  "scripts": {
51
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/fix-dts-extensions.mjs dist",
36
52
  "test:unit": "vitest run",
37
53
  "typecheck": "tsc --noEmit"
38
54
  }
package/AGENTS.md DELETED
@@ -1,82 +0,0 @@
1
- Browser bootstrap for the LastSpace agent UI starter. Published as `@iloveagents/foundry-web-shell`. Composes `@iloveagents/foundry-web-primitives`, `@iloveagents/foundry-agent` (and `@iloveagents/foundry-agent/msal`), and `@iloveagents/foundry-web-ui` into a generic mounting point — customers and modules contribute pages, theme layers, layout extras, tool UIs, and per-request fetch interceptors via the `ChatModule` protocol.
2
-
3
- # Architecture
4
-
5
- Source-shipping (no build step) — same convention as the other `@lastspace/*` packages.
6
-
7
- ```
8
- src/
9
- index.ts ← public barrel (bootstrapShell, ChatModule, defineChatModule, types)
10
- bootstrap-shell.tsx ← entry point: installs fetch interceptors, calls createRoot
11
- shell-app.tsx ← <BrowserRouter> → AuthProvider → [wrappers outer→inner] → AGUIRuntimeProvider → {toolUIs} → Suspense → Routes
12
- shell-layout.tsx ← generic layout (Sidebar + ChatHeader + ToolPanelLayout + ChatBubble + GlobalSelectionPopover + module layoutExtras)
13
- types.ts ← ChatModule, ShellPage, AuthAdapter, BootstrapShellOptions
14
- auth-default.tsx ← default authProvider (wraps @iloveagents/foundry-web-ui's AuthProvider; reads MsalAuthConfig from import.meta.env)
15
- service-fetch-default.ts ← default fetchFn (createServiceFetch + tokenFetch wiring)
16
- ```
17
-
18
- # Public API
19
-
20
- ```ts
21
- import { bootstrapShell, defineChatModule, type ChatModule } from "@iloveagents/foundry-web-shell";
22
- ```
23
-
24
- `bootstrapShell({ modules, pages, theme, authProvider, strictMode })` mounts the SPA. Each `ChatModule` may contribute:
25
-
26
- - `useInit?: () => void` — React hook body called inside the layout (rules-of-hooks apply).
27
- - `toolUIs?: ReactNode` — rendered inside `<AGUIRuntimeProvider>`.
28
- - `layoutExtras?: ReactNode` — rendered inside the layout (banners, dialogs, popovers).
29
- - `wrappers?: ComponentType<{ children }>[]` — applied outer→inner from array.
30
- - `useThemeLayers?: () => ThemeLayer[]` — hook returning theme layers; merged on top of static `theme` prop.
31
- - `pages?: { path; element }[]` — appended to Routes; customer `pages` win on path collision. (Sidebar nav items are mutated via `useNavStore.getState().setConfig(...)` inside `useInit` — see Spaces' `useSpacesNavSync` for the pattern.)
32
- - `fetchInterceptor?: () => void` — installed before `createRoot` (use to install global fetch wrappers).
33
-
34
- # Wrapper ordering
35
-
36
- ```
37
- <StrictMode>
38
- <BrowserRouter>
39
- <AuthProvider>
40
- {wrappers outer→inner from modules}
41
- <AGUIRuntimeProvider>
42
- {toolUIs}
43
- <Suspense>
44
- <Routes>
45
- <Route element={<ShellLayout />}>
46
- {pages — customer wins on path collision}
47
- </Route>
48
- </Routes>
49
- </Suspense>
50
- </AGUIRuntimeProvider>
51
- {/wrappers}
52
- </AuthProvider>
53
- </BrowserRouter>
54
- </StrictMode>
55
- ```
56
-
57
- Asserted by `src/__tests__/wrapper-order.test.tsx`.
58
-
59
- # Import boundary (enforced by guard test)
60
-
61
- `@iloveagents/foundry-web-shell` MAY import: `@iloveagents/foundry-web-primitives`, `@iloveagents/foundry-web-ui`, `@iloveagents/foundry-agent` (+ subpaths). MUST NOT import `@lastspace/spaces-web-ui` — modules are runtime parameters, never compile-time deps. Enforced by `OPEN_CORE_GRAPH` in `packages/web-ui/src/__tests__/open-core-guards.test.ts`.
62
-
63
- `packages/web-shell/src/` MUST stay ≤ 15 files (file-count ceiling, hardFail).
64
-
65
- # Theme
66
-
67
- `bootstrapShell({ theme })` provides static base layers. Modules' `useThemeLayers()` hooks return additional layers stacked on top in module-array order. Final layers feed `<ThemeRuntimeProvider layers={...}>`.
68
-
69
- # Auth
70
-
71
- The default `authProvider` reads `VITE_MSAL_CLIENT_ID`, `VITE_MSAL_AUTHORITY`, `VITE_MSAL_API_SCOPE` from `import.meta.env`. Missing keys render the `AuthConfigError` UI. Customers can override with `authProvider={{ Provider: MyAuthProvider }}` for testing or non-MSAL deployments.
72
-
73
- # Commands
74
-
75
- ```bash
76
- pnpm --filter @iloveagents/foundry-web-shell test:unit # vitest run
77
- pnpm --filter @iloveagents/foundry-web-shell typecheck # tsc --noEmit
78
- ```
79
-
80
- # Authoring a module
81
-
82
- See [`docs/AUTHORING_A_MODULE.md`](../../docs/AUTHORING_A_MODULE.md).
package/CHANGELOG.md DELETED
@@ -1,175 +0,0 @@
1
- # @iloveagents/foundry-web-shell
2
-
3
- ## 0.3.0
4
-
5
- ### Minor Changes
6
-
7
- - df383c9: Add runtime configuration support so the web app can ship as a single prebuilt
8
- container image that boots for any environment without a rebuild. New
9
- `runtimeConfig(key)` helper resolves `window.__APP_CONFIG__[key]` (injected by
10
- the container at startup) before `import.meta.env[key]` (build-time Vite env,
11
- the local-dev fallback). The default MSAL auth adapter and agent fetch client
12
- now read `VITE_MSAL_*` / `VITE_API_BASE_URL` through it. Local `pnpm dev` is
13
- unchanged (no `/config.js` → falls back to `import.meta.env`).
14
-
15
- ### Patch Changes
16
-
17
- - @iloveagents/foundry-agent@0.3.0
18
- - @iloveagents/foundry-web-primitives@0.3.0
19
- - @iloveagents/foundry-web-ui@0.3.0
20
-
21
- ## 0.2.2
22
-
23
- ### Patch Changes
24
-
25
- - 8184a8c: shell: URL is authoritative for the runtime threadId — fixes
26
- resume-creates-new-conversation race
27
-
28
- `ChatConversationAwareRuntime` previously computed
29
- `effectiveThreadId = sticky ?? urlMatch ?? freshIdRef.current`.
30
- When the user navigated from one chat to another via a Recents
31
- click, the synchronous render that followed the URL change had:
32
- - `urlMatch = B` (read from the now-updated `useLocation`)
33
- - `sticky = A` (the active-chat-store hadn't been updated by
34
- `useTrackActiveChatFromUrl`'s `useEffect` yet — effects run
35
- AFTER the commit phase)
36
-
37
- `sticky ?? urlMatch` picked `A` (the previous chat). The AG-UI
38
- adapter was constructed with `threadId = A`. The first message the
39
- user sent went to `/api/agent` with `thread_id = A`, the middleware
40
- created a new row keyed by `A`... wait actually no, here's what
41
- happens — the runtime mints a fresh runner UUID when given a
42
- mismatched threadId; that fresh UUID became a new conversation row,
43
- appearing as "Untitled chat" at the top of the sidebar while the
44
- URL still said `/chat/B`. Two active-looking dots: one for `B`
45
- (NavLink URL match), one for the new row (statusDot from the
46
- updated active-chat-store after the lazy ensure ran).
47
-
48
- Swap the priority: `urlMatch ?? sticky ?? freshIdRef.current`. The
49
- URL is authoritative whenever it's set (i.e. on `/chat/<id>`
50
- routes), eliminating the stale-sticky race entirely. `sticky` is
51
- still consulted as the second-priority fallback for non-chat
52
- routes (`/spaces`, `/tasks`) so the popout chat stays "live" while
53
- the user browses workspaces — that's the original purpose of
54
- `useStickyConversationId` and it's preserved.
55
-
56
- - Updated dependencies [395f0cd]
57
- - Updated dependencies [1ba01ef]
58
- - Updated dependencies [8184a8c]
59
- - @iloveagents/foundry-web-ui@0.2.2
60
- - @iloveagents/foundry-agent@0.2.2
61
- - @iloveagents/foundry-web-primitives@0.2.2
62
-
63
- ## 0.2.1
64
-
65
- ### Patch Changes
66
-
67
- - No source changes. Bumped in lock-step with the fixed group so the
68
- whole group can publish at a version number that's free across all
69
- four packages. (`shell@0.2.0` already published cleanly; this is a
70
- follow-the-group patch bump.)
71
-
72
- ## 0.2.0
73
-
74
- ### Minor Changes
75
-
76
- - shell: ChatConversationConfig API + sticky-runtime threadId
77
-
78
- Lets a downstream module (e.g. `@ltwlf/spaces-web-ui`) attach a
79
- `ChatConversationConfig` to its `ChatModule`. The shell then:
80
- - Pre-mints a fresh thread UUID via `useRef` so the AG-UI runtime's
81
- `threadId` is defined from first render — no `undefined → defined`
82
- remount mid-conversation when the user sends the first message.
83
- - Calls `config.useStickyConversationId()` (when declared) and
84
- feeds the resulting id into the runtime so the chat stays "live"
85
- while the user browses non-chat routes (Workspaces, Jobs, etc.).
86
- - Per-config keyed child components (`StickyAwareRuntime` vs
87
- `UrlOnlyRuntime`) so optional hooks satisfy Rules-of-Hooks across
88
- multi-module config swaps.
89
-
90
- ### Patch Changes
91
-
92
- - Move `@iloveagents/foundry-agent`, `-web-primitives`, `-web-ui` from
93
- `peerDependencies` to `dependencies`. They always ship together as a
94
- coordinated fixed group from this monorepo — they were never
95
- independently-versioned peers. Declaring them as peerDeps caused
96
- changesets' `shouldBumpMajor` cascade to promote the entire group to
97
- a major version on every minor changeset. See the root `foundry-agent`
98
- CHANGELOG entry for details.
99
-
100
- - Updated dependencies
101
- - @iloveagents/foundry-web-ui@0.2.0
102
- - @iloveagents/foundry-agent@0.2.0
103
- - @iloveagents/foundry-web-primitives@0.2.0
104
-
105
- ### NOTE: 1.0.1 was accidental
106
-
107
- Version 1.0.1 was published briefly on 2026-05-27 due to the peerDep
108
- cascade bug described above (1.0.0 was reserved but not published —
109
- 1.0.1 was the retry). It has been unpublished. Both 1.0.0 and 1.0.1
110
- are now permanently reserved on npm and CANNOT be re-published. Do
111
- not depend on 1.0.x.
112
-
113
- ## 0.1.5
114
-
115
- ### Patch Changes
116
-
117
- - Updated dependencies [f9144ca]
118
- - @iloveagents/foundry-web-ui@0.1.5
119
- - @iloveagents/foundry-agent@0.1.5
120
- - @iloveagents/foundry-web-primitives@0.1.5
121
-
122
- ## 0.1.4
123
-
124
- ### Patch Changes
125
-
126
- - Updated dependencies [07ace7e]
127
- - @iloveagents/foundry-agent@0.1.4
128
- - @iloveagents/foundry-web-ui@0.1.4
129
- - @iloveagents/foundry-web-primitives@0.1.4
130
-
131
- ## 0.1.3
132
-
133
- ### Patch Changes
134
-
135
- - Updated dependencies [d9b0460]
136
- - @iloveagents/foundry-agent@0.1.3
137
- - @iloveagents/foundry-web-ui@0.1.3
138
- - @iloveagents/foundry-web-primitives@0.1.3
139
-
140
- ## 0.1.2
141
-
142
- ### Patch Changes
143
-
144
- - Updated dependencies [30a4346]
145
- - @iloveagents/foundry-agent@0.1.2
146
- - @iloveagents/foundry-web-ui@0.1.2
147
- - @iloveagents/foundry-web-primitives@0.1.2
148
-
149
- ## 0.1.1
150
-
151
- ### Patch Changes
152
-
153
- - 689d3e9: feat(sidebar): distinct file-drop affordance for nav containers
154
-
155
- `useNavItemDnd` now exposes `isFileDragOver` separately from `isDragOver`
156
- so consuming nav items can render a prominent file-drop visual (dashed
157
- primary outline + soft primary background + Upload icon) when the user
158
- is dragging native files over the container. Previously file drags
159
- shared the same subtle ring as entity-move drags, so users couldn't
160
- tell that a folder accepted external files. Applies to all five nav-
161
- item shapes (action-row leaf, button leaf, `NestedFolderItem`,
162
- `CollapsibleNavItem` with children/actions, `NavLink` fallthrough) and
163
- updates `ContainerDropZone` for visual parity.
164
-
165
- Also fixes a related regression: the capture-phase
166
- `onDragEnterCapture` / `onDragOverCapture` handlers were calling
167
- `e.stopPropagation()`, which short-circuits React's synthetic dispatch
168
- and prevented the bubble-phase `onDragEnter` (where state actually
169
- mutates) from running. Removed — `preventDefault()` alone is enough to
170
- mark the element as droppable.
171
-
172
- - Updated dependencies [689d3e9]
173
- - @iloveagents/foundry-web-ui@0.1.1
174
- - @iloveagents/foundry-agent@0.1.1
175
- - @iloveagents/foundry-web-primitives@0.1.1
package/CLAUDE.md DELETED
@@ -1 +0,0 @@
1
- @AGENTS.md
@@ -1,142 +0,0 @@
1
- /**
2
- * Module-bootstrap test.
3
- *
4
- * Asserts that every ChatModule extension point fires when ShellApp is
5
- * rendered. Bypasses bootstrap-shell.tsx (which calls createRoot) and
6
- * renders ShellApp directly so we can probe React's tree.
7
- */
8
- import { describe, expect, it, vi } from "vitest";
9
- import { render, screen, waitFor } from "@testing-library/react";
10
- import type { ReactNode } from "react";
11
- import { defineChatModule, type ChatModule, type ShellPage } from "../types.ts";
12
-
13
- // Mock the web-ui surface the shell consumes. Don't spread the real
14
- // module — its top-level zustand stores call window.matchMedia at import
15
- // time, which jsdom doesn't provide. We only need the symbols ShellApp
16
- // + ShellLayout import directly.
17
- vi.mock("@iloveagents/foundry-web-ui", () => ({
18
- AuthProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
19
- AGUIRuntimeProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
20
- Sidebar: () => <div data-testid="sidebar" />,
21
- ChatHeader: () => <div data-testid="chat-header" />,
22
- ChatBubble: () => <div data-testid="chat-bubble" />,
23
- ChatContent: () => <div data-testid="chat-content" />,
24
- ToolPanelLayout: ({ children }: { children: ReactNode }) => <>{children}</>,
25
- ThemeRuntimeProvider: ({ children }: { children: ReactNode }) => <>{children}</>,
26
- ThemeScope: ({ children }: { children: ReactNode }) => <>{children}</>,
27
- ThemeDocumentMetadata: () => null,
28
- GlobalSelectionPopover: () => null,
29
- ContextPins: () => null,
30
- TooltipIconButton: ({ children }: { children: ReactNode }) => <>{children}</>,
31
- findNavItem: () => undefined,
32
- useAppStore: (selector: (s: any) => unknown) =>
33
- selector({ setCurrentPage: () => undefined, setNavContext: () => undefined }),
34
- useNavStore: (selector: (s: any) => unknown) => selector({ config: [] }),
35
- useChatBubbleStore: Object.assign(
36
- (selector: (s: any) => unknown) =>
37
- selector({
38
- isExpanded: false,
39
- showPagePanel: false,
40
- togglePagePanel: () => undefined,
41
- }),
42
- { getState: () => ({ close: () => undefined }) },
43
- ),
44
- useThemeStore: (selector: (s: any) => unknown) => selector({ mode: "system" }),
45
- }));
46
-
47
- vi.mock("@iloveagents/foundry-web-primitives", () => ({
48
- cn: (...classes: unknown[]) => classes.filter(Boolean).join(" "),
49
- }));
50
-
51
- vi.mock("@iloveagents/foundry-agent", () => ({
52
- createServiceFetch: () => fetch,
53
- }));
54
-
55
- vi.mock("@iloveagents/foundry-agent/msal", () => ({
56
- authStore: { getState: () => ({ getAccessToken: () => Promise.resolve("") }) },
57
- }));
58
-
59
- import { ShellApp } from "../shell-app.tsx";
60
- import type { AuthAdapter } from "../types.ts";
61
-
62
- const noopAuth: AuthAdapter = {
63
- Provider: ({ children }) => <>{children}</>,
64
- };
65
-
66
- describe("ChatModule bootstrap composition", () => {
67
- it("calls every extension point when ShellApp renders", async () => {
68
- const useInit = vi.fn();
69
- const useThemeLayers = vi.fn(() => []);
70
- const wrapperRendered = vi.fn();
71
-
72
- function ProbeWrapper({ children }: { children: ReactNode }) {
73
- wrapperRendered();
74
- return <div data-testid="probe-wrapper">{children}</div>;
75
- }
76
-
77
- const homePage: ShellPage = {
78
- path: "/",
79
- element: <div data-testid="home-page">home</div>,
80
- };
81
-
82
- const moduleHome: ShellPage = {
83
- path: "/",
84
- element: <div data-testid="module-home">module home</div>,
85
- };
86
-
87
- const modulePage: ShellPage = {
88
- path: "elsewhere",
89
- element: <div data-testid="module-page">module elsewhere</div>,
90
- };
91
-
92
- const m: ChatModule = defineChatModule({
93
- name: "test",
94
- useInit,
95
- useThemeLayers,
96
- toolUIs: <div data-testid="tool-ui-marker" />,
97
- layoutExtras: <div data-testid="layout-extra-marker" />,
98
- wrappers: [ProbeWrapper],
99
- pages: [moduleHome, modulePage],
100
- });
101
-
102
- render(
103
- <ShellApp
104
- modules={[m]}
105
- pages={[homePage]}
106
- baseThemeLayers={[]}
107
- authProvider={noopAuth}
108
- agentFetch={fetch}
109
- />,
110
- );
111
-
112
- // useInit hook fired.
113
- await waitFor(() => expect(useInit).toHaveBeenCalled());
114
-
115
- // useThemeLayers hook fired.
116
- expect(useThemeLayers).toHaveBeenCalled();
117
-
118
- // Module wrapper rendered.
119
- expect(screen.getByTestId("probe-wrapper")).toBeTruthy();
120
- expect(wrapperRendered).toHaveBeenCalled();
121
-
122
- // Tool UI rendered.
123
- expect(screen.getByTestId("tool-ui-marker")).toBeTruthy();
124
-
125
- // Layout extra rendered.
126
- expect(screen.getByTestId("layout-extra-marker")).toBeTruthy();
127
-
128
- // Customer page wins on path collision (/ resolves to home-page, not module-home).
129
- expect(screen.getByTestId("home-page")).toBeTruthy();
130
- expect(screen.queryByTestId("module-home")).toBeNull();
131
- });
132
-
133
- it("invokes fetchInterceptor (verified by bootstrap-shell test)", () => {
134
- // bootstrap-shell.tsx wraps installSpacesFetchInterceptor BEFORE createRoot.
135
- // The contract is that ShellApp itself does not invoke it — bootstrap-shell does.
136
- // This test just guards the type marker (`defineChatModule` is identity-typed).
137
- const interceptor = vi.fn();
138
- const m = defineChatModule({ name: "x", fetchInterceptor: interceptor });
139
- expect(m.fetchInterceptor).toBe(interceptor);
140
- expect(interceptor).not.toHaveBeenCalled();
141
- });
142
- });