@copilotkit/vue 1.75.2 → 1.77.0

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 (52) hide show
  1. package/PARITY.md +93 -35
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.mjs +275 -158
  5. package/dist/index.mjs.map +1 -1
  6. package/dist/use-render-activity-message-JCCE1lgH.cjs +79 -0
  7. package/dist/use-render-activity-message-JCCE1lgH.cjs.map +1 -0
  8. package/dist/use-render-activity-message-bPlnHjQV.js +12499 -0
  9. package/dist/use-render-activity-message-bPlnHjQV.js.map +1 -0
  10. package/dist/v2/components/MCPAppsActivityRenderer.d.ts +10 -42
  11. package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
  12. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  13. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +8 -152
  14. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  15. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
  16. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
  18. package/dist/v2/components/chat/index.d.ts +3 -3
  19. package/dist/v2/hooks/use-agent.d.ts.map +1 -1
  20. package/dist/v2/index.cjs +1 -1
  21. package/dist/v2/index.d.cts +2 -0
  22. package/dist/v2/index.d.mts +2 -0
  23. package/dist/v2/index.d.ts +2 -0
  24. package/dist/v2/index.d.ts.map +1 -1
  25. package/dist/v2/index.mjs +189 -72
  26. package/dist/v2/index.mjs.map +1 -1
  27. package/package.json +8 -7
  28. package/src/v2/__tests__/utils/test-helpers.ts +26 -6
  29. package/src/v2/components/MCPAppsActivityRenderer.ts +175 -658
  30. package/src/v2/components/__tests__/MCPAppsActivityRenderer.test.ts +39 -20
  31. package/src/v2/components/__tests__/MCPAppsActivityRendererContentError.test.ts +122 -0
  32. package/src/v2/components/__tests__/MCPAppsActivityRendererSeed.test.ts +109 -0
  33. package/src/v2/components/chat/CopilotChat.vue +100 -20
  34. package/src/v2/components/chat/CopilotPopupViewInternal.vue +1 -1
  35. package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +130 -2
  36. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +6 -1
  37. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.ts +22 -40
  38. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.ts +12 -24
  39. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +20 -0
  40. package/src/v2/components/chat/copilotChatToolCallItemRender.ts +3 -2
  41. package/src/v2/hooks/__tests__/use-agent-headers.test.ts +83 -0
  42. package/src/v2/hooks/__tests__/use-agent-stability.test.ts +2 -0
  43. package/src/v2/hooks/__tests__/use-agent.e2e.test.ts +5 -3
  44. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +2 -2
  45. package/src/v2/hooks/use-agent.ts +3 -3
  46. package/src/v2/index.ts +5 -0
  47. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.ts +6 -4
  48. package/vite.config.ts +7 -0
  49. package/dist/use-render-activity-message-CGdS03Xj.cjs +0 -85
  50. package/dist/use-render-activity-message-CGdS03Xj.cjs.map +0 -1
  51. package/dist/use-render-activity-message-XmuT9fKy.js +0 -9799
  52. package/dist/use-render-activity-message-XmuT9fKy.js.map +0 -1
@@ -48,6 +48,32 @@ async function flushAsync() {
48
48
  await nextTick();
49
49
  }
50
50
 
