@agent-native/toolkit 0.198.3-nightly-20260930154911 → 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 (146) 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 +3 -1
  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 +7 -5
  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.spec.tsx +2 -2
  98. package/src/app/chat/AgentSidebar.tsx +3 -3
  99. package/src/app/chat/FileStorageSetupPopover.tsx +2 -2
  100. package/src/app/chat/MultiTabAssistantChat.messageCount.spec.tsx +3 -1
  101. package/src/app/chat/MultiTabAssistantChat.spec.tsx +8 -4
  102. package/src/app/chat/MultiTabAssistantChat.tsx +1 -1
  103. package/src/app/chat/agent-sidebar-url-sync.sessions.spec.tsx +89 -0
  104. package/src/app/chat/agentkit-chat/action-widget.spec.tsx +5 -17
  105. package/src/app/chat/agentkit-chat/connections.spec.tsx +137 -0
  106. package/src/app/chat/agentkit-chat/connections.ts +2 -37
  107. package/src/app/chat/agentkit-chat/history.tsx +23 -8
  108. package/src/app/chat/chat/history-types.ts +1 -1
  109. package/src/app/chat/chat/tool-call-display.spec.tsx +2 -2
  110. package/src/app/chat/chat/widgets/record-change-widget.spec.tsx +20 -29
  111. package/src/app/chat/chat-first/copy.ts +1 -133
  112. package/src/app/chat/chat-first-copy.ts +133 -0
  113. package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +3 -2
  114. package/src/app/feature-flags/FeatureFlagsPanel.tsx +1 -0
  115. package/src/app/i18n/catalogs/ar-SA.ts +2 -1
  116. package/src/app/i18n/catalogs/de-DE.ts +3 -1
  117. package/src/app/i18n/catalogs/en-US.ts +2 -1
  118. package/src/app/i18n/catalogs/es-ES.ts +2 -1
  119. package/src/app/i18n/catalogs/fr-FR.ts +3 -1
  120. package/src/app/i18n/catalogs/hi-IN.ts +2 -1
  121. package/src/app/i18n/catalogs/ja-JP.ts +2 -1
  122. package/src/app/i18n/catalogs/ko-KR.ts +2 -1
  123. package/src/app/i18n/catalogs/pt-BR.ts +3 -1
  124. package/src/app/i18n/catalogs/zh-CN.ts +2 -1
  125. package/src/app/i18n/catalogs/zh-TW.ts +2 -1
  126. package/src/app/integrations/IntegrationDetailPage.spec.tsx +1 -1
  127. package/src/app/labs/LabsSettings.tsx +46 -10
  128. package/src/app/observability/ThumbsFeedback.spec.tsx +15 -0
  129. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +4 -0
  130. package/src/app/org/OrgGeneralSection.tsx +32 -22
  131. package/src/app/org/OrgSwitcher.tsx +3 -2
  132. package/src/app/org/workspace-app-links.spec.ts +4 -0
  133. package/src/app/org/workspace-app-links.ts +8 -1
  134. package/src/app/resources/McpAgentKitConnectionRequest.spec.tsx +371 -2
  135. package/src/app/resources/McpAgentKitConnectionRequest.tsx +171 -11
  136. package/src/app/settings/SettingsTabsPage.spec.tsx +4 -2
  137. package/src/app/settings/shell/SettingsShell.spec.tsx +14 -8
  138. package/src/app/settings/shell/pages/app-group-pages.spec.tsx +35 -5
  139. package/src/app/settings/shell/pages/resource-pages.spec.tsx +3 -0
  140. package/src/app/settings/usage/UsagePage.spec.tsx +3 -3
  141. package/src/app/shared/lazy-chunk-error-boundary.spec.tsx +2 -0
  142. package/src/app-shell/sidebar-overflow.spec.tsx +4 -1
  143. package/src/design-tweaks/visual-style-controls.spec.tsx +38 -0
  144. package/src/editor/useCollabReconcile.concurrent.spec.ts +2 -1
  145. package/src/icons/ResourceIconPicker.spec.tsx +270 -0
  146. package/src/icons/ResourceIconPicker.tsx +72 -4
@@ -52,11 +52,14 @@ afterEach(() => {
52
52
  vi.unstubAllEnvs();
53
53
  });
54
54
 
