@truefoundry/agent-ui-sdk 0.0.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/README.md +634 -0
- package/dist/index.d.ts +718 -0
- package/dist/index.js +2736 -0
- package/dist/index.js.map +1 -0
- package/package.json +83 -0
- package/src/atoms/AskUserPrompt.tsx +102 -0
- package/src/atoms/AttachmentCard.tsx +92 -0
- package/src/atoms/AttachmentPickerButton.tsx +19 -0
- package/src/atoms/AttachmentPreviewDialog.tsx +34 -0
- package/src/atoms/BranchIndicator.tsx +41 -0
- package/src/atoms/CodeBlockHeader.tsx +44 -0
- package/src/atoms/ComposerShell.tsx +112 -0
- package/src/atoms/Markdown.test.tsx +28 -0
- package/src/atoms/Markdown.tsx +288 -0
- package/src/atoms/McpAuthPrompt.tsx +60 -0
- package/src/atoms/MessageActionBar.tsx +60 -0
- package/src/atoms/MessageBubble.tsx +80 -0
- package/src/atoms/MessageErrorBanner.tsx +26 -0
- package/src/atoms/MessageIndicator.tsx +23 -0
- package/src/atoms/OpenUIBlock.test.tsx +37 -0
- package/src/atoms/OpenUIBlock.tsx +50 -0
- package/src/atoms/ReasoningCard.tsx +52 -0
- package/src/atoms/SandboxArtifactList.tsx +63 -0
- package/src/atoms/ScrollToBottomButton.tsx +34 -0
- package/src/atoms/Skeletons.tsx +32 -0
- package/src/atoms/ThreadListMisc.tsx +76 -0
- package/src/atoms/ThreadListRow.tsx +81 -0
- package/src/atoms/ThreadShell.tsx +93 -0
- package/src/atoms/Toast.tsx +60 -0
- package/src/atoms/ToolApprovalBar.tsx +92 -0
- package/src/atoms/ToolCallCard.tsx +186 -0
- package/src/atoms/ToolGroupCard.tsx +52 -0
- package/src/atoms/UserMessageActionBar.tsx +62 -0
- package/src/atoms/WelcomeScreen.tsx +22 -0
- package/src/atoms/lib/cn.ts +6 -0
- package/src/atoms/primitives/Avatar.tsx +57 -0
- package/src/atoms/primitives/Button.tsx +73 -0
- package/src/atoms/primitives/Collapsible.tsx +32 -0
- package/src/atoms/primitives/Dialog.tsx +152 -0
- package/src/atoms/primitives/IconButton.tsx +43 -0
- package/src/atoms/primitives/Skeleton.tsx +17 -0
- package/src/atoms/primitives/Tooltip.tsx +58 -0
- package/src/containers/AskUserContainer.tsx +49 -0
- package/src/containers/AssistantMessageContainer.test.tsx +56 -0
- package/src/containers/AssistantMessageContainer.tsx +128 -0
- package/src/containers/AssistantTextContainer.test.tsx +111 -0
- package/src/containers/AssistantTextContainer.tsx +54 -0
- package/src/containers/AttachmentsContainer.tsx +83 -0
- package/src/containers/ComposerContainer.tsx +66 -0
- package/src/containers/ErrorToasterContainer.tsx +76 -0
- package/src/containers/McpAuthContainer.test.tsx +100 -0
- package/src/containers/McpAuthContainer.tsx +22 -0
- package/src/containers/MessageImageContainer.tsx +37 -0
- package/src/containers/ReasoningContainer.tsx +32 -0
- package/src/containers/RuntimeHarness.tsx +39 -0
- package/src/containers/Thread.tsx +9 -0
- package/src/containers/ThreadContainer.test.tsx +52 -0
- package/src/containers/ThreadContainer.tsx +87 -0
- package/src/containers/ThreadListContainer.tsx +63 -0
- package/src/containers/ToolCallContainer.test.tsx +135 -0
- package/src/containers/ToolCallContainer.tsx +167 -0
- package/src/containers/ToolGroupContainer.test.tsx +56 -0
- package/src/containers/ToolGroupContainer.tsx +24 -0
- package/src/containers/UserEditComposerContainer.test.tsx +52 -0
- package/src/containers/UserEditComposerContainer.tsx +86 -0
- package/src/containers/UserMessageContainer.test.tsx +99 -0
- package/src/containers/UserMessageContainer.tsx +26 -0
- package/src/containers/nestedApprovalBridge.ts +20 -0
- package/src/containers/useAttachmentPreviewSrc.ts +42 -0
- package/src/hooks/useComposerBusyState.test.ts +78 -0
- package/src/hooks/useComposerBusyState.ts +69 -0
- package/src/index.ts +193 -0
- package/src/openui.css +3 -0
- package/src/testSetup.ts +42 -0
- package/src/theme/SlotsProvider.test.tsx +64 -0
- package/src/theme/SlotsProvider.tsx +41 -0
- package/src/theme/defaultSlots.ts +131 -0
- package/src/theme/tokens.ts +98 -0
- package/src/theme/useClassDarkMode.ts +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
# @truefoundry/agent-ui-sdk
|
|
2
|
+
|
|
3
|
+
A design-system-agnostic chat UI SDK for TrueFoundry agents, built on top of
|
|
4
|
+
`[@assistant-ui/react](https://www.assistant-ui.com/)` and
|
|
5
|
+
`@truefoundry/assistant-ui-runtime`.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
truefoundry-gateway-sdk (AgentSessionClient)
|
|
9
|
+
→ @truefoundry/assistant-ui-runtime (useTrueFoundryAgentRuntime)
|
|
10
|
+
→ @assistant-ui/react (primitives, ExternalStoreRuntime)
|
|
11
|
+
→ THIS SDK (Thread, ThreadListContainer, atoms + containers)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It exists to separate **presentation** from **state**, so the visual design
|
|
15
|
+
can be replaced later without touching any data-fetching or runtime-wiring
|
|
16
|
+
logic. You bring the runtime; the SDK renders it.
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Styling](#styling)
|
|
22
|
+
- [Quick start](#quick-start)
|
|
23
|
+
- [OpenUI blocks](#openui-blocks)
|
|
24
|
+
- [Customizing the UI (examples)](#customizing-the-ui-examples)
|
|
25
|
+
- [Architecture](#architecture)
|
|
26
|
+
- [What's in the box](#whats-in-the-box)
|
|
27
|
+
- [Known gaps](#known-gaps)
|
|
28
|
+
- [Troubleshooting: "requires an AuiProvider"](#troubleshooting-requires-an-auiprovider)
|
|
29
|
+
- [Development](#development)
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
yarn add @truefoundry/agent-ui-sdk \
|
|
35
|
+
@assistant-ui/react @assistant-ui/core \
|
|
36
|
+
@truefoundry/assistant-ui-runtime truefoundry-gateway-sdk \
|
|
37
|
+
react react-dom
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Every one of these peers must resolve to a **single instance** across your app.
|
|
41
|
+
Two copies of `react` or `@assistant-ui/core` are the most likely source of a
|
|
42
|
+
`"requires an AuiProvider"` error — see
|
|
43
|
+
[Troubleshooting](#troubleshooting-requires-an-auiprovider).
|
|
44
|
+
|
|
45
|
+
> `@truefoundry/assistant-ui-runtime` isn't on npm yet. Until it is, install it
|
|
46
|
+
> from a prebuilt tarball, e.g. add to your `package.json`:
|
|
47
|
+
> `"@truefoundry/assistant-ui-runtime": "@truefoundry/assistant-ui-runtime"`.
|
|
48
|
+
|
|
49
|
+
## Styling
|
|
50
|
+
|
|
51
|
+
SDK components use Tailwind utility classes. Import the SDK stylesheet in your
|
|
52
|
+
**Tailwind entry CSS file** (for example `globals.css`) with a CSS `@import` so
|
|
53
|
+
the SDK's own `@source` directive participates in the same Tailwind build:
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
@import "tailwindcss";
|
|
57
|
+
@import "@truefoundry/agent-ui-sdk/openui.css";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No manual `@source` pointing into `node_modules` is needed — the SDK stylesheet
|
|
61
|
+
declares `@source "."` relative to its install location, so Tailwind scans the
|
|
62
|
+
SDK's components automatically.
|
|
63
|
+
|
|
64
|
+
**Use a CSS `@import`, not a JS import** (`import "@truefoundry/agent-ui-sdk/openui.css"`).
|
|
65
|
+
A JS import may load OpenUI styles but will not activate Tailwind's content
|
|
66
|
+
scanning for SDK utilities.
|
|
67
|
+
|
|
68
|
+
Provide your own design tokens as CSS variables (`--primary`, `--background`,
|
|
69
|
+
`--muted`, `--border`, …) in the consumer app — the SDK stays
|
|
70
|
+
design-system-agnostic and references those variables rather than hardcoded
|
|
71
|
+
colors or spacing.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
|
77
|
+
import {
|
|
78
|
+
useTrueFoundryAgentRuntime,
|
|
79
|
+
trueFoundryAttachmentAdapter,
|
|
80
|
+
} from "@truefoundry/assistant-ui-runtime";
|
|
81
|
+
import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
|
|
82
|
+
import {
|
|
83
|
+
Thread,
|
|
84
|
+
ThreadListContainer,
|
|
85
|
+
ErrorToasterProvider,
|
|
86
|
+
TooltipProvider,
|
|
87
|
+
} from "@truefoundry/agent-ui-sdk";
|
|
88
|
+
|
|
89
|
+
const client = new AgentSessionClient({
|
|
90
|
+
apiKey: process.env.TFY_API_KEY!,
|
|
91
|
+
environment: process.env.TFY_GATEWAY_URL!, // https://gateway.truefoundry.ai/<tenant>
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export function App() {
|
|
95
|
+
const runtime = useTrueFoundryAgentRuntime({
|
|
96
|
+
client,
|
|
97
|
+
agentName: "my-agent",
|
|
98
|
+
adapters: { attachments: trueFoundryAttachmentAdapter }, // optional: enables file uploads
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
103
|
+
<ErrorToasterProvider>
|
|
104
|
+
<TooltipProvider>
|
|
105
|
+
<main style={{ display: "flex", height: "100dvh" }}>
|
|
106
|
+
<aside style={{ width: 256 }}>
|
|
107
|
+
<ThreadListContainer />
|
|
108
|
+
</aside>
|
|
109
|
+
<div style={{ flex: 1 }}>
|
|
110
|
+
<Thread />
|
|
111
|
+
</div>
|
|
112
|
+
</main>
|
|
113
|
+
</TooltipProvider>
|
|
114
|
+
</ErrorToasterProvider>
|
|
115
|
+
</AssistantRuntimeProvider>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
That's the whole integration. Three things to know:
|
|
121
|
+
|
|
122
|
+
- `**<Thread />**` is the fully-assembled message list + composer. It handles
|
|
123
|
+
the welcome screen, loading skeleton, streaming, tool calls, reasoning
|
|
124
|
+
blocks, attachments, and the ask-user / MCP-auth prompts automatically.
|
|
125
|
+
- `**<ThreadListContainer />**` is the session sidebar (new / select / archive
|
|
126
|
+
/ delete).
|
|
127
|
+
- `**<ErrorToasterProvider>` / `<TooltipProvider>**` are required wrappers:
|
|
128
|
+
gateway/runtime errors surface through the toaster, and tooltips on built-in
|
|
129
|
+
buttons need the provider.
|
|
130
|
+
|
|
131
|
+
Need a custom layout around the input? Compose the pieces yourself instead of
|
|
132
|
+
`<Thread />`:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { ThreadContainer, ComposerContainer } from "@truefoundry/agent-ui-sdk";
|
|
136
|
+
|
|
137
|
+
<ThreadContainer composer={<ComposerContainer />} />;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## OpenUI blocks
|
|
141
|
+
|
|
142
|
+
Assistant messages that include an OpenUI Lang fenced block render
|
|
143
|
+
automatically — no custom markdown wiring required:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
```openui
|
|
147
|
+
Card() {
|
|
148
|
+
title: "Quarterly revenue"
|
|
149
|
+
TextContent() { text: "Revenue is up 12%." }
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`<Thread />` routes assistant text through the SDK `Markdown` atom, which
|
|
155
|
+
dispatches `language-openui` fences to `OpenUIBlock`. Streaming state is passed
|
|
156
|
+
through while the message is still running.
|
|
157
|
+
|
|
158
|
+
`OpenUIBlock` wraps output in OpenUI's `ThemeProvider` and syncs `mode` with a
|
|
159
|
+
`.dark` class on `<html>` by default. Pass an explicit `mode` prop to override,
|
|
160
|
+
or wrap a larger subtree in OpenUI's `ThemeProvider` yourself for custom token
|
|
161
|
+
overrides (`lightTheme` / `darkTheme`).
|
|
162
|
+
|
|
163
|
+
Make sure the SDK stylesheet is imported as CSS (see [Styling](#styling)) — the
|
|
164
|
+
`@import` form is required for OpenUI's Tailwind utilities.
|
|
165
|
+
|
|
166
|
+
## Customizing the UI (examples)
|
|
167
|
+
|
|
168
|
+
Every feature-level atom is resolved through `useSlot`, so overriding one is
|
|
169
|
+
just a prop on `<SlotsProvider>`. Nested `<SlotsProvider>`s fall through to the
|
|
170
|
+
default, so you only ever override what actually changes.
|
|
171
|
+
|
|
172
|
+
### The 60-second version
|
|
173
|
+
|
|
174
|
+
`WelcomeScreen` renders immediately for a new/empty thread, so it's the fastest
|
|
175
|
+
change to see:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { SlotsProvider, Thread } from "@truefoundry/agent-ui-sdk";
|
|
179
|
+
|
|
180
|
+
function MyWelcome({ heading }: { heading?: string }) {
|
|
181
|
+
return <p className="px-4 py-6 text-center text-lg">{heading ?? "Hey 👋 what are we building today?"}</p>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
<SlotsProvider overrides={{ WelcomeScreen: MyWelcome }}>
|
|
185
|
+
<Thread />
|
|
186
|
+
</SlotsProvider>;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Every other atom keeps rendering the SDK defaults. That's the whole pattern;
|
|
190
|
+
the rest of this section applies it to harder cases.
|
|
191
|
+
|
|
192
|
+
### Which names are actually resolved via `useSlot`?
|
|
193
|
+
|
|
194
|
+
Not every name declared on `AtomSlots` is resolved via `useSlot` today.
|
|
195
|
+
|
|
196
|
+
- **Feature-level atoms are** — `MessageBubble`, `ComposerShell`,
|
|
197
|
+
`ToolCallCard`, `WelcomeScreen`, `ThreadListRow`, `Toast`, `ToastStack`,
|
|
198
|
+
`AskUserPrompt`, `ScrollToBottomButton`, etc. Overriding these works exactly
|
|
199
|
+
as documented here.
|
|
200
|
+
- **Shared low-level primitives are not** — `Button`, `IconButton`, `Tooltip`*,
|
|
201
|
+
`Dialog*`, `Avatar*`, `Collapsible*`, `Skeleton`, `CodeBlockHeader` are
|
|
202
|
+
declared in `AtomSlots` and populated in `defaultSlots`, but the atoms that
|
|
203
|
+
render them currently `import` them directly instead of going through
|
|
204
|
+
`useSlot`. Overriding just `Button` will **not** change the Submit button
|
|
205
|
+
inside `AskUserPrompt`.
|
|
206
|
+
|
|
207
|
+
If you need to change a primitive everywhere, override the higher-level atom
|
|
208
|
+
that embeds it (examples 4 and 6 do exactly this).
|
|
209
|
+
|
|
210
|
+
### Restyling
|
|
211
|
+
|
|
212
|
+
**1. Restyle the chat bubbles.** `MessageBubble` is a discriminated union on
|
|
213
|
+
`variant`, so user and assistant messages can have completely different shells
|
|
214
|
+
while still receiving the same slots (`children`, `error`, `branchIndicator`,
|
|
215
|
+
`actionBar`/`attachments`):
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
import { SlotsProvider, Thread, type MessageBubbleProps } from "@truefoundry/agent-ui-sdk";
|
|
219
|
+
|
|
220
|
+
function BubbleV2(props: MessageBubbleProps) {
|
|
221
|
+
if (props.variant === "user") {
|
|
222
|
+
return (
|
|
223
|
+
<div className="ml-auto max-w-[75%] rounded-2xl bg-indigo-600 px-4 py-2 text-white">
|
|
224
|
+
{props.children}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return (
|
|
229
|
+
<div className="mr-auto max-w-[75%] rounded-2xl bg-neutral-100 px-4 py-2 dark:bg-neutral-800">
|
|
230
|
+
{props.children}
|
|
231
|
+
{props.error}
|
|
232
|
+
<div className="mt-1 flex gap-1 text-xs text-neutral-500">
|
|
233
|
+
{props.branchIndicator}
|
|
234
|
+
{props.actionBar}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
<SlotsProvider overrides={{ MessageBubble: BubbleV2 }}>
|
|
241
|
+
<Thread />
|
|
242
|
+
</SlotsProvider>;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**2. Give one part of your app a different look.** `SlotsProvider`s nest, and
|
|
246
|
+
each `useSlot` falls back to the nearest ancestor — so an embedded widget can
|
|
247
|
+
look different from the full-page app without a second theming system, and
|
|
248
|
+
without its overrides leaking out:
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
import { SlotsProvider, Thread } from "@truefoundry/agent-ui-sdk";
|
|
252
|
+
|
|
253
|
+
function CompactWelcome({ heading }: { heading?: string }) {
|
|
254
|
+
return <p className="px-4 py-2 text-sm text-muted-foreground">{heading ?? "Ask me anything"}</p>;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Only this subtree sees CompactWelcome. A <Thread /> elsewhere keeps the default.
|
|
258
|
+
function EmbeddedWidget() {
|
|
259
|
+
return (
|
|
260
|
+
<SlotsProvider overrides={{ WelcomeScreen: CompactWelcome }}>
|
|
261
|
+
<Thread />
|
|
262
|
+
</SlotsProvider>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**3. Decorate instead of replace.** An override can just wrap the default and
|
|
268
|
+
add behavior — the cheapest way to bolt on analytics without owning markup:
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { forwardRef } from "react";
|
|
272
|
+
import {
|
|
273
|
+
SlotsProvider,
|
|
274
|
+
Thread,
|
|
275
|
+
ScrollToBottomButton as DefaultScrollButton,
|
|
276
|
+
type ScrollToBottomButtonProps,
|
|
277
|
+
} from "@truefoundry/agent-ui-sdk";
|
|
278
|
+
|
|
279
|
+
const TrackedScrollButton = forwardRef<HTMLButtonElement, ScrollToBottomButtonProps>((props, ref) => (
|
|
280
|
+
<DefaultScrollButton
|
|
281
|
+
{...props}
|
|
282
|
+
ref={ref}
|
|
283
|
+
onClick={(event) => {
|
|
284
|
+
analytics.track("scroll_to_bottom_clicked");
|
|
285
|
+
props.onClick?.(event);
|
|
286
|
+
}}
|
|
287
|
+
/>
|
|
288
|
+
));
|
|
289
|
+
|
|
290
|
+
<SlotsProvider overrides={{ ScrollToBottomButton: TrackedScrollButton }}>
|
|
291
|
+
<Thread />
|
|
292
|
+
</SlotsProvider>;
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**4. Give specific tools their own visual treatment.** `ToolCallCard` receives
|
|
296
|
+
`name`/`variant`/`status` on every call, so you can special-case one tool and
|
|
297
|
+
fall through to the default (also exported) for everything else — sub-agents,
|
|
298
|
+
mcp-listings, and every other tool:
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { SlotsProvider, Thread, ToolCallCard as DefaultToolCallCard, type ToolCallCardProps } from "@truefoundry/agent-ui-sdk";
|
|
302
|
+
import { SearchIcon } from "lucide-react";
|
|
303
|
+
|
|
304
|
+
function CustomToolCallCard(props: ToolCallCardProps) {
|
|
305
|
+
if (props.variant === "tool" && props.name === "web_search") {
|
|
306
|
+
return (
|
|
307
|
+
<div className={props.isError ? "rounded-lg border border-destructive/40 p-2" : "rounded-lg border p-2"}>
|
|
308
|
+
<button onClick={props.onToggle} className="flex items-center gap-2 text-sm">
|
|
309
|
+
<SearchIcon className="size-4" />
|
|
310
|
+
Searching the web{props.durationText ? ` · ${props.durationText}` : ""}
|
|
311
|
+
</button>
|
|
312
|
+
{props.expanded && props.result && <pre className="mt-2 text-xs whitespace-pre-wrap">{props.result}</pre>}
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return <DefaultToolCallCard {...props} />;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
<SlotsProvider overrides={{ ToolCallCard: CustomToolCallCard }}>
|
|
320
|
+
<Thread />
|
|
321
|
+
</SlotsProvider>;
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Changing behavior, not just looks
|
|
325
|
+
|
|
326
|
+
**5. Turn ask-user into instant click-to-answer.** The shipped `AskUserPrompt`
|
|
327
|
+
uses a select-then-submit radio flow (see [Known gaps](#known-gaps)). Slots
|
|
328
|
+
only cover atoms, not containers — and the fix can't live in an atom override,
|
|
329
|
+
because the shipped `AskUserContainer` computes `onSubmit` from state set by a
|
|
330
|
+
*previous* `onSelectOption`. So skip the container and talk to the runtime hook
|
|
331
|
+
it wraps directly:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
import { useTrueFoundryToolResponses } from "@truefoundry/assistant-ui-runtime";
|
|
335
|
+
import { useThreadIsRunning } from "@assistant-ui/core/react";
|
|
336
|
+
|
|
337
|
+
function InstantAskUser() {
|
|
338
|
+
const { pending, respond } = useTrueFoundryToolResponses();
|
|
339
|
+
const isRunning = useThreadIsRunning();
|
|
340
|
+
const item = pending[0];
|
|
341
|
+
if (item == null) return null;
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div className="flex flex-wrap gap-2 rounded-xl border p-3">
|
|
345
|
+
<p className="w-full text-sm font-medium">{item.question ?? "Answer required"}</p>
|
|
346
|
+
{(item.options ?? []).map((label) => (
|
|
347
|
+
<button
|
|
348
|
+
key={label}
|
|
349
|
+
disabled={isRunning}
|
|
350
|
+
onClick={() => respond({ toolCallId: item.toolCallId, content: label })}
|
|
351
|
+
className="rounded-full border px-3 py-1 text-sm"
|
|
352
|
+
>
|
|
353
|
+
{label}
|
|
354
|
+
</button>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Wire it in with a thin composer wrapper that renders `<InstantAskUser />` in
|
|
362
|
+
place of `<AskUserContainer />` — mirror the branching in
|
|
363
|
+
`[ComposerContainer](./src/containers/ComposerContainer.tsx)` (check
|
|
364
|
+
`mcpPending`, then tool responses, then fall through to `<ComposerShell />`)
|
|
365
|
+
and pass your wrapper to `<ThreadContainer composer={<YourComposer />} />`.
|
|
366
|
+
|
|
367
|
+
**6. Replace the composer while keeping cancel.** `ComposerShell` receives
|
|
368
|
+
`isRunning` and `onCancel` from `ComposerContainer` (the default renders a
|
|
369
|
+
Cancel button while a turn streams), plus `value`/`placeholder`/`disabled`/
|
|
370
|
+
`onValueChange`/`onSubmit`/`onAttach`/`attachments`. Your override owns all the
|
|
371
|
+
markup but keeps the same wiring:
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
import { SlotsProvider, Thread, IconButton, type ComposerShellProps } from "@truefoundry/agent-ui-sdk";
|
|
375
|
+
import { ArrowUpIcon, SquareIcon } from "lucide-react";
|
|
376
|
+
|
|
377
|
+
function CompactComposer({
|
|
378
|
+
value,
|
|
379
|
+
placeholder,
|
|
380
|
+
disabled,
|
|
381
|
+
isRunning,
|
|
382
|
+
onValueChange,
|
|
383
|
+
onSubmit,
|
|
384
|
+
onCancel,
|
|
385
|
+
}: ComposerShellProps) {
|
|
386
|
+
return (
|
|
387
|
+
<div className="border-border/60 flex w-full flex-col gap-2 rounded-3xl border p-2">
|
|
388
|
+
<textarea
|
|
389
|
+
value={value}
|
|
390
|
+
placeholder={placeholder}
|
|
391
|
+
disabled={disabled}
|
|
392
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
393
|
+
className="w-full resize-none bg-transparent px-2 py-1 outline-none"
|
|
394
|
+
/>
|
|
395
|
+
<div className="flex justify-end px-1">
|
|
396
|
+
{isRunning ? (
|
|
397
|
+
<IconButton tooltip="Stop generating" onClick={onCancel} disabled={!onCancel}>
|
|
398
|
+
<SquareIcon />
|
|
399
|
+
</IconButton>
|
|
400
|
+
) : (
|
|
401
|
+
<IconButton tooltip="Send" onClick={onSubmit} disabled={disabled || value.trim().length === 0}>
|
|
402
|
+
<ArrowUpIcon />
|
|
403
|
+
</IconButton>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
<SlotsProvider overrides={{ ComposerShell: CompactComposer }}>
|
|
411
|
+
<Thread />
|
|
412
|
+
</SlotsProvider>;
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**7. Reroute a cross-cutting behavior.** `ErrorToasterProvider` resolves
|
|
416
|
+
`Toast`/`ToastStack` via `useSlot`, so you can pipe every gateway/runtime error
|
|
417
|
+
through a different toast library (e.g. `sonner`) without touching
|
|
418
|
+
`useErrorToaster` or any `showError` call site:
|
|
419
|
+
|
|
420
|
+
```tsx
|
|
421
|
+
import { useEffect } from "react";
|
|
422
|
+
import {
|
|
423
|
+
SlotsProvider,
|
|
424
|
+
ErrorToasterProvider,
|
|
425
|
+
Thread,
|
|
426
|
+
type ToastProps,
|
|
427
|
+
type ToastStackProps,
|
|
428
|
+
} from "@truefoundry/agent-ui-sdk";
|
|
429
|
+
import { Toaster, toast as sonnerToast } from "sonner";
|
|
430
|
+
|
|
431
|
+
function SonnerToast({ title, description, open, onOpenChange }: ToastProps) {
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
if (!open) return;
|
|
434
|
+
sonnerToast.error(title, { description });
|
|
435
|
+
onOpenChange(false); // sonner now owns the visible toast
|
|
436
|
+
}, [open, title, description, onOpenChange]);
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function SonnerToastStack({ children }: ToastStackProps) {
|
|
441
|
+
return (
|
|
442
|
+
<>
|
|
443
|
+
{children}
|
|
444
|
+
<Toaster richColors />
|
|
445
|
+
</>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// SlotsProvider must be an ancestor of ErrorToasterProvider — the provider
|
|
450
|
+
// resolves the slot at its own position in the tree.
|
|
451
|
+
<SlotsProvider overrides={{ Toast: SonnerToast, ToastStack: SonnerToastStack }}>
|
|
452
|
+
<ErrorToasterProvider>
|
|
453
|
+
<Thread />
|
|
454
|
+
</ErrorToasterProvider>
|
|
455
|
+
</SlotsProvider>;
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**8. Layer local-only UI onto a row without touching the container.**
|
|
459
|
+
`ThreadListRow`'s contract (`title`/`active`/`onSelect`/`onArchive`/`onDelete`)
|
|
460
|
+
has no concept of "pinned" — nothing needs to. An override is just a component,
|
|
461
|
+
so it can hold state the container never sees:
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { useState } from "react";
|
|
465
|
+
import {
|
|
466
|
+
SlotsProvider,
|
|
467
|
+
ThreadListContainer,
|
|
468
|
+
ThreadListRow as DefaultRow,
|
|
469
|
+
type ThreadListRowProps,
|
|
470
|
+
} from "@truefoundry/agent-ui-sdk";
|
|
471
|
+
import { PinIcon } from "lucide-react";
|
|
472
|
+
|
|
473
|
+
function PinnableRow(props: ThreadListRowProps) {
|
|
474
|
+
const [pinned, setPinned] = useState(false);
|
|
475
|
+
return (
|
|
476
|
+
<div className="relative">
|
|
477
|
+
<DefaultRow {...props} />
|
|
478
|
+
<button
|
|
479
|
+
onClick={(event) => {
|
|
480
|
+
event.stopPropagation();
|
|
481
|
+
setPinned((prev) => !prev);
|
|
482
|
+
}}
|
|
483
|
+
className="absolute left-1.5 top-1/2 -translate-y-1/2"
|
|
484
|
+
>
|
|
485
|
+
<PinIcon className={pinned ? "size-3.5 fill-current" : "size-3.5"} />
|
|
486
|
+
</button>
|
|
487
|
+
</div>
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
<SlotsProvider overrides={{ ThreadListRow: PinnableRow }}>
|
|
492
|
+
<ThreadListContainer />
|
|
493
|
+
</SlotsProvider>;
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## Architecture
|
|
497
|
+
|
|
498
|
+
Every piece of UI is split into two layers.
|
|
499
|
+
|
|
500
|
+
### Atoms (`src/atoms/**`)
|
|
501
|
+
|
|
502
|
+
Pure, stateless, prop-driven components. An atom:
|
|
503
|
+
|
|
504
|
+
- Never imports from `@assistant-ui/*` or the TrueFoundry runtime.
|
|
505
|
+
- Never calls a runtime/data hook — only local `useState`/`useMemo`/
|
|
506
|
+
`useCallback` for purely visual concerns.
|
|
507
|
+
- Owns **all** Tailwind classes and JSX for what it renders.
|
|
508
|
+
- Only knows "render this data, call this callback."
|
|
509
|
+
|
|
510
|
+
This is the swap point: replacing the design system later means writing new
|
|
511
|
+
components that satisfy the same prop contracts — zero container changes.
|
|
512
|
+
|
|
513
|
+
### Containers (`src/containers/`**)
|
|
514
|
+
|
|
515
|
+
Stateful glue. A container:
|
|
516
|
+
|
|
517
|
+
- Reads assistant-ui primitives/hooks (`ThreadPrimitive`, `MessagePrimitive`,
|
|
518
|
+
`useAui`, `useAuiState`, `@assistant-ui/core/react` hooks) and TrueFoundry
|
|
519
|
+
runtime hooks (`useTrueFoundryApprovals`, `useTrueFoundryToolResponses`,
|
|
520
|
+
`useTrueFoundryCancel`, …).
|
|
521
|
+
- Derives plain data and callbacks from that state.
|
|
522
|
+
- Resolves every atom via `useSlot("AtomName")` — **never** imports an atom
|
|
523
|
+
directly — and passes derived props straight through.
|
|
524
|
+
- Contains no decorative styling of its own.
|
|
525
|
+
|
|
526
|
+
### The slot registry (`src/theme/SlotsProvider.tsx`)
|
|
527
|
+
|
|
528
|
+
`AtomSlots` is an interface that starts empty and gets augmented by every atom
|
|
529
|
+
module via `declare module "../theme/SlotsProvider.js"` — so adding an atom
|
|
530
|
+
never requires editing the registry. `defaultSlots.ts` supplies this SDK's own
|
|
531
|
+
implementation for every slot; `useSlot` falls back through nested
|
|
532
|
+
`<SlotsProvider>`s to those defaults.
|
|
533
|
+
|
|
534
|
+
```tsx
|
|
535
|
+
import { SlotsProvider, Thread } from "@truefoundry/agent-ui-sdk";
|
|
536
|
+
import { MyBubble } from "./my-design-system/MessageBubble";
|
|
537
|
+
|
|
538
|
+
<SlotsProvider overrides={{ MessageBubble: MyBubble }}>
|
|
539
|
+
<Thread />
|
|
540
|
+
</SlotsProvider>;
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Token contract (`src/theme/tokens.ts`)
|
|
544
|
+
|
|
545
|
+
`DesignTokens` is a semantic color/radius/spacing/typography contract exposed
|
|
546
|
+
via `useTokens()` / `<TokensProvider>`. The shipped default atoms don't consume
|
|
547
|
+
it yet (they port the reference app's Tailwind classes verbatim); it exists so
|
|
548
|
+
a future design system has a token surface to bind to.
|
|
549
|
+
|
|
550
|
+
## What's in the box
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
| Area | Containers | Notes |
|
|
554
|
+
| ------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
555
|
+
| Thread shell | `ThreadContainer`, `Thread` | Welcome screen, loading skeleton, scroll-to-bottom, message list |
|
|
556
|
+
| Messages | `AssistantMessageContainer`, `UserMessageContainer`, `AssistantTextContainer` | Text, branch picker, error banner, copy/export action bar; ````openui` fences render via `OpenUIBlock` |
|
|
557
|
+
| Tools | `ToolCallContainer`, `ToolGroupContainer` | Tool-call rendering, approval flow, sub-agent nesting (recursive) |
|
|
558
|
+
| Reasoning | `ReasoningContainer` | Streaming-aware collapsible reasoning blocks |
|
|
559
|
+
| Composer | `ComposerContainer`, `AskUserContainer`, `McpAuthContainer` | Message input with cancel-while-streaming; auto-swaps to ask-user / MCP-auth prompts when a turn is paused. `McpAuthContainer` renders a per-server "Authorize" link; the user confirms with "Continue" (no background polling) |
|
|
560
|
+
| Attachments | `ComposerAttachmentsContainer`, `MessageAttachmentsContainer`, `ComposerAttachmentPickerContainer` | Staging tray in composer; image/file chips in user bubbles |
|
|
561
|
+
| Thread list | `ThreadListContainer` | Flat list (not grouped by date): new / select / archive / delete |
|
|
562
|
+
| Errors | `ErrorToasterProvider`, `useErrorToaster` | Global toast for gateway/runtime errors |
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
Every atom's prop type is exported from `[src/index.ts](./src/index.ts)` — that
|
|
566
|
+
file is the definitive list of what's public.
|
|
567
|
+
|
|
568
|
+
## Known gaps
|
|
569
|
+
|
|
570
|
+
Deliberate deviations from the reference app, flagged rather than silently
|
|
571
|
+
dropped:
|
|
572
|
+
|
|
573
|
+
- `**AskUserPrompt**` uses a select-then-submit radio flow, not the reference
|
|
574
|
+
app's immediate click-to-answer buttons. See example 5 to change this.
|
|
575
|
+
- `**ToolCallCard**` ships an `mcp-listing` variant (type + atom) that no
|
|
576
|
+
container currently selects — the runtime has no signal distinguishing
|
|
577
|
+
"listing MCP tools" from any other tool call. Its `tool` variant also carries
|
|
578
|
+
an `approvalSlot`/`durationText` beyond the originally-sketched contract,
|
|
579
|
+
needed to keep the tool-approval flow working.
|
|
580
|
+
- **Attachments**: image previews use a `contentType` fallback because the
|
|
581
|
+
TrueFoundry attachment adapter tags all uploads as `type: "file"`.
|
|
582
|
+
- `**ThreadListContainer`** renders one flat list, not grouped by
|
|
583
|
+
Today/Yesterday/Earlier.
|
|
584
|
+
- `**McpAuthContainer**` has no per-server auth status. `McpServerAuthInfo`
|
|
585
|
+
(`{ id, name, authUrl }`) carries no authorized/pending flag, so multi-server
|
|
586
|
+
auth is all-or-nothing: one shared Continue button.
|
|
587
|
+
- Several `AtomSlots` entries (`Button`, `IconButton`, `Tooltip*`, `Dialog*`,
|
|
588
|
+
`Avatar*`, `Collapsible*`, `Skeleton`, `CodeBlockHeader`) are declared and
|
|
589
|
+
populated in `defaultSlots`, but no shipped atom resolves them via `useSlot`
|
|
590
|
+
yet — the atoms import them directly. Override the higher-level atom instead
|
|
591
|
+
(see "Which names are actually resolved via `useSlot`?").
|
|
592
|
+
|
|
593
|
+
## Troubleshooting: "requires an AuiProvider"
|
|
594
|
+
|
|
595
|
+
`@assistant-ui/core` (and `@assistant-ui/store`, which holds the React Context
|
|
596
|
+
`AuiProvider` relies on) takes `react` as a peer dependency. If your app and
|
|
597
|
+
this SDK resolve to two different `react` versions — even two patch versions
|
|
598
|
+
apart — your package manager may install two physically separate copies of
|
|
599
|
+
`@assistant-ui/core`/`@assistant-ui/store`. React Context identity is tied to
|
|
600
|
+
the *module instance*, so hooks like `useAui`/`useAuiState` called inside this
|
|
601
|
+
SDK read from a different Context than your `<AssistantRuntimeProvider>`,
|
|
602
|
+
producing:
|
|
603
|
+
|
|
604
|
+
```
|
|
605
|
+
Uncaught Error: You are using a component or hook that requires an
|
|
606
|
+
AuiProvider. Wrap your component in an <AuiProvider> component.
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
even though a provider **is** mounted. Diagnose with:
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
yarn why @assistant-ui/core
|
|
613
|
+
yarn why @assistant-ui/store
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
If either reports more than one instance, align every `react`/`react-dom`
|
|
617
|
+
version across the workspace (same range — don't mix pinned-exact and
|
|
618
|
+
caret-ranged) and reinstall.
|
|
619
|
+
|
|
620
|
+
## Development
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
yarn install
|
|
624
|
+
yarn build # tsup → dist/
|
|
625
|
+
yarn typecheck # tsc --noEmit
|
|
626
|
+
yarn test # vitest run
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Tests render containers against a real (in-memory) assistant-ui runtime via
|
|
630
|
+
`[src/containers/RuntimeHarness.tsx](./src/containers/RuntimeHarness.tsx)` — no
|
|
631
|
+
network calls, no real gateway required.
|
|
632
|
+
|
|
633
|
+
To produce a distributable tarball: `yarn pack:tgz` (installs, builds, then
|
|
634
|
+
`yarn pack`).
|