@agent-native/toolkit 0.221.0-same-host-mcp-20261027-20261027081850 → 0.221.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 (259) hide show
  1. package/agent-native.eject.json +16 -1
  2. package/dist/app/agentkit/react/components.d.ts +5 -3
  3. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  4. package/dist/app/agentkit/react/components.js +103 -42
  5. package/dist/app/agentkit/react/components.js.map +1 -1
  6. package/dist/app/agentkit/react/styles.css +93 -65
  7. package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
  8. package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
  9. package/dist/app/agentkit/react/voice-transcripts.js +209 -8
  10. package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
  11. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  12. package/dist/app/chat/AgentKitAssistantChat.js +55 -40
  13. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  14. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
  15. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  16. package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
  17. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  18. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  19. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  20. package/dist/app/chat/agentkit-chat/index.js +1 -1
  21. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  22. package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
  23. package/dist/app/chat/chat/run-recovery.js +5 -1
  24. package/dist/app/chat/chat/run-recovery.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/reply-connection-suggestion.d.ts +29 -0
  29. package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
  30. package/dist/app/chat/reply-connection-suggestion.js +42 -0
  31. package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
  32. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/ar-SA.js +7 -0
  34. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  35. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  36. package/dist/app/i18n/catalogs/de-DE.js +7 -0
  37. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  38. package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
  39. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/en-US.js +7 -0
  41. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  42. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/es-ES.js +7 -0
  44. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  45. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/fr-FR.js +7 -0
  47. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  48. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/hi-IN.js +7 -0
  50. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  51. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/ja-JP.js +7 -0
  53. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  54. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/ko-KR.js +7 -0
  56. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  57. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/pt-BR.js +7 -0
  59. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  60. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-CN.js +7 -0
  62. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  63. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  64. package/dist/app/i18n/catalogs/zh-TW.js +7 -0
  65. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  66. package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
  67. package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
  68. package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
  69. package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
  70. package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
  71. package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
  72. package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
  73. package/dist/app/onboarding/GettingStartedCard.js +1 -1
  74. package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
  75. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
  76. package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
  77. package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
  78. package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
  79. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  80. package/dist/app/providers/AppProviders.js +14 -1
  81. package/dist/app/providers/AppProviders.js.map +1 -1
  82. package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
  83. package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
  84. package/dist/app/resources/McpConnectionSuggestion.js +59 -36
  85. package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
  86. package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
  87. package/dist/app/settings/account/PreferencesSettings.js +14 -1
  88. package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
  89. package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
  90. package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
  91. package/dist/app/settings/account/search-entries.d.ts +1 -0
  92. package/dist/app/settings/account/search-entries.d.ts.map +1 -1
  93. package/dist/app/settings/account/search-entries.js +5 -0
  94. package/dist/app/settings/account/search-entries.js.map +1 -1
  95. package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
  96. package/dist/app/settings/shell/SettingsShell.js +26 -2
  97. package/dist/app/settings/shell/SettingsShell.js.map +1 -1
  98. package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
  99. package/dist/app/shared/AppShellSkeleton.js +4 -10
  100. package/dist/app/shared/AppShellSkeleton.js.map +1 -1
  101. package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
  102. package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
  103. package/dist/app/shared/EnvironmentBadge.js +2 -3
  104. package/dist/app/shared/EnvironmentBadge.js.map +1 -1
  105. package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
  106. package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
  107. package/dist/app/shared/is-builder-io-employee.js +10 -0
  108. package/dist/app/shared/is-builder-io-employee.js.map +1 -0
  109. package/dist/app/styles/agent-conversation.css +30 -17
  110. package/dist/app/styles/theme-families.css +22 -5
  111. package/dist/app/styles/tokens/agent-kit.css +3 -3
  112. package/dist/composer/PromptComposer.d.ts +15 -3
  113. package/dist/composer/PromptComposer.d.ts.map +1 -1
  114. package/dist/composer/PromptComposer.js +74 -16
  115. package/dist/composer/PromptComposer.js.map +1 -1
  116. package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
  117. package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
  118. package/dist/composer/RealtimeVoiceMode.js +9 -2
  119. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  120. package/dist/composer/TiptapComposer.d.ts +7 -0
  121. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  122. package/dist/composer/TiptapComposer.js +12 -4
  123. package/dist/composer/TiptapComposer.js.map +1 -1
  124. package/dist/composer/VoiceButton.d.ts +2 -0
  125. package/dist/composer/VoiceButton.d.ts.map +1 -1
  126. package/dist/composer/VoiceButton.js +5 -4
  127. package/dist/composer/VoiceButton.js.map +1 -1
  128. package/dist/composer/index.d.ts +2 -2
  129. package/dist/composer/index.d.ts.map +1 -1
  130. package/dist/composer/index.js +1 -1
  131. package/dist/composer/index.js.map +1 -1
  132. package/dist/composer/prompt-attachments.d.ts +20 -0
  133. package/dist/composer/prompt-attachments.d.ts.map +1 -1
  134. package/dist/composer/prompt-attachments.js +188 -11
  135. package/dist/composer/prompt-attachments.js.map +1 -1
  136. package/dist/composer/realtime-voice-transcript.d.ts +27 -0
  137. package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
  138. package/dist/composer/realtime-voice-transcript.js +34 -1
  139. package/dist/composer/realtime-voice-transcript.js.map +1 -1
  140. package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
  141. package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
  142. package/dist/composer/realtime-voice-turn-metrics.js +132 -0
  143. package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
  144. package/dist/composer/runtime-adapters.d.ts +4 -1
  145. package/dist/composer/runtime-adapters.d.ts.map +1 -1
  146. package/dist/composer/runtime-adapters.js +2 -1
  147. package/dist/composer/runtime-adapters.js.map +1 -1
  148. package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
  149. package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
  150. package/dist/composer/useRealtimeVoiceMode.js +536 -99
  151. package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
  152. package/dist/composer/voice-chat-icon.d.ts +7 -0
  153. package/dist/composer/voice-chat-icon.d.ts.map +1 -0
  154. package/dist/composer/voice-chat-icon.js +14 -0
  155. package/dist/composer/voice-chat-icon.js.map +1 -0
  156. package/dist/editor/DragHandle.d.ts +12 -1
  157. package/dist/editor/DragHandle.d.ts.map +1 -1
  158. package/dist/editor/DragHandle.js +165 -41
  159. package/dist/editor/DragHandle.js.map +1 -1
  160. package/dist/editor/RegistryBlockNode.d.ts +20 -0
  161. package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
  162. package/dist/editor/RegistryBlockNode.js +58 -13
  163. package/dist/editor/RegistryBlockNode.js.map +1 -1
  164. package/dist/editor/index.d.ts +2 -2
  165. package/dist/editor/index.d.ts.map +1 -1
  166. package/dist/editor/index.js +1 -1
  167. package/dist/editor/index.js.map +1 -1
  168. package/dist/styles.css +2 -0
  169. package/dist/ui/dropdown-menu.d.ts.map +1 -1
  170. package/dist/ui/dropdown-menu.js +5 -1
  171. package/dist/ui/dropdown-menu.js.map +1 -1
  172. package/package.json +14 -4
  173. package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
  174. package/src/app/agentkit/react/components.spec.tsx +24 -4
  175. package/src/app/agentkit/react/components.tsx +174 -44
  176. package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
  177. package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
  178. package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
  179. package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
  180. package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
  181. package/src/app/agentkit/react/styles.css +93 -65
  182. package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
  183. package/src/app/agentkit/react/voice-transcripts.ts +245 -8
  184. package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
  185. package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
  186. package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
  187. package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
  188. package/src/app/chat/agentkit-chat/index.ts +1 -0
  189. package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
  190. package/src/app/chat/chat/run-recovery.tsx +6 -1
  191. package/src/app/chat/composer/runtime-adapters.tsx +2 -0
  192. package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
  193. package/src/app/chat/reply-connection-suggestion.ts +76 -0
  194. package/src/app/i18n/catalogs/ar-SA.ts +10 -0
  195. package/src/app/i18n/catalogs/de-DE.ts +10 -0
  196. package/src/app/i18n/catalogs/en-US.ts +10 -0
  197. package/src/app/i18n/catalogs/es-ES.ts +10 -0
  198. package/src/app/i18n/catalogs/fr-FR.ts +10 -0
  199. package/src/app/i18n/catalogs/hi-IN.ts +10 -0
  200. package/src/app/i18n/catalogs/ja-JP.ts +10 -0
  201. package/src/app/i18n/catalogs/ko-KR.ts +10 -0
  202. package/src/app/i18n/catalogs/pt-BR.ts +10 -0
  203. package/src/app/i18n/catalogs/zh-CN.ts +10 -0
  204. package/src/app/i18n/catalogs/zh-TW.ts +10 -0
  205. package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
  206. package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
  207. package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
  208. package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
  209. package/src/app/onboarding/GettingStartedCard.tsx +1 -1
  210. package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
  211. package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
  212. package/src/app/providers/AppProviders.spec.tsx +109 -0
  213. package/src/app/providers/AppProviders.tsx +53 -1
  214. package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
  215. package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
  216. package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
  217. package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
  218. package/src/app/settings/account/PreferencesSettings.tsx +24 -7
  219. package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
  220. package/src/app/settings/account/search-entries.spec.ts +14 -0
  221. package/src/app/settings/account/search-entries.ts +9 -0
  222. package/src/app/settings/shell/SettingsShell.tsx +29 -2
  223. package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
  224. package/src/app/shared/AppShellSkeleton.tsx +3 -16
  225. package/src/app/shared/EnvironmentBadge.tsx +3 -4
  226. package/src/app/shared/is-builder-io-employee.ts +17 -0
  227. package/src/app/styles/agent-conversation.css +30 -17
  228. package/src/app/styles/theme-families.css +22 -5
  229. package/src/app/styles/theme-families.spec.ts +73 -0
  230. package/src/app/styles/tokens/agent-kit.css +3 -3
  231. package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
  232. package/src/composer/AttachmentChip.spec.tsx +87 -0
  233. package/src/composer/PromptComposer.context.spec.tsx +8 -4
  234. package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
  235. package/src/composer/PromptComposer.tsx +125 -14
  236. package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
  237. package/src/composer/RealtimeVoiceMode.tsx +45 -2
  238. package/src/composer/TiptapComposer.tsx +25 -0
  239. package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
  240. package/src/composer/VoiceButton.tsx +8 -3
  241. package/src/composer/index.ts +4 -0
  242. package/src/composer/prompt-attachments.spec.ts +160 -8
  243. package/src/composer/prompt-attachments.ts +221 -12
  244. package/src/composer/realtime-voice-transcript.spec.ts +36 -0
  245. package/src/composer/realtime-voice-transcript.ts +56 -1
  246. package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
  247. package/src/composer/realtime-voice-turn-metrics.ts +192 -0
  248. package/src/composer/runtime-adapters.tsx +5 -1
  249. package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
  250. package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
  251. package/src/composer/useRealtimeVoiceMode.tsx +716 -139
  252. package/src/composer/voice-chat-icon.tsx +19 -0
  253. package/src/editor/DragHandle.spec.ts +217 -0
  254. package/src/editor/DragHandle.ts +214 -55
  255. package/src/editor/RegistryBlockNode.spec.ts +172 -2
  256. package/src/editor/RegistryBlockNode.tsx +91 -13
  257. package/src/editor/index.ts +4 -0
  258. package/src/styles.css +2 -0
  259. package/src/ui/dropdown-menu.tsx +4 -0
