@iloveagents/foundry-web-shell 0.1.5 → 0.2.1
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/CHANGELOG.md +50 -0
- package/package.json +7 -5
- package/src/index.ts +1 -0
- package/src/shell-app.tsx +251 -6
- package/src/types.ts +101 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @iloveagents/foundry-web-shell
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- No source changes. Bumped in lock-step with the fixed group so the
|
|
8
|
+
whole group can publish at a version number that's free across all
|
|
9
|
+
four packages. (`shell@0.2.0` already published cleanly; this is a
|
|
10
|
+
follow-the-group patch bump.)
|
|
11
|
+
|
|
12
|
+
## 0.2.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- shell: ChatConversationConfig API + sticky-runtime threadId
|
|
17
|
+
|
|
18
|
+
Lets a downstream module (e.g. `@ltwlf/spaces-web-ui`) attach a
|
|
19
|
+
`ChatConversationConfig` to its `ChatModule`. The shell then:
|
|
20
|
+
- Pre-mints a fresh thread UUID via `useRef` so the AG-UI runtime's
|
|
21
|
+
`threadId` is defined from first render — no `undefined → defined`
|
|
22
|
+
remount mid-conversation when the user sends the first message.
|
|
23
|
+
- Calls `config.useStickyConversationId()` (when declared) and
|
|
24
|
+
feeds the resulting id into the runtime so the chat stays "live"
|
|
25
|
+
while the user browses non-chat routes (Workspaces, Jobs, etc.).
|
|
26
|
+
- Per-config keyed child components (`StickyAwareRuntime` vs
|
|
27
|
+
`UrlOnlyRuntime`) so optional hooks satisfy Rules-of-Hooks across
|
|
28
|
+
multi-module config swaps.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Move `@iloveagents/foundry-agent`, `-web-primitives`, `-web-ui` from
|
|
33
|
+
`peerDependencies` to `dependencies`. They always ship together as a
|
|
34
|
+
coordinated fixed group from this monorepo — they were never
|
|
35
|
+
independently-versioned peers. Declaring them as peerDeps caused
|
|
36
|
+
changesets' `shouldBumpMajor` cascade to promote the entire group to
|
|
37
|
+
a major version on every minor changeset. See the root `foundry-agent`
|
|
38
|
+
CHANGELOG entry for details.
|
|
39
|
+
|
|
40
|
+
- Updated dependencies
|
|
41
|
+
- @iloveagents/foundry-web-ui@0.2.0
|
|
42
|
+
- @iloveagents/foundry-agent@0.2.0
|
|
43
|
+
- @iloveagents/foundry-web-primitives@0.2.0
|
|
44
|
+
|
|
45
|
+
### NOTE: 1.0.1 was accidental
|
|
46
|
+
|
|
47
|
+
Version 1.0.1 was published briefly on 2026-05-27 due to the peerDep
|
|
48
|
+
cascade bug described above (1.0.0 was reserved but not published —
|
|
49
|
+
1.0.1 was the retry). It has been unpublished. Both 1.0.0 and 1.0.1
|
|
50
|
+
are now permanently reserved on npm and CANNOT be re-published. Do
|
|
51
|
+
not depend on 1.0.x.
|
|
52
|
+
|
|
3
53
|
## 0.1.5
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-shell",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -16,10 +16,12 @@
|
|
|
16
16
|
"react-router": "^7.0.0",
|
|
17
17
|
"@assistant-ui/react": "^0.12.25",
|
|
18
18
|
"lucide-react": ">=0.400.0",
|
|
19
|
-
"zustand": "^5.0.0"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"@iloveagents/foundry-
|
|
19
|
+
"zustand": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@iloveagents/foundry-agent": "0.2.1",
|
|
23
|
+
"@iloveagents/foundry-web-primitives": "0.2.1",
|
|
24
|
+
"@iloveagents/foundry-web-ui": "0.2.1"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
27
|
"typescript": "~5.9.3",
|
package/src/index.ts
CHANGED
package/src/shell-app.tsx
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { type ReactNode, Suspense, useMemo } from "react";
|
|
2
|
-
import { BrowserRouter, Routes, Route } from "react-router";
|
|
1
|
+
import { type ReactNode, Suspense, useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import { BrowserRouter, Routes, Route, useLocation } from "react-router";
|
|
3
3
|
import { AGUIRuntimeProvider } from "@iloveagents/foundry-web-ui";
|
|
4
|
+
import type {
|
|
5
|
+
AGUIChatConversationFactoryArgs,
|
|
6
|
+
AGUIHistoryAdapterFactory,
|
|
7
|
+
} from "@iloveagents/foundry-web-ui";
|
|
4
8
|
import type { ThemeLayer } from "@iloveagents/foundry-web-ui";
|
|
5
|
-
import type { ChatModule, AuthAdapter, ShellPage } from "./types.ts";
|
|
9
|
+
import type { ChatConversationConfig, ChatModule, AuthAdapter, ShellPage } from "./types.ts";
|
|
6
10
|
import { ShellAuth } from "./auth-default.tsx";
|
|
7
11
|
import { ShellLayout } from "./shell-layout.tsx";
|
|
8
12
|
import { defaultAgentFetch } from "./service-fetch-default.ts";
|
|
@@ -65,12 +69,22 @@ export function ShellApp({
|
|
|
65
69
|
|
|
66
70
|
const fetchFn = agentFetch ?? defaultAgentFetch;
|
|
67
71
|
|
|
72
|
+
// Aggregate chatConversation configs from all modules. First-match
|
|
73
|
+
// wins at URL-match time (rare to have more than one anyway).
|
|
74
|
+
const chatConversationConfigs = useMemo(
|
|
75
|
+
() => modules.flatMap((m) => (m.chatConversation ? [m.chatConversation] : [])),
|
|
76
|
+
[modules],
|
|
77
|
+
);
|
|
78
|
+
|
|
68
79
|
return (
|
|
69
80
|
<BrowserRouter>
|
|
70
81
|
<ShellAuth adapter={authProvider}>
|
|
71
82
|
<ComposedWrappers wrappers={wrappers}>
|
|
72
|
-
<
|
|
73
|
-
{
|
|
83
|
+
<ChatConversationAwareRuntime
|
|
84
|
+
fetchFn={fetchFn}
|
|
85
|
+
configs={chatConversationConfigs}
|
|
86
|
+
toolUIs={toolUIs}
|
|
87
|
+
>
|
|
74
88
|
<Suspense fallback={null}>
|
|
75
89
|
<Routes>
|
|
76
90
|
<Route element={<ShellLayout modules={modules} baseThemeLayers={baseThemeLayers} />}>
|
|
@@ -80,17 +94,248 @@ export function ShellApp({
|
|
|
80
94
|
</Route>
|
|
81
95
|
</Routes>
|
|
82
96
|
</Suspense>
|
|
83
|
-
</
|
|
97
|
+
</ChatConversationAwareRuntime>
|
|
84
98
|
</ComposedWrappers>
|
|
85
99
|
</ShellAuth>
|
|
86
100
|
</BrowserRouter>
|
|
87
101
|
);
|
|
88
102
|
}
|
|
89
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Reads the current pathname (only meaningful inside ``<BrowserRouter>``),
|
|
106
|
+
* matches it against any module-declared :type:`ChatConversationConfig`,
|
|
107
|
+
* and hands the matched config + URL-extracted id down into
|
|
108
|
+
* :type:`AGUIRuntimeProvider` as a :type:`AGUIHistoryAdapterFactory`.
|
|
109
|
+
*
|
|
110
|
+
* Why a factory (not a pre-built adapter)
|
|
111
|
+
* =======================================
|
|
112
|
+
* The history adapter needs *both* the URL-extracted conversation id
|
|
113
|
+
* (when present) *and* the AG-UI adapter's freshly-minted thread id
|
|
114
|
+
* (always set — covers fresh chats). The AG-UI adapter only exists
|
|
115
|
+
* once :type:`AGUIRuntimeProvider` mounts, so the factory is invoked
|
|
116
|
+
* inside that mount with both pieces available. This makes "fresh
|
|
117
|
+
* chats also persist" a built-in property of the contract, not an
|
|
118
|
+
* afterthought.
|
|
119
|
+
*/
|
|
120
|
+
function ChatConversationAwareRuntime({
|
|
121
|
+
children,
|
|
122
|
+
toolUIs,
|
|
123
|
+
fetchFn,
|
|
124
|
+
configs,
|
|
125
|
+
}: {
|
|
126
|
+
children: ReactNode;
|
|
127
|
+
toolUIs: ReactNode[];
|
|
128
|
+
fetchFn: typeof fetch;
|
|
129
|
+
configs: ChatConversationConfig[];
|
|
130
|
+
}) {
|
|
131
|
+
const { pathname } = useLocation();
|
|
132
|
+
|
|
133
|
+
// Pick the matching config (URL-pattern match) OR fall back to the
|
|
134
|
+
// first config registered — modules without a URL match still want
|
|
135
|
+
// their fresh-chat persistence. With ≥1 config the runtime always
|
|
136
|
+
// gets a factory; without configs it stays in the legacy in-memory
|
|
137
|
+
// mode.
|
|
138
|
+
const { config, urlMatch } = useMemo(() => {
|
|
139
|
+
for (const cfg of configs) {
|
|
140
|
+
const m = pathname.match(cfg.pathPattern);
|
|
141
|
+
if (m && m[1]) {
|
|
142
|
+
return { config: cfg, urlMatch: m[1] };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { config: configs[0], urlMatch: undefined as string | undefined };
|
|
146
|
+
}, [pathname, configs]);
|
|
147
|
+
|
|
148
|
+
if (!config) {
|
|
149
|
+
// No chatConversation configs registered → legacy in-memory
|
|
150
|
+
// runtime, no history / no sticky concerns.
|
|
151
|
+
return (
|
|
152
|
+
<AGUIRuntimeProvider fetchFn={fetchFn}>
|
|
153
|
+
{toolUIs}
|
|
154
|
+
{children}
|
|
155
|
+
</AGUIRuntimeProvider>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Per-config child component, KEYED by config identity. Why a
|
|
160
|
+
// key/child split: ``config.useStickyConversationId`` is optional.
|
|
161
|
+
// Calling it inline with the optional chain ``config?.useSticky?.()``
|
|
162
|
+
// would be a Rules-of-Hooks violation the moment a second
|
|
163
|
+
// ChatConversationConfig registers with different hook-shape — URL
|
|
164
|
+
// navigation flips which cfg matches, the hook count flips, React
|
|
165
|
+
// crashes. The key makes a config swap a full remount, which is a
|
|
166
|
+
// legitimate way to change the hook lineage without violating the
|
|
167
|
+
// rules. ``configIndex`` is stable per config because ``configs`` is
|
|
168
|
+
// memoised in ShellApp.
|
|
169
|
+
const configIndex = configs.indexOf(config);
|
|
170
|
+
if (config.useStickyConversationId) {
|
|
171
|
+
return (
|
|
172
|
+
<StickyAwareRuntime
|
|
173
|
+
key={`sticky:${configIndex}`}
|
|
174
|
+
config={config}
|
|
175
|
+
useSticky={config.useStickyConversationId}
|
|
176
|
+
urlMatch={urlMatch}
|
|
177
|
+
fetchFn={fetchFn}
|
|
178
|
+
toolUIs={toolUIs}
|
|
179
|
+
>
|
|
180
|
+
{children}
|
|
181
|
+
</StickyAwareRuntime>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return (
|
|
185
|
+
<UrlOnlyRuntime
|
|
186
|
+
key={`url:${configIndex}`}
|
|
187
|
+
config={config}
|
|
188
|
+
urlMatch={urlMatch}
|
|
189
|
+
fetchFn={fetchFn}
|
|
190
|
+
toolUIs={toolUIs}
|
|
191
|
+
>
|
|
192
|
+
{children}
|
|
193
|
+
</UrlOnlyRuntime>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface ChildRuntimeProps {
|
|
198
|
+
config: ChatConversationConfig;
|
|
199
|
+
urlMatch: string | undefined;
|
|
200
|
+
fetchFn: typeof fetch;
|
|
201
|
+
toolUIs: ReactNode[];
|
|
202
|
+
children: ReactNode;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Branch for configs that DECLARE :attr:`useStickyConversationId`.
|
|
207
|
+
* Calls the hook unconditionally so React's hook-count invariant
|
|
208
|
+
* holds across every render of this mount.
|
|
209
|
+
*/
|
|
210
|
+
function StickyAwareRuntime({
|
|
211
|
+
config,
|
|
212
|
+
useSticky,
|
|
213
|
+
urlMatch,
|
|
214
|
+
fetchFn,
|
|
215
|
+
toolUIs,
|
|
216
|
+
children,
|
|
217
|
+
}: ChildRuntimeProps & { useSticky: () => string | null }) {
|
|
218
|
+
const sticky = useSticky();
|
|
219
|
+
return (
|
|
220
|
+
<RuntimeBody
|
|
221
|
+
config={config}
|
|
222
|
+
sticky={sticky}
|
|
223
|
+
urlMatch={urlMatch}
|
|
224
|
+
fetchFn={fetchFn}
|
|
225
|
+
toolUIs={toolUIs}
|
|
226
|
+
>
|
|
227
|
+
{children}
|
|
228
|
+
</RuntimeBody>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Branch for configs that DO NOT declare :attr:`useStickyConversationId`.
|
|
234
|
+
* No hook is ever called for sticky state — the runtime threadId
|
|
235
|
+
* follows URL match and the shell-minted fresh UUID only.
|
|
236
|
+
*/
|
|
237
|
+
function UrlOnlyRuntime({ config, urlMatch, fetchFn, toolUIs, children }: ChildRuntimeProps) {
|
|
238
|
+
return (
|
|
239
|
+
<RuntimeBody
|
|
240
|
+
config={config}
|
|
241
|
+
sticky={null}
|
|
242
|
+
urlMatch={urlMatch}
|
|
243
|
+
fetchFn={fetchFn}
|
|
244
|
+
toolUIs={toolUIs}
|
|
245
|
+
>
|
|
246
|
+
{children}
|
|
247
|
+
</RuntimeBody>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Shared rendering body — same useRef/useMemo/useCallback shape for
|
|
253
|
+
* both branches. The hook lineage here is stable per mount because
|
|
254
|
+
* the parent decides which branch (and which key) to use.
|
|
255
|
+
*/
|
|
256
|
+
function RuntimeBody({
|
|
257
|
+
config,
|
|
258
|
+
sticky,
|
|
259
|
+
urlMatch,
|
|
260
|
+
fetchFn,
|
|
261
|
+
toolUIs,
|
|
262
|
+
children,
|
|
263
|
+
}: ChildRuntimeProps & { sticky: string | null }) {
|
|
264
|
+
// Pre-mint a fresh UUID so the runtime's ``threadId`` is set from
|
|
265
|
+
// the very first render — never goes through ``undefined → defined``,
|
|
266
|
+
// which would otherwise force a remount of the assistant-ui runtime
|
|
267
|
+
// (and wipe in-flight messages) the moment the user sent their first
|
|
268
|
+
// message on a fresh chat. The backend collapse (``conversation_id ==
|
|
269
|
+
// agui_thread_id``) ensures the id the runtime uses from the start
|
|
270
|
+
// matches the row the lazy ``ensureConversationId`` creates — same
|
|
271
|
+
// value, no mid-conversation switch.
|
|
272
|
+
//
|
|
273
|
+
// Mint a new one only when the user actually starts a new thread:
|
|
274
|
+
// ``sticky`` transitions from a value back to ``null`` (the module
|
|
275
|
+
// clears it on the ``/`` "New Thread" landing).
|
|
276
|
+
const freshIdRef = useRef<string>(_genUuid());
|
|
277
|
+
const prevStickyRef = useRef<string | null>(sticky);
|
|
278
|
+
if (sticky === null && prevStickyRef.current !== null) {
|
|
279
|
+
// User just navigated back to "/" (New Thread). The previous chat
|
|
280
|
+
// is being abandoned; mint a fresh UUID for the next session.
|
|
281
|
+
freshIdRef.current = _genUuid();
|
|
282
|
+
}
|
|
283
|
+
prevStickyRef.current = sticky;
|
|
284
|
+
|
|
285
|
+
// Effective id seen by AGUIRuntimeProvider — always defined.
|
|
286
|
+
// sticky (current conversation) wins, else fall back to the
|
|
287
|
+
// shell-minted UUID for fresh chats.
|
|
288
|
+
const effectiveThreadId: string = sticky ?? urlMatch ?? freshIdRef.current;
|
|
289
|
+
|
|
290
|
+
const historyAdapterFactory = useCallback<AGUIHistoryAdapterFactory>(
|
|
291
|
+
(args: AGUIChatConversationFactoryArgs) =>
|
|
292
|
+
config.buildHistoryAdapter({
|
|
293
|
+
urlMatch: args.urlMatch,
|
|
294
|
+
aguiThreadId: args.aguiThreadId,
|
|
295
|
+
}),
|
|
296
|
+
[config],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<AGUIRuntimeProvider
|
|
301
|
+
fetchFn={fetchFn}
|
|
302
|
+
threadId={effectiveThreadId}
|
|
303
|
+
// ``urlMatch`` carries the URL-derived resume signal verbatim.
|
|
304
|
+
// ``effectiveThreadId`` is always defined (sticky / urlMatch /
|
|
305
|
+
// freshly minted UUID) so passing it in place of ``urlMatch``
|
|
306
|
+
// would erase the "fresh vs resumed" distinction the history
|
|
307
|
+
// adapter needs to decide whether to load() past messages.
|
|
308
|
+
urlMatch={urlMatch}
|
|
309
|
+
historyAdapterFactory={historyAdapterFactory}
|
|
310
|
+
>
|
|
311
|
+
{toolUIs}
|
|
312
|
+
{children}
|
|
313
|
+
</AGUIRuntimeProvider>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
90
317
|
function ToolUIGroup({ children }: { children: ReactNode }) {
|
|
91
318
|
return <>{children}</>;
|
|
92
319
|
}
|
|
93
320
|
|
|
321
|
+
/**
|
|
322
|
+
* RFC 4122 v4 UUID. Use ``crypto.randomUUID`` when available; fall
|
|
323
|
+
* back to a math-random shim for the rare environments without it
|
|
324
|
+
* (older Safari on non-HTTPS dev hosts). The shim is fine for our
|
|
325
|
+
* use — these UUIDs identify a conversation row in our backend; they
|
|
326
|
+
* don't need cryptographic strength.
|
|
327
|
+
*/
|
|
328
|
+
function _genUuid(): string {
|
|
329
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
330
|
+
return crypto.randomUUID();
|
|
331
|
+
}
|
|
332
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
333
|
+
const r = (Math.random() * 16) | 0;
|
|
334
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
335
|
+
return v.toString(16);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
94
339
|
interface ComposedWrappersProps {
|
|
95
340
|
wrappers: React.ComponentType<{ children: ReactNode }>[];
|
|
96
341
|
children: ReactNode;
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import type { ThemeLayer } from "@iloveagents/foundry-web-ui";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Per-mount inputs the shell hands the module's history-adapter
|
|
6
|
+
* factory. Either ``urlMatch`` (resumed chat — the user clicked a
|
|
7
|
+
* Recents entry) or ``aguiThreadId`` alone (fresh chat — assistant-ui
|
|
8
|
+
* just minted a UUID) drives the canonical conversation id, depending
|
|
9
|
+
* on what the module's backend treats as authoritative.
|
|
10
|
+
*/
|
|
11
|
+
export interface ChatConversationFactoryArgs {
|
|
12
|
+
/**
|
|
13
|
+
* The conversation id captured from the URL via
|
|
14
|
+
* :attr:`ChatConversationConfig.pathPattern`. Set when the user
|
|
15
|
+
* resumed a known chat; ``undefined`` on a fresh thread.
|
|
16
|
+
*/
|
|
17
|
+
urlMatch?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The assistant-ui adapter's stable thread id (a client-minted
|
|
20
|
+
* UUID, unique per "New Thread" click). Always set. Use this as the
|
|
21
|
+
* key for a lazy ``create_or_get`` when ``urlMatch`` is missing so
|
|
22
|
+
* fresh chats also persist.
|
|
23
|
+
*/
|
|
24
|
+
aguiThreadId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* URL-driven chat persistence wiring. Modules declare this so the
|
|
29
|
+
* shell can stand up an assistant-ui :type:`ThreadHistoryAdapter` on
|
|
30
|
+
* every mount of :type:`AGUIRuntimeProvider` — both resumed chats
|
|
31
|
+
* (matched URL) and fresh chats.
|
|
32
|
+
*
|
|
33
|
+
* The shell stays free of feature-specific knowledge: it captures
|
|
34
|
+
* the URL match (if any) and the assistant-ui-minted thread id, and
|
|
35
|
+
* hands both to :attr:`buildHistoryAdapter`. The module owns the
|
|
36
|
+
* actual backend API shape and "fresh chat vs. resume" semantics.
|
|
37
|
+
*/
|
|
38
|
+
export interface ChatConversationConfig {
|
|
39
|
+
/**
|
|
40
|
+
* Regex with exactly one capture group that yields the conversation
|
|
41
|
+
* id from ``window.location.pathname``. Example:
|
|
42
|
+
* ``/^\/chat\/([^/]+)$/``.
|
|
43
|
+
*/
|
|
44
|
+
pathPattern: RegExp;
|
|
45
|
+
/**
|
|
46
|
+
* Optional hook giving the module's view of "what conversation is
|
|
47
|
+
* the user currently engaged with" — separate from the URL.
|
|
48
|
+
*
|
|
49
|
+
* Why this exists: ChatGPT-style UX wants the chat runtime to stay
|
|
50
|
+
* alive while the user browses Workspaces, Jobs, etc. If we drove
|
|
51
|
+
* the runtime's ``threadId`` from the URL only, every non-chat
|
|
52
|
+
* navigation would remount the runtime and wipe in-flight messages.
|
|
53
|
+
*
|
|
54
|
+
* When provided, the shell prefers this value over the URL match
|
|
55
|
+
* for the ``AGUIRuntimeProvider.threadId`` prop. The module is
|
|
56
|
+
* expected to update its sticky state on URL transitions itself
|
|
57
|
+
* (e.g. via :type:`useTrackActiveChatFromUrl` in Spaces).
|
|
58
|
+
*
|
|
59
|
+
* Return ``null`` when the user has no active chat (initial app
|
|
60
|
+
* load, or just clicked "New Thread"). The shell then lets the
|
|
61
|
+
* AG-UI adapter mint a fresh UUID, same as before this hook.
|
|
62
|
+
*/
|
|
63
|
+
useStickyConversationId?: () => string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Build an assistant-ui :type:`ThreadHistoryAdapter` for the
|
|
66
|
+
* current mount. Called inside the runtime provider once the
|
|
67
|
+
* AG-UI adapter has minted (or accepted) its thread id, so both
|
|
68
|
+
* ``urlMatch`` and ``aguiThreadId`` are available.
|
|
69
|
+
*
|
|
70
|
+
* The returned adapter is wired into ``useLocalRuntime``'s
|
|
71
|
+
* ``adapters.history`` slot — assistant-ui calls ``load()`` on
|
|
72
|
+
* mount and ``append()`` after every completed turn.
|
|
73
|
+
*
|
|
74
|
+
* Modules typically:
|
|
75
|
+
* - On resume (``urlMatch`` set): use it directly as the
|
|
76
|
+
* conversation id for load/append.
|
|
77
|
+
* - On fresh (``urlMatch`` undefined): lazily ``create_or_get``
|
|
78
|
+
* a server row keyed by ``aguiThreadId``, cache the resulting
|
|
79
|
+
* conversation id, then load/append against that.
|
|
80
|
+
*/
|
|
81
|
+
buildHistoryAdapter: (args: ChatConversationFactoryArgs) => import("@assistant-ui/react").ThreadHistoryAdapter;
|
|
82
|
+
}
|
|
83
|
+
|
|
4
84
|
/** Route entry contributed by a module or the host app. */
|
|
5
85
|
export interface ShellPage {
|
|
6
86
|
path: string;
|
|
@@ -38,6 +118,27 @@ export interface ChatModule {
|
|
|
38
118
|
useThemeLayers?: () => ThemeLayer[];
|
|
39
119
|
pages?: ShellPage[];
|
|
40
120
|
fetchInterceptor?: () => void;
|
|
121
|
+
/**
|
|
122
|
+
* URL-driven chat persistence.
|
|
123
|
+
*
|
|
124
|
+
* Selection order (see ``ChatConversationAwareRuntime`` in
|
|
125
|
+
* ``shell-app.tsx``):
|
|
126
|
+
*
|
|
127
|
+
* 1. URL match. If a module's ``pathPattern`` matches the current
|
|
128
|
+
* pathname and captures a non-empty group, that module's config
|
|
129
|
+
* wins and its captured id is fed to the history adapter as
|
|
130
|
+
* ``urlMatch``.
|
|
131
|
+
* 2. Fallback. If nothing matches the URL, the shell falls back to
|
|
132
|
+
* the FIRST registered config (``configs[0]``) so a fresh-chat
|
|
133
|
+
* runtime still gets a history adapter and the chat persists
|
|
134
|
+
* from the very first message. ``urlMatch`` is ``undefined``
|
|
135
|
+
* in this case.
|
|
136
|
+
* 3. None. With zero configs registered, the runtime stays in
|
|
137
|
+
* legacy in-memory mode (no persistence).
|
|
138
|
+
*
|
|
139
|
+
* Modules without a config don't participate in selection.
|
|
140
|
+
*/
|
|
141
|
+
chatConversation?: ChatConversationConfig;
|
|
41
142
|
}
|
|
42
143
|
|
|
43
144
|
/**
|