@copilotkit/vue 1.68.3 → 1.69.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.
Files changed (69) hide show
  1. package/PARITY.md +2 -1
  2. package/README.md +1 -1
  3. package/dist/components/copilot-provider/types.d.ts +1 -1
  4. package/dist/index.cjs +1 -1
  5. package/dist/index.mjs +2 -2
  6. package/dist/use-render-activity-message-BtaJ2iQc.cjs +97 -0
  7. package/dist/use-render-activity-message-BtaJ2iQc.cjs.map +1 -0
  8. package/dist/use-render-activity-message-CADUB--8.js +9943 -0
  9. package/dist/use-render-activity-message-CADUB--8.js.map +1 -0
  10. package/dist/v2/components/CopilotKitInspector.vue.d.ts +2 -0
  11. package/dist/v2/components/CopilotKitInspector.vue.d.ts.map +1 -1
  12. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts +1 -1
  13. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  15. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  16. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +18 -0
  17. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  18. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  19. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  20. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts +21 -0
  21. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -0
  22. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  23. package/dist/v2/components/chat/SaveSnippetBeside.vue.d.ts +20 -0
  24. package/dist/v2/components/chat/SaveSnippetBeside.vue.d.ts.map +1 -0
  25. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +10 -0
  26. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -0
  27. package/dist/v2/components/chat/save-snippet-beside.d.ts +20 -0
  28. package/dist/v2/components/chat/save-snippet-beside.d.ts.map +1 -0
  29. package/dist/v2/components/icons/index.d.ts +1 -1
  30. package/dist/v2/components/icons/index.d.ts.map +1 -1
  31. package/dist/v2/hooks/use-threads.d.ts +1 -1
  32. package/dist/v2/index.cjs +1 -1
  33. package/dist/v2/index.mjs +1 -1
  34. package/dist/v2/providers/CopilotKitProvider.types.d.ts +1 -1
  35. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  36. package/dist/v2/providers/keys.d.ts +36 -0
  37. package/dist/v2/providers/keys.d.ts.map +1 -1
  38. package/dist/v2/providers/types.d.ts +3 -0
  39. package/dist/v2/providers/types.d.ts.map +1 -1
  40. package/dist/v2/providers/use-inspector-thread-override.d.ts +10 -0
  41. package/dist/v2/providers/use-inspector-thread-override.d.ts.map +1 -0
  42. package/package.json +5 -5
  43. package/src/components/copilot-provider/types.ts +1 -1
  44. package/src/v2/components/CopilotKitInspector.vue +19 -1
  45. package/src/v2/components/OpenGenerativeUIRenderer.ts +44 -10
  46. package/src/v2/components/chat/CopilotChat.vue +64 -4
  47. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +52 -1
  48. package/src/v2/components/chat/CopilotChatMessageView.vue +57 -23
  49. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +68 -32
  50. package/src/v2/components/chat/CopilotChatToolCallItem.vue +86 -0
  51. package/src/v2/components/chat/CopilotChatToolCallsView.vue +72 -130
  52. package/src/v2/components/chat/SaveSnippetBeside.vue +82 -0
  53. package/src/v2/components/chat/__tests__/CopilotChatToolCallsView.test.ts +302 -1
  54. package/src/v2/components/chat/__tests__/CopilotThreadsDrawer.ssr.test.ts +1 -1
  55. package/src/v2/components/chat/copilotChatToolCallItemRender.ts +126 -0
  56. package/src/v2/components/chat/save-snippet-beside.ts +84 -0
  57. package/src/v2/components/icons/__tests__/icons.test.ts +2 -0
  58. package/src/v2/components/icons/index.ts +1 -0
  59. package/src/v2/hooks/use-threads.ts +1 -1
  60. package/src/v2/providers/CopilotKitProvider.types.ts +1 -1
  61. package/src/v2/providers/CopilotKitProvider.vue +66 -6
  62. package/src/v2/providers/__tests__/inspector-thread-override.test.ts +214 -0
  63. package/src/v2/providers/keys.ts +47 -0
  64. package/src/v2/providers/types.ts +3 -0
  65. package/src/v2/providers/use-inspector-thread-override.ts +99 -0
  66. package/dist/use-render-activity-message-DljRBrXN.js +0 -9513
  67. package/dist/use-render-activity-message-DljRBrXN.js.map +0 -1
  68. package/dist/use-render-activity-message-bkrPa-92.cjs +0 -85
  69. package/dist/use-render-activity-message-bkrPa-92.cjs.map +0 -1
