@agent-native/toolkit 0.225.1 → 0.226.0-nightly-20261106182619

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 (174) hide show
  1. package/agent-native.eject.json +1 -0
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +124 -9
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +1 -1
  6. package/dist/app/agentkit/react/core-file-storage.d.ts +21 -0
  7. package/dist/app/agentkit/react/core-file-storage.d.ts.map +1 -0
  8. package/dist/app/agentkit/react/core-file-storage.js +3 -0
  9. package/dist/app/agentkit/react/core-file-storage.js.map +1 -0
  10. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  11. package/dist/app/auth/AuthPage.js +89 -10
  12. package/dist/app/auth/AuthPage.js.map +1 -1
  13. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  14. package/dist/app/chat/AgentKitAssistantChat.js +39 -58
  15. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  16. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  17. package/dist/app/chat/MultiTabAssistantChat.js +3 -1
  18. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  19. package/dist/app/chat/agentkit-chat/root.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/root.js +66 -2
  21. package/dist/app/chat/agentkit-chat/root.js.map +1 -1
  22. package/dist/app/chat/chat/surface-types.d.ts +1 -0
  23. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  24. package/dist/app/chat/chat/surface-types.js.map +1 -1
  25. package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
  26. package/dist/app/chat/composer/runtime-adapters.js +2 -0
  27. package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
  28. package/dist/app/chat/composer/use-agentkit-capabilities.d.ts +3 -1
  29. package/dist/app/chat/composer/use-agentkit-capabilities.d.ts.map +1 -1
  30. package/dist/app/chat/composer/use-agentkit-capabilities.js +11 -11
  31. package/dist/app/chat/composer/use-agentkit-capabilities.js.map +1 -1
  32. package/dist/app/chat/composer/use-workspace-composer-menu.d.ts +3 -0
  33. package/dist/app/chat/composer/use-workspace-composer-menu.d.ts.map +1 -0
  34. package/dist/app/chat/composer/use-workspace-composer-menu.js +80 -0
  35. package/dist/app/chat/composer/use-workspace-composer-menu.js.map +1 -0
  36. package/dist/app/chat/index.d.ts +1 -0
  37. package/dist/app/chat/index.d.ts.map +1 -1
  38. package/dist/app/chat/index.js +1 -0
  39. package/dist/app/chat/index.js.map +1 -1
  40. package/dist/app/chat/prepare-prompt-composer-agent-handoff.d.ts +5 -0
  41. package/dist/app/chat/prepare-prompt-composer-agent-handoff.d.ts.map +1 -0
  42. package/dist/app/chat/prepare-prompt-composer-agent-handoff.js +108 -0
  43. package/dist/app/chat/prepare-prompt-composer-agent-handoff.js.map +1 -0
  44. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  45. package/dist/app/i18n/catalogs/ar-SA.js +6 -1
  46. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  47. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  48. package/dist/app/i18n/catalogs/de-DE.js +6 -1
  49. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  50. package/dist/app/i18n/catalogs/en-US.d.ts +6 -1
  51. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/en-US.js +6 -1
  53. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  54. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/es-ES.js +6 -1
  56. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  57. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/fr-FR.js +6 -1
  59. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/hi-IN.js +6 -1
  62. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  63. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/ja-JP.js +6 -1
  65. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  66. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  67. package/dist/app/i18n/catalogs/ko-KR.js +6 -1
  68. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  69. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  70. package/dist/app/i18n/catalogs/pt-BR.js +6 -1
  71. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  72. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  73. package/dist/app/i18n/catalogs/zh-CN.js +6 -1
  74. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  75. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  76. package/dist/app/i18n/catalogs/zh-TW.js +6 -1
  77. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  78. package/dist/app/resources/McpIntegrationDialog.d.ts +3 -1
  79. package/dist/app/resources/McpIntegrationDialog.d.ts.map +1 -1
  80. package/dist/app/resources/McpIntegrationDialog.js +2 -2
  81. package/dist/app/resources/McpIntegrationDialog.js.map +1 -1
  82. package/dist/app/settings/useBuilderStatus.d.ts.map +1 -1
  83. package/dist/app/settings/useBuilderStatus.js +119 -40
  84. package/dist/app/settings/useBuilderStatus.js.map +1 -1
  85. package/dist/composer/ComposerContextMenu.d.ts +36 -2
  86. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  87. package/dist/composer/ComposerContextMenu.js +108 -40
  88. package/dist/composer/ComposerContextMenu.js.map +1 -1
  89. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -1
  90. package/dist/composer/ComposerContextPickerDialog.js +63 -26
  91. package/dist/composer/ComposerContextPickerDialog.js.map +1 -1
  92. package/dist/composer/ComposerPlusMenu.d.ts +5 -3
  93. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  94. package/dist/composer/ComposerPlusMenu.js +166 -121
  95. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  96. package/dist/composer/MentionItemMedia.d.ts.map +1 -1
  97. package/dist/composer/MentionItemMedia.js +3 -1
  98. package/dist/composer/MentionItemMedia.js.map +1 -1
  99. package/dist/composer/PromptComposer.d.ts +1 -0
  100. package/dist/composer/PromptComposer.d.ts.map +1 -1
  101. package/dist/composer/PromptComposer.js +1 -0
  102. package/dist/composer/PromptComposer.js.map +1 -1
  103. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  104. package/dist/composer/TiptapComposer.js +117 -50
  105. package/dist/composer/TiptapComposer.js.map +1 -1
  106. package/dist/composer/index.d.ts +1 -0
  107. package/dist/composer/index.d.ts.map +1 -1
  108. package/dist/composer/runtime-adapters.d.ts +15 -0
  109. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  110. package/dist/composer/runtime-adapters.js.map +1 -1
  111. package/dist/composer/use-composer-panel-placement.d.ts.map +1 -1
  112. package/dist/composer/use-composer-panel-placement.js +2 -4
  113. package/dist/composer/use-composer-panel-placement.js.map +1 -1
  114. package/dist/composer/use-mention-search.d.ts +7 -0
  115. package/dist/composer/use-mention-search.d.ts.map +1 -1
  116. package/dist/composer/use-mention-search.js +86 -19
  117. package/dist/composer/use-mention-search.js.map +1 -1
  118. package/dist/composer/useComposerContextPicker.d.ts +5 -1
  119. package/dist/composer/useComposerContextPicker.d.ts.map +1 -1
  120. package/dist/composer/useComposerContextPicker.js +18 -4
  121. package/dist/composer/useComposerContextPicker.js.map +1 -1
  122. package/package.json +4 -4
  123. package/src/app/agentkit/react/components.tsx +158 -11
  124. package/src/app/agentkit/react/composer-context.spec.tsx +180 -0
  125. package/src/app/agentkit/react/core-file-storage.tsx +14 -0
  126. package/src/app/auth/AuthPage.interaction.spec.tsx +143 -0
  127. package/src/app/auth/AuthPage.tsx +159 -13
  128. package/src/app/chat/AgentKitAssistantChat.spec.tsx +65 -3
  129. package/src/app/chat/AgentKitAssistantChat.tsx +57 -65
  130. package/src/app/chat/MultiTabAssistantChat.spec.tsx +21 -0
  131. package/src/app/chat/MultiTabAssistantChat.tsx +4 -0
  132. package/src/app/chat/agentkit-chat/root.tsx +91 -8
  133. package/src/app/chat/chat/surface-types.ts +1 -0
  134. package/src/app/chat/composer/runtime-adapters.tsx +2 -0
  135. package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +26 -1
  136. package/src/app/chat/composer/use-agentkit-capabilities.ts +11 -11
  137. package/src/app/chat/composer/use-agentkit-integration-menu.spec.tsx +2 -4
  138. package/src/app/chat/composer/use-workspace-composer-menu.spec.tsx +174 -0
  139. package/src/app/chat/composer/use-workspace-composer-menu.tsx +122 -0
  140. package/src/app/chat/index.ts +1 -0
  141. package/src/app/chat/prepare-prompt-composer-agent-handoff.spec.ts +162 -0
  142. package/src/app/chat/prepare-prompt-composer-agent-handoff.ts +134 -0
  143. package/src/app/i18n/catalogs/ar-SA.ts +6 -1
  144. package/src/app/i18n/catalogs/de-DE.ts +6 -1
  145. package/src/app/i18n/catalogs/en-US.ts +6 -1
  146. package/src/app/i18n/catalogs/es-ES.ts +6 -1
  147. package/src/app/i18n/catalogs/fr-FR.ts +6 -1
  148. package/src/app/i18n/catalogs/hi-IN.ts +6 -1
  149. package/src/app/i18n/catalogs/ja-JP.ts +6 -1
  150. package/src/app/i18n/catalogs/ko-KR.ts +6 -1
  151. package/src/app/i18n/catalogs/pt-BR.ts +6 -1
  152. package/src/app/i18n/catalogs/zh-CN.ts +6 -1
  153. package/src/app/i18n/catalogs/zh-TW.ts +6 -1
  154. package/src/app/resources/McpIntegrationDialog.tsx +5 -0
  155. package/src/app/settings/useBuilderStatus.spec.tsx +229 -13
  156. package/src/app/settings/useBuilderStatus.ts +145 -47
  157. package/src/composer/ComposerContextMenu.spec.tsx +95 -32
  158. package/src/composer/ComposerContextMenu.tsx +284 -73
  159. package/src/composer/ComposerContextPicker.spec.tsx +13 -14
  160. package/src/composer/ComposerContextPickerDialog.spec.tsx +40 -3
  161. package/src/composer/ComposerContextPickerDialog.tsx +158 -24
  162. package/src/composer/ComposerPlusMenu.parity.spec.tsx +442 -62
  163. package/src/composer/ComposerPlusMenu.tsx +322 -237
  164. package/src/composer/MentionItemMedia.tsx +3 -0
  165. package/src/composer/PromptComposer.context.spec.tsx +253 -82
  166. package/src/composer/PromptComposer.tsx +2 -0
  167. package/src/composer/TiptapComposer.spec.ts +6 -8
  168. package/src/composer/TiptapComposer.tsx +148 -55
  169. package/src/composer/index.ts +5 -0
  170. package/src/composer/runtime-adapters.tsx +18 -0
  171. package/src/composer/use-composer-panel-placement.ts +2 -4
  172. package/src/composer/use-mention-search.spec.tsx +99 -1
  173. package/src/composer/use-mention-search.ts +101 -24
  174. package/src/composer/useComposerContextPicker.ts +25 -5
