@copilotkit/vue 2.0.0-rc.26 → 2.0.0-rc.27

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 (64) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.mjs +2 -2
  3. package/dist/use-render-activity-message-C7b4K-TN.cjs +113 -0
  4. package/dist/use-render-activity-message-C7b4K-TN.cjs.map +1 -0
  5. package/dist/{use-render-activity-message-BVZr0Z-G.js → use-render-activity-message-Dpqx4DE8.js} +5686 -5983
  6. package/dist/use-render-activity-message-Dpqx4DE8.js.map +1 -0
  7. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts +3 -6
  8. package/dist/v2/components/a2ui/A2UIBuiltInToolCallRenderer.d.ts.map +1 -1
  9. package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
  10. package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
  11. package/dist/v2/components/chat/CopilotChatFeedbackCommentPopover.vue.d.ts.map +1 -1
  12. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts +1 -16
  13. package/dist/v2/components/chat/CopilotChatInput.vue.d.ts.map +1 -1
  14. package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
  15. package/dist/v2/components/chat/CopilotChatReasoningMessage.vue.d.ts.map +1 -1
  16. package/dist/v2/components/chat/CopilotChatView.vue.d.ts.map +1 -1
  17. package/dist/v2/components/chat/CopilotPopupViewInternal.vue.d.ts.map +1 -1
  18. package/dist/v2/components/chat/CopilotSidebarViewInternal.vue.d.ts.map +1 -1
  19. package/dist/v2/components/chat/CopilotThreadsDrawer.vue.d.ts.map +1 -1
  20. package/dist/v2/components/chat/message-feedback.d.ts +20 -0
  21. package/dist/v2/components/chat/message-feedback.d.ts.map +1 -1
  22. package/dist/v2/hooks/use-configure-suggestions.d.ts.map +1 -1
  23. package/dist/v2/hooks/use-default-render-tool.d.ts +2 -7
  24. package/dist/v2/hooks/use-default-render-tool.d.ts.map +1 -1
  25. package/dist/v2/hooks/use-frontend-tool.d.ts +2 -2
  26. package/dist/v2/hooks/use-frontend-tool.d.ts.map +1 -1
  27. package/dist/v2/hooks/use-render-activity-message.d.ts.map +1 -1
  28. package/dist/v2/hooks/use-suggestions.d.ts.map +1 -1
  29. package/dist/v2/hooks/use-threads.d.ts +6 -15
  30. package/dist/v2/hooks/use-threads.d.ts.map +1 -1
  31. package/dist/v2/index.cjs +1 -1
  32. package/dist/v2/index.mjs +1 -1
  33. package/dist/v2/lib/vue-core.d.ts +16 -2
  34. package/dist/v2/lib/vue-core.d.ts.map +1 -1
  35. package/dist/v2/providers/CopilotKitProvider.vue.d.ts.map +1 -1
  36. package/package.json +8 -8
  37. package/src/__tests__/build-entries.test.ts +3 -1
  38. package/src/v2/components/a2ui/A2UIBuiltInToolCallRenderer.ts +4 -19
  39. package/src/v2/components/chat/CopilotChat.vue +50 -218
  40. package/src/v2/components/chat/CopilotChatAssistantMessage.vue +28 -141
  41. package/src/v2/components/chat/CopilotChatFeedbackCommentPopover.vue +11 -76
  42. package/src/v2/components/chat/CopilotChatInput.vue +82 -269
  43. package/src/v2/components/chat/CopilotChatMessageView.vue +14 -64
  44. package/src/v2/components/chat/CopilotChatReasoningMessage.vue +25 -52
  45. package/src/v2/components/chat/CopilotChatView.vue +92 -135
  46. package/src/v2/components/chat/CopilotPopupViewInternal.vue +18 -58
  47. package/src/v2/components/chat/CopilotSidebarViewInternal.vue +22 -45
  48. package/src/v2/components/chat/CopilotThreadsDrawer.vue +6 -3
  49. package/src/v2/components/chat/__tests__/CopilotChatInput.test.ts +43 -0
  50. package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.ts +43 -1
  51. package/src/v2/components/chat/message-feedback.ts +33 -1
  52. package/src/v2/hooks/use-configure-suggestions.ts +3 -4
  53. package/src/v2/hooks/use-default-render-tool.ts +7 -68
  54. package/src/v2/hooks/use-frontend-tool.ts +10 -20
  55. package/src/v2/hooks/use-render-activity-message.ts +4 -2
  56. package/src/v2/hooks/use-suggestions.ts +9 -4
  57. package/src/v2/hooks/use-threads.ts +6 -15
  58. package/src/v2/lib/vue-core.ts +19 -2
  59. package/src/v2/providers/CopilotKitProvider.vue +107 -184
  60. package/src/v2/providers/__tests__/CopilotKitProvider.a2ui.test.ts +18 -0
  61. package/src/v2/providers/__tests__/CopilotKitProvider.test.ts +35 -2
  62. package/dist/use-render-activity-message-8p2HwK2y.cjs +0 -114
  63. package/dist/use-render-activity-message-8p2HwK2y.cjs.map +0 -1
  64. package/dist/use-render-activity-message-BVZr0Z-G.js.map +0 -1
