@copilotkit/vue 2.0.0-rc.20 → 2.0.0-rc.21

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 (193) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.mjs +13 -13
  3. package/dist/use-render-activity-message-BAhbpSDh.js +16200 -0
  4. package/dist/use-render-activity-message-BAhbpSDh.js.map +1 -0
  5. package/dist/use-render-activity-message-DJ6q1qHV.cjs +114 -0
  6. package/dist/use-render-activity-message-DJ6q1qHV.cjs.map +1 -0
  7. package/dist/v2/components/A2UIMessageRenderer.d.ts +14 -0
  8. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  9. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts +10 -3
  10. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  11. package/dist/v2/components/CopilotKitInspector.vue.d.ts +5 -1
  12. package/dist/v2/components/CopilotKitInspector.vue.d.ts.map +1 -1
  13. package/dist/v2/components/MCPAppsActivityRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts +7 -21
  15. package/dist/v2/components/OpenGenerativeUIRenderer.d.ts.map +1 -1
  16. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +0 -5
  17. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  18. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts +10 -15
  19. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts.map +1 -1
  20. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +14 -138
  21. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -1
  22. package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
  23. package/dist/v2/components/a2ui/utils.d.ts +3 -15
  24. package/dist/v2/components/a2ui/utils.d.ts.map +1 -1
  25. package/dist/v2/components/a2ui.d.ts +6 -1
  26. package/dist/v2/components/a2ui.d.ts.map +1 -1
  27. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  28. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  29. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts +4 -8
  30. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
  31. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  32. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -1
  33. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  34. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -1
  35. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  36. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  37. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -1
  38. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  39. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  40. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +1 -1
  41. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +1 -1
  42. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  43. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -1
  44. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts +1 -0
  45. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
  46. package/dist/v2/components/chat/audioRecorder.d.ts +3 -4
  47. package/dist/v2/components/chat/audioRecorder.d.ts.map +1 -1
  48. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +3 -2
  49. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
  50. package/dist/v2/components/chat/index.d.ts +6 -6
  51. package/dist/v2/components/chat/tool-call-display-context.d.ts +3 -1
  52. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -1
  53. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -1
  54. package/dist/v2/components/chat/types.d.ts +1 -1
  55. package/dist/v2/components/chat/types.d.ts.map +1 -1
  56. package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -1
  57. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +1 -1
  58. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -1
  59. package/dist/v2/hooks/use-agent-context.d.ts +2 -3
  60. package/dist/v2/hooks/use-agent-context.d.ts.map +1 -1
  61. package/dist/v2/hooks/use-agent.d.ts +4 -1
  62. package/dist/v2/hooks/use-agent.d.ts.map +1 -1
  63. package/dist/v2/hooks/use-attachments.d.ts +7 -0
  64. package/dist/v2/hooks/use-attachments.d.ts.map +1 -1
  65. package/dist/v2/hooks/use-capabilities.d.ts.map +1 -1
  66. package/dist/v2/hooks/use-component.d.ts +1 -0
  67. package/dist/v2/hooks/use-component.d.ts.map +1 -1
  68. package/dist/v2/hooks/use-configure-suggestions.d.ts +4 -9
  69. package/dist/v2/hooks/use-configure-suggestions.d.ts.map +1 -1
  70. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  71. package/dist/v2/hooks/use-human-in-the-loop.d.ts +2 -1
  72. package/dist/v2/hooks/use-human-in-the-loop.d.ts.map +1 -1
  73. package/dist/v2/hooks/use-interrupt.d.ts +0 -1
  74. package/dist/v2/hooks/use-interrupt.d.ts.map +1 -1
  75. package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
  76. package/dist/v2/hooks/use-render-custom-messages.d.ts.map +1 -1
  77. package/dist/v2/hooks/use-suggestions.d.ts.map +1 -1
  78. package/dist/v2/hooks/use-threads.d.ts +0 -8
  79. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  80. package/dist/v2/index.cjs +1 -1
  81. package/dist/v2/index.mjs +1 -1
  82. package/dist/v2/lib/vue-core.d.ts +12 -11
  83. package/dist/v2/lib/vue-core.d.ts.map +1 -1
  84. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  85. package/dist/v2/providers/CopilotKitProvider.types.d.ts +9 -124
  86. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  87. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +9 -10
  88. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  89. package/dist/v2/providers/chat-configuration-store-key.d.ts +8 -0
  90. package/dist/v2/providers/chat-configuration-store-key.d.ts.map +1 -0
  91. package/dist/v2/providers/markdown-renderer.d.ts +3 -1
  92. package/dist/v2/providers/markdown-renderer.d.ts.map +1 -1
  93. package/dist/v2/providers/use-inspector-thread-override.d.ts +5 -0
  94. package/dist/v2/providers/use-inspector-thread-override.d.ts.map +1 -1
  95. package/package.json +8 -8
  96. package/src/v2/__tests__/exports.test.ts +10 -0
  97. package/src/v2/__tests__/mocks/web-inspector.ts +6 -0
  98. package/src/v2/__tests__/utils/test-helpers.ts +34 -4
  99. package/src/v2/components/A2UIMessageRenderer.ts +98 -68
  100. package/src/v2/components/A2UISurfaceActivityRenderer.vue +68 -103
  101. package/src/v2/components/CopilotKitInspector.vue +104 -51
  102. package/src/v2/components/MCPAppsActivityRenderer.ts +75 -201
  103. package/src/v2/components/OpenGenerativeUIRenderer.ts +78 -80
  104. package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +171 -8
  105. package/src/v2/components/__tests__/CopilotKitInspector.test.ts +42 -0
  106. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.ts +9 -2
  107. package/src/v2/components/__tests__/OpenGenerativeUIToolRenderer.test.ts +38 -0
  108. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +1 -6
  109. package/src/v2/components/a2ui/A2UICatalogContext.ts +27 -93
  110. package/src/v2/components/a2ui/A2UIRecoveryStates.ts +127 -327
  111. package/src/v2/components/a2ui/catalog.ts +2 -3
  112. package/src/v2/components/a2ui/utils.ts +13 -67
  113. package/src/v2/components/a2ui.ts +14 -21
  114. package/src/v2/components/chat/CopilotChat.vue +136 -324
  115. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +22 -45
  116. package/src/v2/components/chat/CopilotChatAudioRecorder.vue +19 -246
  117. package/src/v2/components/chat/CopilotChatInput.vue +69 -407
  118. package/src/v2/components/chat/CopilotChatLightbox.vue +10 -31
  119. package/src/v2/components/chat/CopilotChatMessageView.vue +52 -112
  120. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +14 -45
  121. package/src/v2/components/chat/CopilotChatToolCallItem.vue +6 -4
  122. package/src/v2/components/chat/CopilotChatToolCallsView.vue +16 -19
  123. package/src/v2/components/chat/CopilotChatView.vue +28 -23
  124. package/src/v2/components/chat/CopilotPopupViewInternal.vue +21 -97
  125. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -25
  126. package/src/v2/components/chat/CopilotThreadsDrawer.vue +37 -102
  127. package/src/v2/components/chat/StreamingMarkdownDefault.vue +13 -26
  128. package/src/v2/components/chat/__tests__/CopilotChat.test.ts +12 -10
  129. package/src/v2/components/chat/__tests__/CopilotChatAttachmentQueue.test.ts +28 -0
  130. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +56 -0
  131. package/src/v2/components/chat/__tests__/CopilotPopupView.test.ts +19 -0
  132. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +32 -8
  133. package/src/v2/components/chat/__tests__/CopilotSidebarView.test.ts +2 -1
  134. package/src/v2/components/chat/__tests__/StreamingMarkdownDefault.test.ts +54 -0
  135. package/src/v2/components/chat/audioRecorder.ts +4 -7
  136. package/src/v2/components/chat/copilotChatToolCallItemRender.ts +22 -71
  137. package/src/v2/components/chat/tool-call-display-context.ts +6 -1
  138. package/src/v2/components/chat/tool-call-group-parts.ts +13 -33
  139. package/src/v2/components/chat/types.ts +1 -1
  140. package/src/v2/components/chat/use-lightbox.ts +15 -33
  141. package/src/v2/hooks/__tests__/use-agent-error-state.test.ts +5 -3
  142. package/src/v2/hooks/__tests__/use-agent-stability.test.ts +25 -4
  143. package/src/v2/hooks/__tests__/use-agent-throttle.test.ts +5 -1
  144. package/src/v2/hooks/__tests__/use-agent.test.ts +6 -3
  145. package/src/v2/hooks/__tests__/use-component.test.ts +21 -0
  146. package/src/v2/hooks/__tests__/use-configure-suggestions.test.ts +30 -1
  147. package/src/v2/hooks/__tests__/use-default-render-tool.test.ts +10 -10
  148. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +4 -0
  149. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.ts +6 -1
  150. package/src/v2/hooks/__tests__/use-human-in-the-loop.test.ts +187 -12
  151. package/src/v2/hooks/__tests__/use-interrupt.test.ts +97 -0
  152. package/src/v2/hooks/__tests__/use-render-activity-message.test.ts +2 -1
  153. package/src/v2/hooks/__tests__/use-threads.test.ts +48 -6
  154. package/src/v2/hooks/use-agent-context.ts +4 -10
  155. package/src/v2/hooks/use-agent.ts +86 -290
  156. package/src/v2/hooks/use-attachments.ts +42 -166
  157. package/src/v2/hooks/use-capabilities.ts +2 -7
  158. package/src/v2/hooks/use-component.ts +7 -4
  159. package/src/v2/hooks/use-configure-suggestions.ts +28 -218
  160. package/src/v2/hooks/use-default-render-tool.ts +34 -94
  161. package/src/v2/hooks/use-human-in-the-loop.ts +11 -80
  162. package/src/v2/hooks/use-interrupt.ts +29 -308
  163. package/src/v2/hooks/use-render-activity-message.ts +17 -20
  164. package/src/v2/hooks/use-render-custom-messages.ts +20 -63
  165. package/src/v2/hooks/use-suggestions.ts +9 -33
  166. package/src/v2/hooks/use-threads.ts +43 -273
  167. package/src/v2/lib/__tests__/vue-core-renderers.test.ts +42 -0
  168. package/src/v2/lib/vue-core.ts +67 -72
  169. package/src/v2/providers/CopilotChatConfigurationProvider.vue +48 -186
  170. package/src/v2/providers/CopilotKitProvider.types.ts +12 -127
  171. package/src/v2/providers/CopilotKitProvider.vue +188 -402
  172. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.ts +22 -2
  173. package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +19 -1
  174. package/src/v2/providers/__tests__/CopilotKitProvider.config.test.ts +124 -0
  175. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.ts +4 -1
  176. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +45 -2
  177. package/src/v2/providers/chat-configuration-store-key.ts +9 -0
  178. package/src/v2/providers/markdown-renderer.ts +7 -1
  179. package/src/v2/providers/use-inspector-thread-override.ts +22 -80
  180. package/vite.config.ts +5 -1
  181. package/vitest.config.ts +12 -7
  182. package/dist/use-render-activity-message-CsWYyd6r.cjs +0 -91
  183. package/dist/use-render-activity-message-CsWYyd6r.cjs.map +0 -1
  184. package/dist/use-render-activity-message-DntKKNdd.js +0 -17418
  185. package/dist/use-render-activity-message-DntKKNdd.js.map +0 -1
  186. package/dist/v2/components/chat/normalize-auto-scroll.d.ts +0 -3
  187. package/dist/v2/components/chat/normalize-auto-scroll.d.ts.map +0 -1
  188. package/dist/v2/lib/is-mobile-viewport.d.ts +0 -2
  189. package/dist/v2/lib/is-mobile-viewport.d.ts.map +0 -1
  190. package/src/v2/components/__tests__/A2UICatalogContext.test.ts +0 -63
  191. package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +0 -175
  192. package/src/v2/components/chat/normalize-auto-scroll.ts +0 -3
  193. package/src/v2/lib/is-mobile-viewport.ts +0 -5
