@copilotkit/vue 2.0.0-rc.1 → 2.0.0-rc.2

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 (239) hide show
  1. package/PARITY.md +13 -12
  2. package/README.md +1 -2
  3. package/dist/components/copilot-provider/CopilotKit.vue.d.ts +3 -1
  4. package/dist/components/copilot-provider/CopilotKit.vue.d.ts.map +1 -1
  5. package/dist/index.cjs +43 -46
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.mjs +10170 -7506
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/v2/components/A2UIMessageRenderer.d.ts +7 -0
  10. package/dist/v2/components/A2UIMessageRenderer.d.ts.map +1 -1
  11. package/dist/v2/components/A2UISurfaceActivityRenderer.vue.d.ts.map +1 -1
  12. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +11 -2
  13. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  14. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts +136 -0
  15. package/dist/v2/components/a2ui/A2UIRecoveryStates.d.ts.map +1 -0
  16. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChat.vue.d.ts +1 -0
  18. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  19. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +3 -1
  20. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  21. package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts +16 -0
  22. package/dist/v2/components/chat/CopilotChatAssistantToolbarButton.vue.d.ts.map +1 -0
  23. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  24. package/dist/v2/components/chat/CopilotChatAttachmentRenderer.vue.d.ts.map +1 -1
  25. package/dist/v2/components/chat/CopilotChatAudioRecorder.vue.d.ts.map +1 -1
  26. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts +22 -0
  27. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts.map +1 -0
  28. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +69 -16
  29. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  30. package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts +12 -0
  31. package/dist/v2/components/chat/CopilotChatInspectorButton.vue.d.ts.map +1 -0
  32. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts +22 -0
  33. package/dist/v2/components/chat/CopilotChatLightbox.vue.d.ts.map +1 -0
  34. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts +20 -0
  35. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  36. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  37. package/dist/v2/components/chat/CopilotChatSuggestionPill.vue.d.ts.map +1 -1
  38. package/dist/v2/components/chat/CopilotChatSuggestionView.vue.d.ts.map +1 -1
  39. package/dist/v2/components/chat/CopilotChatToggleButton.vue.d.ts.map +1 -1
  40. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts +32 -0
  41. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -0
  42. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  43. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts +11 -1
  44. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  45. package/dist/v2/components/chat/CopilotChatUserMessage.vue.d.ts.map +1 -1
  46. package/dist/v2/components/chat/CopilotChatView.vue.d.ts +1 -0
  47. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  48. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts +5 -0
  49. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts.map +1 -0
  50. package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
  51. package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts.map +1 -1
  52. package/dist/v2/components/chat/CopilotModalHeaderTitle.d.ts.map +1 -1
  53. package/dist/v2/components/chat/CopilotPopup.vue.d.ts +1 -0
  54. package/dist/v2/components/chat/CopilotPopup.vue.d.ts.map +1 -1
  55. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts +1 -0
  56. package/dist/v2/components/chat/CopilotPopupView.vue.d.ts.map +1 -1
  57. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts +1 -0
  58. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  59. package/dist/v2/components/chat/CopilotPopupWelcomeScreen.vue.d.ts.map +1 -1
  60. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts +2 -0
  61. package/dist/v2/components/chat/CopilotSidebar.vue.d.ts.map +1 -1
  62. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts +2 -0
  63. package/dist/v2/components/chat/CopilotSidebarView.vue.d.ts.map +1 -1
  64. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts +2 -0
  65. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  66. package/dist/v2/components/chat/CopilotSidebarWelcomeScreen.vue.d.ts.map +1 -1
  67. package/dist/v2/components/chat/StreamingMarkdownDefault.vue.d.ts.map +1 -1
  68. package/dist/v2/components/chat/index.d.ts +17 -0
  69. package/dist/v2/components/chat/index.d.ts.map +1 -1
  70. package/dist/v2/components/chat/inspector-shortcuts.d.ts +12 -0
  71. package/dist/v2/components/chat/inspector-shortcuts.d.ts.map +1 -0
  72. package/dist/v2/components/chat/message-feedback.d.ts +89 -0
  73. package/dist/v2/components/chat/message-feedback.d.ts.map +1 -0
  74. package/dist/v2/components/chat/tool-call-capture.d.ts +39 -0
  75. package/dist/v2/components/chat/tool-call-capture.d.ts.map +1 -0
  76. package/dist/v2/components/chat/tool-call-display-context.d.ts +39 -0
  77. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -0
  78. package/dist/v2/components/chat/tool-call-group-parts.d.ts +144 -0
  79. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -0
  80. package/dist/v2/components/chat/tool-call-layout.d.ts +115 -0
  81. package/dist/v2/components/chat/tool-call-layout.d.ts.map +1 -0
  82. package/dist/v2/components/chat/turn-work.d.ts +35 -0
  83. package/dist/v2/components/chat/turn-work.d.ts.map +1 -0
  84. package/dist/v2/components/chat/types.d.ts +104 -4
  85. package/dist/v2/components/chat/types.d.ts.map +1 -1
  86. package/dist/v2/components/chat/use-lightbox.d.ts +18 -0
  87. package/dist/v2/components/chat/use-lightbox.d.ts.map +1 -0
  88. package/dist/v2/components/icons/index.d.ts +1 -1
  89. package/dist/v2/components/icons/index.d.ts.map +1 -1
  90. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts +55 -0
  91. package/dist/v2/components/ui/CopilotDropdownMenuContent.vue.d.ts.map +1 -0
  92. package/dist/v2/components/ui/CopilotPopover.vue.d.ts +40 -0
  93. package/dist/v2/components/ui/CopilotPopover.vue.d.ts.map +1 -0
  94. package/dist/v2/components/ui/CopilotTooltip.vue.d.ts +37 -0
  95. package/dist/v2/components/ui/CopilotTooltip.vue.d.ts.map +1 -0
  96. package/dist/v2/components/ui/dropdown-menu.d.ts +14 -0
  97. package/dist/v2/components/ui/dropdown-menu.d.ts.map +1 -0
  98. package/dist/v2/components/ui/floating.d.ts +84 -0
  99. package/dist/v2/components/ui/floating.d.ts.map +1 -0
  100. package/dist/v2/components/ui/presence.d.ts +9 -0
  101. package/dist/v2/components/ui/presence.d.ts.map +1 -0
  102. package/dist/v2/components/ui/use-id.d.ts +9 -0
  103. package/dist/v2/components/ui/use-id.d.ts.map +1 -0
  104. package/dist/v2/hooks/use-default-render-tool.d.ts +6 -0
  105. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  106. package/dist/v2/hooks/use-render-tool.d.ts +4 -0
  107. package/dist/v2/hooks/use-render-tool.d.ts.map +1 -1
  108. package/dist/v2/index.cjs +1 -1
  109. package/dist/v2/index.d.cts +3 -0
  110. package/dist/v2/index.d.mts +3 -0
  111. package/dist/v2/index.d.ts +3 -0
  112. package/dist/v2/index.d.ts.map +1 -1
  113. package/dist/v2/index.mjs +165 -162
  114. package/dist/v2/lib/message-feedback.d.ts +33 -0
  115. package/dist/v2/lib/message-feedback.d.ts.map +1 -0
  116. package/dist/v2/providers/CopilotChatConfigurationProvider.vue.d.ts.map +1 -1
  117. package/dist/v2/providers/CopilotKitProvider.types.d.ts +56 -1
  118. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  119. package/dist/v2/providers/CopilotKitProvider.vue.d.ts +5 -1
  120. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  121. package/dist/v2/providers/index.d.ts +1 -1
  122. package/dist/v2/providers/index.d.ts.map +1 -1
  123. package/dist/v2/providers/keys.d.ts +8 -0
  124. package/dist/v2/providers/keys.d.ts.map +1 -1
  125. package/dist/v2/providers/types.d.ts +9 -0
  126. package/dist/v2/providers/types.d.ts.map +1 -1
  127. package/dist/v2/styles/CopilotKitStyles.d.ts +18 -0
  128. package/dist/v2/styles/CopilotKitStyles.d.ts.map +1 -0
  129. package/dist/v2/types/defineToolCallRenderer.d.ts +2 -0
  130. package/dist/v2/types/defineToolCallRenderer.d.ts.map +1 -1
  131. package/dist/v2/types/vue-tool-call-renderer.d.ts +7 -0
  132. package/dist/v2/types/vue-tool-call-renderer.d.ts.map +1 -1
  133. package/package.json +11 -23
  134. package/src/components/copilot-provider/CopilotKit.vue +4 -1
  135. package/src/v2/__tests__/exports.test.ts +4 -2
  136. package/src/v2/components/A2UIMessageRenderer.ts +55 -67
  137. package/src/v2/components/A2UISurfaceActivityRenderer.vue +11 -23
  138. package/src/v2/components/__tests__/A2UIBasicCatalogColors.test.ts +8 -9
  139. package/src/v2/components/__tests__/A2UIMessageRenderer.test.ts +63 -36
  140. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +14 -257
  141. package/src/v2/components/a2ui/A2UIRecoveryStates.ts +444 -0
  142. package/src/v2/components/a2ui/__tests__/A2UIRecoveryStates.test.ts +175 -0
  143. package/src/v2/components/chat/BasicMarkdown.vue +8 -6
  144. package/src/v2/components/chat/CopilotChat.vue +7 -0
  145. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +267 -67
  146. package/src/v2/components/chat/CopilotChatAssistantToolbarButton.vue +46 -0
  147. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +141 -168
  148. package/src/v2/components/chat/CopilotChatAttachmentRenderer.vue +67 -18
  149. package/src/v2/components/chat/CopilotChatAudioRecorder.vue +2 -2
  150. package/src/v2/components/chat/CopilotChatFeedbackCommentPopover.vue +240 -0
  151. package/src/v2/components/chat/CopilotChatInput.vue +210 -161
  152. package/src/v2/components/chat/CopilotChatInspectorButton.vue +331 -0
  153. package/src/v2/components/chat/CopilotChatLightbox.vue +105 -0
  154. package/src/v2/components/chat/CopilotChatMessageView.vue +261 -18
  155. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +21 -22
  156. package/src/v2/components/chat/CopilotChatSuggestionPill.vue +5 -14
  157. package/src/v2/components/chat/CopilotChatSuggestionView.vue +1 -4
  158. package/src/v2/components/chat/CopilotChatToggleButton.vue +3 -9
  159. package/src/v2/components/chat/CopilotChatToggleButtonCloseIcon.ts +1 -1
  160. package/src/v2/components/chat/CopilotChatToggleButtonOpenIcon.ts +1 -1
  161. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +330 -0
  162. package/src/v2/components/chat/CopilotChatToolCallItem.vue +21 -11
  163. package/src/v2/components/chat/CopilotChatToolCallsView.vue +192 -21
  164. package/src/v2/components/chat/CopilotChatUserMessage.vue +105 -42
  165. package/src/v2/components/chat/CopilotChatView.vue +33 -38
  166. package/src/v2/components/chat/CopilotChatWorkSummary.vue +85 -0
  167. package/src/v2/components/chat/CopilotModalHeader.vue +10 -8
  168. package/src/v2/components/chat/CopilotModalHeaderCloseButton.ts +7 -6
  169. package/src/v2/components/chat/CopilotModalHeaderTitle.ts +1 -4
  170. package/src/v2/components/chat/CopilotPopup.vue +3 -0
  171. package/src/v2/components/chat/CopilotPopupView.vue +1 -0
  172. package/src/v2/components/chat/CopilotPopupViewInternal.vue +9 -19
  173. package/src/v2/components/chat/CopilotPopupWelcomeScreen.vue +5 -8
  174. package/src/v2/components/chat/CopilotSidebar.vue +5 -0
  175. package/src/v2/components/chat/CopilotSidebarView.vue +2 -0
  176. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +24 -17
  177. package/src/v2/components/chat/CopilotSidebarWelcomeScreen.vue +7 -10
  178. package/src/v2/components/chat/StreamingMarkdownDefault.vue +3 -5
  179. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.inspector.test.ts +269 -0
  180. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.learningFeedback.test.ts +760 -0
  181. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.ts +3 -5
  182. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.ts +2 -2
  183. package/src/v2/components/chat/__tests__/CopilotChatInput.addMenu.test.ts +328 -0
  184. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.ts +6 -3
  185. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +22 -13
  186. package/src/v2/components/chat/__tests__/CopilotChatLightbox.test.ts +160 -0
  187. package/src/v2/components/chat/__tests__/CopilotChatMessageView.toolCallDisplay.test.ts +735 -0
  188. package/src/v2/components/chat/__tests__/CopilotChatSuggestionPill.test.ts +1 -1
  189. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.ts +3 -1
  190. package/src/v2/components/chat/__tests__/CopilotChatToolCallsView.learning.test.ts +75 -0
  191. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.attachments.test.ts +143 -0
  192. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.ts +5 -4
  193. package/src/v2/components/chat/__tests__/CopilotChatView.rootWidth.test.ts +7 -33
  194. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.ts +1 -1
  195. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.ts +3 -3
  196. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.ts +126 -0
  197. package/src/v2/components/chat/__tests__/messageFeedbackDefaults.test.ts +33 -0
  198. package/src/v2/components/chat/__tests__/modalChatDisplayProps.test.ts +44 -0
  199. package/src/v2/components/chat/index.ts +2 -0
  200. package/src/v2/components/chat/inspector-shortcuts.ts +34 -0
  201. package/src/v2/components/chat/message-feedback.ts +291 -0
  202. package/src/v2/components/chat/tool-call-capture.ts +55 -0
  203. package/src/v2/components/chat/tool-call-display-context.ts +49 -0
  204. package/src/v2/components/chat/tool-call-group-parts.ts +292 -0
  205. package/src/v2/components/chat/tool-call-layout.ts +304 -0
  206. package/src/v2/components/chat/turn-work.ts +61 -0
  207. package/src/v2/components/chat/types.ts +113 -3
  208. package/src/v2/components/chat/use-lightbox.ts +64 -0
  209. package/src/v2/components/icons/index.ts +4 -0
  210. package/src/v2/components/ui/CopilotDropdownMenuContent.vue +465 -0
  211. package/src/v2/components/ui/CopilotPopover.vue +138 -0
  212. package/src/v2/components/ui/CopilotTooltip.vue +237 -0
  213. package/src/v2/components/ui/__tests__/CopilotTooltip.test.ts +168 -0
  214. package/src/v2/components/ui/dropdown-menu.ts +15 -0
  215. package/src/v2/components/ui/floating.ts +321 -0
  216. package/src/v2/components/ui/presence.ts +54 -0
  217. package/src/v2/components/ui/use-id.ts +16 -0
  218. package/src/v2/hooks/use-default-render-tool.ts +123 -62
  219. package/src/v2/hooks/use-render-tool.ts +8 -0
  220. package/src/v2/index.ts +3 -0
  221. package/src/v2/lib/__tests__/message-feedback.test.ts +152 -0
  222. package/src/v2/lib/message-feedback.ts +120 -0
  223. package/src/v2/providers/CopilotChatConfigurationProvider.vue +29 -2
  224. package/src/v2/providers/CopilotKitProvider.types.ts +57 -0
  225. package/src/v2/providers/CopilotKitProvider.vue +125 -3
  226. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.openThread.test.ts +69 -0
  227. package/src/v2/providers/__tests__/CopilotKitProvider.learning.test.ts +534 -0
  228. package/src/v2/providers/index.ts +4 -1
  229. package/src/v2/providers/keys.ts +12 -0
  230. package/src/v2/providers/types.ts +11 -0
  231. package/src/v2/styles/CopilotKitStyles.ts +95 -0
  232. package/src/v2/styles/__tests__/CopilotKitStyles.client.test.ts +149 -0
  233. package/src/v2/styles/__tests__/CopilotKitStyles.ssr.test.ts +71 -0
  234. package/src/v2/types/defineToolCallRenderer.ts +4 -0
  235. package/src/v2/types/vue-tool-call-renderer.ts +7 -0
  236. package/vite.config.ts +2 -1
  237. package/dist/styles.css +0 -2
  238. package/scripts/scope-preflight.mjs +0 -100
  239. package/src/styles/globals.css +0 -233
