@agent-native/toolkit 0.204.0 → 0.204.1-nightly-20261008034100

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 (144) hide show
  1. package/dist/app/agent-page/AgentJobsTab.d.ts.map +1 -1
  2. package/dist/app/agent-page/AgentJobsTab.js +4 -2
  3. package/dist/app/agent-page/AgentJobsTab.js.map +1 -1
  4. package/dist/app/agent-page/AutomationDetailsDialog.d.ts +2 -1
  5. package/dist/app/agent-page/AutomationDetailsDialog.d.ts.map +1 -1
  6. package/dist/app/agent-page/AutomationDetailsDialog.js +8 -4
  7. package/dist/app/agent-page/AutomationDetailsDialog.js.map +1 -1
  8. package/dist/app/agent-page/ScheduledTriggerNotice.d.ts.map +1 -1
  9. package/dist/app/agent-page/ScheduledTriggerNotice.js +41 -19
  10. package/dist/app/agent-page/ScheduledTriggerNotice.js.map +1 -1
  11. package/dist/app/agent-page/use-jobs.d.ts +1 -1
  12. package/dist/app/agent-page/use-jobs.d.ts.map +1 -1
  13. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  14. package/dist/app/agentkit/react/components.js +124 -45
  15. package/dist/app/agentkit/react/components.js.map +1 -1
  16. package/dist/app/agentkit/react/context.d.ts +22 -1
  17. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  18. package/dist/app/agentkit/react/context.js +14 -1
  19. package/dist/app/agentkit/react/context.js.map +1 -1
  20. package/dist/app/agentkit/react/root.d.ts +3 -2
  21. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  22. package/dist/app/agentkit/react/root.js +2 -2
  23. package/dist/app/agentkit/react/root.js.map +1 -1
  24. package/dist/app/agentkit/react/styles.css +32 -2
  25. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  26. package/dist/app/chat/AgentKitAssistantChat.js +28 -3
  27. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  28. package/dist/app/chat/AgentPanel.js +1 -1
  29. package/dist/app/chat/AgentPanel.js.map +1 -1
  30. package/dist/app/chat/RunStuckBanner.d.ts +8 -1
  31. package/dist/app/chat/RunStuckBanner.d.ts.map +1 -1
  32. package/dist/app/chat/RunStuckBanner.js +68 -6
  33. package/dist/app/chat/RunStuckBanner.js.map +1 -1
  34. package/dist/app/chat/chat/run-recovery.d.ts +4 -2
  35. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  36. package/dist/app/chat/chat/run-recovery.js +3 -3
  37. package/dist/app/chat/chat/run-recovery.js.map +1 -1
  38. package/dist/app/chat/composer/use-mention-search.d.ts +1 -0
  39. package/dist/app/chat/composer/use-mention-search.d.ts.map +1 -1
  40. package/dist/app/chat/feedback-report.d.ts +12 -0
  41. package/dist/app/chat/feedback-report.d.ts.map +1 -0
  42. package/dist/app/chat/feedback-report.js +21 -0
  43. package/dist/app/chat/feedback-report.js.map +1 -0
  44. package/dist/app/chat/reconcile-settled-run.d.ts +15 -0
  45. package/dist/app/chat/reconcile-settled-run.d.ts.map +1 -0
  46. package/dist/app/chat/reconcile-settled-run.js +41 -0
  47. package/dist/app/chat/reconcile-settled-run.js.map +1 -0
  48. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/ar-SA.js +9 -0
  50. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  51. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/de-DE.js +9 -0
  53. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  54. package/dist/app/i18n/catalogs/en-US.d.ts +9 -0
  55. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  56. package/dist/app/i18n/catalogs/en-US.js +9 -0
  57. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  58. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  59. package/dist/app/i18n/catalogs/es-ES.js +9 -0
  60. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  61. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  62. package/dist/app/i18n/catalogs/fr-FR.js +9 -0
  63. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  64. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  65. package/dist/app/i18n/catalogs/hi-IN.js +9 -0
  66. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  67. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  68. package/dist/app/i18n/catalogs/ja-JP.js +9 -0
  69. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  70. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  71. package/dist/app/i18n/catalogs/ko-KR.js +9 -0
  72. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  73. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  74. package/dist/app/i18n/catalogs/pt-BR.js +9 -0
  75. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  76. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  77. package/dist/app/i18n/catalogs/zh-CN.js +9 -0
  78. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  79. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  80. package/dist/app/i18n/catalogs/zh-TW.js +9 -0
  81. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  82. package/dist/app/settings/shell/pages/audit.d.ts.map +1 -1
  83. package/dist/app/settings/shell/pages/audit.js +11 -2
  84. package/dist/app/settings/shell/pages/audit.js.map +1 -1
  85. package/dist/composer/ComposerContextMenu.d.ts +14 -1
  86. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  87. package/dist/composer/ComposerContextMenu.js +65 -19
  88. package/dist/composer/ComposerContextMenu.js.map +1 -1
  89. package/dist/composer/MentionPopover.d.ts.map +1 -1
  90. package/dist/composer/MentionPopover.js +3 -3
  91. package/dist/composer/MentionPopover.js.map +1 -1
  92. package/dist/composer/PromptComposer.d.ts +1 -1
  93. package/dist/composer/PromptComposer.d.ts.map +1 -1
  94. package/dist/composer/TiptapComposer.d.ts +2 -2
  95. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  96. package/dist/composer/TiptapComposer.js +92 -36
  97. package/dist/composer/TiptapComposer.js.map +1 -1
  98. package/dist/composer/use-mention-search.d.ts +1 -0
  99. package/dist/composer/use-mention-search.d.ts.map +1 -1
  100. package/dist/composer/use-mention-search.js +8 -1
  101. package/dist/composer/use-mention-search.js.map +1 -1
  102. package/package.json +4 -4
  103. package/src/app/agent-page/AgentJobsTab.blocked.spec.tsx +43 -0
  104. package/src/app/agent-page/AgentJobsTab.tsx +14 -5
  105. package/src/app/agent-page/AutomationDetailsDialog.tsx +22 -8
  106. package/src/app/agent-page/ScheduledTriggerNotice.spec.tsx +45 -3
  107. package/src/app/agent-page/ScheduledTriggerNotice.tsx +52 -31
  108. package/src/app/agent-page/use-jobs.ts +1 -1
  109. package/src/app/agentkit/react/components.interaction.spec.tsx +311 -1
  110. package/src/app/agentkit/react/components.spec.tsx +106 -2
  111. package/src/app/agentkit/react/components.tsx +217 -80
  112. package/src/app/agentkit/react/context.tsx +36 -0
  113. package/src/app/agentkit/react/root.tsx +4 -0
  114. package/src/app/agentkit/react/styles.css +32 -2
  115. package/src/app/chat/AgentKitAssistantChat.tsx +35 -2
  116. package/src/app/chat/AgentPanel.header.spec.ts +43 -7
  117. package/src/app/chat/AgentPanel.tsx +1 -1
  118. package/src/app/chat/RunStuckBanner.spec.tsx +258 -17
  119. package/src/app/chat/RunStuckBanner.tsx +107 -5
  120. package/src/app/chat/chat/run-recovery.tsx +10 -2
  121. package/src/app/chat/feedback-report.spec.ts +45 -0
  122. package/src/app/chat/feedback-report.ts +27 -0
  123. package/src/app/chat/reconcile-settled-run.spec.ts +112 -0
  124. package/src/app/chat/reconcile-settled-run.ts +57 -0
  125. package/src/app/i18n/catalogs/ar-SA.ts +12 -0
  126. package/src/app/i18n/catalogs/de-DE.ts +12 -0
  127. package/src/app/i18n/catalogs/en-US.ts +12 -0
  128. package/src/app/i18n/catalogs/es-ES.ts +12 -0
  129. package/src/app/i18n/catalogs/fr-FR.ts +12 -0
  130. package/src/app/i18n/catalogs/hi-IN.ts +12 -0
  131. package/src/app/i18n/catalogs/ja-JP.ts +12 -0
  132. package/src/app/i18n/catalogs/ko-KR.ts +12 -0
  133. package/src/app/i18n/catalogs/pt-BR.ts +12 -0
  134. package/src/app/i18n/catalogs/zh-CN.ts +10 -0
  135. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  136. package/src/app/settings/shell/pages/audit.spec.tsx +30 -0
  137. package/src/app/settings/shell/pages/audit.tsx +12 -1
  138. package/src/composer/ComposerContextMenu.tsx +94 -22
  139. package/src/composer/ComposerPlusMenu.parity.spec.tsx +218 -233
  140. package/src/composer/MentionPopover.tsx +12 -3
  141. package/src/composer/PromptComposer.context.spec.tsx +526 -195
  142. package/src/composer/PromptComposer.tsx +1 -1
  143. package/src/composer/TiptapComposer.tsx +105 -37
  144. package/src/composer/use-mention-search.ts +8 -1
