@copilotkit/vue 2.0.0-rc.7 → 2.0.0-rc.8

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 (124) hide show
  1. package/dist/index.cjs +1 -73
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.mjs +292 -16947
  4. package/dist/index.mjs.map +1 -1
  5. package/dist/use-render-activity-message-C5LCG9a9.js +16451 -0
  6. package/dist/use-render-activity-message-C5LCG9a9.js.map +1 -0
  7. package/dist/use-render-activity-message-CPzguixR.cjs +74 -0
  8. package/dist/use-render-activity-message-CPzguixR.cjs.map +1 -0
  9. package/dist/v2/components/LicenseWarningBanner.vue.d.ts.map +1 -1
  10. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  11. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  12. package/dist/v2/components/chat/CopilotChatAttachmentRenderer.vue.d.ts.map +1 -1
  13. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +6 -270
  14. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  15. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts +1 -1
  16. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  18. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts +1 -1
  19. package/dist/v2/components/chat/CopilotChatToolCallGroup.vue.d.ts.map +1 -1
  20. package/dist/v2/components/chat/CopilotChatToolCallItem.vue.d.ts.map +1 -1
  21. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts +1 -1
  22. package/dist/v2/components/chat/CopilotChatToolCallsView.vue.d.ts.map +1 -1
  23. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  24. package/dist/v2/components/chat/CopilotChatWorkSummary.vue.d.ts.map +1 -1
  25. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts +0 -1
  26. package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
  27. package/dist/v2/components/chat/index.d.ts +0 -1
  28. package/dist/v2/components/chat/index.d.ts.map +1 -1
  29. package/dist/v2/components/chat/tool-call-display-context.d.ts +2 -19
  30. package/dist/v2/components/chat/tool-call-display-context.d.ts.map +1 -1
  31. package/dist/v2/components/chat/tool-call-group-parts.d.ts +7 -20
  32. package/dist/v2/components/chat/tool-call-group-parts.d.ts.map +1 -1
  33. package/dist/v2/components/chat/turn-work.d.ts +0 -2
  34. package/dist/v2/components/chat/turn-work.d.ts.map +1 -1
  35. package/dist/v2/components/chat/types.d.ts +1 -2
  36. package/dist/v2/components/chat/types.d.ts.map +1 -1
  37. package/dist/v2/components/chat/use-chat-labels.d.ts +8 -0
  38. package/dist/v2/components/chat/use-chat-labels.d.ts.map +1 -0
  39. package/dist/v2/components/icons/index.d.ts +968 -1
  40. package/dist/v2/components/icons/index.d.ts.map +1 -1
  41. package/dist/v2/hooks/use-frontend-tool.d.ts +5 -0
  42. package/dist/v2/hooks/use-frontend-tool.d.ts.map +1 -1
  43. package/dist/v2/hooks/use-render-activity-message.d.ts +13 -9
  44. package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
  45. package/dist/v2/hooks/use-render-custom-messages.d.ts +5 -7
  46. package/dist/v2/hooks/use-render-custom-messages.d.ts.map +1 -1
  47. package/dist/v2/index.cjs +1 -1
  48. package/dist/v2/index.mjs +196 -199
  49. package/dist/v2/index.mjs.map +1 -1
  50. package/dist/v2/lib/vue-core.d.ts +0 -1
  51. package/dist/v2/lib/vue-core.d.ts.map +1 -1
  52. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  53. package/dist/v2/providers/types.d.ts +3 -46
  54. package/dist/v2/providers/types.d.ts.map +1 -1
  55. package/dist/v2/types/human-in-the-loop.d.ts +8 -29
  56. package/dist/v2/types/human-in-the-loop.d.ts.map +1 -1
  57. package/dist/v2/types/interrupt.d.ts +1 -22
  58. package/dist/v2/types/interrupt.d.ts.map +1 -1
  59. package/dist/v2/types/vue-activity-message-renderer.d.ts +16 -15
  60. package/dist/v2/types/vue-activity-message-renderer.d.ts.map +1 -1
  61. package/dist/v2/types/vue-custom-message-renderer.d.ts +7 -16
  62. package/dist/v2/types/vue-custom-message-renderer.d.ts.map +1 -1
  63. package/dist/v2/types/vue-tool-call-renderer.d.ts +16 -35
  64. package/dist/v2/types/vue-tool-call-renderer.d.ts.map +1 -1
  65. package/package.json +7 -8
  66. package/src/__tests__/build-entries.test.ts +27 -0
  67. package/src/v2/components/LicenseWarningBanner.vue +31 -15
  68. package/src/v2/components/__tests__/LicenseWarningBanner.test.ts +20 -0
  69. package/src/v2/components/chat/CopilotChat.vue +19 -40
  70. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +14 -10
  71. package/src/v2/components/chat/CopilotChatAttachmentRenderer.vue +14 -6
  72. package/src/v2/components/chat/CopilotChatInput.vue +40 -30
  73. package/src/v2/components/chat/CopilotChatMessageView.vue +72 -140
  74. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +31 -13
  75. package/src/v2/components/chat/CopilotChatToolCallGroup.vue +60 -37
  76. package/src/v2/components/chat/CopilotChatToolCallItem.vue +3 -3
  77. package/src/v2/components/chat/CopilotChatToolCallsView.vue +15 -13
  78. package/src/v2/components/chat/CopilotChatView.vue +5 -5
  79. package/src/v2/components/chat/CopilotChatWorkSummary.vue +44 -18
  80. package/src/v2/components/chat/__tests__/CopilotChat.labels.test.ts +19 -4
  81. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +33 -1
  82. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.ts +247 -403
  83. package/src/v2/components/chat/__tests__/CopilotChatMessageView.toolCallDisplay.test.ts +1 -99
  84. package/src/v2/components/chat/copilotChatToolCallItemRender.ts +3 -19
  85. package/src/v2/components/chat/index.ts +0 -1
  86. package/src/v2/components/chat/tool-call-display-context.ts +2 -23
  87. package/src/v2/components/chat/tool-call-group-parts.ts +44 -85
  88. package/src/v2/components/chat/turn-work.ts +0 -9
  89. package/src/v2/components/chat/types.ts +4 -4
  90. package/src/v2/components/chat/use-chat-labels.ts +16 -0
  91. package/src/v2/components/icons/__tests__/icons.test.ts +72 -68
  92. package/src/v2/components/icons/index.ts +109 -27
  93. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +62 -1
  94. package/src/v2/hooks/__tests__/use-render-activity-message.test.ts +105 -0
  95. package/src/v2/hooks/__tests__/use-render-tool.test.ts +3 -6
  96. package/src/v2/hooks/__tests__/zod-regression.test.ts +4 -3
  97. package/src/v2/hooks/use-frontend-tool.ts +10 -10
  98. package/src/v2/hooks/use-render-activity-message.ts +27 -48
  99. package/src/v2/hooks/use-render-custom-messages.ts +14 -35
  100. package/src/v2/lib/vue-core.ts +3 -5
  101. package/src/v2/providers/CopilotKitProvider.vue +29 -80
  102. package/src/v2/providers/__tests__/CopilotKitProvider.headersAtSendTime.test.ts +3 -3
  103. package/src/v2/providers/__tests__/CopilotKitProvider.learning.test.ts +3 -1
  104. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +17 -0
  105. package/src/v2/providers/types.ts +3 -49
  106. package/src/v2/types/__tests__/defineToolCallRenderer.test.ts +28 -33
  107. package/src/v2/types/defineToolCallRenderer.ts +5 -4
  108. package/src/v2/types/human-in-the-loop.ts +8 -32
  109. package/src/v2/types/interrupt.ts +7 -31
  110. package/src/v2/types/vue-activity-message-renderer.ts +24 -15
  111. package/src/v2/types/vue-custom-message-renderer.ts +12 -16
  112. package/src/v2/types/vue-tool-call-renderer.ts +23 -38
  113. package/vite.config.ts +1 -2
  114. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts +0 -9
  115. package/dist/v2/components/chat/BasicMarkdown.vue.d.ts.map +0 -1
  116. package/dist/v2/components/chat/tool-call-layout.d.ts +0 -115
  117. package/dist/v2/components/chat/tool-call-layout.d.ts.map +0 -1
  118. package/dist/v2/lib/transcription-client.d.ts +0 -19
  119. package/dist/v2/lib/transcription-client.d.ts.map +0 -1
  120. package/src/v2/components/chat/BasicMarkdown.vue +0 -160
  121. package/src/v2/components/chat/__tests__/BasicMarkdown.test.ts +0 -69
  122. package/src/v2/components/chat/tool-call-layout.ts +0 -304
  123. package/src/v2/lib/__tests__/transcription-client.test.ts +0 -67
  124. package/src/v2/lib/transcription-client.ts +0 -151