@@ -1,10 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { AGUIConnectNotImplementedError, AbstractAgent } from "@ag-ui/client";
3
- import {
4
- createAttachmentContent,
5
- DEFAULT_AGENT_ID,
6
- randomUUID,
7
- } from "@copilotkit/shared";
3
+ import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared";
8
4
  import {
9
5
  computed,
10
6
  onBeforeUnmount,
@@ -19,12 +15,13 @@ import {
19
15
  import {
20
16
  CopilotChatDefaultLabels,
21
17
  CopilotKitCoreErrorCode,
22
- TranscriptionError,
18
+ ɵcreateChatSubmitter,
19
+ ɵcreateLastUserMessageTracker,
23
20
  ɵisHttpAgent,
24
- ɵtranscribeAudio,
25
- ɵtranscriptionErrorMessageKey,
21
+ ɵresolveChatNotice,
22
+ ɵThreadSwitchCoordinator,
26
23
  } from "@copilotkit/core";
27
- import type { Suggestion } from "@copilotkit/core";
24
+ import type { Suggestion, ɵThreadSelection } from "@copilotkit/core";
28
25
  import CopilotChatConfigurationProvider from "../../providers/CopilotChatConfigurationProvider.vue";
29
26
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
30
27
  import { useCopilotKit } from "../../providers/useCopilotKit";
@@ -38,7 +35,6 @@ import CopilotChatView from "./CopilotChatView.vue";
38
35
  import { LastUserMessageKey } from "./last-user-message-context";
39
36
  import type { LastUserMessageState } from "./last-user-message-context";
40
37
  import type { Message } from "@ag-ui/core";
41
- import type { InputContent } from "@copilotkit/shared";
42
38
  import { useInspectorThreadOverride } from "../../providers/use-inspector-thread-override";
43
39
  import { CopilotKitStyles } from "../../styles/CopilotKitStyles";
44
40
  import type {
@@ -99,27 +95,23 @@ const forwardedChatViewSlotNames = computed(() =>
99
95
  const generatedThreadId = ref(randomUUID());
100
96
  const transcribeMode = ref<"input" | "transcribe" | "processing">("input");
101
97
  const inputValue = ref(props.inputValue ?? "");
102
- const transcriptionError = ref<string | null>(null);
103
- const isTranscribing = ref(false);
104
98
  const isMounted = ref(false);
105
99
  const isUnmounting = ref(false);
106
100
 
107
101
  type ActiveConnectCycle = {
108
102
  core: object;
109
- agent: AbstractAgent;
110
- threadId: string;
111
- inspectorRequestId: string | null;
103
+ selection: ɵThreadSelection;
112
104
  abortController: AbortController;
113
105
  detached: boolean;
114
- clearDiscardedBaseline?: () => void;
115
106
  };
116
107
 
117
108
  const activeConnectCycle = shallowRef<ActiveConnectCycle | null>(null);
118
- let lastConnectCycle: ActiveConnectCycle | null = null;
119
- const detachPromises = new WeakMap<AbstractAgent, Promise<void>>();
120
- // Agent clones can share cursors. Finish every old reset before connecting again.
121
- let pendingTeardown = Promise.resolve();
122
- const baselineThreadIds = new WeakMap<AbstractAgent, string>();
109
+ // Keeps the agent on the thread this chat shows, orders the teardown of the
110
+ // previous thread, and tracks connections a send must wait for.
111
+ const threads = new ɵThreadSwitchCoordinator({
112
+ stopAgent: (params) => copilotkit.value.stopAgent(params),
113
+ subscribe: (subscriber) => copilotkit.value.subscribe(subscriber),
114
+ });
123
115
 
124
116
  const resolvedAgentId = computed(
125
117
  () => props.agentId ?? existingConfig.value?.agentId ?? DEFAULT_AGENT_ID,
@@ -235,27 +227,11 @@ const shouldAllowStop = computed(
235
227
  // Track the latest user message id so descendants (notably `usePinToSend`
236
228
  // inside `CopilotChatView`) can anchor to the most recent user turn.
237
229
  // Mirrors React `CopilotChat.tsx` `lastUserMessageId` + `sendNonce` state.
238
- const lastUserMessageId = computed<string | null>(() => {
239
- for (let i = messages.value.length - 1; i >= 0; i--) {
240
- const message = messages.value[i];
241
- if (message?.role === "user") return message.id;
242
- }
243
- return null;
244
- });
245
- const sendNonce = ref(0);
246
- // Seed with the current value so restoring a thread with existing messages
247
- // does not count as a new send. Only later id transitions bump.
248
- let prevLastUserMessageId: string | null = lastUserMessageId.value;
249
- watch(lastUserMessageId, (next) => {
250
- if (next && next !== prevLastUserMessageId) {
251
- sendNonce.value += 1;
252
- prevLastUserMessageId = next;
253
- }
254
- });
255
- const lastUserMessageState = computed<LastUserMessageState>(() => ({
256
- id: lastUserMessageId.value,
257
- sendNonce: sendNonce.value,
258
- }));
230
+ // Messages the chat starts with (a restored thread) do not count as a send.
231
+ const lastUserMessageTracker = ɵcreateLastUserMessageTracker(messages.value);
232
+ const lastUserMessageState = computed<LastUserMessageState>(() =>
233
+ lastUserMessageTracker.update(messages.value),
234
+ );
259
235
  provide(LastUserMessageKey, lastUserMessageState);
260
236
 
261
237
  watch(
@@ -370,35 +346,23 @@ watch(
370
346
  return;
371
347
  }
372
348
 
373
- const previousCycle = activeConnectCycle.value;
374
- const inspectorTransition =
375
- previousCycle !== null &&
376
- previousCycle.threadId !== threadId &&
377
- previousCycle.inspectorRequestId !== requestId &&
378
- (previousCycle.inspectorRequestId !== null || requestId !== null);
379
- if (inspectorTransition) {
380
- try {
381
- core.stopAgent({ agent: currentAgent });
382
- } catch {
383
- // No live run to stop.
384
- }
385
- }
386
-
387
- // Pin the thread this chat renders onto its agent, before anything issues a
388
- // request. `CopilotKitCore.connectAgent` reads `agent.threadId` synchronously
389
- // to decide whether this is a fresh restore, so a later assignment would let
390
- // /connect address the previous thread. Assigning inside this callback — as
391
- // React does — makes the ordering unconditional instead of depending on
392
- // watcher scheduling. Non-explicit threads skip /connect below, but the first
393
- // runAgent still has to ship the thread the UI is rendering.
394
- baselineThreadIds.set(currentAgent, currentAgent.threadId);
395
- currentAgent.threadId = threadId;
349
+ // Pins the thread on the agent before anything issues a request:
350
+ // `CopilotKitCore.connectAgent` reads `agent.threadId` synchronously. A
351
+ // switch to a fresh, non-explicit thread (a "+ New" reset) empties the
352
+ // agent so the welcome screen shows; moving in or out of an inspector
353
+ // override stops the run.
354
+ const selection = threads.select({
355
+ agent: currentAgent,
356
+ threadId,
357
+ explicit: isExplicit,
358
+ inspectorRequestId: requestId,
359
+ });
396
360
 
397
361
  // When the caller hasn't picked a specific thread, resolvedThreadId is
398
362
  // a UUID minted locally. The backend has never seen it, so /connect
399
363
  // would always 404 — skip the call. A real thread is only created
400
364
  // once the user runs the agent for the first time.
401
- if (!isExplicit) {
365
+ if (!selection.shouldConnect) {
402
366
  return;
403
367
  }
404
368
 
@@ -425,9 +389,7 @@ watch(
425
389
  const hasSameDeps =
426
390
  existingCycle &&
427
391
  existingCycle.core === (core as object) &&
428
- existingCycle.agent === currentAgent &&
429
- existingCycle.threadId === threadId &&
430
- existingCycle.inspectorRequestId === requestId;
392
+ existingCycle.selection === selection;
431
393
 
432
394
  let cycle: ActiveConnectCycle;
433
395
  if (hasSameDeps && existingCycle) {
@@ -436,58 +398,60 @@ watch(
436
398
  const connectAbortController = new AbortController();
437
399
  cycle = {
438
400
  core: core as object,
439
- agent: currentAgent,
440
- threadId,
441
- inspectorRequestId: requestId,
401
+ selection,
442
402
  abortController: connectAbortController,
443
403
  detached: false,
444
404
  };
445
405
  activeConnectCycle.value = cycle;
446
- lastConnectCycle = cycle;
447
-
448
- void pendingTeardown
449
- .then(() => {
450
- if (cycle.detached) return;
451
- if (ɵisHttpAgent(currentAgent))
452
- currentAgent.abortController = connectAbortController;
453
- return core.connectAgent({ agent: currentAgent });
454
- })
455
- .catch((error: unknown) => {
456
- if (cycle.detached) {
457
- cycle.clearDiscardedBaseline?.();
458
- return;
459
- }
460
- if (error instanceof AGUIConnectNotImplementedError) {
461
- return;
462
- }
463
- console.error("CopilotChat: connectAgent failed", error);
464
- if (requestId) {
465
- failInspectorOverride(requestId);
466
- }
467
- })
468
- .finally(() => {
469
- // Whether the connect succeeded or failed, we're no longer in
470
- // the transitional "connecting" state for this thread — release
471
- // the welcome / suggestion gating so the view can settle.
472
- //
473
- // Defer one animation frame so any trailing reactive commits
474
- // from the bootstrap replay (final assistant message content)
475
- // paint before isConnecting flips off. Without this, suggestions
476
- // can briefly render against an incompletely-laid-out message
477
- // tree and visibly snap once the last text chunk lands.
478
- if (cycle.detached) {
479
- cycle.clearDiscardedBaseline?.();
480
- return;
481
- }
482
- const raf =
483
- typeof requestAnimationFrame === "function"
484
- ? requestAnimationFrame
485
- : (cb: () => void) => setTimeout(cb, 16);
486
- raf(() => {
406
+
407
+ // Tracked, so a send waits for the thread to load instead of racing it.
408
+ void threads.trackConnection(selection, () =>
409
+ threads
410
+ .whenTornDown()
411
+ .then(() => {
487
412
  if (cycle.detached) return;
488
- lastConnectedThreadId.value = cycle.threadId;
489
- });
490
- });
413
+ if (ɵisHttpAgent(currentAgent))
414
+ currentAgent.abortController = connectAbortController;
415
+ return core.connectAgent({ agent: currentAgent });
416
+ })
417
+ .catch((error: unknown) => {
418
+ if (cycle.detached) {
419
+ selection.clearDiscardedBaseline();
420
+ return;
421
+ }
422
+ if (error instanceof AGUIConnectNotImplementedError) {
423
+ return;
424
+ }
425
+ if (requestId) {
426
+ failInspectorOverride(requestId);
427
+ }
428
+ // The coordinator logs the failure and blocks sends on it.
429
+ throw error;
430
+ })
431
+ .finally(() => {
432
+ // Whether the connect succeeded or failed, we're no longer in
433
+ // the transitional "connecting" state for this thread — release
434
+ // the welcome / suggestion gating so the view can settle.
435
+ //
436
+ // Defer one animation frame so any trailing reactive commits
437
+ // from the bootstrap replay (final assistant message content)
438
+ // paint before isConnecting flips off. Without this, suggestions
439
+ // can briefly render against an incompletely-laid-out message
440
+ // tree and visibly snap once the last text chunk lands.
441
+ if (cycle.detached) {
442
+ selection.clearDiscardedBaseline();
443
+ return;
444
+ }
445
+ const raf =
446
+ typeof requestAnimationFrame === "function"
447
+ ? requestAnimationFrame
448
+ : (cb: () => void) => setTimeout(cb, 16);
449
+ raf(() => {
450
+ if (cycle.detached) return;
451
+ lastConnectedThreadId.value = selection.threadId;
452
+ });
453
+ }),
454
+ );
491
455
  }
492
456
 
493
457
  onCleanup(() => {
@@ -496,227 +460,94 @@ watch(
496
460
  return;
497
461
  }
498
462
 
463
+ const { selection: cycleSelection } = activeCycle;
499
464
  const shouldDetach =
500
465
  isUnmounting.value ||
501
466
  copilotkit.value !== activeCycle.core ||
502
- agent.value !== activeCycle.agent ||
503
- resolvedThreadId.value !== activeCycle.threadId;
467
+ agent.value !== cycleSelection.agent ||
468
+ resolvedThreadId.value !== cycleSelection.threadId;
504
469
  if (!shouldDetach) {
505
470
  return;
506
471
  }
507
472
 
473
+ cycleSelection.release();
508
474
  activeCycle.detached = true;
509
475
  activeCycle.abortController.abort();
510
- const detach = activeCycle.agent
511
- .detachActiveRun()
512
- .catch((error: unknown) => {
513
- console.error("CopilotChat: detachActiveRun failed", error);
514
- });
515
- detachPromises.set(activeCycle.agent, detach);
516
- pendingTeardown = Promise.all([pendingTeardown, detach]).then(() => {});
517
- void detach.then(() => {
518
- if (detachPromises.get(activeCycle.agent) === detach)
519
- detachPromises.delete(activeCycle.agent);
520
- });
476
+ void threads.detach(cycleSelection.agent);
521
477
  activeConnectCycle.value = null;
522
478
  });
523
479
  },
524
480
  { immediate: true },
525
481
  );
526
482
 
527
- // Clear stale messages when the active thread switches to a fresh,
528
- // non-explicit thread (e.g. a "+ New" reset). Explicit thread switches
529
- // replay their history via the /connect cycle above, and the very first
530
- // resolution (mount) or an agent-store swap that keeps the same thread id
531
- // must never clear — only a real fresh-thread transition does.
532
- let lastFreshSelection:
533
- | { threadId: string; agent: AbstractAgent | null }
534
- | undefined;
535
- watch(
536
- [resolvedThreadId, hasExplicitThreadId, () => agent.value],
537
- ([threadId, isExplicit, currentAgent], _old, onCleanup) => {
538
- const previous = lastFreshSelection;
539
- const selection = { threadId, agent: currentAgent };
540
- lastFreshSelection = selection;
541
- if (!currentAgent) return;
542
- if (isExplicit) return;
543
- if (previous === undefined) return;
544
- if (threadId === previous.threadId) return;
545
- let active = true;
546
- let resetPending = true;
547
- onCleanup(() => {
548
- active = false;
549
- });
550
- const discardedThreadId =
551
- previous.agent === currentAgent
552
- ? previous.threadId
553
- : baselineThreadIds.get(currentAgent);
554
- const clearCursor = () => {
555
- if (!discardedThreadId) return;
556
- if (
557
- "clearReplayCursor" in currentAgent &&
558
- typeof currentAgent.clearReplayCursor === "function"
559
- )
560
- currentAgent.clearReplayCursor(discardedThreadId);
561
- if (
562
- "clearReconnectCursor" in currentAgent &&
563
- typeof currentAgent.clearReconnectCursor === "function"
564
- )
565
- currentAgent.clearReconnectCursor(discardedThreadId);
566
- };
567
- const clearDiscardedBaseline = () => {
568
- if (
569
- !active ||
570
- !resetPending ||
571
- isUnmounting.value ||
572
- lastFreshSelection !== selection ||
573
- agent.value !== currentAgent
574
- )
575
- return;
576
- currentAgent.setMessages([]);
577
- currentAgent.setState({});
578
- currentAgent.pendingInterrupts = [];
579
- clearCursor();
580
- };
581
- if (
582
- lastConnectCycle?.agent === currentAgent &&
583
- lastConnectCycle.threadId === discardedThreadId
584
- )
585
- lastConnectCycle.clearDiscardedBaseline = clearDiscardedBaseline;
586
- const detach =
587
- detachPromises.get(currentAgent) ?? currentAgent.detachActiveRun();
588
- detachPromises.set(currentAgent, detach);
589
- const finishReset = () => {
590
- if (!resetPending) return;
591
- // The cursor belongs to the discarded view, even after a fast reopen.
592
- clearCursor();
593
- clearDiscardedBaseline();
594
- resetPending = false;
595
- };
596
- clearDiscardedBaseline();
597
- pendingTeardown = Promise.all([pendingTeardown, detach])
598
- .then(finishReset)
599
- .catch((error: unknown) => {
600
- console.error("CopilotChat: detachActiveRun failed", error);
601
- })
602
- .finally(() => {
603
- if (detachPromises.get(currentAgent) === detach)
604
- detachPromises.delete(currentAgent);
605
- });
483
+ // The send side — queueing behind the run in flight and the thread's
484
+ // connection, the upload guard, stop, and transcription — lives in core.
485
+ const submitter = ɵcreateChatSubmitter({
486
+ core: {
487
+ get runtimeUrl() {
488
+ return copilotkit.value.runtimeUrl;
489
+ },
490
+ get runtimeTransport() {
491
+ return copilotkit.value.runtimeTransport;
492
+ },
493
+ get credentials() {
494
+ return copilotkit.value.credentials;
495
+ },
496
+ resolveHeaders: () => copilotkit.value.resolveHeaders(),
497
+ runAgent: (params) => copilotkit.value.runAgent(params),
498
+ stopAgent: (params) => copilotkit.value.stopAgent(params),
606
499
  },
607
- { immediate: true },
608
- );
500
+ getAgent: () => agent.value ?? undefined,
501
+ input: {
502
+ get: () => inputValue.value,
503
+ set: (value) => {
504
+ inputValue.value = value;
505
+ },
506
+ },
507
+ attachments: {
508
+ isUploading: () =>
509
+ attachments.value.some((attachment) => attachment.status === "uploading"),
510
+ consumeReady: consumeAttachments,
511
+ },
512
+ threads,
513
+ });
514
+ const submitterState = shallowRef(submitter.getSnapshot());
515
+ const unsubscribeSubmitter = submitter.subscribe(() => {
516
+ submitterState.value = submitter.getSnapshot();
517
+ });
518
+ const isTranscribing = computed(() => submitterState.value.isTranscribing);
519
+ const transcriptionError = computed(() => {
520
+ const { notice } = submitterState.value;
521
+ return notice ? ɵresolveChatNotice(notice, chatLabels.value) : null;
522
+ });
609
523
 
610
524
  onBeforeUnmount(() => {
611
525
  isUnmounting.value = true;
526
+ unsubscribeSubmitter();
527
+ submitter.dispose();
528
+ threads.dispose();
612
529
  });
613
530
 
614
531
  onMounted(() => {
615
532
  isMounted.value = true;
616
533
  });
617
534
 
618
- watch(transcriptionError, (next, _old, onCleanup) => {
619
- if (!next) {
620
- return;
621
- }
622
- const timer = setTimeout(() => {
623
- transcriptionError.value = null;
624
- }, 5000);
625
- onCleanup(() => clearTimeout(timer));
626
- });
627
-
628
- async function runCurrentAgent() {
629
- const currentAgent = agent.value;
630
- if (!currentAgent) {
631
- return;
632
- }
633
-
634
- try {
635
- const activeCycle = activeConnectCycle.value;
636
- if (
637
- activeCycle &&
638
- activeCycle.agent === currentAgent &&
639
- activeCycle.threadId === resolvedThreadId.value
640
- ) {
641
- activeCycle.detached = true;
642
- activeCycle.abortController.abort();
643
- activeConnectCycle.value = null;
644
- await activeCycle.agent.detachActiveRun?.();
645
- }
646
- await copilotkit.value.runAgent({ agent: currentAgent });
647
- } catch (error) {
648
- console.error("CopilotChat: runAgent failed", error);
649
- }
650
- }
651
-
652
535
  async function handleSubmitMessage(value: string) {
653
- const hasUploading = attachments.value.some(
536
+ // A send blocked by an upload shows its notice but is not reported.
537
+ const isBlockedByUpload = attachments.value.some(
654
538
  (attachment) => attachment.status === "uploading",
655
539
  );
656
- if (hasUploading) {
657
- console.error("[CopilotKit] Cannot send while attachments are uploading");
658
- return;
659
- }
660
-
661
- emit("submit-message", value);
662
-
663
- if (!agent.value) {
664
- console.error("[CopilotKit] Cannot send message: no agent available");
665
- return;
666
- }
667
-
668
- const readyAttachments = consumeAttachments();
669
- if (readyAttachments.length > 0) {
670
- const contentParts: InputContent[] = [];
671
- if (value.trim()) {
672
- contentParts.push({ type: "text", text: value });
673
- }
674
- for (const attachment of readyAttachments) {
675
- contentParts.push(createAttachmentContent(attachment));
676
- }
677
- agent.value.addMessage({
678
- id: randomUUID(),
679
- role: "user",
680
- content: contentParts,
681
- });
682
- } else {
683
- agent.value.addMessage({
684
- id: randomUUID(),
685
- role: "user",
686
- content: value,
687
- });
688
- }
689
-
690
- inputValue.value = "";
691
- await runCurrentAgent();
540
+ if (!isBlockedByUpload) emit("submit-message", value);
541
+ await submitter.submit(value);
692
542
  }
693
543
 
694
544
  async function handleSelectSuggestion(suggestion: Suggestion, index: number) {
695
545
  emit("select-suggestion", suggestion, index);
696
- agent.value?.addMessage({
697
- id: randomUUID(),
698
- role: "user",
699
- content: suggestion.message,
700
- });
701
- await runCurrentAgent();
546
+ await submitter.selectSuggestion(suggestion);
702
547
  }
703
548
 
704
549
  function stopCurrentRun() {
705
- const currentAgent = agent.value;
706
- if (!currentAgent) {
707
- return;
708
- }
709
-
710
- try {
711
- copilotkit.value.stopAgent({ agent: currentAgent });
712
- } catch (error) {
713
- console.error("CopilotChat: stopAgent failed", error);
714
- try {
715
- currentAgent.abortRun();
716
- } catch (abortError) {
717
- console.error("CopilotChat: abortRun fallback failed", abortError);
718
- }
719
- }
550
+ submitter.stop();
720
551
  }
721
552
 
722
553
  function handleStop() {
@@ -745,7 +576,7 @@ function handleStartTranscribe() {
745
576
  if (!showTranscription.value) {
746
577
  return;
747
578
  }
748
- transcriptionError.value = null;
579
+ submitter.showNotice(null);
749
580
  transcribeMode.value = "transcribe";
750
581
  emit("start-transcribe");
751
582
  }
@@ -754,7 +585,7 @@ function handleCancelTranscribe() {
754
585
  if (!showTranscription.value) {
755
586
  return;
756
587
  }
757
- transcriptionError.value = null;
588
+ submitter.showNotice(null);
758
589
  transcribeMode.value = "input";
759
590
  emit("cancel-transcribe");
760
591
  }
@@ -776,26 +607,7 @@ async function handleFinishTranscribeWithAudio(audioBlob: Blob) {
776
607
  return;
777
608
  }
778
609
 
779
- isTranscribing.value = true;
780
- try {
781
- transcriptionError.value = null;
782
- const result = await ɵtranscribeAudio(copilotkit.value, audioBlob);
783
- const trimmedPrevious = inputValue.value.trim();
784
- inputValue.value = trimmedPrevious
785
- ? `${trimmedPrevious} ${result.text}`
786
- : result.text;
787
- } catch (error) {
788
- console.error("CopilotChat: Transcription failed", error);
789
- const labels = chatLabels.value;
790
- if (error instanceof TranscriptionError) {
791
- const key = ɵtranscriptionErrorMessageKey(error.info);
792
- transcriptionError.value = key ? labels[key] : error.info.message;
793
- } else {
794
- transcriptionError.value = labels.transcriptionErrorGenericText;
795
- }
796
- } finally {
797
- isTranscribing.value = false;
798
- }
610
+ await submitter.transcribe(audioBlob);
799
611
  }
800
612
 
801
613
  const chatViewSlotProps = computed<CopilotChatViewOverrideSlotProps>(() => ({