@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
@@ -12,7 +12,10 @@
12
12
  import { computed, nextTick, ref, watch } from "vue";
13
13
  import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
14
14
  import { CopilotChatDefaultLabels } from "../../providers/types";
15
- import { ɵMESSAGE_FEEDBACK_MAX_COMMENT_LENGTH as MESSAGE_FEEDBACK_MAX_COMMENT_LENGTH } from "@copilotkit/core";
15
+ import {
16
+ ɵMESSAGE_FEEDBACK_MAX_COMMENT_LENGTH as MESSAGE_FEEDBACK_MAX_COMMENT_LENGTH,
17
+ ɵisOwnFeedbackThumb,
18
+ } from "@copilotkit/core";
16
19
  import type { ɵMessageFeedbackRating as MessageFeedbackRating } from "@copilotkit/core";
17
20
  import CopilotPopover from "../ui/CopilotPopover.vue";
18
21
  import { IconArrowUp } from "../icons";
@@ -38,60 +41,13 @@ const labels = computed(() => config.value?.labels ?? CopilotChatDefaultLabels);
38
41
  const popover = ref<{ content: HTMLElement | null } | null>(null);
39
42
  const contentElement = () => popover.value?.content ?? null;
40
43
 
41
- // Elements Tab stops at.
42
- const FOCUSABLE = [
43
- "a[href]",
44
- "button:not([disabled])",
45
- "input:not([disabled])",
46
- "select:not([disabled])",
47
- "textarea:not([disabled])",
48
- "[tabindex]",
49
- ]
50
- .map((selector) => `${selector}:not([tabindex="-1"])`)
51
- .join(", ");
52
-
53
- /** Approximately the element Tab reaches after `element`, outside `skip`. */
54
- function nextFocusable(
55
- element: Element,
56
- skip: Element | null,
57
- ): HTMLElement | null {
58
- const all = Array.from(
59
- element.ownerDocument.querySelectorAll<HTMLElement>(FOCUSABLE),
60
- ).filter(
61
- (candidate) =>
62
- candidate === element ||
63
- (!skip?.contains(candidate) && !candidate.closest("[hidden], [inert]")),
64
- );
65
- const index = all.indexOf(element as HTMLElement);
66
- return index === -1 ? null : (all[index + 1] ?? null);
67
- }
68
-
69
- const focusInside = () =>
70
- contentElement()?.contains(document.activeElement) ?? false;
71
-
72
- /** Whether `target` is a thumb of this message, whose click decides itself. */
73
- const isOwnThumb = (target: EventTarget | null) => {
74
- const thumb =
75
- target instanceof Element
76
- ? target.closest("[data-copilotkit-rating]")
77
- : null;
78
- return (
79
- thumb?.closest("[data-message-id]")?.getAttribute("data-message-id") ===
80
- props.messageId
81
- );
82
- };
83
-
84
44
  // A keyboard rating moves focus into the field, as the popover opens or, when
85
45
  // it was open already, as the field for the new rating renders.
