@iloveagents/foundry-web-ui 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.
Files changed (223) hide show
  1. package/README.md +14 -5
  2. package/dist/components/ag-ui-runtime-provider.d.ts +64 -0
  3. package/dist/components/ag-ui-runtime-provider.js +71 -0
  4. package/dist/components/app-brand.d.ts +6 -0
  5. package/dist/components/app-brand.js +13 -0
  6. package/dist/components/assistant-chat.d.ts +26 -0
  7. package/dist/components/assistant-chat.js +93 -0
  8. package/dist/components/chat-attachments.d.ts +25 -0
  9. package/dist/components/chat-attachments.js +20 -0
  10. package/dist/components/chat-bubble.d.ts +1 -0
  11. package/dist/components/chat-bubble.js +103 -0
  12. package/dist/components/chat-header.d.ts +2 -0
  13. package/dist/components/chat-header.js +16 -0
  14. package/dist/components/client-tool-executor.d.ts +12 -0
  15. package/dist/components/client-tool-executor.js +108 -0
  16. package/dist/components/collection-empty-state.d.ts +9 -0
  17. package/dist/components/collection-empty-state.js +6 -0
  18. package/dist/components/collection-filter-bar.d.ts +19 -0
  19. package/dist/components/collection-filter-bar.js +28 -0
  20. package/dist/components/collection-search-input.d.ts +6 -0
  21. package/dist/components/collection-search-input.js +8 -0
  22. package/dist/components/collection-skeleton.d.ts +6 -0
  23. package/dist/components/collection-skeleton.js +10 -0
  24. package/dist/components/collection-sort-menu.d.ts +10 -0
  25. package/dist/components/collection-sort-menu.js +9 -0
  26. package/dist/components/collection-surface.d.ts +8 -0
  27. package/dist/components/collection-surface.js +5 -0
  28. package/dist/components/collection-toolbar.d.ts +9 -0
  29. package/dist/components/collection-toolbar.js +5 -0
  30. package/dist/components/collection-view-toggle.d.ts +7 -0
  31. package/dist/components/collection-view-toggle.js +17 -0
  32. package/dist/components/composer-submit-bridge.d.ts +1 -0
  33. package/dist/components/composer-submit-bridge.js +15 -0
  34. package/dist/components/confirm-dialog.d.ts +11 -0
  35. package/dist/components/confirm-dialog.js +6 -0
  36. package/dist/components/confirmation-card.d.ts +14 -0
  37. package/dist/components/confirmation-card.js +12 -0
  38. package/dist/components/context-badges.d.ts +16 -0
  39. package/dist/components/context-badges.js +56 -0
  40. package/dist/components/context-bar.d.ts +41 -0
  41. package/dist/components/context-bar.js +123 -0
  42. package/dist/components/form-dialog.d.ts +12 -0
  43. package/dist/components/form-dialog.js +13 -0
  44. package/dist/components/global-selection-popover.d.ts +7 -0
  45. package/dist/components/global-selection-popover.js +167 -0
  46. package/dist/components/infinite-scroll-sentinel.d.ts +6 -0
  47. package/dist/components/infinite-scroll-sentinel.js +25 -0
  48. package/dist/components/json-viewer.d.ts +10 -0
  49. package/dist/components/json-viewer.js +80 -0
  50. package/dist/components/loading-indicator.d.ts +4 -0
  51. package/dist/components/loading-indicator.js +22 -0
  52. package/dist/components/markdown-text.d.ts +13 -0
  53. package/dist/components/markdown-text.js +221 -0
  54. package/dist/components/name-dialog.d.ts +16 -0
  55. package/dist/components/name-dialog.js +23 -0
  56. package/dist/components/selection-popover.d.ts +12 -0
  57. package/dist/components/selection-popover.js +111 -0
  58. package/dist/components/show-document-tool-ui.d.ts +1 -0
  59. package/dist/components/show-document-tool-ui.js +53 -0
  60. package/dist/components/sidebar.d.ts +4 -0
  61. package/dist/components/sidebar.js +904 -0
  62. package/dist/components/surface-card.d.ts +9 -0
  63. package/dist/components/surface-card.js +33 -0
  64. package/dist/components/theme-runtime-provider.d.ts +15 -0
  65. package/dist/components/theme-runtime-provider.js +48 -0
  66. package/dist/components/theme-toggle.d.ts +2 -0
  67. package/dist/components/theme-toggle.js +19 -0
  68. package/dist/components/tool-call-card.d.ts +27 -0
  69. package/dist/components/tool-call-card.js +13 -0
  70. package/dist/components/tool-fallback.d.ts +26 -0
  71. package/dist/components/tool-fallback.js +129 -0
  72. package/dist/components/tool-panel-layout.d.ts +10 -0
  73. package/dist/components/tool-panel-layout.js +12 -0
  74. package/dist/components/tool-panel.d.ts +1 -0
  75. package/dist/components/tool-panel.js +89 -0
  76. package/dist/components/tooltip-icon-button.d.ts +7 -0
  77. package/dist/components/tooltip-icon-button.js +9 -0
  78. package/dist/components/user-menu.d.ts +2 -0
  79. package/dist/components/user-menu.js +14 -0
  80. package/dist/index.d.ts +68 -0
  81. package/dist/index.js +83 -0
  82. package/dist/lib/ag-ui-adapter.d.ts +29 -0
  83. package/dist/lib/ag-ui-adapter.js +373 -0
  84. package/dist/lib/app-store.d.ts +115 -0
  85. package/dist/lib/app-store.js +199 -0
  86. package/dist/lib/attachment-adapter.d.ts +20 -0
  87. package/dist/lib/attachment-adapter.js +99 -0
  88. package/dist/lib/auth-provider.d.ts +16 -0
  89. package/dist/lib/auth-provider.js +88 -0
  90. package/dist/lib/chat-bubble-store.d.ts +30 -0
  91. package/dist/lib/chat-bubble-store.js +23 -0
  92. package/dist/lib/chat-lifecycle-store.d.ts +10 -0
  93. package/{src/lib/chat-lifecycle-store.ts → dist/lib/chat-lifecycle-store.js} +3 -13
  94. package/dist/lib/client-tools.d.ts +11 -0
  95. package/dist/lib/client-tools.js +274 -0
  96. package/dist/lib/composer-submit-store.d.ts +12 -0
  97. package/dist/lib/composer-submit-store.js +17 -0
  98. package/dist/lib/dev-store.d.ts +38 -0
  99. package/dist/lib/dev-store.js +32 -0
  100. package/dist/lib/nav-config.d.ts +159 -0
  101. package/dist/lib/nav-config.js +162 -0
  102. package/dist/lib/nav-dnd.d.ts +8 -0
  103. package/dist/lib/nav-dnd.js +9 -0
  104. package/dist/lib/nav-store.d.ts +45 -0
  105. package/dist/lib/nav-store.js +49 -0
  106. package/dist/lib/selection-context.d.ts +21 -0
  107. package/dist/lib/selection-context.js +34 -0
  108. package/dist/lib/sidebar-store.d.ts +23 -0
  109. package/dist/lib/sidebar-store.js +98 -0
  110. package/dist/lib/theme-runtime.d.ts +130 -0
  111. package/dist/lib/theme-runtime.js +479 -0
  112. package/dist/lib/theme-store.d.ts +15 -0
  113. package/dist/lib/theme-store.js +53 -0
  114. package/dist/lib/tool-panel-store.d.ts +48 -0
  115. package/dist/lib/tool-panel-store.js +69 -0
  116. package/dist/lib/use-new-conversation.d.ts +8 -0
  117. package/dist/lib/use-new-conversation.js +62 -0
  118. package/dist/lib/use-page-tools.d.ts +27 -0
  119. package/{src/lib/use-page-tools.ts → dist/lib/use-page-tools.js} +11 -13
  120. package/dist/styles.css +142 -0
  121. package/dist/ui/collapsible.d.ts +5 -0
  122. package/{src/ui/collapsible.tsx → dist/ui/collapsible.js} +0 -2
  123. package/dist/ui/dialog.d.ts +15 -0
  124. package/dist/ui/dialog.js +25 -0
  125. package/dist/ui/dropdown-menu.d.ts +7 -0
  126. package/dist/ui/dropdown-menu.js +19 -0
  127. package/dist/ui/popover.d.ts +7 -0
  128. package/dist/ui/popover.js +10 -0
  129. package/dist/ui/select.d.ts +4 -0
  130. package/dist/ui/select.js +9 -0
  131. package/dist/ui/tooltip.d.ts +7 -0
  132. package/dist/ui/tooltip.js +10 -0
  133. package/package.json +31 -12
  134. package/AGENTS.md +0 -59
  135. package/CHANGELOG.md +0 -203
  136. package/CLAUDE.md +0 -1
  137. package/src/__tests__/no-spaces-imports.test.ts +0 -59
  138. package/src/__tests__/open-core-guards.test.ts +0 -307
  139. package/src/__tests__/theme-runtime.test.ts +0 -81
  140. package/src/__tests__/tool-fallback.test.tsx +0 -109
  141. package/src/components/__tests__/context-bar.test.ts +0 -112
  142. package/src/components/ag-ui-runtime-provider.tsx +0 -170
  143. package/src/components/app-brand.tsx +0 -38
  144. package/src/components/assistant-chat.tsx +0 -461
  145. package/src/components/chat-attachments.tsx +0 -68
  146. package/src/components/chat-bubble.tsx +0 -373
  147. package/src/components/chat-header.tsx +0 -44
  148. package/src/components/client-tool-executor.tsx +0 -230
  149. package/src/components/collection-empty-state.tsx +0 -37
  150. package/src/components/collection-filter-bar.tsx +0 -168
  151. package/src/components/collection-search-input.tsx +0 -41
  152. package/src/components/collection-skeleton.tsx +0 -55
  153. package/src/components/collection-sort-menu.tsx +0 -62
  154. package/src/components/collection-surface.tsx +0 -46
  155. package/src/components/collection-toolbar.tsx +0 -33
  156. package/src/components/collection-view-toggle.tsx +0 -61
  157. package/src/components/composer-submit-bridge.tsx +0 -19
  158. package/src/components/confirm-dialog.tsx +0 -49
  159. package/src/components/confirmation-card.tsx +0 -32
  160. package/src/components/context-badges.tsx +0 -136
  161. package/src/components/context-bar.tsx +0 -238
  162. package/src/components/form-dialog.tsx +0 -56
  163. package/src/components/global-selection-popover.tsx +0 -266
  164. package/src/components/infinite-scroll-sentinel.tsx +0 -48
  165. package/src/components/json-viewer.tsx +0 -232
  166. package/src/components/loading-indicator.tsx +0 -40
  167. package/src/components/markdown-text-citations.test.tsx +0 -108
  168. package/src/components/markdown-text.tsx +0 -508
  169. package/src/components/name-dialog.tsx +0 -87
  170. package/src/components/selection-popover.tsx +0 -156
  171. package/src/components/show-document-tool-ui.tsx +0 -90
  172. package/src/components/sidebar.test.tsx +0 -611
  173. package/src/components/sidebar.tsx +0 -1689
  174. package/src/components/surface-card.tsx +0 -45
  175. package/src/components/theme-runtime-provider.tsx +0 -93
  176. package/src/components/theme-toggle.tsx +0 -27
  177. package/src/components/tool-call-card.tsx +0 -126
  178. package/src/components/tool-fallback.tsx +0 -169
  179. package/src/components/tool-panel-layout.tsx +0 -36
  180. package/src/components/tool-panel.tsx +0 -233
  181. package/src/components/tooltip-icon-button.tsx +0 -28
  182. package/src/components/user-menu.tsx +0 -60
  183. package/src/index.ts +0 -185
  184. package/src/lib/__tests__/ag-ui-adapter.test.ts +0 -273
  185. package/src/lib/__tests__/app-store.test.ts +0 -405
  186. package/src/lib/__tests__/attachment-adapter.test.ts +0 -48
  187. package/src/lib/__tests__/chat-bubble-store.test.ts +0 -67
  188. package/src/lib/__tests__/client-tools.test.ts +0 -124
  189. package/src/lib/__tests__/dev-store.test.ts +0 -78
  190. package/src/lib/__tests__/nav-config.test.ts +0 -135
  191. package/src/lib/__tests__/nav-dnd.test.ts +0 -24
  192. package/src/lib/__tests__/nav-store.test.ts +0 -59
  193. package/src/lib/__tests__/selection-context.test.ts +0 -114
  194. package/src/lib/__tests__/sidebar-store.test.ts +0 -144
  195. package/src/lib/__tests__/theme-store.test.ts +0 -83
  196. package/src/lib/__tests__/tool-panel-store.test.ts +0 -61
  197. package/src/lib/__tests__/use-page-context.test.ts +0 -58
  198. package/src/lib/ag-ui-adapter.ts +0 -412
  199. package/src/lib/app-store.ts +0 -361
  200. package/src/lib/attachment-adapter.ts +0 -92
  201. package/src/lib/auth-provider.tsx +0 -144
  202. package/src/lib/chat-bubble-store.ts +0 -46
  203. package/src/lib/client-tools.ts +0 -293
  204. package/src/lib/composer-submit-store.ts +0 -32
  205. package/src/lib/dev-store.ts +0 -69
  206. package/src/lib/nav-config.ts +0 -329
  207. package/src/lib/nav-dnd.ts +0 -14
  208. package/src/lib/nav-store.ts +0 -89
  209. package/src/lib/selection-context.ts +0 -65
  210. package/src/lib/sidebar-store.ts +0 -113
  211. package/src/lib/theme-runtime.ts +0 -660
  212. package/src/lib/theme-store.ts +0 -67
  213. package/src/lib/tool-panel-store.ts +0 -121
  214. package/src/lib/use-new-conversation.test.tsx +0 -168
  215. package/src/lib/use-new-conversation.ts +0 -64
  216. package/src/ui/dialog.tsx +0 -109
  217. package/src/ui/dropdown-menu.tsx +0 -75
  218. package/src/ui/popover.tsx +0 -34
  219. package/src/ui/select.tsx +0 -15
  220. package/src/ui/tooltip.tsx +0 -30
  221. package/src/vite-env.d.ts +0 -1
  222. package/tsconfig.json +0 -16
  223. package/vitest.config.ts +0 -8
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # @iloveagents/foundry-web-ui
2
2
 
