@agent-native/toolkit 0.225.2-nightly-20261106152255 → 0.226.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/agent-native.eject.json +1 -0
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +124 -9
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +1 -1
  6. package/dist/app/agentkit/react/core-file-storage.d.ts +21 -0
  7. package/dist/app/agentkit/react/core-file-storage.d.ts.map +1 -0
  8. package/dist/app/agentkit/react/core-file-storage.js +3 -0
  9. package/dist/app/agentkit/react/core-file-storage.js.map +1 -0
  10. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  11. package/dist/app/auth/AuthPage.js +89 -10
  12. package/dist/app/auth/AuthPage.js.map +1 -1
  13. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  14. package/dist/app/chat/AgentKitAssistantChat.js +39 -58
  15. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  16. package/dist/app/chat/MultiTabAssistantChat.d.ts.map +1 -1
  17. package/dist/app/chat/MultiTabAssistantChat.js +3 -1
  18. package/dist/app/chat/MultiTabAssistantChat.js.map +1 -1
  19. package/dist/app/chat/agentkit-chat/root.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/root.js +66 -2
  21. package/dist/app/chat/agentkit-chat/root.js.map +1 -1
  22. package/dist/app/chat/chat/surface-types.d.ts +1 -0
  23. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  24. package/dist/app/chat/chat/surface-types.js.map +1 -1
  25. package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
  26. package/dist/app/chat/composer/runtime-adapters.js +2 -0
  27. package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
  28. package/dist/app/chat/composer/use-agentkit-capabilities.d.ts +3 -1
  29. package/dist/app/chat/composer/use-agentkit-capabilities.d.ts.map +1 -1
  30. package/dist/app/chat/composer/use-agentkit-capabilities.js +11 -11
  31. package/dist/app/chat/composer/use-agentkit-capabilities.js.map +1 -1
  32. package/dist/app/chat/composer/use-workspace-composer-menu.d.ts +3 -0
  33. package/dist/app/chat/composer/use-workspace-composer-menu.d.ts.map +1 -0
  34. package/dist/app/chat/composer/use-workspace-composer-menu.js +80 -0
  35. package/dist/app/chat/composer/use-workspace-composer-menu.js.map +1 -0
  36. package/dist/app/chat/index.d.ts +1 -0
  37. package/dist/app/chat/index.d.ts.map +1 -1
  38. package/dist/app/chat/index.js +1 -0
  39. package/dist/app/chat/index.js.map +1 -1
  40. package/dist/app/chat/prepare-prompt-composer-agent-handoff.d.ts +5 -0
  41. package/dist/app/chat/prepare-prompt-composer-agent-handoff.d.ts.map +1 -0
  42. package/dist/app/chat/prepare-prompt-composer-agent-handoff.js +108 -0
  43. package/dist/app/chat/prepare-prompt-composer-agent-handoff.js.map +1 -0
  44. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  45. package/dist/app/i18n/catalogs/ar-SA.js +6 -1
  46. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  47. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  48. package/dist/app/i18n/catalogs/de-DE.js +6 -1
  49. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  50. package/dist/app/i18n/catalogs/en-US.d.ts +6 -1
  51. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/en-US.js +6 -1
  53. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  54. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/es-ES.js +6 -1
  56. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  57. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/fr-FR.js +6 -1
  59. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/hi-IN.js +6 -1
  62. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  63. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/ja-JP.js +6 -1
  65. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  66. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  67. package/dist/app/i18n/catalogs/ko-KR.js +6 -1
  68. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  69. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  70. package/dist/app/i18n/catalogs/pt-BR.js +6 -1
  71. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  72. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  73. package/dist/app/i18n/catalogs/zh-CN.js +6 -1
  74. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  75. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  76. package/dist/app/i18n/catalogs/zh-TW.js +6 -1
  77. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  78. package/dist/app/resources/McpIntegrationDialog.d.ts +3 -1
  79. package/dist/app/resources/McpIntegrationDialog.d.ts.map +1 -1
  80. package/dist/app/resources/McpIntegrationDialog.js +2 -2
  81. package/dist/app/resources/McpIntegrationDialog.js.map +1 -1
  82. package/dist/app/settings/useBuilderStatus.d.ts.map +1 -1
  83. package/dist/app/settings/useBuilderStatus.js +119 -40
  84. package/dist/app/settings/useBuilderStatus.js.map +1 -1
  85. package/dist/composer/ComposerContextMenu.d.ts +36 -2
  86. package/dist/composer/ComposerContextMenu.d.ts.map +1 -1
  87. package/dist/composer/ComposerContextMenu.js +108 -40
  88. package/dist/composer/ComposerContextMenu.js.map +1 -1
  89. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -1
  90. package/dist/composer/ComposerContextPickerDialog.js +63 -26
  91. package/dist/composer/ComposerContextPickerDialog.js.map +1 -1
  92. package/dist/composer/ComposerPlusMenu.d.ts +5 -3
  93. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  94. package/dist/composer/ComposerPlusMenu.js +166 -121
  95. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  96. package/dist/composer/MentionItemMedia.d.ts.map +1 -1
  97. package/dist/composer/MentionItemMedia.js +3 -1
  98. package/dist/composer/MentionItemMedia.js.map +1 -1
  99. package/dist/composer/PromptComposer.d.ts +1 -0
  100. package/dist/composer/PromptComposer.d.ts.map +1 -1
  101. package/dist/composer/PromptComposer.js +1 -0
  102. package/dist/composer/PromptComposer.js.map +1 -1
  103. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  104. package/dist/composer/TiptapComposer.js +117 -50
  105. package/dist/composer/TiptapComposer.js.map +1 -1
  106. package/dist/composer/index.d.ts +1 -0
  107. package/dist/composer/index.d.ts.map +1 -1
  108. package/dist/composer/runtime-adapters.d.ts +15 -0
  109. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  110. package/dist/composer/runtime-adapters.js.map +1 -1
  111. package/dist/composer/use-composer-panel-placement.d.ts.map +1 -1
  112. package/dist/composer/use-composer-panel-placement.js +2 -4
  113. package/dist/composer/use-composer-panel-placement.js.map +1 -1
  114. package/dist/composer/use-mention-search.d.ts +7 -0
  115. package/dist/composer/use-mention-search.d.ts.map +1 -1
  116. package/dist/composer/use-mention-search.js +86 -19
  117. package/dist/composer/use-mention-search.js.map +1 -1
  118. package/dist/composer/useComposerContextPicker.d.ts +5 -1
  119. package/dist/composer/useComposerContextPicker.d.ts.map +1 -1
  120. package/dist/composer/useComposerContextPicker.js +18 -4
  121. package/dist/composer/useComposerContextPicker.js.map +1 -1
  122. package/package.json +4 -4
  123. package/src/app/agentkit/react/components.tsx +158 -11
  124. package/src/app/agentkit/react/composer-context.spec.tsx +180 -0
  125. package/src/app/agentkit/react/core-file-storage.tsx +14 -0
  126. package/src/app/auth/AuthPage.interaction.spec.tsx +143 -0
  127. package/src/app/auth/AuthPage.tsx +159 -13
  128. package/src/app/chat/AgentKitAssistantChat.spec.tsx +65 -3
  129. package/src/app/chat/AgentKitAssistantChat.tsx +57 -65
  130. package/src/app/chat/MultiTabAssistantChat.spec.tsx +21 -0
  131. package/src/app/chat/MultiTabAssistantChat.tsx +4 -0
  132. package/src/app/chat/agentkit-chat/root.tsx +91 -8
  133. package/src/app/chat/chat/surface-types.ts +1 -0
  134. package/src/app/chat/composer/runtime-adapters.tsx +2 -0
  135. package/src/app/chat/composer/use-agentkit-capabilities.hardening.spec.tsx +26 -1
  136. package/src/app/chat/composer/use-agentkit-capabilities.ts +11 -11
  137. package/src/app/chat/composer/use-agentkit-integration-menu.spec.tsx +2 -4
  138. package/src/app/chat/composer/use-workspace-composer-menu.spec.tsx +174 -0
  139. package/src/app/chat/composer/use-workspace-composer-menu.tsx +122 -0
  140. package/src/app/chat/index.ts +1 -0
  141. package/src/app/chat/prepare-prompt-composer-agent-handoff.spec.ts +162 -0
  142. package/src/app/chat/prepare-prompt-composer-agent-handoff.ts +134 -0
  143. package/src/app/i18n/catalogs/ar-SA.ts +6 -1
  144. package/src/app/i18n/catalogs/de-DE.ts +6 -1
  145. package/src/app/i18n/catalogs/en-US.ts +6 -1
  146. package/src/app/i18n/catalogs/es-ES.ts +6 -1
  147. package/src/app/i18n/catalogs/fr-FR.ts +6 -1
  148. package/src/app/i18n/catalogs/hi-IN.ts +6 -1
  149. package/src/app/i18n/catalogs/ja-JP.ts +6 -1
  150. package/src/app/i18n/catalogs/ko-KR.ts +6 -1
  151. package/src/app/i18n/catalogs/pt-BR.ts +6 -1
  152. package/src/app/i18n/catalogs/zh-CN.ts +6 -1
  153. package/src/app/i18n/catalogs/zh-TW.ts +6 -1
  154. package/src/app/resources/McpIntegrationDialog.tsx +5 -0
  155. package/src/app/settings/useBuilderStatus.spec.tsx +229 -13
  156. package/src/app/settings/useBuilderStatus.ts +145 -47
  157. package/src/composer/ComposerContextMenu.spec.tsx +95 -32
  158. package/src/composer/ComposerContextMenu.tsx +284 -73
  159. package/src/composer/ComposerContextPicker.spec.tsx +13 -14
  160. package/src/composer/ComposerContextPickerDialog.spec.tsx +40 -3
  161. package/src/composer/ComposerContextPickerDialog.tsx +158 -24
  162. package/src/composer/ComposerPlusMenu.parity.spec.tsx +442 -62
  163. package/src/composer/ComposerPlusMenu.tsx +322 -237
  164. package/src/composer/MentionItemMedia.tsx +3 -0
  165. package/src/composer/PromptComposer.context.spec.tsx +253 -82
  166. package/src/composer/PromptComposer.tsx +2 -0
  167. package/src/composer/TiptapComposer.spec.ts +6 -8
  168. package/src/composer/TiptapComposer.tsx +148 -55
  169. package/src/composer/index.ts +5 -0
  170. package/src/composer/runtime-adapters.tsx +18 -0
  171. package/src/composer/use-composer-panel-placement.ts +2 -4
  172. package/src/composer/use-mention-search.spec.tsx +99 -1
  173. package/src/composer/use-mention-search.ts +101 -24
  174. package/src/composer/useComposerContextPicker.ts +25 -5