86
- const focusField = () => {
87
- const input = props.comment.input.value;
88
- if (input && props.comment.takeFocusRequest()) input.focus();
89
- };
90
46
  watch(
91
47
  () => [props.comment.focusRequest.value, props.comment.open.value],
92
48
  async () => {
93
49
  await nextTick();
94
- focusField();
50
+ props.comment.focusField();
95
51
  },
96
52
  // Immediate: the popover mounts as it opens, after the request was made.
97
53
  { flush: "post", immediate: true },
@@ -107,43 +63,25 @@ const changed = computed(
107
63
  );
108
64
 
109
65
  function onEscape(event: KeyboardEvent) {
110
- const returnFocus = focusInside();
111
66
  // Escape in the comment closes only the comment, not a chat popup around
112
67
  // it; with focus elsewhere it also reaches that control.
113
- if (returnFocus) event.stopPropagation();
68
+ if (props.comment.popoverEscape(contentElement())) event.stopPropagation();
114
69
  event.preventDefault();
115
- props.comment.close({ save: false });
116
- if (returnFocus) props.comment.returnFocus(props.anchor);
117
70
  }
118
71
 
119
72
  function onInteractOutside(event: Event) {
120
73
  // A click on a thumb removes or switches the rating, so closing must not
121
74
  // first save the draft as the answer to the rating it replaces.
122
- if (isOwnThumb(event.target)) event.preventDefault();
75
+ if (ɵisOwnFeedbackThumb(event.target, props.messageId))
76
+ event.preventDefault();
123
77
  }
124
78
 
125
79
  function onKeyDown(event: KeyboardEvent) {
126
- if (event.key !== "Tab") return;
127
- const focusables = Array.from(
128
- contentElement()?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [],
129
- );
130
- const leaving = event.shiftKey
131
- ? document.activeElement === focusables[0]
132
- : document.activeElement === focusables[focusables.length - 1];
133
- const anchor = props.anchor;
134
- if (!leaving || !anchor?.isConnected) return;
135
- event.preventDefault();
136
- const target = event.shiftKey
137
- ? anchor
138
- : (nextFocusable(anchor, contentElement()) ?? anchor);
139
- props.comment.close();
140
- if (target === anchor) props.comment.returnFocus(target);
141
- else target.focus();
80
+ props.comment.popoverKeyDown(event, contentElement());
142
81
  }
143
82
 
144
83
  function onSubmit() {
145
- props.comment.close();
146
- props.comment.returnFocus(props.anchor);
84
+ props.comment.submit();
147
85
  }
148
86
 
149
87
  function onEditingChange(editing: boolean) {
@@ -151,10 +89,7 @@ function onEditingChange(editing: boolean) {
151
89
  }
152
90
 
153
91
  function onLeave(next: Element | null) {
154
- // Focus moving within the popover or to a thumb, which the pointer is
155
- // still on, does not close it.
156
- if (next && (contentElement()?.contains(next) || isOwnThumb(next))) return;
157
- props.comment.leave();
92
+ props.comment.popoverFocusLeave(next, contentElement(), props.messageId);
158
93
  }
159
94
 
160
95
  function onFormFocusOut(event: FocusEvent) {
@@ -10,14 +10,13 @@ import {
10
10
  watch,
11
11
  } from "vue";
12
12
  import {
13
- ɵclampSlashIndex,
13
+ ɵComposerController,
14
14
  ɵcomposerButtonState,
15
15
  ɵcreateComposerAutosize,
16
- ɵfilterCommands,
17
- ɵflattenCommandItems,
18
- ɵparseSlashQuery,
19
- ɵresolveComposerKey,
20
- ɵresolveSlashKey,
16
+ ɵflattenToolsMenuForDisplay,
17
+ ɵnormalizeToolsMenu,
18
+ ɵshowsComposerDisclaimer,
19
+ ɵsyncRecorderToMode,
21
20
  } from "@copilotkit/core";
22
21
  import {
23
22
  IconArrowUp,
@@ -43,16 +42,6 @@ import { useChatLabels } from "./use-chat-labels";
43
42
  defineOptions({ inheritAttrs: false });
44
43
 
45
44
  type MenuEntry = ToolsMenuItem | "-";
46
- type MenuDisplayEntry =
47
- | { type: "separator"; key: string }
48
- | { type: "label"; key: string; label: string; depth: number }
49
- | {
50
- type: "item";
51
- key: string;
52
- label: string;
53
- depth: number;
54
- action: () => void;
55
- };
56
45
 
57
46
  const props = withDefaults(
58
47
  defineProps<{
@@ -124,8 +113,17 @@ const addButtonContainerRef = ref<HTMLElement | null>(null);
124
113
  const actionsContainerRef = ref<HTMLElement | null>(null);
125
114
  const slashMenuRef = ref<HTMLElement | null>(null);
126
115
  const audioRecorderRef = ref<CopilotChatAudioRecorderRef | null>(null);
127
- const localValue = ref(props.modelValue ?? "");
128
116
  const isComposing = ref(false);
117
+ // The text, controlled or not, and the slash-command menu: see core's
118
+ // ɵComposerController.
119
+ const composer = new ɵComposerController<ToolsMenuItem>({
120
+ initialValue: props.modelValue ?? "",
121
+ onChange: (value) => emit("update:modelValue", value),
122
+ });
123
+ const composerState = shallowRef(composer.getSnapshot());
124
+ composer.subscribe(() => {
125
+ composerState.value = composer.getSnapshot();
126
+ });
129
127
  // Sizes the text box and picks the compact or expanded layout.
130
128
  const autosize = ɵcreateComposerAutosize({
131
129
  getElements: () => ({
@@ -140,9 +138,6 @@ const layout = shallowRef(autosize.getSnapshot());
140
138
  autosize.subscribe(() => {
141
139
  layout.value = autosize.getSnapshot();
142
140
  });
143
- const commandQuery = ref<string | null>(null);
144
- const slashHighlightIndex = ref(0);
145
- const previousCommandQuery = ref<string | null>(null);
146
141
  const addMenuOpen = ref(false);
147
142
  // Where focus goes as the add menu opens: its first or last item for the
148
143
  // keyboard, the menu itself for a pointer, as Radix's DropdownMenu does.
@@ -151,9 +146,13 @@ const addMenuAnchor = ref<HTMLElement | null>(null);
151
146
  const addMenuId = useCopilotId("cpk-add-menu");
152
147
  const addMenuTriggerId = `${addMenuId}-trigger`;
153
148
 
154
- const isControlled = computed(() => props.modelValue !== undefined);
155
- const inputValue = computed(() =>
156
- isControlled.value ? (props.modelValue ?? "") : localValue.value,
149
+ const inputValue = computed(() => composerState.value.value);
150
+ const filteredCommands = computed(() => composerState.value.slashCommands);
151
+ const slashHighlightIndex = computed(
152
+ () => composerState.value.slashHighlightIndex,
153
+ );
154
+ const slashMenuVisible = computed(
155
+ () => composerState.value.slashQuery !== null,
157
156
  );
158
157
  const labels = useChatLabels();
159
158
  const resolvedPlaceholder = computed(
@@ -162,8 +161,11 @@ const resolvedPlaceholder = computed(
162
161
  const isExpanded = computed(
163
162
  () => props.mode === "input" && layout.value === "expanded",
164
163
  );
165
- const shouldShowDisclaimer = computed(
166
- () => props.showDisclaimer ?? props.positioning === "absolute",
164
+ const shouldShowDisclaimer = computed(() =>
165
+ ɵshowsComposerDisclaimer({
166
+ showDisclaimer: props.showDisclaimer,
167
+ positioning: props.positioning,
168
+ }),
167
169
  );
168
170
  const hasSubmitAction = computed(
169
171
  () => typeof props.onSubmitMessage === "function",
@@ -208,150 +210,58 @@ const rootAttrs = computed(() => {
208
210
  return rest;
209
211
  });
210
212
 
211
- function isMenuGroup(
212
- item: ToolsMenuItem,
213
- ): item is ToolsMenuItem & { items: MenuEntry[] } {
214
- return Array.isArray((item as ToolsMenuItem).items);
215
- }
216
-
217
- function createDefaultAddItem(): ToolsMenuItem | null {
218
- if (!hasAddFileAction.value) {
219
- return null;
220
- }
221
- return {
222
- label: labels.value.chatInputToolbarAddButtonLabel,
223
- action: () => emit("add-file"),
224
- };
225
- }
226
-
227
- function normalizeMenuItems() {
228
- const items: MenuEntry[] = [];
229
- const addItem = createDefaultAddItem();
230
- if (addItem) {
231
- items.push(addItem);
232
- }
233
-
234
- if (props.toolsMenu.length > 0) {
235
- if (items.length > 0) {
236
- items.push("-");
237
- }
238
-
239
- for (const menuEntry of props.toolsMenu) {
240
- if (menuEntry === "-") {
241
- if (items.length === 0 || items[items.length - 1] === "-") {
242
- continue;
213
+ const menuItems = computed(() =>
214
+ ɵnormalizeToolsMenu<ToolsMenuItem>(
215
+ props.toolsMenu,
216
+ hasAddFileAction.value
217
+ ? {
218
+ label: labels.value.chatInputToolbarAddButtonLabel,
219
+ action: () => emit("add-file"),
243
220
  }
244
- items.push("-");
245
- continue;
246
- }
247
-
248
- items.push(menuEntry);
249
- }
250
- }
251
-
252
- while (items.length > 0 && items[items.length - 1] === "-") {
253
- items.pop();
254
- }
255
-
256
- return items;
257
- }
258
-
259
- const menuItems = computed(() => normalizeMenuItems());
221
+ : undefined,
222
+ ),
223
+ );
260
224
  const hasMenuItems = computed(() => menuItems.value.length > 0);
261
-
262
- const commandItems = computed(() => ɵflattenCommandItems(menuItems.value));
263
-
264
- function flattenMenuDisplay(items: MenuEntry[], depth = 0, prefix = "root") {
265
- const entries: MenuDisplayEntry[] = [];
266
- for (const [index, item] of items.entries()) {
267
- const key = `${prefix}-${index}`;
268
- if (item === "-") {
269
- entries.push({ type: "separator", key: `sep-${key}` });
270
- continue;
271
- }
272
- if (isMenuGroup(item) && item.items.length > 0) {
273
- entries.push({
274
- type: "label",
275
- key: `label-${key}`,
276
- label: item.label,
277
- depth,
278
- });
279
- entries.push(...flattenMenuDisplay(item.items, depth + 1, key));
280
- continue;
281
- }
282
- if (item.action) {
283
- entries.push({
284
- type: "item",
285
- key: `item-${key}`,
286
- label: item.label,
287
- depth,
288
- action: item.action,
289
- });
290
- }
291
- }
292
- return entries;
293
- }
294
-
295
- const menuDisplayItems = computed(() => flattenMenuDisplay(menuItems.value));
296
-
297
- const filteredCommands = computed(() =>
298
- ɵfilterCommands(commandItems.value, commandQuery.value),
225
+ const menuDisplayItems = computed(() =>
226
+ ɵflattenToolsMenuForDisplay(menuItems.value),
299
227
  );
300
228
 
301
- const slashMenuVisible = computed(
302
- () => commandQuery.value !== null && commandItems.value.length > 0,
229
+ watch(
230
+ [() => props.modelValue, () => props.mode, menuItems],
231
+ ([value, mode, menu]) => composer.setInputs({ value, mode, menu }),
232
+ { immediate: true },
303
233
  );
304
234
 
305
235
  function updateInputValue(nextValue: string) {
306
- if (!isControlled.value) {
307
- localValue.value = nextValue;
308
- }
309
- emit("update:modelValue", nextValue);
310
- }
311
-
312
- function clearInputValue() {
313
- updateInputValue("");
314
- }
315
-
316
- function updateSlashState(value: string) {
317
- commandQuery.value = ɵparseSlashQuery(value, commandItems.value.length > 0);
236
+ composer.setValue(nextValue);
318
237
  }
319
238
 
320
- function runCommand(command: ToolsMenuItem) {
321
- clearInputValue();
322
- command.action?.();
323
- commandQuery.value = null;
324
- slashHighlightIndex.value = 0;
239
+ function focusTextareaSoon() {
325
240
  requestAnimationFrame(() => {
326
241
  textareaRef.value?.focus();
327
242
  });
328
243
  }
329
244
 
245
+ function runCommand(command: ToolsMenuItem) {
246
+ composer.runCommand(command);
247
+ focusTextareaSoon();
248
+ }
249
+
330
250
  function submit() {
331
251
  if (props.mode !== "input" || props.disabled || !hasSubmitAction.value) {
332
252
  return;
333
253
  }
334
254
  // In controlled mode, parent-updated modelValue can lag one tick behind
335
255
  // the actual textarea value during intense runtime/connect churn.
336
- const rawValue = textareaRef.value?.value ?? inputValue.value;
337
- const trimmed = rawValue.trim();
338
- if (!trimmed) {
339
- return;
340
- }
341
-
342
- emit("submit-message", trimmed);
343
-
344
- if (props.clearOnSubmit) {
345
- clearInputValue();
346
- }
347
-
348
- textareaRef.value?.focus();
256
+ const sent = composer.submit((text) => emit("submit-message", text), {
257
+ value: textareaRef.value?.value ?? inputValue.value,
258
+ clear: props.clearOnSubmit,
259
+ });
260
+ if (sent) textareaRef.value?.focus();
349
261
  }
350
262
 
351
263
  function handleInput(event: Event) {
352
- const nextValue = (event.target as HTMLTextAreaElement).value;
353
- updateInputValue(nextValue);
354
- updateSlashState(nextValue);
264
+ updateInputValue((event.target as HTMLTextAreaElement).value);
355
265
  }
356
266
 
357
267
  // The button stops a run whatever the input holds. Enter differs: see
@@ -367,48 +277,28 @@ function handleSendButtonClick() {
367
277
  }
368
278
 
369
279
  function handleKeydown(event: KeyboardEvent) {
370
- if (props.disabled) {
371
- return;
372
- }
373
-
374
- if (isComposing.value || event.isComposing || event.keyCode === 229) {
375
- return;
376
- }
377
-
378
- const slash = ɵresolveSlashKey(event, {
379
- query: props.mode === "input" ? commandQuery.value : null,
380
- commands: filteredCommands.value,
381
- highlightIndex: slashHighlightIndex.value,
382
- });
383
- if (slash.type !== "none") event.preventDefault();
384
- switch (slash.type) {
385
- case "highlight":
386
- slashHighlightIndex.value = slash.index;
387
- return;
388
- case "run":
389
- runCommand(slash.command);
390
- return;
391
- case "close":
392
- commandQuery.value = null;
393
- return;
394
- case "none":
395
- break;
396
- }
397
-
398
- if (props.mode !== "input") {
280
+ if (props.disabled || props.mode !== "input") {
399
281
  return;
400
282
  }
401
283
 
402
284
  // Enter with text sends, also while a run is going. Enter in an empty input
403
285
  // stops the run. Read the textarea: a controlled modelValue can lag a tick.
404
- const action = ɵresolveComposerKey(event, {
405
- ...composerInput.value,
406
- value: textareaRef.value?.value ?? inputValue.value,
407
- });
408
- if (event.key === "Enter" && !event.shiftKey) {
409
- event.preventDefault();
410
- }
411
- if (action === "send") {
286
+ const { preventDefault, action } = composer.keyDown(
287
+ {
288
+ key: event.key,
289
+ shiftKey: event.shiftKey,
290
+ isComposing: isComposing.value || event.isComposing,
291
+ keyCode: event.keyCode,
292
+ },
293
+ {
294
+ ...composerInput.value,
295
+ value: textareaRef.value?.value ?? inputValue.value,
296
+ },
297
+ );
298
+ if (preventDefault) event.preventDefault();
299
+ if (action === "command") {
300
+ focusTextareaSoon();
301
+ } else if (action === "send") {
412
302
  submit();
413
303
  } else if (action === "stop") {
414
304
  emit("stop");
@@ -523,19 +413,6 @@ function handleContainerClick(event: MouseEvent) {
523
413
  textareaRef.value?.focus();
524
414
  }
525
415
 
526
- watch(
527
- () => props.modelValue,
528
- (next) => {
529
- if (isControlled.value) {
530
- localValue.value = next ?? "";
531
- }
532
- },
533
- );
534
-
535
- watch(inputValue, (value) => {
536
- updateSlashState(value);
537
- });
538
-
539
416
  watch(
540
417
  [inputValue, () => props.mode],
541
418
  ([value, mode]) => {
@@ -551,88 +428,24 @@ watch(
551
428
  },
552
429
  );
553
430
 
554
- watch(commandItems, () => {
555
- updateSlashState(inputValue.value);
556
- });
557
-
558
- watch(
559
- [commandQuery, () => filteredCommands.value.length],
560
- ([query, filteredCount]) => {
561
- if (
562
- query !== null &&
563
- query !== previousCommandQuery.value &&
564
- filteredCount > 0
565
- ) {
566
- slashHighlightIndex.value = 0;
567
- }
568
-
569
- previousCommandQuery.value = query;
570
- },
571
- { immediate: true },
572
- );
573
-
574
- watch(
575
- [commandQuery, filteredCommands],
576
- () => {
577
- slashHighlightIndex.value =
578
- commandQuery.value === null
579
- ? 0
580
- : ɵclampSlashIndex(
581
- slashHighlightIndex.value,
582
- filteredCommands.value.length,
583
- );
584
- },
585
- { immediate: true },
586
- );
587
-
588
431
  watch(
589
432
  () => props.mode,
590
- async (mode) => {
433
+ (mode) => {
591
434
  if (mode !== "input") {
592
- commandQuery.value = null;
593
435
  closeAddMenu();
594
436
  }
595
-
596
- const recorder = audioRecorderRef.value;
597
- if (!recorder) {
598
- return;
599
- }
600
-
601
- if (mode === "transcribe") {
602
- try {
603
- await recorder.start();
604
- } catch (error) {
605
- console.error(error);
606
- }
607
- return;
608
- }
609
-
610
- if (recorder.state === "recording") {
611
- try {
612
- await recorder.stop();
613
- } catch {
614
- // ignore transition stop failures
615
- }
616
- }
617
437
  },
618
- { immediate: true },
619
438
  );
620
439
 
440
+ // Recording follows the mode: it starts in transcribe mode and stops when the
441
+ // mode changes. The recorder renders only in transcribe mode, so also sync
442
+ // when it appears.
621
443
  watch(
622
- audioRecorderRef,
623
- async (recorder) => {
624
- if (!recorder || props.mode !== "transcribe") {
625
- return;
626
- }
627
- if (recorder.state === "idle") {
628
- try {
629
- await recorder.start();
630
- } catch (error) {
631
- console.error(error);
632
- }
633
- }
444
+ [audioRecorderRef, () => props.mode],
445
+ ([recorder, mode]) => {
446
+ void ɵsyncRecorderToMode(recorder, mode);
634
447
  },
635
- { immediate: true },
448
+ { immediate: true, flush: "post" },
636
449
  );
637
450
 
638
451
  watch(slashHighlightIndex, async (index) => {
@@ -846,7 +659,7 @@ onBeforeUnmount(() => {
846
659
  index === slashHighlightIndex ? 'true' : undefined
847
660
  "
848
661
  class="cpk-chat-input-slash-menu-item"
849
- @mouseenter="slashHighlightIndex = index"
662
+ @mouseenter="composer.highlight(index)"
850
663
  @mousedown.prevent="runCommand(command)"
851
664
  >
852
665
  {{ command.label }}