@@ -1,5 +1,6 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
+ import type { Editor } from "@tiptap/core";
3
4
  import React, { act } from "react";
4
5
  import { createRoot, type Root } from "react-dom/client";
5
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -109,74 +110,400 @@ describe("controlled composer context", () => {
109
110
  container.querySelector('button[aria-label="Add context"]'),
110
111
  ).toBeNull();
111
112
  });
112
- it("opens the same shared Add menu from @ and + without inserting a mention", async () => {
113
+ // Types like a browser: the editor's keydown handlers run first, and only a
114
+ // key they leave unhandled reaches the document.
115
+ async function typeInto(editor: HTMLElement, text: string) {
116
+ const { view } = (editor as HTMLElement & { editor: Editor }).editor;
117
+ for (const key of text) {
118
+ await act(async () => {
119
+ const event = new KeyboardEvent("keydown", {
120
+ key,
121
+ bubbles: true,
122
+ cancelable: true,
123
+ });
124
+ editor.dispatchEvent(event);
125
+ if (!event.defaultPrevented)
126
+ view.dispatch(view.state.tr.insertText(key));
127
+ await new Promise((resolve) => setTimeout(resolve, 0));
128
+ });
129
+ }
130
+ }
131
+ async function pressEnter(editor: HTMLElement) {
132
+ await pressKey(editor, "Enter");
133
+ }
134
+ async function pressKey(editor: HTMLElement, key: string) {
135
+ await act(async () => {
136
+ editor.dispatchEvent(
137
+ new KeyboardEvent("keydown", {
138
+ key,
139
+ bubbles: true,
140
+ cancelable: true,
141
+ }),
142
+ );
143
+ await new Promise((resolve) => setTimeout(resolve, 0));
144
+ });
145
+ }
146
+ const slidesAgent = {
147
+ id: "agent:slides",
148
+ label: "Slides",
149
+ description: "Presentations in this workspace",
150
+ section: "Connected Agents",
151
+ source: "agent",
152
+ refType: "agent",
153
+ refId: "slides",
154
+ refPath: "https://slides.example.test",
155
+ };
156
+
157
+ it("keeps a typed @ address as text and submits it", async () => {
158
+ vi.stubGlobal(
159
+ "fetch",
160
+ vi.fn(async () => new Response('{"items":[]}\n', { status: 200 })),
161
+ );
162
+ const { onSubmit } = await mount({
163
+ initialText: "",
164
+ contextMenuItems: [
165
+ { id: "source", label: "Choose source", onSelect() {} },
166
+ ],
167
+ mentionItems: [slidesAgent],
168
+ });
169
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
170
+ await act(async () => editor.focus());
171
+
172
+ await typeInto(editor, "Ping a @builder.io");
173
+ await act(async () => new Promise((resolve) => setTimeout(resolve, 250)));
174
+ await typeInto(editor, " email");
175
+
176
+ expect(document.querySelector('[role="menu"]')).toBeNull();
177
+ expect(
178
+ document.querySelector('[data-agent-native-mention-popover="true"]'),
179
+ ).toBeNull();
180
+ expect(editor.textContent).toBe("Ping a @builder.io email");
181
+ await pressEnter(editor);
182
+ expect(onSubmit).toHaveBeenCalledOnce();
183
+ expect(onSubmit.mock.calls[0]![0]).toBe("Ping a @builder.io email");
184
+ });
185
+ it("submits a typed @ that nothing matches on Enter", async () => {
186
+ const { onSubmit } = await mount({
187
+ initialText: "",
188
+ includeDefaultMentionSearch: false,
189
+ mentionItems: [slidesAgent],
190
+ });
191
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
192
+ await act(async () => editor.focus());
193
+
194
+ await typeInto(editor, "Reply @");
195
+ expect(
196
+ document.querySelector('[data-agent-native-mention-popover="true"]'),
197
+ ).not.toBeNull();
198
+ await typeInto(editor, "x");
199
+ await pressEnter(editor);
200
+
201
+ expect(onSubmit).toHaveBeenCalledOnce();
202
+ expect(onSubmit.mock.calls[0]![0]).toBe("Reply @x");
203
+ });
204
+ it("keeps @ suggestions open for host mention items that arrive later", async () => {
205
+ await mount({
206
+ initialText: "",
207
+ includeDefaultMentionSearch: false,
208
+ mentionItems: [],
209
+ });
210
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
211
+ await act(async () => editor.focus());
212
+
213
+ await typeInto(editor, "Ask @Sli");
214
+ await mount({
215
+ initialText: "",
216
+ includeDefaultMentionSearch: false,
217
+ mentionItems: [slidesAgent],
218
+ });
219
+
220
+ expect(
221
+ document.querySelector('[data-mention-index="0"]')?.textContent,
222
+ ).toContain("Slides");
223
+ });
224
+ it.each([
225
+ ["finds nothing", '{"items":[]}\n', 200, null],
226
+ ["fails", "", 500, "open"],
227
+ ] as const)(
228
+ "ends a typed @ query only when the mention search %s",
229
+ async (_case, body, status, popoverAfterSearch) => {
230
+ vi.stubGlobal(
231
+ "fetch",
232
+ vi.fn(async () => new Response(body, { status })),
233
+ );
234
+ const { onSubmit } = await mount({ initialText: "" });
235
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
236
+ await act(async () => editor.focus());
237
+
238
+ await typeInto(editor, "Reply @x");
239
+ await act(async () => new Promise((resolve) => setTimeout(resolve, 250)));
240
+
241
+ // A failed search is unknown, not empty, so the popover stays open.
242
+ expect(
243
+ document.querySelector('[data-agent-native-mention-popover="true"]')
244
+ ? "open"
245
+ : null,
246
+ ).toBe(popoverAfterSearch);
247
+ await pressEnter(editor);
248
+ expect(onSubmit).toHaveBeenCalledOnce();
249
+ expect(onSubmit.mock.calls[0]![0]).toBe("Reply @x");
250
+ },
251
+ );
252
+ it.each(["Enter", "Tab"])(
253
+ "turns a typed @ query into a mention when %s picks it",
254
+ async (key) => {
255
+ const onReferencesChange = vi.fn();
256
+ const { onSubmit } = await mount({
257
+ initialText: "",
258
+ contextMenuItems: [],
259
+ includeDefaultMentionSearch: false,
260
+ onReferencesChange,
261
+ mentionItems: [slidesAgent],
262
+ });
263
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
264
+ await act(async () => editor.focus());
265
+
266
+ await typeInto(editor, "Ask @Sli");
267
+ expect(document.querySelector('[role="menu"]')).toBeNull();
268
+ expect(
269
+ document.querySelector('[data-mention-index="0"]')?.textContent,
270
+ ).toContain("Slides");
271
+ await pressKey(editor, key);
272
+
273
+ expect(editor.textContent).toContain("Ask ");
274
+ expect(editor.textContent).toContain("Slides");
275
+ expect(editor.textContent).not.toContain("@Sli");
276
+ expect(onReferencesChange).toHaveBeenLastCalledWith([
277
+ expect.objectContaining({ refType: "agent", refId: "slides" }),
278
+ ]);
279
+ expect(onSubmit).not.toHaveBeenCalled();
280
+ },
281
+ );
282
+ it("keeps an exactly matching @name followed by Space as text", async () => {
283
+ const onReferencesChange = vi.fn();
284
+ const { onSubmit } = await mount({
285
+ initialText: "",
286
+ contextMenuItems: [],
287
+ includeDefaultMentionSearch: false,
288
+ onReferencesChange,
289
+ mentionItems: [slidesAgent],
290
+ });
291
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
292
+ await act(async () => editor.focus());
293
+
294
+ await typeInto(editor, "Ask @Slides rock");
295
+
296
+ expect(editor.textContent).toBe("Ask @Slides rock");
297
+ expect(onReferencesChange).not.toHaveBeenCalledWith([
298
+ expect.objectContaining({ refId: "slides" }),
299
+ ]);
300
+ await pressKey(editor, "Escape");
301
+ await pressEnter(editor);
302
+ expect(onSubmit.mock.calls[0]![0]).toBe("Ask @Slides rock");
303
+ });
304
+ it("suggests host context sources for a typed @ and attaches the one picked", async () => {
305
+ const onSelect = vi.fn();
306
+ const { onSubmit } = await mount({
307
+ initialText: "",
308
+ contextMenuItems: [
309
+ {
310
+ id: "design-context",
311
+ label: "Design context",
312
+ children: [{ id: "figma", label: "Figma", onSelect }],
313
+ },
314
+ ],
315
+ includeDefaultMentionSearch: false,
316
+ mentionItems: [slidesAgent],
317
+ });
318
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
319
+ await act(async () => editor.focus());
320
+
321
+ await typeInto(editor, "Use @fig");
322
+ const popover = document.querySelector(
323
+ '[data-agent-native-mention-popover="true"]',
324
+ );
325
+ expect(popover?.textContent).toContain("Design context");
326
+ expect(popover?.textContent).toContain("Figma");
327
+ expect(popover?.textContent).not.toContain("Slides");
328
+ expect(document.activeElement).toBe(editor);
329
+ await pressEnter(editor);
330
+
331
+ expect(onSelect).toHaveBeenCalledOnce();
332
+ expect(editor.textContent).toBe("Use ");
333
+ expect(document.querySelector('[role="menu"]')).toBeNull();
334
+ expect(onSubmit).not.toHaveBeenCalled();
335
+ });
336
+ it("opens a picked host picker as a dialog and attaches its choice", async () => {
113
337
  const onSelect = vi.fn();
114
338
  await mount({
115
339
  initialText: "",
116
- contextMenuItems: [{ id: "source", label: "Choose source", onSelect }],
340
+ contextMenuItems: [
341
+ {
342
+ id: "documents",
343
+ label: "Documents",
344
+ children: [
345
+ {
346
+ id: "document",
347
+ label: "Reference a document",
348
+ picker: {
349
+ searchPlaceholder: "Search documents",
350
+ items: [{ id: "brief", title: "Project brief" }],
351
+ onSelect,
352
+ },
353
+ },
354
+ {
355
+ id: "notes",
356
+ label: "Document notes",
357
+ onSelect() {},
358
+ render: () => <div>Notes page</div>,
359
+ },
360
+ ],
361
+ },
362
+ ],
363
+ includeDefaultMentionSearch: false,
117
364
  });
118
365
  const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
366
+ await act(async () => editor.focus());
367
+
368
+ await typeInto(editor, "@doc");
369
+ const rows = Array.from(
370
+ document.querySelectorAll("[data-mention-index]"),
371
+ (row) => row.textContent,
372
+ );
373
+ // Custom pages render only inside the + menu.
374
+ expect(rows).toEqual(["Reference a document"]);
375
+ await pressEnter(editor);
119
376
 
377
+ const dialog = document.querySelector('[role="dialog"]')!;
378
+ expect(dialog.textContent).toContain("Reference a document");
379
+ expect(editor.textContent).toBe("");
380
+ const option = dialog.querySelector<HTMLButtonElement>(
381
+ '[role="radio"][value="brief"]',
382
+ )!;
383
+ await act(async () => {
384
+ option.click();
385
+ await new Promise((resolve) => setTimeout(resolve, 0));
386
+ });
387
+ expect(onSelect).toHaveBeenCalledWith(
388
+ expect.objectContaining({ id: "brief" }),
389
+ expect.anything(),
390
+ );
391
+ });
392
+ it("dismisses @ suggestions with one Escape and keeps typing as text", async () => {
393
+ const { onSubmit } = await mount({
394
+ initialText: "",
395
+ contextMenuItems: [
396
+ { id: "source", label: "Choose source", onSelect() {} },
397
+ ],
398
+ includeDefaultMentionSearch: false,
399
+ mentionItems: [slidesAgent],
400
+ });
401
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
402
+ await act(async () => editor.focus());
403
+
404
+ await typeInto(editor, "Ask @Sli");
120
405
  await act(async () => {
121
- editor.focus();
122
406
  editor.dispatchEvent(
123
407
  new KeyboardEvent("keydown", {
124
- key: "@",
408
+ key: "Escape",
125
409
  bubbles: true,
126
410
  cancelable: true,
127
411
  }),
128
412
  );
129
413
  });
414
+ expect(
415
+ document.querySelector('[data-agent-native-mention-popover="true"]'),
416
+ ).toBeNull();
417
+ await typeInto(editor, "des rock");
130
418
 
131
- const mentionMenu = document.querySelector<HTMLElement>('[role="menu"]');
132
- expect(mentionMenu?.textContent).toContain("Upload File");
133
- expect(mentionMenu?.textContent).toContain("Add context");
134
- expect(mentionMenu?.textContent).not.toContain("Choose source");
135
- expect(editor.textContent).toBe("");
136
- const openContext = async () => {
137
- const trigger = Array.from(
138
- document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
139
- ).find((item) => item.textContent === "Add context")!;
140
- await act(async () => {
141
- trigger.focus();
142
- trigger.dispatchEvent(
143
- new KeyboardEvent("keydown", {
144
- key: "ArrowRight",
145
- bubbles: true,
146
- cancelable: true,
147
- }),
148
- );
149
- });
150
- };
151
- const nestedItems = () =>
152
- Array.from(
153
- Array.from(document.querySelectorAll<HTMLElement>('[role="menu"]'))
154
- .at(-1)!
155
- .querySelectorAll<HTMLElement>('[role^="menuitem"]'),
156
- ).map((item) => item.textContent);
157
- await openContext();
158
- const mentionOptions = nestedItems();
419
+ expect(
420
+ document.querySelector('[data-agent-native-mention-popover="true"]'),
421
+ ).toBeNull();
422
+ expect(editor.textContent).toBe("Ask @Slides rock");
423
+ await pressEnter(editor);
424
+ expect(onSubmit.mock.calls[0]![0]).toBe("Ask @Slides rock");
425
+ });
426
+ it("keeps every key typed while a slow mention search is pending", async () => {
427
+ const pending: Array<() => void> = [];
428
+ vi.stubGlobal(
429
+ "fetch",
430
+ vi.fn(
431
+ () =>
432
+ new Promise<Response>((resolve) =>
433
+ pending.push(() =>
434
+ resolve(new Response('{"items":[]}\n', { status: 200 })),
435
+ ),
436
+ ),
437
+ ),
438
+ );
439
+ const { onSubmit } = await mount({ initialText: "" });
440
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
441
+ await act(async () => editor.focus());
442
+
443
+ await typeInto(editor, "Ping a @builder.io email");
444
+ await act(async () => new Promise((resolve) => setTimeout(resolve, 250)));
445
+ expect(editor.textContent).toBe("Ping a @builder.io email");
446
+ expect(document.activeElement).toBe(editor);
447
+
448
+ await act(async () => {
449
+ pending.splice(0).forEach((release) => release());
450
+ await new Promise((resolve) => setTimeout(resolve, 0));
451
+ });
452
+ expect(
453
+ document.querySelector('[data-agent-native-mention-popover="true"]'),
454
+ ).toBeNull();
455
+ await pressEnter(editor);
456
+ expect(onSubmit.mock.calls[0]![0]).toBe("Ping a @builder.io email");
457
+ });
458
+ it("sends a typed @ on Enter while its search is pending, ignoring a late match", async () => {
459
+ const pending: Array<() => void> = [];
460
+ vi.stubGlobal(
461
+ "fetch",
462
+ vi.fn(
463
+ () =>
464
+ new Promise<Response>((resolve) =>
465
+ pending.push(() =>
466
+ resolve(
467
+ new Response(
468
+ JSON.stringify({
469
+ items: [{ ...slidesAgent, label: "Alice" }],
470
+ }) + "\n",
471
+ { status: 200 },
472
+ ),
473
+ ),
474
+ ),
475
+ ),
476
+ ),
477
+ );
478
+ const onReferencesChange = vi.fn();
479
+ const { onSubmit } = await mount({ initialText: "", onReferencesChange });
480
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
481
+ await act(async () => editor.focus());
159
482
 
483
+ await typeInto(editor, "Ask @Ali");
484
+ await pressEnter(editor);
485
+ expect(onSubmit).toHaveBeenCalledOnce();
486
+ expect(onSubmit.mock.calls[0]![0]).toBe("Ask @Ali");
487
+
488
+ await act(async () => {
489
+ pending.splice(0).forEach((release) => release());
490
+ await new Promise((resolve) => setTimeout(resolve, 0));
491
+ });
492
+ expect(onSubmit).toHaveBeenCalledOnce();
493
+ expect(onReferencesChange).not.toHaveBeenCalledWith([
494
+ expect.objectContaining({ refId: "slides" }),
495
+ ]);
496
+ });
497
+ it("opens the shared Add menu from + without inserting a mention", async () => {
498
+ const onSelect = vi.fn();
499
+ await mount({
500
+ initialText: "",
501
+ contextMenuItems: [{ id: "source", label: "Choose source", onSelect }],
502
+ });
503
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
160
504
  const plusButton = container.querySelector<HTMLButtonElement>(
161
505
  'button[aria-label="Add context"]',
162
506
  )!;
163
- await act(async () =>
164
- Array.from(document.querySelectorAll<HTMLElement>('[role^="menuitem"]'))
165
- .find((item) => item.textContent === "Choose source")!
166
- .dispatchEvent(
167
- new KeyboardEvent("keydown", {
168
- key: "Escape",
169
- bubbles: true,
170
- cancelable: true,
171
- }),
172
- ),
173
- );
174
- await act(async () =>
175
- document.dispatchEvent(
176
- new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
177
- ),
178
- );
179
- expect(document.querySelector('[role="menu"]')).toBeNull();
180
507
  await act(async () =>
181
508
  plusButton.dispatchEvent(
182
509
  new KeyboardEvent("keydown", {
@@ -188,51 +515,133 @@ describe("controlled composer context", () => {
188
515
  );
189
516
 
190
517
  const plusMenu = document.querySelector<HTMLElement>('[role="menu"]');
518
+ expect(plusMenu?.textContent).toContain("Upload File");
191
519
  expect(plusMenu?.textContent).toContain("Add context");
192
- await openContext();
193
- expect(nestedItems()).toEqual(mentionOptions);
520
+ expect(plusMenu?.textContent).not.toContain("Choose source");
521
+ const addContext = Array.from(
522
+ document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
523
+ ).find((item) => item.textContent === "Add context")!;
524
+ await act(async () => {
525
+ addContext.focus();
526
+ addContext.dispatchEvent(
527
+ new KeyboardEvent("keydown", {
528
+ key: "ArrowRight",
529
+ bubbles: true,
530
+ cancelable: true,
531
+ }),
532
+ );
533
+ });
194
534
  const sourceAction = Array.from(
195
535
  document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
196
536
  ).find((item) => item.textContent === "Choose source")!;
197
537
  await act(async () => sourceAction.click());
198
538
  expect(onSelect).toHaveBeenCalledOnce();
539
+ expect(editor.textContent).toBe("");
199
540
  });
200
- it.each(["@", "+"])(
201
- "opens the integration submenu from %s without changing the draft",
202
- async (trigger) => {
203
- const onSelect = vi.fn();
204
- await mount({
205
- initialText: "Keep my draft ",
206
- contextMenuItems: [
207
- {
208
- id: "integrations",
209
- label: "Integrations",
210
- picker: {
211
- searchPlaceholder: "Search integrations",
212
- items: [{ id: "github", title: "GitHub" }],
213
- onSelect,
214
- },
541
+ it("opens the integration submenu from + without changing the draft", async () => {
542
+ const onSelect = vi.fn();
543
+ await mount({
544
+ initialText: "Keep my draft ",
545
+ contextMenuItems: [
546
+ {
547
+ id: "integrations",
548
+ label: "Integrations",
549
+ picker: {
550
+ searchPlaceholder: "Search integrations",
551
+ items: [{ id: "github", title: "GitHub" }],
552
+ onSelect,
215
553
  },
216
- ],
217
- });
218
- const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
219
- const button = container.querySelector<HTMLButtonElement>(
220
- 'button[aria-label="Add context"]',
221
- )!;
222
- await act(async () => {
223
- const target = trigger === "@" ? editor : button;
224
- target.focus();
225
- target.dispatchEvent(
226
- new KeyboardEvent("keydown", {
227
- key: trigger === "@" ? "@" : "ArrowDown",
228
- bubbles: true,
229
- cancelable: true,
230
- }),
231
- );
232
- });
233
- const addContext = Array.from(
234
- document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
235
- ).find((item) => item.textContent === "Add context")!;
554
+ },
555
+ ],
556
+ });
557
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
558
+ const button = container.querySelector<HTMLButtonElement>(
559
+ 'button[aria-label="Add context"]',
560
+ )!;
561
+ await act(async () => {
562
+ button.focus();
563
+ button.dispatchEvent(
564
+ new KeyboardEvent("keydown", {
565
+ key: "ArrowDown",
566
+ bubbles: true,
567
+ cancelable: true,
568
+ }),
569
+ );
570
+ });
571
+ const addContext = Array.from(
572
+ document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
573
+ ).find((item) => item.textContent === "Add context")!;
574
+ await act(async () => {
575
+ addContext.focus();
576
+ addContext.dispatchEvent(
577
+ new KeyboardEvent("keydown", {
578
+ key: "ArrowRight",
579
+ bubbles: true,
580
+ cancelable: true,
581
+ }),
582
+ );
583
+ });
584
+ const integrations = Array.from(
585
+ document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
586
+ ).find((item) => item.textContent === "Integrations")!;
587
+ expect(integrations.textContent).toBe("Integrations");
588
+ await act(async () => {
589
+ integrations.focus();
590
+ integrations.dispatchEvent(
591
+ new KeyboardEvent("keydown", {
592
+ key: "ArrowRight",
593
+ bubbles: true,
594
+ cancelable: true,
595
+ }),
596
+ );
597
+ });
598
+ const github = document.querySelector<HTMLElement>(
599
+ '[role="menuitemcheckbox"]',
600
+ )!;
601
+ expect(github.textContent).toBe("GitHub");
602
+ await act(async () => github.click());
603
+ expect(onSelect).toHaveBeenCalledOnce();
604
+ expect(editor.textContent).toBe("Keep my draft ");
605
+ expect(document.querySelector('[role="menu"]')).toBeNull();
606
+ });
607
+ it("adds discovered references from + and restores draft focus without submitting", async () => {
608
+ const onReferencesChange = vi.fn();
609
+ const { onSubmit } = await mount({
610
+ initialText: "My draft ",
611
+ contextMenuItems: [],
612
+ includeDefaultMentionSearch: false,
613
+ onReferencesChange,
614
+ mentionItems: [
615
+ {
616
+ id: "agent:slides",
617
+ label: "Slides",
618
+ description: "Presentations in this workspace",
619
+ section: "Connected Agents",
620
+ source: "agent",
621
+ refType: "agent",
622
+ refId: "slides",
623
+ refPath: "https://slides.example.test",
624
+ },
625
+ ],
626
+ });
627
+ const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
628
+ const target = container.querySelector<HTMLButtonElement>(
629
+ 'button[aria-label="Add context"]',
630
+ )!;
631
+ await act(async () => {
632
+ target.focus();
633
+ target.dispatchEvent(
634
+ new KeyboardEvent("keydown", {
635
+ key: "ArrowDown",
636
+ bubbles: true,
637
+ cancelable: true,
638
+ }),
639
+ );
640
+ });
641
+ const addContext = Array.from(
642
+ document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
643
+ ).find((item) => item.textContent === "Add context");
644
+ if (addContext) {
236
645
  await act(async () => {
237
646
  addContext.focus();
238
647
  addContext.dispatchEvent(
@@ -243,13 +652,14 @@ describe("controlled composer context", () => {
243
652
  }),
244
653
  );
245
654
  });
246
- const integrations = Array.from(
247
- document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
248
- ).find((item) => item.textContent === "Integrations")!;
249
- expect(integrations.textContent).toBe("Integrations");
655
+ }
656
+ const section = Array.from(
657
+ document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
658
+ ).find((item) => item.textContent === "Connected Agents");
659
+ if (section) {
250
660
  await act(async () => {
251
- integrations.focus();
252
- integrations.dispatchEvent(
661
+ section.focus();
662
+ section.dispatchEvent(
253
663
  new KeyboardEvent("keydown", {
254
664
  key: "ArrowRight",
255
665
  bubbles: true,
@@ -257,109 +667,30 @@ describe("controlled composer context", () => {
257
667
  }),
258
668
  );
259
669
  });
260
- const github = document.querySelector<HTMLElement>(
261
- '[role="menuitemcheckbox"]',
262
- )!;
263
- expect(github.textContent).toBe("GitHub");
264
- await act(async () => github.click());
265
- expect(onSelect).toHaveBeenCalledOnce();
266
- expect(editor.textContent).toBe("Keep my draft ");
267
- expect(document.querySelector('[role="menu"]')).toBeNull();
268
- },
269
- );
270
- it.each(["@", "+"])(
271
- "adds discovered references from %s and restores draft focus without submitting",
272
- async (trigger) => {
273
- const onReferencesChange = vi.fn();
274
- const { onSubmit } = await mount({
275
- initialText: "My draft ",
276
- contextMenuItems: [],
277
- includeDefaultMentionSearch: false,
278
- onReferencesChange,
279
- mentionItems: [
280
- {
281
- id: "agent:slides",
282
- label: "Slides",
283
- description: "Presentations in this workspace",
284
- section: "Connected Agents",
285
- source: "agent",
286
- refType: "agent",
287
- refId: "slides",
288
- refPath: "https://slides.example.test",
289
- },
290
- ],
291
- });
292
- const editor = container.querySelector<HTMLElement>(".ProseMirror")!;
293
- const target =
294
- trigger === "@"
295
- ? editor
296
- : container.querySelector<HTMLButtonElement>(
297
- 'button[aria-label="Add context"]',
298
- )!;
299
- await act(async () => {
300
- target.focus();
301
- target.dispatchEvent(
302
- new KeyboardEvent("keydown", {
303
- key: trigger === "@" ? "@" : "ArrowDown",
304
- bubbles: true,
305
- cancelable: true,
306
- }),
307
- );
308
- });
309
- const addContext = Array.from(
310
- document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
311
- ).find((item) => item.textContent === "Add context");
312
- if (addContext) {
313
- await act(async () => {
314
- addContext.focus();
315
- addContext.dispatchEvent(
316
- new KeyboardEvent("keydown", {
317
- key: "ArrowRight",
318
- bubbles: true,
319
- cancelable: true,
320
- }),
321
- );
322
- });
323
- }
324
- const section = Array.from(
325
- document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
326
- ).find((item) => item.textContent === "Connected Agents");
327
- if (section) {
328
- await act(async () => {
329
- section.focus();
330
- section.dispatchEvent(
331
- new KeyboardEvent("keydown", {
332
- key: "ArrowRight",
333
- bubbles: true,
334
- cancelable: true,
335
- }),
336
- );
337
- });
338
- }
339
- const row = Array.from(
340
- document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
341
- ).find(
342
- (item) => item.textContent === "SlidesPresentations in this workspace",
343
- )!;
344
- expect(row).toBeDefined();
345
- await act(async () => row.click());
346
- await act(async () => {
347
- await new Promise((resolve) => requestAnimationFrame(resolve));
348
- });
349
- expect(editor.textContent).toContain("My draft");
350
- expect(editor.textContent).toContain("Slides");
351
- expect(editor.textContent).not.toContain("@");
352
- expect(onReferencesChange).toHaveBeenLastCalledWith([
353
- expect.objectContaining({
354
- refType: "agent",
355
- refId: "slides",
356
- path: "https://slides.example.test",
357
- }),
358
- ]);
359
- expect(document.activeElement).toBe(editor);
360
- expect(onSubmit).not.toHaveBeenCalled();
361
- },
362
- );
670
+ }
671
+ const row = Array.from(
672
+ document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
673
+ ).find(
674
+ (item) => item.textContent === "SlidesPresentations in this workspace",
675
+ )!;
676
+ expect(row).toBeDefined();
677
+ await act(async () => row.click());
678
+ await act(async () => {
679
+ await new Promise((resolve) => requestAnimationFrame(resolve));
680
+ });
681
+ expect(editor.textContent).toContain("My draft");
682
+ expect(editor.textContent).toContain("Slides");
683
+ expect(editor.textContent).not.toContain("@");
684
+ expect(onReferencesChange).toHaveBeenLastCalledWith([
685
+ expect.objectContaining({
686
+ refType: "agent",
687
+ refId: "slides",
688
+ path: "https://slides.example.test",
689
+ }),
690
+ ]);
691
+ expect(document.activeElement).toBe(editor);
692
+ expect(onSubmit).not.toHaveBeenCalled();
693
+ });
363
694
  it("keeps regular @ references when the shared Add menu is explicitly hidden", async () => {
364
695
  await mount({
365
696
  plusMenuMode: "hidden",