55
+ const catalog = createToolkitI18nCatalog({ messages: {} });
56
+
55
57
  describe("ThumbsFeedback localization", () => {
56
58
  it("renders localized accessible labels and explanation copy", async () => {
57
59
  act(() => {
58
60
  root.render(
59
61
  <AgentNativeI18nProvider
62
+ catalog={catalog}
60
63
  initialLocale="de-DE"
61
64
  initialPreference="de-DE"
62
65
  persistPreference={false}
@@ -87,6 +90,7 @@ describe("ThumbsFeedback localization", () => {
87
90
  act(() => {
88
91
  root.render(
89
92
  <AgentNativeI18nProvider
93
+ catalog={catalog}
90
94
  initialLocale="en-US"
91
95
  initialPreference="en-US"
92
96
  persistPreference={false}
@@ -181,6 +185,7 @@ describe("ThumbsFeedback localization", () => {
181
185
  act(() => {
182
186
  root.render(
183
187
  <AgentNativeI18nProvider
188
+ catalog={catalog}
184
189
  initialLocale="en-US"
185
190
  initialPreference="en-US"
186
191
  persistPreference={false}
@@ -295,6 +300,7 @@ describe("ThumbsFeedback localization", () => {
295
300
  act(() => {
296
301
  root.render(
297
302
  <AgentNativeI18nProvider
303
+ catalog={catalog}
298
304
  initialLocale="en-US"
299
305
  initialPreference="en-US"
300
306
  persistPreference={false}
@@ -421,6 +427,7 @@ describe("ThumbsFeedback localization", () => {
421
427
  act(() => {
422
428
  root.render(
423
429
  <AgentNativeI18nProvider
430
+ catalog={catalog}
424
431
  initialLocale="en-US"
425
432
  initialPreference="en-US"
426
433
  persistPreference={false}
@@ -511,6 +518,7 @@ describe("ThumbsFeedback localization", () => {
511
518
  act(() => {
512
519
  root.render(
513
520
  <AgentNativeI18nProvider
521
+ catalog={catalog}
514
522
  initialLocale="en-US"
515
523
  initialPreference="en-US"
516
524
  persistPreference={false}
@@ -558,6 +566,7 @@ describe("ThumbsFeedback localization", () => {
558
566
  act(() => {
559
567
  root.render(
560
568
  <AgentNativeI18nProvider
569
+ catalog={catalog}
561
570
  initialLocale="en-US"
562
571
  initialPreference="en-US"
563
572
  persistPreference={false}
@@ -596,6 +605,7 @@ describe("ThumbsFeedback localization", () => {
596
605
  act(() => {
597
606
  root.render(
598
607
  <AgentNativeI18nProvider
608
+ catalog={catalog}
599
609
  initialLocale="en-US"
600
610
  initialPreference="en-US"
601
611
  persistPreference={false}
@@ -639,6 +649,7 @@ describe("ThumbsFeedback localization", () => {
639
649
  act(() => {
640
650
  root.render(
641
651
  <AgentNativeI18nProvider
652
+ catalog={catalog}
642
653
  initialLocale="en-US"
643
654
  initialPreference="en-US"
644
655
  persistPreference={false}
@@ -674,6 +685,7 @@ describe("ThumbsFeedback localization", () => {
674
685
  act(() => {
675
686
  root.render(
676
687
  <AgentNativeI18nProvider
688
+ catalog={catalog}
677
689
  initialLocale="en-US"
678
690
  initialPreference="en-US"
679
691
  persistPreference={false}
@@ -709,6 +721,7 @@ describe("ThumbsFeedback localization", () => {
709
721
  act(() => {
710
722
  root.render(
711
723
  <AgentNativeI18nProvider
724
+ catalog={catalog}
712
725
  initialLocale="en-US"
713
726
  initialPreference="en-US"
714
727
  persistPreference={false}
@@ -746,6 +759,7 @@ describe("ThumbsFeedback localization", () => {
746
759
  act(() => {
747
760
  root.render(
748
761
  <AgentNativeI18nProvider
762
+ catalog={catalog}
749
763
  initialLocale="en-US"
750
764
  initialPreference="en-US"
751
765
  persistPreference={false}
@@ -797,6 +811,7 @@ describe("ThumbsFeedback localization", () => {
797
811
  act(() => {
798
812
  root.render(
799
813
  <AgentNativeI18nProvider
814
+ catalog={catalog}
800
815
  initialLocale="en-US"
801
816
  initialPreference="en-US"
802
817
  persistPreference={false}
@@ -45,6 +45,10 @@ vi.mock("react-router", async (importOriginal) => {
45
45
  };
46
46
  });
47
47
 
48
+ vi.mock("@agent-native/core/client/feature-flags/use-feature-flag", () => ({
49
+ useFeatureFlagState: () => ({ status: "ready", enabled: true }),
50
+ }));
51
+
48
52
  vi.mock("@agent-native/core/client/onboarding/use-onboarding", () => ({
49
53
  trackOnboardingEvent: mocks.trackOnboardingEvent,
50
54
  useOnboarding: mocks.useOnboarding,
@@ -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");