@@ -1,6 +1,4 @@
1
1
  <script setup lang="ts">
2
- import { AGUIConnectNotImplementedError, AbstractAgent } from "@ag-ui/client";
3
- import { DEFAULT_AGENT_ID, randomUUID } from "@copilotkit/shared";
4
2
  import {
5
3
  computed,
6
4
  onBeforeUnmount,
@@ -14,14 +12,15 @@ import {
14
12
  } from "vue";
15
13
  import {
16
14
  CopilotChatDefaultLabels,
17
- CopilotKitCoreErrorCode,
15
+ ɵChatConnection,
18
16
  ɵcreateChatSubmitter,
19
17
  ɵcreateLastUserMessageTracker,
20
- ɵisHttpAgent,
18
+ ɵFreshThreadId,
19
+ ɵresolveAgentId,
21
20
  ɵresolveChatNotice,
22
- ɵThreadSwitchCoordinator,
21
+ ɵsubscribeToAgentErrors,
23
22
  } from "@copilotkit/core";
24
- import type { Suggestion, ɵThreadSelection } from "@copilotkit/core";
23
+ import type { Suggestion } from "@copilotkit/core";
25
24
  import CopilotChatConfigurationProvider from "../../providers/CopilotChatConfigurationProvider.vue";
26
25
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
27
26
  import { useCopilotKit } from "../../providers/useCopilotKit";
@@ -92,29 +91,35 @@ const forwardedChatViewSlotNames = computed(() =>
92
91
  Object.keys(componentSlots).filter((slotName) => slotName !== "chat-view"),
93
92
  );
94
93
 
95
- const generatedThreadId = ref(randomUUID());
96
94
  const transcribeMode = ref<"input" | "transcribe" | "processing">("input");
97
95
  const inputValue = ref(props.inputValue ?? "");
98
96
  const isMounted = ref(false);
99
- const isUnmounting = ref(false);
100
-
101
- type ActiveConnectCycle = {
102
- core: object;
103
- selection: ɵThreadSelection;
104
- abortController: AbortController;
105
- detached: boolean;
106
- };
107
-
108
- const activeConnectCycle = shallowRef<ActiveConnectCycle | null>(null);
109
- // Keeps the agent on the thread this chat shows, orders the teardown of the
110
- // previous thread, and tracks connections a send must wait for.
111
- const threads = new ɵThreadSwitchCoordinator({
112
- stopAgent: (params) => copilotkit.value.stopAgent(params),
113
- subscribe: (subscriber) => copilotkit.value.subscribe(subscriber),
114
- });
115
97
 
116
- const resolvedAgentId = computed(
117
- () => props.agentId ?? existingConfig.value?.agentId ?? DEFAULT_AGENT_ID,
98
+ // Loads the thread this chat shows: keeps the agent on it, orders the
99
+ // teardown of the previous thread, owns the connect's abort and detach, and
100
+ // tracks connections a send must wait for.
101
+ const connection = new ɵChatConnection({
102
+ core: {
103
+ connectAgent: (params) => copilotkit.value.connectAgent(params),
104
+ stopAgent: (params) => copilotkit.value.stopAgent(params),
105
+ subscribe: (subscriber) => copilotkit.value.subscribe(subscriber),
106
+ },
107
+ onInspectorOverrideFailed: (requestId) => failInspectorOverride(requestId),
108
+ });
109
+ const { threads } = connection;
110
+ const connectionState = shallowRef(connection.getSnapshot());
111
+ connection.subscribe(() => {
112
+ connectionState.value = connection.getSnapshot();
113
+ });
114
+ // The locally minted thread used while no thread is provided. A new one is
115
+ // minted each time the provided thread goes away.
116
+ const freshThreadId = new ɵFreshThreadId();
117
+
118
+ const resolvedAgentId = computed(() =>
119
+ ɵresolveAgentId({
120
+ agentId: props.agentId,
121
+ chatAgentId: existingConfig.value?.agentId,
122
+ }),
118
123
  );
119
124
  // "Provided" — supplied by either the caller's prop or a wrapping
120
125
  // CopilotChatConfigurationProvider. Distinct from `resolvedThreadId`, which
@@ -124,8 +129,8 @@ const resolvedAgentId = computed(
124
129
  const providedThreadId = computed(
125
130
  () => props.threadId ?? existingConfig.value?.threadId,
126
131
  );
127
- const baseThreadId = computed(
128
- () => providedThreadId.value ?? generatedThreadId.value,
132
+ const baseThreadId = computed(() =>
133
+ freshThreadId.resolve(providedThreadId.value),
129
134
  );
130
135
  // "Explicit" means the caller actually picked this thread — via the
131
136
  // `threadId` prop on CopilotChat or a wrapping provider that flagged its
@@ -145,12 +150,14 @@ const resolvedThreadId = computed(
145
150
  const hasExplicitThreadId = computed(
146
151
  () => inspectorThreadId.value !== null || baseHasExplicitThreadId.value,
147
152
  );
148
- const lastConnectedThreadId = ref<string | null>(null);
149
- const isConnecting = computed(
150
- () =>
151
- hasExplicitThreadId.value &&
152
- lastConnectedThreadId.value !== resolvedThreadId.value,
153
- );
153
+ // An explicit thread shows as connecting until its connect has settled.
154
+ const isConnecting = computed(() => {
155
+ void connectionState.value;
156
+ return connection.isConnecting({
157
+ threadId: resolvedThreadId.value,
158
+ explicit: hasExplicitThreadId.value,
159
+ });
160
+ });
154
161
  const stableLabels = useShallowStableRef(computed(() => props.labels));
155
162
  const resolvedLabels = computed(() => stableLabels.value);
156
163
  // The labels this chat shows, the same merge its configuration provider makes.
@@ -263,61 +270,13 @@ watch(
263
270
  },
264
271
  );
265
272
 
266
- watch(
267
- [() => props.threadId, () => existingConfig.value?.threadId],
268
- ([threadId, inheritedThreadId], previousValues) => {
269
- if (threadId || inheritedThreadId) {
270
- return;
271
- }
272
-
273
- if (previousValues && (previousValues[0] || previousValues[1])) {
274
- generatedThreadId.value = randomUUID();
275
- }
276
- },
277
- );
278
-
279
273
  watch(
280
274
  [() => copilotkit.value, resolvedAgentId, () => props.onError],
281
275
  ([core, agentId, onError], _old, onCleanup) => {
282
276
  if (!onError) {
283
277
  return;
284
278
  }
285
-
286
- const subscription = core.subscribe({
287
- onError: (event) => {
288
- if (event.context?.agentId === agentId || !event.context?.agentId) {
289
- void onError({
290
- error: event.error,
291
- code: event.code,
292
- context: event.context,
293
- });
294
- }
295
- },
296
- });
297
-
298
- onCleanup(() => subscription.unsubscribe());
299
- },
300
- { immediate: true },
301
- );
302
-
303
- watch(
304
- [
305
- () => copilotkit.value,
306
- resolvedAgentId,
307
- resolvedThreadId,
308
- inspectorRequestId,
309
- ],
310
- ([core, agentId, threadId, requestId], _old, onCleanup) => {
311
- if (!requestId) return;
312
- const subscription = core.subscribe({
313
- onError: (event) => {
314
- if (event.code !== CopilotKitCoreErrorCode.AGENT_CONNECT_FAILED) return;
315
- if (event.context?.agentId !== agentId) return;
316
- if (event.context?.threadId !== threadId) return;
317
- failInspectorOverride(requestId);
318
- },
319
- });
320
- onCleanup(() => subscription.unsubscribe());
279
+ onCleanup(ɵsubscribeToAgentErrors(core, agentId, onError));
321
280
  },
322
281
  { immediate: true },
323
282
  );
@@ -332,150 +291,24 @@ watch(
332
291
  inspectorRequestId,
333
292
  ],
334
293
  (
335
- [mounted, core, currentAgent, threadId, isExplicit, requestId],
294
+ [mounted, _core, currentAgent, threadId, isExplicit, requestId],
336
295
  _old,
337
296
  onCleanup,
338
297
  ) => {
339
- if (!mounted) {
340
- return;
341
- }
342
- if (typeof window === "undefined") {
343
- return;
344
- }
345
- if (!currentAgent) {
298
+ if (!mounted || typeof window === "undefined" || !currentAgent) {
346
299
  return;
347
300
  }
348
-
349
- // Pins the thread on the agent before anything issues a request:
350
- // `CopilotKitCore.connectAgent` reads `agent.threadId` synchronously. A
351
- // switch to a fresh, non-explicit thread (a "+ New" reset) empties the
352
- // agent so the welcome screen shows; moving in or out of an inspector
353
- // override stops the run.
354
- const selection = threads.select({
301
+ // Pins the thread on the agent before anything issues a request, and
302
+ // connects a thread the caller picked. A switch to a fresh thread (a
303
+ // "+ New" reset) empties the agent so the welcome screen shows; moving in
304
+ // or out of an Inspector override stops the run.
305
+ const handle = connection.open({
355
306
  agent: currentAgent,
356
307
  threadId,
357
308
  explicit: isExplicit,
358
309
  inspectorRequestId: requestId,
359
310
  });
360
-
361
- // When the caller hasn't picked a specific thread, resolvedThreadId is
362
- // a UUID minted locally. The backend has never seen it, so /connect
363
- // would always 404 — skip the call. A real thread is only created
364
- // once the user runs the agent for the first time.
365
- if (!selection.shouldConnect) {
366
- return;
367
- }
368
-
369
- const abstractAgentPrototype = AbstractAgent.prototype as unknown as {
370
- connect?: unknown;
371
- connectAgent?: unknown;
372
- };
373
- const inspectableAgent = currentAgent as unknown as {
374
- connect?: unknown;
375
- connectAgent?: unknown;
376
- };
377
- const hasCustomConnect =
378
- inspectableAgent.connect !== abstractAgentPrototype.connect;
379
- const hasCustomConnectAgent =
380
- inspectableAgent.connectAgent !== abstractAgentPrototype.connectAgent;
381
- if (!hasCustomConnect && !hasCustomConnectAgent) {
382
- // No custom connect to wait for — mark as connected so the welcome
383
- // suppression / suggestion gating release immediately.
384
- lastConnectedThreadId.value = threadId;
385
- return;
386
- }
387
-
388
- const existingCycle = activeConnectCycle.value;
389
- const hasSameDeps =
390
- existingCycle &&
391
- existingCycle.core === (core as object) &&
392
- existingCycle.selection === selection;
393
-
394
- let cycle: ActiveConnectCycle;
395
- if (hasSameDeps && existingCycle) {
396
- cycle = existingCycle;
397
- } else {
398
- const connectAbortController = new AbortController();
399
- cycle = {
400
- core: core as object,
401
- selection,
402
- abortController: connectAbortController,
403
- detached: false,
404
- };
405
- activeConnectCycle.value = cycle;
406
-
407
- // Tracked, so a send waits for the thread to load instead of racing it.
408
- void threads.trackConnection(selection, () =>
409
- threads
410
- .whenTornDown()
411
- .then(() => {
412
- if (cycle.detached) return;
413
- if (ɵisHttpAgent(currentAgent))
414
- currentAgent.abortController = connectAbortController;
415
- return core.connectAgent({ agent: currentAgent });
416
- })
417
- .catch((error: unknown) => {
418
- if (cycle.detached) {
419
- selection.clearDiscardedBaseline();
420
- return;
421
- }
422
- if (error instanceof AGUIConnectNotImplementedError) {
423
- return;
424
- }
425
- if (requestId) {
426
- failInspectorOverride(requestId);
427
- }
428
- // The coordinator logs the failure and blocks sends on it.
429
- throw error;
430
- })
431
- .finally(() => {
432
- // Whether the connect succeeded or failed, we're no longer in
433
- // the transitional "connecting" state for this thread — release
434
- // the welcome / suggestion gating so the view can settle.
435
- //
436
- // Defer one animation frame so any trailing reactive commits
437
- // from the bootstrap replay (final assistant message content)
438
- // paint before isConnecting flips off. Without this, suggestions
439
- // can briefly render against an incompletely-laid-out message
440
- // tree and visibly snap once the last text chunk lands.
441
- if (cycle.detached) {
442
- selection.clearDiscardedBaseline();
443
- return;
444
- }
445
- const raf =
446
- typeof requestAnimationFrame === "function"
447
- ? requestAnimationFrame
448
- : (cb: () => void) => setTimeout(cb, 16);
449
- raf(() => {
450
- if (cycle.detached) return;
451
- lastConnectedThreadId.value = selection.threadId;
452
- });
453
- }),
454
- );
455
- }
456
-
457
- onCleanup(() => {
458
- const activeCycle = activeConnectCycle.value;
459
- if (!activeCycle || activeCycle !== cycle) {
460
- return;
461
- }
462
-
463
- const { selection: cycleSelection } = activeCycle;
464
- const shouldDetach =
465
- isUnmounting.value ||
466
- copilotkit.value !== activeCycle.core ||
467
- agent.value !== cycleSelection.agent ||
468
- resolvedThreadId.value !== cycleSelection.threadId;
469
- if (!shouldDetach) {
470
- return;
471
- }
472
-
473
- cycleSelection.release();
474
- activeCycle.detached = true;
475
- activeCycle.abortController.abort();
476
- void threads.detach(cycleSelection.agent);
477
- activeConnectCycle.value = null;
478
- });
311
+ onCleanup(() => handle.release());
479
312
  },
480
313
  { immediate: true },
481
314
  );
@@ -522,10 +355,9 @@ const transcriptionError = computed(() => {
522
355
  });
523
356
 
524
357
  onBeforeUnmount(() => {
525
- isUnmounting.value = true;
526
358
  unsubscribeSubmitter();
527
359
  submitter.dispose();
528
- threads.dispose();
360
+ connection.dispose();
529
361
  });
530
362
 
531
363
  onMounted(() => {
@@ -1,13 +1,5 @@
1
1
  <script setup lang="ts">
2
- import {
3
- computed,
4
- inject,
5
- onUpdated,
6
- ref,
7
- shallowRef,
8
- toRef,
9
- watch,
10
- } from "vue";
2
+ import { computed, inject, onUpdated, ref, toRef, watch } from "vue";
11
3
  import type { Component } from "vue";
12
4
  import type { AssistantMessage, Message } from "@ag-ui/core";
13
5
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
@@ -34,6 +26,7 @@ import { useInspectorShortcutsHidden } from "./inspector-shortcuts";
34
26
  import { InspectorKey } from "../../providers/keys";
35
27
  import { useFeedbackComment, useMessageFeedback } from "./message-feedback";
36
28
  import {
29
+ ɵflattenUserMessageContent,
37
30
  ɵhasAssistantMessageText,
38
31
  ɵshouldShowAssistantToolbar,
39
32
  } from "@copilotkit/core";
@@ -115,39 +108,9 @@ const config = useCopilotChatConfiguration();
115
108
  const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
116
109
  const { copied, copy } = useCopyFeedback();
117
110
 
118
- function normalizeContent(content: unknown): string {
119
- if (!content) {
120
- return "";
121
- }
122
-
123
- if (typeof content === "string") {
124
- return content;
125
- }
126
-
127
- if (Array.isArray(content)) {
128
- const parts = content as Array<{ type?: unknown; text?: unknown }>;
129
- return parts
130
- .map((part) => {
131
- if (
132
- part &&
133
- typeof part === "object" &&
134
- "type" in part &&
135
- part.type === "text" &&
136
- typeof part.text === "string"
137
- ) {
138
- return part.text;
139
- }
140
- return "";
141
- })
142
- .filter((text) => text.length > 0)
143
- .join("\n");
144
- }
145
-
146
- return "";
147
- }
148
-
111
+ // Assistant text, also from untyped data that sends content parts.
149
112
  const normalizedContent = computed(() =>
150
- normalizeContent(props.message.content),
113
+ ɵflattenUserMessageContent(props.message.content),
151
114
  );
152
115
  const hasContent = computed(() =>
153
116
  ɵhasAssistantMessageText(normalizedContent.value),
@@ -179,10 +142,6 @@ const feedback = useMessageFeedback(toRef(props, "message"));
179
142
  const comment = useFeedbackComment(feedback);
180
143
  const rating = computed(() => feedback.value?.feedback?.rating);
181
144
  const root = ref<HTMLElement | null>(null);
182
- // The chosen thumb the comment popover points at, and the rating the open
183
- // comment answers.
184
- const commentAnchor = shallowRef<HTMLElement | null>(null);
185
- let commentRating = rating.value;
186
145
 
187
146
  const hasThumbsUp = computed(
188
147
  () => typeof props.onThumbsUp === "function" || !!feedback.value,
@@ -207,106 +166,40 @@ const shouldShowToolbar = computed(() =>
207
166
 
208
167
  // A custom thumb that calls its handler without an event gives no element:
209
168
  // point at the chosen thumb once it renders, before the popover reads it.
210
- function findCommentAnchor() {
211
- if (
212
- comment.open.value &&
213
- !commentAnchor.value?.isConnected &&
214
- rating.value &&
215
- root.value
216
- ) {
217
- commentAnchor.value =
218
- root.value.querySelector<HTMLElement>(
219
- `[data-copilotkit-rating="${rating.value}"]`,
220
- ) ??
221
- // A custom thumb that binds only `aria-pressed`.
222
- root.value.querySelector<HTMLElement>('button[aria-pressed="true"]') ??
223
- null;
224
- }
225
- }
169
+ const findCommentAnchor = () => comment.findAnchor(root.value);
226
170
  onUpdated(findCommentAnchor);
227
171
  watch(() => comment.open.value, findCommentAnchor, { flush: "post" });
228
172
 
229
- /** Whether focus came from the keyboard; a pointer click also focuses buttons. */
230
- function isFocusVisible(element: Element): boolean {
231
- try {
232
- return element.matches(":focus-visible");
233
- } catch {
234
- return false;
235
- }
236
- }
237
-
238
- // Checks `visibility: hidden` too, which a detached popover uses.
239
- const HIDDEN_TOO = { checkVisibilityCSS: true, visibilityProperty: true };
240
-
241
173
  /**
242
- * Thumbs are toggles: a new rating is saved and opens its comment, and
243
- * clicking the chosen thumb again removes the rating. A keyboard click
244
- * (`detail` 0) moves focus into the comment, as there is no pointer. Custom
245
- * thumb slots may call the handler without an event.
246
- *
247
- * @returns Whether the app's handler should run: true for a new rating and
248
- * whenever this chat does not record ratings, false when one is removed.
174
+ * A thumb click. A keyboard click (`detail` 0) moves focus into the comment;
175
+ * custom thumb slots may call the handler without an event.
249
176
  */
250
177
  function rate(value: MessageFeedbackRating, event?: MouseEvent): boolean {
251
- const controls = feedback.value;
252
- if (!controls) return true;
253
- if (rating.value === value) {
254
- comment.close({ save: false });
255
- controls.clear();
256
- return false;
257
- }
258
- controls.rate(value);
259
- commentRating = value;
260
- commentAnchor.value =
261
- event?.currentTarget instanceof HTMLElement ? event.currentTarget : null;
262
- comment.show({ comment: "", focus: event?.detail === 0 });
263
- return true;
178
+ return comment.rate(value, {
179
+ target:
180
+ event?.currentTarget instanceof HTMLElement ? event.currentTarget : null,
181
+ keyboard: event?.detail === 0,
182
+ });
264
183
  }
265
184
 
266
185
  /** Attributes that let the chosen thumb open and point at its comment. */
267
186
  function thumbFeedbackProps(
268
187
  value: MessageFeedbackRating,
269
188
  ): Record<string, unknown> {
270
- if (!feedback.value) return {};
271
- const thumb = { "data-copilotkit-rating": value };
272
- if (rating.value !== value) return { ...thumb, "aria-pressed": false };
273
- const enter = (target: HTMLElement, via: "pointer" | "focus") => {
274
- if (!comment.open.value) commentRating = value;
275
- commentAnchor.value = target;
276
- comment.enter(via);
277
- };
189
+ // Read so the thumbs update when the rating or the popover changes.
190
+ void feedback.value;
191
+ void comment.open.value;
192
+ const thumb = comment.thumb(value);
193
+ if (!thumb) return {};
194
+ const { handlers } = thumb;
195
+ if (!handlers) return thumb.attributes;
278
196
  return {
279
- ...thumb,
280
- "aria-pressed": true,
281
- "aria-haspopup": "dialog",
282
- "aria-expanded": comment.open.value,
283
- onPointerenter: (event: PointerEvent) => {
284
- if (event.pointerType !== "touch")
285
- enter(event.currentTarget as HTMLElement, "pointer");
286
- },
287
- onPointerleave: (event: PointerEvent) => {
288
- if (event.pointerType !== "touch") comment.leave("pointer");
289
- },
290
- // Keyboard focus opens the comment; Tab moves on into its field.
291
- onFocus: (event: FocusEvent) => {
292
- const target = event.currentTarget as HTMLElement;
293
- if (isFocusVisible(target)) enter(target, "focus");
294
- },
295
- onBlur: () => comment.leave(),
296
- onKeydown: (event: KeyboardEvent) => {
297
- const input = comment.input.value;
298
- // Not while the popover hides because its thumb scrolled away.
299
- if (
300
- event.key === "Tab" &&
301
- !event.shiftKey &&
302
- comment.open.value &&
303
- input &&
304
- input.checkVisibility?.(HIDDEN_TOO) !== false
305
- ) {
306
- event.preventDefault();
307
- input.focus();
308
- }
309
- },
197
+ ...thumb.attributes,
198
+ onPointerenter: handlers.pointerEnter,
199
+ onPointerleave: handlers.pointerLeave,
200
+ onFocus: handlers.focus,
201
+ onBlur: handlers.blur,
202
+ onKeydown: handlers.keyDown,
310
203
  };
311
204
  }
312
205
 
@@ -315,14 +208,8 @@ const thumbsDownProps = computed(() => thumbFeedbackProps("down"));
315
208
 
316
209
  // The popover shows only with the toolbar and a rating. Hiding the toolbar
317
210
  // keeps the edit; a rating removed elsewhere or Learning turned off ends it.
318
- watch(
319
- [() => comment.open.value, shouldShowToolbar, rating],
320
- ([open, showToolbar, currentRating]) => {
321
- if (!open) return;
322
- // Changed or removed elsewhere: the draft answered the old rating.
323
- if (currentRating !== commentRating) comment.close({ save: false });
324
- else if (!showToolbar) comment.close();
325
- },
211
+ watch([() => comment.open.value, shouldShowToolbar, rating], () =>
212
+ comment.sync({ showsToolbar: shouldShowToolbar.value }),
326
213
  );
327
214
 
328
215
  function handleInspect() {
@@ -539,7 +426,7 @@ function handleRegenerate() {
539
426
  :comment="comment"
540
427
  :saved-comment="feedback.feedback.comment ?? ''"
541
428
  :rating="feedback.feedback.rating"
542
- :anchor="commentAnchor"
429
+ :anchor="comment.anchor.value"
543
430
  :message-id="message.id"
544
431
  >
545
432
  <template v-if="$slots['feedback-comment']" #feedback-comment="slotProps">