@@ -0,0 +1,735 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { defineComponent, h, nextTick, ref } from "vue";
3
+ import type { Ref } from "vue";
4
+ import { fireEvent, render, screen, within } from "@testing-library/vue";
5
+ import { z } from "zod";
6
+ import type { AssistantMessage, Message, ToolMessage } from "@ag-ui/core";
7
+ import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
8
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
9
+ import { defineToolCallRenderer } from "../../../types";
10
+ import type { VueToolCallRenderer } from "../../../types";
11
+ import { useDefaultRenderTool } from "../../../hooks/use-default-render-tool";
12
+ import CopilotChatMessageView from "../CopilotChatMessageView.vue";
13
+ import CopilotChatToolCallsView from "../CopilotChatToolCallsView.vue";
14
+ import {
15
+ getToolCallLayouts,
16
+ getToolCallRendererNames,
17
+ getTurnWork,
18
+ } from "../tool-call-layout";
19
+ import type { CopilotChatToolCallDisplay } from "../tool-call-layout";
20
+
21
+ // Ported from React's CopilotChatMessageView.toolCallDisplay tests.
22
+
23
+ const question: Message = { id: "u1", role: "user", content: "Why?" };
24
+
25
+ const callMessage = (id: string, name = "run_command"): AssistantMessage => ({
26
+ id: `m-${id}`,
27
+ role: "assistant",
28
+ content: "",
29
+ toolCalls: [
30
+ {
31
+ id,
32
+ type: "function",
33
+ function: { name, arguments: JSON.stringify({ command: `cmd ${id}` }) },
34
+ },
35
+ ],
36
+ });
37
+
38
+ const result = (id: string, error?: string): ToolMessage => ({
39
+ id: `r-${id}`,
40
+ role: "tool",
41
+ toolCallId: id,
42
+ content: "ok",
43
+ ...(error ? { error } : {}),
44
+ });
45
+
46
+ const say = (id: string, content: string): Message => ({
47
+ id,
48
+ role: "assistant",
49
+ content,
50
+ });
51
+
52
+ const weather = defineToolCallRenderer({
53
+ name: "weather",
54
+ args: z.object({}),
55
+ render: () => h("div", { "data-testid": "weather-card" }, "Weather"),
56
+ });
57
+
58
+ const names = {
59
+ named: new Set(["weather"]),
60
+ groupedNames: new Set<string>(),
61
+ wildcard: true,
62
+ wildcardGroups: true,
63
+ };
64
+
65
+ interface ViewState {
66
+ messages: Ref<Message[]>;
67
+ isRunning: Ref<boolean>;
68
+ toolCallDisplay: Ref<CopilotChatToolCallDisplay | undefined>;
69
+ }
70
+
71
+ /**
72
+ * Renders a message view inside the providers. Without `renderers`, the
73
+ * built-in card (`useDefaultRenderTool()`) draws every call but `weather`.
74
+ */
75
+ function renderView(
76
+ messages: Message[],
77
+ props: {
78
+ isRunning?: boolean;
79
+ toolCallDisplay?: CopilotChatToolCallDisplay;
80
+ } = {},
81
+ renderers?: VueToolCallRenderer<unknown>[],
82
+ slots: Record<string, (props: any) => unknown> = {},
83
+ ) {
84
+ const state: ViewState = {
85
+ messages: ref(messages),
86
+ isRunning: ref(props.isRunning ?? false),
87
+ toolCallDisplay: ref(props.toolCallDisplay),
88
+ };
89
+ const Harness = defineComponent({
90
+ setup() {
91
+ if (!renderers) useDefaultRenderTool();
92
+ return () =>
93
+ h(
94
+ CopilotChatMessageView,
95
+ {
96
+ messages: state.messages.value,
97
+ isRunning: state.isRunning.value,
98
+ toolCallDisplay: state.toolCallDisplay.value,
99
+ },
100
+ slots,
101
+ );
102
+ },
103
+ });
104
+ const rendered = render(CopilotKitProvider, {
105
+ props: { renderToolCalls: renderers ?? [weather] },
106
+ slots: {
107
+ default: () =>
108
+ h(
109
+ CopilotChatConfigurationProvider,
110
+ { threadId: "tool-call-display" },
111
+ { default: () => h(Harness) },
112
+ ),
113
+ },
114
+ });
115
+ return { ...rendered, state };
116
+ }
117
+
118
+ const firstButton = (element: HTMLElement) =>
119
+ within(element).getAllByRole("button")[0]!;
120
+
121
+ describe("getToolCallLayouts", () => {
122
+ it("groups calls made in a row across messages, owned by the first", () => {
123
+ const messages = [
124
+ question,
125
+ callMessage("a"),
126
+ result("a"),
127
+ callMessage("b"),
128
+ result("b"),
129
+ ];
130
+ const layouts = getToolCallLayouts(messages, messages, names);
131
+
132
+ const first = layouts.get("m-a")!.segments;
133
+ expect(first).toHaveLength(1);
134
+ expect(first[0]).toMatchObject({ kind: "group", trailing: true });
135
+ expect(
136
+ first[0]!.kind === "group" && first[0]!.calls.map((c) => c.toolCall.id),
137
+ ).toEqual(["a", "b"]);
138
+ expect(layouts.get("m-b")!.segments).toEqual([]);
139
+ });
140
+
141
+ it("starts a new group after text or a call with its own renderer", () => {
142
+ const messages = [
143
+ question,
144
+ callMessage("a"),
145
+ say("t", "Checking more."),
146
+ callMessage("b"),
147
+ callMessage("w", "weather"),
148
+ callMessage("c"),
149
+ ];
150
+ const layouts = getToolCallLayouts(messages, messages, names);
151
+ const kinds = ["m-a", "m-b", "m-w", "m-c"].map((id) =>
152
+ layouts.get(id)!.segments.map((segment) => segment.kind),
153
+ );
154
+ expect(kinds).toEqual([["group"], ["group"], ["call"], ["group"]]);
155
+ });
156
+
157
+ it("changes its signature when a result arrives for a call it holds", () => {
158
+ const before = [question, callMessage("a"), callMessage("b")];
159
+ const after = [...before, result("b")];
160
+ const signature = (messages: Message[]) =>
161
+ getToolCallLayouts(messages, messages, names).get("m-a")!.signature;
162
+ expect(signature(after)).not.toBe(signature(before));
163
+ });
164
+ });
165
+
166
+ describe("getTurnWork", () => {
167
+ it("treats everything up to the last call as work, and the rest as the reply", () => {
168
+ const messages = [
169
+ question,
170
+ say("t1", "Looking."),
171
+ callMessage("a"),
172
+ result("a"),
173
+ say("t2", "The answer."),
174
+ ];
175
+ const layouts = getToolCallLayouts(messages, messages, names);
176
+ const turn = getTurnWork(messages, layouts).get("u1")!;
177
+ expect([...turn.workMessageIds]).toEqual(["t1", "m-a"]);
178
+ expect(turn.firstMessageId).toBe("t1");
179
+ expect(turn.calls).toHaveLength(1);
180
+ });
181
+ });
182
+
183
+ describe("getToolCallRendererNames", () => {
184
+ const renderers = [
185
+ { name: "*", group: true },
186
+ { name: "search", group: true },
187
+ ];
188
+
189
+ it("reads the renderers' group opt-in", () => {
190
+ const result = getToolCallRendererNames(renderers);
191
+ expect([...result.groupedNames]).toEqual(["search"]);
192
+ expect(result.wildcardGroups).toBe(true);
193
+ });
194
+
195
+ it("never groups calls a tool-call slot draws", () => {
196
+ const named = getToolCallRendererNames(renderers, ["tool-call-search"]);
197
+ expect(named.named.has("search")).toBe(true);
198
+ expect(named.groupedNames.has("search")).toBe(false);
199
+ expect(named.wildcardGroups).toBe(true);
200
+
201
+ const wildcard = getToolCallRendererNames(renderers, ["tool-call"]);
202
+ expect(wildcard.groupedNames.size).toBe(0);
203
+ expect(wildcard.wildcardGroups).toBe(false);
204
+ });
205
+ });
206
+
207
+ describe("CopilotChatMessageView toolCallDisplay", () => {
208
+ const burst = [
209
+ question,
210
+ callMessage("a"),
211
+ result("a"),
212
+ callMessage("b"),
213
+ result("b", "boom"),
214
+ say("t", "Done."),
215
+ ];
216
+
217
+ it("groups calls into one collapsed card by default", async () => {
218
+ renderView(burst);
219
+ const group = screen.getByTestId("copilot-tool-call-group");
220
+ expect(group.getAttribute("data-appearance")).toBe("card");
221
+ // The header comes first; the rows (closed, so inert) follow it.
222
+ const toggle = firstButton(group);
223
+ expect(toggle.getAttribute("aria-expanded")).toBe("false");
224
+ expect(toggle.getAttribute("aria-controls")).toBe(
225
+ group.querySelector(".cpk-collapsible")!.id,
226
+ );
227
+ expect(toggle.textContent).toContain("Used 2 tools");
228
+ expect(toggle.textContent).toContain("1 failed");
229
+
230
+ await fireEvent.click(toggle);
231
+ expect(toggle.getAttribute("aria-expanded")).toBe("true");
232
+ expect(within(group).getAllByTestId("copilot-tool-render")).toHaveLength(2);
233
+ });
234
+
235
+ it("names the running call while the group is still going", () => {
236
+ renderView([question, callMessage("a"), result("a"), callMessage("b")], {
237
+ isRunning: true,
238
+ });
239
+ const group = screen.getByTestId("copilot-tool-call-group");
240
+ expect(group.getAttribute("data-state")).toBe("running");
241
+ expect(group.textContent).toContain("Running run_command");
242
+ expect(group.textContent).toContain("1 done");
243
+ });
244
+
245
+ it("draws a lone call in the same card a group uses", async () => {
246
+ const { state } = renderView([question, callMessage("a")], {
247
+ isRunning: true,
248
+ });
249
+ const group = screen.getByTestId("copilot-tool-call-group");
250
+ const toggle = firstButton(group);
251
+ expect(toggle.getAttribute("aria-expanded")).toBe("false");
252
+ expect(toggle.textContent).toContain("Running run_command");
253
+ // Nothing has finished yet, so there's no count.
254
+ expect(toggle.textContent).not.toContain("done");
255
+
256
+ state.messages.value = [question, callMessage("a"), result("a")];
257
+ state.isRunning.value = false;
258
+ await nextTick();
259
+ expect(toggle.textContent).toContain("Used 1 tool");
260
+ expect(toggle.textContent).toContain("Done");
261
+ });
262
+
263
+ it("draws a lone call as the top card of a stack", () => {
264
+ renderView([question, callMessage("a"), result("a")], {
265
+ toolCallDisplay: "stacked",
266
+ });
267
+ const group = screen.getByTestId("copilot-tool-call-group");
268
+ const toggle = firstButton(group);
269
+ expect(toggle.textContent).toContain("run_command");
270
+ expect(toggle.textContent).toContain("1 call");
271
+ // The status icon is decorative; screen readers get the status as text.
272
+ expect(within(toggle).getByText("Done").classList).toContain(
273
+ "cpk-tool-call-group-stack-status",
274
+ );
275
+ });
276
+
277
+ it("announces a closed stack's status as it changes", async () => {
278
+ const { state } = renderView([question, callMessage("a")], {
279
+ toolCallDisplay: "stacked",
280
+ isRunning: true,
281
+ });
282
+ const group = screen.getByTestId("copilot-tool-call-group");
283
+ const live = group.querySelector('[aria-live="polite"]')!;
284
+ expect(live.textContent).toBe("run_command: Running");
285
+
286
+ state.messages.value = [question, callMessage("a"), result("a")];
287
+ state.isRunning.value = false;
288
+ await nextTick();
289
+ // Same region, new text: that's what gets announced.
290
+ expect(group.querySelector('[aria-live="polite"]')).toBe(live);
291
+ expect(live.textContent).toBe("run_command: Done");
292
+ });
293
+
294
+ it("opens a stack into the same compact cards, with Collapse", async () => {
295
+ renderView(burst, { toolCallDisplay: "stacked" });
296
+ const group = screen.getByTestId("copilot-tool-call-group");
297
+ await fireEvent.click(firstButton(group));
298
+ expect(within(group).getByText("Collapse")).toBeTruthy();
299
+ const statuses = within(group).getAllByTestId("copilot-tool-render-status");
300
+ expect(statuses).toHaveLength(2);
301
+ // Compact rows, not the standalone card with its status badge.
302
+ for (const status of statuses) {
303
+ expect(status.classList).toContain("cpk-tool-call-row-status");
304
+ }
305
+ });
306
+
307
+ it.each([
308
+ ["line", "line"],
309
+ ["stacked", "stack"],
310
+ ] as const)("draws %s groups", (display, appearance) => {
311
+ renderView(burst, { toolCallDisplay: display });
312
+ expect(
313
+ screen
314
+ .getByTestId("copilot-tool-call-group")
315
+ .getAttribute("data-appearance"),
316
+ ).toBe(appearance);
317
+ });
318
+
319
+ it("draws a finished lone call in line as its row, without a toggle", () => {
320
+ renderView([question, callMessage("a"), result("a")], {
321
+ toolCallDisplay: "line",
322
+ });
323
+ const group = screen.getByTestId("copilot-tool-call-group");
324
+ expect(group.classList).toContain("cpk-tool-call-group-single");
325
+ expect(
326
+ within(group).getByTestId("copilot-tool-render").classList,
327
+ ).toContain("cpk-tool-call-row-line");
328
+ });
329
+
330
+ it("gives every call its own card with separate", () => {
331
+ renderView(burst, { toolCallDisplay: "separate" });
332
+ expect(screen.queryByTestId("copilot-tool-call-group")).toBeNull();
333
+ const cards = screen.getAllByTestId("copilot-tool-render");
334
+ expect(cards).toHaveLength(2);
335
+ expect(cards[0]!.classList).toContain("cpk-default-tool-call");
336
+ });
337
+
338
+ it("keeps calls with their own renderer visible and outside the group", () => {
339
+ renderView([
340
+ question,
341
+ callMessage("a"),
342
+ callMessage("w", "weather"),
343
+ callMessage("b"),
344
+ ]);
345
+ expect(screen.getByTestId("weather-card")).toBeDefined();
346
+ expect(screen.getAllByTestId("copilot-tool-call-group")).toHaveLength(2);
347
+ });
348
+
349
+ it("keeps calls a tool-call slot draws on their own", () => {
350
+ renderView([question, callMessage("a"), callMessage("b")], {}, undefined, {
351
+ "tool-call-run_command": ({ name }: { name: string }) =>
352
+ h("div", { "data-testid": "slot-call" }, name),
353
+ });
354
+ expect(screen.queryByTestId("copilot-tool-call-group")).toBeNull();
355
+ expect(screen.getAllByTestId("slot-call")).toHaveLength(2);
356
+ });
357
+
358
+ it("lets the tool-calls-group slot replace a group", () => {
359
+ renderView(burst, {}, undefined, {
360
+ "tool-calls-group": ({
361
+ calls,
362
+ appearance,
363
+ renderCall,
364
+ }: {
365
+ calls: { toolCall: { id: string } }[];
366
+ appearance: string;
367
+ renderCall: (call: unknown) => unknown;
368
+ }) =>
369
+ h(
370
+ "section",
371
+ { "data-testid": "custom-group", "data-appearance": appearance },
372
+ [`${calls.length} calls`, ...calls.map((call) => renderCall(call))],
373
+ ),
374
+ });
375
+ expect(screen.queryByTestId("copilot-tool-call-group")).toBeNull();
376
+ const group = screen.getByTestId("custom-group");
377
+ expect(group.getAttribute("data-appearance")).toBe("card");
378
+ expect(group.textContent).toContain("2 calls");
379
+ // Calls drawn outside a group get the standalone card.
380
+ expect(within(group).getAllByTestId("copilot-tool-render")).toHaveLength(2);
381
+ });
382
+
383
+ it("folds a finished reply's work into the summary with turn", async () => {
384
+ renderView([question, say("t1", "Looking."), ...burst.slice(1)], {
385
+ toolCallDisplay: "turn",
386
+ });
387
+ const summary = screen.getByTestId("copilot-work-summary");
388
+ expect(summary.getAttribute("aria-expanded")).toBe("false");
389
+ // No timing for a reply this client didn't watch: it counts the tools.
390
+ expect(summary.textContent).toContain("Used 2 tools");
391
+ expect(summary.textContent).toContain("1 failed");
392
+ expect(
393
+ document.querySelector('[data-work-text="narration"]'),
394
+ ).not.toBeNull();
395
+ expect(document.querySelector('[data-work-text="reply"]')).not.toBeNull();
396
+ // The calls are rows of the work, not a group, and fold away with it.
397
+ expect(screen.queryByTestId("copilot-tool-call-group")).toBeNull();
398
+ const work = document.querySelector(".cpk-tool-calls-view-turn")!;
399
+ expect(work.hasAttribute("inert")).toBe(true);
400
+ expect(
401
+ within(work as HTMLElement).getAllByTestId("copilot-tool-render"),
402
+ ).toHaveLength(2);
403
+
404
+ await fireEvent.click(summary);
405
+ expect(summary.getAttribute("aria-expanded")).toBe("true");
406
+ expect(work.hasAttribute("inert")).toBe(false);
407
+ });
408
+
409
+ it("shows Working while the latest reply runs with turn", () => {
410
+ renderView([question], { toolCallDisplay: "turn", isRunning: true });
411
+ const summary = screen.getByTestId("copilot-work-summary");
412
+ expect(summary.getAttribute("data-state")).toBe("working");
413
+ expect(screen.queryByTestId("copilot-loading-cursor")).toBeNull();
414
+ });
415
+
416
+ it("shows the work in the open while the agent works on it with turn", async () => {
417
+ const { state } = renderView([question, say("t1", "Looking.")], {
418
+ toolCallDisplay: "turn",
419
+ isRunning: true,
420
+ });
421
+ state.messages.value = [...state.messages.value, callMessage("a")];
422
+ await nextTick();
423
+ const summary = screen.getByTestId("copilot-work-summary");
424
+ expect(summary.getAttribute("aria-expanded")).toBe("true");
425
+ expect(
426
+ document
427
+ .querySelector(".cpk-tool-calls-view-turn")!
428
+ .hasAttribute("inert"),
429
+ ).toBe(false);
430
+ expect(screen.getByTestId("copilot-tool-render").textContent).toContain(
431
+ "Running",
432
+ );
433
+
434
+ state.messages.value = [
435
+ ...state.messages.value,
436
+ result("a"),
437
+ say("t2", "Done."),
438
+ ];
439
+ state.isRunning.value = false;
440
+ await nextTick();
441
+ expect(summary.getAttribute("aria-expanded")).toBe("false");
442
+ expect(summary.getAttribute("data-state")).toBe("done");
443
+ });
444
+
445
+ it("lets the work-summary slot replace the summary line", async () => {
446
+ renderView(
447
+ [question, ...burst.slice(1)],
448
+ { toolCallDisplay: "turn" },
449
+ undefined,
450
+ {
451
+ "work-summary": (props: {
452
+ isOpen: boolean;
453
+ calls: unknown[];
454
+ onClick: () => void;
455
+ }) =>
456
+ h(
457
+ "button",
458
+ {
459
+ type: "button",
460
+ "data-testid": "custom-summary",
461
+ "aria-expanded": String(props.isOpen),
462
+ onClick: props.onClick,
463
+ },
464
+ `${props.calls.length} steps`,
465
+ ),
466
+ },
467
+ );
468
+ expect(screen.queryByTestId("copilot-work-summary")).toBeNull();
469
+ const summary = screen.getByTestId("custom-summary");
470
+ expect(summary.textContent).toBe("2 steps");
471
+
472
+ await fireEvent.click(summary);
473
+ expect(summary.getAttribute("aria-expanded")).toBe("true");
474
+ });
475
+
476
+ it("makes a closed group's rows inert", async () => {
477
+ renderView(burst);
478
+ const group = screen.getByTestId("copilot-tool-call-group");
479
+ const body = group.querySelector("[id]")!;
480
+ expect(body.hasAttribute("inert")).toBe(true);
481
+ await fireEvent.click(firstButton(group));
482
+ expect(body.hasAttribute("inert")).toBe(false);
483
+ });
484
+
485
+ it("keeps a reasoning message mounted when a later call makes it work", async () => {
486
+ const reasoning: Message = {
487
+ id: "r1",
488
+ role: "reasoning",
489
+ content: "Thinking about it.",
490
+ };
491
+ const { state } = renderView([question, reasoning], {
492
+ toolCallDisplay: "turn",
493
+ isRunning: true,
494
+ });
495
+ const reasoningNode = () =>
496
+ document.querySelector(".cpk-reasoning-message")!;
497
+ const node = reasoningNode();
498
+ expect(node).not.toBeNull();
499
+
500
+ state.messages.value = [question, reasoning, callMessage("a")];
501
+ await nextTick();
502
+ expect(reasoningNode()).toBe(node);
503
+ // It folds away with the work, which is open while the agent works.
504
+ expect(node.closest(".cpk-collapsible")!.hasAttribute("inert")).toBe(false);
505
+ });
506
+
507
+ it("leaves calls drawn by a custom catch-all on their own unless it opts in", () => {
508
+ const draw = ({ name }: { name: string }) =>
509
+ h("div", { "data-testid": "custom-call" }, name);
510
+ const quiet = defineToolCallRenderer({ name: "*", render: draw });
511
+ const messages = [question, callMessage("a"), callMessage("b")];
512
+
513
+ const { unmount } = renderView(messages, {}, [quiet]);
514
+ expect(screen.queryByTestId("copilot-tool-call-group")).toBeNull();
515
+ expect(screen.getAllByTestId("custom-call")).toHaveLength(2);
516
+ unmount();
517
+
518
+ renderView(messages, {}, [
519
+ defineToolCallRenderer({ name: "*", render: draw, group: true }),
520
+ ]);
521
+ expect(screen.getByTestId("copilot-tool-call-group")).toBeDefined();
522
+ });
523
+
524
+ it("tucks a single opted-in call into a closed group too", async () => {
525
+ const draw = ({ name }: { name: string }) =>
526
+ h("div", { "data-testid": "custom-call" }, name);
527
+ const grouped = defineToolCallRenderer({
528
+ name: "*",
529
+ render: draw,
530
+ group: true,
531
+ });
532
+ const messages = [question, callMessage("a"), result("a")];
533
+
534
+ // Grouped: a closed "Used 1 tool" card, like a bigger group.
535
+ const { unmount } = renderView(messages, {}, [grouped]);
536
+ const toggle = firstButton(screen.getByTestId("copilot-tool-call-group"));
537
+ expect(toggle.textContent).toContain("Used 1 tool");
538
+ expect(toggle.getAttribute("aria-expanded")).toBe("false");
539
+ unmount();
540
+
541
+ // Stacked: the call renders once the stack is opened.
542
+ renderView(messages, { toolCallDisplay: "stacked" }, [grouped]);
543
+ expect(screen.queryByTestId("custom-call")).toBeNull();
544
+ await fireEvent.click(
545
+ firstButton(screen.getByTestId("copilot-tool-call-group")),
546
+ );
547
+ expect(screen.getByTestId("custom-call").textContent).toBe("run_command");
548
+ });
549
+
550
+ it("shows a group the run left unfinished as stopped, not running", () => {
551
+ renderView([question, callMessage("a"), result("a"), callMessage("b")], {
552
+ isRunning: false,
553
+ });
554
+ const group = screen.getByTestId("copilot-tool-call-group");
555
+ expect(group.getAttribute("data-state")).toBe("stopped");
556
+ expect(group.textContent).not.toContain("Running");
557
+ expect(group.textContent).toContain("Stopped");
558
+ });
559
+
560
+ it("says how many calls failed instead of Done", () => {
561
+ renderView(burst);
562
+ const group = screen.getByTestId("copilot-tool-call-group");
563
+ expect(group.getAttribute("data-state")).toBe("failed");
564
+ const header = firstButton(group);
565
+ expect(header.textContent).toContain("1 failed");
566
+ expect(header.textContent).not.toContain("Done");
567
+ });
568
+
569
+ it("marks a failed row inside an open group", async () => {
570
+ renderView(burst);
571
+ const group = screen.getByTestId("copilot-tool-call-group");
572
+ await fireEvent.click(firstButton(group));
573
+ const rows = within(group).getAllByTestId("copilot-tool-render-status");
574
+ expect(rows.map((row) => row.textContent)).toEqual(["Done", "Failed"]);
575
+ });
576
+
577
+ it("keeps keyboard focus on the stack's toggle when it opens and closes", async () => {
578
+ renderView(burst, { toolCallDisplay: "stacked" });
579
+ const toggle = firstButton(screen.getByTestId("copilot-tool-call-group"));
580
+ toggle.focus();
581
+
582
+ await fireEvent.click(toggle);
583
+ expect(toggle.getAttribute("aria-expanded")).toBe("true");
584
+ expect(document.activeElement).toBe(toggle);
585
+ await fireEvent.click(toggle);
586
+ expect(toggle.getAttribute("aria-expanded")).toBe("false");
587
+ expect(document.activeElement).toBe(toggle);
588
+ });
589
+
590
+ it("opens a row to its arguments and result", async () => {
591
+ renderView(burst);
592
+ const group = screen.getByTestId("copilot-tool-call-group");
593
+ await fireEvent.click(firstButton(group));
594
+ const row = within(group).getAllByTestId("copilot-tool-render")[0]!;
595
+ const rowToggle = within(row).getByRole("button");
596
+ const details = document.getElementById(
597
+ rowToggle.getAttribute("aria-controls")!,
598
+ )!;
599
+ expect(details.hasAttribute("inert")).toBe(true);
600
+ await fireEvent.click(rowToggle);
601
+ expect(rowToggle.getAttribute("aria-expanded")).toBe("true");
602
+ expect(details.hasAttribute("inert")).toBe(false);
603
+ expect(details.textContent).toContain("Arguments");
604
+ expect(details.textContent).toContain("cmd a");
605
+ });
606
+ });
607
+
608
+ describe("CopilotChatMessageView toolCallDisplay turn answers", () => {
609
+ it("keeps the answer visible in turn when it shares a message with a call", () => {
610
+ const answer: AssistantMessage = {
611
+ ...callMessage("chart", "show_chart"),
612
+ content: "Revenue grew 12%.",
613
+ };
614
+ renderView([question, say("t1", "Looking."), answer], {
615
+ toolCallDisplay: "turn",
616
+ });
617
+ expect(
618
+ document
619
+ .querySelector('[data-message-id="t1"] [data-work-text]')!
620
+ .getAttribute("data-work-text"),
621
+ ).toBe("narration");
622
+ expect(
623
+ document
624
+ .querySelector(`[data-message-id="${answer.id}"] [data-work-text]`)!
625
+ .getAttribute("data-work-text"),
626
+ ).toBe("reply");
627
+ });
628
+ });
629
+
630
+ describe("CopilotChatWorkSummary timing", () => {
631
+ beforeEach(() => {
632
+ vi.useFakeTimers();
633
+ vi.setSystemTime(0);
634
+ });
635
+ afterEach(() => vi.useRealTimers());
636
+
637
+ const finished = [question, callMessage("a"), result("a"), say("t", "Done.")];
638
+ const summary = () => screen.getByTestId("copilot-work-summary").textContent;
639
+ const finish = async (state: ViewState, isRunning = false) => {
640
+ state.messages.value = finished;
641
+ state.isRunning.value = isRunning;
642
+ await nextTick();
643
+ };
644
+
645
+ it("ticks while working and reports the total once done", async () => {
646
+ const { state } = renderView([question], {
647
+ toolCallDisplay: "turn",
648
+ isRunning: true,
649
+ });
650
+ vi.advanceTimersByTime(125_000);
651
+ await nextTick();
652
+ expect(summary()).toContain("Working2m 5s");
653
+
654
+ vi.setSystemTime(65_000 + 60_000);
655
+ await finish(state);
656
+ expect(summary()).toContain("Worked for 2m 5s");
657
+ expect(summary()).toContain("1 tool");
658
+ });
659
+
660
+ it("reports a 65-second turn as 1m 5s", async () => {
661
+ const { state } = renderView([question], {
662
+ toolCallDisplay: "turn",
663
+ isRunning: true,
664
+ });
665
+ vi.setSystemTime(65_000);
666
+ await finish(state);
667
+ expect(summary()).toContain("Worked for 1m 5s");
668
+ });
669
+
670
+ it("leaves out time spent waiting between runs", async () => {
671
+ const { state } = renderView([question], {
672
+ toolCallDisplay: "turn",
673
+ isRunning: true,
674
+ });
675
+ vi.setSystemTime(5_000);
676
+ await finish(state);
677
+
678
+ vi.setSystemTime(185_000); // three minutes waiting on an approval
679
+ await finish(state, true);
680
+ vi.setSystemTime(190_000);
681
+ await finish(state);
682
+ expect(summary()).toContain("Worked for 10s");
683
+ });
684
+ });
685
+
686
+ describe("CopilotChatToolCallsView on its own", () => {
687
+ it("shows a call without a result as running, with no run state to say otherwise", () => {
688
+ const message: AssistantMessage = {
689
+ id: "standalone",
690
+ role: "assistant",
691
+ content: "",
692
+ toolCalls: [
693
+ ...callMessage("a").toolCalls!,
694
+ ...callMessage("b").toolCalls!,
695
+ ],
696
+ };
697
+ const Harness = defineComponent({
698
+ setup() {
699
+ useDefaultRenderTool();
700
+ return () =>
701
+ h(CopilotChatToolCallsView, { message, messages: [result("a")] });
702
+ },
703
+ });
704
+ render(CopilotKitProvider, { slots: { default: () => h(Harness) } });
705
+ const group = screen.getByTestId("copilot-tool-call-group");
706
+ expect(group.getAttribute("data-state")).toBe("running");
707
+ expect(group.textContent).toContain("Running run_command");
708
+ });
709
+
710
+ it("gives every call its own card with display separate", () => {
711
+ const message: AssistantMessage = {
712
+ id: "standalone",
713
+ role: "assistant",
714
+ content: "",
715
+ toolCalls: [
716
+ ...callMessage("a").toolCalls!,
717
+ ...callMessage("b").toolCalls!,
718
+ ],
719
+ };
720
+ const Harness = defineComponent({
721
+ setup() {
722
+ useDefaultRenderTool();
723
+ return () =>
724
+ h(CopilotChatToolCallsView, {
725
+ message,
726
+ messages: [],
727
+ display: "separate",
728
+ });
729
+ },
730
+ });
731
+ render(CopilotKitProvider, { slots: { default: () => h(Harness) } });
732
+ expect(screen.queryByTestId("copilot-tool-call-group")).toBeNull();
733
+ expect(screen.getAllByTestId("copilot-tool-render")).toHaveLength(2);
734
+ });
735
+ });