@iloveagents/foundry-web-shell 0.3.1 → 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.
- 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/src/shell-layout.tsx
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { type ReactNode, useEffect } from "react";
|
|
2
|
-
import { Outlet, useLocation } from "react-router";
|
|
3
|
-
import {
|
|
4
|
-
Sidebar,
|
|
5
|
-
ToolPanelLayout,
|
|
6
|
-
ChatHeader,
|
|
7
|
-
ChatBubble,
|
|
8
|
-
ThemeDocumentMetadata,
|
|
9
|
-
ThemeRuntimeProvider,
|
|
10
|
-
ThemeScope,
|
|
11
|
-
GlobalSelectionPopover,
|
|
12
|
-
ChatContent,
|
|
13
|
-
ContextPins,
|
|
14
|
-
TooltipIconButton,
|
|
15
|
-
useAppStore,
|
|
16
|
-
useNavStore,
|
|
17
|
-
findNavItem,
|
|
18
|
-
useChatBubbleStore,
|
|
19
|
-
useThemeStore,
|
|
20
|
-
type ThemeLayer,
|
|
21
|
-
} from "@iloveagents/foundry-web-ui";
|
|
22
|
-
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
23
|
-
import { X } from "lucide-react";
|
|
24
|
-
import type { ChatModule } from "./types.ts";
|
|
25
|
-
|
|
26
|
-
interface ShellLayoutProps {
|
|
27
|
-
modules: ChatModule[];
|
|
28
|
-
baseThemeLayers: ThemeLayer[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Generic app layout — rendered as the outlet for the customer's routes.
|
|
33
|
-
*
|
|
34
|
-
* Each module contributes:
|
|
35
|
-
* - `useInit`: hook body run here so React's rules-of-hooks apply.
|
|
36
|
-
* - `useThemeLayers`: theme layers stacked above `baseThemeLayers`.
|
|
37
|
-
* - `layoutExtras`: rendered as a sibling group inside <ThemeScope>.
|
|
38
|
-
*
|
|
39
|
-
* The shell knows nothing about Spaces — `useSpacesInit`, dialogs, banners
|
|
40
|
-
* all flow through the module protocol.
|
|
41
|
-
*/
|
|
42
|
-
export function ShellLayout({ modules, baseThemeLayers }: ShellLayoutProps) {
|
|
43
|
-
// Module init hooks — called in registration order. Rules-of-hooks
|
|
44
|
-
// require a stable count, so module additions/removals across renders
|
|
45
|
-
// are forbidden (ChatModule[] is meant to be a static prop).
|
|
46
|
-
for (const m of modules) {
|
|
47
|
-
m.useInit?.();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Module theme-layer hooks — flatten and merge above the static base.
|
|
51
|
-
const moduleLayers: ThemeLayer[] = [];
|
|
52
|
-
for (const m of modules) {
|
|
53
|
-
if (m.useThemeLayers) {
|
|
54
|
-
for (const layer of m.useThemeLayers()) {
|
|
55
|
-
moduleLayers.push(layer);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
const themeLayers = [...baseThemeLayers, ...moduleLayers];
|
|
60
|
-
|
|
61
|
-
const themeMode = useThemeStore((s) => s.mode);
|
|
62
|
-
const { pathname } = useLocation();
|
|
63
|
-
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
64
|
-
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
65
|
-
|
|
66
|
-
// Collapse expanded chat when navigating to root (already full-screen there).
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (pathname === "/" && isExpanded) {
|
|
69
|
-
useChatBubbleStore.getState().close();
|
|
70
|
-
}
|
|
71
|
-
}, [pathname, isExpanded]);
|
|
72
|
-
|
|
73
|
-
// Sync route + navigation context to app store.
|
|
74
|
-
const setCurrentPage = useAppStore((s) => s.setCurrentPage);
|
|
75
|
-
const setNavContext = useAppStore((s) => s.setNavContext);
|
|
76
|
-
const navConfig = useNavStore((s) => s.config);
|
|
77
|
-
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
setCurrentPage(pathname);
|
|
80
|
-
const found = findNavItem(navConfig, pathname);
|
|
81
|
-
const mergedInstructions = [found?.groupInstructions, found?.item.contextInstructions]
|
|
82
|
-
.filter(Boolean)
|
|
83
|
-
.join("\n");
|
|
84
|
-
|
|
85
|
-
setNavContext({
|
|
86
|
-
group: found?.group ?? null,
|
|
87
|
-
groupDescription: found?.groupDescription ?? null,
|
|
88
|
-
groupMeta: found?.groupMeta ?? {},
|
|
89
|
-
label: found?.item.label ?? pathname,
|
|
90
|
-
description: found?.item.description ?? null,
|
|
91
|
-
meta: found?.item.meta ?? {},
|
|
92
|
-
contextInstructions: mergedInstructions || null,
|
|
93
|
-
});
|
|
94
|
-
}, [pathname, navConfig, setCurrentPage, setNavContext]);
|
|
95
|
-
|
|
96
|
-
// Page content (Outlet) — always rendered to keep page tools and context alive.
|
|
97
|
-
const page = <Outlet />;
|
|
98
|
-
|
|
99
|
-
// Aggregate module layoutExtras into a single fragment.
|
|
100
|
-
const layoutExtras: ReactNode[] = [];
|
|
101
|
-
for (const m of modules) {
|
|
102
|
-
if (m.layoutExtras) {
|
|
103
|
-
layoutExtras.push(<ModuleExtra key={m.name}>{m.layoutExtras}</ModuleExtra>);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<ThemeRuntimeProvider layers={themeLayers} mode={themeMode}>
|
|
109
|
-
<ThemeScope className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
|
|
110
|
-
<ThemeDocumentMetadata />
|
|
111
|
-
{layoutExtras}
|
|
112
|
-
|
|
113
|
-
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
114
|
-
<Sidebar />
|
|
115
|
-
|
|
116
|
-
{isExpanded ? (
|
|
117
|
-
<ToolPanelLayout>
|
|
118
|
-
<div className="flex h-full min-w-0 flex-1 overflow-hidden">
|
|
119
|
-
<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
|
120
|
-
<ChatHeader />
|
|
121
|
-
<ChatContent />
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
{showPagePanel && (
|
|
125
|
-
<div
|
|
126
|
-
className={cn(
|
|
127
|
-
"hidden h-full shrink-0 border-l border-border bg-background xl:flex xl:flex-col",
|
|
128
|
-
)}
|
|
129
|
-
style={{ width: 600 }}
|
|
130
|
-
>
|
|
131
|
-
<PagePanelActions />
|
|
132
|
-
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">{page}</div>
|
|
133
|
-
</div>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
</ToolPanelLayout>
|
|
137
|
-
) : (
|
|
138
|
-
<ToolPanelLayout>
|
|
139
|
-
<div className="flex flex-1 flex-col overflow-hidden">
|
|
140
|
-
<ChatHeader />
|
|
141
|
-
{page}
|
|
142
|
-
</div>
|
|
143
|
-
</ToolPanelLayout>
|
|
144
|
-
)}
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
<ChatBubble />
|
|
148
|
-
<GlobalSelectionPopover />
|
|
149
|
-
</ThemeScope>
|
|
150
|
-
</ThemeRuntimeProvider>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function ModuleExtra({ children }: { children: ReactNode }) {
|
|
155
|
-
return <>{children}</>;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function PagePanelActions() {
|
|
159
|
-
const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<div className="shrink-0 flex items-center justify-end px-4 py-3 border-b border-border">
|
|
163
|
-
<div className="flex items-center gap-0.5">
|
|
164
|
-
<ContextPins compact flyoutDirection="down" />
|
|
165
|
-
<TooltipIconButton tooltip="Close panel" size="icon" onClick={togglePagePanel}>
|
|
166
|
-
<X className="size-4" />
|
|
167
|
-
</TooltipIconButton>
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import type { ComponentType, ReactNode } from "react";
|
|
2
|
-
import type { ThemeLayer } from "@iloveagents/foundry-web-ui";
|
|
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
|
-
|
|
84
|
-
/** Route entry contributed by a module or the host app. */
|
|
85
|
-
export interface ShellPage {
|
|
86
|
-
path: string;
|
|
87
|
-
element: ReactNode;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* A ChatModule is the unit of composition for `bootstrapShell`. Each field is
|
|
92
|
-
* optional — modules contribute what they need:
|
|
93
|
-
*
|
|
94
|
-
* - `useInit`: React hook body called once per layout render (rules-of-hooks
|
|
95
|
-
* apply). Use for cross-store sync, registry registration, etc.
|
|
96
|
-
* Modules wanting to mutate `useNavStore` config (sidebar nav items)
|
|
97
|
-
* do it inside `useInit` — see Spaces' `useSpacesNavSync()` for the
|
|
98
|
-
* canonical pattern.
|
|
99
|
-
* - `toolUIs`: ReactNode rendered inside `<AGUIRuntimeProvider>`. Use to
|
|
100
|
-
* register `makeAssistantToolUI` instances.
|
|
101
|
-
* - `layoutExtras`: ReactNode rendered inside the shell layout (banners,
|
|
102
|
-
* global dialogs, popovers).
|
|
103
|
-
* - `wrappers`: ComponentType<{children}>[] applied outer→inner from the
|
|
104
|
-
* array. Wrap providers like SpacesQueryProvider here.
|
|
105
|
-
* - `useThemeLayers`: hook returning ThemeLayer[]; merged on top of the
|
|
106
|
-
* static `theme` prop in module-array order.
|
|
107
|
-
* - `pages`: ShellPage[] appended to Routes; customer-supplied `pages` win
|
|
108
|
-
* on path collision.
|
|
109
|
-
* - `fetchInterceptor`: zero-arg installer called once before `createRoot`.
|
|
110
|
-
* Use to install `window.fetch` wrappers.
|
|
111
|
-
*/
|
|
112
|
-
export interface ChatModule {
|
|
113
|
-
name: string;
|
|
114
|
-
useInit?: () => void;
|
|
115
|
-
toolUIs?: ReactNode;
|
|
116
|
-
layoutExtras?: ReactNode;
|
|
117
|
-
wrappers?: ComponentType<{ children: ReactNode }>[];
|
|
118
|
-
useThemeLayers?: () => ThemeLayer[];
|
|
119
|
-
pages?: ShellPage[];
|
|
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;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Adapter for plugging in an auth Provider. The Provider renders children
|
|
146
|
-
* once authenticated and is responsible for resolving its own config
|
|
147
|
-
* (the default reads MSAL settings from `import.meta.env`). Tests and
|
|
148
|
-
* non-MSAL deployments override with a different Provider.
|
|
149
|
-
*/
|
|
150
|
-
export interface AuthAdapter {
|
|
151
|
-
Provider: ComponentType<{ children: ReactNode }>;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export interface BootstrapShellOptions {
|
|
155
|
-
modules?: ChatModule[];
|
|
156
|
-
/** Customer-supplied pages — win over module pages on `path` collision. */
|
|
157
|
-
pages?: ShellPage[];
|
|
158
|
-
/** Static base theme layers; modules' useThemeLayers stack on top. */
|
|
159
|
-
theme?: ThemeLayer[];
|
|
160
|
-
/** Override default MSAL-backed auth provider (e.g. for tests). */
|
|
161
|
-
authProvider?: AuthAdapter;
|
|
162
|
-
/** Wrap the tree in <StrictMode>. Default: true. */
|
|
163
|
-
strictMode?: boolean;
|
|
164
|
-
/** DOM element to mount into. Default: document.getElementById("root"). */
|
|
165
|
-
rootElement?: HTMLElement;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** Pass-through helper that adds nothing at runtime — pure type marker. */
|
|
169
|
-
export function defineChatModule(m: ChatModule): ChatModule {
|
|
170
|
-
return m;
|
|
171
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"jsx": "react-jsx",
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"resolveJsonModule": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"allowImportingTsExtensions": true,
|
|
12
|
-
"noEmit": true,
|
|
13
|
-
"types": ["vite/client"]
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*"],
|
|
16
|
-
"exclude": ["node_modules"]
|
|
17
|
-
}
|