3
- React UI core for the AG-UI protocol agent starter. Provides hooks, stores, extension-point registries, the AG-UI adapter, ChatPanel, sidebar, and layout components. Depends on `@iloveagents/foundry-web-primitives` for leaf primitives (Button, Input, Field, Checkbox, Textarea, `cn`).
3
+ React UI core for Foundry UI apps.
4
+
5
+ `web-ui` is the package that renders the actual agent experience: chat UI,
6
+ composer, AG-UI adapter, message rendering, tool-call cards, side panels,
7
+ sidebar, theme runtime, context pins, attachment handling, and UI stores. It
8
+ depends on `@iloveagents/foundry-web-primitives` for leaf primitives (Button,
9
+ Input, Field, Checkbox, Textarea, `cn`).
4
10
 
5
11
  ## Install
6
12
 
@@ -22,11 +28,14 @@ import {
22
28
  import { cn } from "@iloveagents/foundry-web-primitives";
23
29
  ```
24
30
 
25
- Consumed as TypeScript source (no build step). Requires Tailwind v4 `@source` directives for both packages:
31
+ This package publishes built ESM JavaScript and `.d.ts` declarations. Host apps
32
+ must include Tailwind v4 `@source` directives so classes used inside package
33
+ components are generated:
26
34
 
27
35
  ```css
28
- @source "../../packages/web-primitives/src";
29
- @source "../../packages/web-ui/src";
36
+ @import "@iloveagents/foundry-web-ui/styles.css";
37
+ @source "../node_modules/@iloveagents/foundry-web-primitives/dist";
38
+ @source "../node_modules/@iloveagents/foundry-web-ui/dist";
30
39
  ```
31
40
 
32
41
  ## Extension Points
@@ -36,6 +45,6 @@ Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via regist
36
45
  - **NavItem.dnd** — drag-and-drop behavior on sidebar items
37
46
  - **Panel renderer registry** — custom content renderers for the side panel
38
47
  - **Citation handler** — handles `[n]` citation clicks in markdown
39
- - **Tool UI** — custom rendering for agent tools via `useAssistantToolUI`
48
+ - **Tool UI** — custom rendering for agent tools via `makeAssistantToolUI`
40
49
 
41
50
  See [AGENTS.md](./AGENTS.md) for architecture details and import boundary rules.
@@ -0,0 +1,64 @@
1
+ import type { ThreadHistoryAdapter } from "@assistant-ui/react";
2
+ import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
3
+ /**
4
+ * Per-mount inputs the host shell passes when it wants a chat
5
+ * conversation history wired to the runtime. Mirrors
6
+ * :type:`ChatConversationFactoryArgs` in ``foundry-web-shell`` —
7
+ * duplicated here (rather than imported) so the web-ui package stays
8
+ * independent of the shell package's dependency graph.
9
+ */
10
+ export interface AGUIChatConversationFactoryArgs {
11
+ /** Conversation id captured from the URL (resumed chat) or undefined (fresh). */
12
+ urlMatch?: string;
13
+ /** Stable AG-UI adapter thread id — present in both cases. */
14
+ aguiThreadId: string;
15
+ }
16
+ /**
17
+ * Factory the consumer passes to build a :type:`ThreadHistoryAdapter`
18
+ * once the AG-UI adapter has minted its thread id. Called inside the
19
+ * keyed runtime mount; assistant-ui invokes ``load()``/``append()``
20
+ * via the returned adapter.
21
+ */
22
+ export type AGUIHistoryAdapterFactory = (args: AGUIChatConversationFactoryArgs) => ThreadHistoryAdapter;
23
+ interface AGUIRuntimeProviderProps {
24
+ children: React.ReactNode;
25
+ /** Custom fetch for agent requests. Handles URL rewriting + auth in production. */
26
+ fetchFn?: typeof fetch;
27
+ /**
28
+ * Optional stable thread id for the AG-UI adapter. When set, the
29
+ * agent's POST body carries this id so server-side persistence
30
+ * (Foundry thread, conversation row) targets the same conversation
31
+ * across reloads / resumes. Changing the id remounts the inner
32
+ * runtime — past messages from a different thread don't bleed
33
+ * into the new one.
34
+ */
35
+ threadId?: string;
36
+ /**
37
+ * Conversation id captured from the current URL when the user
38
+ * resumed an existing chat (e.g. ``/chat/<id>``), or ``undefined``
39
+ * for a brand-new chat. Distinct from ``threadId``: the host shell
40
+ * MAY set ``threadId`` from a non-URL source (a sticky active
41
+ * conversation, a freshly-minted UUID) so ``threadId`` alone can't
42
+ * tell a history adapter "we're resuming". ``urlMatch`` carries
43
+ * that signal verbatim into ``historyAdapterFactory`` so the
44
+ * adapter can decide whether to load history (resume) or
45
+ * initialise empty (fresh) without inferring from ``threadId``.
46
+ */
47
+ urlMatch?: string;
48
+ /**
49
+ * Factory that builds the assistant-ui ``ThreadHistoryAdapter`` for
50
+ * this mount. Called once after the AG-UI adapter exists so it can
51
+ * receive both the URL match (when resuming) and the adapter's own
52
+ * thread id (always set — covers fresh chats). Pass alongside
53
+ * ``threadId`` for resumed chats; pass without ``threadId`` to get
54
+ * a fresh-chat runtime that still persists.
55
+ */
56
+ historyAdapterFactory?: AGUIHistoryAdapterFactory;
57
+ }
58
+ interface AGUIContextValue {
59
+ adapter: AGUIAdapterSDK;
60
+ resetThread: () => void;
61
+ }
62
+ export declare function useAGUIAdapter(): AGUIContextValue;
63
+ export declare function AGUIRuntimeProvider(props: AGUIRuntimeProviderProps): import("react/jsx-runtime").JSX.Element;
64
+ export {};
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useCallback, useMemo } from "react";
3
+ import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
4
+ import { TooltipProvider } from "../ui/tooltip.js";
5
+ import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
6
+ import { FileAttachmentAdapter } from "../lib/attachment-adapter.js";
7
+ import { ShowDocumentToolUI } from "./show-document-tool-ui.js";
8
+ import { ClientToolExecutor } from "./client-tool-executor.js";
9
+ const AGUIAdapterContext = createContext(null);
10
+ export function useAGUIAdapter() {
11
+ const context = useContext(AGUIAdapterContext);
12
+ if (!context) {
13
+ throw new Error("useAGUIAdapter must be used within AGUIRuntimeProvider");
14
+ }
15
+ return context;
16
+ }
17
+ export function AGUIRuntimeProvider(props) {
18
+ // Splitting render across an outer wrapper + a keyed inner is the
19
+ // canonical React idiom for "rebuild this stateful subtree when X
20
+ // changes". assistant-ui's runtime is created inside useLocalRuntime
21
+ // and tracks its messages internally; swapping threadId or history
22
+ // mid-life would leave stale state. Keying the inner subtree on
23
+ // threadId (+ the manual reset counter) guarantees a clean runtime
24
+ // per conversation, matching the ChatGPT-style "click → fresh chat
25
+ // backed by past messages" UX.
26
+ const [resetKey, setResetKey] = useState(0);
27
+ const resetThread = useCallback(() => setResetKey((p) => p + 1), []);
28
+ const innerKey = `${props.threadId ?? "fresh"}:${resetKey}`;
29
+ return (_jsx(AGUIRuntimeInner, { ...props, resetThread: resetThread }, innerKey));
30
+ }
31
+ function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapterFactory, resetThread, }) {
32
+ const adapter = useMemo(() => new AGUIAdapterSDK("/api/agent",
33
+ // Only pass options if at least one is set, mirroring the prior
34
+ // single-arg semantics for the "fresh chat, no overrides" case.
35
+ fetchFn || threadId ? { fetchFn, threadId } : undefined), [fetchFn, threadId]);
36
+ const attachmentAdapter = useMemo(() => new FileAttachmentAdapter(), []);
37
+ // Build the history adapter ONCE per inner mount, after the AG-UI
38
+ // adapter exists so we know its thread id. The keyed-remount in the
39
+ // outer wrapper ensures threadId changes trigger a fresh adapter +
40
+ // fresh history adapter together, so resumed chats hydrate from the
41
+ // right conversation and fresh chats persist under their assistant-
42
+ // ui-minted UUID.
43
+ const historyAdapter = useMemo(() => {
44
+ if (!historyAdapterFactory)
45
+ return undefined;
46
+ return historyAdapterFactory({
47
+ // ``urlMatch`` is the URL-derived resume signal (undefined for
48
+ // fresh chats). ``aguiThreadId`` is always defined — the AG-UI
49
+ // adapter mints it on construction. The factory uses ``urlMatch``
50
+ // to decide "load history" vs "initialise empty", and
51
+ // ``aguiThreadId`` to identify the row to write into.
52
+ urlMatch,
53
+ aguiThreadId: adapter.threadId,
54
+ });
55
+ // adapter is stable for the life of this inner mount (keyed remount
56
+ // covers the threadId change case), so it's OK to depend on the
57
+ // factory + urlMatch here.
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [historyAdapterFactory, urlMatch]);
60
+ const runtime = useLocalRuntime(adapter, {
61
+ adapters: {
62
+ attachments: attachmentAdapter,
63
+ // history is optional — when undefined, the runtime acts as a
64
+ // pure in-memory thread (the original behaviour). When provided,
65
+ // assistant-ui calls .load() on mount to seed past messages and
66
+ // .append() after every turn.
67
+ ...(historyAdapter ? { history: historyAdapter } : {}),
68
+ },
69
+ });
70
+ return (_jsx(AGUIAdapterContext.Provider, { value: { adapter, resetThread }, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsxs(TooltipProvider, { children: [_jsx(ShowDocumentToolUI, {}), _jsx(ClientToolExecutor, {}), children] }) }) }));
71
+ }
@@ -0,0 +1,6 @@
1
+ export declare function AppBrand({ showName, className, iconClassName, labelClassName, }: {
2
+ showName?: boolean;
3
+ className?: string;
4
+ iconClassName?: string;
5
+ labelClassName?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Layers3 } from "lucide-react";
3
+ import { cn } from "@iloveagents/foundry-web-primitives";
4
+ import { useThemeRuntime } from "./theme-runtime-provider.js";
5
+ export function AppBrand({ showName = true, className, iconClassName, labelClassName, }) {
6
+ const runtime = useThemeRuntime();
7
+ const brandName = runtime.branding.appName || "Foundry UI";
8
+ const logoUrl = runtime.mode === "dark"
9
+ ? runtime.branding.darkLogoUrl || runtime.branding.logoUrl
10
+ : runtime.branding.logoUrl || runtime.branding.darkLogoUrl;
11
+ const shouldShowName = showName && !logoUrl;
12
+ return (_jsxs("span", { className: cn("flex items-center gap-2", className), children: [logoUrl ? (_jsx("img", { src: logoUrl, alt: brandName, className: cn("h-5 w-auto max-w-[7rem] shrink-0 object-contain", iconClassName) })) : (_jsx(Layers3, { className: cn("size-5 shrink-0 text-primary", iconClassName) })), shouldShowName && _jsx("span", { className: cn("truncate", labelClassName), children: brandName })] }));
13
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Default starter prompts shown on empty state. Override per-app by passing
3
+ * a `starterSuggestions` prop to `<ChatContent>`. An empty array (`[]`)
4
+ * suppresses the suggestion buttons entirely.
5
+ */
6
+ export declare const DEFAULT_STARTER_SUGGESTIONS: string[];
7
+ export interface ChatContentProps {
8
+ /**
9
+ * Starter prompts shown when the thread is empty. Defaults to
10
+ * `DEFAULT_STARTER_SUGGESTIONS`. Pass an empty array to hide the
11
+ * suggestion buttons entirely. Customer apps (e.g. ANDRITZ Contracts)
12
+ * can pass a workspace-aware list derived from app context.
13
+ */
14
+ starterSuggestions?: string[];
15
+ /**
16
+ * Show the page pin button in the composer. Defaults to false so generic
17
+ * starter apps only show attachment, input, and send controls. Apps with
18
+ * page-aware workflows can opt in explicitly.
19
+ */
20
+ showPinButton?: boolean;
21
+ }
22
+ /**
23
+ * Reusable chat content — used by ChatPage and ChatBubble.
24
+ * Renders inside a ThreadPrimitive.Root context.
25
+ */
26
+ export declare function ChatContent({ starterSuggestions, showPinButton, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useRef, useCallback } from "react";
3
+ import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, ErrorPrimitive, useAuiState, useComposerRuntime, useThreadViewport, } from "@assistant-ui/react";
4
+ import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot, Paperclip, Minimize2, PanelRightClose, PanelRightOpen, } from "lucide-react";
5
+ import { cn } from "@iloveagents/foundry-web-primitives";
6
+ import { Button } from "@iloveagents/foundry-web-primitives";
7
+ import { TooltipIconButton } from "./tooltip-icon-button.js";
8
+ import { userAttachmentComponents, composerAttachmentComponents } from "./chat-attachments.js";
9
+ import { SentContextBadges, ComposerContextBadges } from "./context-badges.js";
10
+ import { ContextPins } from "./context-bar.js";
11
+ import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
12
+ import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
13
+ import { MarkdownText } from "./markdown-text.js";
14
+ import { ToolFallback } from "./tool-fallback.js";
15
+ import { LoadingIndicator, LoadingBar } from "./loading-indicator.js";
16
+ const BranchPicker = () => (_jsxs(BranchPickerPrimitive.Root, { hideWhenSingleBranch: true, className: "inline-flex items-center gap-1 text-xs", children: [_jsx(BranchPickerPrimitive.Previous, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Previous", size: "icon", children: _jsx(ChevronLeft, { className: "size-4" }) }) }), _jsxs("span", { className: "text-muted-foreground", children: [_jsx(BranchPickerPrimitive.Number, {}), " / ", _jsx(BranchPickerPrimitive.Count, {})] }), _jsx(BranchPickerPrimitive.Next, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Next", size: "icon", children: _jsx(ChevronRight, { className: "size-4" }) }) })] }));
17
+ const UserMessage = () => {
18
+ const messageId = useAuiState((s) => s.message.id);
19
+ return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(MessagePrimitive.Attachments, { components: userAttachmentComponents }), _jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: cn("max-w-[80%]", "bg-muted rounded-2xl", "px-4 py-3", "text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-8 items-center justify-end gap-2 pr-2", children: [_jsx(BranchPicker, {}), _jsx("div", { className: "flex h-8 w-10 items-center justify-end", children: _jsx(UserActionBar, {}) })] })] }) }));
20
+ };
21
+ const UserActionBar = () => (_jsx(ActionBarPrimitive.Root, { hideWhenRunning: true, className: "flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(ActionBarPrimitive.Edit, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Edit", size: "icon", children: _jsx(PencilIcon, { className: "size-4" }) }) }) }));
22
+ const AssistantMessage = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsx("div", { className: "text-foreground leading-relaxed flex flex-col gap-4", children: _jsx(MessagePrimitive.Parts, { components: {
23
+ Text: MarkdownText,
24
+ tools: { Fallback: ToolFallback },
25
+ } }) }), _jsx(MessageError, {}), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { className: "flex h-8 w-[4.25rem] items-center", children: _jsx(AssistantActionBar, {}) }), _jsx(BranchPicker, {})] })] }) }));
26
+ const AssistantActionBar = () => (_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 -ml-2 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: [_jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-4" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-4 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Refresh", size: "icon", children: _jsx(RefreshCw, { className: "size-4" }) }) })] }));
27
+ const MessageError = () => (_jsx(MessagePrimitive.Error, { children: _jsx(ErrorPrimitive.Root, { className: "mt-2 rounded-lg border border-destructive bg-destructive/10 p-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-destructive", children: "Error:" }), _jsx(ErrorPrimitive.Message, { className: "text-sm text-destructive" })] }) }) }));
28
+ const EditComposer = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2", children: _jsx("div", { className: "flex items-start justify-end", children: _jsx("div", { className: "w-full max-w-[80%]", children: _jsxs(ComposerPrimitive.Root, { className: cn("flex flex-col gap-2", "rounded-2xl border border-input", "bg-background shadow-sm", "px-4 py-3"), children: [_jsx(ComposerPrimitive.Input, { autoFocus: true, className: cn("w-full bg-transparent", "text-base text-foreground", "outline-none resize-none", "placeholder:text-muted-foreground") }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", children: "Cancel" }) }), _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "sm", children: "Update" }) })] })] }) }) }) }));
29
+ const ComposerActionButton = () => {
30
+ return (_jsxs(_Fragment, { children: [_jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Stop generating", children: _jsx(Square, { className: "size-4" }) }) }) }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Send message", children: _jsx(ArrowUp, { className: "size-5" }) }) }) })] }));
31
+ };
32
+ /**
33
+ * Default starter prompts shown on empty state. Override per-app by passing
34
+ * a `starterSuggestions` prop to `<ChatContent>`. An empty array (`[]`)
35
+ * suppresses the suggestion buttons entirely.
36
+ */
37
+ export const DEFAULT_STARTER_SUGGESTIONS = [
38
+ "Say hello world",
39
+ "What tools can you use?",
40
+ "Help me get started",
41
+ ];
42
+ const ScrollToBottomButton = () => {
43
+ const isAtBottom = useThreadViewport((s) => s.isAtBottom);
44
+ const scrollToBottom = useThreadViewport((s) => s.scrollToBottom);
45
+ if (isAtBottom)
46
+ return null;
47
+ return (_jsx(Button, { variant: "outline", size: "icon", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10 rounded-full shadow-md", onClick: () => scrollToBottom({ behavior: "smooth" }), children: _jsx(ChevronDown, { className: "size-4" }) }));
48
+ };
49
+ const DropZone = ({ children }) => {
50
+ const [isDragging, setIsDragging] = useState(false);
51
+ const dragCounter = useRef(0);
52
+ const composerRuntime = useComposerRuntime();
53
+ const handleDragEnter = useCallback((e) => {
54
+ e.preventDefault();
55
+ dragCounter.current++;
56
+ if (dragCounter.current === 1)
57
+ setIsDragging(true);
58
+ }, []);
59
+ const handleDragLeave = useCallback((e) => {
60
+ e.preventDefault();
61
+ dragCounter.current--;
62
+ if (dragCounter.current === 0)
63
+ setIsDragging(false);
64
+ }, []);
65
+ const handleDragOver = useCallback((e) => {
66
+ e.preventDefault();
67
+ }, []);
68
+ const handleDrop = useCallback(async (e) => {
69
+ e.preventDefault();
70
+ dragCounter.current = 0;
71
+ setIsDragging(false);
72
+ const files = Array.from(e.dataTransfer.files);
73
+ for (const file of files) {
74
+ await composerRuntime.addAttachment(file);
75
+ }
76
+ }, [composerRuntime]);
77
+ return (_jsxs("div", { className: "relative flex flex-col flex-1 overflow-hidden", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [children, isDragging && (_jsx("div", { className: cn("absolute inset-0 z-50 flex items-center justify-center pointer-events-none", "bg-background/80 backdrop-blur-sm", "border-2 border-dashed border-primary rounded-xl"), children: _jsx("p", { className: "text-lg text-primary font-medium", children: "Drop files here" }) }))] }));
78
+ };
79
+ /**
80
+ * Reusable chat content — used by ChatPage and ChatBubble.
81
+ * Renders inside a ThreadPrimitive.Root context.
82
+ */
83
+ export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = false, } = {}) {
84
+ const isExpanded = useChatBubbleStore((s) => s.isExpanded);
85
+ const collapseChat = useChatBubbleStore((s) => s.collapse);
86
+ const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
87
+ const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
88
+ return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto bg-background", children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8", children: [_jsx(ThreadPrimitive.Messages, { components: {
89
+ UserMessage,
90
+ EditComposer,
91
+ AssistantMessage,
92
+ } }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 bg-background", children: [_jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsx("div", { className: "mx-auto max-w-3xl px-4 py-4", children: _jsxs(ComposerPrimitive.Root, { className: cn("flex flex-col gap-2", "rounded-3xl border border-input", "bg-background shadow-sm", "px-4 py-2", "focus-within:border-primary/50 focus-within:shadow-md", "transition-all duration-300"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center -space-x-1 shrink-0", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Attach file", variant: "ghost", className: "shrink-0", children: _jsx(Paperclip, { className: "size-5" }) }) }), _jsx(ContextPins, { showPinButton: showPinButton })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: "Message Assistant...", rows: 1, className: cn("flex-1 bg-transparent", "py-2 text-base", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32") }), _jsx(ComposerActionButton, {})] })] }) })] })] })] }));
93
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Attachment rendering components for chat messages and composer.
3
+ *
4
+ * Following the official assistant-ui pattern, attachment components
5
+ * live in a separate file from the main thread components.
6
+ *
7
+ * @see https://www.assistant-ui.com/ - assistant-ui Documentation
8
+ */
9
+ import { type FC } from "react";
10
+ /** Renders a pending file attachment chip in the composer with a remove button. */
11
+ export declare const ComposerAttachment: FC;
12
+ /** Renders a file badge on sent user messages. */
13
+ export declare const UserMessageAttachment: FC;
14
+ /** Component map for MessagePrimitive.Attachments on user messages. */
15
+ export declare const userAttachmentComponents: {
16
+ Document: FC;
17
+ Image: FC;
18
+ File: FC;
19
+ };
20
+ /** Component map for ComposerPrimitive.Attachments in the composer. */
21
+ export declare const composerAttachmentComponents: {
22
+ Document: FC;
23
+ Image: FC;
24
+ File: FC;
25
+ };
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AttachmentPrimitive } from "@assistant-ui/react";
3
+ import { FileText, X } from "lucide-react";
4
+ import { cn } from "@iloveagents/foundry-web-primitives";
5
+ /** Renders a pending file attachment chip in the composer with a remove button. */
6
+ export const ComposerAttachment = () => (_jsxs(AttachmentPrimitive.Root, { className: cn("inline-flex items-center gap-2 max-w-[250px]", "rounded-lg border border-border bg-muted/50", "px-3 py-2"), children: [_jsx(FileText, { className: "size-4 text-muted-foreground flex-shrink-0" }), _jsx("span", { className: "text-sm truncate", children: _jsx(AttachmentPrimitive.Name, {}) }), _jsx(AttachmentPrimitive.Remove, { asChild: true, children: _jsx("button", { type: "button", className: "p-0.5 rounded hover:bg-muted flex-shrink-0", "aria-label": "Remove attachment", children: _jsx(X, { className: "size-3.5" }) }) })] }));
7
+ /** Renders a file badge on sent user messages. */
8
+ export const UserMessageAttachment = () => (_jsxs(AttachmentPrimitive.Root, { className: cn("inline-flex items-center gap-2 max-w-[250px]", "rounded-lg border border-border/50 bg-muted/30", "px-3 py-1.5"), children: [_jsx(FileText, { className: "size-3.5 text-muted-foreground flex-shrink-0" }), _jsx("span", { className: "text-xs truncate", children: _jsx(AttachmentPrimitive.Name, {}) })] }));
9
+ /** Component map for MessagePrimitive.Attachments on user messages. */
10
+ export const userAttachmentComponents = {
11
+ Document: UserMessageAttachment,
12
+ Image: UserMessageAttachment,
13
+ File: UserMessageAttachment,
14
+ };
15
+ /** Component map for ComposerPrimitive.Attachments in the composer. */
16
+ export const composerAttachmentComponents = {
17
+ Document: ComposerAttachment,
18
+ Image: ComposerAttachment,
19
+ File: ComposerAttachment,
20
+ };
@@ -0,0 +1 @@
1
+ export declare function ChatBubble(): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, useAuiState, useThreadViewport, } from "@assistant-ui/react";
4
+ import { MessageSquare, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, Paperclip, } from "lucide-react";
5
+ import { cn } from "@iloveagents/foundry-web-primitives";
6
+ import { Button } from "@iloveagents/foundry-web-primitives";
7
+ import { TooltipIconButton } from "./tooltip-icon-button.js";
8
+ import { ContextPins } from "./context-bar.js";
9
+ import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
10
+ import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
11
+ import { MarkdownText } from "./markdown-text.js";
12
+ import { LoadingIndicator } from "./loading-indicator.js";
13
+ import { useAppStore } from "../lib/app-store.js";
14
+ import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
15
+ import { useNewConversation } from "../lib/use-new-conversation.js";
16
+ import { RAIL_WIDTH, useSidebarStore } from "../lib/sidebar-store.js";
17
+ const CHAT_BUBBLE_INSET = 8;
18
+ function BubbleAssistantMessage() {
19
+ return (_jsxs(MessagePrimitive.Root, { className: "w-full mb-2 group", children: [_jsx("div", { className: "text-foreground text-sm leading-relaxed flex flex-col gap-2", children: _jsx(MessagePrimitive.Parts, { components: {
20
+ Text: MarkdownText,
21
+ } }) }), _jsx("div", { className: "flex h-7 w-8 items-center mt-1", children: _jsx(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: _jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", className: "size-7", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-3" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-3 text-primary" }) })] }) }) }) })] }));
22
+ }
23
+ function BubbleUserMessage() {
24
+ const messageId = useAuiState((s) => s.message.id);
25
+ return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: "max-w-[85%] bg-muted rounded-2xl px-3 py-2 text-sm text-foreground", children: _jsx(MessagePrimitive.Content, {}) })] }) }));
26
+ }
27
+ function BubbleScrollToBottomButton() {
28
+ const isAtBottom = useThreadViewport((s) => s.isAtBottom);
29
+ const scrollToBottom = useThreadViewport((s) => s.scrollToBottom);
30
+ if (isAtBottom)
31
+ return null;
32
+ return (_jsx(Button, { variant: "outline", size: "icon", className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-10 rounded-full shadow-md size-7", onClick: () => scrollToBottom({ behavior: "smooth" }), children: _jsx(ChevronDown, { className: "size-3" }) }));
33
+ }
34
+ export function ChatBubble() {
35
+ const isOpen = useChatBubbleStore((s) => s.isOpen);
36
+ const isExpanded = useChatBubbleStore((s) => s.isExpanded);
37
+ const openBubble = useChatBubbleStore((s) => s.open);
38
+ const closeBubble = useChatBubbleStore((s) => s.close);
39
+ const expandChat = useChatBubbleStore((s) => s.expand);
40
+ const startNewThread = useNewConversation({ navigateToChat: false, preserveNavigation: true });
41
+ const isSidebarOpen = useSidebarStore((s) => s.isOpen);
42
+ const sidebarWidth = useSidebarStore((s) => s.width);
43
+ const pageLabel = useAppStore((s) => s.navContext.label);
44
+ const currentPage = useAppStore((s) => s.currentPage);
45
+ const threadActive = useAppStore((s) => s.threadActive);
46
+ // Draggable position: null = use default CSS anchored with equal left/bottom inset.
47
+ const [dragPos, setDragPos] = useState(null);
48
+ const dragStart = useRef(null);
49
+ const panelRef = useRef(null);
50
+ // Close and reset drag position so it reopens at the default spot
51
+ const handleClose = () => {
52
+ setDragPos(null);
53
+ closeBubble();
54
+ };
55
+ const onHeaderPointerDown = (e) => {
56
+ // Don't start drag when clicking header buttons
57
+ if (e.target.closest("button"))
58
+ return;
59
+ const panel = panelRef.current;
60
+ if (!panel)
61
+ return;
62
+ e.preventDefault();
63
+ const rect = panel.getBoundingClientRect();
64
+ dragStart.current = { mx: e.clientX, my: e.clientY, px: rect.left, py: rect.top };
65
+ // Capture the initial position so first pointer-move works correctly
66
+ setDragPos({ x: rect.left, y: rect.top });
67
+ e.currentTarget.setPointerCapture(e.pointerId);
68
+ };
69
+ const onHeaderPointerMove = (e) => {
70
+ if (!dragStart.current)
71
+ return;
72
+ const panel = panelRef.current;
73
+ const panelW = panel?.offsetWidth ?? 380;
74
+ const panelH = panel?.offsetHeight ?? 600;
75
+ const dx = e.clientX - dragStart.current.mx;
76
+ const dy = e.clientY - dragStart.current.my;
77
+ // Clamp within viewport so the panel can't be dragged fully off-screen
78
+ const x = Math.max(0, Math.min(window.innerWidth - panelW, dragStart.current.px + dx));
79
+ const y = Math.max(0, Math.min(window.innerHeight - panelH, dragStart.current.py + dy));
80
+ setDragPos({ x, y });
81
+ };
82
+ const onHeaderPointerUp = (e) => {
83
+ if (!dragStart.current)
84
+ return;
85
+ dragStart.current = null;
86
+ e.currentTarget.releasePointerCapture(e.pointerId);
87
+ };
88
+ const onHeaderLostPointerCapture = () => {
89
+ dragStart.current = null;
90
+ };
91
+ // Hide bubble on chat page (already full-screen) and in expanded mode
92
+ if (currentPage === "/" || isExpanded)
93
+ return null;
94
+ return (_jsxs(_Fragment, { children: [!isOpen && (_jsx("div", { "aria-hidden": "true", className: cn("pointer-events-none fixed bottom-0 left-0 z-40 hidden md:block", "border-r border-sidebar-border", "bg-gradient-to-b from-sidebar/0 via-sidebar/88 to-sidebar/96", "supports-[backdrop-filter]:backdrop-blur"), style: {
95
+ width: isSidebarOpen ? sidebarWidth : RAIL_WIDTH,
96
+ height: 64,
97
+ } })), !isOpen && (_jsx("button", { type: "button", onClick: openBubble, className: cn("fixed bottom-4 z-50", "size-10 rounded-full", "bg-primary text-primary-foreground shadow-lg", "flex items-center justify-center", "hover:bg-primary/90 transition-all duration-200", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), style: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, "aria-label": "Open chat", children: _jsx(MessageSquare, { className: "size-4" }) })), isOpen && (_jsxs("div", { ref: panelRef, className: cn("fixed z-50", "rounded-2xl border border-border", "bg-background shadow-2xl", "flex flex-col overflow-hidden", "w-95 h-150"), style: dragPos
98
+ ? { left: dragPos.x, top: dragPos.y }
99
+ : { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsxs("div", { className: cn("flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0", "cursor-grab active:cursor-grabbing select-none"), style: { touchAction: "none" }, onPointerDown: onHeaderPointerDown, onPointerMove: onHeaderPointerMove, onPointerUp: onHeaderPointerUp, onLostPointerCapture: onHeaderLostPointerCapture, children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(MessageSquare, { className: "size-3.5 text-primary shrink-0" }), _jsx("span", { className: "font-semibold text-foreground truncate", children: pageLabel })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: threadActive ? "Active thread" : "New conversation" })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", className: "size-7", onClick: startNewThread, children: _jsx(SquarePen, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Expand chat", size: "icon", className: "size-7", onClick: expandChat, children: _jsx(Maximize2, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Close", size: "icon", className: "size-7", onClick: handleClose, children: _jsx(X, { className: "size-4" }) })] })] }), _jsxs(ThreadPrimitive.Root, { className: "flex flex-col flex-1 overflow-hidden", children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto", children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Ask the agent about this page" }) }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "px-4 pt-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
100
+ UserMessage: BubbleUserMessage,
101
+ AssistantMessage: BubbleAssistantMessage,
102
+ } }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsx(BubbleScrollToBottomButton, {}) })] }), _jsx("div", { className: "shrink-0 border-t border-border px-3 py-3", children: _jsxs(ComposerPrimitive.Root, { className: cn("flex flex-col gap-1", "rounded-2xl border border-input", "bg-background shadow-sm", "px-3 py-2", "focus-within:border-primary/50", "transition-all duration-200"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center -space-x-1 shrink-0", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Attach file", size: "icon", className: "size-7 shrink-0", children: _jsx(Paperclip, { className: "size-3.5" }) }) }), _jsx(ContextPins, { compact: true })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: "Message...", rows: 1, className: cn("flex-1 bg-transparent", "py-1 text-sm", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-20") }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Stop", children: _jsx(Square, { className: "size-3" }) }) }) }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Send", children: _jsx(ArrowUp, { className: "size-4" }) }) }) })] })] }) })] })] }))] }));
103
+ }
@@ -0,0 +1,2 @@
1
+ import { type FC } from "react";
2
+ export declare const ChatHeader: FC;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Link } from "react-router";
3
+ import { SquarePen, Menu } from "lucide-react";
4
+ import { cn } from "@iloveagents/foundry-web-primitives";
5
+ import { AppBrand } from "./app-brand.js";
6
+ import { TooltipIconButton } from "./tooltip-icon-button.js";
7
+ import { ThemeToggle } from "./theme-toggle.js";
8
+ import { UserMenu } from "./user-menu.js";
9
+ import { useSidebarStore } from "../lib/sidebar-store.js";
10
+ import { useNewConversation } from "../lib/use-new-conversation.js";
11
+ export const ChatHeader = () => {
12
+ const handleNewConversation = useNewConversation();
13
+ const toggleMobile = useSidebarStore((s) => s.toggleMobile);
14
+ const isSidebarOpen = useSidebarStore((s) => s.isOpen);
15
+ return (_jsx("div", { className: cn("shrink-0", "bg-background border-b border-border"), children: _jsxs("div", { className: "px-4 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TooltipIconButton, { tooltip: "Open sidebar", onClick: toggleMobile, className: "md:hidden", children: _jsx(Menu, { className: "size-5" }) }), _jsx(Link, { to: "/", className: cn("flex items-center gap-2.5 hover:opacity-80 transition-opacity", isSidebarOpen && "md:hidden"), children: _jsx(AppBrand, { labelClassName: "text-sm font-semibold text-foreground" }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-primary" }) }), _jsx(UserMenu, {})] })] }) }));
16
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Client Tool Executor — registers custom UIs for client-side tools.
3
+ *
4
+ * Tool execution happens in the AGUIRunner (client-side tools intercepted at
5
+ * `tool-call-end`). This component only registers the UI rendering and
6
+ * injects the navigate function (which requires React Router context).
7
+ *
8
+ * Modern assistant-ui v0.12 pattern: each tool UI is a top-level
9
+ * `makeAssistantToolUI(...)` component. We mount them as children of
10
+ * `<ClientToolExecutor>` inside the AssistantRuntimeProvider tree.
11
+ */
12
+ export declare function ClientToolExecutor(): import("react/jsx-runtime").JSX.Element;