@copilotkit/react-core 1.56.0 → 1.56.2-canary.pin-to-send
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-BebqQrYT.mjs → copilotkit-BBYbekCa.mjs} +265 -76
- package/dist/copilotkit-BBYbekCa.mjs.map +1 -0
- package/dist/{copilotkit-Cvb6WpAX.cjs → copilotkit-D5JT2Pu3.cjs} +264 -75
- package/dist/copilotkit-D5JT2Pu3.cjs.map +1 -0
- package/dist/{copilotkit-f2Uq0RwG.d.mts → copilotkit-DArT2Iuw.d.mts} +71 -18
- package/dist/copilotkit-DArT2Iuw.d.mts.map +1 -0
- package/dist/{copilotkit-Dv8zU8_U.d.cts → copilotkit-KEc28l8G.d.cts} +71 -18
- package/dist/copilotkit-KEc28l8G.d.cts.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.umd.js +30 -46
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +264 -79
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/components/CopilotListeners.tsx +15 -4
- package/src/components/__tests__/CopilotListeners.test.tsx +38 -0
- package/src/v2/components/chat/CopilotChat.tsx +80 -4
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +4 -4
- package/src/v2/components/chat/CopilotChatInput.tsx +43 -2
- package/src/v2/components/chat/CopilotChatView.tsx +206 -11
- package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +66 -0
- package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +300 -2
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +72 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +38 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +56 -0
- package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +94 -0
- package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -1
- package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +37 -0
- package/src/v2/components/chat/index.ts +2 -0
- package/src/v2/components/chat/last-user-message-context.ts +21 -0
- package/src/v2/components/chat/normalize-auto-scroll.ts +17 -0
- package/src/v2/components/license-warning-banner.tsx +20 -1
- package/src/v2/components/ui/button.tsx +12 -11
- package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +6 -0
- package/src/v2/hooks/__tests__/use-agent-thread-isolation.test.tsx +6 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +76 -50
- package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +219 -0
- package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +55 -0
- package/src/v2/hooks/__tests__/use-threads.test.tsx +68 -0
- package/src/v2/hooks/use-agent.tsx +34 -77
- package/src/v2/hooks/use-pin-to-send.ts +94 -0
- package/src/v2/hooks/use-render-custom-messages.tsx +1 -1
- package/src/v2/hooks/use-render-tool-call.tsx +3 -0
- package/src/v2/hooks/use-render-tool.tsx +3 -0
- package/src/v2/hooks/use-threads.tsx +55 -12
- package/src/v2/providers/CopilotKitProvider.tsx +2 -11
- package/src/v2/types/defineToolCallRenderer.ts +3 -0
- package/src/v2/types/react-tool-call-renderer.ts +3 -0
- package/dist/copilotkit-BebqQrYT.mjs.map +0 -1
- package/dist/copilotkit-Cvb6WpAX.cjs.map +0 -1
- package/dist/copilotkit-Dv8zU8_U.d.cts.map +0 -1
- package/dist/copilotkit-f2Uq0RwG.d.mts.map +0 -1
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { useCopilotKit } from "../../providers/CopilotKitProvider";
|
|
5
|
+
import { CopilotKitCoreRuntimeConnectionStatus } from "@copilotkit/core";
|
|
5
6
|
|
|
6
7
|
vi.mock("../../providers/CopilotKitProvider", () => ({
|
|
7
8
|
useCopilotKit: vi.fn(),
|
|
@@ -148,6 +149,7 @@ function setupCopilotKit(runtimeUrl = "http://localhost:4000") {
|
|
|
148
149
|
mockUseCopilotKit.mockReturnValue({
|
|
149
150
|
copilotkit: {
|
|
150
151
|
runtimeUrl,
|
|
152
|
+
runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
151
153
|
headers: { Authorization: "Bearer test-token" },
|
|
152
154
|
intelligence: {
|
|
153
155
|
wsUrl: "ws://localhost:4000/client",
|
|
@@ -484,4 +486,70 @@ describe("useThreads", () => {
|
|
|
484
486
|
expect(channel.left).toBe(true);
|
|
485
487
|
expect(socket.disconnected).toBe(true);
|
|
486
488
|
});
|
|
489
|
+
|
|
490
|
+
it("waits for runtimeConnectionStatus=Connected before fetching /threads", async () => {
|
|
491
|
+
// Start in Connecting — hook should hold off on dispatching any request
|
|
492
|
+
// so the initial list fetch includes wsUrl and avoids a redundant second
|
|
493
|
+
// call once /info resolves.
|
|
494
|
+
mockUseCopilotKit.mockReturnValue({
|
|
495
|
+
copilotkit: {
|
|
496
|
+
runtimeUrl: "http://localhost:4000",
|
|
497
|
+
runtimeConnectionStatus:
|
|
498
|
+
CopilotKitCoreRuntimeConnectionStatus.Connecting,
|
|
499
|
+
headers: { Authorization: "Bearer test-token" },
|
|
500
|
+
intelligence: undefined,
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
fetchMock
|
|
505
|
+
.mockReturnValueOnce(
|
|
506
|
+
jsonResponse({ threads: sampleThreads, joinCode: "jc-1" }),
|
|
507
|
+
)
|
|
508
|
+
.mockReturnValueOnce(jsonResponse({ joinToken: "jt-1" }));
|
|
509
|
+
|
|
510
|
+
const { result, rerender } = renderHook(() => useThreads(defaultInput));
|
|
511
|
+
|
|
512
|
+
// Give effects a tick to settle; no fetch should occur while Connecting.
|
|
513
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
514
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
515
|
+
|
|
516
|
+
// While waiting for Connected, the hook must surface isLoading=true so
|
|
517
|
+
// consumers don't render an empty-state flash before the first fetch
|
|
518
|
+
// is even dispatched. The store's own isLoading is false at this
|
|
519
|
+
// point (no contextChanged action yet), so the hook synthesizes it.
|
|
520
|
+
expect(result.current.isLoading).toBe(true);
|
|
521
|
+
expect(result.current.threads).toEqual([]);
|
|
522
|
+
|
|
523
|
+
// Flip to Connected with wsUrl populated, re-render. The effect now
|
|
524
|
+
// dispatches exactly one list fetch (+ one subscribe after it lands).
|
|
525
|
+
mockUseCopilotKit.mockReturnValue({
|
|
526
|
+
copilotkit: {
|
|
527
|
+
runtimeUrl: "http://localhost:4000",
|
|
528
|
+
runtimeConnectionStatus:
|
|
529
|
+
CopilotKitCoreRuntimeConnectionStatus.Connected,
|
|
530
|
+
headers: { Authorization: "Bearer test-token" },
|
|
531
|
+
intelligence: { wsUrl: "ws://localhost:4000/client" },
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
rerender();
|
|
536
|
+
|
|
537
|
+
await waitFor(() => {
|
|
538
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
539
|
+
expect.stringContaining("/threads?agentId=agent-1"),
|
|
540
|
+
expect.objectContaining({ method: "GET" }),
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Exactly the expected pair — no speculative list call before Connected.
|
|
545
|
+
const listCalls = fetchMock.mock.calls.filter(
|
|
546
|
+
([url]) => typeof url === "string" && /\/threads\?agentId=/.test(url),
|
|
547
|
+
);
|
|
548
|
+
expect(listCalls).toHaveLength(1);
|
|
549
|
+
|
|
550
|
+
// After the fetch settles, isLoading returns to false.
|
|
551
|
+
await waitFor(() => {
|
|
552
|
+
expect(result.current.isLoading).toBe(false);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
487
555
|
});
|
|
@@ -6,6 +6,7 @@ import { AbstractAgent, HttpAgent } from "@ag-ui/client";
|
|
|
6
6
|
import {
|
|
7
7
|
ProxiedCopilotRuntimeAgent,
|
|
8
8
|
CopilotKitCoreRuntimeConnectionStatus,
|
|
9
|
+
type SubscribeToAgentSubscriber,
|
|
9
10
|
} from "@copilotkit/core";
|
|
10
11
|
|
|
11
12
|
export enum UseAgentUpdate {
|
|
@@ -25,23 +26,26 @@ export interface UseAgentProps {
|
|
|
25
26
|
threadId?: string;
|
|
26
27
|
updates?: UseAgentUpdate[];
|
|
27
28
|
/**
|
|
28
|
-
* Throttle interval (in milliseconds) for
|
|
29
|
-
* `
|
|
30
|
-
* during high-frequency
|
|
29
|
+
* Throttle interval (in milliseconds) for re-renders triggered by
|
|
30
|
+
* `onMessagesChanged` and `onStateChanged` notifications. Useful to reduce
|
|
31
|
+
* re-render frequency during high-frequency streaming updates.
|
|
31
32
|
*
|
|
32
|
-
* Uses leading+trailing
|
|
33
|
-
* within the window are coalesced,
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* Uses a leading+trailing pattern with a shared window — first update
|
|
34
|
+
* fires immediately, subsequent updates within the window are coalesced,
|
|
35
|
+
* and a trailing timer ensures the most recent update fires after the
|
|
36
|
+
* window expires. See `CopilotKitCore.subscribeToAgentWithOptions` in `@copilotkit/core`
|
|
37
|
+
* for details.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* always fire immediately. If `updates` does not include
|
|
42
|
-
* `OnMessagesChanged`, this property has no effect.
|
|
39
|
+
* Resolved as: `throttleMs ?? provider defaultThrottleMs ?? 0`.
|
|
40
|
+
* Passing `throttleMs={0}` explicitly disables throttling even when the
|
|
41
|
+
* provider specifies a non-zero `defaultThrottleMs`.
|
|
43
42
|
*
|
|
44
|
-
*
|
|
43
|
+
* Run lifecycle callbacks (`onRunInitialized`, `onRunFinalized`,
|
|
44
|
+
* `onRunFailed`, `onRunErrorEvent`) always fire immediately.
|
|
45
|
+
*
|
|
46
|
+
* @default undefined
|
|
47
|
+
* When unset, inherits from the provider's `defaultThrottleMs`;
|
|
48
|
+
* if that is also unset, the effective value is `0` (no throttle).
|
|
45
49
|
*/
|
|
46
50
|
throttleMs?: number;
|
|
47
51
|
}
|
|
@@ -123,6 +127,9 @@ export function useAgent({
|
|
|
123
127
|
agentId ??= DEFAULT_AGENT_ID;
|
|
124
128
|
|
|
125
129
|
const { copilotkit } = useCopilotKit();
|
|
130
|
+
// Read the provider-level default so it appears in the effect's dep array.
|
|
131
|
+
// subscribeToAgentWithOptions reads it from the core instance, but React needs the dep
|
|
132
|
+
// to know when to re-subscribe.
|
|
126
133
|
const providerThrottleMs = copilotkit.defaultThrottleMs;
|
|
127
134
|
// Fall back to the enclosing CopilotChatConfigurationProvider's threadId so
|
|
128
135
|
// that useAgent() called without explicit threadId (e.g. inside a custom
|
|
@@ -131,23 +138,6 @@ export function useAgent({
|
|
|
131
138
|
const chatConfig = useCopilotChatConfiguration();
|
|
132
139
|
threadId ??= chatConfig?.threadId;
|
|
133
140
|
|
|
134
|
-
const effectiveThrottleMs = useMemo(() => {
|
|
135
|
-
const resolved = throttleMs ?? providerThrottleMs ?? 0;
|
|
136
|
-
if (!Number.isFinite(resolved) || resolved < 0) {
|
|
137
|
-
// When both throttleMs and providerThrottleMs are undefined, resolved
|
|
138
|
-
// is 0 which passes validation — so one of them must be defined here.
|
|
139
|
-
const source =
|
|
140
|
-
throttleMs !== undefined
|
|
141
|
-
? "hook-level throttleMs"
|
|
142
|
-
: "provider-level defaultThrottleMs";
|
|
143
|
-
console.error(
|
|
144
|
-
`useAgent: ${source} must be a non-negative finite number, got ${resolved}. Falling back to unthrottled.`,
|
|
145
|
-
);
|
|
146
|
-
return 0;
|
|
147
|
-
}
|
|
148
|
-
return resolved;
|
|
149
|
-
}, [throttleMs, providerThrottleMs]);
|
|
150
|
-
|
|
151
141
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
152
142
|
|
|
153
143
|
const updateFlags = useMemo(
|
|
@@ -277,9 +267,8 @@ export function useAgent({
|
|
|
277
267
|
useEffect(() => {
|
|
278
268
|
if (updateFlags.length === 0) return;
|
|
279
269
|
|
|
280
|
-
const handlers: Parameters<AbstractAgent["subscribe"]>[0] = {};
|
|
281
|
-
let timerId: ReturnType<typeof setTimeout> | null = null;
|
|
282
270
|
let active = true;
|
|
271
|
+
const handlers: SubscribeToAgentSubscriber = {};
|
|
283
272
|
|
|
284
273
|
// Microtask-batched forceUpdate: coalesces multiple synchronous
|
|
285
274
|
// notifications (e.g. OnStateChanged + OnRunStatusChanged firing in the
|
|
@@ -301,45 +290,7 @@ export function useAgent({
|
|
|
301
290
|
};
|
|
302
291
|
|
|
303
292
|
if (updateFlags.includes(UseAgentUpdate.OnMessagesChanged)) {
|
|
304
|
-
|
|
305
|
-
if (ms > 0) {
|
|
306
|
-
// Throttled onMessagesChanged: leading+trailing pattern.
|
|
307
|
-
// First notification fires immediately, subsequent ones within the
|
|
308
|
-
// window are coalesced. Trailing timer fires after the window to
|
|
309
|
-
// ensure the final state is rendered.
|
|
310
|
-
let throttleActive = false;
|
|
311
|
-
// Tracks whether a notification arrived during the throttle window,
|
|
312
|
-
// so the trailing timer knows whether a re-render is needed.
|
|
313
|
-
let pending = false;
|
|
314
|
-
|
|
315
|
-
const throttledNotify = () => {
|
|
316
|
-
if (!active) return;
|
|
317
|
-
if (!throttleActive) {
|
|
318
|
-
// Leading edge — fire immediately and start the throttle window
|
|
319
|
-
throttleActive = true;
|
|
320
|
-
pending = false;
|
|
321
|
-
forceUpdate();
|
|
322
|
-
timerId = setTimeout(function trailingEdge() {
|
|
323
|
-
timerId = null;
|
|
324
|
-
if (active && pending) {
|
|
325
|
-
// Trailing edge — fire and restart the window
|
|
326
|
-
pending = false;
|
|
327
|
-
forceUpdate();
|
|
328
|
-
timerId = setTimeout(trailingEdge, ms);
|
|
329
|
-
} else {
|
|
330
|
-
// No pending notifications — end the window
|
|
331
|
-
throttleActive = false;
|
|
332
|
-
}
|
|
333
|
-
}, ms);
|
|
334
|
-
} else {
|
|
335
|
-
pending = true;
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
handlers.onMessagesChanged = throttledNotify;
|
|
340
|
-
} else {
|
|
341
|
-
handlers.onMessagesChanged = forceUpdate;
|
|
342
|
-
}
|
|
293
|
+
handlers.onMessagesChanged = forceUpdate;
|
|
343
294
|
}
|
|
344
295
|
|
|
345
296
|
if (updateFlags.includes(UseAgentUpdate.OnStateChanged)) {
|
|
@@ -350,18 +301,24 @@ export function useAgent({
|
|
|
350
301
|
handlers.onRunInitialized = batchedForceUpdate;
|
|
351
302
|
handlers.onRunFinalized = batchedForceUpdate;
|
|
352
303
|
handlers.onRunFailed = batchedForceUpdate;
|
|
304
|
+
// Protocol-level RUN_ERROR event (distinct from onRunFailed which
|
|
305
|
+
// handles local exceptions like network errors).
|
|
306
|
+
handlers.onRunErrorEvent = batchedForceUpdate;
|
|
353
307
|
}
|
|
354
308
|
|
|
355
|
-
const subscription =
|
|
309
|
+
const subscription = copilotkit.subscribeToAgentWithOptions(
|
|
310
|
+
agent,
|
|
311
|
+
handlers,
|
|
312
|
+
{
|
|
313
|
+
throttleMs,
|
|
314
|
+
},
|
|
315
|
+
);
|
|
356
316
|
return () => {
|
|
357
317
|
active = false;
|
|
358
|
-
if (timerId !== null) {
|
|
359
|
-
clearTimeout(timerId);
|
|
360
|
-
}
|
|
361
318
|
subscription.unsubscribe();
|
|
362
319
|
};
|
|
363
320
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
364
|
-
}, [agent, forceUpdate,
|
|
321
|
+
}, [agent, forceUpdate, throttleMs, providerThrottleMs, updateFlags]);
|
|
365
322
|
|
|
366
323
|
// Keep HttpAgent headers fresh without mutating inside useMemo, which is
|
|
367
324
|
// unsafe in concurrent mode (React may invoke useMemo multiple times and
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from "react";
|
|
2
|
+
import { LastUserMessageContext } from "../components/chat/last-user-message-context";
|
|
3
|
+
|
|
4
|
+
export type UsePinToSendOptions = {
|
|
5
|
+
scrollRef: React.RefObject<HTMLElement | null>;
|
|
6
|
+
contentRef: React.RefObject<HTMLElement | null>;
|
|
7
|
+
spacerRef: React.RefObject<HTMLElement | null>;
|
|
8
|
+
topOffset?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function usePinToSend({
|
|
12
|
+
scrollRef,
|
|
13
|
+
contentRef,
|
|
14
|
+
spacerRef,
|
|
15
|
+
topOffset = 16,
|
|
16
|
+
}: UsePinToSendOptions): void {
|
|
17
|
+
const { id, sendNonce } = useContext(LastUserMessageContext);
|
|
18
|
+
const lastNonceRef = useRef<number>(-1);
|
|
19
|
+
const currentSpacerHeightRef = useRef<number>(0);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (sendNonce === lastNonceRef.current) return;
|
|
23
|
+
lastNonceRef.current = sendNonce;
|
|
24
|
+
|
|
25
|
+
if (!id) return;
|
|
26
|
+
const scrollEl = scrollRef.current;
|
|
27
|
+
const contentEl = contentRef.current;
|
|
28
|
+
const spacerEl = spacerRef.current;
|
|
29
|
+
if (!scrollEl || !contentEl || !spacerEl) return;
|
|
30
|
+
|
|
31
|
+
const escaped =
|
|
32
|
+
typeof CSS !== "undefined" && CSS.escape
|
|
33
|
+
? CSS.escape(id)
|
|
34
|
+
: id.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, "\\$&");
|
|
35
|
+
const targetEl = contentEl.querySelector<HTMLElement>(
|
|
36
|
+
`[data-message-id="${escaped}"]`,
|
|
37
|
+
);
|
|
38
|
+
if (!targetEl) return;
|
|
39
|
+
|
|
40
|
+
// The target message's element has a top padding (e.g. `pt-10`) that
|
|
41
|
+
// creates breathing room above the visible bubble. When we "anchor at
|
|
42
|
+
// the top", we mean anchor the *bubble*, not the element's padded box.
|
|
43
|
+
// So we scroll past the padding (it goes above the viewport, hiding
|
|
44
|
+
// whatever was above the element too — including the previous message's
|
|
45
|
+
// trailing copy button).
|
|
46
|
+
const viewportHeight = scrollEl.clientHeight;
|
|
47
|
+
const userMessageHeight = targetEl.getBoundingClientRect().height;
|
|
48
|
+
const paddingTop = parseFloat(getComputedStyle(targetEl).paddingTop) || 0;
|
|
49
|
+
const bubbleHeight = Math.max(0, userMessageHeight - paddingTop);
|
|
50
|
+
const spacerHeight = Math.max(0, viewportHeight - bubbleHeight - topOffset);
|
|
51
|
+
|
|
52
|
+
spacerEl.style.height = `${spacerHeight}px`;
|
|
53
|
+
currentSpacerHeightRef.current = spacerHeight;
|
|
54
|
+
|
|
55
|
+
const raf = requestAnimationFrame(() => {
|
|
56
|
+
// Scroll so the BUBBLE is `topOffset` from the viewport top — the
|
|
57
|
+
// padding above the bubble ends up scrolled off-screen.
|
|
58
|
+
const targetTop =
|
|
59
|
+
computeOffsetTop(targetEl, scrollEl) + paddingTop - topOffset;
|
|
60
|
+
scrollEl.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Shrink-only ResizeObserver: as the assistant response grows below the
|
|
64
|
+
// anchored user message, collapse the spacer by the same amount so total
|
|
65
|
+
// scrollable space below the bubble stays constant (and the bubble stays
|
|
66
|
+
// pinned). Never grow the spacer after initial sizing.
|
|
67
|
+
const ro = new ResizeObserver(() => {
|
|
68
|
+
if (!contentEl || !spacerEl || !scrollEl) return;
|
|
69
|
+
const contentHeight = contentEl.getBoundingClientRect().height;
|
|
70
|
+
const targetOffsetWithinContent = computeOffsetTop(targetEl, contentEl);
|
|
71
|
+
const consumedBelow =
|
|
72
|
+
contentHeight - targetOffsetWithinContent - userMessageHeight;
|
|
73
|
+
const remaining = Math.max(0, spacerHeight - consumedBelow);
|
|
74
|
+
if (remaining < currentSpacerHeightRef.current) {
|
|
75
|
+
spacerEl.style.height = `${remaining}px`;
|
|
76
|
+
currentSpacerHeightRef.current = remaining;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
ro.observe(contentEl);
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
cancelAnimationFrame(raf);
|
|
83
|
+
ro.disconnect();
|
|
84
|
+
};
|
|
85
|
+
}, [id, sendNonce, scrollRef, contentRef, spacerRef, topOffset]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Compute the offset of el relative to stopAt, accounting for stopAt's current scrollTop.
|
|
89
|
+
// Uses getBoundingClientRect so it works regardless of CSS positioning (including position:static).
|
|
90
|
+
function computeOffsetTop(el: HTMLElement, stopAt: HTMLElement): number {
|
|
91
|
+
const elRect = el.getBoundingClientRect();
|
|
92
|
+
const stopRect = stopAt.getBoundingClientRect();
|
|
93
|
+
return elRect.top - stopRect.top + stopAt.scrollTop;
|
|
94
|
+
}
|
|
@@ -45,7 +45,7 @@ export function useRenderCustomMessages() {
|
|
|
45
45
|
const registryAgent = copilotkit.getAgent(agentId);
|
|
46
46
|
const agent = getThreadClone(registryAgent, threadId) ?? registryAgent;
|
|
47
47
|
if (!agent) {
|
|
48
|
-
|
|
48
|
+
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const messagesIdsInRun = resolvedRunId
|
|
@@ -47,6 +47,7 @@ const ToolCallRenderer = React.memo(
|
|
|
47
47
|
return (
|
|
48
48
|
<RenderComponent
|
|
49
49
|
name={toolName}
|
|
50
|
+
toolCallId={toolCall.id}
|
|
50
51
|
args={args}
|
|
51
52
|
status={ToolCallStatus.Complete}
|
|
52
53
|
result={toolMessage.content}
|
|
@@ -56,6 +57,7 @@ const ToolCallRenderer = React.memo(
|
|
|
56
57
|
return (
|
|
57
58
|
<RenderComponent
|
|
58
59
|
name={toolName}
|
|
60
|
+
toolCallId={toolCall.id}
|
|
59
61
|
args={args}
|
|
60
62
|
status={ToolCallStatus.Executing}
|
|
61
63
|
result={undefined}
|
|
@@ -65,6 +67,7 @@ const ToolCallRenderer = React.memo(
|
|
|
65
67
|
return (
|
|
66
68
|
<RenderComponent
|
|
67
69
|
name={toolName}
|
|
70
|
+
toolCallId={toolCall.id}
|
|
68
71
|
args={args}
|
|
69
72
|
status={ToolCallStatus.InProgress}
|
|
70
73
|
result={undefined}
|
|
@@ -7,6 +7,7 @@ const EMPTY_DEPS: ReadonlyArray<unknown> = [];
|
|
|
7
7
|
|
|
8
8
|
export interface RenderToolInProgressProps<S extends StandardSchemaV1> {
|
|
9
9
|
name: string;
|
|
10
|
+
toolCallId: string;
|
|
10
11
|
parameters: Partial<InferSchemaOutput<S>>;
|
|
11
12
|
status: "inProgress";
|
|
12
13
|
result: undefined;
|
|
@@ -14,6 +15,7 @@ export interface RenderToolInProgressProps<S extends StandardSchemaV1> {
|
|
|
14
15
|
|
|
15
16
|
export interface RenderToolExecutingProps<S extends StandardSchemaV1> {
|
|
16
17
|
name: string;
|
|
18
|
+
toolCallId: string;
|
|
17
19
|
parameters: InferSchemaOutput<S>;
|
|
18
20
|
status: "executing";
|
|
19
21
|
result: undefined;
|
|
@@ -21,6 +23,7 @@ export interface RenderToolExecutingProps<S extends StandardSchemaV1> {
|
|
|
21
23
|
|
|
22
24
|
export interface RenderToolCompleteProps<S extends StandardSchemaV1> {
|
|
23
25
|
name: string;
|
|
26
|
+
toolCallId: string;
|
|
24
27
|
parameters: InferSchemaOutput<S>;
|
|
25
28
|
status: "complete";
|
|
26
29
|
result: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCopilotKit } from "../providers/CopilotKitProvider";
|
|
2
2
|
import {
|
|
3
|
+
CopilotKitCoreRuntimeConnectionStatus,
|
|
3
4
|
ɵcreateThreadStore,
|
|
4
5
|
ɵselectThreads,
|
|
5
6
|
ɵselectThreadsError,
|
|
@@ -30,6 +31,13 @@ export interface Thread {
|
|
|
30
31
|
archived: boolean;
|
|
31
32
|
createdAt: string;
|
|
32
33
|
updatedAt: string;
|
|
34
|
+
/**
|
|
35
|
+
* ISO-8601 timestamp of the most recent agent run on this thread. Absent
|
|
36
|
+
* when the thread has never been run. Prefer this over `updatedAt` for
|
|
37
|
+
* user-facing "last activity" displays — it is not bumped by metadata-only
|
|
38
|
+
* actions like rename or archive.
|
|
39
|
+
*/
|
|
40
|
+
lastRunAt?: string;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
@@ -179,13 +187,14 @@ export function useThreads({
|
|
|
179
187
|
const threads: Thread[] = useMemo(
|
|
180
188
|
() =>
|
|
181
189
|
coreThreads.map(
|
|
182
|
-
({ id, agentId, name, archived, createdAt, updatedAt }) => ({
|
|
190
|
+
({ id, agentId, name, archived, createdAt, updatedAt, lastRunAt }) => ({
|
|
183
191
|
id,
|
|
184
192
|
agentId,
|
|
185
193
|
name,
|
|
186
194
|
archived,
|
|
187
195
|
createdAt,
|
|
188
196
|
updatedAt,
|
|
197
|
+
...(lastRunAt !== undefined ? { lastRunAt } : {}),
|
|
189
198
|
}),
|
|
190
199
|
),
|
|
191
200
|
[coreThreads],
|
|
@@ -211,7 +220,18 @@ export function useThreads({
|
|
|
211
220
|
|
|
212
221
|
return new Error("Runtime URL is not configured");
|
|
213
222
|
}, [copilotkit.runtimeUrl]);
|
|
214
|
-
|
|
223
|
+
|
|
224
|
+
// Tracks whether we've dispatched the first real context to the store.
|
|
225
|
+
// The store itself starts with `isLoading: false`, so before we dispatch
|
|
226
|
+
// consumers would otherwise see an empty, non-loading state (empty-list
|
|
227
|
+
// flash). While runtimeUrl is set and we haven't dispatched yet, we
|
|
228
|
+
// synthesize `isLoading: true` so the UI keeps its loading indicator until
|
|
229
|
+
// the first fetch is in flight (at which point the store's own
|
|
230
|
+
// isLoading takes over).
|
|
231
|
+
const [hasDispatchedContext, setHasDispatchedContext] = useState(false);
|
|
232
|
+
const preConnectLoading = !!copilotkit.runtimeUrl && !hasDispatchedContext;
|
|
233
|
+
|
|
234
|
+
const isLoading = runtimeError ? false : preConnectLoading || storeIsLoading;
|
|
215
235
|
const error = runtimeError ?? storeError;
|
|
216
236
|
|
|
217
237
|
useEffect(() => {
|
|
@@ -221,22 +241,45 @@ export function useThreads({
|
|
|
221
241
|
};
|
|
222
242
|
}, [store]);
|
|
223
243
|
|
|
244
|
+
// Defer setting the context until the runtime reports Connected. Before
|
|
245
|
+
// `/info` resolves we don't know `intelligence.wsUrl`, so dispatching the
|
|
246
|
+
// context early would issue a list fetch with `wsUrl: undefined`, then a
|
|
247
|
+
// second list fetch (and a `/threads/subscribe`) once the flag lands.
|
|
248
|
+
// Waiting lets the hook issue just one `/threads?…` + one `/threads/subscribe`.
|
|
249
|
+
//
|
|
250
|
+
// When `runtimeUrl` is absent we dispatch `null` to clear the store. For
|
|
251
|
+
// transient states (Disconnected/Connecting/Error with a URL still set) we
|
|
252
|
+
// leave the previously-dispatched context in place — any in-flight
|
|
253
|
+
// realtime subscription or cached thread list stays usable while the
|
|
254
|
+
// runtime recovers, and we don't re-trigger a fetch storm on transitions.
|
|
255
|
+
const runtimeStatus = copilotkit.runtimeConnectionStatus;
|
|
224
256
|
useEffect(() => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
257
|
+
if (!copilotkit.runtimeUrl) {
|
|
258
|
+
store.setContext(null);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Wait for /info to land so we can include `wsUrl` in the initial
|
|
263
|
+
// context and avoid a redundant second list fetch.
|
|
264
|
+
if (runtimeStatus !== CopilotKitCoreRuntimeConnectionStatus.Connected) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const context: ɵThreadRuntimeContext = {
|
|
269
|
+
runtimeUrl: copilotkit.runtimeUrl,
|
|
270
|
+
headers: { ...copilotkit.headers },
|
|
271
|
+
wsUrl: copilotkit.intelligence?.wsUrl,
|
|
272
|
+
agentId,
|
|
273
|
+
includeArchived,
|
|
274
|
+
limit,
|
|
275
|
+
};
|
|
235
276
|
|
|
236
277
|
store.setContext(context);
|
|
278
|
+
setHasDispatchedContext(true);
|
|
237
279
|
}, [
|
|
238
280
|
store,
|
|
239
281
|
copilotkit.runtimeUrl,
|
|
282
|
+
runtimeStatus,
|
|
240
283
|
headersKey,
|
|
241
284
|
copilotkit.intelligence?.wsUrl,
|
|
242
285
|
agentId,
|
|
@@ -720,19 +720,10 @@ export const CopilotKitProvider: React.FC<CopilotKitProviderProps> = ({
|
|
|
720
720
|
}, []);
|
|
721
721
|
|
|
722
722
|
// Sync defaultThrottleMs to the core instance on prop changes.
|
|
723
|
-
// Initial value is set synchronously during instance creation (
|
|
724
|
-
// ref
|
|
723
|
+
// Initial value is set synchronously during instance creation (inside the
|
|
724
|
+
// ref-init block above) so child hooks see the correct value on first render.
|
|
725
725
|
// This effect handles subsequent updates when the prop changes.
|
|
726
726
|
useEffect(() => {
|
|
727
|
-
if (
|
|
728
|
-
defaultThrottleMs !== undefined &&
|
|
729
|
-
(!Number.isFinite(defaultThrottleMs) || defaultThrottleMs < 0)
|
|
730
|
-
) {
|
|
731
|
-
console.error(
|
|
732
|
-
`CopilotKitProvider: defaultThrottleMs must be a non-negative finite number, got ${defaultThrottleMs}. ` +
|
|
733
|
-
`useAgent hooks without an explicit throttleMs will fall back to unthrottled.`,
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
727
|
copilotkit.setDefaultThrottleMs(defaultThrottleMs);
|
|
737
728
|
}, [copilotkit, defaultThrottleMs]);
|
|
738
729
|
|
|
@@ -14,18 +14,21 @@ import { ToolCallStatus } from "@copilotkit/core";
|
|
|
14
14
|
type RenderProps<T> =
|
|
15
15
|
| {
|
|
16
16
|
name: string;
|
|
17
|
+
toolCallId: string;
|
|
17
18
|
args: Partial<T>;
|
|
18
19
|
status: ToolCallStatus.InProgress;
|
|
19
20
|
result: undefined;
|
|
20
21
|
}
|
|
21
22
|
| {
|
|
22
23
|
name: string;
|
|
24
|
+
toolCallId: string;
|
|
23
25
|
args: T;
|
|
24
26
|
status: ToolCallStatus.Executing;
|
|
25
27
|
result: undefined;
|
|
26
28
|
}
|
|
27
29
|
| {
|
|
28
30
|
name: string;
|
|
31
|
+
toolCallId: string;
|
|
29
32
|
args: T;
|
|
30
33
|
status: ToolCallStatus.Complete;
|
|
31
34
|
result: string;
|
|
@@ -12,18 +12,21 @@ export interface ReactToolCallRenderer<T = unknown> {
|
|
|
12
12
|
render: React.ComponentType<
|
|
13
13
|
| {
|
|
14
14
|
name: string;
|
|
15
|
+
toolCallId: string;
|
|
15
16
|
args: Partial<T>;
|
|
16
17
|
status: ToolCallStatus.InProgress;
|
|
17
18
|
result: undefined;
|
|
18
19
|
}
|
|
19
20
|
| {
|
|
20
21
|
name: string;
|
|
22
|
+
toolCallId: string;
|
|
21
23
|
args: T;
|
|
22
24
|
status: ToolCallStatus.Executing;
|
|
23
25
|
result: undefined;
|
|
24
26
|
}
|
|
25
27
|
| {
|
|
26
28
|
name: string;
|
|
29
|
+
toolCallId: string;
|
|
27
30
|
args: T;
|
|
28
31
|
status: ToolCallStatus.Complete;
|
|
29
32
|
result: string;
|