@@ -0,0 +1,126 @@
1
+ import type { Message, ToolCall, ToolMessage } from "@ag-ui/core";
2
+ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
3
+ import { ToolCallStatus } from "@copilotkit/core";
4
+ import { partialJSONParse } from "@copilotkit/shared";
5
+ import type { CopilotChatToolCallRenderSlotProps } from "./types";
6
+ import type {
7
+ VueToolCallRenderer,
8
+ VueToolCallRendererRenderProps,
9
+ } from "../../types";
10
+
11
+ export function findToolMessage(
12
+ messages: Message[],
13
+ toolCallId: string,
14
+ ): ToolMessage | undefined {
15
+ return messages.find(
16
+ (message) =>
17
+ message.role === "tool" &&
18
+ (message as ToolMessage).toolCallId === toolCallId,
19
+ ) as ToolMessage | undefined;
20
+ }
21
+
22
+ export function getToolCallSlotName(toolName: string): `tool-call-${string}` {
23
+ return `tool-call-${toolName}`;
24
+ }
25
+
26
+ export function getCoreRenderConfig(
27
+ toolCall: ToolCall,
28
+ renderToolCalls: readonly VueToolCallRenderer<unknown>[],
29
+ agentId: string,
30
+ ): VueToolCallRenderer<unknown> | undefined {
31
+ const exactMatches = renderToolCalls.filter(
32
+ (renderConfig) => renderConfig.name === toolCall.function.name,
33
+ );
34
+
35
+ return (
36
+ exactMatches.find((renderConfig) => renderConfig.agentId === agentId) ??
37
+ exactMatches.find((renderConfig) => !renderConfig.agentId) ??
38
+ exactMatches[0] ??
39
+ renderToolCalls.find((renderConfig) => renderConfig.name === "*")
40
+ );
41
+ }
42
+
43
+ export function getToolCallRenderProps(
44
+ toolCall: ToolCall,
45
+ messages: Message[],
46
+ executingToolCallIds: ReadonlySet<string>,
47
+ ): CopilotChatToolCallRenderSlotProps {
48
+ const toolMessage = findToolMessage(messages, toolCall.id);
49
+ const parsedArgs = partialJSONParse(toolCall.function.arguments);
50
+
51
+ if (toolMessage) {
52
+ return {
53
+ name: toolCall.function.name,
54
+ args: parsedArgs,
55
+ status: ToolCallStatus.Complete,
56
+ result: toolMessage.content,
57
+ toolCall,
58
+ toolMessage,
59
+ };
60
+ }
61
+
62
+ const isExecuting = executingToolCallIds.has(toolCall.id);
63
+ return {
64
+ name: toolCall.function.name,
65
+ args: parsedArgs,
66
+ status: isExecuting ? ToolCallStatus.Executing : ToolCallStatus.InProgress,
67
+ result: undefined,
68
+ toolCall,
69
+ toolMessage: undefined,
70
+ };
71
+ }
72
+
73
+ export function getCoreRenderProps(
74
+ toolCall: ToolCall,
75
+ messages: Message[],
76
+ executingToolCallIds: ReadonlySet<string>,
77
+ ): VueToolCallRendererRenderProps<unknown> {
78
+ const toolMessage = findToolMessage(messages, toolCall.id);
79
+ const parsedArgs = partialJSONParse(toolCall.function.arguments);
80
+
81
+ if (toolMessage) {
82
+ return {
83
+ name: toolCall.function.name,
84
+ toolCallId: toolCall.id,
85
+ args: parsedArgs,
86
+ status: ToolCallStatus.Complete,
87
+ result: toolMessage.content,
88
+ };
89
+ }
90
+
91
+ const isExecuting = executingToolCallIds.has(toolCall.id);
92
+ if (isExecuting) {
93
+ return {
94
+ name: toolCall.function.name,
95
+ toolCallId: toolCall.id,
96
+ args: parsedArgs,
97
+ status: ToolCallStatus.Executing,
98
+ result: undefined,
99
+ };
100
+ }
101
+
102
+ return {
103
+ name: toolCall.function.name,
104
+ toolCallId: toolCall.id,
105
+ args: parsedArgs && typeof parsedArgs === "object" ? parsedArgs : {},
106
+ status: ToolCallStatus.InProgress,
107
+ result: undefined,
108
+ };
109
+ }
110
+
111
+ export function getToolCallMemoDeps(
112
+ toolCall: ToolCall,
113
+ messages: Message[],
114
+ executingToolCallIds: ReadonlySet<string>,
115
+ renderToolCalls: readonly VueToolCallRenderer<unknown>[],
116
+ agentId: string = DEFAULT_AGENT_ID,
117
+ ): unknown[] {
118
+ return [
119
+ toolCall.id,
120
+ toolCall.function.name,
121
+ toolCall.function.arguments,
122
+ findToolMessage(messages, toolCall.id)?.content,
123
+ executingToolCallIds.has(toolCall.id),
124
+ getCoreRenderConfig(toolCall, renderToolCalls, agentId)?.render,
125
+ ];
126
+ }
@@ -0,0 +1,84 @@
1
+ export const SAVE_SNIPPET_ICON_SLOT_PX = 36;
2
+
3
+ export const SAVE_SNIPPET_BESIDE_WRAP_CLASS =
4
+ "cpk:relative cpk:w-full cpk:overflow-visible";
5
+
6
+ export const SAVE_SNIPPET_BESIDE_BODY_CLASS = "cpk:w-full";
7
+
8
+ export const SAVE_SNIPPET_BESIDE_SAVE_CLASS = "cpk:absolute cpk:top-0 cpk:z-10";
9
+
10
+ export function saveSnippetBesideStyle(side: "left" | "right") {
11
+ if (side === "left") {
12
+ return {
13
+ left: "auto",
14
+ right: "100%",
15
+ marginLeft: "0px",
16
+ marginRight: "4px",
17
+ } as const;
18
+ }
19
+ return {
20
+ left: "100%",
21
+ right: "auto",
22
+ marginLeft: "4px",
23
+ marginRight: "0px",
24
+ } as const;
25
+ }
26
+
27
+ export function pickSaveSnippetSide(
28
+ roomRight: number,
29
+ roomLeft: number,
30
+ slotPx = SAVE_SNIPPET_ICON_SLOT_PX,
31
+ ): "left" | "right" {
32
+ if (roomRight >= slotPx) {
33
+ return "right";
34
+ }
35
+ if (roomLeft >= slotPx) {
36
+ return "left";
37
+ }
38
+ return "right";
39
+ }
40
+
41
+ export function findOverflowAncestor(el: HTMLElement): HTMLElement {
42
+ let node: HTMLElement | null = el.parentElement;
43
+ while (node) {
44
+ const style = getComputedStyle(node);
45
+ const overflowX = style.overflowX;
46
+ const overflow = style.overflow;
47
+ if (
48
+ overflowX === "hidden" ||
49
+ overflowX === "auto" ||
50
+ overflowX === "scroll" ||
51
+ overflow === "hidden" ||
52
+ overflow === "auto" ||
53
+ overflow === "scroll"
54
+ ) {
55
+ return node;
56
+ }
57
+ node = node.parentElement;
58
+ }
59
+ return document.documentElement;
60
+ }
61
+
62
+ export function measurableBox(el: HTMLElement): HTMLElement {
63
+ let node: HTMLElement | null = el;
64
+ while (node && getComputedStyle(node).display === "contents") {
65
+ node = node.firstElementChild as HTMLElement | null;
66
+ }
67
+ return node ?? el;
68
+ }
69
+
70
+ export function measureSaveSnippetSide(
71
+ wrap: HTMLElement,
72
+ body: HTMLElement,
73
+ ): "left" | "right" {
74
+ const clip = findOverflowAncestor(wrap);
75
+ const clipRect = clip.getBoundingClientRect();
76
+ const box = measurableBox(
77
+ (body.firstElementChild as HTMLElement | null) ?? body,
78
+ );
79
+ const bodyRect = box.getBoundingClientRect();
80
+ return pickSaveSnippetSide(
81
+ clipRect.right - bodyRect.right,
82
+ bodyRect.left - clipRect.left,
83
+ );
84
+ }
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import { mount } from "@vue/test-utils";
3
3
  import {
4
4
  IconArrowUp,
5
+ IconBookmark,
5
6
  IconCheck,
6
7
  IconCheckCircle,
7
8
  IconChevronDown,
@@ -31,6 +32,7 @@ describe("icons adapter", () => {
31
32
  IconPlus,
32
33
  IconMic,
33
34
  IconArrowUp,
35
+ IconBookmark,
34
36
  IconCheck,
35
37
  IconSquare,
36
38
  IconLoader2,
@@ -5,6 +5,7 @@ export {
5
5
  Plus as IconPlus,
6
6
  Mic as IconMic,
7
7
  ArrowUp as IconArrowUp,
8
+ Bookmark as IconBookmark,
8
9
  Check as IconCheck,
9
10
  Square as IconSquare,
10
11
  Loader2 as IconLoader2,
@@ -80,7 +80,7 @@ function bindThreadStoreSelector<T>(
80
80
  }
81
81
 
82
82
  /**
83
- * Vue composable for listing and managing Intelligence platform threads.
83
+ * Vue composable for listing and managing CopilotKit Intelligence threads.
84
84
  *
85
85
  * The hook fetches threads for the runtime-authenticated user and the given
86
86
  * `agentId`, then keeps the result in sync via the core thread store's realtime
@@ -20,7 +20,7 @@ export interface CopilotKitProviderProps {
20
20
  publicApiKey?: string;
21
21
  publicLicenseKey?: string;
22
22
  /**
23
- * Signed license token for offline verification of Enterprise Intelligence Platform features.
23
+ * Signed license token for offline verification of CopilotKit Intelligence features.
24
24
  * Obtain from https://dashboard.operations.copilotkit.ai.
25
25
  */
26
26
  licenseToken?: string;
@@ -34,7 +34,8 @@ import {
34
34
  MCPAppsActivityRenderer,
35
35
  MCPAppsActivityType,
36
36
  } from "../components/MCPAppsActivityRenderer";
37
- import { CopilotKitKey, SandboxFunctionsKey } from "./keys";
37
+ import { CopilotKitKey, InspectorKey, SandboxFunctionsKey } from "./keys";
38
+ import type { VueInspectorOpenRequest, VueInspectorSaveRequest } from "./keys";
38
39
  import {
39
40
  LicenseContextKey,
40
41
  createLicenseContextValue,
@@ -109,28 +110,86 @@ const props = withDefaults(defineProps<CopilotKitProviderProps>(), {
109
110
  });
110
111
 
111
112
  const shouldRenderInspector = ref(false);
113
+ // Mounting the overlay follows showDevConsole. The in-chat affordances are
114
+ // narrower: a dev build on localhost only, so `showDevConsole: true` on a
115
+ // staging URL never puts a bookmark into a production chat. Matches React.
116
+ const canShowLocalInspectorAction = ref(false);
112
117
 
113
118
  const updateInspectorVisibility = () => {
119
+ const isLocalhost =
120
+ typeof window !== "undefined" &&
121
+ new Set(["localhost", "127.0.0.1"]).has(window.location?.hostname ?? "");
122
+ canShowLocalInspectorAction.value =
123
+ process.env.NODE_ENV === "development" && isLocalhost;
124
+
114
125
  if (props.showDevConsole === true) {
115
126
  shouldRenderInspector.value = true;
116
127
  return;
117
128
  }
118
129
  if (props.showDevConsole === "auto") {
119
- if (typeof window === "undefined") {
120
- shouldRenderInspector.value = false;
121
- return;
130
+ shouldRenderInspector.value = isLocalhost;
131
+ if (!isLocalhost) {
132
+ canShowLocalInspectorAction.value = false;
122
133
  }
123
- const localhostHosts = new Set(["localhost", "127.0.0.1"]);
124
- shouldRenderInspector.value = localhostHosts.has(window.location.hostname);
125
134
  return;
126
135
  }
127
136
  shouldRenderInspector.value = false;
137
+ canShowLocalInspectorAction.value = false;
128
138
  };
129
139
 
130
140
  watch(() => props.showDevConsole, updateInspectorVisibility, {
131
141
  immediate: true,
132
142
  });
133
143
 
144
+ const inspectorOpenRequest = ref<VueInspectorOpenRequest | null>(null);
145
+ const isLocalInspectorEnabled = computed(
146
+ () => shouldRenderInspector.value && canShowLocalInspectorAction.value,
147
+ );
148
+
149
+ function openInspector(request: VueInspectorOpenRequest) {
150
+ inspectorOpenRequest.value = { ...request };
151
+ }
152
+
153
+ async function saveEventSnippet(request: VueInspectorSaveRequest) {
154
+ try {
155
+ const mod = await import("@copilotkit/web-inspector");
156
+ const threadId = request.threadId ?? "inspector-snippet";
157
+ const runId = `inspector-snippet-${Date.now()}`;
158
+ const compiled = mod.compileChatSnippet({
159
+ ...request,
160
+ threadId,
161
+ runId,
162
+ });
163
+ const now = new Date().toISOString();
164
+ const snippet = {
165
+ id: crypto.randomUUID(),
166
+ name: compiled.name,
167
+ recipe: compiled.recipe,
168
+ events: compiled.events,
169
+ createdAt: now,
170
+ updatedAt: now,
171
+ };
172
+ mod.upsertEventSnippet(snippet);
173
+ openInspector({
174
+ messageId: request.messageId,
175
+ threadId: request.threadId,
176
+ agentId: request.agentId,
177
+ menu: "event-snippets",
178
+ snippetId: snippet.id,
179
+ });
180
+ } catch (error) {
181
+ // Compile can throw on bad args, and storage can throw QuotaExceededError.
182
+ // Callers fire this as `void saveEventSnippet(...)`, so report it here.
183
+ console.error("[CopilotKit] Could not save the event snippet.", error);
184
+ }
185
+ }
186
+
187
+ provide(InspectorKey, {
188
+ isLocalInspectorEnabled,
189
+ openInspector,
190
+ saveEventSnippet,
191
+ });
192
+
134
193
  const initialFrontendTools = props.frontendTools;
135
194
  const initialHumanInTheLoop = props.humanInTheLoop;
136
195
  const initialRenderCustomMessages = props.renderCustomMessages;
@@ -657,6 +716,7 @@ const showExpiringBanner = computed(
657
716
  v-if="shouldRenderInspector"
658
717
  :core="copilotkit"
659
718
  :default-anchor="props.inspectorDefaultAnchor"
719
+ :open-request="inspectorOpenRequest"
660
720
  />
661
721
  <!-- License warnings — driven by server-reported status -->
662
722
  <LicenseWarningBanner v-if="showNoLicenseBanner" type="no_license" />
@@ -0,0 +1,214 @@
1
+ import {
2
+ cleanup,
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from "@testing-library/vue";
8
+ import { defineComponent } from "vue";
9
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
+ import {
11
+ emitInspectorStopViewing,
12
+ emitInspectorViewThread,
13
+ onInspectorActiveThread,
14
+ onInspectorViewThreadResult,
15
+ } from "@copilotkit/core";
16
+ import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
17
+ import { MockStepwiseAgent } from "../../__tests__/utils/test-helpers";
18
+ import CopilotChat from "../../components/chat/CopilotChat.vue";
19
+ import CopilotKitProvider from "../CopilotKitProvider.vue";
20
+ import CopilotChatConfigurationProvider from "../CopilotChatConfigurationProvider.vue";
21
+ import { useCopilotChatConfiguration } from "../useCopilotChatConfiguration";
22
+
23
+ class TrackingAgent extends MockStepwiseAgent {
24
+ connectCalls: string[] = [];
25
+ failThreadId: string | null = null;
26
+
27
+ async connectAgent(
28
+ _params: unknown,
29
+ _subscriber: unknown,
30
+ ): Promise<{ result: unknown; newMessages: [] }> {
31
+ this.connectCalls.push(this.threadId ?? "");
32
+ if (this.threadId === this.failThreadId) {
33
+ throw new Error("connect failed");
34
+ }
35
+ return { result: undefined, newMessages: [] };
36
+ }
37
+ }
38
+
39
+ const ThreadControls = defineComponent({
40
+ setup() {
41
+ const configuration = useCopilotChatConfiguration();
42
+ return { configuration };
43
+ },
44
+ template: `
45
+ <button @click="configuration?.setActiveThreadId('app-picked')">select</button>
46
+ <button @click="configuration?.startNewThread()">new</button>
47
+ `,
48
+ });
49
+
50
+ function renderChat(
51
+ agent: TrackingAgent,
52
+ options: { threadId?: string; withControls?: boolean } = {},
53
+ ) {
54
+ agent.agentId = DEFAULT_AGENT_ID;
55
+ const Root = defineComponent({
56
+ components: {
57
+ CopilotChat,
58
+ CopilotChatConfigurationProvider,
59
+ CopilotKitProvider,
60
+ ThreadControls,
61
+ },
62
+ setup() {
63
+ return {
64
+ agents: { [DEFAULT_AGENT_ID]: agent },
65
+ threadId: options.threadId,
66
+ withControls: options.withControls ?? false,
67
+ };
68
+ },
69
+ template: `
70
+ <CopilotKitProvider :agents__unsafe_dev_only="agents">
71
+ <CopilotChatConfigurationProvider v-if="withControls" :agent-id="'default'">
72
+ <CopilotChat :thread-id="threadId" />
73
+ <ThreadControls />
74
+ </CopilotChatConfigurationProvider>
75
+ <CopilotChat v-else :thread-id="threadId" />
76
+ </CopilotKitProvider>
77
+ `,
78
+ });
79
+ return render(Root);
80
+ }
81
+
82
+ function viewRequest(requestId: string, threadId = "saved-thread") {
83
+ return emitInspectorViewThread({
84
+ requestId,
85
+ threadId,
86
+ agentId: DEFAULT_AGENT_ID,
87
+ });
88
+ }
89
+
90
+ describe("inspector thread override", () => {
91
+ const cleanups: Array<() => void> = [];
92
+
93
+ beforeEach(() => {
94
+ cleanups.length = 0;
95
+ });
96
+
97
+ afterEach(() => {
98
+ for (const unsubscribe of cleanups.splice(0)) unsubscribe();
99
+ cleanup();
100
+ });
101
+
102
+ it("switches a pinned threadId when view-thread matches the agent", async () => {
103
+ const agent = new TrackingAgent();
104
+ renderChat(agent, { threadId: "pinned-thread" });
105
+ await waitFor(() => expect(agent.threadId).toBe("pinned-thread"));
106
+
107
+ expect(viewRequest("request-1")).toBe(true);
108
+
109
+ await waitFor(() => expect(agent.threadId).toBe("saved-thread"));
110
+ expect(agent.connectCalls).toContain("saved-thread");
111
+ });
112
+
113
+ it("ignores view-thread for a different agent", async () => {
114
+ const agent = new TrackingAgent();
115
+ renderChat(agent, { threadId: "pinned-thread" });
116
+ await waitFor(() => expect(agent.threadId).toBe("pinned-thread"));
117
+
118
+ expect(
119
+ emitInspectorViewThread({
120
+ requestId: "request-2",
121
+ threadId: "other-thread",
122
+ agentId: "other-agent",
123
+ }),
124
+ ).toBe(false);
125
+
126
+ expect(agent.threadId).toBe("pinned-thread");
127
+ });
128
+
129
+ it("restores the previous thread on stop-viewing", async () => {
130
+ const agent = new TrackingAgent();
131
+ renderChat(agent, { threadId: "pinned-thread" });
132
+ viewRequest("request-3");
133
+ await waitFor(() => expect(agent.threadId).toBe("saved-thread"));
134
+
135
+ emitInspectorStopViewing({
136
+ requestId: "request-3",
137
+ agentId: DEFAULT_AGENT_ID,
138
+ });
139
+
140
+ await waitFor(() => expect(agent.threadId).toBe("pinned-thread"));
141
+ });
142
+
143
+ it("keeps the original previous thread after a second view-thread", async () => {
144
+ const agent = new TrackingAgent();
145
+ renderChat(agent, { threadId: "pinned-thread" });
146
+ viewRequest("request-4a", "first-saved");
147
+ await waitFor(() => expect(agent.threadId).toBe("first-saved"));
148
+ viewRequest("request-4b", "second-saved");
149
+ await waitFor(() => expect(agent.threadId).toBe("second-saved"));
150
+
151
+ emitInspectorStopViewing({
152
+ requestId: "request-4b",
153
+ agentId: DEFAULT_AGENT_ID,
154
+ });
155
+
156
+ await waitFor(() => expect(agent.threadId).toBe("pinned-thread"));
157
+ });
158
+
159
+ it("ends the override when the app picks a thread", async () => {
160
+ const agent = new TrackingAgent();
161
+ renderChat(agent, { withControls: true });
162
+ viewRequest("request-5");
163
+ await waitFor(() => expect(agent.threadId).toBe("saved-thread"));
164
+
165
+ await fireEvent.click(screen.getByText("select"));
166
+
167
+ await waitFor(() => expect(agent.threadId).toBe("app-picked"));
168
+ });
169
+
170
+ it("ends the override when the app starts a new thread", async () => {
171
+ const agent = new TrackingAgent();
172
+ renderChat(agent, { withControls: true });
173
+ viewRequest("request-6");
174
+ await waitFor(() => expect(agent.threadId).toBe("saved-thread"));
175
+
176
+ await fireEvent.click(screen.getByText("new"));
177
+
178
+ await waitFor(() => expect(agent.threadId).not.toBe("saved-thread"));
179
+ });
180
+
181
+ it("rolls back when connect fails", async () => {
182
+ const agent = new TrackingAgent();
183
+ agent.failThreadId = "saved-thread";
184
+ renderChat(agent, { threadId: "pinned-thread" });
185
+ const results: boolean[] = [];
186
+ cleanups.push(
187
+ onInspectorViewThreadResult((payload) => {
188
+ if (payload.requestId === "request-7") results.push(payload.ok);
189
+ }),
190
+ );
191
+
192
+ viewRequest("request-7");
193
+
194
+ await waitFor(() => expect(results).toContain(false));
195
+ await waitFor(() => expect(agent.threadId).toBe("pinned-thread"));
196
+ });
197
+
198
+ it("emits applied result and active-thread on view", async () => {
199
+ const agent = new TrackingAgent();
200
+ const results: boolean[] = [];
201
+ const activeSources: string[] = [];
202
+ cleanups.push(
203
+ onInspectorViewThreadResult((payload) => results.push(payload.ok)),
204
+ onInspectorActiveThread((payload) => activeSources.push(payload.source)),
205
+ );
206
+ renderChat(agent, { threadId: "pinned-thread" });
207
+
208
+ expect(viewRequest("request-8")).toBe(true);
209
+
210
+ await waitFor(() => expect(agent.threadId).toBe("saved-thread"));
211
+ expect(results).toContain(true);
212
+ expect(activeSources).toContain("override");
213
+ });
214
+ });
@@ -23,3 +23,50 @@ export const CopilotChatConfigurationKey: InjectionKey<
23
23
  export const SandboxFunctionsKey: InjectionKey<
24
24
  Ref<readonly SandboxFunction[]>
25
25
  > = Symbol("SandboxFunctions");
26
+
27
+ export type VueInspectorOpenRequest = {
28
+ messageId: string;
29
+ threadId?: string;
30
+ agentId?: string;
31
+ menu?: "event-snippets";
32
+ snippetId?: string;
33
+ };
34
+
35
+ export type VueInspectorSaveRequest = {
36
+ threadId?: string;
37
+ agentId?: string;
38
+ } & (
39
+ | {
40
+ kind: "text";
41
+ messageId: string;
42
+ content: string;
43
+ }
44
+ | {
45
+ kind: "reasoning";
46
+ messageId: string;
47
+ content: string;
48
+ }
49
+ | {
50
+ kind: "tool-call";
51
+ messageId: string;
52
+ toolCallId: string;
53
+ toolName: string;
54
+ argsJson: string | Record<string, unknown>;
55
+ }
56
+ | {
57
+ kind: "activity";
58
+ messageId: string;
59
+ activityType: string;
60
+ content: unknown;
61
+ }
62
+ );
63
+
64
+ export type VueInspectorContextValue = {
65
+ isLocalInspectorEnabled: ComputedRef<boolean>;
66
+ openInspector: (request: VueInspectorOpenRequest) => void;
67
+ saveEventSnippet: (request: VueInspectorSaveRequest) => Promise<void>;
68
+ };
69
+
70
+ export const InspectorKey: InjectionKey<VueInspectorContextValue> = Symbol(
71
+ "CopilotKitInspector",
72
+ );
@@ -8,6 +8,9 @@ export const CopilotChatDefaultLabels = {
8
8
  assistantMessageToolbarCopyCodeLabel: "Copy",
9
9
  assistantMessageToolbarCopyCodeCopiedLabel: "Copied",
10
10
  assistantMessageToolbarCopyMessageLabel: "Copy",
11
+ assistantMessageToolbarInspectorLabel: "View in Inspector",
12
+ assistantMessageToolbarInspectorLocalOnlyLabel: "Local Only",
13
+ assistantMessageToolbarSaveSnippetLabel: "Save as snippet",
11
14
  assistantMessageToolbarThumbsUpLabel: "Good response",
12
15
  assistantMessageToolbarThumbsDownLabel: "Bad response",
13
16
  assistantMessageToolbarReadAloudLabel: "Read aloud",