@@ -1,10 +1,12 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
3
  import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
4
+ import { waitFor } from "@testing-library/react";
4
5
  import React, { act } from "react";
5
6
  import { createRoot, type Root } from "react-dom/client";
6
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
8
 
9
+ import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
8
10
  import { TooltipProvider } from "../ui/tooltip.js";
9
11
  import { mergeComposerMenuItems } from "./ComposerPlusMenu.js";
10
12
  import { PromptComposer, type PromptComposerProps } from "./PromptComposer.js";
@@ -99,7 +101,7 @@ async function mount(
99
101
  });
100
102
  return { composerRef, onSubmit };
101
103
  }
102
- async function open(enterAddContext = false) {
104
+ async function open(_enterAddContext = false) {
103
105
  const target = container.querySelector<HTMLElement>(
104
106
  'button[aria-label="Add context"]',
105
107
  );
@@ -115,13 +117,13 @@ async function open(enterAddContext = false) {
115
117
  );
116
118
  await settle();
117
119
  });
118
- if (enterAddContext && labels().includes("Add context"))
119
- await choose("Add context");
120
120
  }
121
121
  function item(label: string) {
122
122
  const row = Array.from(
123
123
  document.querySelectorAll<HTMLElement>('[role^="menuitem"]'),
124
- ).find((row) => row.textContent?.trim() === label);
124
+ )
125
+ .reverse()
126
+ .find((row) => row.textContent?.trim() === label);
125
127
  expect(row, label).toBeDefined();
