@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
- import { IconPlus } from "@tabler/icons-react";
1
+ import { IconPaperclip, IconPlus } from "@tabler/icons-react";
2
2
  import {
3
3
  useCallback,
4
+ useId,
4
5
  useEffect,
5
6
  useRef,
6
7
  useState,
7
8
  type ReactNode,
9
+ type ReactElement,
8
10
  } from "react";
9
11
 
10
12
  import { Button } from "../ui/button.js";
@@ -14,12 +16,13 @@ import {
14
16
  DropdownMenuCheckboxItem,
15
17
  DropdownMenuGroup,
16
18
  DropdownMenuItem,
19
+ DropdownMenuLabel,
20
+ DropdownMenuSeparator,
17
21
  DropdownMenuSub,
18
22
  DropdownMenuSubContent,
19
23
  DropdownMenuSubTrigger,
20
24
  DropdownMenuTrigger,
21
25
  } from "../ui/dropdown-menu.js";
22
- import { Skeleton } from "../ui/skeleton.js";
23
26
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
24
27
  import { formatAttachmentError } from "./attachment-accept.js";
25
28
  import {
@@ -49,20 +52,47 @@ interface ComposerContextMenuEntry {
49
52
  label: string;
50
53
  description?: string;
51
54
  intent?: "add-context" | "invoke-integration";
55
+ order?: number;
56
+ section?: "context" | "integrations" | "commands";
57
+ referenceType?: string;
58
+ referencePicker?: { referenceType: string; source: string };
52
59
  keywords?: readonly string[];
53
60
  icon?: ReactNode;
54
61
  disabled?: boolean;
62
+ opensDialog?: boolean;
55
63
  }
56
64
  export type ComposerContextMenuAction = ComposerContextMenuEntry & {
57
65
  checked?: boolean;
58
66
  onDismiss?: () => void;
59
67
  children?: never;
60
68
  } & (
61
- | { picker: ComposerContextPickerConfig; render?: never; onSelect?: never }
69
+ | {
70
+ picker: ComposerContextPickerConfig;
71
+ referencePicker?: never;
72
+ render?: never;
73
+ onSelect?: never;
74
+ renderLink?: never;
75
+ }
76
+ | {
77
+ referencePicker: { referenceType: string; source: string };
78
+ picker?: never;
79
+ render?: never;
80
+ onSelect?: never;
81
+ renderLink?: never;
82
+ }
83
+ | {
84
+ renderLink: (children: ReactNode) => ReactElement;
85
+ referencePicker?: never;
86
+ picker?: never;
87
+ render?: never;
88
+ onSelect?: never;
89
+ }
62
90
  | {
63
91
  onSelect: () => void | Promise<void>;
92
+ referencePicker?: never;
64
93
  render?: (controls: ComposerContextPageControls) => ReactNode;
65
94
  picker?: never;
95
+ renderLink?: never;
66
96
  }
67
97
  );
68
98
  export interface ComposerContextPageControls {
@@ -71,6 +101,7 @@ export interface ComposerContextPageControls {
71
101
  onResume(): void;
72
102
  }
73
103
  export interface ComposerContextMenuCategory extends ComposerContextMenuEntry {
104
+ referencePicker?: never;
74
105
  children: readonly ComposerContextMenuItem[];
75
106
  searchPlaceholder?: string;
76
107
  onSelect?: never;
@@ -82,6 +113,12 @@ export type ComposerContextMenuItem =
82
113
  | ComposerContextMenuCategory;
83
114
  export interface ComposerContextMenuProps {
84
115
  items: readonly ComposerContextMenuItem[];
116
+ integrationStatus?: {
117
+ scopeKey?: string;
118
+ loading?: boolean;
119
+ error?: string;
120
+ onRetry?: () => void | Promise<unknown>;
121
+ };
85
122
  menuActionItems?: readonly ComposerContextMenuItem[];
86
123
  loading?: boolean;
87
124
  error?: string;
@@ -223,7 +260,11 @@ function ContextSubmenu({
223
260
  return (
224
261
  <DropdownMenuSub open={open} onOpenChange={onOpenChange}>
225
262
  <DropdownMenuSubTrigger ref={trigger} disabled={disabled}>
226
- {icon}
263
+ {icon && (
264
+ <span aria-hidden="true" className="shrink-0 [&>svg]:size-4">
265
+ {icon}
266
+ </span>
267
+ )}
227
268
  <span className="min-w-0 truncate">{label}</span>
228
269
  </DropdownMenuSubTrigger>
229
270
  <DropdownMenuSubContent
@@ -269,30 +310,42 @@ function LegacyContextPage({ children }: { children: ReactNode }) {
269
310
  return <div ref={element}>{children}</div>;
270
311
  }
271
312
 
272
- function ContextEntryLabel({ label, description }: ComposerContextMenuEntry) {
313
+ function ContextEntryLabel({
314
+ label,
315
+ description,
316
+ icon,
317
+ }: ComposerContextMenuEntry) {
273
318
  return (
274
- <span className="flex min-w-0 flex-1 flex-col gap-0.5 @md:flex-row @md:items-baseline @md:gap-2">
275
- <span
276
- className={
277
- description
278
- ? "min-w-0 truncate @md:max-w-[60%] @md:shrink-0"
279
- : "min-w-0 truncate"
280
- }
281
- >
282
- {label}
283
- </span>
284
- {description && (
285
- <span className="min-w-0 flex-1 truncate text-muted-foreground">
286
- {description}
319
+ <>
320
+ {icon && (
321
+ <span aria-hidden="true" className="shrink-0 [&>svg]:size-4">
322
+ {icon}
287
323
  </span>
288
324
  )}
289
- </span>
325
+ <span className="flex min-w-0 flex-1 flex-col gap-0.5 @md:flex-row @md:items-baseline @md:gap-2">
326
+ <span
327
+ className={
328
+ description
329
+ ? "min-w-0 truncate @md:max-w-[60%] @md:shrink-0"
330
+ : "min-w-0 truncate"
331
+ }
332
+ >
333
+ {label}
334
+ </span>
335
+ {description && (
336
+ <span className="min-w-0 flex-1 truncate text-muted-foreground">
337
+ {description}
338
+ </span>
339
+ )}
340
+ </span>
341
+ </>
290
342
  );
291
343
  }
292
344
 
293
345
  export function ComposerContextMenu({
294
346
  items,
295
347
  menuActionItems = [],
348
+ integrationStatus,
296
349
  loading,
297
350
  error: searchError,
298
351
  onRetry,
@@ -309,6 +362,66 @@ export function ComposerContextMenu({
309
362
  openEntry,
310
363
  }: ComposerContextMenuProps) {
311
364
  const t = useComposerRuntimeAdapters().translate!;
365
+ const sectionId = useId();
366
+ const sortEntries = (entries: readonly ComposerContextMenuItem[]) =>
367
+ [...entries].sort(
368
+ (left, right) =>
369
+ (left.order ?? 100) - (right.order ?? 100) ||
370
+ left.label.localeCompare(right.label) ||
371
+ left.id.localeCompare(right.id),
372
+ );
373
+ const seen = new Set<string>();
374
+ const sections: Record<
375
+ "context" | "integrations" | "commands",
376
+ ComposerContextMenuItem[]
377
+ > = { context: [], integrations: [], commands: [] };
378
+ const collect = (
379
+ entries: readonly ComposerContextMenuItem[],
380
+ inherited?: "context" | "integrations" | "commands",
381
+ inheritedDisabled = false,
382
+ ) => {
383
+ const retained: ComposerContextMenuItem[] = [];
384
+ for (const item of entries) {
385
+ if (seen.has(item.id)) continue;
386
+ seen.add(item.id);
387
+ const section =
388
+ item.section ??
389
+ (item.intent === "invoke-integration"
390
+ ? "integrations"
391
+ : (inherited ?? "context"));
392
+ const entry = inheritedDisabled ? { ...item, disabled: true } : item;
393
+ if (item.children && !item.section && !item.intent) {
394
+ const children = collect(
395
+ item.children,
396
+ section,
397
+ inheritedDisabled || item.disabled === true,
398
+ );
399
+ if (children.length)
400
+ retained.push({
401
+ ...item,
402
+ ...(inheritedDisabled ? { disabled: true } : {}),
403
+ children,
404
+ });
405
+ } else if (section !== (inherited ?? "context"))
406
+ sections[section].push(entry);
407
+ else retained.push(entry);
408
+ }
409
+ return retained;
410
+ };
411
+ sections.context.push(...collect(items));
412
+ sections.commands.push(...collect(menuActionItems, "commands"));
413
+ const hasIntegrations = Boolean(
414
+ sections.integrations.length ||
415
+ integrationStatus?.loading ||
416
+ integrationStatus?.error,
417
+ );
418
+ const hasAttachments = Boolean(
419
+ sections.context.length ||
420
+ addAttachment ||
421
+ onAttachmentRequest ||
422
+ loading ||
423
+ searchError,
424
+ );
312
425
  const allItems = [...menuActionItems, ...items];
313
426
  const onDisabledFocusRef = useRef(onDisabledFocus);
314
427
  onDisabledFocusRef.current = onDisabledFocus;
@@ -363,7 +476,7 @@ export function ComposerContextMenu({
363
476
  defaultValue: "Add context",
364
477
  });
365
478
  const uploadLabel = t("agentChat.composer.menu.uploadFile", {
366
- defaultValue: "Upload File",
479
+ defaultValue: "Files and images",
367
480
  });
368
481
  const reportError = useCallback(
369
482
  (cause: unknown) => {
@@ -493,7 +606,7 @@ export function ComposerContextMenu({
493
606
  origin: string[],
494
607
  ): ReactNode => (
495
608
  <>
496
- {entries.map((entry) => {
609
+ {sortEntries(entries).map((entry) => {
497
610
  const branch = [...origin, entry.id];
498
611
  const expanded = branch.every((id, index) => path[index] === id);
499
612
  if (entry.children) {
@@ -501,9 +614,17 @@ export function ComposerContextMenu({
501
614
  <ContextSubmenu
502
615
  key={entry.id}
503
616
  label={entry.label}
617
+ icon={entry.icon}
504
618
  disabled={entry.disabled}
505
619
  open={expanded}
506
- onOpenChange={(next) => {
620
+ onOpenChange={(next, reason) => {
621
+ if (
622
+ !next &&
623
+ reason !== "escape" &&
624
+ resumedPageClose.current &&
625
+ pathRef.current.includes(resumedPageClose.current)
626
+ )
627
+ return;
507
628
  const isOpen = branch.every(
508
629
  (id, index) => pathRef.current[index] === id,
509
630
  );
@@ -548,6 +669,7 @@ export function ComposerContextMenu({
548
669
  <ContextSubmenu
549
670
  key={entry.id}
550
671
  label={entry.label}
672
+ icon={entry.icon}
551
673
  disabled={entry.disabled}
552
674
  open={expanded}
553
675
  onOpenChange={(next, reason) => {
@@ -605,7 +727,6 @@ export function ComposerContextMenu({
605
727
  );
606
728
  return;
607
729
  }
608
- // Radix can emit one stale close after native-picker focus returns.
609
730
  resumedPageClose.current = entry.id;
610
731
  if (resumedPageCloseFrame.current !== null)
611
732
  window.cancelAnimationFrame(
@@ -627,6 +748,17 @@ export function ComposerContextMenu({
627
748
  </ContextSubmenu>
628
749
  );
629
750
  }
751
+ if (entry.renderLink) {
752
+ return (
753
+ <DropdownMenuItem
754
+ key={entry.id}
755
+ asChild
756
+ disabled={entry.disabled === true}
757
+ >
758
+ {entry.renderLink(<ContextEntryLabel {...entry} />)}
759
+ </DropdownMenuItem>
760
+ );
761
+ }
630
762
  if (entry.checked !== undefined) {
631
763
  return (
632
764
  <DropdownMenuCheckboxItem
@@ -647,6 +779,7 @@ export function ComposerContextMenu({
647
779
  key={entry.id}
648
780
  disabled={entry.disabled}
649
781
  onSelect={() => {
782
+ if (entry.opensDialog) restoreFocusOnClose.current = false;
650
783
  changeOpen(false);
651
784
  selectAction(entry);
652
785
  }}
@@ -717,7 +850,7 @@ export function ComposerContextMenu({
717
850
  maxHeight: placement.maxHeight,
718
851
  boxShadow: "none",
719
852
  }}
720
- className={`@container flex w-64 max-w-[calc(100vw-24px)] flex-col p-1 data-[state=open]:fade-in-100 data-[state=closed]:fade-out-100 ${COMPOSER_POPOVER_SURFACE_CLASS}`}
853
+ className={`@container [&_[role^=menuitem]]:min-h-8 flex w-64 max-w-[calc(100vw-24px)] flex-col overflow-y-auto p-1 data-[state=open]:fade-in-100 data-[state=closed]:fade-out-100 ${COMPOSER_POPOVER_SURFACE_CLASS}`}
721
854
  data-agent-native-composer-popover="true"
722
855
  onCloseAutoFocus={(event) => {
723
856
  if (pendingDialog.current) {
@@ -744,76 +877,154 @@ export function ComposerContextMenu({
744
877
  restoreFocusOnClose.current = true;
745
878
  }}
746
879
  >
747
- <div className="min-h-0 overflow-y-auto">
748
- <DropdownMenuGroup>
749
- {(addAttachment || onAttachmentRequest) && (
750
- <DropdownMenuItem
751
- onSelect={() => {
752
- if (addAttachment) {
753
- changeOpen(false);
754
- inputRef.current?.click();
755
- } else {
756
- pendingAttachmentRequest.current = true;
757
- changeOpen(false);
758
- }
759
- }}
880
+ <div className="contents">
881
+ {hasAttachments && (
882
+ <DropdownMenuGroup
883
+ aria-labelledby={`${sectionId}-context`}
884
+ aria-busy={loading || undefined}
885
+ >
886
+ <DropdownMenuLabel id={`${sectionId}-context`}>
887
+ {t("agentChat.composer.menu.attachToMessage", {
888
+ defaultValue: "Add context",
889
+ })}
890
+ </DropdownMenuLabel>
891
+ {(addAttachment || onAttachmentRequest) && (
892
+ <DropdownMenuItem
893
+ onSelect={() => {
894
+ if (onAttachmentRequest) {
895
+ pendingAttachmentRequest.current = true;
896
+ changeOpen(false);
897
+ } else {
898
+ changeOpen(false);
899
+ inputRef.current?.click();
900
+ }
901
+ }}
902
+ >
903
+ <IconPaperclip aria-hidden="true" />
904
+ {uploadLabel}
905
+ </DropdownMenuItem>
906
+ )}
907
+ {renderEntries(sections.context, [])}
908
+ <div
909
+ role="presentation"
910
+ aria-hidden="true"
911
+ className={
912
+ loading
913
+ ? "px-2 py-1 text-xs text-muted-foreground"
914
+ : "sr-only"
915
+ }
760
916
  >
761
- {uploadLabel}
762
- </DropdownMenuItem>
763
- )}
764
- {(items.length > 0 || loading || searchError) && (
765
- <ContextSubmenu
766
- label={label}
767
- open={path[0] === "add-context"}
768
- onOpenChange={(next) => {
769
- if (next === (pathRef.current[0] === "add-context")) return;
770
- if (!next) {
771
- dismissPage();
772
- updatePath([]);
773
- return;
774
- }
775
- setError(null);
776
- updatePath(["add-context"]);
777
- }}
917
+ {loading
918
+ ? t("agentChat.composer.contextPending", {
919
+ defaultValue: "Loading context…",
920
+ })
921
+ : ""}
922
+ </div>
923
+ {searchError && (
924
+ <div
925
+ role="presentation"
926
+ aria-hidden="true"
927
+ className="px-2 py-1 text-sm text-destructive"
928
+ >
929
+ {searchError}
930
+ </div>
931
+ )}
932
+ {searchError && onRetry && (
933
+ <DropdownMenuItem
934
+ onSelect={(event) => {
935
+ event.preventDefault();
936
+ try {
937
+ void Promise.resolve(onRetry()).catch(reportError);
938
+ } catch (cause) {
939
+ reportError(cause);
940
+ }
941
+ }}
942
+ >
943
+ {t("agentChat.common.retry", { defaultValue: "Retry" })}
944
+ </DropdownMenuItem>
945
+ )}
946
+ </DropdownMenuGroup>
947
+ )}
948
+ {hasIntegrations && (
949
+ <>
950
+ {hasAttachments && <DropdownMenuSeparator />}
951
+ <DropdownMenuGroup
952
+ aria-labelledby={`${sectionId}-integrations`}
953
+ aria-busy={integrationStatus?.loading || undefined}
778
954
  >
779
- {renderEntries(items, ["add-context"])}
780
- {loading && (
955
+ <DropdownMenuLabel id={`${sectionId}-integrations`}>
956
+ {t("agentChat.composer.menu.integrations", {
957
+ defaultValue: "Integrations",
958
+ })}
959
+ </DropdownMenuLabel>
960
+ {renderEntries(sections.integrations, [])}
961
+ {integrationStatus?.loading && (
781
962
  <div
782
- role="status"
783
- aria-label={t("agentChat.composer.contextPending", {
784
- defaultValue: "Loading context…",
785
- })}
786
- className="grid gap-2 p-3"
963
+ role="presentation"
964
+ aria-hidden="true"
965
+ className="px-2 py-1 text-xs text-muted-foreground"
787
966
  >
788
- <Skeleton className="h-5 w-2/3" />
789
- <Skeleton className="h-5 w-1/2" />
967
+ {t("agentChat.common.loading", {
968
+ defaultValue: "Loading...",
969
+ })}
790
970
  </div>
791
971
  )}
792
- {searchError && (
972
+ {integrationStatus?.error && (
793
973
  <div
794
- role="alert"
795
- className="px-3 py-2 text-sm text-destructive"
974
+ role="presentation"
975
+ aria-hidden="true"
976
+ className="px-2 py-1 text-sm text-destructive"
796
977
  >
797
- {searchError}
978
+ {integrationStatus.error}
798
979
  </div>
799
980
  )}
800
- {searchError && onRetry && (
981
+ {integrationStatus?.error && integrationStatus.onRetry && (
801
982
  <DropdownMenuItem
802
983
  onSelect={(event) => {
803
984
  event.preventDefault();
804
- onRetry();
985
+ try {
986
+ void Promise.resolve(
987
+ integrationStatus.onRetry?.(),
988
+ ).catch(reportError);
989
+ } catch (cause) {
990
+ reportError(cause);
991
+ }
805
992
  }}
806
993
  >
807
994
  {t("agentChat.common.retry", { defaultValue: "Retry" })}
808
995
  </DropdownMenuItem>
809
996
  )}
810
- </ContextSubmenu>
811
- )}
812
- {renderEntries(menuActionItems, [])}
813
- </DropdownMenuGroup>
997
+ </DropdownMenuGroup>
998
+ </>
999
+ )}
1000
+ {sections.commands.length > 0 && (
1001
+ <>
1002
+ {(hasAttachments || hasIntegrations) && (
1003
+ <DropdownMenuSeparator />
1004
+ )}
1005
+ <DropdownMenuGroup aria-labelledby={`${sectionId}-commands`}>
1006
+ <DropdownMenuLabel id={`${sectionId}-commands`}>
1007
+ {t("agentChat.composer.menu.commands", {
1008
+ defaultValue: "Commands",
1009
+ })}
1010
+ </DropdownMenuLabel>
1011
+ {renderEntries(sections.commands, [])}
1012
+ </DropdownMenuGroup>
1013
+ </>
1014
+ )}
814
1015
  </div>
815
1016
  </DropdownMenuContent>
816
1017
  </DropdownMenu>
1018
+ <div role="status" aria-live="polite" className="sr-only">
1019
+ {open && (loading || integrationStatus?.loading)
1020
+ ? t("agentChat.common.loading", { defaultValue: "Loading..." })
1021
+ : ""}
1022
+ </div>
1023
+ {open && (searchError || integrationStatus?.error) && (
1024
+ <div role="alert" className="sr-only">
1025
+ {searchError ?? integrationStatus?.error}
1026
+ </div>
1027
+ )}
817
1028
  {dialog && dialogAvailable && dialogAction?.picker && (
818
1029
  <ComposerContextPickerDialog
819
1030
  key={JSON.stringify([dialog.id, dialogAction.picker.scopeKey])}
@@ -71,7 +71,6 @@ async function open() {
71
71
  container.querySelector('button[aria-label="Add context"]')!,
72
72
  "ArrowDown",
73
73
  );
74
- await click("Add context");
75
74
  await click("Category");
76
75
  await click("Source");
77
76
  }
@@ -169,7 +168,7 @@ describe("declarative context picker", () => {
169
168
  expect(select).not.toHaveBeenCalled();
170
169
  await click("Clear selection");
171
170
  expect(clear).toHaveBeenCalledOnce();
172
- expect(menus()).toHaveLength(4);
171
+ expect(menus()).toHaveLength(3);
173
172
  });
174
173
  it("retries a failed selection and closes only on success", async () => {
175
174
  const onSelect = vi
@@ -194,7 +193,7 @@ describe("declarative context picker", () => {
194
193
  await render({ ...config, scopeKey: "account-b" });
195
194
  expect(onSelect.mock.calls[0][1].signal.aborted).toBe(true);
196
195
  await act(async () => pending.resolve());
197
- expect(menus()).toHaveLength(4);
196
+ expect(menus()).toHaveLength(3);
198
197
  });
199
198
  it("keeps link, search and page on sync refresh, and aborts a dismissed load", async () => {
200
199
  const pending = deferred<{ items: { id: string; title: string }[] }>();
@@ -233,13 +232,13 @@ describe("declarative context picker", () => {
233
232
  await act(async () =>
234
233
  pending.resolve({ items: [{ id: "late", title: "Late" }] }),
235
234
  );
236
- expect(menus()).toHaveLength(3);
235
+ expect(menus()).toHaveLength(2);
237
236
  expect(document.body.textContent).not.toContain("Late");
238
237
  });
239
238
  it("keeps connected panels, integrated headers and source search autofocus", async () => {
240
239
  await render(local());
241
240
  await open();
242
- expect(menus()).toHaveLength(4);
241
+ expect(menus()).toHaveLength(3);
243
242
  expect(menus().at(-1)?.classList.contains("w-64")).toBe(true);
244
243
  const searches = document.querySelectorAll('input[role="searchbox"]');
245
244
  expect(searches).toHaveLength(1);
@@ -272,7 +271,7 @@ describe("declarative context picker", () => {
272
271
  signal: expect.any(AbortSignal),
273
272
  }),
274
273
  );
275
- expect(menus()).toHaveLength(4);
274
+ expect(menus()).toHaveLength(3);
276
275
  });
277
276
  it("routes typing and arrows through the root item and picker submenu", async () => {
278
277
  await render(local());
@@ -287,10 +286,10 @@ describe("declarative context picker", () => {
287
286
  await key(row("One"), "ArrowDown");
288
287
  expect(document.activeElement).toBe(row("Two"));
289
288
  await key(row("Two"), "ArrowLeft");
290
- expect(menus()).toHaveLength(3);
289
+ expect(menus()).toHaveLength(2);
291
290
  expect(document.activeElement).toBe(row("Source"));
292
291
  await key(row("Source"), "Escape");
293
- expect(menus()).toHaveLength(2);
292
+ expect(menus()).toHaveLength(1);
294
293
  });
295
294
  it("loads pages with cursor history and preserves location on refresh without descriptor loops", async () => {
296
295
  const load = vi
@@ -390,7 +389,7 @@ describe("declarative context picker", () => {
390
389
  expect(signal.aborted).toBe(true);
391
390
  await click("Source");
392
391
  await act(async () => pending.resolve());
393
- expect(menus()).toHaveLength(4);
392
+ expect(menus()).toHaveLength(3);
394
393
  });
395
394
  it("validates links before reading, searches results, and returns to the populated link stage", async () => {
396
395
  const load = vi
@@ -435,19 +434,19 @@ describe("declarative context picker", () => {
435
434
  'input[placeholder="Source URL"]',
436
435
  )?.value,
437
436
  ).toBe("https://example.com/source");
438
- expect(menus()).toHaveLength(4);
437
+ expect(menus()).toHaveLength(3);
439
438
  await key(
440
439
  document.querySelector('input[placeholder="Source URL"]')!,
441
440
  "ArrowLeft",
442
441
  );
443
- expect(menus()).toHaveLength(4);
442
+ expect(menus()).toHaveLength(3);
444
443
  await key(
445
444
  document.querySelector('input[placeholder="Source URL"]')!,
446
445
  "ArrowRight",
447
446
  );
448
- expect(menus()).toHaveLength(4);
449
- await key(row("Read link"), "ArrowLeft");
450
447
  expect(menus()).toHaveLength(3);
448
+ await key(row("Read link"), "ArrowLeft");
449
+ expect(menus()).toHaveLength(2);
451
450
  });
452
451
  it("keeps a genuine link footer present in empty and populated states", async () => {
453
452
  const config = {
@@ -514,6 +513,6 @@ describe("declarative context picker", () => {
514
513
  await click("Retry");
515
514
  expect(footer).toHaveBeenCalledTimes(2);
516
515
  expect(load).not.toHaveBeenCalled();
517
- expect(menus()).toHaveLength(4);
516
+ expect(menus()).toHaveLength(3);
518
517
  });
519
518
  });
@@ -140,7 +140,6 @@ async function open() {
140
140
  new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
141
141
  ),
142
142
  );
143
- await act(async () => row("Add context").click());
144
143
  await act(async () => row("Design").click());
145
144
  expect(row("Attach source").hasAttribute("aria-haspopup")).toBe(false);
146
145
  await act(async () => row("Attach source").click());
@@ -184,6 +183,46 @@ function deferred<T>() {
184
183
  }
185
184
 
186
185
  describe("shared context URL dialog", () => {
186
+ it("keeps an opened loading source in its dialog when the URL form becomes available", async () => {
187
+ await render({
188
+ presentation: { type: "dialog", mode: "single" },
189
+ scopeKey: "account-a",
190
+ searchPlaceholder: "Search frames",
191
+ items: [],
192
+ loading: true,
193
+ onSelect: vi.fn(),
194
+ });
195
+ await open();
196
+ expect(dialog()).not.toBeNull();
197
+ const connected = multiple();
198
+ await render(connected);
199
+ await tick();
200
+ const input =
201
+ dialog()?.querySelector<HTMLInputElement>('input[type="url"]');
202
+ expect(input).not.toBeNull();
203
+ expect(document.activeElement).toBe(input);
204
+ expect(connected.load).not.toHaveBeenCalled();
205
+ await render({
206
+ presentation: { type: "dialog", mode: "single" },
207
+ scopeKey: "account-a",
208
+ searchPlaceholder: "Search frames",
209
+ items: [],
210
+ emptyMessage: "Connect this source",
211
+ footerAction: {
212
+ label: "Connect source",
213
+ renderLink: (children) => (
214
+ <a href="/settings/integrations">{children}</a>
215
+ ),
216
+ },
217
+ onSelect: vi.fn(),
218
+ });
219
+ await tick();
220
+ expect(dialog()).not.toBeNull();
221
+ expect(
222
+ dialog()?.querySelector('a[href="/settings/integrations"]')?.textContent,
223
+ ).toBe("Connect source");
224
+ });
225
+
187
226
  it("supports gallery previews with immediate single selection", async () => {
188
227
  const onSelect = vi.fn().mockResolvedValue(undefined);
189
228
  const onClose = vi.fn();
@@ -455,8 +494,6 @@ describe("shared context URL dialog", () => {
455
494
  ),
456
495
  );
457
496
  expect(document.querySelector('[role="searchbox"]')).toBeNull();
458
- expect(row("Add context")).toBeDefined();
459
- await act(async () => row("Add context").click());
460
497
  expect(row("Design")).toBeDefined();
461
498
  await act(async () => row("Design").click());
462
499
  expect(row("Attach source").querySelector("svg")).toBeNull();