@agent-native/toolkit 0.198.3 → 0.198.5

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 (157) hide show
  1. package/dist/app/agentkit/react/components.d.ts +8 -3
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +24 -10
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  6. package/dist/app/chat/AgentKitAssistantChat.js +65 -33
  7. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  8. package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
  9. package/dist/app/chat/AgentSidebar.js +2 -0
  10. package/dist/app/chat/AgentSidebar.js.map +1 -1
  11. package/dist/app/chat/FileStorageSetupPopover.js +2 -2
  12. package/dist/app/chat/FileStorageSetupPopover.js.map +1 -1
  13. package/dist/app/chat/MultiTabAssistantChat.js +1 -1
  14. package/dist/app/chat/agentkit-chat/connections.d.ts +1 -4
  15. package/dist/app/chat/agentkit-chat/connections.d.ts.map +1 -1
  16. package/dist/app/chat/agentkit-chat/connections.js +1 -26
  17. package/dist/app/chat/agentkit-chat/connections.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/history.d.ts +4 -1
  19. package/dist/app/chat/agentkit-chat/history.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/history.js +17 -5
  21. package/dist/app/chat/agentkit-chat/history.js.map +1 -1
  22. package/dist/app/chat/chat/history-types.d.ts +1 -1
  23. package/dist/app/chat/chat/history-types.d.ts.map +1 -1
  24. package/dist/app/chat/chat-first/copy.d.ts +1 -2
  25. package/dist/app/chat/chat-first/copy.d.ts.map +1 -1
  26. package/dist/app/chat/chat-first/copy.js +1 -121
  27. package/dist/app/chat/chat-first/copy.js.map +1 -1
  28. package/dist/app/chat/chat-first-copy.d.ts +3 -0
  29. package/dist/app/chat/chat-first-copy.d.ts.map +1 -0
  30. package/dist/app/chat/chat-first-copy.js +122 -0
  31. package/dist/app/chat/chat-first-copy.js.map +1 -0
  32. package/dist/app/feature-flags/FeatureFlagsPanel.d.ts.map +1 -1
  33. package/dist/app/feature-flags/FeatureFlagsPanel.js +1 -0
  34. package/dist/app/feature-flags/FeatureFlagsPanel.js.map +1 -1
  35. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/ar-SA.js +2 -1
  37. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  38. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  39. package/dist/app/i18n/catalogs/de-DE.js +2 -1
  40. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  41. package/dist/app/i18n/catalogs/en-US.d.ts +2 -1
  42. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/en-US.js +2 -1
  44. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  45. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/es-ES.js +2 -1
  47. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  48. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/fr-FR.js +2 -1
  50. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  51. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/hi-IN.js +2 -1
  53. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  54. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/ja-JP.js +2 -1
  56. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  57. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/ko-KR.js +2 -1
  59. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/pt-BR.js +2 -1
  62. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  63. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/zh-CN.js +2 -1
  65. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  66. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  67. package/dist/app/i18n/catalogs/zh-TW.js +2 -1
  68. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  69. package/dist/app/labs/LabsSettings.d.ts.map +1 -1
  70. package/dist/app/labs/LabsSettings.js +7 -3
  71. package/dist/app/labs/LabsSettings.js.map +1 -1
  72. package/dist/app/org/OrgGeneralSection.d.ts.map +1 -1
  73. package/dist/app/org/OrgGeneralSection.js +21 -18
  74. package/dist/app/org/OrgGeneralSection.js.map +1 -1
  75. package/dist/app/org/OrgSwitcher.d.ts.map +1 -1
  76. package/dist/app/org/OrgSwitcher.js +3 -2
  77. package/dist/app/org/OrgSwitcher.js.map +1 -1
  78. package/dist/app/org/workspace-app-links.d.ts +1 -0
  79. package/dist/app/org/workspace-app-links.d.ts.map +1 -1
  80. package/dist/app/org/workspace-app-links.js +3 -0
  81. package/dist/app/org/workspace-app-links.js.map +1 -1
  82. package/dist/app/resources/McpAgentKitConnectionRequest.d.ts +7 -2
  83. package/dist/app/resources/McpAgentKitConnectionRequest.d.ts.map +1 -1
  84. package/dist/app/resources/McpAgentKitConnectionRequest.js +132 -13
  85. package/dist/app/resources/McpAgentKitConnectionRequest.js.map +1 -1
  86. package/dist/composer/PromptComposer.d.ts +3 -0
  87. package/dist/composer/PromptComposer.d.ts.map +1 -1
  88. package/dist/composer/PromptComposer.js +2 -2
  89. package/dist/composer/PromptComposer.js.map +1 -1
  90. package/dist/composer/TiptapComposer.d.ts +4 -1
  91. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  92. package/dist/composer/TiptapComposer.js +8 -4
  93. package/dist/composer/TiptapComposer.js.map +1 -1
  94. package/dist/icons/ResourceIconPicker.d.ts +6 -1
  95. package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
  96. package/dist/icons/ResourceIconPicker.js +45 -6
  97. package/dist/icons/ResourceIconPicker.js.map +1 -1
  98. package/package.json +6 -4
  99. package/src/app/agentkit/react/components.interaction.spec.tsx +93 -0
  100. package/src/app/agentkit/react/components.spec.tsx +4 -5
  101. package/src/app/agentkit/react/components.tsx +51 -8
  102. package/src/app/agentkit/react/composer-context.spec.tsx +7 -0
  103. package/src/app/agentkit/react/lifecycle.spec.tsx +42 -0
  104. package/src/app/chat/AgentKitAssistantChat.spec.tsx +151 -11
  105. package/src/app/chat/AgentKitAssistantChat.tsx +110 -28
  106. package/src/app/chat/AgentPanel.header.spec.ts +59 -22
  107. package/src/app/chat/AgentSidebar.tsx +2 -0
  108. package/src/app/chat/FileStorageSetupPopover.tsx +2 -2
  109. package/src/app/chat/MultiTabAssistantChat.messageCount.spec.tsx +3 -1
  110. package/src/app/chat/MultiTabAssistantChat.spec.tsx +8 -4
  111. package/src/app/chat/MultiTabAssistantChat.tsx +1 -1
  112. package/src/app/chat/agentkit-chat/action-widget.spec.tsx +5 -17
  113. package/src/app/chat/agentkit-chat/connections.spec.tsx +137 -0
  114. package/src/app/chat/agentkit-chat/connections.ts +2 -37
  115. package/src/app/chat/agentkit-chat/history.tsx +23 -8
  116. package/src/app/chat/chat/history-types.ts +1 -1
  117. package/src/app/chat/chat/tool-call-display.spec.tsx +2 -2
  118. package/src/app/chat/chat/widgets/record-change-widget.spec.tsx +20 -29
  119. package/src/app/chat/chat-first/copy.ts +1 -133
  120. package/src/app/chat/chat-first-copy.ts +133 -0
  121. package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +3 -2
  122. package/src/app/feature-flags/FeatureFlagsPanel.tsx +1 -0
  123. package/src/app/i18n/catalogs/ar-SA.ts +2 -1
  124. package/src/app/i18n/catalogs/de-DE.ts +3 -1
  125. package/src/app/i18n/catalogs/en-US.ts +2 -1
  126. package/src/app/i18n/catalogs/es-ES.ts +2 -1
  127. package/src/app/i18n/catalogs/fr-FR.ts +3 -1
  128. package/src/app/i18n/catalogs/hi-IN.ts +2 -1
  129. package/src/app/i18n/catalogs/ja-JP.ts +2 -1
  130. package/src/app/i18n/catalogs/ko-KR.ts +2 -1
  131. package/src/app/i18n/catalogs/pt-BR.ts +3 -1
  132. package/src/app/i18n/catalogs/zh-CN.ts +2 -1
  133. package/src/app/i18n/catalogs/zh-TW.ts +2 -1
  134. package/src/app/integrations/IntegrationDetailPage.spec.tsx +1 -1
  135. package/src/app/labs/LabsSettings.tsx +46 -10
  136. package/src/app/observability/ThumbsFeedback.spec.tsx +15 -0
  137. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +4 -0
  138. package/src/app/org/OrgGeneralSection.tsx +32 -22
  139. package/src/app/org/OrgSwitcher.tsx +3 -2
  140. package/src/app/org/workspace-app-links.spec.ts +4 -0
  141. package/src/app/org/workspace-app-links.ts +8 -1
  142. package/src/app/resources/McpAgentKitConnectionRequest.spec.tsx +371 -2
  143. package/src/app/resources/McpAgentKitConnectionRequest.tsx +171 -11
  144. package/src/app/settings/SettingsTabsPage.spec.tsx +4 -2
  145. package/src/app/settings/shell/SettingsShell.spec.tsx +14 -8
  146. package/src/app/settings/shell/pages/app-group-pages.spec.tsx +35 -5
  147. package/src/app/settings/shell/pages/resource-pages.spec.tsx +3 -0
  148. package/src/app/settings/usage/UsagePage.spec.tsx +3 -3
  149. package/src/app/shared/lazy-chunk-error-boundary.spec.tsx +2 -0
  150. package/src/app-shell/sidebar-overflow.spec.tsx +4 -1
  151. package/src/composer/PromptComposer.tsx +7 -0
  152. package/src/composer/TiptapComposer.spec.ts +26 -4
  153. package/src/composer/TiptapComposer.tsx +12 -3
  154. package/src/design-tweaks/visual-style-controls.spec.tsx +38 -0
  155. package/src/editor/useCollabReconcile.concurrent.spec.ts +2 -1
  156. package/src/icons/ResourceIconPicker.spec.tsx +270 -0
  157. package/src/icons/ResourceIconPicker.tsx +72 -4
