@agent-native/toolkit 0.198.3 → 0.198.4

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 (144) hide show
  1. package/dist/app/agentkit/react/components.d.ts +4 -1
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +12 -4
  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 +43 -21
  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/icons/ResourceIconPicker.d.ts +6 -1
  87. package/dist/icons/ResourceIconPicker.d.ts.map +1 -1
  88. package/dist/icons/ResourceIconPicker.js +45 -6
  89. package/dist/icons/ResourceIconPicker.js.map +1 -1
  90. package/package.json +6 -4
  91. package/src/app/agentkit/react/components.spec.tsx +3 -3
  92. package/src/app/agentkit/react/components.tsx +22 -4
  93. package/src/app/agentkit/react/lifecycle.spec.tsx +42 -0
  94. package/src/app/chat/AgentKitAssistantChat.spec.tsx +114 -11
  95. package/src/app/chat/AgentKitAssistantChat.tsx +78 -17
  96. package/src/app/chat/AgentPanel.header.spec.ts +59 -22
  97. package/src/app/chat/AgentSidebar.tsx +2 -0
  98. package/src/app/chat/FileStorageSetupPopover.tsx +2 -2
  99. package/src/app/chat/MultiTabAssistantChat.messageCount.spec.tsx +3 -1
  100. package/src/app/chat/MultiTabAssistantChat.spec.tsx +8 -4
  101. package/src/app/chat/MultiTabAssistantChat.tsx +1 -1
  102. package/src/app/chat/agentkit-chat/action-widget.spec.tsx +5 -17
  103. package/src/app/chat/agentkit-chat/connections.spec.tsx +137 -0
  104. package/src/app/chat/agentkit-chat/connections.ts +2 -37
  105. package/src/app/chat/agentkit-chat/history.tsx +23 -8
  106. package/src/app/chat/chat/history-types.ts +1 -1
  107. package/src/app/chat/chat/tool-call-display.spec.tsx +2 -2
  108. package/src/app/chat/chat/widgets/record-change-widget.spec.tsx +20 -29
  109. package/src/app/chat/chat-first/copy.ts +1 -133
  110. package/src/app/chat/chat-first-copy.ts +133 -0
  111. package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +3 -2
  112. package/src/app/feature-flags/FeatureFlagsPanel.tsx +1 -0
  113. package/src/app/i18n/catalogs/ar-SA.ts +2 -1
  114. package/src/app/i18n/catalogs/de-DE.ts +3 -1
  115. package/src/app/i18n/catalogs/en-US.ts +2 -1
  116. package/src/app/i18n/catalogs/es-ES.ts +2 -1
  117. package/src/app/i18n/catalogs/fr-FR.ts +3 -1
  118. package/src/app/i18n/catalogs/hi-IN.ts +2 -1
  119. package/src/app/i18n/catalogs/ja-JP.ts +2 -1
  120. package/src/app/i18n/catalogs/ko-KR.ts +2 -1
  121. package/src/app/i18n/catalogs/pt-BR.ts +3 -1
  122. package/src/app/i18n/catalogs/zh-CN.ts +2 -1
  123. package/src/app/i18n/catalogs/zh-TW.ts +2 -1
  124. package/src/app/integrations/IntegrationDetailPage.spec.tsx +1 -1
  125. package/src/app/labs/LabsSettings.tsx +46 -10
  126. package/src/app/observability/ThumbsFeedback.spec.tsx +15 -0
  127. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +4 -0
  128. package/src/app/org/OrgGeneralSection.tsx +32 -22
  129. package/src/app/org/OrgSwitcher.tsx +3 -2
  130. package/src/app/org/workspace-app-links.spec.ts +4 -0
  131. package/src/app/org/workspace-app-links.ts +8 -1
  132. package/src/app/resources/McpAgentKitConnectionRequest.spec.tsx +371 -2
  133. package/src/app/resources/McpAgentKitConnectionRequest.tsx +171 -11
  134. package/src/app/settings/SettingsTabsPage.spec.tsx +4 -2
  135. package/src/app/settings/shell/SettingsShell.spec.tsx +14 -8
  136. package/src/app/settings/shell/pages/app-group-pages.spec.tsx +35 -5
  137. package/src/app/settings/shell/pages/resource-pages.spec.tsx +3 -0
  138. package/src/app/settings/usage/UsagePage.spec.tsx +3 -3
  139. package/src/app/shared/lazy-chunk-error-boundary.spec.tsx +2 -0
  140. package/src/app-shell/sidebar-overflow.spec.tsx +4 -1
  141. package/src/design-tweaks/visual-style-controls.spec.tsx +38 -0
  142. package/src/editor/useCollabReconcile.concurrent.spec.ts +2 -1
  143. package/src/icons/ResourceIconPicker.spec.tsx +270 -0
  144. package/src/icons/ResourceIconPicker.tsx +72 -4