@@ -124,12 +124,14 @@ import {
124
124
  snapshotComposerValue,
125
125
  type AgentKitComposerSubmission,
126
126
  } from "./composer-submission.js";
127
+ import { CoreFileStorageSetupContext } from "./core-file-storage.js";
127
128
  export type { AgentKitComposerSubmission } from "./composer-submission.js";
128
129
 
129
130
  import { splitAgentKitMessageContext } from "@agent-native/agentkit";
130
131
  import type { AgentThreadState } from "@agent-native/agentkit/client";
131
132
  import {
132
133
  AgentKitCapabilityError,
134
+ AgentKitUploadError,
133
135
  hasActiveAgentRuns,
134
136
  isCurrentAgentSuggestion,
135
137
  selectAgentSuggestions,
@@ -6506,6 +6508,100 @@ export function AgentKitComposer({
6506
6508
  () => attachmentAdapter ?? createChatAttachmentAdapter(composerTranslate),
6507
6509
  [attachmentAdapter, composerTranslate],
6508
6510
  );
6511
+ const formatUploadError = (error: unknown): string => {
6512
+ const formatFailure = (failure: unknown): string => {
6513
+ const code =
6514
+ failure && typeof failure === "object" && "code" in failure
6515
+ ? failure.code
6516
+ : undefined;
6517
+ const key =
6518
+ code === "upload_too_large"
6519
+ ? "agentChat.composer.fileTooLarge"
6520
+ : code === "upload_session_expired"
6521
+ ? "agentChat.composer.sessionExpired"
6522
+ : code === "upload_unsupported_type"
6523
+ ? "agentChat.composer.unsupportedFileType"
6524
+ : code === "upload_network_error"
6525
+ ? "agentChat.composer.uploadOffline"
6526
+ : code === "upload_unavailable"
6527
+ ? "agentChat.composer.uploadUnavailable"
6528
+ : code === "upload_invalid_response"
6529
+ ? "agentChat.composer.uploadFailed"
6530
+ : undefined;
6531
+ return key
6532
+ ? composerTranslate(key, { size: 25 })
6533
+ : formatAttachmentError(failure, labels.dropFileFailed);
6534
+ };
6535
+ return error instanceof AgentKitUploadError
6536
+ ? error.failures
6537
+ .map((failure) => `${failure.name}: ${formatFailure(failure.error)}`)
6538
+ .join("\n")
6539
+ : formatFailure(error);
6540
+ };
6541
+ const coreFileStorageSetup = useContext(CoreFileStorageSetupContext);
6542
+ const storageAnchorRef = useRef<HTMLDivElement>(null);
6543
+ const [storageState, setStorageState] = useState<
6544
+ "unknown" | "pending" | "ready" | "missing" | "unavailable"
6545
+ >("unknown");
6546
+ const [storageOpen, setStorageOpen] = useState(false);
6547
+ const storageOpenRef = useRef(false);
6548
+ const changeStorageOpen = useCallback((open: boolean) => {
6549
+ storageOpenRef.current = open;
6550
+ setStorageOpen(open);
6551
+ }, []);
6552
+ const storageScope = useMemo(
6553
+ () => ({ current: true }),
6554
+ [controller, threadId, canUpload, coreFileStorageSetup?.scopeKey],
6555
+ );
6556
+ useEffect(() => {
6557
+ storageScope.current = true;
6558
+ return () => {
6559
+ storageScope.current = false;
6560
+ };
6561
+ }, [storageScope, changeStorageOpen]);
6562
+ useEffect(() => {
6563
+ changeStorageOpen(false);
6564
+ setStorageState("unknown");
6565
+ }, [storageScope, changeStorageOpen]);
6566
+ const storageReadRef = useRef<{
6567
+ scope: typeof storageScope;
6568
+ pending: Promise<{ configured: boolean }>;
6569
+ } | null>(null);
6570
+ const requestCoreAttachment = useCallback(async () => {
6571
+ const isCurrentScope = () => storageScope.current;
6572
+ if (!canUpload || !isCurrentScope() || !coreFileStorageSetup)
6573
+ return "stale" as const;
6574
+ setStorageState("pending");
6575
+ changeStorageOpen(false);
6576
+ const previous = storageReadRef.current;
6577
+ const sharedRead =
6578
+ previous?.scope === storageScope ? previous.pending : null;
6579
+ const pending = sharedRead ?? coreFileStorageSetup.readStatus();
6580
+ storageReadRef.current = { scope: storageScope, pending };
6581
+ try {
6582
+ const status = await pending;
6583
+ if (!isCurrentScope()) return "stale" as const;
6584
+ setStorageState(status.configured ? "ready" : "missing");
6585
+ changeStorageOpen(!status.configured);
6586
+ return status.configured ? ("ready" as const) : ("missing" as const);
6587
+ } catch (error) {
6588
+ if (!isCurrentScope()) return "stale" as const;
6589
+ setStorageState("unavailable");
6590
+ changeStorageOpen(true);
6591
+ onAttachmentError?.(formatAttachmentError(error, labels.dropFileFailed));
6592
+ return "unavailable" as const;
6593
+ } finally {
6594
+ const currentRead = () => storageReadRef.current;
6595
+ if (currentRead()?.pending === pending) storageReadRef.current = null;
6596
+ }
6597
+ }, [
6598
+ canUpload,
6599
+ coreFileStorageSetup,
6600
+ storageScope,
6601
+ onAttachmentError,
6602
+ labels.dropFileFailed,
6603
+ changeStorageOpen,
6604
+ ]);
6509
6605
  const starting = useAgentKitSubmissionStarting(threadId);
6510
6606
  const active = hasActiveRuns(thread) || starting;
6511
6607
  const composerInitialText = editingMessage
@@ -6538,10 +6634,12 @@ export function AgentKitComposer({
6538
6634
  return true;
6539
6635
  };
6540
6636
  const focusComposer = useCallback(() => {
6541
- if (!autoFocus) return;
6637
+ if (!autoFocus || storageOpenRef.current) return;
6542
6638
  composerRef.current?.focus();
6543
6639
  if (typeof globalThis.requestAnimationFrame === "function") {
6544
- globalThis.requestAnimationFrame(() => composerRef.current?.focus());
6640
+ globalThis.requestAnimationFrame(() => {
6641
+ if (!storageOpenRef.current) composerRef.current?.focus();
6642
+ });
6545
6643
  }
6546
6644
  }, [autoFocus]);
6547
6645
  const reportAttachmentError = useCallback(
@@ -6650,6 +6748,25 @@ export function AgentKitComposer({
6650
6748
  const onLocalSubmit = () => {
6651
6749
  options.onLocalSubmit?.();
6652
6750
  };
6751
+ if (files.length && !uploadGateRef.current.enabled) {
6752
+ throw new AgentKitCapabilityError(
6753
+ "uploads",
6754
+ "unavailable",
6755
+ uploadGateRef.current.reason ?? labels.error,
6756
+ );
6757
+ }
6758
+ if (files.length && coreFileStorageSetup) {
6759
+ const status = await requestCoreAttachment();
6760
+ if (status !== "ready") {
6761
+ throw new Error(
6762
+ composerTranslate(
6763
+ status === "missing"
6764
+ ? "onboarding.fileStorage.title"
6765
+ : "onboarding.fileStorage.statusUnavailable",
6766
+ ),
6767
+ );
6768
+ }
6769
+ }
6653
6770
  if (!editingMessage && onSubmitOverride) {
6654
6771
  const readinessEngine = options.engine ?? selectedEngine;
6655
6772
  await controller.assertAiSetupReady({
@@ -6663,13 +6780,6 @@ export function AgentKitComposer({
6663
6780
  await onSubmitOverride(text, files, references, submitOptions);
6664
6781
  return;
6665
6782
  }
6666
- if (files.length && !uploadGateRef.current.enabled) {
6667
- throw new AgentKitCapabilityError(
6668
- "uploads",
6669
- "unavailable",
6670
- uploadGateRef.current.reason ?? labels.error,
6671
- );
6672
- }
6673
6783
  const effort = options.effort;
6674
6784
  const executionMode = options.requestMode ?? "act";
6675
6785
  const contextItemsForSubmission = options.contextItems ?? contextItems;
@@ -6911,7 +7021,40 @@ export function AgentKitComposer({
6911
7021
  const suggestions = selectAgentSuggestions(thread);
6912
7022
  return (
6913
7023
  <ComposerNoticeScope>
6914
- <div className={`agentkit-composer-stack ${className ?? ""}`}>
7024
+ <div
7025
+ ref={storageAnchorRef}
7026
+ className={`agentkit-composer-stack ${className ?? ""}`}
7027
+ >
7028
+ {coreFileStorageSetup && canUpload && (
7029
+ <>
7030
+ <coreFileStorageSetup.Setup
7031
+ open={storageOpen}
7032
+ anchorRef={storageAnchorRef}
7033
+ onOpenChange={(open) => {
7034
+ changeStorageOpen(open);
7035
+ if (!open) composerRef.current?.focus();
7036
+ }}
7037
+ onConnected={() => {
7038
+ void requestCoreAttachment();
7039
+ }}
7040
+ {...(storageState === "unavailable"
7041
+ ? {
7042
+ status: "unavailable" as const,
7043
+ onRetry: () => {
7044
+ void requestCoreAttachment();
7045
+ },
7046
+ }
7047
+ : { status: "missing" as const })}
7048
+ />
7049
+ {storageState === "pending" && (
7050
+ <span role="status" className="text-sm text-foreground">
7051
+ {composerTranslate("agentChat.common.loading", {
7052
+ defaultValue: "Loading...",
7053
+ })}
7054
+ </span>
7055
+ )}
7056
+ </>
7057
+ )}
6915
7058
  {announcePendingSubmission && submissionPending ? (
6916
7059
  <span
6917
7060
  className="agentkit-visually-hidden"
@@ -7134,6 +7277,10 @@ export function AgentKitComposer({
7134
7277
  stopButton={stopButton === undefined ? defaultStopButton : stopButton}
7135
7278
  noticeOutlet
7136
7279
  attachmentsEnabled={canUpload}
7280
+ onAttachmentRequest={onAttachmentRequest}
7281
+ contextButtonTooltipDisabled={
7282
+ contextButtonTooltipDisabled || storageOpen
7283
+ }
7137
7284
  includeDefaultSlashCommands={includeDefaultSlashCommands ?? false}
7138
7285
  includeDefaultSlashSkills={includeDefaultSlashSkills ?? false}
7139
7286
  plusMenuMode={plusMenuMode ?? "full"}
@@ -7170,7 +7317,7 @@ export function AgentKitComposer({
7170
7317
  dismissLabel={labels.dismissNotice}
7171
7318
  >
7172
7319
  <span {...SESSION_REPLAY_MASK_PROPS}>
7173
- {sendOrCommandError.message}
7320
+ {formatUploadError(sendOrCommandError)}
7174
7321
  </span>
7175
7322
  </ComposerNotice>
7176
7323
  ) : null}
@@ -18,6 +18,8 @@ import type {
18
18
  AgentChatContextItem,
19
19
  ComposerContextMenuItem,
20
20
  } from "../../../agentkit.js";
21
+ import { ComposerRuntimeAdaptersProvider } from "../../../composer/runtime-adapters.js";
22
+ import { FileStorageSetupPopover } from "../../chat/FileStorageSetupPopover.js";
21
23
  import {
22
24
  AgentKitChat,
23
25
  AgentKitComposer,
@@ -25,9 +27,14 @@ import {
25
27
  } from "./components.js";
26
28
  import { createAgentKitComposerSubmission } from "./composer-submission.js";
27
29
  import { AgentKitProvider } from "./context.js";
30
+ import { CoreFileStorageSetupContext } from "./core-file-storage.js";
28
31
 
29
32
  const capture = vi.hoisted(() => ({
30
33
  props: undefined as PromptComposerProps | undefined,
34
+ storageStatus: vi.fn(),
35
+ storage: undefined as
36
+ | { open: boolean; status?: string; onRetry?: () => void }
37
+ | undefined,
31
38
  }));
32
39
  vi.mock("../../../agentkit.js", async (importOriginal) => {
33
40
  const actual = await importOriginal<typeof import("../../../agentkit.js")>();
@@ -40,6 +47,20 @@ vi.mock("../../../agentkit.js", async (importOriginal) => {
40
47
  };
41
48
  });
42
49
 
50
+ vi.mock("@agent-native/core/client/uploads", () => ({
51
+ readFileUploadStatus: capture.storageStatus,
52
+ }));
53
+ vi.mock("../../chat/FileStorageSetupPopover.js", () => ({
54
+ FileStorageSetupPopover: (props: {
55
+ open: boolean;
56
+ status?: string;
57
+ onRetry?: () => void;
58
+ }) => {
59
+ capture.storage = props;
60
+ return null;
61
+ },
62
+ }));
63
+
43
64
  let root: Root;
44
65
  let container: HTMLDivElement;
45
66
  let client: AgentKitClient;
@@ -50,6 +71,8 @@ beforeEach(() => {
50
71
  document.body.append(container);
51
72
  root = createRoot(container);
52
73
  capture.props = undefined;
74
+ capture.storage = undefined;
75
+ capture.storageStatus.mockReset();
53
76
  });
54
77
 
55
78
  afterEach(async () => {
@@ -98,6 +121,163 @@ function withActiveRun(runtime: ReturnType<typeof transport>) {
98
121
  }
99
122
 
100
123
  describe("AgentKit composer context submission", () => {
124
+ it.each(["missing", "unavailable"] as const)(
125
+ "blocks raw-file custom submissions with visible %s storage state",
126
+ async (state) => {
127
+ client = new AgentKitClient({
128
+ transport: { ...transport(), capabilities: { uploads: true } },
129
+ });
130
+ if (state === "missing")
131
+ capture.storageStatus.mockResolvedValue({ configured: false });
132
+ else capture.storageStatus.mockRejectedValue(new Error("Status denied"));
133
+ const submit = vi.fn();
134
+ const upload = vi.spyOn(client, "uploadFiles");
135
+ await act(async () =>
136
+ root.render(
137
+ <AgentKitProvider controller={client} threadId="thread-1">
138
+ <CoreFileStorageSetupContext.Provider
139
+ value={{
140
+ scopeKey: "user-1:org-1",
141
+ readStatus: capture.storageStatus,
142
+ Setup: FileStorageSetupPopover,
143
+ }}
144
+ >
145
+ <AgentKitComposer onSubmit={submit} />
146
+ </CoreFileStorageSetupContext.Provider>
147
+ </AgentKitProvider>,
148
+ ),
149
+ );
150
+ await act(async () => {
151
+ await expect(
152
+ capture.props?.onSubmit(
153
+ "Preserve draft",
154
+ [new File(["bytes"], "note.txt")],
155
+ [],
156
+ {},
157
+ ),
158
+ ).rejects.toThrow();
159
+ });
160
+ expect(submit).not.toHaveBeenCalled();
161
+ expect(upload).not.toHaveBeenCalled();
162
+ expect(capture.storage?.open).toBe(true);
163
+ expect(capture.storage?.status).toBe(state);
164
+ },
165
+ );
166
+ it("localizes create-upload failures while retaining the original draft and files", async () => {
167
+ const failure = Object.assign(new Error("upload_too_large"), {
168
+ code: "upload_too_large",
169
+ status: 413,
170
+ });
171
+ const runtime = {
172
+ ...transport(),
173
+ capabilities: { uploads: true },
174
+ createUpload: vi.fn().mockRejectedValue(failure),
175
+ completeUpload: vi.fn(),
176
+ } satisfies AgentTransport;
177
+ const upload = vi.fn();
178
+ client = new AgentKitClient({ transport: runtime, upload });
179
+ const onLocalSubmit = vi.fn();
180
+ await act(async () =>
181
+ root.render(
182
+ <ComposerRuntimeAdaptersProvider
183
+ adapters={{ translate: (key) => `translated:${key}` }}
184
+ >
185
+ <AgentKitProvider controller={client} threadId="thread-1">
186
+ <AgentKitComposer initialText="Original draft" />
187
+ </AgentKitProvider>
188
+ </ComposerRuntimeAdaptersProvider>,
189
+ ),
190
+ );
191
+ const files = [
192
+ new File(["Original bytes"], "note.txt", { type: "text/plain" }),
193
+ ];
194
+ await act(async () => {
195
+ await expect(
196
+ capture.props?.onSubmit("Original draft", files, [], { onLocalSubmit }),
197
+ ).rejects.toThrow();
198
+ });
199
+ expect(runtime.createUpload).toHaveBeenCalledOnce();
200
+ expect(upload).not.toHaveBeenCalled();
201
+ expect(runtime.startRun).not.toHaveBeenCalled();
202
+ expect(onLocalSubmit).not.toHaveBeenCalled();
203
+ expect(files[0].name).toBe("note.txt");
204
+ expect(container.textContent).toContain(
205
+ "translated:agentChat.composer.fileTooLarge",
206
+ );
207
+ expect(container.textContent).not.toContain("upload_too_large");
208
+ });
209
+ it("ignores a delayed storage response after the identity scope changes", async () => {
210
+ client = new AgentKitClient({
211
+ transport: { ...transport(), capabilities: { uploads: true } },
212
+ });
213
+ let resolveStatus: ((value: { configured: boolean }) => void) | undefined;
214
+ capture.storageStatus.mockImplementation(
215
+ () =>
216
+ new Promise((resolve) => {
217
+ resolveStatus = resolve;
218
+ }),
219
+ );
220
+ const render = (scopeKey: string) =>
221
+ root.render(
222
+ <AgentKitProvider controller={client} threadId="thread-1">
223
+ <CoreFileStorageSetupContext.Provider
224
+ value={{
225
+ scopeKey,
226
+ readStatus: capture.storageStatus,
227
+ Setup: FileStorageSetupPopover,
228
+ }}
229
+ >
230
+ <AgentKitComposer />
231
+ </CoreFileStorageSetupContext.Provider>
232
+ </AgentKitProvider>,
233
+ );
234
+ await act(async () => render("user-1:org-1"));
235
+ let submission: Promise<unknown> | undefined;
236
+ await act(async () => {
237
+ submission = capture.props?.onSubmit(
238
+ "draft",
239
+ [new File(["bytes"], "note.txt")],
240
+ [],
241
+ {},
242
+ );
243
+ });
244
+ await act(async () => render("user-2:org-2"));
245
+ await act(async () => {
246
+ resolveStatus?.({ configured: false });
247
+ await submission?.catch(() => undefined);
248
+ });
249
+ expect(capture.storage?.open).toBe(false);
250
+ expect(capture.props?.attachmentsEnabled).toBe(true);
251
+ });
252
+ it("preserves supported external upload adapters without querying Core storage", async () => {
253
+ client = new AgentKitClient({
254
+ transport: { ...transport(), capabilities: { uploads: true } },
255
+ });
256
+ await act(async () =>
257
+ root.render(
258
+ <AgentKitProvider controller={client} threadId="thread-1">
259
+ <AgentKitComposer />
260
+ </AgentKitProvider>,
261
+ ),
262
+ );
263
+ expect(capture.props?.attachmentsEnabled).toBe(true);
264
+ expect(capture.props?.onAttachmentRequest).toBeUndefined();
265
+ expect(capture.storageStatus).not.toHaveBeenCalled();
266
+ });
267
+ it("keeps unsupported generic transports outside the Core storage setup flow", async () => {
268
+ client = new AgentKitClient({ transport: transport() });
269
+ await act(async () =>
270
+ root.render(
271
+ <AgentKitProvider controller={client} threadId="thread-1">
272
+ <AgentKitComposer />
273
+ </AgentKitProvider>,
274
+ ),
275
+ );
276
+ expect(capture.props?.attachmentsEnabled).toBe(false);
277
+ expect(capture.props?.onAttachmentRequest).toBeUndefined();
278
+ expect(capture.storageStatus).not.toHaveBeenCalled();
279
+ });
280
+
101
281
  it.each([
102
282
  ["immediate", "capability"],
103
283
  ["queued", "capability"],
@@ -0,0 +1,14 @@
1
+ import { createContext, type ComponentType, type RefObject } from "react";
2
+
3
+ type SetupProps = {
4
+ open: boolean;
5
+ anchorRef: RefObject<HTMLElement | null>;
6
+ onOpenChange: (open: boolean) => void;
7
+ onConnected: () => void;
8
+ } & ({ status: "missing" } | { status: "unavailable"; onRetry: () => void });
9
+
10
+ export const CoreFileStorageSetupContext = createContext<{
11
+ scopeKey: string;
12
+ readStatus: () => Promise<{ configured: boolean }>;
13
+ Setup: ComponentType<SetupProps>;
14
+ } | null>(null);
@@ -43,6 +43,149 @@ describe("AuthPage interactions", () => {
43
43
  vi.unstubAllGlobals();
44
44
  });
45
45
 
46
+ it.each(["control", "automatic"] as const)(
47
+ "shows Builder legal disclosure before Google-only signup only for the %s assignment",
48
+ async (variant) => {
49
+ vi.stubGlobal(
50
+ "fetch",
51
+ vi.fn(async (input: RequestInfo | URL) => ({
52
+ ok: true,
53
+ status: 200,
54
+ json: async () =>
55
+ String(input).includes("builder-signup-experiment")
56
+ ? {
57
+ status: "assigned",
58
+ id: "a".repeat(32),
59
+ variant,
60
+ policy: "builder-signup-2026-11-06",
61
+ }
62
+ : { available: false },
63
+ })),
64
+ );
65
+ await act(async () => {
66
+ root.render(
67
+ <AuthPage
68
+ {...propsFromHtml(
69
+ getOnboardingHtml({
70
+ requestHost: "127.0.0.1",
71
+ requestPath: "/signup",
72
+ }),
73
+ )}
74
+ trackingApp="design"
75
+ googleOnly
76
+ showGoogle
77
+ identitySsoEnabled={false}
78
+ />,
79
+ );
80
+ });
81
+ expect(
82
+ container.textContent?.includes(
83
+ "Your Design signup also creates a Builder.io account",
84
+ ),
85
+ ).toBe(variant === "automatic");
86
+ expect(
87
+ container.querySelector<HTMLButtonElement>("#google-btn")?.disabled,
88
+ ).toBe(false);
89
+ if (variant === "automatic") {
90
+ expect(
91
+ container.querySelector(
92
+ 'a[href="https://www.builder.io/legal/terms"]',
93
+ ),
94
+ ).not.toBeNull();
95
+ expect(
96
+ container.querySelector(
97
+ 'a[href="https://www.builder.io/legal/privacy"]',
98
+ ),
99
+ ).not.toBeNull();
100
+ }
101
+ },
102
+ );
103
+
104
+ it.each([200, 409])(
105
+ "opens Google immediately and preserves the existing flow when acceptance returns %s",
106
+ async (acceptStatus) => {
107
+ window.history.replaceState({}, "", "/signup");
108
+ const popup = {
109
+ closed: false,
110
+ location: { href: "" },
111
+ } as unknown as Window;
112
+ const open = vi.fn(() => popup);
113
+ vi.stubGlobal("open", open);
114
+ let accept: () => void = () => {
115
+ throw new Error("Acceptance has not started");
116
+ };
117
+ const oauthRequests = vi.fn();
118
+ let oauthUrl: string | undefined;
119
+ vi.stubGlobal(
120
+ "fetch",
121
+ vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
122
+ const url = String(input);
123
+ if (
124
+ url.includes("builder-signup-experiment") &&
125
+ init?.method === "POST"
126
+ ) {
127
+ return new Promise<Response>((resolve) => {
128
+ accept = () =>
129
+ resolve(
130
+ new Response(JSON.stringify({ status: "accepted" }), {
131
+ status: acceptStatus,
132
+ }),
133
+ );
134
+ });
135
+ }
136
+ if (url.includes("google/auth-url")) {
137
+ oauthUrl = url;
138
+ oauthRequests();
139
+ return new Promise<Response>(() => {});
140
+ }
141
+ return Promise.resolve(
142
+ new Response(
143
+ JSON.stringify(
144
+ url.includes("builder-signup-experiment")
145
+ ? {
146
+ status: "assigned",
147
+ id: "a".repeat(32),
148
+ variant: "automatic",
149
+ policy: "builder-signup-2026-11-06",
150
+ }
151
+ : { available: false },
152
+ ),
153
+ ),
154
+ );
155
+ }),
156
+ );
157
+ await act(async () => {
158
+ root.render(
159
+ <AuthPage
160
+ {...propsFromHtml(
161
+ getOnboardingHtml({
162
+ requestHost: "127.0.0.1",
163
+ requestPath: "/signup",
164
+ }),
165
+ )}
166
+ trackingApp="design"
167
+ identitySsoEnabled={false}
168
+ googleViaIdentitySso={false}
169
+ googleAuthMode="popup"
170
+ showGoogle
171
+ />,
172
+ );
173
+ });
174
+ const button = container.querySelector<HTMLButtonElement>("#google-btn");
175
+ await act(async () => button?.click());
176
+ expect(open).toHaveBeenCalledTimes(1);
177
+ expect(button?.getAttribute("aria-busy")).toBe("true");
178
+ expect(oauthRequests).not.toHaveBeenCalled();
179
+ await act(async () => accept());
180
+ expect(oauthRequests).toHaveBeenCalledTimes(1);
181
+ expect(
182
+ new URL(oauthUrl ?? "", "https://design.example.com").searchParams.has(
183
+ "builder_signup_attempt",
184
+ ),
185
+ ).toBe(acceptStatus === 200);
186
+ },
187
+ );
188
+
46
189
  it("opens and closes the auth options without dropping focus from the localized toggle", async () => {
47
190
  await act(async () => {
48
191
  root.render(