@iloveagents/foundry-web-shell 0.2.1 → 0.3.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/CHANGELOG.md +60 -0
- package/package.json +4 -4
- package/src/auth-default.tsx +16 -13
- package/src/index.ts +1 -0
- package/src/runtime-config.ts +30 -0
- package/src/service-fetch-default.ts +2 -1
- package/src/shell-app.tsx +33 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# @iloveagents/foundry-web-shell
|
|
2
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
|
+
|
|
3
63
|
## 0.2.1
|
|
4
64
|
|
|
5
65
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"zustand": "^5.0.0"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@iloveagents/foundry-agent": "0.
|
|
23
|
-
"@iloveagents/foundry-web-primitives": "0.
|
|
24
|
-
"@iloveagents/foundry-web-ui": "0.
|
|
22
|
+
"@iloveagents/foundry-agent": "0.3.0",
|
|
23
|
+
"@iloveagents/foundry-web-primitives": "0.3.0",
|
|
24
|
+
"@iloveagents/foundry-web-ui": "0.3.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"typescript": "~5.9.3",
|
package/src/auth-default.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import type { FC, ReactNode } from "react";
|
|
|
2
2
|
import { AuthProvider } from "@iloveagents/foundry-web-ui";
|
|
3
3
|
import type { MsalAuthConfig } from "@iloveagents/foundry-agent/msal";
|
|
4
4
|
import type { AuthAdapter } from "./types.ts";
|
|
5
|
+
import { runtimeConfig } from "./runtime-config.ts";
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
@@ -23,15 +24,16 @@ function AuthConfigError({ missingKeys }: AuthConfigErrorProps) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* Read MSAL configuration
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Read MSAL configuration. Prefers runtime config (`window.__APP_CONFIG__`,
|
|
28
|
+
* injected by the container at startup) and falls back to build-time Vite env
|
|
29
|
+
* (`import.meta.env`) for local dev — see {@link runtimeConfig}. Returns the
|
|
30
|
+
* resolved config or the list of missing keys; the shell renders
|
|
31
|
+
* <AuthConfigError /> when any key is missing.
|
|
29
32
|
*/
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const apiScope = env.VITE_MSAL_API_SCOPE;
|
|
33
|
+
function readMsalConfig(): MsalAuthConfig | { missing: string[] } {
|
|
34
|
+
const clientId = runtimeConfig("VITE_MSAL_CLIENT_ID");
|
|
35
|
+
const authority = runtimeConfig("VITE_MSAL_AUTHORITY");
|
|
36
|
+
const apiScope = runtimeConfig("VITE_MSAL_API_SCOPE");
|
|
35
37
|
const missing = [
|
|
36
38
|
!clientId ? "VITE_MSAL_CLIENT_ID" : null,
|
|
37
39
|
!authority ? "VITE_MSAL_AUTHORITY" : null,
|
|
@@ -39,21 +41,22 @@ function readMsalConfigFromEnv(): MsalAuthConfig | { missing: string[] } {
|
|
|
39
41
|
].filter((v): v is string => v !== null);
|
|
40
42
|
if (missing.length > 0) return { missing };
|
|
41
43
|
return {
|
|
42
|
-
clientId
|
|
43
|
-
authority
|
|
44
|
-
apiScope
|
|
44
|
+
clientId,
|
|
45
|
+
authority,
|
|
46
|
+
apiScope,
|
|
45
47
|
redirectUri: window.location.origin,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
|
-
* Default MSAL-backed auth adapter. Reads VITE_MSAL_* from
|
|
52
|
+
* Default MSAL-backed auth adapter. Reads VITE_MSAL_* from runtime config
|
|
53
|
+
* (`window.__APP_CONFIG__`) with `import.meta.env` fallback for local dev.
|
|
51
54
|
* Customers override via `bootstrapShell({ authProvider: { Provider: ... } })`
|
|
52
55
|
* for tests or non-MSAL deployments.
|
|
53
56
|
*/
|
|
54
57
|
export const defaultAuthAdapter: AuthAdapter = {
|
|
55
58
|
Provider: ({ children }: { children: ReactNode }) => {
|
|
56
|
-
const result =
|
|
59
|
+
const result = readMsalConfig();
|
|
57
60
|
if ("missing" in result) {
|
|
58
61
|
return <AuthConfigError missingKeys={result.missing} />;
|
|
59
62
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { ShellApp } from "./shell-app.tsx";
|
|
|
3
3
|
export { ShellLayout } from "./shell-layout.tsx";
|
|
4
4
|
export { defaultAuthAdapter, ShellAuth } from "./auth-default.tsx";
|
|
5
5
|
export { defaultAgentFetch } from "./service-fetch-default.ts";
|
|
6
|
+
export { runtimeConfig } from "./runtime-config.ts";
|
|
6
7
|
export { defineChatModule } from "./types.ts";
|
|
7
8
|
export type {
|
|
8
9
|
ChatModule,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration reader.
|
|
3
|
+
*
|
|
4
|
+
* The web app ships as a SINGLE prebuilt container image that must boot
|
|
5
|
+
* correctly for any customer / environment without a rebuild. Per-env values
|
|
6
|
+
* (API base URL, MSAL client/authority/scopes) therefore arrive at RUNTIME —
|
|
7
|
+
* the container entrypoint writes a small `/config.js` from env vars that sets
|
|
8
|
+
* `window.__APP_CONFIG__`, loaded before the app bundle.
|
|
9
|
+
*
|
|
10
|
+
* `runtimeConfig(key)` resolves, in order:
|
|
11
|
+
* 1. `window.__APP_CONFIG__[key]` — runtime config injected by the container
|
|
12
|
+
* 2. `import.meta.env[key]` — build-time Vite env (local `pnpm dev`)
|
|
13
|
+
* 3. `""` — absent
|
|
14
|
+
*
|
|
15
|
+
* Keeping `import.meta.env` as the fallback means local dev is unchanged: there
|
|
16
|
+
* is no `/config.js` in dev, so `window.__APP_CONFIG__` is undefined and the
|
|
17
|
+
* existing `VITE_*` values from `.env` are used exactly as before.
|
|
18
|
+
*
|
|
19
|
+
* The window cast is inline (no `declare global`) so this can be duplicated in
|
|
20
|
+
* sibling packages without conflicting global augmentations.
|
|
21
|
+
*/
|
|
22
|
+
export function runtimeConfig(key: string): string {
|
|
23
|
+
const fromWindow =
|
|
24
|
+
typeof window !== "undefined"
|
|
25
|
+
? (window as { __APP_CONFIG__?: Record<string, string | undefined> })
|
|
26
|
+
.__APP_CONFIG__?.[key]
|
|
27
|
+
: undefined;
|
|
28
|
+
const fromEnv = (import.meta.env as Record<string, string | undefined>)[key];
|
|
29
|
+
return fromWindow ?? fromEnv ?? "";
|
|
30
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServiceFetch } from "@iloveagents/foundry-agent";
|
|
2
2
|
import { authStore } from "@iloveagents/foundry-agent/msal";
|
|
3
|
+
import { runtimeConfig } from "./runtime-config.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Default agent fetch client wired to the MSAL token store and the host's
|
|
@@ -21,5 +22,5 @@ export const defaultAgentFetch: typeof fetch = createServiceFetch({
|
|
|
21
22
|
acquireToken: (options) => authStore.getState().getAccessToken("api", options),
|
|
22
23
|
recoverFromHardAuthFailure: (reason) =>
|
|
23
24
|
authStore.getState().recoverFromHardAuthFailure(reason),
|
|
24
|
-
baseUrl:
|
|
25
|
+
baseUrl: runtimeConfig("VITE_API_BASE_URL"),
|
|
25
26
|
});
|
package/src/shell-app.tsx
CHANGED
|
@@ -283,9 +283,39 @@ function RuntimeBody({
|
|
|
283
283
|
prevStickyRef.current = sticky;
|
|
284
284
|
|
|
285
285
|
// Effective id seen by AGUIRuntimeProvider — always defined.
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
286
|
+
//
|
|
287
|
+
// Priority order matters and is intentional:
|
|
288
|
+
// 1. ``urlMatch`` — when the user is on ``/chat/<id>`` the URL is
|
|
289
|
+
// ALWAYS the authoritative conversation. This MUST come before
|
|
290
|
+
// ``sticky`` because of a stale-render race on cross-chat
|
|
291
|
+
// navigation:
|
|
292
|
+
// - User is on ``/chat/A``. ``active-chat-store`` holds ``A``.
|
|
293
|
+
// So ``sticky = A``.
|
|
294
|
+
// - User clicks a Recents row for ``B`` →
|
|
295
|
+
// ``react-router`` updates pathname to ``/chat/B``.
|
|
296
|
+
// - On the synchronous render that follows, ``urlMatch`` is
|
|
297
|
+
// ``B`` (read from ``useLocation``) but
|
|
298
|
+
// ``useTrackActiveChatFromUrl``'s ``useEffect`` hasn't run
|
|
299
|
+
// yet — so ``sticky`` is still ``A``.
|
|
300
|
+
// - With ``sticky ?? urlMatch`` we'd compute
|
|
301
|
+
// ``effectiveThreadId = A``, bind the AG-UI adapter to ``A``,
|
|
302
|
+
// and the first message the user sends would create a NEW
|
|
303
|
+
// row at ``A``'s old thread id while the URL says ``B``.
|
|
304
|
+
// Symptoms: TWO active dots in the sidebar (URL highlights
|
|
305
|
+
// row ``B``, the new row's id activates the dot on a
|
|
306
|
+
// freshly-appeared "Untitled chat"); resume appears to
|
|
307
|
+
// mint a new conversation; messages land on the wrong row.
|
|
308
|
+
// Putting ``urlMatch`` first eliminates the race entirely: when
|
|
309
|
+
// the URL says we're on chat ``B``, the runtime targets ``B``
|
|
310
|
+
// from the very first render, regardless of stale store state.
|
|
311
|
+
// 2. ``sticky`` — for non-chat URLs (``/spaces``, ``/tasks`` …)
|
|
312
|
+
// ``urlMatch`` is ``undefined``. ``sticky`` keeps the popout
|
|
313
|
+
// chat "live" while the user browses elsewhere.
|
|
314
|
+
// 3. ``freshIdRef.current`` — brand-new chat on ``/`` with no
|
|
315
|
+
// sticky and no URL match. Pre-minted so the runtime's
|
|
316
|
+
// ``threadId`` is defined from the first render (no
|
|
317
|
+
// ``undefined → defined`` remount of assistant-ui).
|
|
318
|
+
const effectiveThreadId: string = urlMatch ?? sticky ?? freshIdRef.current;
|
|
289
319
|
|
|
290
320
|
const historyAdapterFactory = useCallback<AGUIHistoryAdapterFactory>(
|
|
291
321
|
(args: AGUIChatConversationFactoryArgs) =>
|