51
+ /**
52
+ * Wait for the sandbox iframe to be configured by the shared session. The host
53
+ * mounts the iframe synchronously, but the bridge package is loaded through a
54
+ * dynamic import and the resource is fetched through the agent, so `srcdoc` (and
55
+ * the sandbox contract attributes) land a few macrotasks later.
56
+ */
57
+ async function waitForSandboxIframe(wrapper: {
58
+ find: (selector: string) => { exists: () => boolean; element: Element };
59
+ }): Promise<HTMLIFrameElement> {
60
+ let iframe: HTMLIFrameElement | undefined;
61
+ await vi.waitFor(
62
+ () => {
63
+ const found = wrapper.find("iframe");
64
+ expect(found.exists()).toBe(true);
65
+ const element = found.element as HTMLIFrameElement;
66
+ expect(element.srcdoc).toBeTruthy();
67
+ iframe = element;
68
+ },
69
+ // The bridge arrives through a dynamic import, so this waits on real module
70
+ // loading rather than a microtask. `vi.waitFor`'s 1s default is enough in
71
+ // isolation but not when the whole monorepo's suites run in parallel.
72
+ { timeout: 10_000, interval: 50 },
73
+ );
74
+ return iframe!;
75
+ }
76
+
51
77
  describe("MCPAppsActivityRenderer", () => {
52
78
  it("exports the expected activity type and schema", () => {
53
79
  expect(MCPAppsActivityType).toBe("mcp-apps");
@@ -80,8 +106,8 @@ describe("MCPAppsActivityRenderer", () => {
80
106
  },
81
107
  message: {
82
108
  id: "activity-1",
83
- role: "assistant",
84
- content: "",
109
+ role: "activity",
110
+ content: {},
85
111
  activityType: MCPAppsActivityType,
86
112
  },
87
113
  agent: undefined,
@@ -123,8 +149,8 @@ describe("MCPAppsActivityRenderer", () => {
123
149
  },
124
150
  message: {
125
151
  id: "activity-no-border",
126
- role: "assistant",
127
- content: "",
152
+ role: "activity",
153
+ content: {},
128
154
  activityType: MCPAppsActivityType,
129
155
  },
130
156
  agent,
@@ -174,18 +200,15 @@ describe("MCPAppsActivityRenderer", () => {
174
200
  },
175
201
  message: {
176
202
  id: "activity-csp",
177
- role: "assistant",
178
- content: "",
203
+ role: "activity",
204
+ content: {},
179
205
  activityType: MCPAppsActivityType,
180
206
  },
181
207
  agent,
182
208
  },
183
209
  });
184
210
 
185
- await flushAsync();
186
- await flushAsync();
187
-
188
- const iframe = wrapper.find("iframe").element as HTMLIFrameElement;
211
+ const iframe = await waitForSandboxIframe(wrapper);
189
212
  expect(iframe.srcdoc).toContain("script-src");
190
213
  expect(iframe.srcdoc).toContain("frame-src");
191
214
  expect(iframe.srcdoc).toContain("https://widgets.example.com");
@@ -214,18 +237,15 @@ describe("MCPAppsActivityRenderer", () => {
214
237
  },
215
238
  message: {
216
239
  id: "activity-default-csp",
217
- role: "assistant",
218
- content: "",
240
+ role: "activity",
241
+ content: {},
219
242
  activityType: MCPAppsActivityType,
220
243
  },
221
244
  agent,
222
245
  },
223
246
  });
224
247
 
225
- await flushAsync();
226
- await flushAsync();
227
-
228
- const iframe = wrapper.find("iframe").element as HTMLIFrameElement;
248
+ const iframe = await waitForSandboxIframe(wrapper);
229
249
  expect(iframe.srcdoc).toContain(
230
250
  "script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' 'unsafe-eval' blob: data: http://localhost:* https://localhost:*;",
231
251
  );
@@ -260,16 +280,15 @@ describe("MCPAppsActivityRenderer", () => {
260
280
  },
261
281
  message: {
262
282
  id: "activity-testid",
263
- role: "assistant",
264
- content: "",
283
+ role: "activity",
284
+ content: {},
265
285
  activityType: MCPAppsActivityType,
266
286
  },
267
287
  agent,
268
288
  },
269
289
  });
270
290
 
271
- await flushAsync();
272
- await flushAsync();
291
+ await waitForSandboxIframe(wrapper);
273
292
 
274
293
  const iframe = wrapper.find('iframe[data-testid="mcp-app-iframe"]');
275
294
  expect(iframe.exists()).toBe(true);
