@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,18 @@
1
1
  import { describeModelForEngine } from "@agent-native/core/agent/model-catalog";
2
+ import {
3
+ getAgentNativeAgentKitUploadDriver,
4
+ clearAgentNativeAgentKitUploads,
5
+ } from "@agent-native/core/client/agentkit-chat/transport";
2
6
  import { callAction } from "@agent-native/core/client/hooks";
3
7
  import { useT } from "@agent-native/core/client/i18n";
4
- import { useMemo } from "react";
8
+ import { readFileUploadStatus } from "@agent-native/core/client/uploads";
9
+ import { useSession } from "@agent-native/core/client/use-session";
10
+ import { useMemo, useEffect } from "react";
5
11
 
12
+ import { CoreFileStorageSetupContext } from "../../agentkit/react/core-file-storage.js";
6
13
  import { AgentKitRoot } from "../../agentkit/react/root.js";
7
14
  import type { AgentKitRootProps } from "../../agentkit/react/root.js";
15
+ import { FileStorageSetupPopover } from "../FileStorageSetupPopover.js";
8
16
  import { AgentKitActionWidget } from "./action-widget.js";
9
17
  import { CoreAgentKitApproval } from "./approval-card.js";
10
18
  import { agentKitWorkStepLabels } from "./work-step-labels.js";
