@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
@@ -8,6 +8,11 @@ import {
8
8
  useAttrs,
9
9
  watch,
10
10
  } from "vue";
11
+ import {
12
+ ɵPOPUP_ANIMATION_MS,
13
+ ɵbindPopupDismiss,
14
+ ɵdimensionToCss,
15
+ } from "@copilotkit/core";
11
16
  import { CopilotChatDefaultLabels } from "../../providers/types";
12
17
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
13
18
  import CopilotChatToggleButton from "./CopilotChatToggleButton.vue";
@@ -32,7 +37,7 @@ defineOptions({ inheritAttrs: false });
32
37
 
33
38
  const DEFAULT_POPUP_WIDTH = 420;
34
39
  const DEFAULT_POPUP_HEIGHT = 560;
35
- const POPUP_ANIMATION_MS = 200;
40
+ const POPUP_ANIMATION_MS = ɵPOPUP_ANIMATION_MS;
36
41
 
37
42
  const props = withDefaults(
38
43
  defineProps<Omit<CopilotPopupViewProps, "defaultOpen">>(),
@@ -100,7 +105,6 @@ const containerRef = ref<HTMLElement | null>(null);
100
105
  const isRendered = ref(config.value?.isModalOpen ?? false);
101
106
  const isAnimatingOut = ref(false);
102
107
  let animationTimer: ReturnType<typeof setTimeout> | undefined;
103
- let focusTimer: ReturnType<typeof setTimeout> | undefined;
104
108
 
105
109
  const isPopupOpen = computed(() => config.value?.isModalOpen ?? false);
106
110
  const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
@@ -113,8 +117,11 @@ const popupAnimationClass = computed(() =>
113
117
  const popupStyle = computed(
114
118
  () =>
115
119
  ({
116
- "--copilot-popup-width": dimensionToCss(props.width, DEFAULT_POPUP_WIDTH),
117
- "--copilot-popup-height": dimensionToCss(
120
+ "--copilot-popup-width": ɵdimensionToCss(
121
+ props.width,
122
+ DEFAULT_POPUP_WIDTH,
123
+ ),
124
+ "--copilot-popup-height": ɵdimensionToCss(
118
125
  props.height,
119
126
  DEFAULT_POPUP_HEIGHT,
120
127
  ),
@@ -127,19 +134,6 @@ const popupStyle = computed(
127
134
  }) as Record<string, string>,
128
135
  );
129
136
 
130
- function dimensionToCss(
131
- value: number | string | undefined,
132
- fallback: number,
133
- ): string {
134
- if (typeof value === "number" && Number.isFinite(value)) {
135
- return `${value}px`;
136
- }
137
- if (typeof value === "string" && value.trim().length > 0) {
138
- return value;
139
- }
140
- return `${fallback}px`;
141
- }
142
-
143
137
  function closePopup() {
144
138
  config.value?.setModalOpen?.(false);
145
139
  }
@@ -246,87 +240,20 @@ watch(
246
240
  { immediate: true },
247
241
  );
248
242
 
249
- watch(
250
- isPopupOpen,
251
- (open, _old, onCleanup) => {
252
- if (!open || typeof window === "undefined") {
253
- return;
254
- }
255
-
256
- const handleKeyDown = (event: KeyboardEvent) => {
257
- if (event.key === "Escape") {
258
- event.preventDefault();
259
- closePopup();
260
- }
261
- };
262
-
263
- window.addEventListener("keydown", handleKeyDown);
264
- onCleanup(() => {
265
- window.removeEventListener("keydown", handleKeyDown);
266
- });
267
- },
268
- { immediate: true },
269
- );
270
-
271
- watch(
272
- isPopupOpen,
273
- (open, _old, onCleanup) => {
274
- if (!open) {
275
- return;
276
- }
277
-
278
- focusTimer = setTimeout(() => {
279
- if (typeof document === "undefined") {
280
- return;
281
- }
282
- const container = containerRef.value;
283
- if (container && !container.contains(document.activeElement)) {
284
- container.focus({ preventScroll: true });
285
- }
286
- }, POPUP_ANIMATION_MS);
287
-
288
- onCleanup(() => {
289
- if (focusTimer) {
290
- clearTimeout(focusTimer);
291
- focusTimer = undefined;
292
- }
293
- });
294
- },
295
- { immediate: true },
296
- );
297
-
243
+ // Escape, a click outside (when enabled), and focus once the popup opens.
298
244
  watch(
299
245
  [isPopupOpen, () => props.clickOutsideToClose],
300
- ([open, allowClose], _old, onCleanup) => {
301
- if (!open || !allowClose || typeof document === "undefined") {
246
+ ([open, clickOutsideToClose], _old, onCleanup) => {
247
+ if (!open) {
302
248
  return;
303
249
  }
304
-
305
- const handlePointerDown = (event: PointerEvent) => {
306
- const target = event.target as Node | null;
307
- if (!target) {
308
- return;
309
- }
310
-
311
- const container = containerRef.value;
312
- if (container?.contains(target)) {
313
- return;
314
- }
315
-
316
- const toggleButton = document.querySelector(
317
- "[data-slot='chat-toggle-button']",
318
- );
319
- if (toggleButton?.contains(target)) {
320
- return;
321
- }
322
-
323
- closePopup();
324
- };
325
-
326
- document.addEventListener("pointerdown", handlePointerDown);
327
- onCleanup(() => {
328
- document.removeEventListener("pointerdown", handlePointerDown);
329
- });
250
+ onCleanup(
251
+ ɵbindPopupDismiss({
252
+ getContainer: () => containerRef.value,
253
+ onDismiss: closePopup,
254
+ clickOutsideToClose: clickOutsideToClose === true,
255
+ }),
256
+ );
330
257
  },
331
258
  { immediate: true },
332
259
  );
@@ -339,9 +266,6 @@ onBeforeUnmount(() => {
339
266
  if (animationTimer) {
340
267
  clearTimeout(animationTimer);
341
268
  }
342
- if (focusTimer) {
343
- clearTimeout(focusTimer);
344
- }
345
269
  });
346
270
  </script>
347
271
 
@@ -8,6 +8,8 @@ import {
8
8
  useAttrs,
9
9
  watch,
10
10
  } from "vue";
11
+ import { ɵcreateDockedSidebar, ɵdimensionToCss } from "@copilotkit/core";
12
+ import type { ɵDockedSidebar } from "@copilotkit/core";
11
13
  import { CopilotChatDefaultLabels } from "../../providers/types";
12
14
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
13
15
  import CopilotChatToggleButton from "./CopilotChatToggleButton.vue";
@@ -122,32 +124,23 @@ const asideAttrs = computed(() => {
122
124
  const asideStyle = computed(
123
125
  () =>
124
126
  ({
125
- "--sidebar-width": widthToCss(resolvedSidebarWidth.value),
127
+ "--sidebar-width": widthCss.value,
126
128
  paddingTop: "env(safe-area-inset-top)",
127
129
  paddingBottom: "env(safe-area-inset-bottom)",
128
130
  }) as Record<string, string>,
129
131
  );
130
- // The page makes room on the docked edge (desktop only).
131
- const bodyMarginStyle = computed(() => {
132
- const margin =
133
- props.position === "left" ? "margin-inline-start" : "margin-inline-end";
134
- return `
135
- @media (min-width: 768px) {
136
- body {
137
- ${margin}: ${widthToMargin(resolvedSidebarWidth.value)};
138
- transition: ${margin} ${SIDEBAR_TRANSITION_MS}ms ease;
139
- }
140
- }
141
- `;
142
- });
143
-
144
- function widthToCss(width: number | string): string {
145
- return typeof width === "number" ? `${width}px` : width;
146
- }
132
+ const widthCss = computed(() =>
133
+ ɵdimensionToCss(resolvedSidebarWidth.value, DEFAULT_SIDEBAR_WIDTH),
134
+ );
147
135
 
148
- function widthToMargin(width: number | string): string {
149
- return typeof width === "number" ? `${width}px` : width;
150
- }
136
+ // The page makes room on the docked edge (desktop only).
137
+ let dock: ɵDockedSidebar | undefined;
138
+ watch(
139
+ [isSidebarOpen, () => props.position, widthCss],
140
+ ([open, position, width]) => {
141
+ dock?.set({ open, position, width });
142
+ },
143
+ );
151
144
 
152
145
  function updateMeasuredWidth() {
153
146
  if (props.width !== undefined || !sidebarRef.value) {
@@ -245,6 +238,14 @@ watch(
245
238
  );
246
239
 
247
240
  onMounted(async () => {
241
+ dock = ɵcreateDockedSidebar(document, {
242
+ transitionMs: SIDEBAR_TRANSITION_MS,
243
+ });
244
+ dock.set({
245
+ open: isSidebarOpen.value,
246
+ position: props.position,
247
+ width: widthCss.value,
248
+ });
248
249
  await nextTick();
249
250
  updateMeasuredWidth();
250
251
 
@@ -263,16 +264,14 @@ onMounted(async () => {
263
264
  });
264
265
 
265
266
  onBeforeUnmount(() => {
267
+ dock?.dispose();
268
+ dock = undefined;
266
269
  resizeObserver?.disconnect();
267
270
  resizeObserver = null;
268
271
  });
269
272
  </script>
270
273
 
271
274
  <template>
272
- <component :is="'style'" v-if="isSidebarOpen">
273
- {{ bodyMarginStyle }}
274
- </component>
275
-
276
275
  <slot
277
276
  name="toggle-button"
278
277
  :is-open="isSidebarOpen"
@@ -8,7 +8,6 @@ import { computed, onMounted, onScopeDispose, ref, watchEffect } from "vue";
8
8
  // below is kept at module scope.
9
9
  import type {
10
10
  CopilotKitThreadsDrawer as CopilotKitThreadsDrawerElement,
11
- DrawerThread,
12
11
  ThreadSelectedDetail,
13
12
  ArchiveDetail,
14
13
  UnarchiveDetail,
@@ -23,6 +22,12 @@ import type {
23
22
  // in this worktree predate it.
24
23
  type CollapseChangeDetail = { collapsed: boolean };
25
24
  import { DEFAULT_AGENT_ID } from "@copilotkit/shared";
25
+ import {
26
+ ɵcreateThreadsDrawerActions,
27
+ ɵgetThreadsDrawerElementProps,
28
+ ɵresolveThreadsLicense,
29
+ ɵtoDrawerThread,
30
+ } from "@copilotkit/core";
26
31
  import { useThreads } from "../../hooks/use-threads";
27
32
  import type { Thread } from "../../hooks/use-threads";
28
33
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
@@ -71,15 +76,11 @@ const emit = defineEmits<{
71
76
  const config = useCopilotChatConfiguration();
72
77
  const license = useLicenseContext();
73
78
 
74
- const licensed = computed(() => {
75
- const status = license.value.status;
76
- // `unknown` is an unrecovered retryable entitlement lookup, not a settled
77
- // negative: fetch the list and let the threads endpoint decide.
78
- if (status === "unknown") return true;
79
- const present = status === "valid" || status === "expiring";
80
- return present && license.value.checkFeature("threads");
81
- });
82
- const licensePending = computed(() => license.value.status === null);
79
+ // `unknown` (an unrecovered entitlement lookup) counts as licensed so the
80
+ // threads endpoint decides; `null` (not reported yet) is pending.
81
+ const threadsLicense = computed(() => ɵresolveThreadsLicense(license.value));
82
+ const licensed = computed(() => threadsLicense.value.licensed);
83
+ const licensePending = computed(() => threadsLicense.value.licensePending);
83
84
 
84
85
  const resolvedAgentId = computed(
85
86
  () => props.agentId ?? config.value?.agentId ?? DEFAULT_AGENT_ID,
@@ -107,18 +108,8 @@ const threadsApi = useThreads({
107
108
  limit: () => props.limit,
108
109
  });
109
110
 
110
- function toDrawerThread(t: Thread): DrawerThread {
111
- return {
112
- id: t.id,
113
- name: t.name,
114
- archived: t.archived,
115
- createdAt: t.createdAt,
116
- updatedAt: t.updatedAt,
117
- ...(t.lastRunAt !== undefined ? { lastRunAt: t.lastRunAt } : {}),
118
- };
119
- }
120
111
  const drawerThreads = computed(() =>
121
- threadsApi.threads.value.map(toDrawerThread),
112
+ threadsApi.threads.value.map(ɵtoDrawerThread),
122
113
  );
123
114
 
124
115
  // Client-only element registration (SSR-safe). The element module is imported
@@ -144,10 +135,15 @@ watchEffect(
144
135
  const el = elRef.value;
145
136
  if (!el) return;
146
137
  el.threads = drawerThreads.value;
147
- el.loading = threadsApi.isLoading.value || licensePending.value;
138
+ const elementProps = ɵgetThreadsDrawerElementProps({
139
+ loading: threadsApi.isLoading.value,
140
+ licensed: licensed.value,
141
+ licensePending: licensePending.value,
142
+ });
143
+ el.loading = elementProps.loading;
148
144
  el.error = threadsApi.listError.value?.message ?? null;
149
145
  el.activeThreadId = activeThreadId.value;
150
- el.licensed = licensed.value || licensePending.value;
146
+ el.licensed = elementProps.licensed;
151
147
  el.hasMore = threadsApi.hasMoreThreads.value;
152
148
  el.fetchingMore = threadsApi.isFetchingMoreThreads.value;
153
149
  // Dedicated fetch-more error channel: drives the element's inline
@@ -177,101 +173,40 @@ const unregisterDrawer = config.value?.registerDrawer?.();
177
173
  if (unregisterDrawer) onScopeDispose(unregisterDrawer);
178
174
 
179
175
  // --- Outbound event handlers ------------------------------------------------
180
- /** The chat input textarea's documented `data-testid`. */
181
- const CHAT_INPUT_TESTID = "copilot-chat-input-textarea";
182
- /** The chat view container's documented `data-testid`. */
183
- const CHAT_CONTAINER_TESTID = "copilot-chat-view";
184
-
185
- /**
186
- * Returns the chat input element for focus-return after a thread is selected.
187
- *
188
- * Best-effort and SCOPED: walks up from the drawer's custom element looking
189
- * for an ancestor that contains a chat-view container
190
- * (`data-testid="copilot-chat-view"`), then returns the chat input within
191
- * that subtree. This avoids focusing the wrong composer on a page hosting
192
- * more than one chat (multi-chat dashboards), where a document-global lookup
193
- * would grab whichever input appears first in DOM order rather than the one
194
- * this drawer drives.
195
- *
196
- * Falls back to a document-global lookup when no scoping ancestor is found
197
- * (e.g. the drawer and chat share no common container, or headless usage).
198
- */
199
- function findChatInput(origin: Element | null): HTMLElement | null {
200
- if (typeof document === "undefined") return null;
201
- const container = origin?.closest?.(
202
- `[data-testid="${CHAT_CONTAINER_TESTID}"]`,
203
- );
204
- if (container) {
205
- const scoped = container.querySelector<HTMLElement>(
206
- `[data-testid="${CHAT_INPUT_TESTID}"]`,
207
- );
208
- if (scoped) return scoped;
209
- }
210
- return document.querySelector<HTMLElement>(
211
- `[data-testid="${CHAT_INPUT_TESTID}"]`,
212
- );
213
- }
214
-
215
- function focusChatInput() {
216
- findChatInput(elRef.value)?.focus();
217
- }
176
+ // Core owns the drawer actions; they read the latest values at call time.
177
+ const actions = ɵcreateThreadsDrawerActions({
178
+ getThreads: () => threadsApi,
179
+ getConfig: () => config.value,
180
+ getCallbacks: () => ({
181
+ onThreadSelect: props.onThreadSelect,
182
+ onNewThread: props.onNewThread,
183
+ }),
184
+ getElement: () => elRef.value,
185
+ });
218
186
 
219
187
  function onThreadSelected(event: Event) {
220
188
  const { threadId } = (event as CustomEvent<ThreadSelectedDetail>).detail;
221
- if (props.onThreadSelect) props.onThreadSelect(threadId);
222
- else config.value?.setActiveThreadId?.(threadId, { explicit: true });
223
- focusChatInput();
224
- }
225
- function handleNewThread() {
226
- threadsApi.startNewThread();
227
- if (props.onNewThread) props.onNewThread();
228
- else config.value?.startNewThread?.();
189
+ actions.selectThread(threadId);
229
190
  }
191
+ const handleNewThread = actions.newThread;
230
192
  function onArchive(event: Event) {
231
- const { threadId } = (event as CustomEvent<ArchiveDetail>).detail;
232
- void threadsApi
233
- .archiveThread(threadId)
234
- .catch((e) =>
235
- console.error("CopilotThreadsDrawer: archiveThread failed", e),
236
- );
193
+ actions.archive((event as CustomEvent<ArchiveDetail>).detail.threadId);
237
194
  }
238
195
  function onUnarchive(event: Event) {
239
- const { threadId } = (event as CustomEvent<UnarchiveDetail>).detail;
240
- void threadsApi
241
- .unarchiveThread(threadId)
242
- .catch((e) =>
243
- console.error("CopilotThreadsDrawer: unarchiveThread failed", e),
244
- );
196
+ actions.unarchive((event as CustomEvent<UnarchiveDetail>).detail.threadId);
245
197
  }
246
198
  function onDelete(event: Event) {
247
- const { threadId } = (event as CustomEvent<DeleteDetail>).detail;
248
- const wasActive = threadId === activeThreadId.value;
249
- void threadsApi
250
- .deleteThread(threadId)
251
- .then(() => {
252
- if (wasActive) {
253
- handleNewThread();
254
- }
255
- })
256
- .catch((e) =>
257
- console.error("CopilotThreadsDrawer: deleteThread failed", e),
258
- );
259
- }
260
- function onFilterChange() {
261
- threadsApi.refetchThreads();
199
+ actions.remove((event as CustomEvent<DeleteDetail>).detail.threadId);
262
200
  }
201
+ const onFilterChange = actions.refetch;
263
202
  function onCollapseChange(event: Event) {
264
203
  const { collapsed } = (event as CustomEvent<CollapseChangeDetail>).detail;
265
204
  emit("collapse-change", collapsed);
266
205
  }
267
206
  function onRetry(event: Event) {
268
- const { scope } = (event as CustomEvent<RetryDetail>).detail;
269
- if (scope === "fetch-more") threadsApi.fetchMoreThreads();
270
- else threadsApi.refetchThreads();
271
- }
272
- function onLoadMore() {
273
- threadsApi.fetchMoreThreads();
207
+ actions.retry((event as CustomEvent<RetryDetail>).detail.scope);
274
208
  }
209
+ const onLoadMore = actions.loadMore;
275
210
  function onOpenChange(event: Event) {
276
211
  const { open } = (event as CustomEvent<OpenChangeDetail>).detail;
277
212
  setDrawerOpen(open);
@@ -1,8 +1,10 @@
1
1
  <script setup lang="ts">
2
- import { computed, h, watchEffect } from "vue";
2
+ import { computed, watchEffect } from "vue";
3
3
  import { StreamingMarkdownRenderer } from "@copilotkit/markdown/vue";
4
- import type { VueStreamingMarkdownNodeRenderer } from "@copilotkit/markdown/vue";
5
- import { warnUnsupportedRichSyntaxOnce } from "@copilotkit/markdown";
4
+ import {
5
+ CHAT_MARKDOWN_NODE_CLASS_NAMES,
6
+ warnUnsupportedRichSyntaxOnce,
7
+ } from "@copilotkit/markdown";
6
8
  import type { StreamingMarkdownParserOptions } from "@copilotkit/markdown";
7
9
  import type { DefaultMarkdownRendererProps } from "../../providers/markdown-renderer";
8
10
 
@@ -10,34 +12,15 @@ const props = withDefaults(
10
12
  defineProps<
11
13
  { content: string; isStreaming?: boolean } & DefaultMarkdownRendererProps
12
14
  >(),
13
- { isStreaming: false },
15
+ { isStreaming: false, caret: undefined },
14
16
  );
15
17
 
18
+ // vue-tsc reads the renderer's `options` prop as the full options type; the
19
+ // renderer itself accepts a partial object.
16
20
  const resolvedOptions = computed(
17
21
  () => props.options as StreamingMarkdownParserOptions | undefined,
18
22
  );
19
23
 
20
- const defaultCodeBlockRenderer: VueStreamingMarkdownNodeRenderer = (
21
- node,
22
- defaultVNode,
23
- ) => {
24
- if (node.type !== "code-block") return defaultVNode;
25
- return h(
26
- "pre",
27
- {
28
- "data-streaming-markdown-node": "code-block",
29
- "data-node-open": String(!node.closed),
30
- class: "cpk-markdown-code-block",
31
- },
32
- [h("code", { "data-code-info": node.info ?? undefined }, node.text)],
33
- );
34
- };
35
-
36
- const mergedNodeRenderers = computed(() => ({
37
- codeBlock: defaultCodeBlockRenderer,
38
- ...props.nodeRenderers,
39
- }));
40
-
41
24
  // Dev-only: nudge upgraders from the bundled Streamdown default when their
42
25
  // content needs math/syntax highlighting the built-in renderer doesn't do.
43
26
  watchEffect(() => warnUnsupportedRichSyntaxOnce(props.content));
@@ -45,11 +28,15 @@ watchEffect(() => warnUnsupportedRichSyntaxOnce(props.content));
45
28
 
46
29
  <template>
47
30
  <StreamingMarkdownRenderer
31
+ v-if="props.content"
48
32
  :content="props.content"
49
33
  :is-complete="!props.isStreaming"
50
- :node-renderers="mergedNodeRenderers"
34
+ :node-renderers="props.nodeRenderers"
35
+ :node-class-names="CHAT_MARKDOWN_NODE_CLASS_NAMES"
51
36
  :caret="props.caret ?? props.isStreaming"
52
37
  :options="resolvedOptions"
53
38
  :class="props.class"
39
+ :on-link-click="props.onLinkClick"
40
+ :on-citation-click="props.onCitationClick"
54
41
  />
55
42
  </template>
@@ -60,6 +60,11 @@ describe("CopilotChat", () => {
60
60
  }) => void | Promise<void>,
61
61
  errorMessage: string,
62
62
  ) => Promise<void>;
63
+ emitError: (params: {
64
+ error: Error;
65
+ code: CopilotKitCoreErrorCode;
66
+ context?: Record<string, unknown>;
67
+ }) => Promise<void>;
63
68
  }
64
69
 
65
70
  beforeEach(() => {
@@ -405,16 +410,13 @@ describe("CopilotChat", () => {
405
410
  },
406
411
  });
407
412
 
408
- await (core as unknown as CopilotKitCoreTestAccess).notifySubscribers(
409
- (subscriber) =>
410
- subscriber.onError?.({
411
- copilotkit: core!,
412
- error: new Error("matching agent"),
413
- code: CopilotKitCoreErrorCode.AGENT_RUN_FAILED,
414
- context: { agentId: "default" },
415
- }),
416
- "test chat onError",
417
- );
413
+ // Through core's error path: the provider's onError is core's handler,
414
+ // the chat's is a subscriber.
415
+ await (core as unknown as CopilotKitCoreTestAccess).emitError({
416
+ error: new Error("matching agent"),
417
+ code: CopilotKitCoreErrorCode.AGENT_RUN_FAILED,
418
+ context: { agentId: "default" },
419
+ });
418
420
 
419
421
  await nextTick();
420
422
 
@@ -186,6 +186,34 @@ describe("CopilotChatAttachmentQueue", () => {
186
186
  ).toContain("hello attachment preview");
187
187
  });
188
188
 
189
+ it("decodes a text document as UTF-8", async () => {
190
+ const { getByTestId } = render(CopilotChatAttachmentQueue, {
191
+ props: {
192
+ attachments: [
193
+ createAttachment({
194
+ id: "doc-utf8",
195
+ type: "document",
196
+ status: "ready",
197
+ // "héllo ✓" in UTF-8, base64-encoded.
198
+ source: {
199
+ type: "data",
200
+ value: "aMOpbGxvIOKckw==",
201
+ mimeType: "text/plain",
202
+ },
203
+ }),
204
+ ],
205
+ },
206
+ });
207
+
208
+ await fireEvent.click(
209
+ getByTestId("copilot-chat-attachment-document-button"),
210
+ );
211
+ expect(
212
+ screen.getByTestId("copilot-chat-attachment-lightbox-document-text")
213
+ .textContent,
214
+ ).toContain("héllo ✓");
215
+ });
216
+
189
217
  it("non-previewable document remains inert", async () => {
190
218
  const { getByTestId, queryByTestId } = render(CopilotChatAttachmentQueue, {
191
219
  props: {
@@ -128,9 +128,14 @@ beforeEach(() => {
128
128
  mockOnSubmitMessage.mockClear();
129
129
  vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(
130
130
  () =>
131
+ // A partial 2D context: the input measures text, and the recorder's
132
+ // waveform scales, clears, and fills.
131
133
  ({
132
134
  measureText: (text: string) => ({ width: text.length * 8 }),
133
135
  font: "",
136
+ scale: () => {},
137
+ clearRect: () => {},
138
+ fillRect: () => {},
134
139
  }) as unknown as CanvasRenderingContext2D,
135
140
  );
136
141
  });
@@ -397,6 +402,32 @@ describe("CopilotChatInput", () => {
397
402
  });
398
403
  });
399
404
 
405
+ it("does not empty the textarea to re-measure it on each keystroke on a phone", async () => {
406
+ vi.stubGlobal("matchMedia", (query: string) => ({
407
+ matches: query === "(max-width: 767px)",
408
+ }));
409
+ const valueSetter = vi.spyOn(HTMLTextAreaElement.prototype, "value", "set");
410
+ try {
411
+ renderWithProvider({
412
+ listeners: { onSubmitMessage: mockOnSubmitMessage },
413
+ });
414
+ const textarea = screen.getByRole("textbox");
415
+ await fireEvent.input(textarea, { target: { value: "ABCD" } });
416
+ const grid = textarea.closest("[data-layout]");
417
+ await waitFor(() => {
418
+ expect(grid?.getAttribute("data-layout")).toBe("expanded");
419
+ });
420
+
421
+ valueSetter.mockClear();
422
+ await fireEvent.input(textarea, { target: { value: "AEBCD" } });
423
+
424
+ expect(valueSetter).not.toHaveBeenCalledWith("");
425
+ } finally {
426
+ valueSetter.mockRestore();
427
+ vi.unstubAllGlobals();
428
+ }
429
+ });
430
+
400
431
  it("executes slash commands via keyboard selection", async () => {
401
432
  const handleFirst = vi.fn();
402
433
  const handleSecond = vi.fn();
@@ -477,6 +508,31 @@ describe("CopilotChatInput", () => {
477
508
  });
478
509
  });
479
510
 
511
+ it("opens the slash menu for typed text once commands arrive", async () => {
512
+ const toolsMenu = ref<{ label: string; action: () => void }[]>([]);
513
+ const Host = defineComponent({
514
+ setup: () => () =>
515
+ h(
516
+ CopilotChatConfigurationProvider,
517
+ { threadId: TEST_THREAD_ID },
518
+ {
519
+ default: () => h(CopilotChatInput, { toolsMenu: toolsMenu.value }),
520
+ },
521
+ ),
522
+ });
523
+ render(Host);
524
+
525
+ await fireEvent.input(screen.getByRole("textbox"), {
526
+ target: { value: "/sum" },
527
+ });
528
+ expect(screen.queryByTestId("copilot-slash-menu")).toBeNull();
529
+
530
+ toolsMenu.value = [{ label: "Summarize", action: vi.fn() }];
531
+
532
+ const menu = await screen.findByTestId("copilot-slash-menu");
533
+ expect(within(menu).getByRole("option").textContent).toContain("Summarize");
534
+ });
535
+
480
536
  it("limits slash menu height when commands exceed five items", async () => {
481
537
  const tools = Array.from({ length: 6 }, (_, index) => ({
482
538
  label: `Command ${index + 1}`,