@@ -1,3 +1,4 @@
1
+ import { useActionQuery } from "@agent-native/core/client/hooks";
1
2
  import { useIconPickerLabels, useT } from "@agent-native/core/client/i18n";
2
3
  import {
3
4
  useOrg,
@@ -8,8 +9,11 @@ import {
8
9
  useSwitchOrg,
9
10
  useSetOrgWorkspaceUrl,
10
11
  } from "@agent-native/core/client/org";
11
- import { uploadEditorImage } from "@agent-native/core/client/uploads";
12
- import { useFileUploadStatus } from "@agent-native/core/client/uploads/use-file-upload-status";
12
+ import {
13
+ uploadWorkspacePrivateIcon,
14
+ workspacePrivateIconLibraryUrl,
15
+ workspacePrivateIconUrl,
16
+ } from "@agent-native/core/client/uploads";
13
17
  import type { IconValue } from "@agent-native/core/icons";
14
18
  import { docsUrl } from "@agent-native/core/shared/docs-url";
15
19
  import { ResourceIcon, ResourceIconPicker } from "@agent-native/toolkit/icons";
@@ -368,31 +372,37 @@ export function OrgIconControl({
368
372
  }) {
369
373
  const t = useT();
370
374
  const iconPickerLabels = useIconPickerLabels();
371
- const fileUploadStatus = useFileUploadStatus(canEdit);
372
- const fileStorageConfigured =
373
- fileUploadStatus.data?.configured === true && !fileUploadStatus.isError;
375
+ const { data: org } = useOrg();
376
+ const uploadedIcons = useActionQuery<{
377
+ assets: Array<{ id: string; alt?: string }>;
378
+ }>("list-workspace-icons", {}, { enabled: canEdit && !!org?.orgId });
374
379
  return canEdit ? (
375
380
  <ResourceIconPicker
376
381
  value={icon}
382
+ uploadedImages={(uploadedIcons.data?.assets ?? []).map((asset) => ({
383
+ version: 1,
384
+ kind: "image",
385
+ authority: "private-icon",
386
+ assetId: asset.id,
387
+ ...(asset.alt ? { alt: asset.alt } : {}),
388
+ }))}
389
+ uploadedImagesError={Boolean(uploadedIcons.error)}
390
+ onUploadedImagesRetry={() => {
391
+ void uploadedIcons.refetch();
392
+ }}
377
393
  onValueChange={async (next) => {
378
394
  await setVisualIdentity.mutateAsync(next);
379
395
  }}
380
- onUpload={
381
- fileStorageConfigured
382
- ? async (file) => {
383
- const uploaded = await uploadEditorImage(file);
384
- return {
385
- version: 1,
386
- kind: "image",
387
- authority: "url",
388
- assetId: uploaded.src,
389
- alt: uploaded.alt || file.name,
390
- };
391
- }
392
- : undefined
393
- }
396
+ onUpload={async (file) => {
397
+ const uploaded = await uploadWorkspacePrivateIcon(file);
398
+ void uploadedIcons.refetch();
399
+ return uploaded;
400
+ }}
401
+ formatUploadError={() => iconPickerLabels.uploadFailed}
394
402
  resolveImageUrl={(image) =>
395
- image.authority === "url" ? image.assetId : undefined
403
+ image.assetId === (icon?.kind === "image" ? icon.assetId : null)
404
+ ? workspacePrivateIconUrl(org?.orgId ?? "", image)
405
+ : workspacePrivateIconLibraryUrl(org?.orgId ?? "", image)
396
406
  }
397
407
  disabled={setVisualIdentity.isPending}
398
408
  labels={{
@@ -439,7 +449,7 @@ export function OrgIconControl({
439
449
  value={icon}
440
450
  size={16}
441
451
  resolveImageUrl={(image) =>
442
- image.authority === "url" ? image.assetId : undefined
452
+ workspacePrivateIconUrl(org?.orgId ?? "", image)
443
453
  }
444
454
  fallback={<IconUsersGroup className="size-4 text-muted-foreground" />}
445
455
  />
@@ -450,7 +460,7 @@ export function OrgIconControl({
450
460
  value={icon}
451
461
  size={16}
452
462
  resolveImageUrl={(image) =>
453
- image.authority === "url" ? image.assetId : undefined
463
+ workspacePrivateIconUrl(org?.orgId ?? "", image)
454
464
  }
455
465
  fallback={<IconUsersGroup className="size-4 text-muted-foreground" />}
456
466
  />
@@ -11,6 +11,7 @@ import {
11
11
  useJoinByDomain,
12
12
  } from "@agent-native/core/client/org";
13
13
  import { signOut } from "@agent-native/core/client/sign-out";
14
+ import { workspacePrivateIconUrl } from "@agent-native/core/client/uploads";
14
15
  import { setBrowserDemoModeEnabled } from "@agent-native/core/demo/browser-state";
15
16
  import { buildSettingsRoute } from "@agent-native/core/navigation";
16
17
  import { shouldOfferWorkspace } from "@agent-native/core/org/workspace-url";
@@ -503,7 +504,7 @@ export function OrgSwitcher({
503
504
  value={org.icon}
504
505
  size={12}
505
506
  resolveImageUrl={(image) =>
506
- image.authority === "url" ? image.assetId : undefined
507
+ workspacePrivateIconUrl(org.orgId ?? "", image)
507
508
  }
508
509
  fallback={<IconBriefcase className="size-3 shrink-0" />}
509
510
  />
@@ -585,7 +586,7 @@ export function OrgSwitcher({
585
586
  value={o.icon}
586
587
  size={14}
587
588
  resolveImageUrl={(image) =>
588
- image.authority === "url" ? image.assetId : undefined
589
+ workspacePrivateIconUrl(o.orgId, image)
589
590
  }
590
591
  fallback={<IconBriefcase className={ITEM_ICON_CLASS} />}
591
592
  />
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  defaultOrgAppLinks,
5
5
  dispatchAppsHref,
6
+ dispatchIntegrationsHref,
6
7
  isWorkspaceAppEnvironment,
7
8
  parseWorkspaceAppLinks,
8
9
  visibleOrgAppLinks,
@@ -66,6 +67,9 @@ describe("org switcher app links", () => {
66
67
  expect(dispatchAppsHref(apps ?? [])).toBe(
67
68
  "http://127.0.0.1:8080/dispatch/apps",
68
69
  );
70
+ expect(dispatchIntegrationsHref(apps ?? [])).toBe(
71
+ "http://127.0.0.1:8080/dispatch/integrations",
72
+ );
69
73
  });
70
74
 
71
75
  it("uses a published custom home path for workspace app links", () => {
@@ -266,7 +266,7 @@ export function isWorkspaceAppEnvironment(
266
266
 
267
267
  function dispatchPageHref(
268
268
  apps: OrgSwitcherAppLink[],
269
- page: "overview" | "apps" | "vault" | "workspace",
269
+ page: "overview" | "apps" | "vault" | "workspace" | "integrations",
270
270
  env: RuntimeEnv,
271
271
  ): string {
272
272
  const dispatch = apps.find((app) => app.isDispatch);
@@ -303,6 +303,13 @@ export function dispatchResourcesHref(
303
303
  return dispatchPageHref(apps, "workspace", env);
304
304
  }
305
305
 
306
+ export function dispatchIntegrationsHref(
307
+ apps: OrgSwitcherAppLink[],
308
+ env: RuntimeEnv = runtimeEnv(),
309
+ ): string {
310
+ return dispatchPageHref(apps, "integrations", env);
311
+ }
312
+
306
313
  export function visibleOrgAppLinks(
307
314
  apps: OrgSwitcherAppLink[],
308
315
  max = ORG_SWITCHER_MAX_APP_LINKS,
@@ -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");