@copilotkit/vue 1.81.0 → 1.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/AGENTS.md +8 -7
  2. package/CONTRIBUTOR_GUIDE.md +120 -0
  3. package/PARITY.md +19 -10
  4. package/README.md +24 -373
  5. package/dist/index.cjs +1 -1
  6. package/dist/index.mjs +2 -2
  7. package/dist/styles.css +1 -1
  8. package/dist/use-render-activity-message-B7sDGUl4.cjs +70 -0
  9. package/dist/use-render-activity-message-B7sDGUl4.cjs.map +1 -0
  10. package/dist/{use-render-activity-message-5H6AfuJB.js → use-render-activity-message-mQvByEYF.js} +3189 -3147
  11. package/dist/use-render-activity-message-mQvByEYF.js.map +1 -0
  12. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts +10 -0
  13. package/dist/v2/components/a2ui/A2UICatalogContext.d.ts.map +1 -1
  14. package/dist/v2/components/a2ui/catalog.d.ts.map +1 -1
  15. package/dist/v2/components/a2ui/utils.d.ts +1 -1
  16. package/dist/v2/components/a2ui/utils.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotChatAttachmentQueue.vue.d.ts.map +1 -1
  18. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +54 -0
  19. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  20. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts +3 -0
  21. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  22. package/dist/v2/components/chat/CopilotModalHeader.vue.d.ts.map +1 -1
  23. package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts +4 -0
  24. package/dist/v2/components/chat/CopilotModalHeaderCloseButton.d.ts.map +1 -1
  25. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  26. package/dist/v2/index.cjs +1 -1
  27. package/dist/v2/index.mjs +1 -1
  28. package/dist/v2/providers/CopilotKitProvider.types.d.ts +15 -0
  29. package/dist/v2/providers/CopilotKitProvider.types.d.ts.map +1 -1
  30. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  31. package/dist/v2/providers/types.d.ts +9 -0
  32. package/dist/v2/providers/types.d.ts.map +1 -1
  33. package/package.json +6 -6
  34. package/src/styles/globals.css +7 -0
  35. package/src/v2/components/__tests__/A2UIBasicCatalogColors.test.ts +188 -0
  36. package/src/v2/components/__tests__/A2UICatalogContext.test.ts +63 -0
  37. package/src/v2/components/a2ui/A2UICatalogContext.ts +14 -2
  38. package/src/v2/components/a2ui/catalog.ts +44 -15
  39. package/src/v2/components/a2ui/utils.ts +1 -1
  40. package/src/v2/components/chat/CopilotChatAttachmentQueue.vue +8 -3
  41. package/src/v2/components/chat/CopilotChatInput.vue +2 -2
  42. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +11 -1
  43. package/src/v2/components/chat/CopilotModalHeader.vue +4 -1
  44. package/src/v2/components/chat/CopilotModalHeaderCloseButton.ts +11 -1
  45. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +3 -1
  46. package/src/v2/components/chat/__tests__/CopilotChat.labels.test.ts +198 -0
  47. package/src/v2/providers/CopilotKitProvider.types.ts +15 -0
  48. package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +50 -0
  49. package/src/v2/providers/types.ts +9 -0
  50. package/dist/use-render-activity-message-5H6AfuJB.js.map +0 -1
  51. package/dist/use-render-activity-message-DxOdM2Qt.cjs +0 -70
  52. package/dist/use-render-activity-message-DxOdM2Qt.cjs.map +0 -1
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { z } from "zod";
3
+ import { Catalog } from "@a2ui/web_core/v0_9";
4
+ import type { ComponentApi } from "@a2ui/web_core/v0_9";
5
+ import { buildCatalogContextValue } from "../a2ui/A2UICatalogContext";
6
+ import { vueBasicCatalog } from "../a2ui/catalog";
7
+
8
+ function makeCustomCatalog(): Catalog<ComponentApi> {
9
+ const components: ComponentApi[] = [
10
+ { name: "FlightCard", schema: z.object({ airline: z.string() }) },
11
+ {
12
+ name: "PieChart",
13
+ schema: z.object({ innerRadius: z.number().optional() }),
14
+ },
15
+ ];
16
+ return new Catalog<ComponentApi>(
17
+ "copilotkit://custom-catalog",
18
+ components,
19
+ [],
20
+ );
21
+ }
22
+
23
+ describe("buildCatalogContextValue", () => {
24
+ it("includes each custom component's JSON schema by default", () => {
25
+ const value = buildCatalogContextValue(makeCustomCatalog());
26
+
27
+ expect(value).toContain("- copilotkit://custom-catalog");
28
+ expect(value).toContain(" - FlightCard:");
29
+ expect(value).toContain('"airline"');
30
+ expect(value).toContain('"innerRadius"');
31
+ });
32
+
33
+ it("includes schemas when includeSchema is true", () => {
34
+ const catalog = makeCustomCatalog();
35
+
36
+ expect(buildCatalogContextValue(catalog, { includeSchema: true })).toBe(
37
+ buildCatalogContextValue(catalog),
38
+ );
39
+ });
40
+
41
+ it("lists custom components by name only when includeSchema is false", () => {
42
+ const value = buildCatalogContextValue(makeCustomCatalog(), {
43
+ includeSchema: false,
44
+ });
45
+
46
+ expect(value).toBe(
47
+ [
48
+ "Available A2UI catalog:",
49
+ "- copilotkit://custom-catalog",
50
+ " Custom catalog (does NOT include all basic components).",
51
+ " Custom components:",
52
+ " - FlightCard",
53
+ " - PieChart",
54
+ ].join("\n"),
55
+ );
56
+ });
57
+
58
+ it("describes the basic catalog the same way regardless of includeSchema", () => {
59
+ expect(
60
+ buildCatalogContextValue(vueBasicCatalog, { includeSchema: false }),
61
+ ).toBe(buildCatalogContextValue(vueBasicCatalog));
62
+ });
63
+ });
@@ -25,8 +25,14 @@ const A2UI_SCHEMA_CONTEXT_DESCRIPTION =
25
25
  /**
26
26
  * Build a context string describing the available A2UI catalog.
27
27
  * Vue-native equivalent of buildCatalogContextValue from a2ui-renderer.
28
+ *
29
+ * When `options.includeSchema` is `false`, custom components are listed by name
30
+ * only and their JSON schemas are omitted.
28
31
  */
