@copilotkit/react-core 1.70.3 → 1.71.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/dist/{copilotkit-BU3OvveB.cjs → copilotkit-BkVEkUS0.cjs} +369 -154
- package/dist/copilotkit-BkVEkUS0.cjs.map +1 -0
- package/dist/{copilotkit-Bs98akp9.d.mts → copilotkit-ChjzWqn6.d.mts} +33 -8
- package/dist/copilotkit-ChjzWqn6.d.mts.map +1 -0
- package/dist/{copilotkit-Ap_yisA5.mjs → copilotkit-D5BTo0YG.mjs} +368 -153
- package/dist/copilotkit-D5BTo0YG.mjs.map +1 -0
- package/dist/{copilotkit-X2eGbOwf.d.cts → copilotkit-DCIXZawe.d.cts} +33 -8
- package/dist/copilotkit-DCIXZawe.d.cts.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +180 -139
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/context.cjs +4 -0
- package/dist/v2/context.cjs.map +1 -1
- package/dist/v2/context.d.cts +3 -1
- package/dist/v2/context.d.cts.map +1 -1
- package/dist/v2/context.d.mts +3 -1
- package/dist/v2/context.d.mts.map +1 -1
- package/dist/v2/context.mjs +3 -1
- package/dist/v2/context.mjs.map +1 -1
- package/dist/v2/headless.cjs +20 -5
- package/dist/v2/headless.cjs.map +1 -1
- package/dist/v2/headless.d.cts +9 -4
- package/dist/v2/headless.d.cts.map +1 -1
- package/dist/v2/headless.d.mts +9 -4
- package/dist/v2/headless.d.mts.map +1 -1
- package/dist/v2/headless.mjs +21 -6
- package/dist/v2/headless.mjs.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +1 -1
- package/dist/v2/index.d.mts +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +495 -278
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +8 -9
- package/dist/copilotkit-Ap_yisA5.mjs.map +0 -1
- package/dist/copilotkit-BU3OvveB.cjs.map +0 -1
- package/dist/copilotkit-Bs98akp9.d.mts.map +0 -1
- package/dist/copilotkit-X2eGbOwf.d.cts.map +0 -1
- package/skills/react-core/SKILL.md +0 -110
- package/skills/react-core/references/agent-access.md +0 -355
- package/skills/react-core/references/attachments.md +0 -311
- package/skills/react-core/references/capabilities.md +0 -138
- package/skills/react-core/references/chat-components.md +0 -229
- package/skills/react-core/references/client-side-tools.md +0 -358
- package/skills/react-core/references/custom-message-renderers.md +0 -223
- package/skills/react-core/references/debug-mode.md +0 -140
- package/skills/react-core/references/human-in-the-loop.md +0 -312
- package/skills/react-core/references/provider-setup.md +0 -358
- package/skills/react-core/references/rendering-activity-messages.md +0 -201
- package/skills/react-core/references/rendering-tool-calls.md +0 -319
- package/skills/react-core/references/suggestions.md +0 -211
- package/skills/react-core/references/switching-agents-recipes.md +0 -161
- package/skills/react-core/references/switching-agents.md +0 -240
- package/skills/react-core/references/threads.md +0 -289
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
# CopilotKit Agent Access (React)
|
|
2
|
-
|
|
3
|
-
This skill builds on `copilotkit/provider-setup`. `useAgent` reads from the
|
|
4
|
-
same registry the provider populates from `/info`.
|
|
5
|
-
|
|
6
|
-
Two complementary surfaces:
|
|
7
|
-
|
|
8
|
-
- `useAgent` — imperative access to an agent instance, subscribe to
|
|
9
|
-
messages/state/run-status changes.
|
|
10
|
-
- `useAgentContext` — declarative push of app state to every agent run.
|
|
11
|
-
|
|
12
|
-
## Setup
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
"use client";
|
|
16
|
-
import {
|
|
17
|
-
useAgent,
|
|
18
|
-
useAgentContext,
|
|
19
|
-
UseAgentUpdate,
|
|
20
|
-
} from "@copilotkit/react-core/v2";
|
|
21
|
-
import { useMemo } from "react";
|
|
22
|
-
|
|
23
|
-
export function ChatDriver({
|
|
24
|
-
route,
|
|
25
|
-
userId,
|
|
26
|
-
}: {
|
|
27
|
-
route: string;
|
|
28
|
-
userId: string;
|
|
29
|
-
}) {
|
|
30
|
-
const { agent } = useAgent({
|
|
31
|
-
agentId: "default",
|
|
32
|
-
threadId: "main",
|
|
33
|
-
updates: [
|
|
34
|
-
UseAgentUpdate.OnMessagesChanged,
|
|
35
|
-
UseAgentUpdate.OnRunStatusChanged,
|
|
36
|
-
],
|
|
37
|
-
throttleMs: 100,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const context = useMemo(() => ({ route, userId }), [route, userId]);
|
|
41
|
-
useAgentContext({ description: "app context", value: context });
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<div>
|
|
45
|
-
{agent.isRunning ? "…thinking" : "idle"} — {agent.messages.length}{" "}
|
|
46
|
-
messages
|
|
47
|
-
</div>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Core Patterns
|
|
53
|
-
|
|
54
|
-
### Send a message and stream the response
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
|
-
const { agent } = useAgent({ agentId: "default" });
|
|
58
|
-
const { copilotkit } = useCopilotKit();
|
|
59
|
-
|
|
60
|
-
async function ask(text: string) {
|
|
61
|
-
agent.addMessage({ id: crypto.randomUUID(), role: "user", content: text });
|
|
62
|
-
await copilotkit.runAgent({ agent });
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Subscribe only to run-status to reduce re-renders
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
const { agent } = useAgent({
|
|
70
|
-
agentId: "default",
|
|
71
|
-
updates: [UseAgentUpdate.OnRunStatusChanged],
|
|
72
|
-
});
|
|
73
|
-
const isRunning = agent.isRunning;
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`useAgent` returns `{ agent, isReady }`; `isRunning` lives on the agent
|
|
77
|
-
itself. Subscribing to `OnRunStatusChanged` forces a re-render when the
|
|
78
|
-
value flips, so reading `agent.isRunning` stays live.
|
|
79
|
-
|
|
80
|
-
### Share app state with every agent run (global)
|
|
81
|
-
|
|
82
|
-
```tsx
|
|
83
|
-
const value = useMemo(
|
|
84
|
-
() => ({ cartItems: cart.items, currentRoute: router.pathname }),
|
|
85
|
-
[cart.items, router.pathname],
|
|
86
|
-
);
|
|
87
|
-
useAgentContext({ description: "user cart + route", value });
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Abort the run
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
const { agent } = useAgent({ agentId: "default" });
|
|
94
|
-
<button onClick={() => agent.abortRun()}>Stop</button>;
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Wait for the real agent before attaching to it
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
const { agent, isReady } = useAgent({ agentId: "default" });
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!isReady) return; // provisional stand-in — don't attach yet
|
|
104
|
-
const sub = agent.subscribe({ onRunStartedEvent: handleRunStarted });
|
|
105
|
-
return () => sub.unsubscribe();
|
|
106
|
-
}, [agent, isReady]);
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Until the runtime `/info` sync resolves, `agent` is a provisional
|
|
110
|
-
stand-in. It is a fully-constructed `AbstractAgent`, so every call on it
|
|
111
|
-
is safe — but it is then **replaced**, and `agent` changes reference.
|
|
112
|
-
Anything keyed to the old instance goes with it.
|
|
113
|
-
|
|
114
|
-
## Common Mistakes
|
|
115
|
-
|
|
116
|
-
### CRITICAL — Custom `AbstractAgent.clone()` that returns `this`
|
|
117
|
-
|
|
118
|
-
Wrong:
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
class MyAgent extends AbstractAgent {
|
|
122
|
-
clone() {
|
|
123
|
-
return this; // wrong — same instance is reused across threads
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Correct:
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
class MyAgent extends AbstractAgent {
|
|
132
|
-
clone() {
|
|
133
|
-
const next = new MyAgent(this.config);
|
|
134
|
-
next.state = { ...this.state };
|
|
135
|
-
return next;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
`useAgent` calls `source.clone()` to build a per-thread clone and throws
|
|
141
|
-
`clone() must return a new, independent object` if the clone is the same
|
|
142
|
-
instance. This guards per-thread isolation.
|
|
143
|
-
|
|
144
|
-
Source: `packages/react-core/src/v2/hooks/use-agent.tsx:58-69`
|
|
145
|
-
|
|
146
|
-
### HIGH — Deriving app state from `agent` without guarding on `isReady`
|
|
147
|
-
|
|
148
|
-
Wrong:
|
|
149
|
-
|
|
150
|
-
```tsx
|
|
151
|
-
const { agent } = useAgent({ agentId: "default" });
|
|
152
|
-
|
|
153
|
-
// Correlation map for matching responses back to the row that asked.
|
|
154
|
-
const pending = useRef(new Map<string, string>());
|
|
155
|
-
|
|
156
|
-
useEffect(() => {
|
|
157
|
-
pending.current = new Map(); // re-runs when `agent` is swapped
|
|
158
|
-
const sub = agent.subscribe({ onRunFinishedEvent: resolvePending });
|
|
159
|
-
return () => sub.unsubscribe();
|
|
160
|
-
}, [agent]);
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Correct:
|
|
164
|
-
|
|
165
|
-
```tsx
|
|
166
|
-
const { agent, isReady } = useAgent({ agentId: "default" });
|
|
167
|
-
|
|
168
|
-
// Owned by the component, not by the agent — survives the swap.
|
|
169
|
-
const pending = useRef(new Map<string, string>());
|
|
170
|
-
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
if (!isReady) return;
|
|
173
|
-
const sub = agent.subscribe({ onRunFinishedEvent: resolvePending });
|
|
174
|
-
return () => sub.unsubscribe();
|
|
175
|
-
}, [agent, isReady]);
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
`agent` changes reference exactly once per mount, when `/info` resolves and
|
|
179
|
-
the provisional stand-in is swapped for the real instance. Any effect with
|
|
180
|
-
`agent` in its dependency array re-runs at that moment — so app state
|
|
181
|
-
initialized inside such an effect is silently reset partway through the
|
|
182
|
-
first interaction.
|
|
183
|
-
|
|
184
|
-
This bites hardest with Intelligence configured, because license
|
|
185
|
-
verification and thread-endpoint discovery lengthen the provisional window
|
|
186
|
-
past the first user action. In plain SSE mode the window usually closes
|
|
187
|
-
before anyone can interact, which is why the bug does not reproduce in
|
|
188
|
-
OSS-only development.
|
|
189
|
-
|
|
190
|
-
Never put per-component bookkeeping (correlation maps, in-flight request
|
|
191
|
-
records, refs) behind an `agent` dependency. Initialize it in the ref
|
|
192
|
-
itself and let the effect only manage the subscription.
|
|
193
|
-
|
|
194
|
-
Source: `packages/react-core/src/v2/hooks/use-agent.tsx:226-290,465-481`
|
|
195
|
-
|
|
196
|
-
### HIGH — Mutating `agent.messages` directly
|
|
197
|
-
|
|
198
|
-
Wrong:
|
|
199
|
-
|
|
200
|
-
```tsx
|
|
201
|
-
agent.messages.push({ id, role: "user", content: "hi" });
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
Correct:
|
|
205
|
-
|
|
206
|
-
```tsx
|
|
207
|
-
agent.addMessage({ id: crypto.randomUUID(), role: "user", content: "hi" });
|
|
208
|
-
// or:
|
|
209
|
-
agent.setMessages([...agent.messages, newMessage]);
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
AG-UI fires `onMessagesChanged` subscribers via `addMessage` /
|
|
213
|
-
`setMessages`. Direct array mutation bypasses subscribers and the UI never
|
|
214
|
-
re-renders.
|
|
215
|
-
|
|
216
|
-
Source: `packages/react-core/src/v2/hooks/use-agent.tsx` (throughout)
|
|
217
|
-
|
|
218
|
-
### HIGH — Registering non-serializable values via `useAgentContext`
|
|
219
|
-
|
|
220
|
-
Wrong:
|
|
221
|
-
|
|
222
|
-
```tsx
|
|
223
|
-
useAgentContext({
|
|
224
|
-
description: "user",
|
|
225
|
-
value: {
|
|
226
|
-
name: "Alice",
|
|
227
|
-
lastLogin: new Date(),
|
|
228
|
-
onLogout: () => logout(), // dropped silently
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Correct:
|
|
234
|
-
|
|
235
|
-
```tsx
|
|
236
|
-
useAgentContext({
|
|
237
|
-
description: "user",
|
|
238
|
-
value: { name: "Alice", lastLogin: new Date().toISOString() },
|
|
239
|
-
});
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
`useAgentContext` runs the value through `JSON.stringify`. Functions are
|
|
243
|
-
dropped, `Date` coerces to an ISO string (which the agent has to parse), and
|
|
244
|
-
circular references throw.
|
|
245
|
-
|
|
246
|
-
Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx:30-35`
|
|
247
|
-
|
|
248
|
-
### MEDIUM — Expecting lifecycle callbacks to be throttled
|
|
249
|
-
|
|
250
|
-
Wrong:
|
|
251
|
-
|
|
252
|
-
```tsx
|
|
253
|
-
useAgent({
|
|
254
|
-
agentId: "default",
|
|
255
|
-
throttleMs: 300,
|
|
256
|
-
// expecting onRunInitialized / onRunFinalized / onRunFailed to also be throttled
|
|
257
|
-
});
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
Correct:
|
|
261
|
-
|
|
262
|
-
```tsx
|
|
263
|
-
// Only OnMessagesChanged / OnStateChanged / OnRunStatusChanged are throttled.
|
|
264
|
-
// Lifecycle callbacks always fire immediately — handle them synchronously.
|
|
265
|
-
useAgent({ agentId: "default", throttleMs: 300 });
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
`throttleMs` only applies to the three subscribed updates enumerated in
|
|
269
|
-
`UseAgentUpdate`. Lifecycle callbacks bypass the throttler.
|
|
270
|
-
|
|
271
|
-
Source: `packages/react-core/src/v2/hooks/use-agent.tsx:36-48`
|
|
272
|
-
|
|
273
|
-
### MEDIUM — Unstable context value identity
|
|
274
|
-
|
|
275
|
-
Wrong:
|
|
276
|
-
|
|
277
|
-
```tsx
|
|
278
|
-
useAgentContext({ description: "cart", value: { items: cart.items } });
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Correct:
|
|
282
|
-
|
|
283
|
-
```tsx
|
|
284
|
-
const value = useMemo(() => ({ items: cart.items }), [cart.items]);
|
|
285
|
-
useAgentContext({ description: "cart", value });
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
A fresh object literal on every render invalidates the `useMemo` inside
|
|
289
|
-
`useAgentContext` that serializes the value, causing constant
|
|
290
|
-
remove/re-add churn in the core context store.
|
|
291
|
-
|
|
292
|
-
Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx:30-35`
|
|
293
|
-
|
|
294
|
-
### MEDIUM — Expecting `useAgentContext` or `copilotkit.addContext` to scope context per agent
|
|
295
|
-
|
|
296
|
-
Wrong:
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
useAgentContext({ agentId: "research", description: "paper list", value });
|
|
300
|
-
// or the imperative form:
|
|
301
|
-
copilotkit.addContext({
|
|
302
|
-
description: "paper list",
|
|
303
|
-
value: JSON.stringify(value),
|
|
304
|
-
agentId: "research",
|
|
305
|
-
});
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Correct:
|
|
309
|
-
|
|
310
|
-
```tsx
|
|
311
|
-
// Context is global — every agent run sees every registered entry.
|
|
312
|
-
useAgentContext({ description: "paper list", value });
|
|
313
|
-
|
|
314
|
-
// When only one agent should key off a value, branch inside its prompt
|
|
315
|
-
// or tool logic instead of trying to scope the context entry.
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
Context is intentionally global and there is no per-agent scoping hook.
|
|
319
|
-
`useAgentContext` has no `agentId` parameter, and `copilotkit.addContext`
|
|
320
|
-
destructures only `{ description, value }` — any `agentId` passed is
|
|
321
|
-
silently dropped. Treat context as "state of the world" that every agent
|
|
322
|
-
sees.
|
|
323
|
-
|
|
324
|
-
Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx` (no `agentId` parameter); `packages/core/src/core/context-store.ts:26-31`
|
|
325
|
-
|
|
326
|
-
### MEDIUM — Two components using the same `(agentId, threadId)` expecting isolation
|
|
327
|
-
|
|
328
|
-
Wrong:
|
|
329
|
-
|
|
330
|
-
```tsx
|
|
331
|
-
function A() {
|
|
332
|
-
const { agent } = useAgent({ agentId: "default", threadId: "t1" });
|
|
333
|
-
}
|
|
334
|
-
function B() {
|
|
335
|
-
const { agent } = useAgent({ agentId: "default", threadId: "t1" });
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
Correct:
|
|
340
|
-
|
|
341
|
-
```tsx
|
|
342
|
-
function A() {
|
|
343
|
-
useAgent({ agentId: "default", threadId: "a" });
|
|
344
|
-
}
|
|
345
|
-
function B() {
|
|
346
|
-
useAgent({ agentId: "default", threadId: "b" });
|
|
347
|
-
}
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Per-thread clones are cached in a module-level WeakMap keyed by
|
|
351
|
-
`(registryAgent, threadId)`. Two consumers of the same `(agentId,
|
|
352
|
-
threadId)` observe the same state. Give each surface a distinct `threadId`
|
|
353
|
-
when isolation is intentional.
|
|
354
|
-
|
|
355
|
-
Source: `packages/react-core/src/v2/hooks/use-agent.tsx:78-119`
|
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
# CopilotKit Attachments (React)
|
|
2
|
-
|
|
3
|
-
This skill builds on `copilotkit/provider-setup` and
|
|
4
|
-
`copilotkit/chat-components`. `useAttachments` is exposed both as an opt-in
|
|
5
|
-
prop on `<CopilotChat>` and as a direct hook for custom chat surfaces.
|
|
6
|
-
|
|
7
|
-
## Setup
|
|
8
|
-
|
|
9
|
-
### Easiest: turn attachments on via `<CopilotChat>`
|
|
10
|
-
|
|
11
|
-
```tsx
|
|
12
|
-
"use client";
|
|
13
|
-
import { CopilotChat } from "@copilotkit/react-core/v2";
|
|
14
|
-
import "@copilotkit/react-core/v2/styles.css";
|
|
15
|
-
|
|
16
|
-
export function ChatPanel() {
|
|
17
|
-
return (
|
|
18
|
-
<CopilotChat
|
|
19
|
-
agentId="default"
|
|
20
|
-
attachments={{
|
|
21
|
-
enabled: true,
|
|
22
|
-
accept: "image/*",
|
|
23
|
-
maxSize: 10 * 1024 * 1024, // 10 MB
|
|
24
|
-
onUploadFailed: ({ reason, file, message }) => {
|
|
25
|
-
console.warn(`[attachments] ${reason}: ${file.name} — ${message}`);
|
|
26
|
-
},
|
|
27
|
-
}}
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### Direct hook usage for custom surfaces
|
|
34
|
-
|
|
35
|
-
```tsx
|
|
36
|
-
"use client";
|
|
37
|
-
import {
|
|
38
|
-
useAttachments,
|
|
39
|
-
useAgent,
|
|
40
|
-
useCopilotKit,
|
|
41
|
-
} from "@copilotkit/react-core/v2";
|
|
42
|
-
import type { InputContent } from "@ag-ui/core";
|
|
43
|
-
|
|
44
|
-
export function CustomChatInput() {
|
|
45
|
-
const { agent } = useAgent({ agentId: "default" });
|
|
46
|
-
const { copilotkit } = useCopilotKit();
|
|
47
|
-
const {
|
|
48
|
-
attachments,
|
|
49
|
-
containerRef,
|
|
50
|
-
fileInputRef,
|
|
51
|
-
handleFileUpload,
|
|
52
|
-
handleDragOver,
|
|
53
|
-
handleDragLeave,
|
|
54
|
-
handleDrop,
|
|
55
|
-
removeAttachment,
|
|
56
|
-
consumeAttachments,
|
|
57
|
-
} = useAttachments({ config: { enabled: true, accept: "*/*" } });
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div
|
|
61
|
-
ref={containerRef}
|
|
62
|
-
onDragOver={handleDragOver}
|
|
63
|
-
onDragLeave={handleDragLeave}
|
|
64
|
-
onDrop={handleDrop}
|
|
65
|
-
>
|
|
66
|
-
<input type="file" ref={fileInputRef} onChange={handleFileUpload} />
|
|
67
|
-
{attachments.map((a) => (
|
|
68
|
-
<button key={a.id} onClick={() => removeAttachment(a.id)}>
|
|
69
|
-
{a.filename} ({a.status})
|
|
70
|
-
</button>
|
|
71
|
-
))}
|
|
72
|
-
<button
|
|
73
|
-
onClick={async () => {
|
|
74
|
-
const ready = consumeAttachments();
|
|
75
|
-
// `ready` is Attachment[] — map each to an AG-UI InputContent part
|
|
76
|
-
// before spreading into the message content array.
|
|
77
|
-
const contentParts: InputContent[] = [
|
|
78
|
-
{ type: "text", text: "See attachments." },
|
|
79
|
-
...ready.map(
|
|
80
|
-
(att) =>
|
|
81
|
-
({
|
|
82
|
-
type: att.type,
|
|
83
|
-
source: att.source,
|
|
84
|
-
metadata: {
|
|
85
|
-
...(att.filename ? { filename: att.filename } : {}),
|
|
86
|
-
...att.metadata,
|
|
87
|
-
},
|
|
88
|
-
}) as InputContent,
|
|
89
|
-
),
|
|
90
|
-
];
|
|
91
|
-
agent.addMessage({
|
|
92
|
-
id: crypto.randomUUID(),
|
|
93
|
-
role: "user",
|
|
94
|
-
content: contentParts,
|
|
95
|
-
});
|
|
96
|
-
await copilotkit.runAgent({ agent });
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
Send
|
|
100
|
-
</button>
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
`consumeAttachments()` returns the `Attachment[]` queue — each entry has
|
|
107
|
-
`{ id, type, source, filename, status, metadata }` and is NOT a valid
|
|
108
|
-
AG-UI content part. Map each attachment to an `InputContent` shape
|
|
109
|
-
(`{ type, source, metadata }`) before spreading into a message's `content`
|
|
110
|
-
array. See `packages/react-core/src/v2/components/chat/CopilotChat.tsx:247-268`
|
|
111
|
-
for the canonical transform.
|
|
112
|
-
|
|
113
|
-
## Core Patterns
|
|
114
|
-
|
|
115
|
-
### Custom upload backend (S3 / presigned URL)
|
|
116
|
-
|
|
117
|
-
`onUpload` replaces the default base64-inline strategy. Return an
|
|
118
|
-
`Attachment.source` describing where the file lives.
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
useAttachments({
|
|
122
|
-
config: {
|
|
123
|
-
enabled: true,
|
|
124
|
-
accept: "image/*,application/pdf",
|
|
125
|
-
maxSize: 50 * 1024 * 1024,
|
|
126
|
-
onUpload: async (file) => {
|
|
127
|
-
const { url } = await fetch("/api/upload", {
|
|
128
|
-
method: "POST",
|
|
129
|
-
body: file,
|
|
130
|
-
}).then((r) => r.json());
|
|
131
|
-
return { type: "url", value: url, mimeType: file.type };
|
|
132
|
-
},
|
|
133
|
-
onUploadFailed: ({ reason, file, message }) => {
|
|
134
|
-
toast.error(`${file.name}: ${message}`);
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Upload concurrency
|
|
141
|
-
|
|
142
|
-
Multi-file selections upload one file at a time by default, and the whole
|
|
143
|
-
selection is queued as `status: "uploading"` right away. Raise
|
|
144
|
-
`maxConcurrentUploads` to run several together — the rest then start as slots
|
|
145
|
-
free up, `Infinity` lifts the limit, and `onUpload` may be called concurrently,
|
|
146
|
-
so above `1` it must not assume the previous file finished. The pool is per hook,
|
|
147
|
-
not per call, so a paste landing mid-upload shares the same slots instead of
|
|
148
|
-
opening its own.
|
|
149
|
-
|
|
150
|
-
```tsx
|
|
151
|
-
useAttachments({
|
|
152
|
-
config: {
|
|
153
|
-
enabled: true,
|
|
154
|
-
maxConcurrentUploads: 6,
|
|
155
|
-
onUpload: async (file) => uploadToStorage(file),
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Feedback on failed uploads
|
|
161
|
-
|
|
162
|
-
```tsx
|
|
163
|
-
useAttachments({
|
|
164
|
-
config: {
|
|
165
|
-
enabled: true,
|
|
166
|
-
maxSize: 5 * 1024 * 1024,
|
|
167
|
-
onUploadFailed: ({ reason, file, message }) => {
|
|
168
|
-
// reason: "file-too-large" | "invalid-type" | "upload-failed"
|
|
169
|
-
toast.error(message);
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Common Mistakes
|
|
176
|
-
|
|
177
|
-
### HIGH — Forgetting to call `consumeAttachments` on submit
|
|
178
|
-
|
|
179
|
-
Wrong:
|
|
180
|
-
|
|
181
|
-
```tsx
|
|
182
|
-
const { attachments } = useAttachments({ config: { enabled: true } });
|
|
183
|
-
const onSubmit = () => {
|
|
184
|
-
sendMessage({ text, attachments });
|
|
185
|
-
// attachments queue never cleared — sticks around for the next message
|
|
186
|
-
};
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
Correct:
|
|
190
|
-
|
|
191
|
-
```tsx
|
|
192
|
-
const { consumeAttachments } = useAttachments({ config: { enabled: true } });
|
|
193
|
-
const onSubmit = () => {
|
|
194
|
-
const ready = consumeAttachments();
|
|
195
|
-
sendMessage({ text, attachments: ready });
|
|
196
|
-
};
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
`consumeAttachments()` returns ready attachments AND drains the internal
|
|
200
|
-
queue. If you submit without calling it, attachments stay in state and
|
|
201
|
-
accompany every subsequent message.
|
|
202
|
-
|
|
203
|
-
Source: `packages/react-core/src/v2/hooks/use-attachments.tsx:40-46`
|
|
204
|
-
|
|
205
|
-
### HIGH — Passing `maxSize` in KB or MB
|
|
206
|
-
|
|
207
|
-
Wrong:
|
|
208
|
-
|
|
209
|
-
```tsx
|
|
210
|
-
useAttachments({ config: { enabled: true, maxSize: 10 } });
|
|
211
|
-
// 10 bytes! Effectively blocks every file.
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
Correct:
|
|
215
|
-
|
|
216
|
-
```tsx
|
|
217
|
-
useAttachments({
|
|
218
|
-
config: { enabled: true, maxSize: 10 * 1024 * 1024 }, // 10 MB
|
|
219
|
-
});
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
`maxSize` is bytes. The default is `20 * 1024 * 1024` (20 MB). Passing a
|
|
223
|
-
small number without the multiplier silently rejects every file via
|
|
224
|
-
`onUploadFailed({ reason: "file-too-large" })`.
|
|
225
|
-
|
|
226
|
-
Source: `packages/react-core/src/v2/hooks/use-attachments.tsx:73-74`
|
|
227
|
-
|
|
228
|
-
### HIGH — Missing `containerRef` on the paste-scope element
|
|
229
|
-
|
|
230
|
-
Wrong:
|
|
231
|
-
|
|
232
|
-
```tsx
|
|
233
|
-
const { enabled } = useAttachments({ config: { enabled: true } });
|
|
234
|
-
return (
|
|
235
|
-
<div>
|
|
236
|
-
<input type="text" />
|
|
237
|
-
</div>
|
|
238
|
-
); // no containerRef attached
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
Correct:
|
|
242
|
-
|
|
243
|
-
```tsx
|
|
244
|
-
const { containerRef, handleDragOver, handleDrop } = useAttachments({
|
|
245
|
-
config: { enabled: true },
|
|
246
|
-
});
|
|
247
|
-
return (
|
|
248
|
-
<div ref={containerRef} onDragOver={handleDragOver} onDrop={handleDrop}>
|
|
249
|
-
<input type="text" />
|
|
250
|
-
</div>
|
|
251
|
-
);
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
Clipboard paste is scoped to the element `containerRef` points at. Without
|
|
255
|
-
attaching the ref, `Ctrl+V` / `Cmd+V` never reaches the paste handler and
|
|
256
|
-
users silently can't paste images from screenshots.
|
|
257
|
-
|
|
258
|
-
Source: `packages/react-core/src/v2/hooks/use-attachments.tsx:207-239`
|
|
259
|
-
|
|
260
|
-
### MEDIUM — Using `imageUploadsEnabled` on `<CopilotChat>`
|
|
261
|
-
|
|
262
|
-
Wrong:
|
|
263
|
-
|
|
264
|
-
```tsx
|
|
265
|
-
<CopilotChat imageUploadsEnabled />
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Correct:
|
|
269
|
-
|
|
270
|
-
```tsx
|
|
271
|
-
<CopilotChat
|
|
272
|
-
attachments={{
|
|
273
|
-
enabled: true,
|
|
274
|
-
accept: "image/*",
|
|
275
|
-
maxSize: 5 * 1024 * 1024,
|
|
276
|
-
}}
|
|
277
|
-
/>
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
`imageUploadsEnabled` was the v1 flag. v2 replaces it with the `attachments`
|
|
281
|
-
config object, which is powered by `useAttachments` internally and supports
|
|
282
|
-
any MIME type, not only images.
|
|
283
|
-
|
|
284
|
-
Source: `docs/content/docs/(root)/migration-guides/migrate-attachments.mdx`
|
|
285
|
-
|
|
286
|
-
### MEDIUM — Ignoring `onUploadFailed`
|
|
287
|
-
|
|
288
|
-
Wrong:
|
|
289
|
-
|
|
290
|
-
```tsx
|
|
291
|
-
useAttachments({ config: { enabled: true } });
|
|
292
|
-
// Rejected files silently disappear. User has no idea why.
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Correct:
|
|
296
|
-
|
|
297
|
-
```tsx
|
|
298
|
-
useAttachments({
|
|
299
|
-
config: {
|
|
300
|
-
enabled: true,
|
|
301
|
-
onUploadFailed: ({ reason, file, message }) => {
|
|
302
|
-
toast.error(message);
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
Size violations, MIME mismatches, and `onUpload` throws all drop the file
|
|
309
|
-
from the queue with no UI feedback unless `onUploadFailed` is wired.
|
|
310
|
-
|
|
311
|
-
Source: `packages/react-core/src/v2/hooks/use-attachments.tsx:79-157`
|