@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,6 +1,7 @@
1
1
  import { useComposer, useComposerRuntime } from "@assistant-ui/react";
2
2
  import {
3
3
  IconCheck,
4
+ IconSearch,
4
5
  IconChevronDown,
5
6
  IconBulb,
6
7
  IconClock,
@@ -113,7 +114,11 @@ import type {
113
114
  ComposerMode,
114
115
  AgentComposerLayoutVariant,
115
116
  } from "./types.js";
116
- import { useMentionSearch } from "./use-mention-search.js";
117
+ import {
118
+ useMentionSearch,
119
+ loadWorkspaceMentionItems,
120
+ MentionSearchError,
121
+ } from "./use-mention-search.js";
117
122
  import { useSkills } from "./use-skills.js";
118
123
  import {
119
124
  RealtimeVoiceModeBoundary,
@@ -3229,7 +3234,7 @@ function TiptapComposerSurface({
3229
3234
  settledQuery: settledMentionQuery,
3230
3235
  } = useMentionSearch(
3231
3236
  popover?.type === "@" ? popover.query : "",
3232
- includeDefaultMentionSearch && (contextMenuOpen || popover?.type === "@"),
3237
+ includeDefaultMentionSearch && popover?.type === "@",
3233
3238
  );
3234
3239
  const mentionQuery = popover?.type === "@" ? popover.query : "";
3235
3240
  const filteredMentionItems = useMemo(
@@ -6455,51 +6460,140 @@ function TiptapComposerSurface({
6455
6460
  const launchersDisabled = disabled || contextControlsDisabled;
6456
6461
 
6457
6462
  const contextLauncherItems = useMemo(() => {
6458
- const sections = new Map<string, ComposerContextMenuItem[]>();
6459
- for (const item of filteredMentionItems) {
6460
- const section = item.section || "Other";
6461
- const entries = sections.get(section) ?? [];
6462
- entries.push({
6463
- id: `mention:${item.id}`,
6464
- label: item.label,
6465
- description: item.description,
6466
- onSelect: () => insertReference(composerReferenceFromMentionItem(item)),
6467
- });
6468
- sections.set(section, entries);
6469
- }
6470
- const sectionKeys: Record<string, string> = {
6471
- Agents: "agents",
6472
- "Connected Agents": "connectedAgents",
6473
- Files: "files",
6474
- Other: "other",
6463
+ const excludedTypes = new Set<string>();
6464
+ const visit = (entries: readonly ComposerContextMenuItem[]) => {
6465
+ for (const entry of entries) {
6466
+ if (entry.referenceType) excludedTypes.add(entry.referenceType);
6467
+ if (entry.referencePicker)
6468
+ excludedTypes.add(entry.referencePicker.referenceType);
6469
+ if (entry.children) visit(entry.children);
6470
+ }
6471
+ };
6472
+ visit(contextMenuItems ?? []);
6473
+ const pickerFor = (filter?: { referenceType: string; source: string }) => {
6474
+ const available = new Map<string, MentionItem>();
6475
+ return {
6476
+ identityScope: "workspace" as const,
6477
+ presentation: { type: "dialog" as const, mode: "action" as const },
6478
+ searchPlaceholder: t("agentChat.composer.menu.searchWorkspace", {
6479
+ defaultValue: "Search workspace…",
6480
+ }),
6481
+ load: async ({
6482
+ search,
6483
+ signal,
6484
+ }: {
6485
+ search: string;
6486
+ signal: AbortSignal;
6487
+ }) => {
6488
+ const remote =
6489
+ filter || includeDefaultMentionSearch
6490
+ ? await loadWorkspaceMentionItems(
6491
+ search,
6492
+ adapters.resolvePath,
6493
+ signal,
6494
+ filter?.source,
6495
+ ).catch((cause: unknown) => {
6496
+ if (signal.aborted) throw cause;
6497
+ if (cause instanceof MentionSearchError) throw cause;
6498
+ throw new Error(
6499
+ t("agentChat.composer.contextLoadFailed", {
6500
+ defaultValue: "Could not load context.",
6501
+ }),
6502
+ { cause },
6503
+ );
6504
+ })
6505
+ : [];
6506
+ const entries = [
6507
+ ...mentionItemsForQuery(hostMentionItems, search),
6508
+ ...remote,
6509
+ ].filter((item) =>
6510
+ filter
6511
+ ? item.refType === filter.referenceType &&
6512
+ (!filter.source || item.source === filter.source)
6513
+ : !excludedTypes.has(item.refType),
6514
+ );
6515
+ if (signal.aborted)
6516
+ throw new DOMException("The operation was aborted.", "AbortError");
6517
+ available.clear();
6518
+ for (const item of entries) available.set(item.id, item);
6519
+ return {
6520
+ items: Array.from(available.values()).map((item) => ({
6521
+ id: item.id,
6522
+ title: item.label,
6523
+ ...(item.description === undefined
6524
+ ? {}
6525
+ : { description: item.description }),
6526
+ ...(item.section === undefined
6527
+ ? {}
6528
+ : {
6529
+ group:
6530
+ item.section === "Files"
6531
+ ? "agentChat.mentions.sections.files"
6532
+ : item.section === "Agents"
6533
+ ? "agentChat.mentions.sections.agents"
6534
+ : item.section === "Connected Agents"
6535
+ ? "agentChat.mentions.sections.connectedAgents"
6536
+ : item.section,
6537
+ }),
6538
+ icon: (
6539
+ <MentionItemMedia
6540
+ {...(item.icon === undefined ? {} : { icon: item.icon })}
6541
+ {...(item.media === undefined ? {} : { media: item.media })}
6542
+ size="sm"
6543
+ />
6544
+ ),
6545
+ })),
6546
+ };
6547
+ },
6548
+ onSelect: (item: { id: string }) => {
6549
+ const reference = available.get(item.id);
6550
+ if (!reference)
6551
+ throw new Error(
6552
+ t("agentChat.composer.contextLoadFailed", {
6553
+ defaultValue: "Could not load context.",
6554
+ }),
6555
+ );
6556
+ insertReference(composerReferenceFromMentionItem(reference));
6557
+ },
6558
+ };
6475
6559
  };
6560
+ const resolveEntries = (
6561
+ entries: readonly ComposerContextMenuItem[],
6562
+ ): ComposerContextMenuItem[] =>
6563
+ entries.map((entry) => {
6564
+ if (entry.referencePicker) {
6565
+ const { referencePicker, ...rest } = entry;
6566
+ return { ...rest, picker: pickerFor(referencePicker) };
6567
+ }
6568
+ return entry.children
6569
+ ? { ...entry, children: resolveEntries(entry.children) }
6570
+ : entry;
6571
+ });
6476
6572
  return [
6477
- ...(contextMenuItems ?? []),
6478
- ...Array.from(sections)
6479
- .sort(([left], [right]) => {
6480
- const order = (section: string) =>
6481
- section === "Agents"
6482
- ? 0
6483
- : section === "Connected Agents"
6484
- ? 1
6485
- : section === "Files"
6486
- ? 3
6487
- : section === "Other"
6488
- ? 4
6489
- : 2;
6490
- return order(left) - order(right) || left.localeCompare(right);
6491
- })
6492
- .map(([section, children]) => ({
6493
- id: `mentions:${section}`,
6494
- label: sectionKeys[section]
6495
- ? t(`agentChat.mentions.sections.${sectionKeys[section]}`, {
6496
- defaultValue: section,
6497
- })
6498
- : section,
6499
- children,
6500
- })),
6573
+ ...resolveEntries(contextMenuItems ?? []),
6574
+ ...(includeDefaultMentionSearch || hostMentionItems.length
6575
+ ? [
6576
+ {
6577
+ id: "composer-workspace-search",
6578
+ section: "context" as const,
6579
+ order: 90,
6580
+ label: t("agentChat.composer.menu.searchWorkspace", {
6581
+ defaultValue: "Search workspace…",
6582
+ }),
6583
+ icon: <IconSearch />,
6584
+ picker: pickerFor(),
6585
+ },
6586
+ ]
6587
+ : []),
6501
6588
  ];
6502
- }, [contextMenuItems, filteredMentionItems, insertReference, t]);
6589
+ }, [
6590
+ contextMenuItems,
6591
+ hostMentionItems,
6592
+ includeDefaultMentionSearch,
6593
+ adapters.resolvePath,
6594
+ insertReference,
6595
+ t,
6596
+ ]);
6503
6597
 
6504
6598
  const sendButton = (
6505
6599
  <ComposerSendButton
@@ -6786,15 +6880,7 @@ function TiptapComposerSurface({
6786
6880
  attachmentsEnabled={attachmentsEnabled}
6787
6881
  open={contextMenuOpen}
6788
6882
  onOpenChange={setContextMenuOpen}
6789
- loading={mentionsLoading}
6790
- error={
6791
- mentionsError
6792
- ? t("agentChat.composer.contextLoadFailed", {
6793
- defaultValue: "Could not load context.",
6794
- })
6795
- : undefined
6796
- }
6797
- onRetry={retryMentions}
6883
+ onSelectIntegration={insertReference}
6798
6884
  onRestoreFocus={
6799
6885
  launchersDisabled
6800
6886
  ? undefined
@@ -6909,8 +6995,15 @@ function TiptapComposerSurface({
6909
6995
  commands={filteredCommands}
6910
6996
  hint={hint}
6911
6997
  isLoading={popover?.type === "@" ? mentionsLoading : skillsLoading}
6912
- error={mentionItemsError}
6913
- onRetry={onRetryMentionItems}
6998
+ error={
6999
+ mentionItemsError ??
7000
+ (mentionsError
7001
+ ? t("agentChat.composer.contextLoadFailed", {
7002
+ defaultValue: "Could not load context.",
7003
+ })
7004
+ : undefined)
7005
+ }
7006
+ onRetry={onRetryMentionItems ?? retryMentions}
6914
7007
  query={popover?.query ?? ""}
6915
7008
  onSelectMention={handleSelectMention}
6916
7009
  onSelectSkill={handleSelectSkill}
@@ -169,3 +169,8 @@ export type {
169
169
  Reference,
170
170
  SlashCommand,
171
171
  } from "./types.js";
172
+
173
+ export type {
174
+ WorkspaceComposerMenuOptions,
175
+ WorkspaceComposerMenuContribution,
176
+ } from "./runtime-adapters.js";
@@ -8,6 +8,7 @@ import {
8
8
  type ReactNode,
9
9
  } from "react";
10
10
 
11
+ import type { ComposerContextMenuItem } from "./ComposerContextMenu.js";
11
12
  import type { MentionItemMedia } from "./types.js";
12
13
 
13
14
  const referenceEventTargets = new WeakMap<Event, HTMLElement>();
@@ -188,6 +189,20 @@ export interface VoiceContextPack {
188
189
  terms?: Array<{ term: string; replacement?: string }>;
189
190
  }
190
191
 
192
+ export interface WorkspaceComposerMenuOptions {
193
+ enabled: boolean;
194
+ onRestoreFocus?: () => void;
195
+ onSelectIntegration: (reference: AgentComposerReference) => void;
196
+ }
197
+ export interface WorkspaceComposerMenuContribution {
198
+ scopeKey?: string;
199
+ items: readonly ComposerContextMenuItem[];
200
+ loading?: boolean;
201
+ error?: string;
202
+ onRetry?: () => void | Promise<unknown>;
203
+ overlays?: ReactNode;
204
+ }
205
+
191
206
  export interface ComposerRuntimeAdapters {
192
207
  resolvePath?: (path: string) => string;
193
208
  translate?: ComposerTranslate;
@@ -242,6 +257,9 @@ export interface ComposerRuntimeAdapters {
242
257
  isTrustedBuilderMessage?: (event: MessageEvent) => boolean;
243
258
  };
244
259
  resources?: {
260
+ useWorkspaceMenu?: (
261
+ options: WorkspaceComposerMenuOptions,
262
+ ) => WorkspaceComposerMenuContribution;
245
263
  useOrg?: () => {
246
264
  data?: { orgId?: string | null; role?: string | null } | null;
247
265
  };
@@ -1,8 +1,6 @@
1
1
  import { useLayoutEffect, useState, type RefObject } from "react";
2
2
 
3
- // Keep the compact menu's actions reachable in short viewports.
4
- const MIN_USABLE_PANEL_HEIGHT = 120;
5
- const MAX_COMPOSER_PANEL_HEIGHT = 280;
3
+ const MAX_COMPOSER_PANEL_HEIGHT = 480;
6
4
 
7
5
  export function useComposerPanelPlacement(
8
6
  triggerRef: RefObject<HTMLElement | null>,
@@ -32,7 +30,7 @@ export function useComposerPanelPlacement(
32
30
  const spaceAbove = Math.max(0, button.top - viewportTop - 24);
33
31
  const spaceBelow = Math.max(0, viewportBottom - button.bottom - 24);
34
32
  const side =
35
- spaceAbove < MIN_USABLE_PANEL_HEIGHT && spaceBelow > spaceAbove
33
+ spaceAbove < MAX_COMPOSER_PANEL_HEIGHT && spaceBelow > spaceAbove
36
34
  ? "bottom"
37
35
  : "top";
38
36
  setPlacement({
@@ -4,7 +4,10 @@ import { act } from "react";
4
4
  import { createRoot } from "react-dom/client";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
 
7
- import { useMentionSearch } from "./use-mention-search.js";
7
+ import {
8
+ useMentionSearch,
9
+ loadWorkspaceMentionItems,
10
+ } from "./use-mention-search.js";
8
11
 
9
12
  const resolvePath = (path: string) => `/design${path}`;
10
13
  const file = {
@@ -45,6 +48,68 @@ afterEach(async () => {
45
48
  vi.unstubAllGlobals();
46
49
  });
47
50
  describe("shared launcher mention discovery", () => {
51
+ it("surfaces localized structured source failures and rejects malformed HTTP errors", async () => {
52
+ vi.stubGlobal(
53
+ "fetch",
54
+ vi
55
+ .fn()
56
+ .mockResolvedValueOnce(
57
+ new Response(
58
+ JSON.stringify({
59
+ error: {
60
+ code: "mention_source_unavailable",
61
+ message: "Impossible de charger les références.",
62
+ source: "dashboards",
63
+ },
64
+ }),
65
+ { status: 503 },
66
+ ),
67
+ )
68
+ .mockResolvedValueOnce(new Response("not json", { status: 503 })),
69
+ );
70
+ await expect(
71
+ loadWorkspaceMentionItems(
72
+ "",
73
+ resolvePath,
74
+ new AbortController().signal,
75
+ "dashboards",
76
+ ),
77
+ ).rejects.toMatchObject({
78
+ message: "Impossible de charger les références.",
79
+ code: "mention_source_unavailable",
80
+ source: "dashboards",
81
+ });
82
+ await expect(
83
+ loadWorkspaceMentionItems(
84
+ "",
85
+ resolvePath,
86
+ new AbortController().signal,
87
+ "dashboards",
88
+ ),
89
+ ).rejects.toThrow("Mention search unavailable");
90
+ });
91
+ it("rejects a failed source after earlier batches instead of returning a partial workspace result", async () => {
92
+ vi.stubGlobal(
93
+ "fetch",
94
+ vi.fn(
95
+ async () =>
96
+ new Response(
97
+ JSON.stringify({ items: [file] }) +
98
+ "\n" +
99
+ JSON.stringify({
100
+ error: {
101
+ code: "mention_source_unavailable",
102
+ message: "Could not load workspace references.",
103
+ source: "dashboards",
104
+ },
105
+ }),
106
+ ),
107
+ ),
108
+ );
109
+ await expect(
110
+ loadWorkspaceMentionItems("", resolvePath, new AbortController().signal),
111
+ ).rejects.toThrow("Could not load workspace references.");
112
+ });
48
113
  it("does not load until the launcher opens", async () => {
49
114
  const fetch = vi.fn();
50
115
  vi.stubGlobal("fetch", fetch);
@@ -66,6 +131,39 @@ describe("shared launcher mention discovery", () => {
66
131
  expect(result.error).toBeNull();
67
132
  expect(result.isLoading).toBe(false);
68
133
  });
134
+ it.each([false, true])(
135
+ "keeps healthy streamed batches around a source error (earlier batch: %s)",
136
+ async (earlier) => {
137
+ const second = { ...file, id: "file:two", label: "Notes" };
138
+ const lines = [
139
+ ...(earlier ? [{ items: [file] }] : []),
140
+ { error: { message: "Source unavailable" } },
141
+ { items: [second] },
142
+ ];
143
+ vi.stubGlobal(
144
+ "fetch",
145
+ vi.fn(
146
+ async () =>
147
+ new Response(
148
+ new ReadableStream({
149
+ start(controller) {
150
+ for (const line of lines)
151
+ controller.enqueue(
152
+ new TextEncoder().encode(JSON.stringify(line) + "\n"),
153
+ );
154
+ controller.close();
155
+ },
156
+ }),
157
+ ),
158
+ ),
159
+ );
160
+ await render();
161
+ expect(result.items).toEqual(earlier ? [file, second] : [second]);
162
+ expect(result.error?.message).toBe("Source unavailable");
163
+ expect(result.settledQuery).toBeNull();
164
+ expect(result.isLoading).toBe(false);
165
+ },
166
+ );
69
167
  it.each([
70
168
  new Response("{}"),
71
169
  new Response("{broken"),
@@ -3,6 +3,95 @@ import { useState, useEffect, useRef, useCallback } from "react";
3
3
  import { useComposerRuntimeAdapters } from "./runtime-adapters.js";
4
4
  import type { MentionItem } from "./types.js";
5
5
 
6
+ export class MentionSearchError extends Error {
7
+ constructor(message: string, details: { code?: string; source?: string }) {
8
+ super(message);
9
+ Object.assign(this, details);
10
+ }
11
+ }
12
+
13
+ function parseMentionBatch(
14
+ line: string,
15
+ ): { items: MentionItem[] } | { error: Error } {
16
+ const data: unknown = JSON.parse(line);
17
+ if (
18
+ data &&
19
+ typeof data === "object" &&
20
+ "error" in data &&
21
+ data.error &&
22
+ typeof data.error === "object" &&
23
+ "message" in data.error &&
24
+ typeof data.error.message === "string"
25
+ )
26
+ return {
27
+ error: new MentionSearchError(data.error.message, {
28
+ ...("code" in data.error && typeof data.error.code === "string"
29
+ ? { code: data.error.code }
30
+ : {}),
31
+ ...("source" in data.error && typeof data.error.source === "string"
32
+ ? { source: data.error.source }
33
+ : {}),
34
+ }),
35
+ };
36
+ if (
37
+ !data ||
38
+ typeof data !== "object" ||
39
+ !("items" in data) ||
40
+ !Array.isArray(data.items) ||
41
+ !data.items.every(
42
+ (item: unknown) =>
43
+ item &&
44
+ typeof item === "object" &&
45
+ "id" in item &&
46
+ typeof item.id === "string" &&
47
+ "label" in item &&
48
+ typeof item.label === "string" &&
49
+ "refType" in item &&
50
+ typeof item.refType === "string",
51
+ )
52
+ ) {
53
+ throw new Error("Invalid mention search response");
54
+ }
55
+ return { items: data.items as MentionItem[] };
56
+ }
57
+
58
+ async function mentionResponseError(response: Response): Promise<Error> {
59
+ const fallback = new Error("Mention search unavailable");
60
+ const body = await response.text();
61
+ try {
62
+ const batch = parseMentionBatch(body);
63
+ return "error" in batch ? batch.error : fallback;
64
+ } catch {
65
+ return fallback;
66
+ }
67
+ }
68
+ export async function loadWorkspaceMentionItems(
69
+ query: string,
70
+ resolvePath: (path: string) => string,
71
+ signal: AbortSignal,
72
+ source?: string,
73
+ ): Promise<MentionItem[]> {
74
+ const response = await fetch(
75
+ resolvePath(
76
+ `/_agent-native/agent-chat/mentions?q=${encodeURIComponent(query)}${source ? `&source=${encodeURIComponent(source)}` : ""}`,
77
+ ),
78
+ { signal },
79
+ );
80
+ if (!response.ok) throw await mentionResponseError(response);
81
+ if (!response.body) throw new Error("Mention search unavailable");
82
+ const body = await response.text();
83
+ if (signal.aborted)
84
+ throw new DOMException("The operation was aborted.", "AbortError");
85
+ return body
86
+ .split("\n")
87
+ .filter((line) => line.trim())
88
+ .flatMap((line) => {
89
+ const batch = parseMentionBatch(line);
90
+ if ("error" in batch) throw batch.error;
91
+ return batch.items;
92
+ });
93
+ }
94
+
6
95
  export function useMentionSearch(
7
96
  query: string,
8
97
  enabled: boolean,
@@ -47,40 +136,26 @@ export function useMentionSearch(
47
136
  ),
48
137
  { signal: abort.signal },
49
138
  );
50
- if (!res.ok || !res.body) throw new Error("Mention search unavailable");
139
+ if (!res.ok) throw await mentionResponseError(res);
140
+ if (!res.body) throw new Error("Mention search unavailable");
51
141
 
52
142
  const reader = res.body.getReader();
53
143
  const decoder = new TextDecoder();
54
144
  let buf = "";
145
+ const sourceError: { current: Error | null } = { current: null };
55
146
  const receive = (line: string) => {
56
147
  if (!line.trim()) return;
57
- const data: unknown = JSON.parse(line);
58
- if (
59
- !data ||
60
- typeof data !== "object" ||
61
- !("items" in data) ||
62
- !Array.isArray(data.items) ||
63
- !data.items.every(
64
- (item: unknown) =>
65
- item &&
66
- typeof item === "object" &&
67
- "id" in item &&
68
- typeof item.id === "string" &&
69
- "label" in item &&
70
- typeof item.label === "string" &&
71
- "refType" in item &&
72
- typeof item.refType === "string",
73
- )
74
- ) {
75
- throw new Error("Invalid mention search response");
148
+ const batch = parseMentionBatch(line);
149
+ if ("error" in batch) {
150
+ sourceError.current ??= batch.error;
151
+ return;
76
152
  }
77
- const batch = data.items as MentionItem[];
78
153
  if (id !== requestIdRef.current || abort.signal.aborted) return;
79
154
  setItems((previous) => {
80
155
  const seen = new Set(previous.map((item) => item.id));
81
156
  return [
82
157
  ...previous,
83
- ...batch.filter((item) => {
158
+ ...batch.items.filter((item) => {
84
159
  if (seen.has(item.id)) return false;
85
160
  seen.add(item.id);
86
161
  return true;
@@ -106,8 +181,10 @@ export function useMentionSearch(
106
181
  }
107
182
  // Only a completed search settles its query: a failed one is unknown,
108
183
  // not empty, and must not read as "nothing matches".
109
- if (id === requestIdRef.current && !abort.signal.aborted)
110
- setSettledQuery(query);
184
+ if (id === requestIdRef.current && !abort.signal.aborted) {
185
+ if (sourceError.current) setError(sourceError.current);
186
+ else setSettledQuery(query);
187
+ }
111
188
  } catch (err: unknown) {
112
189
  if (abort.signal.aborted) return;
113
190
  if (id === requestIdRef.current)
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  useEffect,
3
+ useCallback,
3
4
  useRef,
4
5
  useState,
5
6
  type ReactNode,
@@ -13,6 +14,8 @@ export interface ComposerContextPickerItem {
13
14
  id: string;
14
15
  title: string;
15
16
  description?: string | null;
17
+ group?: string;
18
+ icon?: ReactNode;
16
19
  preview?: ReactNode;
17
20
  metadata?: ReactNode;
18
21
  disabled?: boolean;
@@ -45,8 +48,10 @@ export type ComposerContextPickerFooterAction = {
45
48
  export type ComposerContextPickerConfig = {
46
49
  footerAction?: ComposerContextPickerFooterAction;
47
50
  searchPlaceholder: string;
51
+ searchable?: boolean;
48
52
  selectedIds?: readonly string[];
49
53
  scopeKey?: string;
54
+ identityScope?: "workspace";
50
55
  refreshKey?: string | number;
51
56
  link?: {
52
57
  label?: string;
@@ -84,7 +89,7 @@ export type ComposerContextPickerConfig = {
84
89
  | { type: "dialog"; mode: "url" }
85
90
  | {
86
91
  type: "dialog";
87
- mode: "single";
92
+ mode: "single" | "action";
88
93
  layout?: "list" | "gallery";
89
94
  };
90
95
  onSelect: (
@@ -148,9 +153,17 @@ export function useComposerContextPicker({
148
153
  const [chosen, setChosen] = useState<
149
154
  ReadonlyMap<string, ComposerContextPickerItem>
150
155
  >(() => new Map());
151
- const [stage, setStage] = useState<"link" | "results">(
152
- config.link || presentation?.mode === "url" ? "link" : "results",
156
+ const linkMode = Boolean(config.link || presentation?.mode === "url");
157
+ const [previousLinkMode, setPreviousLinkMode] = useState(linkMode);
158
+ const [currentStage, setStage] = useState<"link" | "results">(
159
+ linkMode ? "link" : "results",
153
160
  );
161
+ const stage =
162
+ previousLinkMode === linkMode
163
+ ? currentStage
164
+ : linkMode
165
+ ? "link"
166
+ : "results";
154
167
  const [link, setLink] = useState("");
155
168
  const [location, setLocation] = useState<PickerLocation>({
156
169
  search: "",
@@ -174,13 +187,13 @@ export function useComposerContextPicker({
174
187
  const keyRef = useRef(key);
175
188
  keyRef.current = key;
176
189
 
177
- const invalidateSelection = () => {
190
+ const invalidateSelection = useCallback(() => {
178
191
  epoch.current++;
179
192
  pendingSelection.current?.abort();
180
193
  pendingSelection.current = null;
181
194
  setSelecting(false);
182
195
  setActionError(undefined);
183
- };
196
+ }, []);
184
197
  useEffect(
185
198
  () => () => {
186
199
  epoch.current++;
@@ -189,6 +202,13 @@ export function useComposerContextPicker({
189
202
  [],
190
203
  );
191
204
 
205
+ useEffect(() => {
206
+ if (previousLinkMode === linkMode) return;
207
+ invalidateSelection();
208
+ setPreviousLinkMode(linkMode);
209
+ setStage(linkMode ? "link" : "results");
210
+ }, [previousLinkMode, linkMode, invalidateSelection]);
211
+
192
212
  useEffect(() => {
193
213
  if (!remote || stage !== "results") return;
194
214
  const abort = new AbortController();