126
128
  return row!;
127
129
  }
@@ -159,7 +161,6 @@ function labels() {
159
161
  );
160
162
  }
161
163
  async function uploadSkill() {
162
- await choose("Create Skill");
163
164
  await choose("Upload skill file");
164
165
  const input = container.querySelector<HTMLInputElement>(
165
166
  'input[accept=".md,text/markdown"]',
@@ -172,11 +173,11 @@ async function uploadSkill() {
172
173
  input.dispatchEvent(new Event("change", { bubbles: true }));
173
174
  await settle();
174
175
  });
175
- return document.querySelector<HTMLFormElement>('[role="menu"] form')!;
176
+ return document.querySelector<HTMLFormElement>('[role="dialog"] form')!;
176
177
  }
177
178
 
178
179
  describe("shared default action preservation", () => {
179
- it("preserves host action IDs and replaces defaults even inside a capability group", () => {
180
+ it("preserves overridden actions at the shared command slot without empty host groups", () => {
180
181
  const defaults = [
181
182
  { id: "integrations", label: "Setup", onSelect: vi.fn() },
182
183
  { id: "create-skill", label: "Skill", onSelect: vi.fn() },
@@ -191,8 +192,203 @@ describe("shared default action preservation", () => {
191
192
  },
192
193
  ];
193
194
  const merged = mergeComposerMenuItems(defaults, provided);
194
- expect(merged).toEqual([provided[0], defaults[1]]);
195
- expect(merged[0]).toBe(provided[0]);
195
+ expect(merged).toEqual([provided[0].children[0], defaults[1]]);
196
+ });
197
+
198
+ it("keeps host context at the root and keeps nested integration overrides in their labeled section", async () => {
199
+ const selectContext = vi.fn();
200
+ const selectIntegration = vi.fn();
201
+ await mount({
202
+ contextMenuItems: [
203
+ {
204
+ id: "host-group",
205
+ label: "Workspace",
206
+ children: [
207
+ {
208
+ id: "reference",
209
+ label: "Reference",
210
+ icon: <svg data-testid="reference-icon" />,
211
+ onSelect: selectContext,
212
+ },
213
+ {
214
+ id: "integrations",
215
+ label: "Use integration",
216
+ order: -100,
217
+ children: [
218
+ {
219
+ id: "invoke",
220
+ label: "Invoke service",
221
+ intent: "invoke-integration",
222
+ onSelect: selectIntegration,
223
+ },
224
+ ],
225
+ },
226
+ ],
227
+ },
228
+ ],
229
+ });
230
+ await open();
231
+ expect(labels()).toEqual([
232
+ "Files and images",
233
+ "Workspace",
234
+ "Use integration",
235
+ "Schedule Task",
236
+ "Create Automation",
237
+ "Create Skill",
238
+ "Upload skill file",
239
+ ]);
240
+ await choose("Workspace");
241
+ expect(
242
+ item("Reference").querySelector('[data-testid="reference-icon"]'),
243
+ ).not.toBeNull();
244
+ expect(
245
+ Array.from(document.querySelectorAll('[role="menu"]')).at(-1)
246
+ ?.textContent,
247
+ ).not.toContain("Use integration");
248
+ await choose("Reference");
249
+ expect(selectContext).toHaveBeenCalledOnce();
250
+ await open();
251
+ expect(
252
+ document.querySelector('[role="group"][aria-labelledby]'),
253
+ ).not.toBeNull();
254
+ expect(labels()).toContain("Use integration");
255
+ expect(
256
+ labels().filter((label) => label === "Use integration"),
257
+ ).toHaveLength(1);
258
+ expect(labels()).not.toContain("Integrations");
259
+ await choose("Use integration");
260
+ await choose("Invoke service");
261
+ expect(selectIntegration).toHaveBeenCalledOnce();
262
+ expect(document.querySelector('[role="dialog"]')).toBeNull();
263
+ });
264
+
265
+ it("preserves disabled ancestry when host commands replace defaults", async () => {
266
+ await mount({
267
+ contextMenuItems: [
268
+ {
269
+ id: "disabled",
270
+ label: "Unavailable",
271
+ disabled: true,
272
+ children: [
273
+ { id: "integrations", label: "Use integration", onSelect: vi.fn() },
274
+ ],
275
+ },
276
+ ],
277
+ });
278
+ await open();
279
+ expect(labels()).toEqual([
280
+ "Files and images",
281
+ "Use integration",
282
+ "Schedule Task",
283
+ "Create Automation",
284
+ "Create Skill",
285
+ "Upload skill file",
286
+ ]);
287
+ expect(item("Use integration").getAttribute("aria-disabled")).toBe("true");
288
+ });
289
+
290
+ it("routes nested integration commands separately from context siblings", async () => {
291
+ const invoke = vi.fn();
292
+ await mount({
293
+ contextMenuItems: [
294
+ {
295
+ id: "workspace",
296
+ label: "Workspace",
297
+ children: [
298
+ { id: "reference", label: "Reference", onSelect: vi.fn() },
299
+ {
300
+ id: "invoke",
301
+ label: "Use service",
302
+ intent: "invoke-integration",
303
+ onSelect: invoke,
304
+ },
305
+ ],
306
+ },
307
+ ],
308
+ });
309
+ await open();
310
+ await choose("Workspace");
311
+ expect(labels()).toContain("Reference");
312
+ expect(
313
+ Array.from(document.querySelectorAll('[role="menu"]')).at(-1)
314
+ ?.textContent,
315
+ ).not.toContain("Use service");
316
+ await act(async () =>
317
+ item("Reference").dispatchEvent(
318
+ new KeyboardEvent("keydown", {
319
+ key: "Escape",
320
+ bubbles: true,
321
+ cancelable: true,
322
+ }),
323
+ ),
324
+ );
325
+ await choose("Use service");
326
+ expect(invoke).toHaveBeenCalledOnce();
327
+ });
328
+
329
+ it("distinguishes file uploads from saved file references", async () => {
330
+ const { onSubmit, composerRef } = await mount({
331
+ mentionItems: [
332
+ {
333
+ id: "other",
334
+ label: "Other reference",
335
+ source: "resource",
336
+ refType: "custom",
337
+ section: "Other",
338
+ },
339
+ {
340
+ id: "connected",
341
+ label: "Connected agent",
342
+ source: "resource",
343
+ refType: "agent",
344
+ section: "Connected Agents",
345
+ },
346
+ {
347
+ id: "agent",
348
+ label: "Local agent",
349
+ source: "resource",
350
+ refType: "agent",
351
+ section: "Agents",
352
+ },
353
+ {
354
+ id: "brief",
355
+ label: "Launch brief",
356
+ source: "resource",
357
+ refType: "file",
358
+ refPath: "brief.md",
359
+ section: "Files",
360
+ icon: "file",
361
+ },
362
+ ],
363
+ });
364
+ await open();
365
+ expect(labels()).toEqual([
366
+ "Files and images",
367
+ "Search workspace…",
368
+ "Integrations",
369
+ "Schedule Task",
370
+ "Create Automation",
371
+ "Create Skill",
372
+ "Upload skill file",
373
+ ]);
374
+ await choose("Search workspace…");
375
+ expect(document.querySelector('[role="dialog"]')).not.toBeNull();
376
+ const option = Array.from(
377
+ document.querySelectorAll<HTMLElement>('[role="option"]'),
378
+ ).find((element) => element.textContent === "Launch brief")!;
379
+ expect(option).toBeDefined();
380
+ await act(async () => option.click());
381
+ await act(async () => {
382
+ await composerRef.current?.submit?.();
383
+ });
384
+ expect(onSubmit).toHaveBeenCalledWith(
385
+ expect.any(String),
386
+ expect.any(Array),
387
+ expect.arrayContaining([
388
+ expect.objectContaining({ name: "Launch brief", path: "brief.md" }),
389
+ ]),
390
+ expect.anything(),
391
+ );
196
392
  });
197
393
 
198
394
  it.each(["full", "upload-only", "terminal", "hidden"] as const)(
@@ -212,15 +408,16 @@ describe("shared default action preservation", () => {
212
408
  const expected =
213
409
  mode === "full"
214
410
  ? [
215
- "Upload File",
411
+ "Files and images",
412
+ "Integrations",
216
413
  "Schedule Task",
217
414
  "Create Automation",
218
- "Integrations",
219
415
  "Create Skill",
416
+ "Upload skill file",
220
417
  ]
221
418
  : mode === "terminal"
222
419
  ? ["New terminal", "CLI terminal mode"]
223
- : ["Upload File"];
420
+ : ["Files and images"];
224
421
  expect(labels()).toEqual(expected);
225
422
  }
226
423
  if (mode === "full") {
@@ -234,11 +431,202 @@ describe("shared default action preservation", () => {
234
431
  },
235
432
  );
236
433
 
434
+ it("hands focus to a contributed dialog and restores the draft only after it closes", async () => {
435
+ adapters.resources!.useWorkspaceMenu = function useWorkspaceMenu({
436
+ onRestoreFocus,
437
+ }) {
438
+ const [visible, setVisible] = React.useState(false);
439
+ return {
440
+ scopeKey: "one",
441
+ items: [
442
+ {
443
+ id: "connect",
444
+ label: "Connect workspace",
445
+ section: "integrations",
446
+ opensDialog: true,
447
+ onSelect: () => setVisible(true),
448
+ },
449
+ ],
450
+ overlays: (
451
+ <Dialog open={visible} onOpenChange={setVisible}>
452
+ <DialogContent
453
+ aria-describedby={undefined}
454
+ onCloseAutoFocus={(event) => {
455
+ event.preventDefault();
456
+ onRestoreFocus?.();
457
+ }}
458
+ >
459
+ <DialogTitle>Connect workspace</DialogTitle>
460
+ <button onClick={() => setVisible(false)}>
461
+ Finish connection
462
+ </button>
463
+ </DialogContent>
464
+ </Dialog>
465
+ ),
466
+ };
467
+ };
468
+ await mount();
469
+ await open();
470
+ await choose("Connect workspace");
471
+ const dialog = document.querySelector('[role="dialog"]');
472
+ expect(dialog).not.toBeNull();
473
+ expect(dialog?.contains(document.activeElement)).toBe(true);
474
+ await act(async () => {
475
+ const done = Array.from(
476
+ document.querySelectorAll<HTMLButtonElement>('[role="dialog"] button'),
477
+ ).find((button) => button.textContent === "Finish connection");
478
+ if (!done) throw new Error("Missing connection control");
479
+ done.click();
480
+ await settle();
481
+ });
482
+ await act(async () => {
483
+ await new Promise(requestAnimationFrame);
484
+ });
485
+ await waitFor(() =>
486
+ expect(document.activeElement).toBe(
487
+ container.querySelector(".ProseMirror"),
488
+ ),
489
+ );
490
+ });
491
+
492
+ it.each(["full", "upload-only"] as const)(
493
+ "merges workspace contributions into labeled groups in %s and inserts their references",
494
+ async (mode) => {
495
+ const useWorkspaceMenu = vi.fn(
496
+ ({
497
+ onSelectIntegration,
498
+ }: {
499
+ enabled: boolean;
500
+ onSelectIntegration: (reference: {
501
+ label: string;
502
+ source: string;
503
+ refType: string;
504
+ refId: string;
505
+ }) => void;
506
+ }) => ({
507
+ scopeKey: "org:one",
508
+ items: [
509
+ {
510
+ id: "calendar",
511
+ label: "Use calendar",
512
+ section: "integrations" as const,
513
+ onSelect: () =>
514
+ onSelectIntegration({
515
+ label: "Calendar",
516
+ source: "workspace-integration",
517
+ refType: "integration",
518
+ refId: "calendar",
519
+ }),
520
+ },
521
+ ],
522
+ }),
523
+ );
524
+ adapters.resources!.useWorkspaceMenu = useWorkspaceMenu;
525
+ const { composerRef, onSubmit } = await mount({
526
+ plusMenuMode: mode,
527
+ contextMenuItems: [
528
+ {
529
+ id: "brief",
530
+ label: "Reference brief",
531
+ section: "context",
532
+ onSelect: vi.fn(),
533
+ },
534
+ ],
535
+ });
536
+ expect(useWorkspaceMenu).toHaveBeenLastCalledWith(
537
+ expect.objectContaining({ enabled: false }),
538
+ );
539
+ await open();
540
+ expect(useWorkspaceMenu).toHaveBeenLastCalledWith(
541
+ expect.objectContaining({ enabled: true }),
542
+ );
543
+ const groups = Array.from(
544
+ document.querySelectorAll<HTMLElement>(
545
+ '[role="group"][aria-labelledby]',
546
+ ),
547
+ );
548
+ expect(
549
+ groups.map(
550
+ (group) =>
551
+ document.getElementById(group.getAttribute("aria-labelledby")!)
552
+ ?.textContent,
553
+ ),
554
+ ).toEqual(
555
+ mode === "full"
556
+ ? ["Add context", "Integrations", "Commands"]
557
+ : ["Add context", "Integrations"],
558
+ );
559
+ expect(groups[1].textContent).toContain("Use calendar");
560
+ expect(groups[1].textContent).not.toContain("Reference brief");
561
+ expect(labels()).not.toContain("Integrations");
562
+ await choose("Use calendar");
563
+ await act(async () => composerRef.current!.submit());
564
+ expect(onSubmit.mock.calls[0][2]).toEqual(
565
+ expect.arrayContaining([
566
+ expect.objectContaining({
567
+ refType: "integration",
568
+ refId: "calendar",
569
+ }),
570
+ ]),
571
+ );
572
+ },
573
+ );
574
+
575
+ it.each(["upload-only", "hidden"] as const)(
576
+ "keeps contributed integration commands separate in %s without adding default modes",
577
+ async (mode) => {
578
+ const attach = vi.fn();
579
+ const invoke = vi.fn();
580
+ await mount({
581
+ plusMenuMode: mode,
582
+ contextMenuItems: [
583
+ {
584
+ id: "source",
585
+ label: "Reference brief",
586
+ order: 20,
587
+ onSelect: attach,
588
+ },
589
+ {
590
+ id: "workspace-tools",
591
+ label: "Workspace tools",
592
+ intent: "invoke-integration",
593
+ children: [
594
+ { id: "provider", label: "Connected provider", onSelect: invoke },
595
+ ],
596
+ },
597
+ ],
598
+ });
599
+ await open();
600
+ expect(labels()).toEqual([
601
+ "Files and images",
602
+ "Reference brief",
603
+ "Workspace tools",
604
+ ]);
605
+ expect(labels()).toContain("Workspace tools");
606
+ for (const label of [
607
+ "Schedule Task",
608
+ "Create Automation",
609
+ "Create Skill",
610
+ "Upload skill file",
611
+ "Integrations",
612
+ ])
613
+ expect(labels()).not.toContain(label);
614
+ await choose("Workspace tools");
615
+ await choose("Connected provider");
616
+ expect(invoke).toHaveBeenCalledOnce();
617
+ expect(attach).not.toHaveBeenCalled();
618
+ expect(useOrg).not.toHaveBeenCalled();
619
+ await open();
620
+ await choose("Reference brief");
621
+ expect(attach).toHaveBeenCalledOnce();
622
+ },
623
+ );
624
+
237
625
  it.each([
238
626
  ["Schedule Task", "Create a recurring job: Draft", "manage-jobs"],
239
627
  ["Create Automation", "Create an automation: Draft", "manage-automations"],
240
628
  ["Create Extension", "Create an extension: Draft", "create-extension"],
241
- ["Create new skill", "Create a skill: Draft", "resources"],
629
+ ["Create Skill", "Create a skill: Draft", "resources"],
242
630
  ])(
243
631
  "+ submits %s instructions through the accepted host path",
244
632
  async (action, message, context) => {
@@ -249,8 +637,7 @@ describe("shared default action preservation", () => {
249
637
  extensionTools: true,
250
638
  });
251
639
  await open();
252
- expect(labels()).not.toContain("Host source");
253
- if (action === "Create new skill") await choose("Create Skill");
640
+ expect(labels()).toContain("Host source");
254
641
  await choose(action);
255
642
  expect(
256
643
  container.querySelector('[data-agent-composer-slot="mode-row"]'),
@@ -331,14 +718,14 @@ describe("shared default action preservation", () => {
331
718
  await mount({ attachmentsEnabled: false, onAttachmentRequest });
332
719
  await open();
333
720
  expect(onAttachmentRequest).not.toHaveBeenCalled();
334
- await choose("Upload File");
721
+ await choose("Files and images");
335
722
  await act(async () => {
336
723
  await new Promise(requestAnimationFrame);
337
724
  });
338
725
  expect(onAttachmentRequest).toHaveBeenCalledOnce();
339
726
  await mount({ attachmentsEnabled: false });
340
727
  await open();
341
- expect(labels()).not.toContain("Upload File");
728
+ expect(labels()).not.toContain("Files and images");
342
729
  await close();
343
730
  await mount();
344
731
  const input = container.querySelector<HTMLInputElement>(
@@ -346,7 +733,7 @@ describe("shared default action preservation", () => {
346
733
  )!;
347
734
  const click = vi.spyOn(input, "click").mockImplementation(() => {});
348
735
  await open();
349
- await choose("Upload File");
736
+ await choose("Files and images");
350
737
  expect(click).toHaveBeenCalledOnce();
351
738
  });
352
739
 
@@ -434,7 +821,7 @@ describe("shared default action preservation", () => {
434
821
  contextMenuItems: [{ id: "host", label: "Host source", onSelect }],
435
822
  });
436
823
  await open(true);
437
- expect(labels()).toEqual(["Upload File", "Add context", "Host source"]);
824
+ expect(labels()).toEqual(["Files and images", "Host source"]);
438
825
  await choose("Host source");
439
826
  expect(onSelect).toHaveBeenCalledOnce();
440
827
  expect(useOrg).not.toHaveBeenCalled();
@@ -659,24 +1046,32 @@ describe("shared default action preservation", () => {
659
1046
  );
660
1047
  });
661
1048
 
662
- it("keeps chat actions at the root, nests context sources, and omits Generate Image", async () => {
1049
+ it("labels chat actions as Commands and keeps context at the root", async () => {
663
1050
  await mount({
664
1051
  contextMenuItems: [
665
1052
  { id: "host-source", label: "Host source", onSelect: vi.fn() },
666
1053
  ],
667
1054
  });
668
1055
  await open();
1056
+ expect(labels()).toEqual([
1057
+ "Files and images",
1058
+ "Host source",
1059
+ "Integrations",
1060
+ "Schedule Task",
1061
+ "Create Automation",
1062
+ "Create Skill",
1063
+ "Upload skill file",
1064
+ ]);
669
1065
  expect(labels()).toContain("Schedule Task");
670
1066
  expect(labels()).toContain("Create Automation");
671
1067
  expect(labels()).toContain("Create Skill");
672
1068
  expect(labels()).not.toContain("Generate Image");
673
- expect(labels()).not.toContain("Host source");
674
- await choose("Add context");
1069
+ expect(labels()).toContain("Host source");
675
1070
  expect(labels()).toContain("Host source");
676
1071
  expect(labels()).toContain("Schedule Task");
677
1072
  });
678
1073
 
679
- it("cancels a skill save when its submenu closes and retains retry after errors", async () => {
1074
+ it("cancels a skill save when its dialog closes and retains retry after errors", async () => {
680
1075
  let resolve!: (response: Response) => void;
681
1076
  const fetch = vi
682
1077
  .fn()
@@ -717,19 +1112,21 @@ describe("shared default action preservation", () => {
717
1112
  await settle();
718
1113
  });
719
1114
  expect(request.signal?.aborted).toBe(true);
720
- expect(document.querySelector('[role="menu"] form')).toBeNull();
1115
+ expect(document.querySelector('[role="dialog"] form')).toBeNull();
721
1116
  await act(async () => {
722
1117
  resolve(new Response("{}"));
723
1118
  await settle();
724
1119
  });
725
- expect(document.querySelector('[role="status"]')).toBeNull();
1120
+ expect(
1121
+ document.querySelector('[role="status"]')?.textContent,
1122
+ ).not.toContain("added");
1123
+ await open();
726
1124
  expect(item("Upload skill file")).toBeDefined();
727
1125
  });
728
1126
 
729
- it("resumes skill-file review after the native picker dismisses the menu", async () => {
1127
+ it("opens skill-file review in a dialog after the native picker returns", async () => {
730
1128
  await mount();
731
1129
  await open();
732
- await choose("Create Skill");
733
1130
  await choose("Upload skill file");
734
1131
 
735
1132
  await act(async () => {
@@ -751,7 +1148,7 @@ describe("shared default action preservation", () => {
751
1148
  await settle();
752
1149
  });
753
1150
 
754
- const form = document.querySelector<HTMLFormElement>("[role=menu] form");
1151
+ const form = document.querySelector<HTMLFormElement>("[role=dialog] form");
755
1152
  expect(form).not.toBeNull();
756
1153
  expect(form?.textContent).toContain(
757
1154
  "Review the content from Test Skill.md before saving.",
@@ -761,10 +1158,9 @@ describe("shared default action preservation", () => {
761
1158
  );
762
1159
  });
763
1160
 
764
- it("restores the skill submenu when the native picker is canceled", async () => {
1161
+ it("leaves the menu closed when the native picker is canceled", async () => {
765
1162
  await mount();
766
1163
  await open();
767
- await choose("Create Skill");
768
1164
  await choose("Upload skill file");
769
1165
 
770
1166
  await act(async () => {
@@ -779,41 +1175,25 @@ describe("shared default action preservation", () => {
779
1175
  await settle();
780
1176
  });
781
1177
 
782
- expect(labels()).toContain("Upload skill file");
1178
+ expect(document.querySelector('[role="dialog"]')).toBeNull();
1179
+ expect(document.querySelector('[role="menu"]')).toBeNull();
783
1180
  });
784
1181
 
785
- it("does not resume the skill page if the composer is disabled", async () => {
1182
+ it("closes skill review and aborts its pending save when the composer is disabled", async () => {
1183
+ const fetch = vi.fn(() => new Promise<Response>(() => {}));
1184
+ vi.stubGlobal("fetch", fetch);
786
1185
  await mount();
787
1186
  await open();
788
- await choose("Create Skill");
789
- await choose("Upload skill file");
790
-
791
- const input = container.querySelector<HTMLInputElement>(
792
- 'input[accept=".md,text/markdown"]',
793
- )!;
794
- vi.useFakeTimers();
795
- try {
796
- const scheduledTimeouts = vi.spyOn(globalThis, "setTimeout");
797
- const clearedTimeouts = vi.spyOn(globalThis, "clearTimeout");
798
- const timersBeforeCancel = vi.getTimerCount();
799
- const timeoutCallIndex = scheduledTimeouts.mock.calls.length;
800
- act(() => input.dispatchEvent(new Event("cancel")));
801
- expect(vi.getTimerCount()).toBe(timersBeforeCancel + 1);
802
- const resumeTimer =
803
- scheduledTimeouts.mock.results[timeoutCallIndex]?.value;
804
- expect(resumeTimer).toBeDefined();
805
-
806
- await mount({ disabled: true }, false);
807
- expect(clearedTimeouts).toHaveBeenCalledWith(resumeTimer);
808
-
809
- await act(async () => {
810
- await vi.advanceTimersByTimeAsync(0);
811
- });
812
-
813
- expect(document.querySelector("[role=menu] form")).toBeNull();
814
- expect(labels()).not.toContain("Upload skill file");
815
- } finally {
816
- vi.useRealTimers();
817
- }
1187
+ const form = await uploadSkill();
1188
+ await act(async () => {
1189
+ form.dispatchEvent(
1190
+ new Event("submit", { bubbles: true, cancelable: true }),
1191
+ );
1192
+ await settle();
1193
+ });
1194
+ const request = fetch.mock.calls[0][1] as RequestInit;
1195
+ await mount({ disabled: true });
1196
+ expect(request.signal?.aborted).toBe(true);
1197
+ expect(document.querySelector('[role="dialog"]')).toBeNull();
818
1198
  });
819
1199
  });