@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
@@ -856,6 +856,10 @@ export function useBuilderConnectFlow(
856
856
  const [builderEnabled, setBuilderEnabled] = useState(false);
857
857
  const [orgName, setOrgName] = useState<string | null>(null);
858
858
  const [connecting, setConnecting] = useState(false);
859
+ const [reconciling, setReconciling] = useState<string | null>(null);
860
+ const pollEpochRef = useRef(0);
861
+ const [pollEpoch, setPollEpoch] = useState(0);
862
+ const pendingPollRef = useRef<number | null>(null);
859
863
  const errorRevisionRef = useRef(0);
860
864
  const [errorState, setErrorState] = useState<{
861
865
  kind: BuilderConnectErrorKind;
@@ -888,6 +892,11 @@ export function useBuilderConnectFlow(
888
892
  const provisionAccountAttemptRef = useRef(false);
889
893
  const connectAttemptIdRef = useRef<string | null>(null);
890
894
  const cancelledConnectAttemptIdRef = useRef<string | null>(null);
895
+ const blockedLaunchRef = useRef<{
896
+ from: string | null;
897
+ to: string;
898
+ revision: number;
899
+ } | null>(null);
891
900
  const activePopupRef = useRef<Window | null>(null);
892
901
  const confirmConnectSuccessRef = useRef<(attemptId: string) => Promise<void>>(
893
902
  async () => {},
@@ -927,13 +936,14 @@ export function useBuilderConnectFlow(
927
936
  source: trackingSource,
928
937
  flow: trackingFlow,
929
938
  });
939
+ // A new generation must not wait for a transport that ignores abort.
930
940
  const getConnectPollInterval = useCallback(
931
941
  () =>
932
942
  Math.min(
933
943
  MAX_POLL_INTERVAL_MS,
934
944
  POLL_INTERVAL_MS * 2 ** statusPollFailuresRef.current,
935
945
  ),
936
- [],
946
+ [pollEpoch],
937
947
  );
938
948
 
939
949
  useEffect(() => {
@@ -1062,6 +1072,7 @@ export function useBuilderConnectFlow(
1062
1072
  setBuilderEnabled(false);
1063
1073
  setOrgName(null);
1064
1074
  setConnecting(false);
1075
+ setReconciling(null);
1065
1076
  setError(null);
1066
1077
  markStatusAvailable();
1067
1078
  setAccountExists(false);
@@ -1079,15 +1090,31 @@ export function useBuilderConnectFlow(
1079
1090
  let refreshGeneration = 0;
1080
1091
  const refresh = async () => {
1081
1092
  const errorRevisionAtStart = errorRevisionRef.current;
1093
+ const attemptIdAtStart = connectAttemptIdRef.current;
1082
1094
  const generation = ++refreshGeneration;
1083
1095
  const isCurrentRefresh = () => generation === refreshGeneration;
1084
1096
  const s = await fetchStatus();
1085
- if (cancelled || !mountedRef.current || !isCurrentRefresh()) return;
1086
- if (errorRevisionRef.current !== errorRevisionAtStart) return;
1097
+ const blocked = blockedLaunchRef.current;
1098
+ const sameBlockedLaunch =
1099
+ blocked?.from === attemptIdAtStart &&
1100
+ blocked.to === connectAttemptIdRef.current &&
1101
+ blocked.revision === errorRevisionRef.current;
1102
+ if (
1103
+ cancelled ||
1104
+ !mountedRef.current ||
1105
+ !isCurrentRefresh() ||
1106
+ (!sameBlockedLaunch &&
1107
+ (connectAttemptIdRef.current !== attemptIdAtStart ||
1108
+ errorRevisionRef.current !== errorRevisionAtStart))
1109
+ )
1110
+ return;
1087
1111
  setHasFetchedStatus(true);
1088
1112
  setStatusReadSettledCount((count) => count + 1);
1089
1113
  if (!s) {
1090
- markStatusUnavailable();
1114
+ if (sameBlockedLaunch) {
1115
+ statusUnavailableRef.current = true;
1116
+ setStatusUnavailableRevision(errorRevisionAtStart);
1117
+ } else markStatusUnavailable();
1091
1118
  return;
1092
1119
  }
1093
1120
  if (statusUnavailableRef.current) {
@@ -1102,7 +1129,11 @@ export function useBuilderConnectFlow(
1102
1129
  setCanDisconnect(!!s.canDisconnect);
1103
1130
  setAgentNativeProvisioningEnabled(!!s.agentNativeProvisioningEnabled);
1104
1131
  setAgentNativeProvisioningToken(s.agentNativeProvisioningToken ?? null);
1105
- setAccountExists(s.connectError?.code === "account_exists");
1132
+ if (
1133
+ !connectAttemptIdRef.current ||
1134
+ cancelledConnectAttemptIdRef.current !== connectAttemptIdRef.current
1135
+ )
1136
+ setAccountExists(s.connectError?.code === "account_exists");
1106
1137
  setBuilderEnabled(!!s.builderEnabled);
1107
1138
  const nextConnectUrl = s.connectUrl ?? null;
1108
1139
  setStatusConnectUrl(nextConnectUrl);
@@ -1116,6 +1147,7 @@ export function useBuilderConnectFlow(
1116
1147
  if (connectComplete) {
1117
1148
  connectStartedAtRef.current = null;
1118
1149
  setConnecting(false);
1150
+ setReconciling(null);
1119
1151
  }
1120
1152
  if (connectComplete && !notifiedConnectedRef.current) {
1121
1153
  notifiedConnectedRef.current = true;
@@ -1131,7 +1163,12 @@ export function useBuilderConnectFlow(
1131
1163
  }
1132
1164
  if (errorRevisionRef.current === errorRevisionAtStart) {
1133
1165
  const activeConnectStartedAt = connectStartedAtRef.current;
1134
- if (isCurrentConnectError(s.connectError, activeConnectStartedAt)) {
1166
+ if (
1167
+ (!connectAttemptIdRef.current ||
1168
+ cancelledConnectAttemptIdRef.current !==
1169
+ connectAttemptIdRef.current) &&
1170
+ isCurrentConnectError(s.connectError, activeConnectStartedAt)
1171
+ ) {
1135
1172
  setError(s.connectError.message);
1136
1173
  } else if (!activeConnectStartedAt && s.authError?.message) {
1137
1174
  setError(s.authError.message);
@@ -1180,11 +1217,33 @@ export function useBuilderConnectFlow(
1180
1217
  setError,
1181
1218
  ]);
1182
1219
 
1220
+ useEffect(() => {
1221
+ if (!reconciling) return;
1222
+ const timer = setTimeout(
1223
+ () => {
1224
+ if (connectAttemptIdRef.current !== reconciling) return;
1225
+ callbackSuccessCancelRef.current?.cancel();
1226
+ callbackSuccessRequestControllerRef.current?.controller.abort();
1227
+ connectStartedAtRef.current = null;
1228
+ setReconciling(null);
1229
+ if (pendingPollRef.current === pollEpochRef.current)
1230
+ markStatusUnavailable();
1231
+ },
1232
+ POPUP_CLOSED_CONFIRMATION_GRACE_MS +
1233
+ CALLBACK_SUCCESS_STATUS_RETRIES *
1234
+ (STATUS_FETCH_ABORT_MS + CALLBACK_SUCCESS_STATUS_RETRY_MS),
1235
+ );
1236
+ return () => clearTimeout(timer);
1237
+ }, [reconciling, markStatusUnavailable]);
1238
+
1183
1239
  const retry = useCallback(() => retryStatusRef.current(), []);
1184
1240
  const cancel = useCallback(() => {
1185
1241
  const started = connectStartedAtRef.current;
1186
1242
  if (started === null) return;
1187
1243
  const attemptId = connectAttemptIdRef.current;
1244
+ setConnecting(false);
1245
+ setReconciling(attemptId);
1246
+ setPollEpoch(++pollEpochRef.current);
1188
1247
  provisionAccountAttemptRef.current = false;
1189
1248
  cancelledConnectAttemptIdRef.current = attemptId;
1190
1249
  popupClosedAtRef.current ??= Date.now();
@@ -1192,6 +1251,7 @@ export function useBuilderConnectFlow(
1192
1251
  callbackSuccessCancelRef.current.cancel();
1193
1252
  if (callbackSuccessRequestControllerRef.current?.started === started)
1194
1253
  callbackSuccessRequestControllerRef.current.controller.abort();
1254
+ callbackSuccessStartedAtRef.current = null;
1195
1255
  try {
1196
1256
  activePopupRef.current?.close();
1197
1257
  } catch {
@@ -1218,7 +1278,9 @@ export function useBuilderConnectFlow(
1218
1278
  (startOptions?: BuilderConnectStartOptions) => {
1219
1279
  if (!enabled) return;
1220
1280
  const started = Date.now();
1281
+ const previousAttemptId = connectAttemptIdRef.current;
1221
1282
  const connectAttemptId = createBuilderConnectAttemptId();
1283
+ blockedLaunchRef.current = null;
1222
1284
  const clickTrackingSource =
1223
1285
  startOptions?.trackingSource ?? trackingSource;
1224
1286
  const clickTrackingFlow = startOptions?.trackingFlow ?? trackingFlow;
@@ -1244,6 +1306,8 @@ export function useBuilderConnectFlow(
1244
1306
  callbackSuccessRequestControllerRef.current?.controller.abort();
1245
1307
  connectStartedAtRef.current = started;
1246
1308
  connectAttemptIdRef.current = connectAttemptId;
1309
+ setReconciling(null);
1310
+ setPollEpoch(++pollEpochRef.current);
1247
1311
  provisionAccountAttemptRef.current = provisionAccountForStart;
1248
1312
  cancelledConnectAttemptIdRef.current = null;
1249
1313
  statusPollFailuresRef.current = 0;
@@ -1551,18 +1615,21 @@ export function useBuilderConnectFlow(
1551
1615
  "Couldn't open Builder. Allow popups and try again.",
1552
1616
  "launch",
1553
1617
  );
1618
+ blockedLaunchRef.current = {
1619
+ from: previousAttemptId,
1620
+ to: connectAttemptId,
1621
+ revision: errorRevisionRef.current,
1622
+ };
1554
1623
  return;
1555
1624
  }
1556
1625
 
1626
+ const isCurrentHostAttempt = () =>
1627
+ mountedRef.current &&
1628
+ connectAttemptIdRef.current === connectAttemptId &&
1629
+ cancelledConnectAttemptIdRef.current !== connectAttemptId;
1557
1630
  void (async () => {
1558
1631
  const s = await fetchStatus(undefined, connectAttemptId);
1559
- if (
1560
- !mountedRef.current ||
1561
- connectAttemptIdRef.current !== connectAttemptId ||
1562
- cancelledConnectAttemptIdRef.current === connectAttemptId
1563
- ) {
1564
- return;
1565
- }
1632
+ if (!isCurrentHostAttempt()) return;
1566
1633
  if (s) markStatusAvailable();
1567
1634
  else markStatusUnavailable();
1568
1635
  if (s) {
@@ -1599,7 +1666,7 @@ export function useBuilderConnectFlow(
1599
1666
  });
1600
1667
  const openedByHost =
1601
1668
  await openBuilderConnectViaMcpHost(trackedHostUrl);
1602
- if (!mountedRef.current || openedByHost) return;
1669
+ if (!isCurrentHostAttempt() || openedByHost) return;
1603
1670
  connectStartedAtRef.current = null;
1604
1671
  setConnecting(false);
1605
1672
  if (!s) restoreExistingAccount();
@@ -1738,12 +1805,18 @@ export function useBuilderConnectFlow(
1738
1805
  usePollLoop(
1739
1806
  async (signal) => {
1740
1807
  const started = connectStartedAtRef.current;
1741
- if (started == null) return;
1742
- const s = await fetchStatus(
1743
- signal,
1744
- connectAttemptIdRef.current ?? undefined,
1745
- );
1746
- if (!mountedRef.current || connectStartedAtRef.current !== started) {
1808
+ const attemptId = connectAttemptIdRef.current;
1809
+ const epoch = pollEpochRef.current;
1810
+ if (started == null || !attemptId) return;
1811
+ pendingPollRef.current = epoch;
1812
+ const s = await fetchStatus(signal, attemptId);
1813
+ if (pendingPollRef.current === epoch) pendingPollRef.current = null;
1814
+ if (
1815
+ !mountedRef.current ||
1816
+ connectStartedAtRef.current !== started ||
1817
+ connectAttemptIdRef.current !== attemptId ||
1818
+ pollEpochRef.current !== epoch
1819
+ ) {
1747
1820
  return;
1748
1821
  }
1749
1822
  if (s) {
@@ -1767,7 +1840,8 @@ export function useBuilderConnectFlow(
1767
1840
  setCanDisconnect(!!s.canDisconnect);
1768
1841
  setAgentNativeProvisioningEnabled(!!s.agentNativeProvisioningEnabled);
1769
1842
  setAgentNativeProvisioningToken(s.agentNativeProvisioningToken ?? null);
1770
- setAccountExists(s.connectError?.code === "account_exists");
1843
+ if (cancelledConnectAttemptIdRef.current !== attemptId)
1844
+ setAccountExists(s.connectError?.code === "account_exists");
1771
1845
  setBuilderEnabled(!!s.builderEnabled);
1772
1846
  const nextConnectUrl = s.connectUrl ?? null;
1773
1847
  setStatusConnectUrl(nextConnectUrl);
@@ -1777,6 +1851,7 @@ export function useBuilderConnectFlow(
1777
1851
  if (s && isBuilderConnectComplete(s, connectTargetRef.current)) {
1778
1852
  setAccountExists(false);
1779
1853
  setConnecting(false);
1854
+ setReconciling(null);
1780
1855
  connectStartedAtRef.current = null;
1781
1856
  notifiedConnectedRef.current = true;
1782
1857
  notifyProvisionedAccount();
@@ -1788,7 +1863,10 @@ export function useBuilderConnectFlow(
1788
1863
  // already flipped; re-arming the flow on a consumer callback failure
1789
1864
  // would reconnect an account that is connected.
1790
1865
  }
1791
- } else if (isCurrentConnectError(s?.connectError, started)) {
1866
+ } else if (
1867
+ cancelledConnectAttemptIdRef.current !== attemptId &&
1868
+ isCurrentConnectError(s?.connectError, started)
1869
+ ) {
1792
1870
  connectStartedAtRef.current = null;
1793
1871
  setConnecting(false);
1794
1872
  setAccountExists(s.connectError.code === "account_exists");
@@ -1798,6 +1876,7 @@ export function useBuilderConnectFlow(
1798
1876
  : `Couldn't save Builder credentials: ${s.connectError.message}. Try again or contact support.`,
1799
1877
  );
1800
1878
  } else if (
1879
+ cancelledConnectAttemptIdRef.current !== attemptId &&
1801
1880
  (isPopupClosed(activePopupRef.current) ||
1802
1881
  popupClosedAtRef.current !== null) &&
1803
1882
  callbackSuccessInFlightAtRef.current !== started
@@ -1821,7 +1900,10 @@ export function useBuilderConnectFlow(
1821
1900
  });
1822
1901
  if (s) setError(t("agentChat.settingsShell.builder.setupFailed"));
1823
1902
  }
1824
- } else if (Date.now() - started > POLL_TIMEOUT_MS) {
1903
+ } else if (
1904
+ cancelledConnectAttemptIdRef.current !== attemptId &&
1905
+ Date.now() - started > POLL_TIMEOUT_MS
1906
+ ) {
1825
1907
  connectStartedAtRef.current = null;
1826
1908
  setConnecting(false);
1827
1909
  const { source, flow } = activeTrackingRef.current;
@@ -1844,7 +1926,7 @@ export function useBuilderConnectFlow(
1844
1926
  {
1845
1927
  intervalMs: getConnectPollInterval,
1846
1928
  leading: false,
1847
- enabled: enabled && connecting,
1929
+ enabled: enabled && (connecting || !!reconciling),
1848
1930
  },
1849
1931
  );
1850
1932
 
@@ -1859,7 +1941,11 @@ export function useBuilderConnectFlow(
1859
1941
  code: string | undefined,
1860
1942
  attemptId: string | undefined,
1861
1943
  ) => {
1862
- if (!isCurrentConnectAttempt(attemptId)) return;
1944
+ if (
1945
+ !isCurrentConnectAttempt(attemptId) ||
1946
+ cancelledConnectAttemptIdRef.current === attemptId
1947
+ )
1948
+ return;
1863
1949
  connectStartedAtRef.current = null;
1864
1950
  setConnecting(false);
1865
1951
  setAccountExists(code === "account_exists");
@@ -1893,6 +1979,9 @@ export function useBuilderConnectFlow(
1893
1979
  started,
1894
1980
  cancel: cancelConfirmation,
1895
1981
  };
1982
+ const isActiveConfirmation = () =>
1983
+ callbackSuccessCancelRef.current?.cancel === cancelConfirmation;
1984
+ const wasPopupClosed = () => popupClosedAtRef.current !== null;
1896
1985
  try {
1897
1986
  for (let i = 0; i < CALLBACK_SUCCESS_STATUS_RETRIES; i += 1) {
1898
1987
  const controller =
@@ -1910,10 +1999,7 @@ export function useBuilderConnectFlow(
1910
1999
  : null;
1911
2000
  try {
1912
2001
  s = await Promise.race([
1913
- fetchStatus(
1914
- controller?.signal,
1915
- connectAttemptIdRef.current ?? undefined,
1916
- ),
2002
+ fetchStatus(controller?.signal, attemptId),
1917
2003
  cancelledPromise,
1918
2004
  ]);
1919
2005
  } finally {
@@ -1928,14 +2014,16 @@ export function useBuilderConnectFlow(
1928
2014
  if (
1929
2015
  cancelled ||
1930
2016
  !mountedRef.current ||
1931
- connectStartedAtRef.current !== started
2017
+ connectStartedAtRef.current !== started ||
2018
+ connectAttemptIdRef.current !== attemptId
1932
2019
  ) {
1933
2020
  return;
1934
2021
  }
1935
2022
  if (s) markStatusAvailable();
1936
2023
  if (
1937
2024
  (s && isBuilderConnectComplete(s, connectTargetRef.current)) ||
1938
- isCurrentConnectError(s?.connectError, started)
2025
+ (cancelledConnectAttemptIdRef.current !== attemptId &&
2026
+ isCurrentConnectError(s?.connectError, started))
1939
2027
  ) {
1940
2028
  break;
1941
2029
  }
@@ -1948,31 +2036,39 @@ export function useBuilderConnectFlow(
1948
2036
  }
1949
2037
  }
1950
2038
  } finally {
1951
- if (callbackSuccessCancelRef.current?.started === started) {
2039
+ if (isActiveConfirmation()) {
1952
2040
  callbackSuccessCancelRef.current = null;
1953
- }
1954
- if (callbackSuccessRequestControllerRef.current?.started === started) {
1955
- callbackSuccessRequestControllerRef.current.controller.abort();
1956
- callbackSuccessRequestControllerRef.current = null;
1957
- }
1958
- if (callbackSuccessInFlightAtRef.current === started) {
1959
- callbackSuccessInFlightAtRef.current = null;
1960
2041
  if (
1961
- popupClosedAtRef.current !== null &&
1962
- !(s && isBuilderConnectComplete(s, connectTargetRef.current))
2042
+ callbackSuccessRequestControllerRef.current?.started === started
1963
2043
  ) {
1964
- popupClosedAtRef.current = Date.now();
2044
+ callbackSuccessRequestControllerRef.current.controller.abort();
2045
+ callbackSuccessRequestControllerRef.current = null;
2046
+ }
2047
+ if (callbackSuccessInFlightAtRef.current === started) {
2048
+ callbackSuccessInFlightAtRef.current = null;
2049
+ if (
2050
+ wasPopupClosed() &&
2051
+ !(s && isBuilderConnectComplete(s, connectTargetRef.current))
2052
+ ) {
2053
+ popupClosedAtRef.current = Date.now();
2054
+ }
1965
2055
  }
1966
2056
  }
1967
2057
  }
1968
- if (!mountedRef.current || connectStartedAtRef.current !== started) {
2058
+ if (
2059
+ !mountedRef.current ||
2060
+ connectStartedAtRef.current !== started ||
2061
+ connectAttemptIdRef.current !== attemptId
2062
+ ) {
1969
2063
  return;
1970
2064
  }
1971
2065
  if (!s) return;
1972
2066
  if (!isBuilderConnectComplete(s, connectTargetRef.current)) {
1973
- const connectError = isCurrentConnectError(s?.connectError, started)
1974
- ? s?.connectError
1975
- : null;
2067
+ const connectError =
2068
+ cancelledConnectAttemptIdRef.current !== attemptId &&
2069
+ isCurrentConnectError(s.connectError, started)
2070
+ ? s.connectError
2071
+ : null;
1976
2072
  setHasFetchedStatus(true);
1977
2073
  if (s) {
1978
2074
  setStatusResolved(true);
@@ -1990,7 +2086,8 @@ export function useBuilderConnectFlow(
1990
2086
  setAgentNativeProvisioningToken(
1991
2087
  s.agentNativeProvisioningToken ?? null,
1992
2088
  );
1993
- setAccountExists(s.connectError?.code === "account_exists");
2089
+ if (cancelledConnectAttemptIdRef.current !== attemptId)
2090
+ setAccountExists(s.connectError?.code === "account_exists");
1994
2091
  setBuilderEnabled(!!s.builderEnabled);
1995
2092
  const nextConnectUrl = s.connectUrl ?? null;
1996
2093
  setStatusConnectUrl(nextConnectUrl);
@@ -2027,6 +2124,7 @@ export function useBuilderConnectFlow(
2027
2124
  const org = s.orgName ?? null;
2028
2125
  setOrgName(org);
2029
2126
  setConnecting(false);
2127
+ setReconciling(null);
2030
2128
  connectStartedAtRef.current = null;
2031
2129
  notifyProvisionedAccount();
2032
2130
  notifiedConnectedRef.current = true;
@@ -136,32 +136,91 @@ describe("connected composer menus", () => {
136
136
  );
137
137
  const open = async () => {
138
138
  await openRoot();
139
- if (document.querySelector('[role="menuitem"][aria-haspopup="menu"]'))
140
- await key(row("Add context"), "ArrowRight");
141
139
  };
142
140
  async function click(label: string) {
143
141
  await act(async () => row(label).click());
144
142
  }
143
+ it("keeps integration failures visible with a working retry outside source choices", async () => {
144
+ const retry = vi.fn().mockResolvedValue(undefined);
145
+ await render(
146
+ [
147
+ {
148
+ id: "source",
149
+ label: "Reference",
150
+ section: "context",
151
+ onSelect: vi.fn(),
152
+ },
153
+ ],
154
+ { integrationStatus: { loading: true } },
155
+ );
156
+ await openRoot();
157
+ const menu = menus()[0];
158
+ expect(menu.querySelector('[aria-busy="true"]')).not.toBeNull();
159
+ expect(menu.textContent).toContain("Loading...");
160
+ expect(menu.querySelector('[role="status"]')).toBeNull();
161
+ await render(
162
+ [
163
+ {
164
+ id: "source",
165
+ label: "Reference",
166
+ section: "context",
167
+ onSelect: vi.fn(),
168
+ },
169
+ ],
170
+ {
171
+ integrationStatus: { error: "Connection unavailable", onRetry: retry },
172
+ },
173
+ );
174
+ expect(menus()[0].textContent).toContain("Connection unavailable");
175
+ expect(row("Reference")).toBeDefined();
176
+ await click("Retry");
177
+ expect(retry).toHaveBeenCalledOnce();
178
+ expect(menus()).toHaveLength(1);
179
+ });
145
180
  it("keeps the root menu compact and puts context choices in nested menus", async () => {
146
- await render(items, { addAttachment: vi.fn() });
181
+ await render(
182
+ [
183
+ {
184
+ id: "references",
185
+ label: "Design references",
186
+ order: 20,
187
+ children: items,
188
+ },
189
+ { id: "figma", label: "Figma design", order: 10, onSelect: vi.fn() },
190
+ { id: "zeta", label: "Zeta", order: 30, onSelect: vi.fn() },
191
+ { id: "alpha", label: "Alpha", order: 30, onSelect: vi.fn() },
192
+ ],
193
+ { addAttachment: vi.fn() },
194
+ );
147
195
  await openRoot();
148
196
  expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(0);
149
- // Menus are text-only.
150
- expect(row("Upload File").querySelector("svg")).toBeNull();
151
- expect(row("Add context").getAttribute("aria-haspopup")).toBe("menu");
197
+ expect(row("Files and images").querySelector("svg")).not.toBeNull();
198
+ expect(
199
+ Array.from(menus()[0].querySelectorAll('[role="menuitem"]')).map(
200
+ (entry) => entry.textContent,
201
+ ),
202
+ ).toEqual([
203
+ "Files and images",
204
+ "Figma design",
205
+ "Design references",
206
+ "Alpha",
207
+ "Zeta",
208
+ ]);
209
+ expect(row("Design references").getAttribute("aria-haspopup")).toBe("menu");
210
+ expect(menus()[0].textContent).toContain("Add context");
152
211
  expect(menus()).toHaveLength(1);
153
212
  expect(menus()[0].classList.contains("w-64")).toBe(true);
154
213
  expect(menus()[0].style.boxShadow).toBe("none");
155
214
  expect(menus()[0].className).toContain("data-[state=open]:fade-in-100");
156
215
  expect(menus()[0].className).toContain("data-[state=closed]:fade-out-100");
157
- await key(row("Add context"), "ArrowRight");
216
+ await key(row("Design references"), "ArrowRight");
158
217
  expect(row("Documents")).toBeDefined();
159
218
  expect(row("Library")).toBeDefined();
160
219
  expect(document.querySelectorAll('[role="searchbox"]')).toHaveLength(0);
161
220
  expect(menus()).toHaveLength(2);
162
- expect(menus()[1].style.boxShadow).toBe("none");
163
- expect(menus()[1].className).toContain("data-[state=open]:fade-in-100");
164
- expect(menus()[1].className).toContain("data-[state=closed]:fade-out-100");
221
+ expect(menus()[0].style.boxShadow).toBe("none");
222
+ expect(menus()[0].className).toContain("data-[state=open]:fade-in-100");
223
+ expect(menus()[0].className).toContain("data-[state=closed]:fade-out-100");
165
224
  });
166
225
  it("hides the Add context tooltip while the host storage popover is open", async () => {
167
226
  const warning = vi.spyOn(console, "warn").mockImplementation(() => {});
@@ -198,7 +257,7 @@ describe("connected composer menus", () => {
198
257
  expect(row("Workspace apps")).toBeDefined();
199
258
  await key(row("Workspace apps"), "ArrowRight");
200
259
  expect(row("DesignInteractive prototypes")).toBeDefined();
201
- expect(menus()).toHaveLength(3);
260
+ expect(menus()).toHaveLength(2);
202
261
  await key(row("DesignInteractive prototypes"), "Enter");
203
262
  expect(menus()).toHaveLength(0);
204
263
  });
@@ -235,13 +294,13 @@ describe("connected composer menus", () => {
235
294
  await openRoot();
236
295
  expect(menus()[0].classList.contains("w-64")).toBe(true);
237
296
  expect(menus()[0].style.width).toBe("");
238
- expect(menus()[0].style.maxHeight).toBe("280px");
297
+ expect(menus()[0].style.maxHeight).toBe("376px");
239
298
  },
240
299
  );
241
300
  it.each([
242
301
  [8, "bottom", 240],
243
302
  [40, "bottom", 208],
244
- [100, "top", 136],
303
+ [100, "bottom", 148],
245
304
  [180, "top", 216],
246
305
  ] as const)(
247
306
  "keeps actions reachable at plus-button top %ipx in a short viewport",
@@ -270,7 +329,7 @@ describe("connected composer menus", () => {
270
329
  expect(menus()[0].style.maxHeight).toBe(`${maxHeight}px`);
271
330
  expect(menus()[0].getAttribute("data-side")).toBe(side);
272
331
  expect(menus()[0].querySelector('[role="menuitem"]')).not.toBeNull();
273
- expect(menus()[0].querySelector(".overflow-y-auto")).not.toBeNull();
332
+ expect(menus()[0].classList.contains("overflow-y-auto")).toBe(true);
274
333
  } finally {
275
334
  window.innerHeight = previousHeight;
276
335
  }
@@ -318,7 +377,7 @@ describe("connected composer menus", () => {
318
377
  viewport.dispatchEvent(new Event("scroll"));
319
378
  });
320
379
  expect(menus()[0].style.maxHeight).toBe("118px");
321
- await key(row("Add context"), "Escape");
380
+ await key(row("Documents"), "Escape");
322
381
  expect(removeListener).toHaveBeenCalledWith(
323
382
  "resize",
324
383
  expect.any(Function),
@@ -351,7 +410,6 @@ describe("connected composer menus", () => {
351
410
  },
352
411
  ]);
353
412
  await openRoot();
354
- await key(row("Add context"), "ArrowLeft");
355
413
  expect(menus()[0].getAttribute("dir")).toBe("rtl");
356
414
  await key(row("Source"), "ArrowLeft");
357
415
  expect(row("One")).toBeDefined();
@@ -398,7 +456,6 @@ describe("connected composer menus", () => {
398
456
  }
399
457
  await key(row("Project brief"), "Escape");
400
458
  await key(row("Documents"), "Escape");
401
- await key(row("Add context"), "Escape");
402
459
  expect(observation.disconnect).toHaveBeenCalled();
403
460
  });
404
461
  it("closes a context picker when its composer becomes disabled", async () => {
@@ -469,7 +526,7 @@ describe("connected composer menus", () => {
469
526
  await key(row("Documents"), "ArrowRight");
470
527
  expect(row("Project brief")).toBeDefined();
471
528
  expect(row("Notes")).toBeDefined();
472
- expect(menus()).toHaveLength(3);
529
+ expect(menus()).toHaveLength(2);
473
530
  await click("Project brief");
474
531
  expect(select).toHaveBeenCalledOnce();
475
532
  expect(menus()).toHaveLength(0);
@@ -477,21 +534,20 @@ describe("connected composer menus", () => {
477
534
  it("supports native keyboard navigation across nested context menus", async () => {
478
535
  await render();
479
536
  await openRoot();
480
- await key(row("Add context"), "ArrowRight");
481
537
  await key(row("Documents"), "ArrowRight");
482
- expect(menus()).toHaveLength(3);
483
- await key(row("Project brief"), "ArrowDown");
538
+ expect(menus()).toHaveLength(2);
539
+ await key(row("Archive"), "ArrowDown");
540
+ expect(document.activeElement).toBe(row("Project brief"));
541
+ await key(row("Project brief"), "ArrowUp");
484
542
  expect(document.activeElement).toBe(row("Archive"));
485
543
  await key(row("Archive"), "ArrowRight");
486
544
  expect(row("Meeting notes")).toBeDefined();
487
- expect(menus()).toHaveLength(4);
488
- await key(row("Meeting notes"), "Escape");
489
545
  expect(menus()).toHaveLength(3);
490
- await key(row("Archive"), "Escape");
546
+ await key(row("Meeting notes"), "Escape");
491
547
  expect(menus()).toHaveLength(2);
492
- await key(row("Documents"), "Escape");
548
+ await key(row("Archive"), "Escape");
493
549
  expect(menus()).toHaveLength(1);
494
- await key(row("Add context"), "Escape");
550
+ await key(row("Documents"), "Escape");
495
551
  expect(menus()).toHaveLength(0);
496
552
  });
497
553
  it("preserves native upload accepts, cancellation, multiple files and errors", async () => {
@@ -511,7 +567,7 @@ describe("connected composer menus", () => {
511
567
  expect(input.multiple).toBe(true);
512
568
  expect(input.accept).toBe("text/plain");
513
569
  await openRoot();
514
- await click("Upload File");
570
+ await click("Files and images");
515
571
  expect(picker).toHaveBeenCalledOnce();
516
572
  await act(async () =>
517
573
  input.dispatchEvent(new Event("change", { bubbles: true })),
@@ -527,11 +583,18 @@ describe("connected composer menus", () => {
527
583
  });
528
584
  it("requests gated upload after the menu closes", async () => {
529
585
  const onAttachmentRequest = vi.fn();
530
- await render([], { onAttachmentRequest });
586
+ const addAttachment = vi.fn();
587
+ await render([], { onAttachmentRequest, addAttachment });
588
+ const input =
589
+ document.querySelector<HTMLInputElement>('input[type="file"]');
590
+ if (!input) throw new Error("Missing file input");
591
+ const choose = vi.spyOn(input, "click");
531
592
  await openRoot();
532
- await click("Upload File");
593
+ await click("Files and images");
533
594
  await act(async () => new Promise((resolve) => setTimeout(resolve, 0)));
534
595
  expect(onAttachmentRequest).toHaveBeenCalledOnce();
596
+ expect(choose).not.toHaveBeenCalled();
597
+ expect(addAttachment).not.toHaveBeenCalled();
535
598
  expect(menus()).toHaveLength(0);
536
599
  expect(document.activeElement?.getAttribute("aria-label")).toBe(
537
600
  "Add context",
@@ -570,7 +633,7 @@ describe("connected composer menus", () => {
570
633
  expect(select).toHaveBeenCalledOnce();
571
634
  expect(container.querySelector('[aria-label="Prompt draft"]')).toBe(draft);
572
635
  await act(async () => controls.onBack());
573
- expect(menus()).toHaveLength(2);
636
+ expect(menus()).toHaveLength(1);
574
637
  expect(dismiss).toHaveBeenCalledTimes(2);
575
638
  });
576
639
  it("ignores stale legacy close and reports resume of a removed source", async () => {
@@ -593,7 +656,7 @@ describe("connected composer menus", () => {
593
656
  const old = controls;
594
657
  await act(async () => controls.onBack());
595
658
  await act(async () => old.onClose());
596
- expect(menus()).toHaveLength(2);
659
+ expect(menus()).toHaveLength(1);
597
660
  await render([]);
598
661
  await act(async () => old.onResume());
599
662
  expect(document.querySelector('[role="alert"]')?.textContent).toBe(
@@ -617,7 +680,7 @@ describe("connected composer menus", () => {
617
680
  ]);
618
681
  await open();
619
682
  await click("Reference");
620
- expect(menus()).toHaveLength(3);
683
+ expect(menus()).toHaveLength(2);
621
684
  expect(document.querySelector('[role="alert"]')?.textContent).toBe(
622
685
  "Reference unavailable",
623
686
  );