@copilotkit/react-core 1.55.3 → 1.56.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-dwDWYpya.d.cts → copilotkit-BtP7w7cT.d.cts} +56 -10
- package/dist/copilotkit-BtP7w7cT.d.cts.map +1 -0
- package/dist/{copilotkit-BuhSUZHb.d.mts → copilotkit-CCbxm6JM.d.mts} +56 -10
- package/dist/copilotkit-CCbxm6JM.d.mts.map +1 -0
- package/dist/{copilotkit-Dgdpbqjt.cjs → copilotkit-CSJw5BG8.cjs} +129 -58
- package/dist/copilotkit-CSJw5BG8.cjs.map +1 -0
- package/dist/{copilotkit-Cd-NrDyp.mjs → copilotkit-Cj2ZIxVr.mjs} +125 -60
- package/dist/copilotkit-Cj2ZIxVr.mjs.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 +55 -23
- 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 +124 -59
- 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/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 +26 -6
- package/src/components/copilot-provider/copilotkit.tsx +4 -1
- package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +22 -19
- package/src/v2/components/chat/CopilotChatInput.tsx +21 -2
- 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__/CopilotChatAssistantMessage.thumbs.test.tsx +72 -0
- package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +241 -0
- package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +38 -0
- package/src/v2/components/ui/button.tsx +12 -11
- 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/__tests__/use-render-custom-messages.test.tsx +55 -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/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/providers/CopilotKitProvider.tsx +15 -2
- package/src/v2/types/defineToolCallRenderer.ts +3 -0
- package/src/v2/types/react-tool-call-renderer.ts +3 -0
- 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.
|
|
3
|
+
"version": "1.56.1",
|
|
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/
|
|
77
|
-
"@copilotkit/
|
|
78
|
-
"@copilotkit/
|
|
79
|
-
"@copilotkit/
|
|
80
|
-
"@copilotkit/a2ui-renderer": "1.
|
|
76
|
+
"@copilotkit/runtime-client-gql": "1.56.1",
|
|
77
|
+
"@copilotkit/core": "1.56.1",
|
|
78
|
+
"@copilotkit/shared": "1.56.1",
|
|
79
|
+
"@copilotkit/web-inspector": "1.56.1",
|
|
80
|
+
"@copilotkit/a2ui-renderer": "1.56.1"
|
|
81
81
|
},
|
|
82
82
|
"devDependencies": {
|
|
83
83
|
"@tailwindcss/cli": "^4.1.11",
|
|
@@ -60,16 +60,27 @@ const usePredictStateSubscription = (agent?: AbstractAgent) => {
|
|
|
60
60
|
}, [agent, getSubscriber]);
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
const { copilotkit } = useCopilotKit();
|
|
63
|
+
function CopilotListenersAgentSubscription() {
|
|
65
64
|
const existingConfig = useCopilotChatConfiguration();
|
|
66
65
|
const resolvedAgentId = existingConfig?.agentId;
|
|
67
|
-
const { setBannerError } = useToast();
|
|
68
66
|
|
|
69
67
|
const { agent } = useAgent({ agentId: resolvedAgentId });
|
|
70
68
|
|
|
71
69
|
usePredictStateSubscription(agent);
|
|
72
70
|
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function CopilotListeners() {
|
|
75
|
+
const { copilotkit } = useCopilotKit();
|
|
76
|
+
const { setBannerError } = useToast();
|
|
77
|
+
|
|
78
|
+
// Only render the agent subscription when agents are registered or a runtime
|
|
79
|
+
// is configured. Without this guard, useAgent() throws when the agents map is
|
|
80
|
+
// empty and no runtimeUrl is set (#3249).
|
|
81
|
+
const hasAgents = Object.keys(copilotkit.agents ?? {}).length > 0;
|
|
82
|
+
const hasRuntime = copilotkit.runtimeUrl !== undefined;
|
|
83
|
+
|
|
73
84
|
useEffect(() => {
|
|
74
85
|
const subscriber: CopilotKitCoreSubscriber = {
|
|
75
86
|
onError: ({ error, code, context }) => {
|
|
@@ -122,5 +133,5 @@ export function CopilotListeners() {
|
|
|
122
133
|
};
|
|
123
134
|
}, [copilotkit?.subscribe]);
|
|
124
135
|
|
|
125
|
-
return null;
|
|
136
|
+
return hasAgents || hasRuntime ? <CopilotListenersAgentSubscription /> : null;
|
|
126
137
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { CopilotListeners } from "../CopilotListeners";
|
|
5
|
+
import { CopilotKitProvider } from "../../v2/providers/CopilotKitProvider";
|
|
6
|
+
import { CopilotChatConfigurationProvider } from "../../v2/providers/CopilotChatConfigurationProvider";
|
|
7
|
+
import { ToastProvider } from "../toast/toast-provider";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regression test for #3249: CopilotListeners throws when no agents registered.
|
|
11
|
+
*
|
|
12
|
+
* When CopilotKitProvider has no agents registered (empty agents map) and no
|
|
13
|
+
* runtimeUrl, useAgent() inside CopilotListeners throws. The component should
|
|
14
|
+
* handle this gracefully and render null without crashing.
|
|
15
|
+
*/
|
|
16
|
+
describe("CopilotListeners (#3249)", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not throw when no agents are registered", () => {
|
|
22
|
+
// No agents, no runtimeUrl - should not crash
|
|
23
|
+
expect(() => {
|
|
24
|
+
render(
|
|
25
|
+
<ToastProvider enabled={false}>
|
|
26
|
+
<CopilotKitProvider>
|
|
27
|
+
<CopilotChatConfigurationProvider
|
|
28
|
+
agentId="default"
|
|
29
|
+
threadId="test-thread"
|
|
30
|
+
>
|
|
31
|
+
<CopilotListeners />
|
|
32
|
+
</CopilotChatConfigurationProvider>
|
|
33
|
+
</CopilotKitProvider>
|
|
34
|
+
</ToastProvider>,
|
|
35
|
+
);
|
|
36
|
+
}).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -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],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ForwardedParametersInput } from "@copilotkit/runtime-client-gql";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
3
|
import { AuthState } from "../../context/copilot-context";
|
|
4
|
-
import { CopilotErrorHandler } from "@copilotkit/shared";
|
|
4
|
+
import { CopilotErrorHandler, DebugConfig } from "@copilotkit/shared";
|
|
5
5
|
import { CopilotKitProviderProps } from "../../v2";
|
|
6
6
|
/**
|
|
7
7
|
* Props for CopilotKit.
|
|
@@ -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.
|
|
@@ -191,4 +195,20 @@ export interface CopilotKitProps extends Omit<
|
|
|
191
195
|
* to enabled.
|
|
192
196
|
*/
|
|
193
197
|
enableInspector?: boolean;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Enable debug logging. On the server (`CopilotRuntime`), this enables
|
|
201
|
+
* structured Pino logging of the AG-UI event pipeline. On the client,
|
|
202
|
+
* this configuration is forwarded to the AG-UI transport layer
|
|
203
|
+
* (`transformChunks`) for transport-level debug output.
|
|
204
|
+
*
|
|
205
|
+
* Pass `true` for full output, or an object for granular control:
|
|
206
|
+
*
|
|
207
|
+
* ```tsx
|
|
208
|
+
* <CopilotKit debug={true} runtimeUrl="...">
|
|
209
|
+
* {children}
|
|
210
|
+
* </CopilotKit>
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
debug?: DebugConfig;
|
|
194
214
|
}
|
|
@@ -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
|
);
|
|
@@ -100,7 +98,7 @@ export function CopilotChatAssistantMessage({
|
|
|
100
98
|
thumbsUpButton,
|
|
101
99
|
CopilotChatAssistantMessage.ThumbsUpButton,
|
|
102
100
|
{
|
|
103
|
-
onClick: onThumbsUp,
|
|
101
|
+
onClick: onThumbsUp ? () => onThumbsUp(message) : undefined,
|
|
104
102
|
},
|
|
105
103
|
);
|
|
106
104
|
|
|
@@ -108,7 +106,7 @@ export function CopilotChatAssistantMessage({
|
|
|
108
106
|
thumbsDownButton,
|
|
109
107
|
CopilotChatAssistantMessage.ThumbsDownButton,
|
|
110
108
|
{
|
|
111
|
-
onClick: onThumbsDown,
|
|
109
|
+
onClick: onThumbsDown ? () => onThumbsDown(message) : undefined,
|
|
112
110
|
},
|
|
113
111
|
);
|
|
114
112
|
|
|
@@ -116,7 +114,7 @@ export function CopilotChatAssistantMessage({
|
|
|
116
114
|
readAloudButton,
|
|
117
115
|
CopilotChatAssistantMessage.ReadAloudButton,
|
|
118
116
|
{
|
|
119
|
-
onClick: onReadAloud,
|
|
117
|
+
onClick: onReadAloud ? () => onReadAloud(message) : undefined,
|
|
120
118
|
},
|
|
121
119
|
);
|
|
122
120
|
|
|
@@ -124,7 +122,7 @@ export function CopilotChatAssistantMessage({
|
|
|
124
122
|
regenerateButton,
|
|
125
123
|
CopilotChatAssistantMessage.RegenerateButton,
|
|
126
124
|
{
|
|
127
|
-
onClick: onRegenerate,
|
|
125
|
+
onClick: onRegenerate ? () => onRegenerate(message) : undefined,
|
|
128
126
|
},
|
|
129
127
|
);
|
|
130
128
|
|
|
@@ -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
|
|
|
@@ -384,6 +384,12 @@ export function CopilotChatInput({
|
|
|
384
384
|
);
|
|
385
385
|
|
|
386
386
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
387
|
+
// Skip key handling during IME composition (e.g. CJK input).
|
|
388
|
+
// The compositionend event will fire separately when composition ends.
|
|
389
|
+
if (e.nativeEvent.isComposing || e.keyCode === 229) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
387
393
|
if (commandQuery !== null && mode === "input") {
|
|
388
394
|
if (e.key === "ArrowDown") {
|
|
389
395
|
if (filteredCommands.length > 0) {
|
|
@@ -455,10 +461,12 @@ export function CopilotChatInput({
|
|
|
455
461
|
|
|
456
462
|
onSubmitMessage(trimmed);
|
|
457
463
|
|
|
464
|
+
// Always clear the input after sending, including controlled mode.
|
|
465
|
+
// In controlled mode, onChange("") notifies the parent to reset its state.
|
|
458
466
|
if (!isControlled) {
|
|
459
467
|
setInternalValue("");
|
|
460
|
-
onChange?.("");
|
|
461
468
|
}
|
|
469
|
+
onChange?.("");
|
|
462
470
|
|
|
463
471
|
if (inputRef.current) {
|
|
464
472
|
inputRef.current.focus();
|
|
@@ -470,6 +478,12 @@ export function CopilotChatInput({
|
|
|
470
478
|
value: resolvedValue,
|
|
471
479
|
onChange: handleChange,
|
|
472
480
|
onKeyDown: handleKeyDown,
|
|
481
|
+
onCompositionStart: () => {
|
|
482
|
+
isComposingRef.current = true;
|
|
483
|
+
},
|
|
484
|
+
onCompositionEnd: () => {
|
|
485
|
+
isComposingRef.current = false;
|
|
486
|
+
},
|
|
473
487
|
autoFocus: autoFocus,
|
|
474
488
|
className: twMerge(
|
|
475
489
|
"cpk:w-full cpk:py-3",
|
|
@@ -612,9 +626,14 @@ export function CopilotChatInput({
|
|
|
612
626
|
}
|
|
613
627
|
};
|
|
614
628
|
|
|
629
|
+
// Track whether an IME composition is active so we can avoid
|
|
630
|
+
// resetting textarea.value during measurement (which would break
|
|
631
|
+
// the composition session).
|
|
632
|
+
const isComposingRef = useRef(false);
|
|
633
|
+
|
|
615
634
|
const ensureMeasurements = useCallback(() => {
|
|
616
635
|
const textarea = inputRef.current;
|
|
617
|
-
if (!textarea) {
|
|
636
|
+
if (!textarea || isComposingRef.current) {
|
|
618
637
|
return;
|
|
619
638
|
}
|
|
620
639
|
|
|
@@ -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
|
|