@iloveagents/foundry-web-shell 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/auth-default.d.ts +18 -0
- package/dist/auth-default.js +55 -0
- package/dist/bootstrap-shell.d.ts +11 -0
- package/dist/bootstrap-shell.js +28 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/runtime-config.d.ts +22 -0
- package/{src/runtime-config.ts → dist/runtime-config.js} +7 -8
- package/dist/service-fetch-default.d.ts +17 -0
- package/{src/service-fetch-default.ts → dist/service-fetch-default.js} +5 -7
- package/dist/shell-app.d.ts +18 -0
- package/dist/shell-app.js +227 -0
- package/dist/shell-layout.d.ts +19 -0
- package/dist/shell-layout.js +82 -0
- package/dist/types.d.ts +174 -0
- package/dist/types.js +4 -0
- package/package.json +26 -10
- package/AGENTS.md +0 -82
- package/CHANGELOG.md +0 -184
- package/CLAUDE.md +0 -1
- package/src/__tests__/module-bootstrap.test.tsx +0 -142
- package/src/__tests__/wrapper-order.test.tsx +0 -162
- package/src/auth-default.tsx +0 -78
- package/src/bootstrap-shell.tsx +0 -49
- package/src/index.ts +0 -14
- package/src/shell-app.tsx +0 -386
- package/src/shell-layout.tsx +0 -171
- package/src/types.ts +0 -171
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -9
package/dist/types.d.ts
ADDED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-shell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
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.
|
|
23
|
-
"@iloveagents/foundry-web-primitives": "0.
|
|
24
|
-
"@iloveagents/foundry-web-ui": "0.
|
|
31
|
+
"@iloveagents/foundry-agent": "^0.5.0",
|
|
32
|
+
"@iloveagents/foundry-web-primitives": "^0.5.0",
|
|
33
|
+
"@iloveagents/foundry-web-ui": "^0.5.0"
|
|
25
34
|
},
|
|
26
35
|
"devDependencies": {
|
|
27
|
-
"
|
|
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
|
-
"
|
|
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 `@iloveagents/foundry-*` 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
|
-
The `ChatModule` protocol is documented in **Public API** above; the canonical type definitions live in [`src/types.ts`](src/types.ts) (`ChatModule`, `defineChatModule`). For the nav-mutation-from-`useInit` pattern, have your module's `useInit` write the sidebar nav `config` into `useNavStore` (the same store [`src/shell-layout.tsx`](src/shell-layout.tsx) reads via `useNavStore((s) => s.config)`).
|
package/CHANGELOG.md
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
# @iloveagents/foundry-web-shell
|
|
2
|
-
|
|
3
|
-
## 0.3.1
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [9ec870e]
|
|
8
|
-
- @iloveagents/foundry-web-ui@0.3.1
|
|
9
|
-
- @iloveagents/foundry-agent@0.3.1
|
|
10
|
-
- @iloveagents/foundry-web-primitives@0.3.1
|
|
11
|
-
|
|
12
|
-
## 0.3.0
|
|
13
|
-
|
|
14
|
-
### Minor Changes
|
|
15
|
-
|
|
16
|
-
- df383c9: Add runtime configuration support so the web app can ship as a single prebuilt
|
|
17
|
-
container image that boots for any environment without a rebuild. New
|
|
18
|
-
`runtimeConfig(key)` helper resolves `window.__APP_CONFIG__[key]` (injected by
|
|
19
|
-
the container at startup) before `import.meta.env[key]` (build-time Vite env,
|
|
20
|
-
the local-dev fallback). The default MSAL auth adapter and agent fetch client
|
|
21
|
-
now read `VITE_MSAL_*` / `VITE_API_BASE_URL` through it. Local `pnpm dev` is
|
|
22
|
-
unchanged (no `/config.js` → falls back to `import.meta.env`).
|
|
23
|
-
|
|
24
|
-
### Patch Changes
|
|
25
|
-
|
|
26
|
-
- @iloveagents/foundry-agent@0.3.0
|
|
27
|
-
- @iloveagents/foundry-web-primitives@0.3.0
|
|
28
|
-
- @iloveagents/foundry-web-ui@0.3.0
|
|
29
|
-
|
|
30
|
-
## 0.2.2
|
|
31
|
-
|
|
32
|
-
### Patch Changes
|
|
33
|
-
|
|
34
|
-
- 8184a8c: shell: URL is authoritative for the runtime threadId — fixes
|
|
35
|
-
resume-creates-new-conversation race
|
|
36
|
-
|
|
37
|
-
`ChatConversationAwareRuntime` previously computed
|
|
38
|
-
`effectiveThreadId = sticky ?? urlMatch ?? freshIdRef.current`.
|
|
39
|
-
When the user navigated from one chat to another via a Recents
|
|
40
|
-
click, the synchronous render that followed the URL change had:
|
|
41
|
-
- `urlMatch = B` (read from the now-updated `useLocation`)
|
|
42
|
-
- `sticky = A` (the active-chat-store hadn't been updated by
|
|
43
|
-
`useTrackActiveChatFromUrl`'s `useEffect` yet — effects run
|
|
44
|
-
AFTER the commit phase)
|
|
45
|
-
|
|
46
|
-
`sticky ?? urlMatch` picked `A` (the previous chat). The AG-UI
|
|
47
|
-
adapter was constructed with `threadId = A`. The first message the
|
|
48
|
-
user sent went to `/api/agent` with `thread_id = A`, the middleware
|
|
49
|
-
created a new row keyed by `A`... wait actually no, here's what
|
|
50
|
-
happens — the runtime mints a fresh runner UUID when given a
|
|
51
|
-
mismatched threadId; that fresh UUID became a new conversation row,
|
|
52
|
-
appearing as "Untitled chat" at the top of the sidebar while the
|
|
53
|
-
URL still said `/chat/B`. Two active-looking dots: one for `B`
|
|
54
|
-
(NavLink URL match), one for the new row (statusDot from the
|
|
55
|
-
updated active-chat-store after the lazy ensure ran).
|
|
56
|
-
|
|
57
|
-
Swap the priority: `urlMatch ?? sticky ?? freshIdRef.current`. The
|
|
58
|
-
URL is authoritative whenever it's set (i.e. on `/chat/<id>`
|
|
59
|
-
routes), eliminating the stale-sticky race entirely. `sticky` is
|
|
60
|
-
still consulted as the second-priority fallback for non-chat
|
|
61
|
-
routes (`/spaces`, `/tasks`) so the popout chat stays "live" while
|
|
62
|
-
the user browses workspaces — that's the original purpose of
|
|
63
|
-
`useStickyConversationId` and it's preserved.
|
|
64
|
-
|
|
65
|
-
- Updated dependencies [395f0cd]
|
|
66
|
-
- Updated dependencies [1ba01ef]
|
|
67
|
-
- Updated dependencies [8184a8c]
|
|
68
|
-
- @iloveagents/foundry-web-ui@0.2.2
|
|
69
|
-
- @iloveagents/foundry-agent@0.2.2
|
|
70
|
-
- @iloveagents/foundry-web-primitives@0.2.2
|
|
71
|
-
|
|
72
|
-
## 0.2.1
|
|
73
|
-
|
|
74
|
-
### Patch Changes
|
|
75
|
-
|
|
76
|
-
- No source changes. Bumped in lock-step with the fixed group so the
|
|
77
|
-
whole group can publish at a version number that's free across all
|
|
78
|
-
four packages. (`shell@0.2.0` already published cleanly; this is a
|
|
79
|
-
follow-the-group patch bump.)
|
|
80
|
-
|
|
81
|
-
## 0.2.0
|
|
82
|
-
|
|
83
|
-
### Minor Changes
|
|
84
|
-
|
|
85
|
-
- shell: ChatConversationConfig API + sticky-runtime threadId
|
|
86
|
-
|
|
87
|
-
Lets a downstream module (e.g. `@ltwlf/spaces-web-ui`) attach a
|
|
88
|
-
`ChatConversationConfig` to its `ChatModule`. The shell then:
|
|
89
|
-
- Pre-mints a fresh thread UUID via `useRef` so the AG-UI runtime's
|
|
90
|
-
`threadId` is defined from first render — no `undefined → defined`
|
|
91
|
-
remount mid-conversation when the user sends the first message.
|
|
92
|
-
- Calls `config.useStickyConversationId()` (when declared) and
|
|
93
|
-
feeds the resulting id into the runtime so the chat stays "live"
|
|
94
|
-
while the user browses non-chat routes (Workspaces, Jobs, etc.).
|
|
95
|
-
- Per-config keyed child components (`StickyAwareRuntime` vs
|
|
96
|
-
`UrlOnlyRuntime`) so optional hooks satisfy Rules-of-Hooks across
|
|
97
|
-
multi-module config swaps.
|
|
98
|
-
|
|
99
|
-
### Patch Changes
|
|
100
|
-
|
|
101
|
-
- Move `@iloveagents/foundry-agent`, `-web-primitives`, `-web-ui` from
|
|
102
|
-
`peerDependencies` to `dependencies`. They always ship together as a
|
|
103
|
-
coordinated fixed group from this monorepo — they were never
|
|
104
|
-
independently-versioned peers. Declaring them as peerDeps caused
|
|
105
|
-
changesets' `shouldBumpMajor` cascade to promote the entire group to
|
|
106
|
-
a major version on every minor changeset. See the root `foundry-agent`
|
|
107
|
-
CHANGELOG entry for details.
|
|
108
|
-
|
|
109
|
-
- Updated dependencies
|
|
110
|
-
- @iloveagents/foundry-web-ui@0.2.0
|
|
111
|
-
- @iloveagents/foundry-agent@0.2.0
|
|
112
|
-
- @iloveagents/foundry-web-primitives@0.2.0
|
|
113
|
-
|
|
114
|
-
### NOTE: 1.0.1 was accidental
|
|
115
|
-
|
|
116
|
-
Version 1.0.1 was published briefly on 2026-05-27 due to the peerDep
|
|
117
|
-
cascade bug described above (1.0.0 was reserved but not published —
|
|
118
|
-
1.0.1 was the retry). It has been unpublished. Both 1.0.0 and 1.0.1
|
|
119
|
-
are now permanently reserved on npm and CANNOT be re-published. Do
|
|
120
|
-
not depend on 1.0.x.
|
|
121
|
-
|
|
122
|
-
## 0.1.5
|
|
123
|
-
|
|
124
|
-
### Patch Changes
|
|
125
|
-
|
|
126
|
-
- Updated dependencies [f9144ca]
|
|
127
|
-
- @iloveagents/foundry-web-ui@0.1.5
|
|
128
|
-
- @iloveagents/foundry-agent@0.1.5
|
|
129
|
-
- @iloveagents/foundry-web-primitives@0.1.5
|
|
130
|
-
|
|
131
|
-
## 0.1.4
|
|
132
|
-
|
|
133
|
-
### Patch Changes
|
|
134
|
-
|
|
135
|
-
- Updated dependencies [07ace7e]
|
|
136
|
-
- @iloveagents/foundry-agent@0.1.4
|
|
137
|
-
- @iloveagents/foundry-web-ui@0.1.4
|
|
138
|
-
- @iloveagents/foundry-web-primitives@0.1.4
|
|
139
|
-
|
|
140
|
-
## 0.1.3
|
|
141
|
-
|
|
142
|
-
### Patch Changes
|
|
143
|
-
|
|
144
|
-
- Updated dependencies [d9b0460]
|
|
145
|
-
- @iloveagents/foundry-agent@0.1.3
|
|
146
|
-
- @iloveagents/foundry-web-ui@0.1.3
|
|
147
|
-
- @iloveagents/foundry-web-primitives@0.1.3
|
|
148
|
-
|
|
149
|
-
## 0.1.2
|
|
150
|
-
|
|
151
|
-
### Patch Changes
|
|
152
|
-
|
|
153
|
-
- Updated dependencies [30a4346]
|
|
154
|
-
- @iloveagents/foundry-agent@0.1.2
|
|
155
|
-
- @iloveagents/foundry-web-ui@0.1.2
|
|
156
|
-
- @iloveagents/foundry-web-primitives@0.1.2
|
|
157
|
-
|
|
158
|
-
## 0.1.1
|
|
159
|
-
|
|
160
|
-
### Patch Changes
|
|
161
|
-
|
|
162
|
-
- 689d3e9: feat(sidebar): distinct file-drop affordance for nav containers
|
|
163
|
-
|
|
164
|
-
`useNavItemDnd` now exposes `isFileDragOver` separately from `isDragOver`
|
|
165
|
-
so consuming nav items can render a prominent file-drop visual (dashed
|
|
166
|
-
primary outline + soft primary background + Upload icon) when the user
|
|
167
|
-
is dragging native files over the container. Previously file drags
|
|
168
|
-
shared the same subtle ring as entity-move drags, so users couldn't
|
|
169
|
-
tell that a folder accepted external files. Applies to all five nav-
|
|
170
|
-
item shapes (action-row leaf, button leaf, `NestedFolderItem`,
|
|
171
|
-
`CollapsibleNavItem` with children/actions, `NavLink` fallthrough) and
|
|
172
|
-
updates `ContainerDropZone` for visual parity.
|
|
173
|
-
|
|
174
|
-
Also fixes a related regression: the capture-phase
|
|
175
|
-
`onDragEnterCapture` / `onDragOverCapture` handlers were calling
|
|
176
|
-
`e.stopPropagation()`, which short-circuits React's synthetic dispatch
|
|
177
|
-
and prevented the bubble-phase `onDragEnter` (where state actually
|
|
178
|
-
mutates) from running. Removed — `preventDefault()` alone is enough to
|
|
179
|
-
mark the element as droppable.
|
|
180
|
-
|
|
181
|
-
- Updated dependencies [689d3e9]
|
|
182
|
-
- @iloveagents/foundry-web-ui@0.1.1
|
|
183
|
-
- @iloveagents/foundry-agent@0.1.1
|
|
184
|
-
- @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
|
-
});
|