@copilotkit/react-core 1.55.2 → 1.55.3-canary.1776243725
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-Cd-NrDyp.mjs → copilotkit-Bm4ox8G0.mjs} +89 -42
- package/dist/copilotkit-Bm4ox8G0.mjs.map +1 -0
- package/dist/{copilotkit-Dgdpbqjt.cjs → copilotkit-BoOnQHlE.cjs} +93 -40
- package/dist/copilotkit-BoOnQHlE.cjs.map +1 -0
- package/dist/{copilotkit-dwDWYpya.d.cts → copilotkit-EfopO2gn.d.cts} +27 -9
- package/dist/copilotkit-EfopO2gn.d.cts.map +1 -0
- package/dist/{copilotkit-BuhSUZHb.d.mts → copilotkit-opur-20s.d.mts} +27 -9
- package/dist/copilotkit-opur-20s.d.mts.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 +36 -15
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/index.cjs +2 -1
- package/dist/v2/index.d.cts +2 -2
- package/dist/v2/index.d.mts +2 -2
- package/dist/v2/index.mjs +2 -2
- package/dist/v2/index.umd.js +87 -40
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +70 -0
- package/src/components/copilot-provider/copilot-messages.tsx +39 -24
- package/src/components/copilot-provider/copilotkit-props.tsx +9 -5
- package/src/components/copilot-provider/copilotkit.tsx +4 -1
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +18 -15
- package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +17 -4
- package/src/v2/components/chat/CopilotChatUserMessage.tsx +13 -10
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +131 -5
- package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +1 -1
- package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +241 -0
- package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +10 -10
- package/src/v2/hooks/__tests__/use-capabilities.test.tsx +76 -0
- package/src/v2/hooks/index.ts +1 -0
- package/src/v2/hooks/use-agent.tsx +23 -4
- package/src/v2/hooks/use-capabilities.tsx +25 -0
- package/src/v2/providers/CopilotKitProvider.tsx +6 -2
- package/dist/copilotkit-BuhSUZHb.d.mts.map +0 -1
- package/dist/copilotkit-Cd-NrDyp.mjs.map +0 -1
- package/dist/copilotkit-Dgdpbqjt.cjs.map +0 -1
- package/dist/copilotkit-dwDWYpya.d.cts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/react-core",
|
|
3
|
-
"version": "1.55.
|
|
3
|
+
"version": "1.55.3-canary.1776243725",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -73,11 +73,11 @@
|
|
|
73
73
|
"untruncate-json": "^0.0.1",
|
|
74
74
|
"use-stick-to-bottom": "^1.1.1",
|
|
75
75
|
"zod-to-json-schema": "^3.24.5",
|
|
76
|
-
"@copilotkit/a2ui-renderer": "1.55.
|
|
77
|
-
"@copilotkit/core": "1.55.
|
|
78
|
-
"@copilotkit/
|
|
79
|
-
"@copilotkit/
|
|
80
|
-
"@copilotkit/web-inspector": "1.55.
|
|
76
|
+
"@copilotkit/a2ui-renderer": "1.55.3-canary.1776243725",
|
|
77
|
+
"@copilotkit/core": "1.55.3-canary.1776243725",
|
|
78
|
+
"@copilotkit/shared": "1.55.3-canary.1776243725",
|
|
79
|
+
"@copilotkit/runtime-client-gql": "1.55.3-canary.1776243725",
|
|
80
|
+
"@copilotkit/web-inspector": "1.55.3-canary.1776243725"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.1.11",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ErrorVisibility } from "@copilotkit/shared";
|
|
3
|
+
import { getErrorSuppression } from "../copilot-messages";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression tests for #2431: error visibility when showDevConsole=false.
|
|
7
|
+
*
|
|
8
|
+
* The bug: `routeError` returned early for ALL errors when `isDev` was false,
|
|
9
|
+
* suppressing TOAST and BANNER errors that should always reach the user.
|
|
10
|
+
*
|
|
11
|
+
* The fix: only SILENT and DEV_ONLY errors are suppressed in production;
|
|
12
|
+
* TOAST, BANNER, and untagged errors are always surfaced.
|
|
13
|
+
*/
|
|
14
|
+
describe("getErrorSuppression — error visibility routing (#2431)", () => {
|
|
15
|
+
// --- Production (isDev = false) ---
|
|
16
|
+
|
|
17
|
+
it("surfaces TOAST errors in production", () => {
|
|
18
|
+
expect(getErrorSuppression(ErrorVisibility.TOAST, false)).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("surfaces BANNER errors in production", () => {
|
|
22
|
+
expect(getErrorSuppression(ErrorVisibility.BANNER, false)).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("suppresses DEV_ONLY errors in production", () => {
|
|
26
|
+
expect(getErrorSuppression(ErrorVisibility.DEV_ONLY, false)).not.toBeNull();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("suppresses SILENT errors in production", () => {
|
|
30
|
+
expect(getErrorSuppression(ErrorVisibility.SILENT, false)).not.toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("surfaces errors with no visibility tag in production", () => {
|
|
34
|
+
expect(getErrorSuppression(undefined, false)).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// --- Development (isDev = true) ---
|
|
38
|
+
|
|
39
|
+
it("surfaces TOAST errors in development", () => {
|
|
40
|
+
expect(getErrorSuppression(ErrorVisibility.TOAST, true)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("surfaces BANNER errors in development", () => {
|
|
44
|
+
expect(getErrorSuppression(ErrorVisibility.BANNER, true)).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("surfaces DEV_ONLY errors in development", () => {
|
|
48
|
+
expect(getErrorSuppression(ErrorVisibility.DEV_ONLY, true)).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("suppresses SILENT errors in development", () => {
|
|
52
|
+
expect(getErrorSuppression(ErrorVisibility.SILENT, true)).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("surfaces errors with no visibility tag in development", () => {
|
|
56
|
+
expect(getErrorSuppression(undefined, true)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// --- Log prefix strings ---
|
|
60
|
+
|
|
61
|
+
it("returns a 'Silent Error' prefix for SILENT visibility", () => {
|
|
62
|
+
const prefix = getErrorSuppression(ErrorVisibility.SILENT, false);
|
|
63
|
+
expect(prefix).toContain("Silent");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns a 'hidden in production' prefix for DEV_ONLY visibility", () => {
|
|
67
|
+
const prefix = getErrorSuppression(ErrorVisibility.DEV_ONLY, false);
|
|
68
|
+
expect(prefix).toContain("hidden in production");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -31,6 +31,33 @@ import {
|
|
|
31
31
|
} from "@copilotkit/shared";
|
|
32
32
|
import { Suggestion } from "@copilotkit/core";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Determine whether a GraphQL error should be suppressed based on its visibility
|
|
36
|
+
* and whether the dev console is active.
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` when the error should be surfaced to the UI, or a log prefix
|
|
39
|
+
* string when the error should be suppressed (logged to console only).
|
|
40
|
+
*
|
|
41
|
+
* Exported for unit testing.
|
|
42
|
+
*/
|
|
43
|
+
export function getErrorSuppression(
|
|
44
|
+
visibility: ErrorVisibility | undefined,
|
|
45
|
+
isDev: boolean,
|
|
46
|
+
): string | null {
|
|
47
|
+
// Silent errors are always suppressed
|
|
48
|
+
if (visibility === ErrorVisibility.SILENT) {
|
|
49
|
+
return "CopilotKit Silent Error:";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// DEV_ONLY errors are suppressed in production
|
|
53
|
+
if (!isDev && visibility === ErrorVisibility.DEV_ONLY) {
|
|
54
|
+
return "CopilotKit Error (hidden in production):";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// All other visibilities (TOAST, BANNER, undefined) are always surfaced
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
// Helper to determine if error should show as banner based on visibility and legacy patterns
|
|
35
62
|
function shouldShowAsBanner(gqlError: GraphQLError): boolean {
|
|
36
63
|
const extensions = gqlError.extensions;
|
|
@@ -224,20 +251,13 @@ export function CopilotMessages({ children }: { children: ReactNode }) {
|
|
|
224
251
|
const visibility = extensions?.visibility as ErrorVisibility;
|
|
225
252
|
const isDev = shouldShowDevConsole(showDevConsole);
|
|
226
253
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
gqlError.message,
|
|
231
|
-
);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Silent errors - just log
|
|
236
|
-
if (visibility === ErrorVisibility.SILENT) {
|
|
237
|
-
console.error("CopilotKit Silent Error:", gqlError.message);
|
|
254
|
+
const suppression = getErrorSuppression(visibility, isDev);
|
|
255
|
+
if (suppression) {
|
|
256
|
+
console.error(suppression, gqlError.message);
|
|
238
257
|
return;
|
|
239
258
|
}
|
|
240
259
|
|
|
260
|
+
// TOAST and BANNER errors are always surfaced, even in production
|
|
241
261
|
// All other errors (including DEV_ONLY) show as banners for consistency
|
|
242
262
|
const ckError = createStructuredError(gqlError);
|
|
243
263
|
if (ckError) {
|
|
@@ -259,19 +279,14 @@ export function CopilotMessages({ children }: { children: ReactNode }) {
|
|
|
259
279
|
// Process all errors as banners
|
|
260
280
|
graphQLErrors.forEach(routeError);
|
|
261
281
|
} else {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
setBannerError(fallbackError);
|
|
272
|
-
// Trace the non-GraphQL error
|
|
273
|
-
traceUIError(fallbackError, error);
|
|
274
|
-
}
|
|
282
|
+
// Non-GraphQL errors are always surfaced to the user
|
|
283
|
+
const fallbackError = new CopilotKitError({
|
|
284
|
+
message: error?.message || String(error),
|
|
285
|
+
code: CopilotKitErrorCode.UNKNOWN,
|
|
286
|
+
});
|
|
287
|
+
setBannerError(fallbackError);
|
|
288
|
+
// Trace the non-GraphQL error
|
|
289
|
+
traceUIError(fallbackError, error);
|
|
275
290
|
}
|
|
276
291
|
},
|
|
277
292
|
[setBannerError, showDevConsole, traceUIError],
|
|
@@ -68,15 +68,19 @@ export interface CopilotKitProps extends Omit<
|
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Additional headers to be sent with the request.
|
|
71
|
+
* Can be a static object or a function that returns headers dynamically
|
|
72
|
+
* (useful for refreshing auth tokens).
|
|
71
73
|
*
|
|
72
74
|
* For example:
|
|
73
|
-
* ```
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
* ```tsx
|
|
76
|
+
* // Static headers
|
|
77
|
+
* headers={{ "Authorization": "Bearer X" }}
|
|
78
|
+
*
|
|
79
|
+
* // Dynamic headers (re-evaluated on each render)
|
|
80
|
+
* headers={() => ({ "Authorization": `Bearer ${getToken()}` })}
|
|
77
81
|
* ```
|
|
78
82
|
*/
|
|
79
|
-
headers?: Record<string, string
|
|
83
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
80
84
|
|
|
81
85
|
/**
|
|
82
86
|
* The children to be rendered within the CopilotKit.
|
|
@@ -311,7 +311,10 @@ export function CopilotKitInternal(cpkProps: CopilotKitProps) {
|
|
|
311
311
|
publicApiKey: publicApiKey,
|
|
312
312
|
...(cloud ? { cloud } : {}),
|
|
313
313
|
chatApiEndpoint: chatApiEndpoint,
|
|
314
|
-
headers:
|
|
314
|
+
headers:
|
|
315
|
+
typeof props.headers === "function"
|
|
316
|
+
? props.headers()
|
|
317
|
+
: props.headers || {},
|
|
315
318
|
properties: props.properties || {},
|
|
316
319
|
transcribeAudioUrl: props.transcribeAudioUrl,
|
|
317
320
|
textToSpeechUrl: props.textToSpeechUrl,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { useKatexStyles } from "../../hooks/useKatexStyles";
|
|
23
23
|
import { WithSlots, renderSlot } from "../../lib/slots";
|
|
24
24
|
import { Streamdown } from "streamdown";
|
|
25
|
+
import { copyToClipboard } from "@copilotkit/shared";
|
|
25
26
|
import CopilotChatToolCallsView from "./CopilotChatToolCallsView";
|
|
26
27
|
|
|
27
28
|
export type CopilotChatAssistantMessageProps = WithSlots<
|
|
@@ -86,12 +87,9 @@ export function CopilotChatAssistantMessage({
|
|
|
86
87
|
{
|
|
87
88
|
onClick: async () => {
|
|
88
89
|
if (message.content) {
|
|
89
|
-
|
|
90
|
-
await navigator.clipboard.writeText(message.content);
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.error("Failed to copy message:", err);
|
|
93
|
-
}
|
|
90
|
+
return await copyToClipboard(message.content);
|
|
94
91
|
}
|
|
92
|
+
return false;
|
|
95
93
|
},
|
|
96
94
|
},
|
|
97
95
|
);
|
|
@@ -275,18 +273,23 @@ export namespace CopilotChatAssistantMessage {
|
|
|
275
273
|
};
|
|
276
274
|
}, []);
|
|
277
275
|
|
|
278
|
-
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
279
|
-
|
|
280
|
-
if (
|
|
281
|
-
|
|
276
|
+
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
277
|
+
let success = false;
|
|
278
|
+
if (onClick) {
|
|
279
|
+
// onClick may return a boolean indicating copy success
|
|
280
|
+
const result = await Promise.resolve(onClick(event));
|
|
281
|
+
success = result === true;
|
|
282
282
|
}
|
|
283
|
-
timerRef.current = setTimeout(() => {
|
|
284
|
-
timerRef.current = null;
|
|
285
|
-
setCopied(false);
|
|
286
|
-
}, 2000);
|
|
287
283
|
|
|
288
|
-
if (
|
|
289
|
-
|
|
284
|
+
if (success) {
|
|
285
|
+
setCopied(true);
|
|
286
|
+
if (timerRef.current !== null) {
|
|
287
|
+
clearTimeout(timerRef.current);
|
|
288
|
+
}
|
|
289
|
+
timerRef.current = setTimeout(() => {
|
|
290
|
+
timerRef.current = null;
|
|
291
|
+
setCopied(false);
|
|
292
|
+
}, 2000);
|
|
290
293
|
}
|
|
291
294
|
};
|
|
292
295
|
|
|
@@ -71,18 +71,31 @@ export function CopilotChatReasoningMessage({
|
|
|
71
71
|
return () => clearInterval(timer);
|
|
72
72
|
}, [isStreaming]);
|
|
73
73
|
|
|
74
|
-
// Default to open while streaming, auto-collapse when streaming ends
|
|
74
|
+
// Default to open while streaming, auto-collapse when streaming ends.
|
|
75
|
+
// Track whether the user has manually toggled so auto-collapse doesn't
|
|
76
|
+
// override their explicit intent (prevents flaky test failures on CI
|
|
77
|
+
// where async forceUpdate timing can race with click handlers).
|
|
75
78
|
const [isOpen, setIsOpen] = useState(isStreaming);
|
|
79
|
+
const userToggledRef = useRef(false);
|
|
76
80
|
|
|
77
81
|
useEffect(() => {
|
|
78
82
|
if (isStreaming) {
|
|
83
|
+
// Reset user-toggle tracking when a new streaming session starts
|
|
84
|
+
userToggledRef.current = false;
|
|
79
85
|
setIsOpen(true);
|
|
80
|
-
} else {
|
|
81
|
-
// Auto-collapse
|
|
86
|
+
} else if (!userToggledRef.current) {
|
|
87
|
+
// Auto-collapse only if the user hasn't manually toggled
|
|
82
88
|
setIsOpen(false);
|
|
83
89
|
}
|
|
84
90
|
}, [isStreaming]);
|
|
85
91
|
|
|
92
|
+
const handleToggle = hasContent
|
|
93
|
+
? () => {
|
|
94
|
+
userToggledRef.current = true;
|
|
95
|
+
setIsOpen((prev) => !prev);
|
|
96
|
+
}
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
86
99
|
const label = isStreaming
|
|
87
100
|
? "Thinking…"
|
|
88
101
|
: `Thought for ${formatDuration(elapsed)}`;
|
|
@@ -92,7 +105,7 @@ export function CopilotChatReasoningMessage({
|
|
|
92
105
|
label,
|
|
93
106
|
hasContent,
|
|
94
107
|
isStreaming,
|
|
95
|
-
onClick:
|
|
108
|
+
onClick: handleToggle,
|
|
96
109
|
});
|
|
97
110
|
|
|
98
111
|
const boundContent = renderSlot(
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type AudioInputPart,
|
|
19
19
|
type VideoInputPart,
|
|
20
20
|
type DocumentInputPart,
|
|
21
|
+
copyToClipboard,
|
|
21
22
|
} from "@copilotkit/shared";
|
|
22
23
|
import { CopilotChatAttachmentRenderer } from "./CopilotChatAttachmentRenderer";
|
|
23
24
|
|
|
@@ -147,12 +148,9 @@ export function CopilotChatUserMessage({
|
|
|
147
148
|
{
|
|
148
149
|
onClick: async () => {
|
|
149
150
|
if (flattenedContent) {
|
|
150
|
-
|
|
151
|
-
await navigator.clipboard.writeText(flattenedContent);
|
|
152
|
-
} catch (err) {
|
|
153
|
-
console.error("Failed to copy message:", err);
|
|
154
|
-
}
|
|
151
|
+
return await copyToClipboard(flattenedContent);
|
|
155
152
|
}
|
|
153
|
+
return false;
|
|
156
154
|
},
|
|
157
155
|
},
|
|
158
156
|
);
|
|
@@ -314,12 +312,17 @@ export namespace CopilotChatUserMessage {
|
|
|
314
312
|
const labels = config?.labels ?? CopilotChatDefaultLabels;
|
|
315
313
|
const [copied, setCopied] = useState(false);
|
|
316
314
|
|
|
317
|
-
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
318
|
-
|
|
319
|
-
setTimeout(() => setCopied(false), 2000);
|
|
320
|
-
|
|
315
|
+
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
316
|
+
let success = false;
|
|
321
317
|
if (onClick) {
|
|
322
|
-
onClick
|
|
318
|
+
// onClick may return a boolean indicating copy success
|
|
319
|
+
const result = await Promise.resolve(onClick(event));
|
|
320
|
+
success = result === true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (success) {
|
|
324
|
+
setCopied(true);
|
|
325
|
+
setTimeout(() => setCopied(false), 2000);
|
|
323
326
|
}
|
|
324
327
|
};
|
|
325
328
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect } from "react";
|
|
2
|
-
import { screen, fireEvent, waitFor } from "@testing-library/react";
|
|
2
|
+
import { screen, fireEvent, waitFor, act } from "@testing-library/react";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { defineToolCallRenderer, ReactToolCallRenderer } from "../../../types";
|
|
5
5
|
import {
|
|
@@ -1060,6 +1060,128 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
|
|
|
1060
1060
|
agent.complete();
|
|
1061
1061
|
});
|
|
1062
1062
|
|
|
1063
|
+
it("should not auto-collapse when user manually toggled during streaming", async () => {
|
|
1064
|
+
const agent = new MockStepwiseAgent();
|
|
1065
|
+
renderWithCopilotKit({ agent });
|
|
1066
|
+
|
|
1067
|
+
const input = await screen.findByRole("textbox");
|
|
1068
|
+
fireEvent.change(input, { target: { value: "User toggle test" } });
|
|
1069
|
+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
|
|
1070
|
+
|
|
1071
|
+
await waitFor(() => {
|
|
1072
|
+
expect(screen.getByText("User toggle test")).toBeDefined();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const reasoningId = testId("reasoning");
|
|
1076
|
+
const textId = testId("text");
|
|
1077
|
+
|
|
1078
|
+
// Start streaming reasoning — panel should auto-open
|
|
1079
|
+
agent.emit(runStartedEvent());
|
|
1080
|
+
agent.emit(reasoningStartEvent(reasoningId));
|
|
1081
|
+
agent.emit(reasoningMessageStartEvent(reasoningId));
|
|
1082
|
+
agent.emit(
|
|
1083
|
+
reasoningMessageContentEvent(reasoningId, "Deep analysis in progress"),
|
|
1084
|
+
);
|
|
1085
|
+
|
|
1086
|
+
await waitFor(() => {
|
|
1087
|
+
expect(screen.getByText("Thinking…")).toBeDefined();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// Panel should be open (aria-expanded="true") while streaming
|
|
1091
|
+
await waitFor(() => {
|
|
1092
|
+
const header = screen.getByText("Thinking…");
|
|
1093
|
+
const button = header.closest("button");
|
|
1094
|
+
expect(button?.getAttribute("aria-expanded")).toBe("true");
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// User manually collapses during streaming — this sets userToggledRef
|
|
1098
|
+
const header = screen.getByText("Thinking…");
|
|
1099
|
+
const button = header.closest("button");
|
|
1100
|
+
act(() => {
|
|
1101
|
+
if (button) {
|
|
1102
|
+
fireEvent.click(button);
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// Should now be collapsed by user action
|
|
1107
|
+
await waitFor(() => {
|
|
1108
|
+
const btn = screen.getByText("Thinking…").closest("button");
|
|
1109
|
+
expect(btn?.getAttribute("aria-expanded")).toBe("false");
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// Now streaming ends — because userToggledRef is true, the panel
|
|
1113
|
+
// should stay in whatever state the user set (collapsed).
|
|
1114
|
+
agent.emit(reasoningMessageEndEvent(reasoningId));
|
|
1115
|
+
agent.emit(reasoningEndEvent(reasoningId));
|
|
1116
|
+
agent.emit(textChunkEvent(textId, "Done."));
|
|
1117
|
+
agent.emit(runFinishedEvent());
|
|
1118
|
+
agent.complete();
|
|
1119
|
+
|
|
1120
|
+
// Panel should remain collapsed (not flash open then closed)
|
|
1121
|
+
await waitFor(() => {
|
|
1122
|
+
const btn = screen.getByText(/Thought for/).closest("button");
|
|
1123
|
+
expect(btn?.getAttribute("aria-expanded")).toBe("false");
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it("should keep panel open when user re-expands during streaming", async () => {
|
|
1128
|
+
const agent = new MockStepwiseAgent();
|
|
1129
|
+
renderWithCopilotKit({ agent });
|
|
1130
|
+
|
|
1131
|
+
const input = await screen.findByRole("textbox");
|
|
1132
|
+
fireEvent.change(input, {
|
|
1133
|
+
target: { value: "Re-expand toggle test" },
|
|
1134
|
+
});
|
|
1135
|
+
fireEvent.keyDown(input, { key: "Enter", code: "Enter" });
|
|
1136
|
+
|
|
1137
|
+
await waitFor(() => {
|
|
1138
|
+
expect(screen.getByText("Re-expand toggle test")).toBeDefined();
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const reasoningId = testId("reasoning");
|
|
1142
|
+
const textId = testId("text");
|
|
1143
|
+
|
|
1144
|
+
// Start streaming reasoning — panel auto-opens
|
|
1145
|
+
agent.emit(runStartedEvent());
|
|
1146
|
+
agent.emit(reasoningStartEvent(reasoningId));
|
|
1147
|
+
agent.emit(reasoningMessageStartEvent(reasoningId));
|
|
1148
|
+
agent.emit(reasoningMessageContentEvent(reasoningId, "Thinking hard"));
|
|
1149
|
+
|
|
1150
|
+
await waitFor(() => {
|
|
1151
|
+
const btn = screen.getByText("Thinking…").closest("button");
|
|
1152
|
+
expect(btn?.getAttribute("aria-expanded")).toBe("true");
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// User collapses, then re-expands (both set userToggledRef = true)
|
|
1156
|
+
const headerEl = screen.getByText("Thinking…");
|
|
1157
|
+
const btn = headerEl.closest("button");
|
|
1158
|
+
act(() => {
|
|
1159
|
+
if (btn) {
|
|
1160
|
+
fireEvent.click(btn); // collapse
|
|
1161
|
+
fireEvent.click(btn); // re-expand
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
await waitFor(() => {
|
|
1166
|
+
const b = screen.getByText("Thinking…").closest("button");
|
|
1167
|
+
expect(b?.getAttribute("aria-expanded")).toBe("true");
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
// Streaming ends — because userToggledRef is true, panel should
|
|
1171
|
+
// stay in the user's chosen state (open).
|
|
1172
|
+
agent.emit(reasoningMessageEndEvent(reasoningId));
|
|
1173
|
+
agent.emit(reasoningEndEvent(reasoningId));
|
|
1174
|
+
agent.emit(textChunkEvent(textId, "All done."));
|
|
1175
|
+
agent.emit(runFinishedEvent());
|
|
1176
|
+
agent.complete();
|
|
1177
|
+
|
|
1178
|
+
// Panel should remain open (not auto-collapse)
|
|
1179
|
+
await waitFor(() => {
|
|
1180
|
+
const b = screen.getByText(/Thought for/).closest("button");
|
|
1181
|
+
expect(b?.getAttribute("aria-expanded")).toBe("true");
|
|
1182
|
+
});
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1063
1185
|
it("should expand and collapse reasoning content on click", async () => {
|
|
1064
1186
|
const agent = new MockStepwiseAgent();
|
|
1065
1187
|
renderWithCopilotKit({ agent });
|
|
@@ -1094,12 +1216,16 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
|
|
|
1094
1216
|
expect(button?.getAttribute("aria-expanded")).toBe("false");
|
|
1095
1217
|
});
|
|
1096
1218
|
|
|
1097
|
-
// Click to expand
|
|
1219
|
+
// Click to expand — wrap in act() so React 18 flushes the state
|
|
1220
|
+
// update synchronously instead of deferring it through the scheduler,
|
|
1221
|
+
// which can race with waitFor polling on slow CI runners.
|
|
1098
1222
|
const header = screen.getByText(/Thought for/);
|
|
1099
1223
|
const button = header.closest("button");
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1224
|
+
act(() => {
|
|
1225
|
+
if (button) {
|
|
1226
|
+
fireEvent.click(button);
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1103
1229
|
|
|
1104
1230
|
// Should now be expanded
|
|
1105
1231
|
await waitFor(() => {
|