@@ -0,0 +1,122 @@
1
+ /**
2
+ * A content rejection is RECOVERABLE: the session reports it through
3
+ * `onContentError` and, once valid content resumes, calls it again with `null`.
4
+ * The adapter must clear the message then - otherwise the widget starts working
5
+ * again while the host still shows a stale error until the next bind.
6
+ */
7
+ import { describe, expect, it, vi, beforeEach } from "vitest";
8
+ import { nextTick } from "vue";
9
+ import { mount } from "@vue/test-utils";
10
+ import type { AbstractAgent } from "@ag-ui/client";
11
+
12
+ // Capture the hooks the adapter hands to the session, so the test can drive the
13
+ // recoverable/fatal channels directly.
14
+ const { bindMcpAppSpy, sessionSpy } = vi.hoisted(() => {
15
+ const session = {
16
+ sendToolInput: vi.fn(),
17
+ sendToolResult: vi.fn(),
18
+ syncContent: vi.fn(),
19
+ teardown: vi.fn(),
20
+ };
21
+ return { bindMcpAppSpy: vi.fn(() => session), sessionSpy: session };
22
+ });
23
+
24
+ vi.mock("@copilotkit/mcp-apps-renderer", () => ({
25
+ bindMcpApp: bindMcpAppSpy,
26
+ }));
27
+
28
+ vi.mock("../../providers/useCopilotKit", () => ({
29
+ useCopilotKit: () => ({
30
+ copilotkit: { value: { runAgent: vi.fn() } },
31
+ }),
32
+ }));
33
+
34
+ import {
35
+ MCPAppsActivityRenderer,
36
+ MCPAppsActivityType,
37
+ } from "../MCPAppsActivityRenderer";
38
+
39
+ function createAgentMock(): AbstractAgent {
40
+ return {
41
+ threadId: "content-error-thread",
42
+ isRunning: false,
43
+ messages: [],
44
+ runAgent: vi.fn().mockResolvedValue({ result: { contents: [] } }),
45
+ addMessage: vi.fn(),
46
+ subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })),
47
+ } as unknown as AbstractAgent;
48
+ }
49
+
50
+ function mountRenderer() {
51
+ return mount(MCPAppsActivityRenderer, {
52
+ props: {
53
+ activityType: MCPAppsActivityType,
54
+ content: {
55
+ resourceUri: "ui://server/app",
56
+ serverHash: "hash",
57
+ result: { content: [] },
58
+ },
59
+ message: {
60
+ id: "activity-1",
61
+ role: "activity",
62
+ content: {},
63
+ activityType: MCPAppsActivityType,
64
+ },
65
+ agent: createAgentMock(),
66
+ },
67
+ });
68
+ }
69
+
70
+ /** The hooks object passed to the most recent bindMcpApp call. */
71
+ async function capturedHooks() {
72
+ await vi.waitFor(() => expect(bindMcpAppSpy).toHaveBeenCalled());
73
+ const options = bindMcpAppSpy.mock.calls.at(-1)![0] as {
74
+ hooks?: {
75
+ onContentError?: (err: Error | null) => void;
76
+ onError?: (err: Error) => void;
77
+ };
78
+ };
79
+ return options.hooks!;
80
+ }
81
+
82
+ describe("MCPAppsActivityRenderer content error recovery", () => {
83
+ beforeEach(() => {
84
+ vi.clearAllMocks();
85
+ });
86
+
87
+ it("shows a rejected-content error and clears it when valid content resumes", async () => {
88
+ const wrapper = mountRenderer();
89
+ const hooks = await capturedHooks();
90
+
91
+ // valid -> nothing shown
92
+ expect(wrapper.text()).not.toContain("Error:");
93
+
94
+ // invalid -> the rejection is visible
95
+ hooks.onContentError!(new Error("content does not match the schema"));
96
+ await nextTick();
97
+ expect(wrapper.text()).toContain("content does not match the schema");
98
+
99
+ // valid again -> the message disappears
100
+ hooks.onContentError!(null);
101
+ await nextTick();
102
+ expect(wrapper.text()).not.toContain("content does not match the schema");
103
+ expect(wrapper.text()).not.toContain("Error:");
104
+ });
105
+
106
+ it("keeps a fatal session error on screen", async () => {
107
+ const wrapper = mountRenderer();
108
+ const hooks = await capturedHooks();
109
+
110
+ hooks.onError!(new Error("No resource content in response"));
111
+ await nextTick();
112
+ expect(wrapper.text()).toContain("No resource content in response");
113
+
114
+ // Clearing the recoverable channel must not clear a fatal error.
115
+ hooks.onContentError!(null);
116
+ await nextTick();
117
+ expect(wrapper.text()).toContain("No resource content in response");
118
+ });
119
+ });
120
+
121
+ // Referenced so the session stub is not tree-shaken out of the mock factory.
122
+ void sessionSpy;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Vue counterpart of react-core's `MCPAppsActivityRendererSeed.test.tsx`.
3
+ *
4
+ * The session is self-driving: given a `messageId`, it subscribes to the agent's
5
+ * message store and pushes tool input/result to the widget itself. That store is
6
+ * authoritative only for activities that live in it - a host can render an
7
+ * activity from an EXTERNAL messages list (absent from `agent.messages`), whose
8
+ * props may never change after mount. For those, the adapter must seed the
9
+ * widget by calling `session.syncContent(props.content)` immediately after
10
+ * binding, otherwise the widget would never receive its initial content.
11
+ */
12
+ import { describe, expect, it, vi, beforeEach } from "vitest";
13
+ import { mount } from "@vue/test-utils";
14
+ import type { AbstractAgent } from "@ag-ui/client";
15
+
16
+ // Capture the bindMcpApp options and the returned session's calls. Hoisted so
17
+ // the vi.mock factory (which is itself hoisted above imports) can reference them.
18
+ const { bindMcpAppSpy, sessionSpy } = vi.hoisted(() => {
19
+ const session = {
20
+ sendToolInput: vi.fn(),
21
+ sendToolResult: vi.fn(),
22
+ syncContent: vi.fn(),
23
+ teardown: vi.fn(),
24
+ };
25
+ const bind = vi.fn(() => session);
26
+ return { bindMcpAppSpy: bind, sessionSpy: session };
27
+ });
28
+
29
+ // Only the bridge-carrying root entry is mocked; the bridge-free `/activity`
30
+ // entry (activity type + content schema) stays real, as in the React test.
31
+ vi.mock("@copilotkit/mcp-apps-renderer", () => ({
32
+ bindMcpApp: bindMcpAppSpy,
33
+ }));
34
+
35
+ vi.mock("../../providers/useCopilotKit", () => ({
36
+ useCopilotKit: () => ({
37
+ copilotkit: { value: { runAgent: vi.fn() } },
38
+ }),
39
+ }));
40
+
41
+ import {
42
+ MCPAppsActivityRenderer,
43
+ MCPAppsActivityType,
44
+ } from "../MCPAppsActivityRenderer";
45
+ import type { MCPAppsActivityContent } from "../MCPAppsActivityRenderer";
46
+
47
+ function createAgentMock(): AbstractAgent {
48
+ return {
49
+ threadId: "seed-thread",
50
+ isRunning: false,
51
+ // The activity under test is NOT in the store: this is the external
52
+ // messages-list case the seed exists for.
53
+ messages: [],
54
+ runAgent: vi.fn().mockResolvedValue({ result: { contents: [] } }),
55
+ addMessage: vi.fn(),
56
+ subscribe: vi.fn(() => ({ unsubscribe: vi.fn() })),
57
+ } as unknown as AbstractAgent;
58
+ }
59
+
60
+ const content: MCPAppsActivityContent = {
61
+ resourceUri: "ui://server/external",
62
+ serverHash: "hash-external",
63
+ toolInput: { city: "Paris" },
64
+ result: { content: [{ type: "text", text: "Forecast" }], isError: false },
65
+ } as MCPAppsActivityContent;
66
+
67
+ function mountRenderer() {
68
+ return mount(MCPAppsActivityRenderer, {
69
+ props: {
70
+ activityType: MCPAppsActivityType,
71
+ content,
72
+ message: {
73
+ id: "external-activity-1",
74
+ role: "activity",
75
+ content: {},
76
+ activityType: MCPAppsActivityType,
77
+ },
78
+ agent: createAgentMock(),
79
+ },
80
+ });
81
+ }
82
+
83
+ describe("MCPAppsActivityRenderer external-activity seed", () => {
84
+ beforeEach(() => {
85
+ vi.clearAllMocks();
86
+ });
87
+
88
+ it("passes the activity messageId to the bridge for self-subscription", async () => {
89
+ mountRenderer();
90
+
91
+ await vi.waitFor(() => expect(bindMcpAppSpy).toHaveBeenCalledTimes(1));
92
+ const opts = bindMcpAppSpy.mock.calls[0]![0];
93
+ expect(opts.messageId).toBe("external-activity-1");
94
+ });
95
+
96
+ it("seeds the widget with the initial tool input/result right after binding, even when props never change", async () => {
97
+ mountRenderer();
98
+
99
+ // The content watcher does not re-run for unchanged props, so without the
100
+ // explicit seed the widget would never receive this initial content.
101
+ await vi.waitFor(() => expect(sessionSpy.syncContent).toHaveBeenCalled());
102
+ expect(sessionSpy.syncContent).toHaveBeenCalledWith(
103
+ expect.objectContaining({
104
+ toolInput: { city: "Paris" },
105
+ resourceUri: "ui://server/external",
106
+ }),
107
+ );
108
+ });
109
+ });
@@ -1,9 +1,5 @@
1
1
  <script setup lang="ts">