@@ -31,6 +39,76 @@ function coreModelDisplayName(model: string): string {
31
39
 
32
40
  export function CoreAgentKitRoot(props: AgentKitRootProps) {
33
41
  const t = useT();
42
+ const nativeUpload = props.transport
43
+ ? getAgentNativeAgentKitUploadDriver(props.transport)
44
+ : undefined;
45
+ const { session } = useSession();
46
+ const scopeKey = JSON.stringify([
47
+ session?.email ?? null,
48
+ session?.orgId ?? null,
49
+ session?.authUserId ?? null,
50
+ ]);
51
+ const storageScope = useMemo(
52
+ () =>
53
+ nativeUpload
54
+ ? {
55
+ scopeKey,
56
+ readStatus: readFileUploadStatus,
57
+ Setup: FileStorageSetupPopover,
58
+ }
59
+ : null,
60
+ [nativeUpload, scopeKey],
61
+ );
62
+ useEffect(
63
+ () => () => {
64
+ if (props.transport) clearAgentNativeAgentKitUploads(props.transport);
65
+ },
66
+ [props.transport, scopeKey],
67
+ );
68
+ const upload = useMemo(
69
+ () =>
70
+ nativeUpload
71
+ ? async (...args: Parameters<typeof nativeUpload>) => {
72
+ try {
73
+ await nativeUpload(...args);
74
+ } catch (cause) {
75
+ const code =
76
+ cause && typeof cause === "object" && "code" in cause
77
+ ? cause.code
78
+ : undefined;
79
+ const key =
80
+ code === "upload_too_large"
81
+ ? "agentChat.composer.fileTooLarge"
82
+ : code === "upload_session_expired"
83
+ ? "agentChat.composer.sessionExpired"
84
+ : code === "upload_unsupported_type"
85
+ ? "agentChat.composer.unsupportedFileType"
86
+ : code === "upload_network_error"
87
+ ? "agentChat.composer.uploadOffline"
88
+ : code === "upload_unavailable"
89
+ ? "agentChat.composer.uploadUnavailable"
90
+ : "agentChat.composer.uploadFailed";
91
+ throw Object.assign(new Error(t(key, { size: 25 }), { cause }), {
92
+ code,
93
+ ...(cause && typeof cause === "object" && "status" in cause
94
+ ? { status: cause.status }
95
+ : {}),
96
+ ...(cause && typeof cause === "object" && "retryable" in cause
97
+ ? { retryable: cause.retryable }
98
+ : {}),
99
+ });
100
+ }
101
+ }
102
+ : undefined,
103
+ [nativeUpload, t],
104
+ );
105
+ const clientOptions = useMemo(
106
+ () => ({
107
+ ...props.clientOptions,
108
+ ...(!props.clientOptions?.upload && upload ? { upload } : {}),
109
+ }),
110
+ [props.clientOptions, upload],
111
+ );
34
112
  const slots = useMemo(
35
113
  () => ({
36
114
  ...props.slots,
@@ -109,13 +187,18 @@ export function CoreAgentKitRoot(props: AgentKitRootProps) {
109
187
  }),
110
188
  [props.labels, t],
111
189
  );
190
+ const rootProps: AgentKitRootProps = props.controller
191
+ ? props
192
+ : { ...props, clientOptions };
112
193
  return (
113
- <AgentKitRoot
114
- {...props}
115
- slots={slots}
116
- labels={labels}
117
- loadRunUsage={props.loadRunUsage ?? loadCoreRunUsage}
118
- formatModelName={props.formatModelName ?? coreModelDisplayName}
119
- />
194
+ <CoreFileStorageSetupContext.Provider value={storageScope}>
195
+ <AgentKitRoot
196
+ {...rootProps}
197
+ slots={slots}
198
+ labels={labels}
199
+ loadRunUsage={props.loadRunUsage ?? loadCoreRunUsage}
200
+ formatModelName={props.formatModelName ?? coreModelDisplayName}
201
+ />
202
+ </CoreFileStorageSetupContext.Provider>
120
203
  );
121
204
  }
@@ -51,6 +51,7 @@ export interface AssistantChatSendOptions {
51
51
  trackInRunsTray?: boolean;
52
52
  requestMode?: AgentRequestMode;
53
53
  attachments?: AgentChatAttachment[];
54
+ references?: AgentChatMessage["references"];
54
55
  /** Correlates with `AGENT_CHAT_SUBMIT_RESULT_EVENT` — see agent-chat.ts. */
55
56
  submitMessageId?: string;
56
57
  /** Drop a queued submit instead of handing it to chat after this time. */
@@ -50,6 +50,7 @@ import {
50
50
  import { BuilderSetupCard, BuilderSetupContent } from "../chat/run-recovery.js";
51
51
  import { AssistantUiStaleIndexErrorBoundary } from "./assistant-ui-recovery.js";
52
52
  import { coreComposerModelAdapters } from "./model-runtime-adapters.js";
53
+ import { useWorkspaceComposerMenu } from "./use-workspace-composer-menu.js";
53
54
 
54
55
  const REALTIME_VOICE_REQUEST_SOURCE = "realtime-voice";
55
56
 
@@ -102,6 +103,7 @@ export const coreComposerAdapters: CoreComposerRuntimeAdapters = {
102
103
  isTrustedFrameMessage,
103
104
  },
104
105
  resources: {
106
+ useWorkspaceMenu: useWorkspaceComposerMenu,
105
107
  useOrg,
106
108
  isMcpIntegrationAvailable: isMcpIntegrationCatalogAvailable,
107
109
  useCreateMcpServer,
@@ -71,8 +71,9 @@ describe("AgentKit capability lifecycle", () => {
71
71
  let container: HTMLDivElement;
72
72
  let result: ReturnType<typeof useAgentKitCapabilities>;
73
73
  let unmounted: boolean;
74
+ let enabled = true;
74
75
  function Harness() {
75
- result = useAgentKitCapabilities();
76
+ result = useAgentKitCapabilities({ enabled });
76
77
  return null;
77
78
  }
78
79
  async function render() {
@@ -102,6 +103,7 @@ describe("AgentKit capability lifecycle", () => {
102
103
  document.body.append(container);
103
104
  root = createRoot(container);
104
105
  unmounted = false;
106
+ enabled = true;
105
107
  });
106
108
  afterEach(async () => {
107
109
  if (!unmounted) await act(async () => root.unmount());
@@ -110,6 +112,29 @@ describe("AgentKit capability lifecycle", () => {
110
112
  vi.unstubAllGlobals();
111
113
  });
112
114
 
115
+ it("loads only on demand while preserving identity and cached results when the menu closes", async () => {
116
+ enabled = false;
117
+ await render();
118
+ const identity = result.scopeKey;
119
+ expect(mocks.call).not.toHaveBeenCalled();
120
+ expect(result.integrationsLoading).toBe(false);
121
+ enabled = true;
122
+ await render();
123
+ await settle();
124
+ expect(result.data).toEqual(catalog);
125
+ enabled = false;
126
+ await render();
127
+ expect(result.scopeKey).toBe(identity);
128
+ expect(result.data).toEqual(catalog);
129
+ mocks.call.mockResolvedValueOnce(empty);
130
+ await act(async () => {
131
+ await result.refetchIntegrations();
132
+ });
133
+ await settle();
134
+ expect(result.scopeKey).toBe(identity);
135
+ expect(result.data).toEqual(empty);
136
+ });
137
+
113
138
  it.each(["account", "organization", "app"] as const)(
114
139
  "isolates data and cancels stale requests across a change of %s",
115
140
  async (change) => {
@@ -25,16 +25,16 @@ export function agentKitCapabilityQueryKey(
25
25
  ] as const;
26
26
  }
27
27
 
28
- export function useAgentKitCapabilities() {
28
+ export function useAgentKitCapabilities(options: { enabled?: boolean } = {}) {
29
29
  const { session, status, error: sessionError } = useSession();
30
30
  const queryClient = useQueryClient();
31
- const enabled =
31
+ const authorized =
32
32
  status === "authenticated" &&
33
33
  Boolean(session?.email.trim()) &&
34
34
  !agentNativeApiDisabledReason();
35
35
  const queryKey = agentKitCapabilityQueryKey(
36
36
  agentNativePath("/_agent-native/actions/get-agentkit-capabilities"),
37
- enabled ? session : null,
37
+ authorized ? session : null,
38
38
  );
39
39
  const scopeKey = JSON.stringify(queryKey);
40
40
  const scope = useMemo(() => ({ key: scopeKey }), [scopeKey]);
@@ -55,10 +55,9 @@ export function useAgentKitCapabilities() {
55
55
  undefined,
56
56
  { method: "GET", signal },
57
57
  ),
58
- enabled,
58
+ enabled: authorized && options.enabled !== false,
59
59
  staleTime: 15_000,
60
60
  gcTime: 0,
61
- placeholderData: undefined,
62
61
  refetchOnMount: "always",
63
62
  refetchOnWindowFocus: "always",
64
63
  retry: defaultActionQueryRetry,
@@ -66,7 +65,7 @@ export function useAgentKitCapabilities() {
66
65
  });
67
66
 
68
67
  useEffect(() => {
69
- if (!enabled) return;
68
+ if (!authorized) return;
70
69
  const invalidate = () => {
71
70
  void queryClient.invalidateQueries({
72
71
  queryKey: ["action", "get-agentkit-capabilities"],
@@ -86,12 +85,12 @@ export function useAgentKitCapabilities() {
86
85
  unsubscribe();
87
86
  removeCompleteListener();
88
87
  };
89
- }, [enabled, queryClient]);
88
+ }, [authorized, queryClient]);
90
89
 
91
90
  const { refetch: refetchCapabilities } = capabilityQuery;
92
91
  const refetchIntegrations = useCallback(async () => {
93
92
  const assertCurrent = () => {
94
- if (!enabled || activeScope.current !== scope) {
93
+ if (!authorized || activeScope.current !== scope) {
95
94
  throw new DOMException("The operation was aborted.", "AbortError");
96
95
  }
97
96
  };
@@ -99,14 +98,15 @@ export function useAgentKitCapabilities() {
99
98
  const result = await refetchCapabilities({ throwOnError: true });
100
99
  assertCurrent();
101
100
  return result;
102
- }, [enabled, scope, refetchCapabilities]);
101
+ }, [authorized, scope, refetchCapabilities]);
103
102
 
104
103
  return {
105
104
  ...capabilityQuery,
106
105
  data:
107
- enabled && !capabilityQuery.isError ? capabilityQuery.data : undefined,
106
+ authorized && !capabilityQuery.isError ? capabilityQuery.data : undefined,
108
107
  integrationsLoading:
109
- status === "loading" || (enabled && capabilityQuery.isPending),
108
+ options.enabled !== false &&
109
+ (status === "loading" || (authorized && capabilityQuery.isPending)),
110
110
  integrationsError: sessionError ?? capabilityQuery.error,
111
111
  scopeKey,
112
112
  refetchIntegrations,
@@ -93,7 +93,6 @@ describe("shared AgentKit integration submenu", () => {
93
93
  container.querySelector('button[aria-label="Add context"]')!,
94
94
  "ArrowDown",
95
95
  );
96
- await key(row("Add context"), "ArrowRight");
97
96
  expect(row("Integrations").getAttribute("aria-haspopup")).toBe("menu");
98
97
  await key(row("Integrations"), "ArrowRight");
99
98
  }
@@ -101,7 +100,7 @@ describe("shared AgentKit integration submenu", () => {
101
100
  await render();
102
101
  await open();
103
102
  expect(pathname).toBe("/home");
104
- expect(document.querySelectorAll('[role="menu"]')).toHaveLength(3);
103
+ expect(document.querySelectorAll('[role="menu"]')).toHaveLength(2);
105
104
  expect(document.body.textContent).toContain(
106
105
  "No integrations available for this app.",
107
106
  );
@@ -130,7 +129,6 @@ describe("shared AgentKit integration submenu", () => {
130
129
  "ArrowDown",
131
130
  );
132
131
  expect(document.body.textContent).not.toContain("GitHub");
133
- await key(row("Add context"), "ArrowRight");
134
132
  await key(row("Integrations"), "ArrowRight");
135
133
  expect(row("GitHub").querySelector("svg")).toBeNull();
136
134
  expect(row("Slack")).toBeDefined();
@@ -174,7 +172,7 @@ describe("shared AgentKit integration submenu", () => {
174
172
  await render();
175
173
  await open();
176
174
  await key(row("Connect an integration…"), "Escape");
177
- expect(document.querySelectorAll('[role="menu"]')).toHaveLength(2);
175
+ expect(document.querySelectorAll('[role="menu"]')).toHaveLength(1);
178
176
  expect(document.activeElement).toBe(row("Integrations"));
179
177
  expect(pathname).toBe("/home");
180
178
  });
@@ -0,0 +1,174 @@
1
+ import type { WorkspaceComposerMenuContribution } from "@agent-native/toolkit/composer/runtime-adapters";
2
+ // @vitest-environment happy-dom
3
+ import { act } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import englishMessages from "../../i18n/catalogs/en-US.js";
8
+ import { useWorkspaceComposerMenu } from "./use-workspace-composer-menu.js";
9
+
10
+ const mocks = vi.hoisted(() => ({
11
+ capabilities: vi.fn(),
12
+ refetch: vi.fn(),
13
+ select: vi.fn(),
14
+ org: { orgId: "alpha", role: "member" },
15
+ }));
16
+ vi.mock("./use-agentkit-capabilities.js", () => ({
17
+ useAgentKitCapabilities: mocks.capabilities,
18
+ }));
19
+ vi.mock("@agent-native/core/client/i18n", () => ({
20
+ useT: () => (key: string) =>
21
+ (englishMessages as Record<string, string>)[
22
+ key.replace(/^agentChat\./, "")
23
+ ],
24
+ }));
25
+ vi.mock("@agent-native/core/client/org", () => ({
26
+ useOrg: () => ({ data: mocks.org }),
27
+ }));
28
+ vi.mock("@agent-native/core/client/resources", () => ({
29
+ isMcpIntegrationCatalogAvailable: () => true,
30
+ useCreateMcpServer: () => ({ mutateAsync: vi.fn() }),
31
+ }));
32
+ vi.mock("../../resources/index.js", () => ({
33
+ McpIntegrationDialogDeferred: ({
34
+ open,
35
+ defaultScope,
36
+ }: {
37
+ open: boolean;
38
+ defaultScope: string;
39
+ }) =>
40
+ open ? (
41
+ <div role="dialog" data-scope={defaultScope}>
42
+ Connect tools
43
+ </div>
44
+ ) : null,
45
+ }));
46
+
47
+ let root: ReturnType<typeof createRoot>;
48
+ let container: HTMLDivElement;
49
+ let contribution: WorkspaceComposerMenuContribution;
50
+ let enabled = false;
51
+ let state: {
52
+ scopeKey: string;
53
+ data:
54
+ | {
55
+ sources: { figma: { available: boolean } };
56
+ integrations: { id: string; label: string; kind: "mcp" }[];
57
+ }
58
+ | undefined;
59
+ integrationsLoading: boolean;
60
+ integrationsError: Error | null;
61
+ refetchIntegrations: typeof mocks.refetch;
62
+ };
63
+ function Harness() {
64
+ contribution = useWorkspaceComposerMenu({
65
+ enabled,
66
+ onSelectIntegration: mocks.select,
67
+ });
68
+ return (
69
+ <>
70
+ {contribution.items.map((item) => (
71
+ <button key={item.id} onClick={() => item.onSelect?.()}>
72
+ {item.label}
73
+ </button>
74
+ ))}
75
+ {contribution.error && <span role="alert">{contribution.error}</span>}
76
+ {contribution.overlays}
77
+ </>
78
+ );
79
+ }
80
+ async function render() {
81
+ await act(async () => root.render(<Harness />));
82
+ }
83
+ beforeEach(() => {
84
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
85
+ mocks.select.mockReset();
86
+ mocks.refetch.mockReset();
87
+ mocks.capabilities.mockReset();
88
+ state = {
89
+ scopeKey: "chat:member:alpha",
90
+ data: {
91
+ sources: { figma: { available: false } },
92
+ integrations: [{ id: "mcp:tools", label: "Tools", kind: "mcp" }],
93
+ },
94
+ integrationsLoading: false,
95
+ integrationsError: null,
96
+ refetchIntegrations: mocks.refetch,
97
+ };
98
+ mocks.capabilities.mockImplementation(() => state);
99
+ mocks.org = { orgId: "alpha", role: "member" };
100
+ enabled = false;
101
+ container = document.createElement("div");
102
+ document.body.append(container);
103
+ root = createRoot(container);
104
+ });
105
+ afterEach(async () => {
106
+ await act(async () => root.unmount());
107
+ container.remove();
108
+ vi.unstubAllGlobals();
109
+ });
110
+
111
+ describe("workspace composer contributions", () => {
112
+ it("stages real integration references and offers the shared setup primitive separately", async () => {
113
+ await render();
114
+ expect(mocks.capabilities).toHaveBeenLastCalledWith({ enabled: false });
115
+ enabled = true;
116
+ await render();
117
+ expect(mocks.capabilities).toHaveBeenLastCalledWith({ enabled: true });
118
+ await act(async () => container.querySelector("button")!.click());
119
+ expect(mocks.select).toHaveBeenCalledWith({
120
+ label: "Tools",
121
+ refType: "integration",
122
+ refId: "mcp:tools",
123
+ refPath: "mcp:tools",
124
+ source: "workspace-integration",
125
+ icon: "integration",
126
+ });
127
+ expect(
128
+ contribution.items.every((item) => item.section === "integrations"),
129
+ ).toBe(true);
130
+ await act(async () =>
131
+ [...container.querySelectorAll("button")]
132
+ .find((button) => button.textContent === "Connect an integration…")!
133
+ .click(),
134
+ );
135
+ expect(
136
+ container.querySelector('[role="dialog"]')?.getAttribute("data-scope"),
137
+ ).toBe("user");
138
+ });
139
+ it("starts organization administrators in their allowed shared scope", async () => {
140
+ mocks.org = { orgId: "alpha", role: "owner" };
141
+ await render();
142
+ await act(async () =>
143
+ [...container.querySelectorAll("button")]
144
+ .find((button) => button.textContent === "Connect an integration…")!
145
+ .click(),
146
+ );
147
+ expect(
148
+ container.querySelector('[role="dialog"]')?.getAttribute("data-scope"),
149
+ ).toBe("org");
150
+ });
151
+
152
+ it("rejects an old selection after identity changes away and back, and keeps unreadable catalogs visible as errors", async () => {
153
+ await render();
154
+ const select = contribution.items[0].onSelect!;
155
+ state = { ...state, scopeKey: "chat:member:beta" };
156
+ await render();
157
+ state = { ...state, scopeKey: "chat:member:alpha" };
158
+ await render();
159
+ expect(select).toThrow();
160
+ expect(mocks.select).not.toHaveBeenCalled();
161
+ state = {
162
+ ...state,
163
+ data: undefined,
164
+ integrationsError: new Error("permission lookup unavailable"),
165
+ };
166
+ await render();
167
+ expect(container.querySelector('[role="alert"]')?.textContent).toBe(
168
+ "Could not load integrations.",
169
+ );
170
+ expect(contribution.items.map((item) => item.id)).toEqual([
171
+ "workspace-connect-integrations",
172
+ ]);
173
+ });
174
+ });
@@ -0,0 +1,122 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { useOrg } from "@agent-native/core/client/org";
3
+ import {
4
+ isMcpIntegrationCatalogAvailable,
5
+ useCreateMcpServer,
6
+ } from "@agent-native/core/client/resources";
7
+ import { mcpIntegrationLogo } from "@agent-native/core/client/resources/mcp-integration-logos";
8
+ import type { ComposerContextMenuItem } from "@agent-native/toolkit/composer";
9
+ import type {
10
+ WorkspaceComposerMenuContribution,
11
+ WorkspaceComposerMenuOptions,
12
+ } from "@agent-native/toolkit/composer/runtime-adapters";
13
+ import { IconPlugConnected, IconPlus } from "@tabler/icons-react";
14
+ import { useEffect, useMemo, useRef, useState } from "react";
15
+
16
+ import { McpIntegrationDialogDeferred } from "../../resources/index.js";
17
+ import { useAgentKitCapabilities } from "./use-agentkit-capabilities.js";
18
+
19
+ export function useWorkspaceComposerMenu({
20
+ onSelectIntegration,
21
+ onRestoreFocus,
22
+ enabled,
23
+ }: WorkspaceComposerMenuOptions): WorkspaceComposerMenuContribution {
24
+ const t = useT();
25
+ const capabilities = useAgentKitCapabilities({ enabled });
26
+ const { data: org } = useOrg();
27
+ const createMcp = useCreateMcpServer();
28
+ const canCreateOrgMcp =
29
+ !org?.orgId || org.role === "owner" || org.role === "admin";
30
+ const [open, setOpen] = useState(false);
31
+ const scope = useMemo(
32
+ () => ({ key: capabilities.scopeKey }),
33
+ [capabilities.scopeKey],
34
+ );
35
+ const latest = useRef({ capabilities, onSelectIntegration, scope });
36
+ latest.current = { capabilities, onSelectIntegration, scope };
37
+ const mounted = useRef(true);
38
+ useEffect(() => {
39
+ mounted.current = true;
40
+ return () => {
41
+ mounted.current = false;
42
+ };
43
+ }, []);
44
+ useEffect(() => setOpen(false), [scope]);
45
+ const items: ComposerContextMenuItem[] = (
46
+ capabilities.data?.integrations ?? []
47
+ ).map((integration) => ({
48
+ id: `workspace-integration:${integration.id}`,
49
+ section: "integrations",
50
+ label: integration.label,
51
+ icon: mcpIntegrationLogo(integration.id) ? (
52
+ <img
53
+ src={mcpIntegrationLogo(integration.id)}
54
+ alt=""
55
+ className="size-4 object-contain"
56
+ />
57
+ ) : (
58
+ <IconPlugConnected />
59
+ ),
60
+ onSelect: () => {
61
+ const current = latest.current;
62
+ if (
63
+ !mounted.current ||
64
+ current.scope !== scope ||
65
+ current.capabilities.integrationsError ||
66
+ current.capabilities.integrationsLoading ||
67
+ !current.capabilities.data?.integrations.some(
68
+ (entry) => entry.id === integration.id,
69
+ )
70
+ ) {
71
+ throw new Error(t("agentChat.composer.integrations.loadFailed"));
72
+ }
73
+ current.onSelectIntegration({
74
+ label: integration.label,
75
+ refType: "integration",
76
+ refId: integration.id,
77
+ refPath: integration.id,
78
+ source: "workspace-integration",
79
+ icon: "integration",
80
+ });
81
+ },
82
+ }));
83
+ if (isMcpIntegrationCatalogAvailable())
84
+ items.push({
85
+ id: "workspace-connect-integrations",
86
+ section: "integrations",
87
+ order: 1000,
88
+ opensDialog: true,
89
+ label: t("agentChat.composer.integrations.connect"),
90
+ icon: <IconPlus />,
91
+ onSelect: () => setOpen(true),
92
+ });
93
+ return {
94
+ items,
95
+ scopeKey: capabilities.scopeKey,
96
+ loading: capabilities.integrationsLoading,
97
+ ...(capabilities.integrationsError
98
+ ? { error: t("agentChat.composer.integrations.loadFailed") }
99
+ : {}),
100
+ onRetry: capabilities.refetchIntegrations,
101
+ overlays: (
102
+ <McpIntegrationDialogDeferred
103
+ open={open}
104
+ onOpenChange={setOpen}
105
+ onCloseAutoFocus={(event) => {
106
+ if (
107
+ onRestoreFocus &&
108
+ mounted.current &&
109
+ latest.current.scope === scope
110
+ ) {
111
+ event.preventDefault();
112
+ onRestoreFocus();
113
+ }
114
+ }}
115
+ defaultScope={org?.orgId && !canCreateOrgMcp ? "user" : "org"}
116
+ canCreateOrgMcp={canCreateOrgMcp}
117
+ hasOrg={Boolean(org?.orgId)}
118
+ onCreateMcpServer={(args) => createMcp.mutateAsync(args)}
119
+ />
120
+ ),
121
+ };
122
+ }
@@ -80,3 +80,4 @@ export {
80
80
  type McpAppRendererProps,
81
81
  } from "./mcp-apps/McpAppRenderer.js";
82
82
  export type { AgentChatContextItem } from "@agent-native/core/client/agent-chat";
83
+ export { preparePromptComposerAgentHandoff } from "./prepare-prompt-composer-agent-handoff.js";