@@ -18,6 +18,7 @@ import {
18
18
  useOnboardingPreviewMode,
19
19
  useOnboardingPreviewStep,
20
20
  } from "@agent-native/core/client/onboarding/use-preview-mode";
21
+ import { useActionMutation } from "@agent-native/core/client/use-action";
21
22
  import { SETTINGS_REDESIGN_FLAG } from "@agent-native/core/feature-flags/registry";
22
23
  import {
23
24
  buildSettingsRoute,
@@ -27,6 +28,10 @@ import type {
27
28
  OnboardingAppProfile,
28
29
  OnboardingCapability,
29
30
  } from "@agent-native/core/onboarding/types";
31
+ import {
32
+ FIRST_RUN_BUILDER_CREDITS_V2_EXPERIMENT_ID,
33
+ FIRST_RUN_BUILDER_CREDITS_V2_FLAG_KEY,
34
+ } from "@agent-native/core/shared";
30
35
  import { Badge } from "@agent-native/toolkit/ui/badge";
31
36
  import { Button } from "@agent-native/toolkit/ui/button";
32
37
  import {
@@ -43,6 +48,7 @@ import {
43
48
  } from "@agent-native/toolkit/ui/tooltip";
44
49
  import { cn } from "@agent-native/toolkit/utils";
45
50
  import {
51
+ IconAlertCircle,
46
52
  IconArrowRight,
47
53
  IconChevronDown,
48
54
  IconChevronUp,
@@ -77,6 +83,7 @@ import {
77
83
 
78
84
  type FirstRunScreen = "choice" | "role" | "connecting" | "extension";
79
85
  type FirstRunExtensionPlacement = "before-setup" | "after-setup";
86
+ const FIRST_RUN_CREDITS_V2_APP_IDS = new Set(["clips", "design", "slides"]);
80
87
  type FirstRunSetupMethodId =
81
88
  | "builder_create_account"
82
89
  | "builder_sign_in"
@@ -118,6 +125,7 @@ function firstRunStepProperties(
118
125
  extensionIndex: number,
119
126
  extensionStepIndex: number,
120
127
  setupSkippable: boolean,
128
+ setupRecoverySkippable = false,
121
129
  ): Record<string, unknown> {
122
130
  // `optional` must match whether the screen offers a skip: Analytics funnels
123
131
  // read it to keep a skip from counting as drop-off.
@@ -156,7 +164,10 @@ function firstRunStepProperties(
156
164
  flow: "first_run",
157
165
  step_id: screen,
158
166
  step_index: stepIndex,
159
- optional: screen === "role" || (screen === "choice" && setupSkippable),
167
+ optional:
168
+ screen === "role" ||
169
+ (screen === "choice" && setupSkippable) ||
170
+ (screen === "connecting" && setupRecoverySkippable),
160
171
  };
161
172
  }
162
173
 
@@ -222,17 +233,68 @@ export function FirstRunOnboarding({
222
233
  const [setupExperimentDecision, setSetupExperimentDecision] = useState<
223
234
  boolean | null
224
235
  >(null);
225
- const setupVariant = useMemo(
226
- () =>
227
- (previewMode && getPreviewFirstRunSetupVariant(window.location.search)) ||
228
- assignFirstRunSetupVariant(),
229
- [previewMode],
236
+ const [creditsV2Assignment, setCreditsV2Assignment] = useState<{
237
+ experiment_id: typeof FIRST_RUN_BUILDER_CREDITS_V2_EXPERIMENT_ID;
238
+ experiment_variant: "control" | "credits";
239
+ } | null>(null);
240
+ const [creditsV2ExposureTracked, setCreditsV2ExposureTracked] =
241
+ useState(false);
242
+ const creditsV2Flag = useFeatureFlagState(
243
+ FIRST_RUN_BUILDER_CREDITS_V2_FLAG_KEY,
230
244
  );
245
+ const creditsV2FlagAppliesToApp = FIRST_RUN_CREDITS_V2_APP_IDS.has(
246
+ profile?.appId ?? "",
247
+ );
248
+ const creditsV2Exposure = useActionMutation<
249
+ {
250
+ assignment: {
251
+ experiment_id: string;
252
+ experiment_variant: string;
253
+ } | null;
254
+ },
255
+ { confirmedVariant?: "control" | "credits" }
256
+ >("record-first-run-builder-credits-v2-exposure" as never, {
257
+ uiOnly: true,
258
+ skipActionQueryInvalidation: true,
259
+ });
260
+ const previewCreditsV2 =
261
+ previewMode &&
262
+ typeof window !== "undefined" &&
263
+ new URLSearchParams(window.location.search).get("experiment") === "v2";
264
+ const creditsV2Enabled =
265
+ creditsV2FlagAppliesToApp &&
266
+ creditsV2Flag.status === "ready" &&
267
+ creditsV2Flag.enabled;
268
+ const creditsV2FlowEnabled = creditsV2Enabled || previewCreditsV2;
269
+ const creditsV2FlagPending =
270
+ !previewMode &&
271
+ creditsV2FlagAppliesToApp &&
272
+ creditsV2Flag.status === "loading";
273
+ const setupVariant = useMemo(() => {
274
+ const previewVariant =
275
+ previewMode && getPreviewFirstRunSetupVariant(window.location.search);
276
+ if (previewCreditsV2) return previewVariant;
277
+ if (previewVariant) return previewVariant;
278
+ if (creditsV2FlowEnabled) {
279
+ return creditsV2Assignment?.experiment_variant ?? null;
280
+ }
281
+ return assignFirstRunSetupVariant();
282
+ }, [
283
+ creditsV2FlowEnabled,
284
+ creditsV2Assignment,
285
+ previewCreditsV2,
286
+ previewMode,
287
+ profile?.appId,
288
+ ]);
289
+ const setupExperimentId = creditsV2FlowEnabled
290
+ ? FIRST_RUN_BUILDER_CREDITS_V2_EXPERIMENT_ID
291
+ : FIRST_RUN_SETUP_EXPERIMENT_ID;
231
292
  // The credits arm offers Skip in every app, so analytics must mark the choice step optional there too.
232
293
  const choiceSkippable =
233
294
  setupSkippable ||
234
295
  (setupVariant === "credits" &&
235
- (previewMode || setupExperimentDecision === true));
296
+ (previewMode || setupExperimentDecision === true) &&
297
+ (!creditsV2Enabled || creditsV2ExposureTracked));
236
298
  const [extensionPlacement, setExtensionPlacement] =
237
299
  useState<FirstRunExtensionPlacement>("after-setup");
238
300
  const [extensionIndex, setExtensionIndex] = useState(0);
@@ -332,6 +394,7 @@ export function FirstRunOnboarding({
332
394
  reason = "user_action",
333
395
  stepExtensionIndex = extensionIndex,
334
396
  stepIndex = extensionStepIndex,
397
+ optionalOverride?: boolean,
335
398
  ) => {
336
399
  if (previewMode) return;
337
400
  trackOnboardingEvent("onboarding_step_skipped", {
@@ -344,6 +407,9 @@ export function FirstRunOnboarding({
344
407
  stepIndex,
345
408
  choiceSkippable,
346
409
  ),
410
+ ...(optionalOverride !== undefined
411
+ ? { optional: optionalOverride }
412
+ : {}),
347
413
  reason,
348
414
  });
349
415
  },
@@ -365,6 +431,12 @@ export function FirstRunOnboarding({
365
431
  const completionInFlightRef = useRef(false);
366
432
  const completionRedirectRef = useRef<string | null>(null);
367
433
  const setupSkipStartedRef = useRef(false);
434
+ const creditsV2ExposureStartedRef = useRef(false);
435
+ const creditsV2ExposureFailedRef = useRef(false);
436
+ const creditsV2ExposureSettledRef = useRef(false);
437
+ const creditsV2ExposureTrackedRef = useRef(false);
438
+ const creditsV2ExposureConfirmationStartedRef = useRef(false);
439
+ const setupExperimentTimedOutRef = useRef(false);
368
440
  const onboardingTerminalRef = useRef(false);
369
441
  const abandonmentTrackedRef = useRef(false);
370
442
  const onboardingStartedRef = useRef(false);
@@ -437,14 +509,150 @@ export function FirstRunOnboarding({
437
509
  trackOnboardingEvent("onboarding_started", { flow: "first_run" });
438
510
  }
439
511
  }, [canTrackOnboardingScreen, firstRun, previewMode]);
440
- // Both arms render a skeleton until the experiment decision is known, and the
441
- // credits arm's skip depends on it, so the choice view waits for the decision
442
- // and is sent once with its resolved optional flag. Without an assigned arm
443
- // no decision is ever made, and the choice UI renders as is.
512
+ // The v2 choice event waits until the rendered arm has been attributed.
444
513
  const choiceStepKnown =
445
514
  screen !== "choice" ||
446
- setupVariant === null ||
447
- setupExperimentDecision !== null;
515
+ (!creditsV2FlagPending &&
516
+ (!creditsV2FlowEnabled || setupExperimentDecision !== null) &&
517
+ (setupVariant === null || setupExperimentDecision !== null) &&
518
+ (!creditsV2Enabled ||
519
+ setupExperimentDecision !== true ||
520
+ creditsV2ExposureTracked));
521
+ const beginBeforeSetup = useCallback(() => {
522
+ if (beforeSetupExtensions.length === 0) {
523
+ setScreen("choice");
524
+ return;
525
+ }
526
+ setExtensionPlacement("before-setup");
527
+ setExtensionIndex(0);
528
+ setExtensionStepIndex(0);
529
+ setScreen("extension");
530
+ }, [beforeSetupExtensions.length]);
531
+ const handleFinish = useCallback(
532
+ (completedScreen: FirstRunScreen | null, track = true) => {
533
+ if (afterSetupExtensions.length === 0) {
534
+ void finishOnboarding(completedScreen);
535
+ return;
536
+ }
537
+ if (completedScreen && track) trackFirstRunStepCompleted(completedScreen);
538
+ setExtensionPlacement("after-setup");
539
+ setExtensionIndex(0);
540
+ setExtensionStepIndex(0);
541
+ setScreen("extension");
542
+ },
543
+ [afterSetupExtensions.length, finishOnboarding, trackFirstRunStepCompleted],
544
+ );
545
+ const skipFailedBuilderSetup = useCallback(() => {
546
+ if (setupSkipStartedRef.current || completionInFlightRef.current) return;
547
+ setupSkipStartedRef.current = true;
548
+ trackFirstRunStepSkipped(
549
+ "connecting",
550
+ "builder_provision_failed",
551
+ extensionIndex,
552
+ extensionStepIndex,
553
+ true,
554
+ );
555
+ if (setupSkippable) completionRedirectRef.current = "/record";
556
+ handleFinish(null);
557
+ }, [
558
+ extensionIndex,
559
+ extensionStepIndex,
560
+ handleFinish,
561
+ setupSkippable,
562
+ trackFirstRunStepSkipped,
563
+ ]);
564
+ const handleBuilderConnected = useCallback(() => {
565
+ trackFirstRunSetupOutcome(builderSetupAttemptRef.current, "connected");
566
+ builderSetupAttemptRef.current = null;
567
+ setupAttemptRef.current = null;
568
+ trackFirstRunStepCompleted("choice");
569
+ trackFirstRunStepCompleted("connecting");
570
+ handleFinish(null);
571
+ }, [handleFinish, trackFirstRunStepCompleted]);
572
+ const connectFlow = useBuilderConnectFlow({
573
+ enabled: firstRun && !previewMode,
574
+ provisionAccount: true,
575
+ trackingSource: "first_run_onboarding",
576
+ trackingFlow: "connect_llm",
577
+ onConnected: handleBuilderConnected,
578
+ });
579
+ const accountExists = connectFlow.accountExists && !connectFlow.connecting;
580
+ const provisioning = builderConnectionMode === "provision" && !accountExists;
581
+ const provisioningFailed =
582
+ provisioning &&
583
+ !connectFlow.connecting &&
584
+ Boolean(connectFlow.terminalError);
585
+ const connectionFailed = provisioningFailed;
586
+ useEffect(() => {
587
+ if (
588
+ previewMode ||
589
+ !firstRun ||
590
+ !canTrackOnboardingScreen ||
591
+ screen !== "choice" ||
592
+ !creditsV2Enabled ||
593
+ setupExperimentDecision !== true ||
594
+ creditsV2ExposureTrackedRef.current ||
595
+ !creditsV2Assignment
596
+ ) {
597
+ return;
598
+ }
599
+ creditsV2ExposureTrackedRef.current = true;
600
+ setOnboardingExperiment({
601
+ id: creditsV2Assignment.experiment_id,
602
+ variant: creditsV2Assignment.experiment_variant,
603
+ });
604
+ setCreditsV2ExposureTracked(true);
605
+ trackOnboardingEvent("onboarding_experiment_exposed", {
606
+ flow: "first_run",
607
+ step_id: "choice",
608
+ experiment_id: creditsV2Assignment.experiment_id,
609
+ experiment_variant: creditsV2Assignment.experiment_variant,
610
+ });
611
+ if (creditsV2ExposureConfirmationStartedRef.current) return;
612
+ creditsV2ExposureConfirmationStartedRef.current = true;
613
+ const persistExposure = async () => {
614
+ let lastError: unknown;
615
+ // A write can commit even when its response is lost, so retry the idempotent assignment to reconcile it.
616
+ for (const delayMs of [0, 0, 1_000, 5_000, 30_000]) {
617
+ if (delayMs > 0) {
618
+ await new Promise<void>((resolve) =>
619
+ window.setTimeout(resolve, delayMs),
620
+ );
621
+ }
622
+ try {
623
+ const { assignment } = await creditsV2Exposure.mutateAsync({
624
+ confirmedVariant: creditsV2Assignment.experiment_variant,
625
+ });
626
+ if (
627
+ assignment?.experiment_id !== creditsV2Assignment.experiment_id ||
628
+ assignment.experiment_variant !==
629
+ creditsV2Assignment.experiment_variant
630
+ ) {
631
+ throw new Error(
632
+ "The server did not persist the displayed first-run Builder credits v2 assignment.",
633
+ );
634
+ }
635
+ return;
636
+ } catch (error: unknown) {
637
+ lastError = error;
638
+ }
639
+ }
640
+ console.error(
641
+ "Failed to persist first-run Builder credits v2 exposure:",
642
+ lastError,
643
+ );
644
+ };
645
+ void persistExposure();
646
+ }, [
647
+ canTrackOnboardingScreen,
648
+ creditsV2Assignment,
649
+ creditsV2Enabled,
650
+ creditsV2Exposure.mutateAsync,
651
+ firstRun,
652
+ previewMode,
653
+ setupExperimentDecision,
654
+ screen,
655
+ ]);
448
656
  useEffect(() => {
449
657
  if (
450
658
  previewMode ||
@@ -463,8 +671,14 @@ export function FirstRunOnboarding({
463
671
  extensionIndex,
464
672
  extensionStepIndex,
465
673
  choiceSkippable,
674
+ provisioningFailed,
466
675
  );
467
- const key = [step.step_id, step.extension_id, step.step_index].join(":");
676
+ const key = [
677
+ step.step_id,
678
+ step.extension_id,
679
+ step.step_index,
680
+ step.optional,
681
+ ].join(":");
468
682
  if (stepViewRef.current?.key !== key) {
469
683
  stepViewRef.current = { key, id: createOnboardingCorrelationId() };
470
684
  }
@@ -485,6 +699,7 @@ export function FirstRunOnboarding({
485
699
  previewMode,
486
700
  canTrackOnboardingScreen,
487
701
  choiceStepKnown,
702
+ provisioningFailed,
488
703
  screen,
489
704
  ]);
490
705
  useEffect(() => {
@@ -507,6 +722,7 @@ export function FirstRunOnboarding({
507
722
  extensionIndex,
508
723
  extensionStepIndex,
509
724
  choiceSkippable,
725
+ provisioningFailed,
510
726
  ),
511
727
  reason: "page_exit",
512
728
  });
@@ -524,46 +740,8 @@ export function FirstRunOnboarding({
524
740
  canTrackOnboardingScreen,
525
741
  screen,
526
742
  choiceSkippable,
743
+ provisioningFailed,
527
744
  ]);
528
- const beginBeforeSetup = useCallback(() => {
529
- if (beforeSetupExtensions.length === 0) {
530
- setScreen("choice");
531
- return;
532
- }
533
- setExtensionPlacement("before-setup");
534
- setExtensionIndex(0);
535
- setExtensionStepIndex(0);
536
- setScreen("extension");
537
- }, [beforeSetupExtensions.length]);
538
- const handleFinish = useCallback(
539
- (completedScreen: FirstRunScreen | null, track = true) => {
540
- if (afterSetupExtensions.length === 0) {
541
- void finishOnboarding(completedScreen);
542
- return;
543
- }
544
- if (completedScreen && track) trackFirstRunStepCompleted(completedScreen);
545
- setExtensionPlacement("after-setup");
546
- setExtensionIndex(0);
547
- setExtensionStepIndex(0);
548
- setScreen("extension");
549
- },
550
- [afterSetupExtensions.length, finishOnboarding, trackFirstRunStepCompleted],
551
- );
552
- const handleBuilderConnected = useCallback(() => {
553
- trackFirstRunSetupOutcome(builderSetupAttemptRef.current, "connected");
554
- builderSetupAttemptRef.current = null;
555
- setupAttemptRef.current = null;
556
- trackFirstRunStepCompleted("choice");
557
- trackFirstRunStepCompleted("connecting");
558
- handleFinish(null);
559
- }, [handleFinish, trackFirstRunStepCompleted]);
560
- const connectFlow = useBuilderConnectFlow({
561
- enabled: firstRun && !previewMode,
562
- provisionAccount: true,
563
- trackingSource: "first_run_onboarding",
564
- trackingFlow: "connect_llm",
565
- onConnected: handleBuilderConnected,
566
- });
567
745
  const builderStatusReadCount = connectFlow.statusReadSettledCount ?? 0;
568
746
  useEffect(() => {
569
747
  const startedAt = retryingBuilderStatusAtCountRef.current;
@@ -587,12 +765,13 @@ export function FirstRunOnboarding({
587
765
  trackFirstRunSetupOutcome(attempt, "failed", "account_exists");
588
766
  return;
589
767
  }
590
- if (connectFlow.terminalError) {
768
+ if (connectFlow.terminalError || connectFlow.statusUnavailable) {
591
769
  trackFirstRunSetupOutcome(attempt, "failed", "connection_error");
592
770
  }
593
771
  }, [
594
772
  connectFlow.accountExists,
595
773
  connectFlow.connecting,
774
+ connectFlow.statusUnavailable,
596
775
  connectFlow.terminalError,
597
776
  ]);
598
777
  // Preview never fetches status, so show the activation path it would offer.
@@ -608,12 +787,15 @@ export function FirstRunOnboarding({
608
787
  const setupExperimentEligible =
609
788
  setupStatusSettled &&
610
789
  canActivateBuilderFreeCredits &&
611
- !connectFlow.configured;
790
+ !connectFlow.configured &&
791
+ !connectFlow.statusUnavailable;
612
792
  useEffect(() => {
613
793
  if (
614
794
  previewMode ||
615
795
  !firstRun ||
616
796
  screen !== "choice" ||
797
+ creditsV2FlagPending ||
798
+ creditsV2FlowEnabled ||
617
799
  !setupVariant ||
618
800
  !setupStatusSettled ||
619
801
  setupExperimentDecision !== null
@@ -622,12 +804,14 @@ export function FirstRunOnboarding({
622
804
  }
623
805
  if (setupExperimentEligible) {
624
806
  setOnboardingExperiment({
625
- id: FIRST_RUN_SETUP_EXPERIMENT_ID,
807
+ id: setupExperimentId,
626
808
  variant: setupVariant,
627
809
  });
628
810
  trackOnboardingEvent("onboarding_experiment_exposed", {
629
811
  flow: "first_run",
630
812
  step_id: "choice",
813
+ experiment_id: setupExperimentId,
814
+ experiment_variant: setupVariant,
631
815
  });
632
816
  }
633
817
  setSetupExperimentDecision(setupExperimentEligible);
@@ -635,11 +819,73 @@ export function FirstRunOnboarding({
635
819
  firstRun,
636
820
  previewMode,
637
821
  screen,
822
+ creditsV2FlagPending,
823
+ creditsV2FlowEnabled,
638
824
  setupExperimentEligible,
639
825
  setupExperimentDecision,
640
826
  setupStatusSettled,
827
+ setupExperimentId,
641
828
  setupVariant,
642
829
  ]);
830
+ useEffect(() => {
831
+ if (
832
+ previewMode ||
833
+ !firstRun ||
834
+ screen !== "choice" ||
835
+ !creditsV2Enabled ||
836
+ !setupStatusSettled ||
837
+ setupExperimentTimedOutRef.current ||
838
+ setupExperimentDecision === true
839
+ ) {
840
+ return;
841
+ }
842
+ if (!setupExperimentEligible) {
843
+ creditsV2ExposureFailedRef.current = false;
844
+ setSetupExperimentDecision(false);
845
+ return;
846
+ }
847
+ if (creditsV2ExposureFailedRef.current) return;
848
+ if (creditsV2ExposureStartedRef.current) return;
849
+ creditsV2ExposureStartedRef.current = true;
850
+ creditsV2ExposureSettledRef.current = false;
851
+ setSetupExperimentDecision(null);
852
+ void creditsV2Exposure
853
+ .mutateAsync({})
854
+ .then(({ assignment }) => {
855
+ creditsV2ExposureSettledRef.current = true;
856
+ if (setupExperimentTimedOutRef.current) return;
857
+ if (
858
+ assignment?.experiment_id !==
859
+ FIRST_RUN_BUILDER_CREDITS_V2_EXPERIMENT_ID ||
860
+ (assignment.experiment_variant !== "control" &&
861
+ assignment.experiment_variant !== "credits")
862
+ ) {
863
+ setSetupExperimentDecision(false);
864
+ return;
865
+ }
866
+ setCreditsV2Assignment({
867
+ experiment_id: FIRST_RUN_BUILDER_CREDITS_V2_EXPERIMENT_ID,
868
+ experiment_variant: assignment.experiment_variant,
869
+ });
870
+ setSetupExperimentDecision(true);
871
+ })
872
+ .catch(() => {
873
+ creditsV2ExposureSettledRef.current = true;
874
+ if (setupExperimentTimedOutRef.current) return;
875
+ creditsV2ExposureStartedRef.current = false;
876
+ creditsV2ExposureFailedRef.current = true;
877
+ setSetupExperimentDecision(false);
878
+ });
879
+ }, [
880
+ creditsV2Enabled,
881
+ creditsV2Exposure.mutateAsync,
882
+ firstRun,
883
+ previewMode,
884
+ screen,
885
+ setupExperimentDecision,
886
+ setupExperimentEligible,
887
+ setupStatusSettled,
888
+ ]);
643
889
  // A stalled Builder status read must not leave the setup screen blank. Past
644
890
  // the limit the person gets the existing page and stays out of the
645
891
  // experiment, identically in both arms, so exposure counts stay honest.
@@ -648,17 +894,28 @@ export function FirstRunOnboarding({
648
894
  previewMode ||
649
895
  !firstRun ||
650
896
  screen !== "choice" ||
651
- !setupVariant ||
897
+ creditsV2FlagPending ||
898
+ (!setupVariant && !creditsV2FlowEnabled) ||
652
899
  setupExperimentDecision !== null
653
900
  ) {
654
901
  return;
655
902
  }
656
- const timer = window.setTimeout(
657
- () => setSetupExperimentDecision((decision) => decision ?? false),
658
- SETUP_EXPERIMENT_DECISION_TIMEOUT_MS,
659
- );
903
+ const timer = window.setTimeout(() => {
904
+ if (creditsV2ExposureSettledRef.current) return;
905
+ setupExperimentTimedOutRef.current = true;
906
+ setSetupExperimentDecision((decision) => decision ?? false);
907
+ }, SETUP_EXPERIMENT_DECISION_TIMEOUT_MS);
660
908
  return () => window.clearTimeout(timer);
661
- }, [firstRun, previewMode, screen, setupExperimentDecision, setupVariant]);
909
+ }, [
910
+ creditsV2FlowEnabled,
911
+ creditsV2FlagPending,
912
+ firstRun,
913
+ previewMode,
914
+ screen,
915
+ setupExperimentDecision,
916
+ setupStatusSettled,
917
+ setupVariant,
918
+ ]);
662
919
  const retryOnboardingCompletion = useCallback(() => {
663
920
  const attempt = completionAttemptRef.current;
664
921
  void finishOnboarding(
@@ -887,9 +1144,12 @@ export function FirstRunOnboarding({
887
1144
  );
888
1145
 
889
1146
  const inCreditsArm = setupVariant === "credits";
890
- // Both arms must wait: the control setup actions can be used as soon as
891
- // they render, so exposure has to be recorded before either arm is usable.
892
- if (!previewMode && setupVariant && setupExperimentDecision === null) {
1147
+ if (
1148
+ !previewMode &&
1149
+ (creditsV2FlagPending ||
1150
+ (creditsV2FlowEnabled && setupExperimentDecision === null) ||
1151
+ (setupVariant && setupExperimentDecision === null))
1152
+ ) {
893
1153
  return <OnboardingSkeleton />;
894
1154
  }
895
1155
  const creditsEligible =
@@ -1093,6 +1353,17 @@ export function FirstRunOnboarding({
1093
1353
  >
1094
1354
  {t("agentChat.onboarding.builderExistingAccount")}
1095
1355
  </button>
1356
+ {creditsV2FlowEnabled && (
1357
+ <button
1358
+ type="button"
1359
+ data-testid="first-run-credits-own-api-keys"
1360
+ className="min-h-7 text-xs font-medium text-muted-foreground underline underline-offset-4 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1361
+ onClick={() => void handleOpenSettings()}
1362
+ disabled={connectFlow.connecting}
1363
+ >
1364
+ {t("agentChat.onboarding.creditsUseOwnApiKeys")}
1365
+ </button>
1366
+ )}
1096
1367
  <p className="text-xs leading-5 text-muted-foreground">
1097
1368
  {t("agentChat.onboarding.creditsFinePrint")}{" "}
1098
1369
  <a
@@ -1141,74 +1412,88 @@ export function FirstRunOnboarding({
1141
1412
  </div>
1142
1413
  )}
1143
1414
  <div className="fixed bottom-6 right-6 z-10">
1144
- <Popover open={skipConfirmOpen} onOpenChange={setSkipConfirmOpen}>
1145
- <PopoverTrigger asChild>
1146
- <button
1147
- type="button"
1148
- data-testid="first-run-credits-skip"
1149
- className="inline-flex min-h-6 items-center px-2 rounded-sm text-xs font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1150
- >
1151
- {t("agentChat.onboarding.creditsSkip")}
1152
- </button>
1153
- </PopoverTrigger>
1154
- <PopoverContent
1155
- side="top"
1156
- align="end"
1157
- portalled={false}
1158
- aria-labelledby="first-run-credits-skip-title"
1159
- aria-describedby="first-run-credits-skip-body"
1160
- className="w-96 max-w-[calc(100vw-2rem)] space-y-3 p-4 text-left"
1161
- onOpenAutoFocus={(event) => {
1162
- event.preventDefault();
1163
- skipGoBackRef.current?.focus();
1164
- }}
1415
+ {creditsV2FlowEnabled ? (
1416
+ <button
1417
+ type="button"
1418
+ data-testid="first-run-credits-skip"
1419
+ className="inline-flex min-h-6 items-center px-2 rounded-sm text-xs font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1420
+ onClick={skipSetup}
1165
1421
  >
1166
- <div className="space-y-1">
1167
- <p
1168
- id="first-run-credits-skip-title"
1169
- className="text-sm font-semibold text-foreground"
1170
- >
1171
- {t("agentChat.onboarding.creditsSkipConfirmTitle")}
1172
- </p>
1173
- <p
1174
- id="first-run-credits-skip-body"
1175
- className="text-xs leading-5 text-muted-foreground"
1176
- >
1177
- {t("agentChat.onboarding.creditsSkipConfirmBody")}
1178
- </p>
1179
- </div>
1180
- <div className="flex flex-wrap items-center justify-between gap-2">
1181
- <Button
1422
+ {t("agentChat.onboarding.creditsSkip")}
1423
+ </button>
1424
+ ) : (
1425
+ <Popover
1426
+ open={skipConfirmOpen}
1427
+ onOpenChange={setSkipConfirmOpen}
1428
+ >
1429
+ <PopoverTrigger asChild>
1430
+ <button
1182
1431
  type="button"
1183
- variant="ghost"
1184
- size="sm"
1185
- className="px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
1186
- onClick={() => void handleOpenSettings()}
1432
+ data-testid="first-run-credits-skip"
1433
+ className="inline-flex min-h-6 items-center px-2 rounded-sm text-xs font-medium text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1187
1434
  >
1188
- {t("agentChat.onboarding.creditsUseCustomKeys")}
1189
- </Button>
1190
- <div className="flex flex-wrap justify-end gap-2">
1191
- <Button
1192
- type="button"
1193
- variant="secondary"
1194
- size="sm"
1195
- data-testid="first-run-credits-skip-confirm"
1196
- onClick={skipSetup}
1435
+ {t("agentChat.onboarding.creditsSkip")}
1436
+ </button>
1437
+ </PopoverTrigger>
1438
+ <PopoverContent
1439
+ side="top"
1440
+ align="end"
1441
+ portalled={false}
1442
+ aria-labelledby="first-run-credits-skip-title"
1443
+ aria-describedby="first-run-credits-skip-body"
1444
+ className="w-96 max-w-[calc(100vw-2rem)] space-y-3 p-4 text-left"
1445
+ onOpenAutoFocus={(event) => {
1446
+ event.preventDefault();
1447
+ skipGoBackRef.current?.focus();
1448
+ }}
1449
+ >
1450
+ <div className="space-y-1">
1451
+ <p
1452
+ id="first-run-credits-skip-title"
1453
+ className="text-sm font-semibold text-foreground"
1197
1454
  >
1198
- {t("agentChat.onboarding.creditsSkipAnyway")}
1199
- </Button>
1455
+ {t("agentChat.onboarding.creditsSkipConfirmTitle")}
1456
+ </p>
1457
+ <p
1458
+ id="first-run-credits-skip-body"
1459
+ className="text-xs leading-5 text-muted-foreground"
1460
+ >
1461
+ {t("agentChat.onboarding.creditsSkipConfirmBody")}
1462
+ </p>
1463
+ </div>
1464
+ <div className="flex flex-wrap items-center justify-between gap-2">
1200
1465
  <Button
1201
1466
  type="button"
1467
+ variant="ghost"
1202
1468
  size="sm"
1203
- ref={skipGoBackRef}
1204
- onClick={() => setSkipConfirmOpen(false)}
1469
+ className="px-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
1470
+ onClick={() => void handleOpenSettings()}
1205
1471
  >
1206
- {t("agentChat.onboarding.creditsSkipGoBack")}
1472
+ {t("agentChat.onboarding.creditsUseCustomKeys")}
1207
1473
  </Button>
1474
+ <div className="flex flex-wrap justify-end gap-2">
1475
+ <Button
1476
+ type="button"
1477
+ variant="secondary"
1478
+ size="sm"
1479
+ data-testid="first-run-credits-skip-confirm"
1480
+ onClick={skipSetup}
1481
+ >
1482
+ {t("agentChat.onboarding.creditsSkipAnyway")}
1483
+ </Button>
1484
+ <Button
1485
+ type="button"
1486
+ size="sm"
1487
+ ref={skipGoBackRef}
1488
+ onClick={() => setSkipConfirmOpen(false)}
1489
+ >
1490
+ {t("agentChat.onboarding.creditsSkipGoBack")}
1491
+ </Button>
1492
+ </div>
1208
1493
  </div>
1209
- </div>
1210
- </PopoverContent>
1211
- </Popover>
1494
+ </PopoverContent>
1495
+ </Popover>
1496
+ )}
1212
1497
  </div>
1213
1498
  </div>
1214
1499
  </OnboardingShell>
@@ -1375,8 +1660,6 @@ export function FirstRunOnboarding({
1375
1660
  );
1376
1661
  }
1377
1662
 
1378
- const accountExists = connectFlow.accountExists && !connectFlow.connecting;
1379
- const provisioning = builderConnectionMode === "provision" && !accountExists;
1380
1663
  const returnToSetupChoice = () => setScreen("choice");
1381
1664
  return (
1382
1665
  <OnboardingShell
@@ -1391,23 +1674,37 @@ export function FirstRunOnboarding({
1391
1674
  aria-live="polite"
1392
1675
  aria-busy={connectFlow.connecting}
1393
1676
  >
1394
- <div className="flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary">
1395
- {accountExists ? (
1396
- <IconKey size={19} />
1397
- ) : (
1398
- <IconLoader2 className="animate-spin" size={19} />
1677
+ <div
1678
+ className={cn(
1679
+ "flex size-10 items-center justify-center rounded-full",
1680
+ connectionFailed
1681
+ ? "bg-destructive/10 text-destructive"
1682
+ : "bg-primary/10 text-primary",
1399
1683
  )}
1684
+ >
1685
+ {accountExists ? <IconKey size={19} /> : null}
1686
+ {connectionFailed ? <IconAlertCircle size={19} /> : null}
1687
+ {!accountExists && !connectionFailed ? (
1688
+ <IconLoader2 className="animate-spin" size={19} />
1689
+ ) : null}
1400
1690
  </div>
1401
1691
  <h1 className="mt-5 text-xl font-semibold tracking-[-0.04em]">
1402
1692
  {accountExists
1403
1693
  ? t("agentChat.onboarding.builderAccountExistsTitle")
1404
- : provisioning
1405
- ? t("agentChat.onboarding.creditsActivating")
1406
- : t("agentChat.onboarding.builderConnecting")}
1694
+ : provisioningFailed
1695
+ ? t("agentChat.settingsShell.builder.setupFailed")
1696
+ : provisioning
1697
+ ? t("agentChat.onboarding.creditsActivating")
1698
+ : t("agentChat.onboarding.builderConnecting")}
1407
1699
  </h1>
1408
- <p className="mt-2 text-sm text-muted-foreground">
1700
+ <p
1701
+ className="mt-2 text-sm text-muted-foreground"
1702
+ role={connectionFailed ? "alert" : undefined}
1703
+ >
1409
1704
  {accountExists ? (
1410
1705
  t("agentChat.onboarding.builderAccountExistsDescription")
1706
+ ) : provisioningFailed ? (
1707
+ t("agentChat.onboarding.builderProvisionFailedDetail")
1411
1708
  ) : provisioning ? (
1412
1709
  <>
1413
1710
  {t("agentChat.onboarding.builderProvisioningDescription")}
@@ -1444,18 +1741,20 @@ export function FirstRunOnboarding({
1444
1741
  </button>
1445
1742
  ) : (
1446
1743
  <>
1447
- <div className="mt-7 w-full rounded-xl bg-muted/35 p-4 text-left">
1448
- <div className="flex items-center justify-between gap-3">
1449
- <Skeleton className="h-3 w-28" />
1450
- <Skeleton className="h-5 w-16 rounded-full" />
1451
- </div>
1452
- <Skeleton className="mt-4 h-8 w-full" />
1453
- <div className="mt-3 grid grid-cols-3 gap-2">
1454
- <Skeleton className="h-7 w-full" />
1455
- <Skeleton className="h-7 w-full" />
1456
- <Skeleton className="h-7 w-full" />
1744
+ {connectFlow.connecting ? (
1745
+ <div className="mt-7 w-full rounded-xl bg-muted/35 p-4 text-left">
1746
+ <div className="flex items-center justify-between gap-3">
1747
+ <Skeleton className="h-3 w-28" />
1748
+ <Skeleton className="h-5 w-16 rounded-full" />
1749
+ </div>
1750
+ <Skeleton className="mt-4 h-8 w-full" />
1751
+ <div className="mt-3 grid grid-cols-3 gap-2">
1752
+ <Skeleton className="h-7 w-full" />
1753
+ <Skeleton className="h-7 w-full" />
1754
+ <Skeleton className="h-7 w-full" />
1755
+ </div>
1457
1756
  </div>
1458
- </div>
1757
+ ) : null}
1459
1758
  {connectFlow.connecting &&
1460
1759
  (builderCancelRequested ? (
1461
1760
  <div className="mt-4 flex flex-col items-center gap-2">
@@ -1502,6 +1801,7 @@ export function FirstRunOnboarding({
1502
1801
  </p>
1503
1802
  )}
1504
1803
  {!connectFlow.connecting &&
1804
+ !provisioningFailed &&
1505
1805
  (connectFlow.statusUnavailable || connectFlow.terminalError) && (
1506
1806
  <div className="mt-4 flex flex-col items-center gap-2">
1507
1807
  <p className="text-xs text-destructive" role="alert">
@@ -1529,6 +1829,34 @@ export function FirstRunOnboarding({
1529
1829
  </button>
1530
1830
  </div>
1531
1831
  )}
1832
+ {provisioningFailed ? (
1833
+ <div className="mt-4 flex w-full flex-col items-center gap-2">
1834
+ <button
1835
+ type="button"
1836
+ data-testid="first-run-retry-builder"
1837
+ className={cn(ONBOARDING_PRIMARY_BUTTON_CLASS, "w-full")}
1838
+ onClick={() => handleBuilder(true)}
1839
+ >
1840
+ {t("agentChat.settingsShell.builder.retry")}
1841
+ </button>
1842
+ <button
1843
+ type="button"
1844
+ data-testid="first-run-open-key-settings-after-failure"
1845
+ className={cn(secondaryButtonClass, "w-full")}
1846
+ onClick={() => void handleOpenSettings()}
1847
+ >
1848
+ {t("agentChat.onboarding.openAiKeySettings")}
1849
+ </button>
1850
+ <button
1851
+ type="button"
1852
+ data-testid="first-run-skip-builder-after-failure"
1853
+ className="inline-flex min-h-9 items-center justify-center rounded-lg px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1854
+ onClick={skipFailedBuilderSetup}
1855
+ >
1856
+ {t("agentChat.onboarding.skipForNow")}
1857
+ </button>
1858
+ </div>
1859
+ ) : null}
1532
1860
  {!connectFlow.connecting &&
1533
1861
  !connectFlow.statusUnavailable &&
1534
1862
  !connectFlow.terminalError && (