2
- import {
3
- AGUIConnectNotImplementedError,
4
- AbstractAgent,
5
- HttpAgent,
6
- } from "@ag-ui/client";
2
+ import { AGUIConnectNotImplementedError, AbstractAgent } from "@ag-ui/client";
7
3
  import {
8
4
  createAttachmentContent,
9
5
  DEFAULT_AGENT_ID,
@@ -21,7 +17,7 @@ import {
21
17
  useSlots,
22
18
  watch,
23
19
  } from "vue";
24
- import { CopilotKitCoreErrorCode } from "@copilotkit/core";
20
+ import { CopilotKitCoreErrorCode, ɵisHttpAgent } from "@copilotkit/core";
25
21
  import type { Suggestion } from "@copilotkit/core";
26
22
  import CopilotChatConfigurationProvider from "../../providers/CopilotChatConfigurationProvider.vue";
27
23
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
@@ -111,9 +107,15 @@ type ActiveConnectCycle = {
111
107
  inspectorRequestId: string | null;
112
108
  abortController: AbortController;
113
109
  detached: boolean;
110
+ clearDiscardedBaseline?: () => void;
114
111
  };
115
112
 
116
113
  const activeConnectCycle = shallowRef<ActiveConnectCycle | null>(null);
114
+ let lastConnectCycle: ActiveConnectCycle | null = null;
115
+ const detachPromises = new WeakMap<AbstractAgent, Promise<void>>();
116
+ // Agent clones can share cursors. Finish every old reset before connecting again.
117
+ let pendingTeardown = Promise.resolve();
118
+ const baselineThreadIds = new WeakMap<AbstractAgent, string>();
117
119
 
118
120
  const resolvedAgentId = computed(
119
121
  () => props.agentId ?? existingConfig.value?.agentId ?? DEFAULT_AGENT_ID,
@@ -379,6 +381,7 @@ watch(
379
381
  // React does — makes the ordering unconditional instead of depending on
380
382
  // watcher scheduling. Non-explicit threads skip /connect below, but the first
381
383
  // runAgent still has to ship the thread the UI is rendering.
384
+ baselineThreadIds.set(currentAgent, currentAgent.threadId);
382
385
  currentAgent.threadId = threadId;
383
386
 
384
387
  // When the caller hasn't picked a specific thread, resolvedThreadId is
@@ -421,10 +424,6 @@ watch(
421
424
  cycle = existingCycle;
422
425
  } else {
423
426
  const connectAbortController = new AbortController();
424
- if (currentAgent instanceof HttpAgent) {
425
- currentAgent.abortController = connectAbortController;
426
- }
427
-
428
427
  cycle = {
429
428
  core: core as object,
430
429
  agent: currentAgent,
@@ -434,11 +433,18 @@ watch(
434
433
  detached: false,
435
434
  };
436
435
  activeConnectCycle.value = cycle;
437
-
438
- void core
439
- .connectAgent({ agent: currentAgent })
436
+ lastConnectCycle = cycle;
437
+
438
+ void pendingTeardown
439
+ .then(() => {
440
+ if (cycle.detached) return;
441
+ if (ɵisHttpAgent(currentAgent))
442
+ currentAgent.abortController = connectAbortController;
443
+ return core.connectAgent({ agent: currentAgent });
444
+ })
440
445
  .catch((error: unknown) => {
441
446
  if (cycle.detached) {
447
+ cycle.clearDiscardedBaseline?.();
442
448
  return;
443
449
  }
444
450
  if (error instanceof AGUIConnectNotImplementedError) {
@@ -460,6 +466,7 @@ watch(
460
466
  // can briefly render against an incompletely-laid-out message
461
467
  // tree and visibly snap once the last text chunk lands.
462
468
  if (cycle.detached) {
469
+ cycle.clearDiscardedBaseline?.();
463
470
  return;
464
471
  }
465
472
  const raf =
@@ -490,7 +497,17 @@ watch(
490
497
 
491
498
  activeCycle.detached = true;
492
499
  activeCycle.abortController.abort();
493
- void activeCycle.agent.detachActiveRun?.();
500
+ const detach = activeCycle.agent
501
+ .detachActiveRun()
502
+ .catch((error: unknown) => {
503
+ console.error("CopilotChat: detachActiveRun failed", error);
504
+ });
505
+ detachPromises.set(activeCycle.agent, detach);
506
+ pendingTeardown = Promise.all([pendingTeardown, detach]).then(() => {});
507
+ void detach.then(() => {
508
+ if (detachPromises.get(activeCycle.agent) === detach)
509
+ detachPromises.delete(activeCycle.agent);
510
+ });
494
511
  activeConnectCycle.value = null;
495
512
  });
496
513
  },
@@ -502,17 +519,80 @@ watch(
502
519
  // replay their history via the /connect cycle above, and the very first
503
520
  // resolution (mount) or an agent-store swap that keeps the same thread id
504
521
  // must never clear — only a real fresh-thread transition does.
505
- const lastFreshThreadId = ref<string | undefined>(undefined);
522
+ let lastFreshSelection:
523
+ | { threadId: string; agent: AbstractAgent | null }
524
+ | undefined;
506
525
  watch(
507
526
  [resolvedThreadId, hasExplicitThreadId, () => agent.value],
508
- ([threadId, isExplicit, currentAgent]) => {
509
- const previous = lastFreshThreadId.value;
510
- lastFreshThreadId.value = threadId;
527
+ ([threadId, isExplicit, currentAgent], _old, onCleanup) => {
528
+ const previous = lastFreshSelection;
529
+ const selection = { threadId, agent: currentAgent };
530
+ lastFreshSelection = selection;
511
531
  if (!currentAgent) return;
512
532
  if (isExplicit) return;
513
533
  if (previous === undefined) return;
514
- if (threadId === previous) return;
515
- currentAgent.setMessages([]);
534
+ if (threadId === previous.threadId) return;
535
+ let active = true;
536
+ let resetPending = true;
537
+ onCleanup(() => {
538
+ active = false;
539
+ });
540
+ const discardedThreadId =
541
+ previous.agent === currentAgent
542
+ ? previous.threadId
543
+ : baselineThreadIds.get(currentAgent);
544
+ const clearCursor = () => {
545
+ if (!discardedThreadId) return;
546
+ if (
547
+ "clearReplayCursor" in currentAgent &&
548
+ typeof currentAgent.clearReplayCursor === "function"
549
+ )
550
+ currentAgent.clearReplayCursor(discardedThreadId);
551
+ if (
552
+ "clearReconnectCursor" in currentAgent &&
553
+ typeof currentAgent.clearReconnectCursor === "function"
554
+ )
555
+ currentAgent.clearReconnectCursor(discardedThreadId);
556
+ };
557
+ const clearDiscardedBaseline = () => {
558
+ if (
559
+ !active ||
560
+ !resetPending ||
561
+ isUnmounting.value ||
562
+ lastFreshSelection !== selection ||
563
+ agent.value !== currentAgent
564
+ )
565
+ return;
566
+ currentAgent.setMessages([]);
567
+ currentAgent.setState({});
568
+ currentAgent.pendingInterrupts = [];
569
+ clearCursor();
570
+ };
571
+ if (
572
+ lastConnectCycle?.agent === currentAgent &&
573
+ lastConnectCycle.threadId === discardedThreadId
574
+ )
575
+ lastConnectCycle.clearDiscardedBaseline = clearDiscardedBaseline;
576
+ const detach =
577
+ detachPromises.get(currentAgent) ?? currentAgent.detachActiveRun();
578
+ detachPromises.set(currentAgent, detach);
579
+ const finishReset = () => {
580
+ if (!resetPending) return;
581
+ // The cursor belongs to the discarded view, even after a fast reopen.
582
+ clearCursor();
583
+ clearDiscardedBaseline();
584
+ resetPending = false;
585
+ };
586
+ clearDiscardedBaseline();
587
+ pendingTeardown = Promise.all([pendingTeardown, detach])
588
+ .then(finishReset)
589
+ .catch((error: unknown) => {
590
+ console.error("CopilotChat: detachActiveRun failed", error);
591
+ })
592
+ .finally(() => {
593
+ if (detachPromises.get(currentAgent) === detach)
594
+ detachPromises.delete(currentAgent);
595
+ });
516
596
  },
517
597
  { immediate: true },
518
598
  );
@@ -352,7 +352,7 @@ onBeforeUnmount(() => {
352
352
  <div
353
353
  v-if="isRendered"
354
354
  data-copilotkit
355
- class="cpk:fixed cpk:inset-0 cpk:z-[1200] cpk:flex cpk:max-w-full cpk:flex-col cpk:items-stretch cpk:md:inset-auto cpk:md:bottom-24 cpk:md:right-6 cpk:md:items-end cpk:md:gap-4"
355
+ class="cpk:fixed cpk:inset-0 cpk:z-[1200] cpk:flex cpk:max-w-full cpk:flex-col cpk:items-stretch cpk:bg-transparent cpk:md:inset-auto cpk:md:bottom-24 cpk:md:right-6 cpk:md:items-end cpk:md:gap-4"
356
356
  >
357
357
  <div
358
358
  ref="containerRef"