@@ -1,15 +1,79 @@
1
1
  // @vitest-environment happy-dom
2
2
 
3
- import { saveMcpConnectionResume } from "@agent-native/core/client/resources/mcp-connection-resume";
3
+ import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
4
+ import {
5
+ clearMcpConnectionResume,
6
+ consumeMcpConnectionResume,
7
+ notifyMcpConnectionComplete,
8
+ saveMcpConnectionResume,
9
+ } from "@agent-native/core/client/resources/mcp-connection-resume";
4
10
  import React, { act } from "react";
5
11
  import { createRoot } from "react-dom/client";
6
- import { describe, expect, it, vi } from "vitest";
12
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
13
 
14
+ import { dispatchIntegrationsHref } from "../org/workspace-app-links.js";
8
15
  import {
9
16
  McpAgentKitConnectionRequestCard,
10
17
  McpAgentKitConnectionResume,
11
18
  } from "./McpAgentKitConnectionRequest.js";
12
19
 
20
+ const popupState = vi.hoisted(() => ({ popup: null as unknown }));
21
+
22
+ afterEach(() => {
23
+ clearMcpConnectionResume();
24
+ window.localStorage.clear();
25
+ vi.unstubAllEnvs();
26
+ });
27
+
28
+ vi.mock("@agent-native/core/client/oauth-popup", () => ({
29
+ openOAuthPopup: vi.fn(() => popupState.popup as Window | null),
30
+ }));
31
+
32
+ vi.mock("../org/workspace-app-links.js", async (importOriginal) => {
33
+ const actual =
34
+ await importOriginal<typeof import("../org/workspace-app-links.js")>();
35
+ return {
36
+ ...actual,
37
+ useOrgSwitcherAppLinks: () => ({
38
+ apps: [],
39
+ isWorkspace: false,
40
+ isLoading: false,
41
+ dispatchHref: "",
42
+ dispatchAllAppsHref: "",
43
+ dispatchVaultHref: "",
44
+ dispatchResourcesHref: "",
45
+ }),
46
+ };
47
+ });
48
+
49
+ vi.mock("../agentkit/react/components.js", async () => {
50
+ const { createElement } = await import("react");
51
+ return {
52
+ AgentConnectionRequestCard: ({
53
+ request,
54
+ retry,
55
+ onConnect,
56
+ }: {
57
+ request: { provider: string; status: string };
58
+ retry?: boolean;
59
+ onConnect?: () => void | boolean | Promise<void | boolean>;
60
+ }) => {
61
+ if (request.status === "connected" || request.status === "declined") {
62
+ return null;
63
+ }
64
+ return createElement(
65
+ "button",
66
+ {
67
+ type: "button",
68
+ "data-status": request.status,
69
+ onClick: () => void onConnect?.(),
70
+ },
71
+ `${retry ? "Try again" : "Connect"} ${request.provider}`,
72
+ );
73
+ },
74
+ };
75
+ });
76
+
13
77
  describe("McpAgentKitConnectionRequestCard", () => {
14
78
  it("keeps an unknown provider request visible without trusting its setup data", () => {
15
79
  const container = document.createElement("div");
@@ -37,6 +101,311 @@ describe("McpAgentKitConnectionRequestCard", () => {
37
101
  act(() => root.unmount());
38
102
  });
39
103
 
104
+ it("keeps the request alive in a popup and resumes it on OAuth completion", async () => {
105
+ window.sessionStorage.clear();
106
+ vi.stubEnv("VITE_APP_BASE_PATH", "/dispatch");
107
+ window.history.replaceState(
108
+ {},
109
+ "",
110
+ "/dispatch/chat/thread-1?tab=docs#selected",
111
+ );
112
+ vi.mocked(openOAuthPopup).mockClear();
113
+ const locationAssign = vi.fn();
114
+ const popup = {
115
+ closed: false,
116
+ location: { assign: locationAssign },
117
+ close: vi.fn(),
118
+ };
119
+ popupState.popup = popup;
120
+ const onConnected = vi.fn();
121
+ const target = {
122
+ threadId: "thread-1",
123
+ runId: "run-1",
124
+ requestId: "request-1",
125
+ };
126
+ const container = document.createElement("div");
127
+ const root = createRoot(container);
128
+
129
+ await act(async () => {
130
+ root.render(
131
+ <>
132
+ <McpAgentKitConnectionRequestCard
133
+ provider="google_drive"
134
+ reason="connect"
135
+ appId="dispatch"
136
+ source={{
137
+ id: "google_drive",
138
+ kind: "workspace_connection",
139
+ label: "Google Drive",
140
+ }}
141
+ target={target}
142
+ onConnected={onConnected}
143
+ onDeclined={() => undefined}
144
+ />
145
+ <McpAgentKitConnectionResume
146
+ onResume={() => onConnected()}
147
+ onMessageResume={() => undefined}
148
+ />
149
+ </>,
150
+ );
151
+ });
152
+ const button = container.querySelector("button");
153
+ await act(async () => button?.click());
154
+
155
+ expect(openOAuthPopup).toHaveBeenCalledWith({
156
+ features: "width=640,height=760",
157
+ });
158
+ const oauthUrl = new URL(
159
+ locationAssign.mock.calls[0]![0],
160
+ window.location.origin,
161
+ );
162
+ expect(oauthUrl.pathname).toContain(
163
+ "/connections/oauth/google_drive/start",
164
+ );
165
+ expect(oauthUrl.searchParams.get("appId")).toBe("dispatch");
166
+ expect(oauthUrl.searchParams.get("scope")).toBe("user");
167
+ const returnPath = oauthUrl.searchParams.get("return");
168
+ expect(returnPath).toContain(
169
+ "/_agent-native/oauth/popup?complete=workspace-connection&resume=",
170
+ );
171
+ expect(returnPath).not.toContain("#selected");
172
+ expect(consumeMcpConnectionResume()).toBeNull();
173
+ const completionId = new URL(
174
+ returnPath!,
175
+ window.location.origin,
176
+ ).searchParams.get("resume");
177
+ expect(completionId).toMatch(
178
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
179
+ );
180
+
181
+ await act(async () => {
182
+ window.dispatchEvent(
183
+ new MessageEvent("message", {
184
+ data: { type: "agent-native:workspace-connection-complete" },
185
+ origin: "https://untrusted.example",
186
+ source: popup as unknown as MessageEventSource,
187
+ }),
188
+ );
189
+ window.dispatchEvent(
190
+ new MessageEvent("message", {
191
+ data: { type: "agent-native:workspace-connection-complete" },
192
+ origin: window.location.origin,
193
+ source: null,
194
+ }),
195
+ );
196
+ await Promise.resolve();
197
+ });
198
+ expect(onConnected).not.toHaveBeenCalled();
199
+
200
+ act(() => root.unmount());
201
+ expect(popup.close).not.toHaveBeenCalled();
202
+ const resumedRoot = createRoot(container);
203
+ await act(async () => {
204
+ notifyMcpConnectionComplete(completionId!);
205
+ resumedRoot.render(
206
+ <McpAgentKitConnectionResume
207
+ onResume={() => onConnected()}
208
+ onMessageResume={() => undefined}
209
+ />,
210
+ );
211
+ await new Promise((resolve) => setTimeout(resolve, 0));
212
+ });
213
+ expect(onConnected).toHaveBeenCalledOnce();
214
+ act(() => resumedRoot.unmount());
215
+ });
216
+
217
+ it("keeps a granted connection request pending until the user retries", async () => {
218
+ const container = document.createElement("div");
219
+ const root = createRoot(container);
220
+ vi.mocked(openOAuthPopup).mockClear();
221
+ const locationAssign = vi.fn();
222
+ const popup = {
223
+ opener: window,
224
+ location: { assign: locationAssign },
225
+ close: vi.fn(),
226
+ } as unknown as Window;
227
+ const open = vi
228
+ .spyOn(window, "open")
229
+ .mockReturnValueOnce(null)
230
+ .mockReturnValue(popup);
231
+ const onConnected = vi.fn();
232
+
233
+ act(() => {
234
+ root.render(
235
+ <McpAgentKitConnectionRequestCard
236
+ provider="google_drive"
237
+ reason="grant"
238
+ appId="dispatch"
239
+ source={{
240
+ id: "google_drive",
241
+ kind: "workspace_connection",
242
+ label: "Google Drive",
243
+ }}
244
+ target={{
245
+ threadId: "thread-1",
246
+ runId: "run-1",
247
+ requestId: "request-1",
248
+ }}
249
+ onConnected={onConnected}
250
+ onDeclined={() => undefined}
251
+ />,
252
+ );
253
+ });
254
+
255
+ expect(container.textContent).toContain("Connect google_drive");
256
+ expect(openOAuthPopup).not.toHaveBeenCalled();
257
+ await act(async () => container.querySelector("button")?.click());
258
+ expect(open).toHaveBeenCalledWith("", "_blank");
259
+ expect(onConnected).not.toHaveBeenCalled();
260
+ expect(container.textContent).toContain("Connect google_drive");
261
+
262
+ await act(async () => container.querySelector("button")?.click());
263
+ expect(open).toHaveBeenCalledTimes(2);
264
+ expect(popup.opener).toBeNull();
265
+ expect(locationAssign).toHaveBeenCalledWith(dispatchIntegrationsHref([]));
266
+ expect(container.textContent).toContain("Try again google_drive");
267
+ expect(onConnected).not.toHaveBeenCalled();
268
+ await act(async () => container.querySelector("button")?.click());
269
+ expect(onConnected).toHaveBeenCalledOnce();
270
+
271
+ await act(async () => {
272
+ root.render(
273
+ <McpAgentKitConnectionRequestCard
274
+ provider="google_drive"
275
+ reason="grant"
276
+ status="connected"
277
+ appId="dispatch"
278
+ source={{
279
+ id: "google_drive",
280
+ kind: "workspace_connection",
281
+ label: "Google Drive",
282
+ }}
283
+ target={{
284
+ threadId: "thread-1",
285
+ runId: "run-1",
286
+ requestId: "request-1",
287
+ }}
288
+ onConnected={onConnected}
289
+ onDeclined={() => undefined}
290
+ />,
291
+ );
292
+ });
293
+ expect(container.querySelector("button")).toBeNull();
294
+
295
+ await act(async () => {
296
+ root.render(
297
+ <McpAgentKitConnectionRequestCard
298
+ provider="google_drive"
299
+ reason="grant"
300
+ status="failed"
301
+ appId="dispatch"
302
+ source={{
303
+ id: "google_drive",
304
+ kind: "workspace_connection",
305
+ label: "Google Drive",
306
+ }}
307
+ target={{
308
+ threadId: "thread-1",
309
+ runId: "run-1",
310
+ requestId: "request-1",
311
+ }}
312
+ onConnected={onConnected}
313
+ onDeclined={() => undefined}
314
+ />,
315
+ );
316
+ });
317
+ expect(container.querySelector("button")?.getAttribute("data-status")).toBe(
318
+ "failed",
319
+ );
320
+ await act(async () => container.querySelector("button")?.click());
321
+ expect(open).toHaveBeenCalledTimes(3);
322
+ expect(onConnected).toHaveBeenCalledOnce();
323
+ await act(async () => container.querySelector("button")?.click());
324
+ expect(onConnected).toHaveBeenCalledTimes(2);
325
+ act(() => root.unmount());
326
+ open.mockRestore();
327
+ });
328
+
329
+ it("keeps non-OAuth workspace requests pending until the user retries", async () => {
330
+ const container = document.createElement("div");
331
+ const root = createRoot(container);
332
+ vi.mocked(openOAuthPopup).mockClear();
333
+ const locationAssign = vi.fn();
334
+ const popup = {
335
+ opener: window,
336
+ location: { assign: locationAssign },
337
+ close: vi.fn(),
338
+ } as unknown as Window;
339
+ const open = vi.spyOn(window, "open").mockReturnValue(popup);
340
+ const onConnected = vi.fn();
341
+
342
+ act(() => {
343
+ root.render(
344
+ <McpAgentKitConnectionRequestCard
345
+ provider="slack"
346
+ appId="dispatch"
347
+ source={{
348
+ id: "slack",
349
+ kind: "workspace_connection",
350
+ label: "Slack",
351
+ }}
352
+ target={{
353
+ threadId: "thread-1",
354
+ runId: "run-1",
355
+ requestId: "request-1",
356
+ }}
357
+ onConnected={onConnected}
358
+ onDeclined={() => undefined}
359
+ fallback={<div data-unsupported-provider="">Setup unavailable</div>}
360
+ />,
361
+ );
362
+ });
363
+
364
+ expect(container.textContent).toContain("Connect slack");
365
+ expect(openOAuthPopup).not.toHaveBeenCalled();
366
+ await act(async () => container.querySelector("button")?.click());
367
+ expect(open).toHaveBeenCalledWith("", "_blank");
368
+ expect(popup.opener).toBeNull();
369
+ expect(locationAssign).toHaveBeenCalledWith(dispatchIntegrationsHref([]));
370
+ expect(onConnected).not.toHaveBeenCalled();
371
+ expect(container.textContent).toContain("Try again slack");
372
+ await act(async () => container.querySelector("button")?.click());
373
+ expect(onConnected).toHaveBeenCalledOnce();
374
+ act(() => root.unmount());
375
+ open.mockRestore();
376
+ });
377
+
378
+ it("passes terminal request state through to the shared card", () => {
379
+ const container = document.createElement("div");
380
+ const root = createRoot(container);
381
+
382
+ act(() => {
383
+ root.render(
384
+ <McpAgentKitConnectionRequestCard
385
+ provider="google_drive"
386
+ reason="connect"
387
+ status="declined"
388
+ appId="dispatch"
389
+ source={{
390
+ id: "google_drive",
391
+ kind: "workspace_connection",
392
+ label: "Google Drive",
393
+ }}
394
+ target={{
395
+ threadId: "thread-1",
396
+ runId: "run-1",
397
+ requestId: "request-1",
398
+ }}
399
+ onConnected={() => undefined}
400
+ onDeclined={() => undefined}
401
+ />,
402
+ );
403
+ });
404
+
405
+ expect(container.querySelector("button")).toBeNull();
406
+ act(() => root.unmount());
407
+ });
408
+
40
409
  it("resumes a prose-inferred connection request through the host thread", async () => {
41
410
  window.sessionStorage.clear();
42
411
  window.history.replaceState({}, "", "/chat/thread-1");
@@ -1,5 +1,18 @@
1
+ import type { AgentConnectionRequest } from "@agent-native/agentkit/protocol";
1
2
  import {
2
- consumeMcpConnectionResume,
3
+ agentNativePath,
4
+ appBasePath,
5
+ } from "@agent-native/core/client/api-path";
6
+ import { useT } from "@agent-native/core/client/i18n";
7
+ import {
8
+ getWorkspaceConnectionProvider,
9
+ workspaceProviderOAuthUrl,
10
+ } from "@agent-native/core/client/integrations";
11
+ import { openOAuthPopup } from "@agent-native/core/client/oauth-popup";
12
+ import {
13
+ addMcpConnectionCompleteListener,
14
+ clearMcpConnectionResume,
15
+ getPendingMcpConnectionResume,
3
16
  saveMcpConnectionResume,
4
17
  type McpConnectionResumeRequest,
5
18
  } from "@agent-native/core/client/resources/mcp-connection-resume";
@@ -7,8 +20,14 @@ import {
7
20
  getDefaultMcpIntegrations,
8
21
  navigateToMcpOAuthStart,
9
22
  } from "@agent-native/core/client/resources/mcp-integration-catalog";
10
- import { useEffect, useMemo, useRef, type ReactNode } from "react";
23
+ import { IconAlertCircle } from "@tabler/icons-react";
24
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
11
25
 
26
+ import { AgentConnectionRequestCard } from "../agentkit/react/components.js";
27
+ import {
28
+ dispatchIntegrationsHref,
29
+ useOrgSwitcherAppLinks,
30
+ } from "../org/workspace-app-links.js";
12
31
  import { McpConnectionSuggestion } from "./McpConnectionSuggestion.js";
13
32
 
14
33
  export interface McpAgentKitConnectionTarget {
@@ -20,6 +39,10 @@ export interface McpAgentKitConnectionTarget {
20
39
  export interface McpAgentKitConnectionRequestCardProps {
21
40
  provider: string;
22
41
  detail?: string;
42
+ reason?: AgentConnectionRequest["reason"];
43
+ status?: AgentConnectionRequest["status"];
44
+ appId?: string;
45
+ source?: AgentConnectionRequest["source"];
23
46
  target: McpAgentKitConnectionTarget;
24
47
  onConnected: () => void | Promise<void>;
25
48
  onDeclined: () => void | Promise<void>;
@@ -29,18 +52,35 @@ export interface McpAgentKitConnectionRequestCardProps {
29
52
  export function McpAgentKitConnectionRequestCard({
30
53
  provider,
31
54
  detail,
55
+ reason,
56
+ status = "requested",
57
+ appId,
58
+ source,
32
59
  target,
33
60
  onConnected,
34
61
  onDeclined,
35
62
  fallback = null,
36
63
  }: McpAgentKitConnectionRequestCardProps) {
37
64
  const settledRef = useRef(false);
65
+ const [workspaceSetupOpened, setWorkspaceSetupOpened] = useState(false);
38
66
  const integrations = useMemo(() => getDefaultMcpIntegrations(), []);
39
67
  const integration = integrations.find(
40
68
  (candidate) =>
41
69
  candidate.id.toLowerCase() === provider.trim().toLowerCase() ||
42
70
  candidate.provider.toLowerCase() === provider.trim().toLowerCase(),
43
71
  );
72
+ const workspaceProvider =
73
+ source?.kind === "workspace_connection" && source.id === provider
74
+ ? getWorkspaceConnectionProvider(source.id)
75
+ : null;
76
+ const needsWorkspaceSetup =
77
+ source?.kind === "workspace_connection" &&
78
+ source.id === provider &&
79
+ (reason === "grant" || !workspaceProvider?.oauth);
80
+ const { apps: workspaceApps } = useOrgSwitcherAppLinks(needsWorkspaceSetup);
81
+ useEffect(() => {
82
+ if (status === "failed") setWorkspaceSetupOpened(false);
83
+ }, [status]);
44
84
  const settle = async (callback: () => void | Promise<void>) => {
45
85
  if (settledRef.current) return;
46
86
  settledRef.current = true;
@@ -51,6 +91,86 @@ export function McpAgentKitConnectionRequestCard({
51
91
  throw error;
52
92
  }
53
93
  };
94
+ if (source?.kind === "workspace_connection") {
95
+ if (!workspaceProvider || !appId) return fallback;
96
+ const request: AgentConnectionRequest = {
97
+ id: target.requestId,
98
+ provider,
99
+ reason: reason ?? "connect",
100
+ status,
101
+ appId,
102
+ detail,
103
+ source,
104
+ };
105
+ if (reason === "grant" || !workspaceProvider.oauth) {
106
+ return (
107
+ <AgentConnectionRequestCard
108
+ request={request}
109
+ runId={target.runId}
110
+ providerLabel={source.label}
111
+ retry={workspaceSetupOpened}
112
+ onConnect={() => {
113
+ if (workspaceSetupOpened) return onConnected();
114
+ const setupWindow = window.open("", "_blank");
115
+ if (!setupWindow) return false;
116
+ try {
117
+ setupWindow.opener = null;
118
+ setupWindow.location.assign(
119
+ dispatchIntegrationsHref(workspaceApps),
120
+ );
121
+ } catch {
122
+ setupWindow.close();
123
+ return false;
124
+ }
125
+ setWorkspaceSetupOpened(true);
126
+ }}
127
+ />
128
+ );
129
+ }
130
+ return (
131
+ <AgentConnectionRequestCard
132
+ request={request}
133
+ runId={target.runId}
134
+ providerLabel={source.label}
135
+ onConnect={() => {
136
+ const popup = openOAuthPopup({ features: "width=640,height=760" });
137
+ if (!popup) return false;
138
+ const completionId = crypto.randomUUID();
139
+ const message = detail ?? `Continue after connecting ${provider}.`;
140
+ if (!saveMcpConnectionResume(message, target, completionId)) {
141
+ popup.close();
142
+ return false;
143
+ }
144
+ const returnUrl = new URL(
145
+ agentNativePath("/_agent-native/oauth/popup"),
146
+ window.location.href,
147
+ );
148
+ returnUrl.searchParams.set("complete", "workspace-connection");
149
+ returnUrl.searchParams.set("resume", completionId);
150
+ const basePath = appBasePath();
151
+ const hasBasePath =
152
+ basePath && returnUrl.pathname.startsWith(basePath + "/");
153
+ const returnPath =
154
+ (hasBasePath
155
+ ? returnUrl.pathname.slice(basePath.length)
156
+ : returnUrl.pathname) + returnUrl.search;
157
+ try {
158
+ popup.location.assign(
159
+ workspaceProviderOAuthUrl(source.id, {
160
+ appId,
161
+ scope: "user",
162
+ returnPath,
163
+ }),
164
+ );
165
+ } catch (error) {
166
+ clearMcpConnectionResume(completionId);
167
+ popup.close();
168
+ throw error;
169
+ }
170
+ }}
171
+ />
172
+ );
173
+ }
54
174
  if (!integration) return fallback;
55
175
  return (
56
176
  <McpConnectionSuggestion
@@ -87,14 +207,54 @@ export function McpAgentKitConnectionResume({
87
207
  onResume,
88
208
  onMessageResume,
89
209
  }: McpAgentKitConnectionResumeProps) {
210
+ const t = useT();
211
+ const onResumeRef = useRef(onResume);
212
+ const onMessageResumeRef = useRef(onMessageResume);
213
+ const processingRef = useRef(false);
214
+ const [failed, setFailed] = useState(false);
215
+ onResumeRef.current = onResume;
216
+ onMessageResumeRef.current = onMessageResume;
217
+
90
218
  useEffect(() => {
91
- const pending: McpConnectionResumeRequest | null =
92
- consumeMcpConnectionResume();
93
- if (pending?.agentKit) {
94
- void Promise.resolve(onResume(pending.agentKit, pending)).catch(() => {});
95
- } else if (pending) {
96
- void Promise.resolve(onMessageResume?.(pending)).catch(() => {});
97
- }
98
- }, [onMessageResume, onResume]);
99
- return null;
219
+ const resumePending = async () => {
220
+ if (processingRef.current) return;
221
+ const pending = getPendingMcpConnectionResume();
222
+ if (!pending) return;
223
+ processingRef.current = true;
224
+ try {
225
+ if (pending.agentKit) {
226
+ try {
227
+ await onResumeRef.current(pending.agentKit, pending);
228
+ } catch (error) {
229
+ if (
230
+ !(error instanceof Error) ||
231
+ !error.message.startsWith("Unknown AgentKit run:") ||
232
+ !onMessageResumeRef.current
233
+ ) {
234
+ throw error;
235
+ }
236
+ await onMessageResumeRef.current(pending);
237
+ }
238
+ } else {
239
+ if (!onMessageResumeRef.current) return;
240
+ await onMessageResumeRef.current(pending);
241
+ }
242
+ clearMcpConnectionResume(pending);
243
+ setFailed(false);
244
+ } finally {
245
+ processingRef.current = false;
246
+ }
247
+ };
248
+ const resume = () => {
249
+ void resumePending().catch(() => setFailed(true));
250
+ };
251
+ resume();
252
+ return addMcpConnectionCompleteListener(resume);
253
+ }, []);
254
+ return failed ? (
255
+ <div className="agentkit-command-error" role="alert">
256
+ <IconAlertCircle aria-hidden="true" className="agentkit-icon" />
257
+ <span>{t("agentChat.connection.failed")}</span>
258
+ </div>
259
+ ) : null;
100
260
  }
@@ -518,6 +518,7 @@ describe("SettingsTabsPage", () => {
518
518
  });
519
519
 
520
520
  it("renders What's new markdown in the legacy settings fallback", async () => {
521
+ await import("../changelog/Changelog.js");
521
522
  act(() => {
522
523
  root.render(
523
524
  <SettingsTabsPage
@@ -607,11 +608,11 @@ describe("SettingsTabsPage", () => {
607
608
  ]);
608
609
  });
609
610
 
610
- it("keeps shell-only app groups out of the tabbed page", () => {
611
+ it("keeps shell-only app groups out of the tabbed page with legacy General content", () => {
611
612
  act(() => {
612
613
  root.render(
613
614
  <SettingsTabsPage
614
- general={<div>General content</div>}
615
+ general={<div>Legacy General content</div>}
615
616
  generalGroups={<div>App groups</div>}
616
617
  notifications={<div>Email settings</div>}
617
618
  notificationsLabel="Notifications"
@@ -644,6 +645,7 @@ describe("SettingsTabsPage", () => {
644
645
  ]);
645
646
  // `generalGroups` may call useSettingsShell(), which throws here.
646
647
  expect(container.textContent).not.toContain("App groups");
648
+ expect(container.textContent).toContain("Legacy General content");
647
649
  });
648
650
 
649
651
  it("visually separates app, agent, and workspace tabs", () => {
@@ -35,10 +35,14 @@ const appState = vi.hoisted(() => ({
35
35
  write: vi.fn(async (_key: string, value: unknown) => value),
36
36
  remove: vi.fn(async () => undefined),
37
37
  }));
38
- vi.mock("@agent-native/core/client/application-state", () => ({
39
- writeClientAppState: appState.write,
40
- deleteClientAppState: appState.remove,
41
- }));
38
+ vi.mock(
39
+ "@agent-native/core/client/application-state",
40
+ async (importOriginal) => ({
41
+ ...(await importOriginal<Record<string, unknown>>()),
42
+ writeClientAppState: appState.write,
43
+ deleteClientAppState: appState.remove,
44
+ }),
45
+ );
42
46
 
43
47
  import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
44
48
 
@@ -728,10 +732,12 @@ describe("SettingsShell", () => {
728
732
  // the router is still committing the previous navigation.
729
733
  const [, setTick] = useState(0);
730
734
  React.useEffect(() => {
731
- // Several ticks land inside each 60ms loader, so the shell re-renders
732
- // mid-navigation; a tick shorter than one render keeps act() from ever
733
- // going idle on a slow runner.
734
- const timer = setInterval(() => setTick((tick) => tick + 1), 25);
735
+ // Re-render during the 60ms loader without keeping act() busy forever.
736
+ let ticks = 0;
737
+ const timer = setInterval(() => {
738
+ setTick((tick) => tick + 1);
739
+ if (++ticks === 5) clearInterval(timer);
740
+ }, 10);
735
741
  return () => clearInterval(timer);
736
742
  }, []);
737
743
  const current = useLocation();