29
- function buildCatalogContextValue(catalog?: Catalog<ComponentApi>): string {
32
+ export function buildCatalogContextValue(
33
+ catalog?: Catalog<ComponentApi>,
34
+ options?: { includeSchema?: boolean },
35
+ ): string {
30
36
  const resolved = catalog ?? vueBasicCatalog;
31
37
  const BASIC_CATALOG_ID =
32
38
  "https://a2ui.org/specification/v0_9/basic_catalog.json";
@@ -68,6 +74,10 @@ function buildCatalogContextValue(catalog?: Catalog<ComponentApi>): string {
68
74
  for (const name of customNames) {
69
75
  const comp = resolved.components.get(name);
70
76
  if (!comp) continue;
77
+ if (options?.includeSchema === false) {
78
+ lines.push(` - ${name}`);
79
+ continue;
80
+ }
71
81
  const jsonSchema = zodToJsonSchema(comp.schema);
72
82
  lines.push(` - ${name}:`);
73
83
  lines.push(
@@ -127,7 +137,9 @@ export function registerA2UICatalogContext(
127
137
  },
128
138
  ): void {
129
139
  const contextValue = computed(() =>
130
- buildCatalogContextValue(options.catalog()),
140
+ buildCatalogContextValue(options.catalog(), {
141
+ includeSchema: options.includeSchema(),
142
+ }),
131
143
  );
132
144
 
133
145
  // Register catalog capabilities context
@@ -88,7 +88,13 @@ const Text = createVueComponent(TextApi, ({ props }) => {
88
88
  case "caption":
89
89
  return h(
90
90
  "small",
91
- { style: { ...style, color: "#666", textAlign: "left" } },
91
+ {
92
+ style: {
93
+ ...style,
94
+ color: "var(--a2ui-color-muted, #666)",
95
+ textAlign: "left",
96
+ },
97
+ },
92
98
  text,
93
99
  );
94
100
  case "body":
@@ -175,7 +181,12 @@ const AudioPlayer = createVueComponent(AudioPlayerApi, ({ props }) => {
175
181
  props.description
176
182
  ? h(
177
183
  "span",
178
- { style: { fontSize: "12px", color: "#666" } },
184
+ {
185
+ style: {
186
+ fontSize: "12px",
187
+ color: "var(--a2ui-color-muted, #666)",
188
+ },
189
+ },
179
190
  props.description,
180
191
  )
181
192
  : null,
@@ -239,7 +250,7 @@ const List = createVueComponent(ListApi, ({ props, buildChild }) => {
239
250
  const Card = createVueComponent(CardApi, ({ props, buildChild }) => {
240
251
  const style = {
241
252
  ...getBaseContainerStyle(),
242
- backgroundColor: "#fff",
253
+ backgroundColor: "var(--a2ui-color-surface, #fff)",
243
254
  boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
244
255
  width: "100%",
245
256
  };
@@ -268,7 +279,7 @@ const Tabs = createVueComponent(
268
279
  {
269
280
  style: {
270
281
  display: "flex",
271
- borderBottom: "1px solid #ccc",
282
+ borderBottom: STANDARD_BORDER,
272
283
  marginBottom: "8px",
273
284
  },
274
285
  },
@@ -321,7 +332,7 @@ const Divider = createVueComponent(DividerApi, ({ props }) => {
321
332
  const style: Record<string, string> = {
322
333
  margin: LEAF_MARGIN,
323
334
  border: "none",
324
- backgroundColor: "#ccc",
335
+ backgroundColor: "var(--a2ui-color-border, #ccc)",
325
336
  };
326
337
 
327
338
  if (isVertical) {
@@ -374,7 +385,7 @@ const Modal = createVueComponent(
374
385
  "div",
375
386
  {
376
387
  style: {
377
- backgroundColor: "#fff",
388
+ backgroundColor: "var(--a2ui-color-surface, #fff)",
378
389
  padding: "24px",
379
390
  borderRadius: "8px",
380
391
  maxWidth: "90%",
@@ -426,13 +437,13 @@ const Button = createVueComponent(ButtonApi, ({ props, buildChild }) => {
426
437
  margin: LEAF_MARGIN,
427
438
  padding: "8px 16px",
428
439
  cursor: "pointer",
429
- border: props.variant === "borderless" ? "none" : "1px solid #ccc",
440
+ border: props.variant === "borderless" ? "none" : STANDARD_BORDER,
430
441
  backgroundColor:
431
442
  props.variant === "primary"
432
443
  ? "var(--a2ui-primary-color, #007bff)"
433
444
  : props.variant === "borderless"
434
445
  ? "transparent"
435
- : "#fff",
446
+ : "var(--a2ui-color-surface, #fff)",
436
447
  color: props.variant === "primary" ? "#fff" : "inherit",
437
448
  borderRadius: "4px",
438
449
  display: "inline-flex",
@@ -469,7 +480,7 @@ const TextField = createVueComponent(
469
480
  width: "100%",
470
481
  border:
471
482
  props.validationErrors && props.validationErrors.length > 0
472
- ? "1px solid red"
483
+ ? "1px solid var(--a2ui-color-error, red)"
473
484
  : STANDARD_BORDER,
474
485
  borderRadius: STANDARD_RADIUS,
475
486
  boxSizing: "border-box",
@@ -519,7 +530,12 @@ const TextField = createVueComponent(
519
530
  hasError
520
531
  ? h(
521
532
  "span",
522
- { style: { fontSize: "12px", color: "red" } },
533
+ {
534
+ style: {
535
+ fontSize: "12px",
536
+ color: "var(--a2ui-color-error, red)",
537
+ },
538
+ },
523
539
  props.validationErrors![0],
524
540
  )
525
541
  : null,
@@ -558,7 +574,9 @@ const CheckBox = createVueComponent(
558
574
  props.setValue((e.target as HTMLInputElement).checked),
559
575
  style: {
560
576
  cursor: "pointer",
561
- outline: hasError ? "1px solid red" : "none",
577
+ outline: hasError
578
+ ? "1px solid var(--a2ui-color-error, red)"
579
+ : "none",
562
580
  },
563
581
  }),
564
582
  props.label
@@ -568,7 +586,9 @@ const CheckBox = createVueComponent(
568
586
  for: uniqueId,
569
587
  style: {
570
588
  cursor: "pointer",
571
- color: hasError ? "red" : "inherit",
589
+ color: hasError
590
+ ? "var(--a2ui-color-error, red)"
591
+ : "inherit",
572
592
  },
573
593
  },
574
594
  props.label,
@@ -580,7 +600,11 @@ const CheckBox = createVueComponent(
580
600
  ? h(
581
601
  "span",
582
602
  {
583
- style: { fontSize: "12px", color: "red", marginTop: "4px" },
603
+ style: {
604
+ fontSize: "12px",
605
+ color: "var(--a2ui-color-error, red)",
606
+ marginTop: "4px",
607
+ },
584
608
  },
585
609
  props.validationErrors?.[0],
586
610
  )
@@ -681,7 +705,7 @@ const ChoicePicker = createVueComponent(
681
705
  : STANDARD_BORDER,
682
706
  backgroundColor: isSelected
683
707
  ? "var(--a2ui-primary-color, #007bff)"
684
- : "#fff",
708
+ : "var(--a2ui-color-surface, #fff)",
685
709
  color: isSelected ? "#fff" : "inherit",
686
710
  cursor: "pointer",
687
711
  fontSize: "12px",
@@ -754,7 +778,12 @@ const Slider = createVueComponent(
754
778
  : null,
755
779
  h(
756
780
  "span",
757
- { style: { fontSize: "12px", color: "#666" } },
781
+ {
782
+ style: {
783
+ fontSize: "12px",
784
+ color: "var(--a2ui-color-muted, #666)",
785
+ },
786
+ },
758
787
  String(props.value),
759
788
  ),
760
789
  ],
@@ -12,7 +12,7 @@ export const LEAF_MARGIN = "8px";
12
12
  export const CONTAINER_PADDING = "16px";
13
13
 
14
14
  /** Standard border for cards and inputs. */
15
- export const STANDARD_BORDER = "1px solid #ccc";
15
+ export const STANDARD_BORDER = "1px solid var(--a2ui-color-border, #ccc)";
16
16
 
17
17
  /** Standard border radius. */
18
18
  export const STANDARD_RADIUS = "8px";
@@ -7,6 +7,8 @@ import {
7
7
  } from "@copilotkit/shared";
8
8
  import type { Attachment } from "@copilotkit/shared";
9
9
  import type { CopilotChatAttachmentQueueProps } from "./types";
10
+ import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
11
+ import { CopilotChatDefaultLabels } from "../../providers/types";
10
12
 
11
13
  const props = withDefaults(defineProps<CopilotChatAttachmentQueueProps>(), {
12
14
  attachments: () => [],
@@ -17,6 +19,9 @@ const emit = defineEmits<{
17
19
  "remove-attachment": [id: string];
18
20
  }>();
19
21
 
22
+ const config = useCopilotChatConfiguration();
23
+ const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
24
+
20
25
  const activeLightboxAttachment = ref<Attachment | null>(null);
21
26
  const documentBlobUrl = ref<string | null>(null);
22
27
 
@@ -215,7 +220,7 @@ function createBlobUrl(
215
220
  <button
216
221
  type="button"
217
222
  class="cpk:absolute cpk:inset-0 cpk:flex cpk:items-center cpk:justify-center cpk:z-10 cpk:cursor-pointer cpk:bg-black/20 cpk:border-none cpk:p-0"
218
- aria-label="Play video"
223
+ :aria-label="labels.attachmentPlayVideoButtonLabel"
219
224
  data-testid="copilot-chat-attachment-video-play"
220
225
  @click="openLightbox(attachment)"
221
226
  >
@@ -294,7 +299,7 @@ function createBlobUrl(
294
299
  <button
295
300
  type="button"
296
301
  class="cpk:absolute cpk:bg-black/60 cpk:text-white cpk:border-none cpk:rounded-full cpk:w-5 cpk:h-5 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:text-[10px] cpk:z-20 cpk:top-1 cpk:right-1"
297
- aria-label="Remove attachment"
302
+ :aria-label="labels.attachmentRemoveButtonLabel"
298
303
  @click="emit('remove-attachment', attachment.id)"
299
304
  >
300
305
  ✕
@@ -312,7 +317,7 @@ function createBlobUrl(
312
317
  <button
313
318
  type="button"
314
319
  class="cpk:absolute cpk:top-4 cpk:right-4 cpk:text-white cpk:bg-white/10 cpk:hover:bg-white/20 cpk:rounded-full cpk:w-10 cpk:h-10 cpk:flex cpk:items-center cpk:justify-center cpk:cursor-pointer cpk:border-none"
315
- aria-label="Close preview"
320
+ :aria-label="labels.attachmentPreviewCloseButtonLabel"
316
321
  @click.stop="closeLightbox"
317
322
  >
318
323
  ✕
@@ -1118,7 +1118,7 @@ onBeforeUnmount(() => {
1118
1118
  ref="slashMenuRef"
1119
1119
  data-testid="copilot-slash-menu"
1120
1120
  role="listbox"
1121
- aria-label="Slash commands"
1121
+ :aria-label="labels.chatInputSlashMenuLabel"
1122
1122
  class="cpk:absolute cpk:bottom-full cpk:left-0 cpk:right-0 cpk:z-30 cpk:mb-2 cpk:max-h-64 cpk:overflow-y-auto cpk:rounded-lg cpk:border cpk:border-border cpk:bg-white cpk:shadow-lg cpk:dark:border-[#3a3a3a] cpk:dark:bg-[#1f1f1f]"
1123
1123
  :style="{ maxHeight: `${5 * 40}px` }"
1124
1124
  >
@@ -1126,7 +1126,7 @@ onBeforeUnmount(() => {
1126
1126
  v-if="filteredCommands.length === 0"
1127
1127
  class="cpk:px-3 cpk:py-2 cpk:text-sm cpk:text-muted-foreground"
1128
1128
  >
1129
- No commands found
1129
+ {{ labels.chatInputSlashMenuEmptyText }}
1130
1130
  </div>
1131
1131
  <button
1132
1132
  v-for="(command, index) in filteredCommands"
@@ -3,6 +3,8 @@ import { computed, onBeforeUnmount, ref, watch } from "vue";
3
3
  import type { Message, ReasoningMessage } from "@ag-ui/core";
4
4
  import { StreamMarkdown } from "streamdown-vue";
5
5
  import { IconChevronRight } from "../icons";
6
+ import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
7
+ import { CopilotChatDefaultLabels } from "../../providers/types";
6
8
 
7
9
  const props = withDefaults(
8
10
  defineProps<{
@@ -26,6 +28,7 @@ defineSlots<{
26
28
  label: string;
27
29
  hasContent: boolean;
28
30
  isStreaming: boolean;
31
+ elapsedSeconds: number;
29
32
  onClick?: () => void;
30
33
  };
31
34
  contentView: {
@@ -47,6 +50,8 @@ defineSlots<{
47
50
  label: string;
48
51
  hasContent: boolean;
49
52
  isStreaming: boolean;
53
+ /** Seconds spent reasoning so far, for building a localized label. */
54
+ elapsedSeconds: number;
50
55
  onClick?: () => void;
51
56
  }) => unknown;
52
57
  "content-view"?: (props: {
@@ -128,9 +133,12 @@ onBeforeUnmount(() => {
128
133
  clearElapsedInterval();
129
134
  });
130
135
 
136
+ const config = useCopilotChatConfiguration();
137
+ /** `labels.reasoningMessageThinkingLabel` while streaming, then "Thought for …". */
131
138
  const label = computed(() =>
132
139
  isStreaming.value
133
- ? "Thinking…"
140
+ ? (config.value?.labels ?? CopilotChatDefaultLabels)
141
+ .reasoningMessageThinkingLabel
134
142
  : `Thought for ${formatDuration(elapsed.value)}`,
135
143
  );
136
144
 
@@ -153,6 +161,7 @@ function toggleOpen() {
153
161
  label,
154
162
  hasContent,
155
163
  isStreaming,
164
+ elapsedSeconds: elapsed,
156
165
  onClick: hasContent ? toggleOpen : undefined,
157
166
  }"
158
167
  :content-view="{
@@ -175,6 +184,7 @@ function toggleOpen() {
175
184
  :label="label"
176
185
  :has-content="hasContent"
177
186
  :is-streaming="isStreaming"
187
+ :elapsed-seconds="elapsed"
178
188
  :on-click="hasContent ? toggleOpen : undefined"
179
189
  >
180
190
  <button
@@ -107,7 +107,10 @@ function toggleDrawer() {
107
107
  data-testid="drawer-launcher"
108
108
  class="cpk:inline-flex cpk:size-8 cpk:items-center cpk:justify-center cpk:rounded-full cpk:text-muted-foreground cpk:transition cpk:cursor-pointer cpk:hover:bg-muted cpk:hover:text-foreground cpk:focus-visible:outline-none cpk:focus-visible:ring-2 cpk:focus-visible:ring-ring"
109
109
  :aria-expanded="config?.drawerOpen ?? false"
110
- aria-label="Open threads"
110
+ :aria-label="
111
+ config?.labels.modalHeaderThreadsButtonLabel ??
112
+ CopilotChatDefaultLabels.modalHeaderThreadsButtonLabel
113
+ "
111
114
  @click="toggleDrawer"
112
115
  >
113
116
  <IconPanelLeftOpen class="cpk:h-4 cpk:w-4" aria-hidden="true" />
@@ -1,13 +1,23 @@
1
1
  import { computed, defineComponent, h, useAttrs } from "vue";
2
2
  import { IconX } from "../icons";
3
+ import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
4
+ import { CopilotChatDefaultLabels } from "../../providers/types";
3
5
 
6
+ /**
7
+ * Closes the popup or sidebar. Its accessible name is the `aria-label` the
8
+ * parent passes, or `labels.modalHeaderCloseButtonLabel`.
9
+ */
4
10
  export default defineComponent({
5
11
  name: "CopilotModalHeaderCloseButton",
6
12
  inheritAttrs: false,
7
13
  setup(_, { slots }) {
8
14
  const attrs = useAttrs();
15
+ const config = useCopilotChatConfiguration();
9
16
  const ariaLabel = computed(() =>
10
- typeof attrs["aria-label"] === "string" ? attrs["aria-label"] : "Close",
17
+ typeof attrs["aria-label"] === "string"
18
+ ? attrs["aria-label"]
19
+ : (config.value?.labels ?? CopilotChatDefaultLabels)
20
+ .modalHeaderCloseButtonLabel,
11
21
  );
12
22
 
13
23
  return () => {
@@ -277,7 +277,9 @@ onBeforeUnmount(() => {
277
277
  data-copilot-sidebar
278
278
  :class="asideClass"
279
279
  :style="asideStyle"
280
- aria-label="Copilot chat sidebar"
280
+ :aria-label="
281
+ config?.labels.sidebarLabel ?? CopilotChatDefaultLabels.sidebarLabel
282
+ "
281
283
  :aria-hidden="!isSidebarOpen"
282
284
  role="complementary"
283
285
  v-bind="asideAttrs"
@@ -0,0 +1,198 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { defineComponent, nextTick, onMounted } from "vue";
3
+ import { fireEvent, render, screen } from "@testing-library/vue";
4
+ import type { Message, ReasoningMessage } from "@ag-ui/core";
5
+ import type { Attachment } from "@copilotkit/shared";
6
+ import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
7
+ import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
8
+ import { useCopilotChatConfiguration } from "../../../providers/useCopilotChatConfiguration";
9
+ import type { CopilotChatLabels } from "../../../providers/types";
10
+ import CopilotChatReasoningMessage from "../CopilotChatReasoningMessage.vue";
11
+ import CopilotChatAttachmentQueue from "../CopilotChatAttachmentQueue.vue";
12
+ import CopilotChatInput from "../CopilotChatInput.vue";
13
+ import { CopilotModalHeader, CopilotSidebarView } from "../index";
14
+
15
+ // Strings an app translates through `labels`. Every one of them used to be
16
+ // hard-coded English.
17
+ const germanLabels: Partial<Record<keyof CopilotChatLabels, string>> = {
18
+ reasoningMessageThinkingLabel: "Denkt nach…",
19
+ chatInputSlashMenuLabel: "Befehle",
20
+ chatInputSlashMenuEmptyText: "Keine Befehle gefunden",
21
+ attachmentRemoveButtonLabel: "Anhang entfernen",
22
+ attachmentPlayVideoButtonLabel: "Video abspielen",
23
+ attachmentPreviewCloseButtonLabel: "Vorschau schließen",
24
+ modalHeaderCloseButtonLabel: "Schließen",
25
+ modalHeaderThreadsButtonLabel: "Unterhaltungen öffnen",
26
+ sidebarLabel: "Chat-Seitenleiste",
27
+ };
28
+
29
+ const reasoning = {
30
+ id: "reasoning-1",
31
+ role: "reasoning",
32
+ content: "",
33
+ } as ReasoningMessage;
34
+
35
+ const video: Attachment = {
36
+ id: "clip",
37
+ type: "video",
38
+ source: { type: "data", value: "AAAA", mimeType: "video/mp4" },
39
+ filename: "clip.mp4",
40
+ status: "ready",
41
+ };
42
+
43
+ /** Registers a threads drawer, which the header needs before it shows its launcher. */
44
+ const DrawerRegistrar = defineComponent({
45
+ setup() {
46
+ const config = useCopilotChatConfiguration();
47
+ onMounted(() => config.value?.registerDrawer());
48
+ },
49
+ template: `<span />`,
50
+ });
51
+
52
+ /** Renders `template` inside the providers, with the German labels configured. */
53
+ function renderWithLabels(template: string) {
54
+ const Host = defineComponent({
55
+ components: {
56
+ CopilotKitProvider,
57
+ CopilotChatConfigurationProvider,
58
+ CopilotChatReasoningMessage,
59
+ CopilotChatAttachmentQueue,
60
+ CopilotChatInput,
61
+ CopilotModalHeader,
62
+ CopilotSidebarView,
63
+ DrawerRegistrar,
64
+ },
65
+ setup() {
66
+ return {
67
+ labels: germanLabels,
68
+ reasoning,
69
+ reasoningMessages: [reasoning] as Message[],
70
+ attachments: [video],
71
+ toolsMenu: [{ label: "Say hi", action: vi.fn() }],
72
+ };
73
+ },
74
+ template: `
75
+ <CopilotKitProvider runtime-url="/api/copilotkit">
76
+ <CopilotChatConfigurationProvider thread-id="thread-1" :labels="labels">
77
+ ${template}
78
+ </CopilotChatConfigurationProvider>
79
+ </CopilotKitProvider>
80
+ `,
81
+ });
82
+ return render(Host);
83
+ }
84
+
85
+ describe("chat strings from labels", () => {
86
+ beforeEach(() => {
87
+ vi.stubGlobal(
88
+ "ResizeObserver",
89
+ class {
90
+ observe() {}
91
+ unobserve() {}
92
+ disconnect() {}
93
+ },
94
+ );
95
+ });
96
+
97
+ afterEach(() => {
98
+ vi.useRealTimers();
99
+ vi.unstubAllGlobals();
100
+ });
101
+
102
+ it("uses the configured reasoning label while the model reasons", () => {
103
+ renderWithLabels(`
104
+ <CopilotChatReasoningMessage
105
+ :message="reasoning"
106
+ :messages="reasoningMessages"
107
+ :is-running="true"
108
+ />
109
+ `);
110
+
111
+ expect(screen.getByText("Denkt nach…")).toBeDefined();
112
+ expect(screen.queryByText("Thinking…")).toBeNull();
113
+ });
114
+
115
+ it("passes the elapsed reasoning time to a custom header", async () => {
116
+ vi.useFakeTimers();
117
+ renderWithLabels(`
118
+ <CopilotChatReasoningMessage
119
+ :message="reasoning"
120
+ :messages="reasoningMessages"
121
+ :is-running="true"
122
+ >
123
+ <template #header="{ elapsedSeconds }">
124
+ <span>{{ Math.round(elapsedSeconds) }} Sekunden</span>
125
+ </template>
126
+ </CopilotChatReasoningMessage>
127
+ `);
128
+
129
+ expect(screen.getByText("0 Sekunden")).toBeDefined();
130
+ vi.advanceTimersByTime(3000);
131
+ await nextTick();
132
+ expect(screen.getByText("3 Sekunden")).toBeDefined();
133
+ });
134
+
135
+ it("names the slash-command menu and its empty state", async () => {
136
+ renderWithLabels(`<CopilotChatInput :tools-menu="toolsMenu" />`);
137
+
138
+ await fireEvent.input(screen.getByRole("textbox"), {
139
+ target: { value: "/zzz" },
140
+ });
141
+
142
+ expect(
143
+ await screen.findByRole("listbox", { name: "Befehle" }),
144
+ ).toBeDefined();
145
+ expect(screen.getByText("Keine Befehle gefunden")).toBeDefined();
146
+ });
147
+
148
+ it("names the attachment queue buttons and the preview close button", async () => {
149
+ renderWithLabels(
150
+ `<CopilotChatAttachmentQueue :attachments="attachments" />`,
151
+ );
152
+
153
+ expect(
154
+ screen.getByRole("button", { name: "Anhang entfernen" }),
155
+ ).toBeDefined();
156
+ await fireEvent.click(
157
+ screen.getByRole("button", { name: "Video abspielen" }),
158
+ );
159
+ expect(
160
+ await screen.findByRole("button", { name: "Vorschau schließen" }),
161
+ ).toBeDefined();
162
+ });
163
+
164
+ it("names the modal header close and threads buttons", async () => {
165
+ // A mobile viewport: the threads launcher only renders there.
166
+ vi.stubGlobal(
167
+ "matchMedia",
168
+ vi.fn().mockReturnValue({
169
+ matches: true,
170
+ media: "(max-width: 767px)",
171
+ onchange: null,
172
+ addEventListener: vi.fn(),
173
+ removeEventListener: vi.fn(),
174
+ addListener: vi.fn(),
175
+ removeListener: vi.fn(),
176
+ dispatchEvent: vi.fn(),
177
+ }),
178
+ );
179
+ renderWithLabels(`<CopilotModalHeader /><DrawerRegistrar />`);
180
+
181
+ expect(
182
+ await screen.findByRole("button", { name: "Unterhaltungen öffnen" }),
183
+ ).toBeDefined();
184
+ expect(screen.getByRole("button", { name: "Schließen" })).toBeDefined();
185
+ });
186
+
187
+ it("names the sidebar", () => {
188
+ renderWithLabels(`<CopilotSidebarView />`);
189
+
190
+ // The sidebar starts closed, so it is aria-hidden until opened.
191
+ expect(
192
+ screen.getByRole("complementary", {
193
+ name: "Chat-Seitenleiste",
194
+ hidden: true,
195
+ }),
196
+ ).toBeDefined();
197
+ });
198
+ });
@@ -90,6 +90,21 @@ export interface CopilotKitProviderProps {
90
90
  theme?: A2UITheme;
91
91
  catalog?: any;
92
92
  loadingComponent?: Component;
93
+ /**
94
+ * When true (the default), the full component schemas from the catalog are
95
+ * sent as agent context so the agent knows what components and props are
96
+ * available. The A2UI middleware can overwrite this with a server-side
97
+ * schema if configured.
98
+ *
99
+ * When false, the agent context still describes the catalog, but only by its
100
+ * catalog ID and the names of its custom components, without their JSON
101
+ * schemas. The component schema entry and the A2UI generation and design
102
+ * guidelines are not sent. With the CopilotKit LangGraph middleware's
103
+ * injected A2UI tool, the A2UI subagent then gets the component names only;
104
+ * a host can still give it a composition guide through the middleware's
105
+ * guidelines (`a2uiParams.guidelines.compositionGuide` in TypeScript,
106
+ * `a2ui_params["guidelines"]["composition_guide"]` in Python).
107
+ */
93
108
  includeSchema?: boolean;
94
109
  };
95
110
  /** Configuration for the built-in MCP Apps renderer. */