@@ -4,19 +4,27 @@ import { getDocumentIcon, getSourceUrl } from "@copilotkit/shared";
4
4
  import type { CopilotChatAttachmentRendererProps } from "./types";
5
5
  import CopilotChatLightbox from "./CopilotChatLightbox.vue";
6
6
  import { useLightbox } from "./use-lightbox";
7
+ import { useChatLabels } from "./use-chat-labels";
7
8
 
8
9
  const props = withDefaults(defineProps<CopilotChatAttachmentRendererProps>(), {
9
10
  filename: undefined,
10
11
  className: "",
11
12
  });
12
13
 
14
+ const labels = useChatLabels();
13
15
  const imageLoadFailed = ref(false);
14
16
  const sourceUrl = computed(() => getSourceUrl(props.source));
15
17
  const documentLabel = computed(
16
- () => props.filename || props.source.mimeType || "Unknown type",
18
+ () =>
19
+ props.filename ||
20
+ props.source.mimeType ||
21
+ labels.value.attachmentUnknownTypeText,
17
22
  );
18
23
  const documentAriaLabel = computed(
19
- () => props.filename || props.source.mimeType || "Document attachment",
24
+ () =>
25
+ props.filename ||
26
+ props.source.mimeType ||
27
+ labels.value.attachmentDocumentAltText,
20
28
  );
21
29
 
22
30
  const imageButton = ref<HTMLButtonElement | null>(null);
@@ -43,7 +51,7 @@ const {
43
51
  <img
44
52
  ref="thumbnailRef"
45
53
  :src="sourceUrl"
46
- alt="Image attachment"
54
+ :alt="labels.attachmentImageAltText"
47
55
  class="cpk-attachment-renderer cpk-attachment-renderer-image"
48
56
  :class="props.className"
49
57
  data-testid="copilot-chat-attachment-renderer-image"
@@ -52,14 +60,14 @@ const {
52
60
  <!-- Teleported to the body; inside the button only to keep one root. -->
53
61
  <CopilotChatLightbox
54
62
  v-if="lightboxOpen"
55
- label="Image attachment"
63
+ :label="labels.attachmentImageAltText"
56
64
  :return-focus-to="imageButton"
57
65
  @close="closeLightbox"
58
66
  >
59
67
  <img
60
68
  :style="{ viewTransitionName: vtName }"
61
69
  :src="sourceUrl"
62
- alt="Image attachment"
70
+ :alt="labels.attachmentImageAltText"
63
71
  class="cpk-attachment-renderer-lightbox-image"
64
72
  data-testid="copilot-chat-attachment-renderer-lightbox-image"
65
73
  />
@@ -71,7 +79,7 @@ const {
71
79
  :class="props.className"
72
80
  data-testid="copilot-chat-attachment-renderer-image-fallback"
73
81
  >
74
- <span>Failed to load image</span>
82
+ <span>{{ labels.attachmentImageLoadErrorText }}</span>
75
83
  </div>
76
84
 
77
85
  <div
@@ -8,8 +8,7 @@ import {
8
8
  useAttrs,
9
9
  watch,
10
10
  } from "vue";
11
- import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
12
- import { CopilotChatDefaultLabels } from "../../providers/types";
11
+ import { ɵcomposerButtonState, ɵresolveComposerKey } from "@copilotkit/core";
13
12
  import {
14
13
  IconArrowUp,
15
14
  IconCheck,
@@ -29,6 +28,7 @@ import type {
29
28
  import { useCopilotId } from "../ui/use-id";
30
29
  import type { CopilotChatAudioRecorderRef } from "./audioRecorder";
31
30
  import type { CopilotChatInputMode, ToolsMenuItem } from "./types";
31
+ import { useChatLabels } from "./use-chat-labels";
32
32
 
33
33
  defineOptions({ inheritAttrs: false });
34
34
 
@@ -107,7 +107,6 @@ const emit = defineEmits<{
107
107
  }>();
108
108
 
109
109
  const attrs = useAttrs();
110
- const config = useCopilotChatConfiguration();
111
110
  const shellRef = ref<HTMLElement | null>(null);
112
111
  const textareaRef = ref<HTMLTextAreaElement | null>(null);
113
112
  const gridRef = ref<HTMLElement | null>(null);
@@ -148,16 +147,13 @@ const isControlled = computed(() => props.modelValue !== undefined);
148
147
  const inputValue = computed(() =>
149
148
  isControlled.value ? (props.modelValue ?? "") : localValue.value,
150
149
  );
151
- const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
150
+ const labels = useChatLabels();
152
151
  const resolvedPlaceholder = computed(
153
152
  () => props.placeholder ?? labels.value.chatInputPlaceholder,
154
153
  );
155
154
  const isExpanded = computed(
156
155
  () => props.mode === "input" && layout.value === "expanded",
157
156
  );
158
- const isProcessing = computed(
159
- () => props.mode !== "transcribe" && props.isRunning,
160
- );
161
157
  const shouldShowDisclaimer = computed(
162
158
  () => props.showDisclaimer ?? props.positioning === "absolute",
163
159
  );
@@ -177,16 +173,20 @@ const hasFinishTranscribeAction = computed(
177
173
  typeof props.onFinishTranscribe === "function" ||
178
174
  typeof props.onFinishTranscribeWithAudio === "function",
179
175
  );
180
- const canSend = computed(
181
- () =>
182
- props.mode === "input" &&
183
- !props.disabled &&
184
- hasSubmitAction.value &&
185
- inputValue.value.trim().length > 0,
186
- );
187
- const sendDisabled = computed(() =>
188
- isProcessing.value ? !hasStopAction.value : !canSend.value,
176
+ const composerInput = computed(() => ({
177
+ mode: props.mode,
178
+ isRunning: props.isRunning,
179
+ value: inputValue.value,
180
+ canSubmit: hasSubmitAction.value,
181
+ canStop: hasStopAction.value,
182
+ disabled: props.disabled,
183
+ }));
184
+ const composerButton = computed(() =>
185
+ ɵcomposerButtonState(composerInput.value),
189
186
  );
187
+ const isProcessing = computed(() => composerButton.value.isProcessing);
188
+ const sendDisabled = computed(() => composerButton.value.disabled);
189
+ const showsStop = computed(() => composerButton.value.action === "stop");
190
190
 
191
191
  const containerClass = computed(() => [
192
192
  "cpk-chat-input",
@@ -408,14 +408,16 @@ function handleInput(event: Event) {
408
408
  updateSlashState(nextValue);
409
409
  }
410
410
 
411
+ // The button stops a run whatever the input holds. Enter differs: see
412
+ // handleKeydown.
411
413
  function handleSendButtonClick() {
412
- if (isProcessing.value) {
413
- if (hasStopAction.value) {
414
- emit("stop");
415
- }
414
+ if (showsStop.value) {
415
+ emit("stop");
416
416
  return;
417
417
  }
418
- submit();
418
+ if (!isProcessing.value) {
419
+ submit();
420
+ }
419
421
  }
420
422
 
421
423
  function handleKeydown(event: KeyboardEvent) {
@@ -473,15 +475,23 @@ function handleKeydown(event: KeyboardEvent) {
473
475
  }
474
476
  }
475
477
 
476
- if (event.key === "Enter" && !event.shiftKey && props.mode === "input") {
478
+ if (props.mode !== "input") {
479
+ return;
480
+ }
481
+
482
+ // Enter with text sends, also while a run is going. Enter in an empty input
483
+ // stops the run. Read the textarea: a controlled modelValue can lag a tick.
484
+ const action = ɵresolveComposerKey(event, {
485
+ ...composerInput.value,
486
+ value: textareaRef.value?.value ?? inputValue.value,
487
+ });
488
+ if (event.key === "Enter" && !event.shiftKey) {
477
489
  event.preventDefault();
478
- if (isProcessing.value) {
479
- if (hasStopAction.value) {
480
- emit("stop");
481
- }
482
- return;
483
- }
490
+ }
491
+ if (action === "send") {
484
492
  submit();
493
+ } else if (action === "stop") {
494
+ emit("stop");
485
495
  }
486
496
  }
487
497
 
@@ -1295,7 +1305,7 @@ onBeforeUnmount(() => {
1295
1305
  type="button"
1296
1306
  data-testid="copilot-chat-input-send"
1297
1307
  :aria-label="
1298
- isProcessing && hasStopAction
1308
+ showsStop
1299
1309
  ? labels.chatInputToolbarStopButtonLabel
1300
1310
  : labels.chatInputToolbarSendButtonLabel
1301
1311
  "
@@ -1304,7 +1314,7 @@ onBeforeUnmount(() => {
1304
1314
  @click="handleSendButtonClick"
1305
1315
  >
1306
1316
  <IconSquare
1307
- v-if="isProcessing && hasStopAction"
1317
+ v-if="showsStop"
1308
1318
  class="cpk-chat-input-stop-icon"
1309
1319
  />
1310
1320
  <IconArrowUp v-else class="cpk-chat-input-send-icon" />
@@ -15,6 +15,27 @@ import {
15
15
  createRowKeyStore,
16
16
  resolveRowRenderKeysById,
17
17
  } from "@copilotkit/shared";
18
+ import {
19
+ ɵdeduplicateMessages,
20
+ ɵgetToolCallLayouts,
21
+ ɵgetToolCallRendererNames,
22
+ ɵgetTurnWork,
23
+ ɵgetWorkOpenState,
24
+ ɵisTurnWorkOpen,
25
+ ɵisTurnWorking,
26
+ ɵrecordWorkTime,
27
+ ɵresolveActivityRenderer,
28
+ ɵresolveCustomMessageRenderers,
29
+ ɵgetLatestTurnKey,
30
+ } from "@copilotkit/core";
31
+ import type {
32
+ CopilotChatToolCallDisplay,
33
+ ToolCallEntry,
34
+ ɵToolCallDisplayContextValue,
35
+ ɵToolCallLayout,
36
+ ɵTurnWork,
37
+ ɵWorkTime,
38
+ } from "@copilotkit/core";
18
39
  import type {
19
40
  InterruptRenderProps,
20
41
  VueCustomMessageRendererProps,
@@ -33,18 +54,6 @@ import {
33
54
  } from "./message-feedback";
34
55
  import type { MessageFeedbackScope } from "./message-feedback";
35
56
  import { ToolCallDisplayContextKey } from "./tool-call-display-context";
36
- import type { ToolCallDisplayContextValue } from "./tool-call-display-context";
37
- import {
38
- getToolCallLayouts,
39
- getToolCallRendererNames,
40
- getTurnWork,
41
- } from "./tool-call-layout";
42
- import type {
43
- CopilotChatToolCallDisplay,
44
- ToolCallEntry,
45
- ToolCallLayout,
46
- TurnWork,
47
- } from "./tool-call-layout";
48
57
  import type { CopilotChatWorkSummarySlotProps } from "./types";
49
58
 
50
59
  interface MessageMetaProps {
@@ -203,47 +212,22 @@ watch(
203
212
  { immediate: true },
204
213
  );
205
214
 
206
- function deduplicateMessages(messages: Message[]): Message[] {
207
- const byId = new Map<string, Message>();
208
- for (const message of messages) {
209
- const existing = byId.get(message.id);
210
- if (
211
- existing &&
212
- message.role === "assistant" &&
213
- existing.role === "assistant"
214
- ) {
215
- const content = message.content || existing.content;
216
- const toolCalls = message.toolCalls ?? existing.toolCalls;
217
- byId.set(message.id, {
218
- ...existing,
219
- ...message,
220
- content,
221
- toolCalls,
222
- } as AssistantMessage);
223
- } else {
224
- byId.set(message.id, message);
225
- }
226
- }
227
- return [...byId.values()];
228
- }
229
-
230
215
  const deduplicatedMessages = computed(() =>
231
- deduplicateMessages(props.messages),
216
+ ɵdeduplicateMessages(props.messages),
232
217
  );
233
218
 
234
219
  // Groups of calls can span messages (an agent often sends each call as its
235
220
  // own message), so they're laid out here for the whole list and handed to
236
221
  // each message's tool calls view through provide/inject.
237
- const toolCallLayouts = computed<ReadonlyMap<string, ToolCallLayout>>(() =>
222
+ const toolCallLayouts = computed<ReadonlyMap<string, ɵToolCallLayout>>(() =>
238
223
  props.toolCallDisplay === "separate"
239
224
  ? new Map()
240
- : getToolCallLayouts(
225
+ : ɵgetToolCallLayouts(
241
226
  deduplicatedMessages.value,
242
227
  props.messages,
243
- getToolCallRendererNames(
244
- copilotkit.value.renderToolCalls,
245
- forwardedSlotNames.value,
246
- ),
228
+ ɵgetToolCallRendererNames(copilotkit.value.renderToolCalls, {
229
+ slotNames: forwardedSlotNames.value,
230
+ }),
247
231
  ),
248
232
  );
249
233
  const learningFeedbackEnabled = useLearningFeedbackEnabled(copilotkit);
@@ -265,11 +249,11 @@ provide(
265
249
 
266
250
  const turnWork = computed(() =>
267
251
  props.toolCallDisplay === "turn"
268
- ? getTurnWork(deduplicatedMessages.value, toolCallLayouts.value)
252
+ ? ɵgetTurnWork(deduplicatedMessages.value, toolCallLayouts.value)
269
253
  : undefined,
270
254
  );
271
255
  const turnByFirstMessage = computed(() => {
272
- const byFirst = new Map<string, TurnWork>();
256
+ const byFirst = new Map<string, ɵTurnWork>();
273
257
  for (const turn of turnWork.value?.values() ?? []) {
274
258
  byFirst.set(turn.firstMessageId, turn);
275
259
  }
@@ -280,30 +264,23 @@ const lastRenderedMessage = computed(
280
264
  () => deduplicatedMessages.value[deduplicatedMessages.value.length - 1],
281
265
  );
282
266
  // The latest reply is keyed by the user message it answers (see getTurnWork).
283
- const latestTurnKey = computed(() => {
284
- const rendered = deduplicatedMessages.value;
285
- for (let i = rendered.length - 1; i >= 0; i--) {
286
- if (rendered[i]!.role === "user") return rendered[i]!.id;
287
- }
288
- return "start";
289
- });
290
- const isWorkingOn = (turnKey: string) =>
291
- props.isRunning && turnKey === latestTurnKey.value;
267
+ const latestTurnKey = computed(() =>
268
+ ɵgetLatestTurnKey(deduplicatedMessages.value),
269
+ );
292
270
 
293
271
  // Work shows while the agent is on it and folds once the reply is done,
294
272
  // unless the user has opened or closed it themselves.
295
273
  const toggledTurns = ref<ReadonlyMap<string, boolean>>(new Map());
296
- const isWorkOpen = (turnKey: string) =>
297
- toggledTurns.value.get(turnKey) ?? isWorkingOn(turnKey);
298
-
299
- // How long the agent worked, for "Worked for 9s": the sum of its runs on the
300
- // reply, so waiting between them (say, for an approval) doesn't count. Only
301
- // known for replies this client watched start from the user's message; a
302
- // restored thread shows the tool count instead.
303
- const workTimes = new Map<
304
- string,
305
- { elapsedMs: number; workingSince?: number }
306
- >();
274
+ const turnOpenState = computed(() => ({
275
+ toggledTurns: toggledTurns.value,
276
+ isRunning: props.isRunning,
277
+ latestTurnKey: latestTurnKey.value,
278
+ }));
279
+
280
+ // How long the agent worked, for "Worked for 9s". Only known for replies this
281
+ // client watched start from the user's message; a restored thread shows the
282
+ // tool count instead.
283
+ const workTimes = new Map<string, ɵWorkTime>();
307
284
  const workTimesVersion = ref(0);
308
285
  watch(
309
286
  [
@@ -314,42 +291,25 @@ watch(
314
291
  ],
315
292
  ([display, isRunning, turnKey, lastRole]) => {
316
293
  if (display !== "turn") return;
317
- const now = Date.now();
318
- if (isRunning) {
319
- const times = workTimes.get(turnKey);
320
- if (times) times.workingSince ??= now;
321
- else if (lastRole === "user")
322
- workTimes.set(turnKey, { elapsedMs: 0, workingSince: now });
323
- } else {
324
- for (const times of workTimes.values()) {
325
- if (times.workingSince === undefined) continue;
326
- times.elapsedMs += now - times.workingSince;
327
- times.workingSince = undefined;
328
- }
329
- }
294
+ ɵrecordWorkTime(workTimes, {
295
+ isRunning,
296
+ latestTurnKey: turnKey,
297
+ lastMessageRole: lastRole,
298
+ now: Date.now(),
299
+ });
330
300
  workTimesVersion.value++;
331
301
  },
332
302
  { immediate: true },
333
303
  );
334
304
 
335
- // Whether each working-phase message's work shows. Text that ends the reply
336
- // stays out even when its message also calls a tool: it's the answer.
337
- const workOpenState = computed(() => {
338
- const open = new Map<string, boolean>();
339
- const textOpen = new Map<string, boolean>();
340
- for (const turn of turnWork.value?.values() ?? []) {
341
- const turnOpen = isWorkOpen(turn.key);
342
- for (const id of turn.workMessageIds) {
343
- open.set(id, turnOpen);
344
- if (id !== turn.replyTextMessageId) textOpen.set(id, turnOpen);
345
- }
346
- }
347
- return { workOpen: open, workTextOpen: textOpen };
348
- });
305
+ // Whether each working-phase message's work, and its text, shows.
306
+ const workOpenState = computed(() =>
307
+ ɵgetWorkOpenState(turnWork.value?.values() ?? [], turnOpenState.value),
308
+ );
349
309
 
350
310
  provide(
351
311
  ToolCallDisplayContextKey,
352
- computed<ToolCallDisplayContextValue>(() => ({
312
+ computed<ɵToolCallDisplayContextValue>(() => ({
353
313
  display: props.toolCallDisplay,
354
314
  isRunning: props.isRunning,
355
315
  layouts: toolCallLayouts.value,
@@ -364,8 +324,8 @@ const WorkSummaryRow: FunctionalComponent<{
364
324
  calls: ToolCallEntry[];
365
325
  }> = ({ turnKey, calls }) => {
366
326
  void workTimesVersion.value;
367
- const isWorking = isWorkingOn(turnKey);
368
- const isOpen = isWorkOpen(turnKey);
327
+ const isWorking = ɵisTurnWorking(turnKey, turnOpenState.value);
328
+ const isOpen = ɵisTurnWorkOpen(turnKey, turnOpenState.value);
369
329
  const times = workTimes.get(turnKey);
370
330
  const summaryProps: CopilotChatWorkSummarySlotProps = {
371
331
  isWorking,
@@ -534,21 +494,12 @@ function resolveCustomMessageRenderer(
534
494
  return null;
535
495
  }
536
496
 
537
- const agentId = resolvedAgentId.value;
538
- const renderers = [...copilotkit.value.renderCustomMessages]
539
- .filter(
540
- (renderer) =>
541
- renderer.agentId === undefined || renderer.agentId === agentId,
542
- )
543
- .sort((a, b) => {
544
- const aHasAgent = a.agentId !== undefined;
545
- const bHasAgent = b.agentId !== undefined;
546
- if (aHasAgent === bHasAgent) return 0;
547
- return aHasAgent ? -1 : 1;
548
- });
549
-
550
- const selected = renderers[0];
551
- if (!selected?.render) {
497
+ // Try each renderer in order and use the first that has something to draw.
498
+ const selected = ɵresolveCustomMessageRenderers(
499
+ copilotkit.value.renderCustomMessages,
500
+ resolvedAgentId.value,
501
+ ).find((renderer) => renderer.render);
502
+ if (!selected) {
552
503
  return null;
553
504
  }
554
505
 
@@ -563,45 +514,26 @@ function resolveActivityRenderer(
563
514
  ): { renderer: Component; props: ActivitySlotProps } | null {
564
515
  stateTick.value += 0;
565
516
 
566
- const agentId = resolvedAgentId.value;
567
- const renderer = [...copilotkit.value.renderActivityMessages]
568
- .filter(
569
- (entry) =>
570
- entry.activityType === message.activityType &&
571
- (entry.agentId === undefined || entry.agentId === agentId),
572
- )
573
- .sort((a, b) => {
574
- const aScoped = a.agentId !== undefined;
575
- const bScoped = b.agentId !== undefined;
576
- if (aScoped === bScoped) return 0;
577
- return aScoped ? -1 : 1;
578
- })[0];
579
-
580
- if (!renderer) return null;
581
- const parsed = renderer.content.safeParse(message.content);
582
- if (!parsed.success) return null;
517
+ const match = ɵresolveActivityRenderer(
518
+ copilotkit.value.renderActivityMessages,
519
+ {
520
+ activityType: message.activityType,
521
+ agentId: resolvedAgentId.value,
522
+ content: message.content,
523
+ },
524
+ );
525
+ if (!match) return null;
583
526
 
584
527
  return {
585
- renderer: renderer.render as Component,
528
+ renderer: match.renderer.render as Component,
586
529
  props: {
587
530
  activityType: message.activityType,
588
- content: parsed.data,
531
+ content: match.content,
589
532
  message,
590
533
  agent: resolvedThreadAgent.value,
591
534
  },
592
535
  };
593
536
  }
594
-
595
- function resolveToolMessage(
596
- message: Message,
597
- toolCallId: string,
598
- ): ToolMessage | undefined {
599
- return props.messages.find(
600
- (candidate) =>
601
- candidate.role === "tool" &&
602
- (candidate as ToolMessage).toolCallId === toolCallId,
603
- ) as ToolMessage | undefined;
604
- }
605
537
  </script>
606
538
 
607
539
  <template>
@@ -3,8 +3,9 @@ import { computed, onBeforeUnmount, ref, watch } from "vue";
3
3
  import type { Message, ReasoningMessage } from "@ag-ui/core";
4
4
  import { IconChevronRight } from "../icons";
5
5
  import StreamingMarkdownDefault from "./StreamingMarkdownDefault.vue";
6
- import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
7
- import { CopilotChatDefaultLabels } from "../../providers/types";
6
+ import { ɵformatLabel, ɵformatReasoningDuration } from "@copilotkit/core";
7
+ import type { CopilotChatLabels } from "@copilotkit/core";
8
+ import { useChatLabels } from "./use-chat-labels";
8
9
 
9
10
  const props = withDefaults(
10
11
  defineProps<{
@@ -69,13 +70,29 @@ defineSlots<{
69
70
  }) => unknown;
70
71
  }>();
71
72
 
72
- function formatDuration(seconds: number): string {
73
- if (seconds < 1) return "a few seconds";
74
- if (seconds < 60) return `${Math.round(seconds)} seconds`;
75
- const mins = Math.floor(seconds / 60);
76
- const secs = Math.round(seconds % 60);
77
- if (secs === 0) return `${mins} minute${mins > 1 ? "s" : ""}`;
78
- return `${mins}m ${secs}s`;
73
+ /** How long the model reasoned, in the chat's labels: "4 seconds", "1m 5s". */
74
+ function formatDuration(labels: Readonly<CopilotChatLabels>, seconds: number) {
75
+ const duration = ɵformatReasoningDuration(seconds);
76
+ switch (duration.unit) {
77
+ case "fewSeconds":
78
+ return labels.reasoningMessageDurationFewSeconds;
79
+ case "seconds":
80
+ return ɵformatLabel(labels.reasoningMessageDurationSeconds, {
81
+ count: duration.seconds,
82
+ });
83
+ case "minutes":
84
+ return ɵformatLabel(
85
+ duration.minutes === 1
86
+ ? labels.reasoningMessageDurationMinutesOne
87
+ : labels.reasoningMessageDurationMinutesOther,
88
+ { count: duration.minutes },
89
+ );
90
+ case "minutesSeconds":
91
+ return ɵformatLabel(labels.reasoningMessageDurationMinutesSeconds, {
92
+ minutes: duration.minutes,
93
+ seconds: duration.seconds,
94
+ });
95
+ }
79
96
  }
80
97
 
81
98
  const normalizedContent = computed(() =>
@@ -133,13 +150,14 @@ onBeforeUnmount(() => {
133
150
  clearElapsedInterval();
134
151
  });
135
152
 
136
- const config = useCopilotChatConfiguration();
153
+ const labels = useChatLabels();
137
154
  /** `labels.reasoningMessageThinkingLabel` while streaming, then "Thought for …". */
138
155
  const label = computed(() =>
139
156
  isStreaming.value
140
- ? (config.value?.labels ?? CopilotChatDefaultLabels)
141
- .reasoningMessageThinkingLabel
142
- : `Thought for ${formatDuration(elapsed.value)}`,
157
+ ? labels.value.reasoningMessageThinkingLabel
158
+ : ɵformatLabel(labels.value.reasoningMessageThoughtForLabel, {
159
+ duration: formatDuration(labels.value, elapsed.value),
160
+ }),
143
161
  );
144
162
 
145
163
  function toggleOpen() {