From 6a944e707964a97f8f53fc973b27a2d3bccdf569 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 15:55:52 -0700 Subject: [PATCH 1/9] fix: make first-run Builder setup recoverable --- .../toolkit/src/app/i18n/catalogs/ar-SA.ts | 4 +- .../toolkit/src/app/i18n/catalogs/de-DE.ts | 4 +- .../toolkit/src/app/i18n/catalogs/en-US.ts | 4 +- .../toolkit/src/app/i18n/catalogs/es-ES.ts | 5 +- .../toolkit/src/app/i18n/catalogs/fr-FR.ts | 5 +- .../toolkit/src/app/i18n/catalogs/hi-IN.ts | 4 +- .../toolkit/src/app/i18n/catalogs/ja-JP.ts | 4 +- .../toolkit/src/app/i18n/catalogs/ko-KR.ts | 4 +- .../toolkit/src/app/i18n/catalogs/pt-BR.ts | 4 +- .../toolkit/src/app/i18n/catalogs/zh-CN.ts | 4 +- .../toolkit/src/app/i18n/catalogs/zh-TW.ts | 4 +- .../onboarding/FirstRunOnboarding.spec.tsx | 81 ++++++++++++++++++- .../src/app/onboarding/FirstRunOnboarding.tsx | 19 +++++ 13 files changed, 122 insertions(+), 24 deletions(-) diff --git a/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts b/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts index 1b873ac6b8a..dad82f2d289 100644 --- a/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts +++ b/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts @@ -131,8 +131,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "صف دورك", "onboarding.skipForNow": "تخطي الآن", "onboarding.saveRoleError": "تعذر حفظ دورك.", - "onboarding.builderCreateAccount": "استخدم Builder.io", - "onboarding.builderSignInWithAccount": "استخدم Builder.io", + "onboarding.builderCreateAccount": "أنشئ حسابًا على Builder.io", + "onboarding.builderSignInWithAccount": "سجّل الدخول بحساب Builder.io", "onboarding.builderActivateDescription": "أنشئ حساب Builder.io الخاص بك أو أعد استخدامه وفعّل أرصدته المجانية بنقرة واحدة.", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/de-DE.ts b/packages/toolkit/src/app/i18n/catalogs/de-DE.ts index ad5b0d22aab..e11530a7865 100644 --- a/packages/toolkit/src/app/i18n/catalogs/de-DE.ts +++ b/packages/toolkit/src/app/i18n/catalogs/de-DE.ts @@ -137,8 +137,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "Beschreibe deine Rolle", "onboarding.skipForNow": "Vorerst überspringen", "onboarding.saveRoleError": "Deine Rolle konnte nicht gespeichert werden.", - "onboarding.builderCreateAccount": "Builder.io verwenden", - "onboarding.builderSignInWithAccount": "Builder.io verwenden", + "onboarding.builderCreateAccount": "Builder.io-Konto erstellen", + "onboarding.builderSignInWithAccount": "Mit einem Builder.io-Konto anmelden", "onboarding.builderActivateDescription": "Erstelle oder verwende dein Builder.io-Konto erneut und aktiviere seine Gratiscredits mit einem Klick.", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/en-US.ts b/packages/toolkit/src/app/i18n/catalogs/en-US.ts index 9459996b19d..6e2bb7d4336 100644 --- a/packages/toolkit/src/app/i18n/catalogs/en-US.ts +++ b/packages/toolkit/src/app/i18n/catalogs/en-US.ts @@ -129,8 +129,8 @@ const messages = { "onboarding.roleOtherInputLabel": "Describe your role", "onboarding.skipForNow": "Skip for now", "onboarding.saveRoleError": "Could not save your role.", - "onboarding.builderCreateAccount": "Use Builder.io", - "onboarding.builderSignInWithAccount": "Use Builder.io", + "onboarding.builderCreateAccount": "Create a Builder.io account", + "onboarding.builderSignInWithAccount": "Sign in with a Builder.io account", "onboarding.builderActivateDescription": "Create or reuse your Builder.io account and activate its free credits in one click.", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/es-ES.ts b/packages/toolkit/src/app/i18n/catalogs/es-ES.ts index 547ac4f828c..7adba30a6c1 100644 --- a/packages/toolkit/src/app/i18n/catalogs/es-ES.ts +++ b/packages/toolkit/src/app/i18n/catalogs/es-ES.ts @@ -133,8 +133,9 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "Describe tu función", "onboarding.skipForNow": "Omitir por ahora", "onboarding.saveRoleError": "No se pudo guardar tu rol.", - "onboarding.builderCreateAccount": "Usar Builder.io", - "onboarding.builderSignInWithAccount": "Usar Builder.io", + "onboarding.builderCreateAccount": "Crear una cuenta de Builder.io", + "onboarding.builderSignInWithAccount": + "Iniciar sesión con una cuenta de Builder.io", "onboarding.builderActivateDescription": "Crea o reutiliza tu cuenta de Builder.io y activa sus créditos gratuitos con un solo clic.", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts b/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts index 8ff3ecc691c..b529e06ecc2 100644 --- a/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts +++ b/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts @@ -136,8 +136,9 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "Décrivez votre rôle", "onboarding.skipForNow": "Ignorer pour l’instant", "onboarding.saveRoleError": "Impossible d’enregistrer votre rôle.", - "onboarding.builderCreateAccount": "Utiliser Builder.io", - "onboarding.builderSignInWithAccount": "Utiliser Builder.io", + "onboarding.builderCreateAccount": "Créer un compte Builder.io", + "onboarding.builderSignInWithAccount": + "Se connecter avec un compte Builder.io", "onboarding.builderActivateDescription": "Créez ou réutilisez votre compte Builder.io et activez ses crédits gratuits en un clic.", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts b/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts index f3770f7d586..ace5ce9af97 100644 --- a/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts +++ b/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts @@ -130,8 +130,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "अपनी भूमिका बताएं", "onboarding.skipForNow": "अभी छोड़ें", "onboarding.saveRoleError": "आपकी भूमिका सहेजी नहीं जा सकी।", - "onboarding.builderCreateAccount": "Builder.io इस्तेमाल करें", - "onboarding.builderSignInWithAccount": "Builder.io इस्तेमाल करें", + "onboarding.builderCreateAccount": "Builder.io खाता बनाएँ", + "onboarding.builderSignInWithAccount": "Builder.io खाते से साइन इन करें", "onboarding.builderActivateDescription": "एक क्लिक में अपना Builder.io खाता बनाएँ या फिर से इस्तेमाल करें और उसके मुफ़्त क्रेडिट सक्रिय करें।", "onboarding.builderActiveCredits": "सक्रिय Builder.io मुफ़्त क्रेडिट में शामिल", diff --git a/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts b/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts index 8ce2b860ce5..165c5c44f3e 100644 --- a/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts +++ b/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts @@ -134,8 +134,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "役割を入力してください", "onboarding.skipForNow": "今はスキップ", "onboarding.saveRoleError": "役割を保存できませんでした。", - "onboarding.builderCreateAccount": "Builder.io を使う", - "onboarding.builderSignInWithAccount": "Builder.io を使う", + "onboarding.builderCreateAccount": "Builder.io アカウントを作成", + "onboarding.builderSignInWithAccount": "Builder.io アカウントでサインイン", "onboarding.builderActivateDescription": "Builder.io アカウントを作成または再利用し、ワンクリックで無料クレジットを有効化します。", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts b/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts index 61b7b01fafb..a4e51d68565 100644 --- a/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts +++ b/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts @@ -131,8 +131,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "역할을 입력해 주세요", "onboarding.skipForNow": "지금 건너뛰기", "onboarding.saveRoleError": "역할을 저장하지 못했습니다.", - "onboarding.builderCreateAccount": "Builder.io 사용", - "onboarding.builderSignInWithAccount": "Builder.io 사용", + "onboarding.builderCreateAccount": "Builder.io 계정 만들기", + "onboarding.builderSignInWithAccount": "Builder.io 계정으로 로그인", "onboarding.builderActivateDescription": "한 번의 클릭으로 Builder.io 계정을 생성하거나 재사용하고 무료 크레딧을 활성화합니다.", "onboarding.builderActiveCredits": "활성 Builder.io 무료 크레딧에 포함", diff --git a/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts b/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts index 1451f91f69a..acfba66eeae 100644 --- a/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts +++ b/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts @@ -132,8 +132,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "Descreva sua função", "onboarding.skipForNow": "Pular por enquanto", "onboarding.saveRoleError": "Não foi possível salvar sua função.", - "onboarding.builderCreateAccount": "Usar Builder.io", - "onboarding.builderSignInWithAccount": "Usar Builder.io", + "onboarding.builderCreateAccount": "Criar uma conta do Builder.io", + "onboarding.builderSignInWithAccount": "Entrar com uma conta do Builder.io", "onboarding.builderActivateDescription": "Crie ou reutilize sua conta do Builder.io e ative os créditos gratuitos com um clique.", "onboarding.builderActiveCredits": diff --git a/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts b/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts index b79ce787cde..45416b1c75f 100644 --- a/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts +++ b/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts @@ -126,8 +126,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "描述你的角色", "onboarding.skipForNow": "暂时跳过", "onboarding.saveRoleError": "无法保存你的角色。", - "onboarding.builderCreateAccount": "使用 Builder.io", - "onboarding.builderSignInWithAccount": "使用 Builder.io", + "onboarding.builderCreateAccount": "创建 Builder.io 账户", + "onboarding.builderSignInWithAccount": "使用 Builder.io 账户登录", "onboarding.builderActivateDescription": "一键创建或重新使用您的 Builder.io 账户,并激活免费额度。", "onboarding.builderActiveCredits": "包含在有效的 Builder.io 免费额度中", diff --git a/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts b/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts index b7c02feb123..7ca8ba0c509 100644 --- a/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts +++ b/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts @@ -127,8 +127,8 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.roleOtherInputLabel": "描述你的角色", "onboarding.skipForNow": "暫時略過", "onboarding.saveRoleError": "無法儲存你的角色。", - "onboarding.builderCreateAccount": "使用 Builder.io", - "onboarding.builderSignInWithAccount": "使用 Builder.io", + "onboarding.builderCreateAccount": "建立 Builder.io 帳戶", + "onboarding.builderSignInWithAccount": "使用 Builder.io 帳戶登入", "onboarding.builderActivateDescription": "只要按一下即可建立或重新使用您的 Builder.io 帳戶,並啟用免費額度。", "onboarding.builderActiveCredits": "包含於有效的 Builder.io 免費額度", diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index 09b3f51c039..4522c344fe0 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -531,7 +531,7 @@ describe("FirstRunOnboarding", () => { expect( document.body.querySelector('[data-testid="first-run-builder-sign-in"]') ?.textContent, - ).toBe("Use Builder.io"); + ).toBe("Sign in with a Builder.io account"); expect( document.body .querySelector('[data-testid="first-run-builder-sign-in"]') @@ -806,7 +806,11 @@ describe("FirstRunOnboarding", () => { document.body.querySelector( '[data-testid="first-run-builder-create-account"]', )?.textContent, - ).toBe("Use Builder.io"); + ).toBe("Create a Builder.io account"); + expect( + document.body.querySelector('[data-testid="first-run-builder-sign-in"]') + ?.textContent, + ).toBe("Sign in with a Builder.io account"); expect( document.body .querySelector('[data-testid="first-run-builder-create-account"]') @@ -1265,6 +1269,79 @@ describe("FirstRunOnboarding", () => { trackingFlow: "connect_llm", provisionAccount: false, }); + + act(() => { + document.body + .querySelector( + '[data-testid="first-run-back-to-choice"]', + ) + ?.click(); + }); + + expect( + document.body.querySelector('[data-onboarding-screen="choice"]'), + ).toBeTruthy(); + expect( + document.body.querySelector( + '[data-testid="first-run-open-key-settings"]', + ), + ).toBeTruthy(); + }); + + it("returns to setup choices after Builder provisioning fails", () => { + const flow = { + hasFetchedStatus: true, + statusResolved: true, + configured: false, + agentNativeProvisioningEnabled: true, + connecting: false, + terminalError: "Builder account provisioning failed (503).", + error: "Builder account provisioning failed (503).", + start: vi.fn(), + retry: vi.fn(), + }; + mocks.useBuilderConnectFlow.mockReturnValue(flow); + + act(() => { + root.render( + + + , + ); + }); + act(() => { + document.body + .querySelector('[data-testid="first-run-role-skip"]') + ?.click(); + }); + act(() => { + document.body + .querySelector( + '[data-testid="first-run-builder-create-account"]', + ) + ?.click(); + }); + + expect( + document.body.querySelector('[data-testid="first-run-back-to-choice"]'), + ).toBeTruthy(); + + act(() => { + document.body + .querySelector( + '[data-testid="first-run-back-to-choice"]', + ) + ?.click(); + }); + + expect( + document.body.querySelector('[data-onboarding-screen="choice"]'), + ).toBeTruthy(); + expect( + document.body.querySelector( + '[data-testid="first-run-open-key-settings"]', + ), + ).toBeTruthy(); }); it("shows the role step first", () => { diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index 214f3cf05e9..1ac30180462 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -1020,6 +1020,7 @@ export function FirstRunOnboarding({ const accountExists = connectFlow.accountExists; const provisioning = builderConnectionMode === "provision" && !accountExists; + const returnToSetupChoice = () => setScreen("choice"); return ( Try again + )} )} + {accountExists && !connectFlow.connecting && ( + + )} ); From e3a24b0d98f567fb296d772a016878b1df3de2d6 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 16:08:25 -0700 Subject: [PATCH 2/9] fix: clear stale Builder state during sign-in --- .../onboarding/FirstRunOnboarding.spec.tsx | 23 +++++- .../src/app/onboarding/FirstRunOnboarding.tsx | 2 +- .../app/settings/useBuilderStatus.spec.tsx | 79 +++++++++++++++++++ .../src/app/settings/useBuilderStatus.ts | 2 +- 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index 4522c344fe0..dd9bf36328f 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -1242,10 +1242,11 @@ describe("FirstRunOnboarding", () => { )?.click(); }); - mocks.useBuilderConnectFlow.mockReturnValue({ + const accountExistsFlow = { ...flow, accountExists: true, - }); + }; + mocks.useBuilderConnectFlow.mockReturnValue(accountExistsFlow); act(() => { root.render( @@ -1286,6 +1287,24 @@ describe("FirstRunOnboarding", () => { '[data-testid="first-run-open-key-settings"]', ), ).toBeTruthy(); + + accountExistsFlow.start.mockImplementation(() => { + accountExistsFlow.connecting = true; + }); + act(() => { + document.body + .querySelector( + '[data-testid="first-run-builder-sign-in"]', + ) + ?.click(); + }); + + expect(document.body.textContent).not.toContain( + "You already have a Builder.io account", + ); + expect( + document.body.querySelector('[data-testid="first-run-cancel-builder"]'), + ).toBeTruthy(); }); it("returns to setup choices after Builder provisioning fails", () => { diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index 1ac30180462..aeaed5ae7a2 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -1018,7 +1018,7 @@ export function FirstRunOnboarding({ ); } - const accountExists = connectFlow.accountExists; + const accountExists = connectFlow.accountExists && !connectFlow.connecting; const provisioning = builderConnectionMode === "provision" && !accountExists; const returnToSetupChoice = () => setScreen("choice"); return ( diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx index 0832ea9282e..616368d63d4 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx +++ b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx @@ -1641,6 +1641,85 @@ describe("useBuilderConnectFlow", () => { expect(container.textContent).toContain("account-exists"); }); + it("clears existing-account state while starting a normal sign-in", async () => { + setUserAgent("Mozilla/5.0 Chrome/140.0"); + const popup = createPopupStub(); + openSpy.mockReturnValue(popup); + const pendingReads: Array<(response: Response) => void> = []; + vi.mocked(fetch).mockReset(); + vi.mocked(fetch) + .mockResolvedValueOnce( + jsonResponse({ + configured: false, + agentNativeProvisioningEnabled: true, + agentNativeProvisioningToken: provisioningToken, + envManaged: false, + builderEnabled: true, + orgName: null, + connectUrl: signedConnectUrl, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + pendingReads.push(resolve); + }), + ); + + await act(async () => { + root.render(); + await Promise.resolve(); + await Promise.resolve(); + }); + await flushAfterPaint(); + + expect(container.textContent).toContain("account-exists"); + + act(() => { + container.querySelector("button")?.click(); + }); + + expect(container.textContent).toContain( + "connecting resolved no-account-exists", + ); + expect( + container.querySelector('[data-testid="cancel-connect"]'), + ).toBeTruthy(); + expect(pendingReads).toHaveLength(1); + + await act(async () => { + pendingReads[0]?.( + jsonResponse({ + configured: false, + agentNativeProvisioningEnabled: true, + agentNativeProvisioningToken: provisioningToken, + envManaged: false, + builderEnabled: true, + orgName: null, + connectUrl: signedConnectUrl, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }), + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(container.textContent).toContain( + "connecting resolved account-exists", + ); + }); + it("keeps existing-account mode when a login attempt is blocked", async () => { vi.mocked(fetch).mockImplementation(async () => jsonResponse({ diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.ts b/packages/toolkit/src/app/settings/useBuilderStatus.ts index b28e893628a..b03a83ccb17 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.ts +++ b/packages/toolkit/src/app/settings/useBuilderStatus.ts @@ -1249,7 +1249,7 @@ export function useBuilderConnectFlow( }; setConnecting(true); setError(null); - if (provisionAccountForStart) setAccountExists(false); + setAccountExists(false); const origin = getCallbackOrigin() || window.location.origin; const cachedFreshUrl = isFreshSignedConnectUrl( From 9dbbef3ac35efb38cea179761b25898758684f48 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 16:18:47 -0700 Subject: [PATCH 3/9] fix: keep blocked Builder sign-in recoverable --- .../app/settings/useBuilderStatus.spec.tsx | 48 ++++++++++++++----- .../src/app/settings/useBuilderStatus.ts | 12 +++++ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx index 616368d63d4..16acf465b0e 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx +++ b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx @@ -1638,7 +1638,12 @@ describe("useBuilderConnectFlow", () => { await flushAfterPaint(); - expect(container.textContent).toContain("account-exists"); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); }); it("clears existing-account state while starting a normal sign-in", async () => { @@ -1679,15 +1684,23 @@ describe("useBuilderConnectFlow", () => { }); await flushAfterPaint(); - expect(container.textContent).toContain("account-exists"); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); act(() => { container.querySelector("button")?.click(); }); - expect(container.textContent).toContain( - "connecting resolved no-account-exists", - ); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured connecting resolved no-account-exists"); expect( container.querySelector('[data-testid="cancel-connect"]'), ).toBeTruthy(); @@ -1715,12 +1728,15 @@ describe("useBuilderConnectFlow", () => { await Promise.resolve(); }); - expect(container.textContent).toContain( - "connecting resolved account-exists", - ); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured connecting resolved account-exists"); }); - it("keeps existing-account mode when a login attempt is blocked", async () => { + it("keeps the existing-account path available when a login popup is blocked", async () => { vi.mocked(fetch).mockImplementation(async () => jsonResponse({ configured: false, @@ -1747,13 +1763,23 @@ describe("useBuilderConnectFlow", () => { }); await flushAfterPaint(); - expect(container.textContent).toContain("account-exists"); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); await act(async () => { container.querySelector("button")?.click(); }); - expect(container.textContent).toContain("account-exists"); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); expect(container.textContent).toContain("Allow popups and try again."); }); diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.ts b/packages/toolkit/src/app/settings/useBuilderStatus.ts index b03a83ccb17..ff7ed7bdec5 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.ts +++ b/packages/toolkit/src/app/settings/useBuilderStatus.ts @@ -1220,6 +1220,11 @@ export function useBuilderConnectFlow( const scopeForStart = startOptions?.scope ?? null; const provisionAccountForStart = startOptions?.provisionAccount ?? provisionAccount; + const restoreExistingAccount = () => { + if (!provisionAccountForStart && accountExists) { + setAccountExists(true); + } + }; const targetGrantAtStart = scopeForStart ? connectionsRef.current.grants?.[scopeForStart] : undefined; @@ -1449,6 +1454,7 @@ export function useBuilderConnectFlow( if (!openDesktopConnectUrl) { connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError(t("agentChat.settingsShell.builder.setupStartFailed")); return; } @@ -1513,6 +1519,7 @@ export function useBuilderConnectFlow( if (!result?.ok) { connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError( result?.error ?? t("agentChat.settingsShell.builder.setupStartFailed"), @@ -1533,6 +1540,7 @@ export function useBuilderConnectFlow( if (!embeddedWindow) { connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError( "Couldn't open Builder. Allow popups and try again.", "launch", @@ -1588,6 +1596,7 @@ export function useBuilderConnectFlow( if (!mountedRef.current || openedByHost) return; connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError( t("agentChat.settingsShell.builder.setupHostFailed"), "launch", @@ -1677,6 +1686,7 @@ export function useBuilderConnectFlow( } connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError( "Couldn't navigate the Builder popup. Allow popups and try again.", "launch", @@ -1690,6 +1700,7 @@ export function useBuilderConnectFlow( if (!navigateBuilderConnectPopup(opened, trackedFreshUrl)) { connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError( "Couldn't navigate the Builder popup. Allow popups and try again.", "launch", @@ -1714,6 +1725,7 @@ export function useBuilderConnectFlow( statusConnectUrl, trackingFlow, trackingSource, + accountExists, ], ); From f1be6a89337ca07e4fbc3ced15097912ee63022a Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 16:26:43 -0700 Subject: [PATCH 4/9] fix: restore Builder account state after desktop read failure --- .../app/settings/useBuilderStatus.spec.tsx | 73 +++++++++++++++++++ .../src/app/settings/useBuilderStatus.ts | 1 + 2 files changed, 74 insertions(+) diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx index 16acf465b0e..affdc98216e 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx +++ b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx @@ -14,7 +14,9 @@ import { useBuilderConnectFlow, requestBuilderAccountActivation, withBuilderConnectTrackingParams, + type BuilderConnectTransport, type BuilderConnectionScope, + type BuilderStatus, } from "./useBuilderStatus.js"; const toolkitI18nCatalog = createToolkitI18nCatalog({ messages: {} }); @@ -53,6 +55,7 @@ function BuilderConnectProbeContent({ enabled = true, popupUrl, provisionAccount = false, + transport, startProvisionAccount, startScope, onConnected, @@ -60,6 +63,7 @@ function BuilderConnectProbeContent({ enabled?: boolean; popupUrl?: string; provisionAccount?: boolean; + transport?: BuilderConnectTransport; startProvisionAccount?: boolean; startScope?: BuilderConnectionScope; onConnected?: (state: { orgName: string | null }) => void | Promise; @@ -68,6 +72,7 @@ function BuilderConnectProbeContent({ enabled, popupUrl, provisionAccount, + transport, onConnected, }); return ( @@ -1783,6 +1788,74 @@ describe("useBuilderConnectFlow", () => { expect(container.textContent).toContain("Allow popups and try again."); }); + it("restores existing-account state when desktop status refresh fails", async () => { + setUserAgent("Mozilla/5.0 Electron/41.2.2 AgentNativeDesktop/0.1.7"); + const accountExistsStatus: BuilderStatus = { + configured: false, + builderEnabled: true, + agentNativeProvisioningEnabled: true, + agentNativeProvisioningToken: provisioningToken, + envManaged: false, + connectUrl: signedConnectUrl, + appHost: "https://builder.io", + apiHost: "https://api.builder.io", + publicKeyConfigured: false, + privateKeyConfigured: false, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }; + const readStatus: BuilderConnectTransport["readStatus"] = async ({ + connectAttemptId, + }) => (connectAttemptId ? null : accountExistsStatus); + const openConnectUrl = vi.fn(async () => ({ ok: true as const })); + const transport: BuilderConnectTransport = { + readStatus, + activateAccount: async () => ({ ok: true }), + openConnectUrl, + }; + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + await flushAfterPaint(); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); + + await act(async () => { + container.querySelector("button")?.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); + expect( + container.querySelector('[data-testid="status-unavailable"]') + ?.textContent, + ).toBe("unavailable"); + expect(openConnectUrl).not.toHaveBeenCalled(); + }); + it("falls back to the cached signed URL when the click-time status refresh fails", async () => { setUserAgent("Mozilla/5.0 Chrome/140.0"); const popup = createPopupStub(); diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.ts b/packages/toolkit/src/app/settings/useBuilderStatus.ts index ff7ed7bdec5..101b71d1f6b 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.ts +++ b/packages/toolkit/src/app/settings/useBuilderStatus.ts @@ -1469,6 +1469,7 @@ export function useBuilderConnectFlow( markStatusUnavailable(); connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); return; } markStatusAvailable(); From d9975ca75ff317f6dfe8024ee1a6f815116ee1f9 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 16:37:32 -0700 Subject: [PATCH 5/9] fix: restore Builder onboarding after cancelled sign-in --- .../onboarding/FirstRunOnboarding.spec.tsx | 24 ++++++- .../src/app/onboarding/FirstRunOnboarding.tsx | 12 ++++ .../app/settings/useBuilderStatus.spec.tsx | 69 +++++++++++++++++++ .../src/app/settings/useBuilderStatus.ts | 1 + 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index dd9bf36328f..b20bcff6a30 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -578,7 +578,7 @@ describe("FirstRunOnboarding", () => { ); }); - it("lets users cancel a direct Builder connect during first run", () => { + it("lets users cancel a direct Builder connect and return to setup", () => { const flow = { hasFetchedStatus: true, statusResolved: true, @@ -590,6 +590,9 @@ describe("FirstRunOnboarding", () => { cancel: vi.fn(), retry: vi.fn(), }; + flow.cancel.mockImplementation(() => { + flow.connecting = false; + }); flow.start.mockImplementation(() => { flow.connecting = true; }); @@ -619,6 +622,25 @@ describe("FirstRunOnboarding", () => { expect(cancelButton?.textContent).toBe("Cancel"); act(() => cancelButton?.click()); expect(flow.cancel).toHaveBeenCalledOnce(); + + act(() => { + root.render( + + + , + ); + }); + act(() => { + document.body + .querySelector( + '[data-testid="first-run-back-to-choice"]', + ) + ?.click(); + }); + + expect( + document.body.querySelector('[data-onboarding-screen="choice"]'), + ).toBeTruthy(); }); it("keeps Cancel during a failed status poll without offering a fake retry", () => { diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index aeaed5ae7a2..263a6a5f29e 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -1131,6 +1131,18 @@ export function FirstRunOnboarding({ )} + {!connectFlow.connecting && + !connectFlow.statusUnavailable && + !connectFlow.terminalError && ( + + )} )} {accountExists && !connectFlow.connecting && ( diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx index affdc98216e..6e6c9f0197d 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx +++ b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx @@ -1856,6 +1856,75 @@ describe("useBuilderConnectFlow", () => { expect(openConnectUrl).not.toHaveBeenCalled(); }); + it("restores existing-account state when the desktop connect URL is invalid", async () => { + setUserAgent("Mozilla/5.0 Electron/41.2.2 AgentNativeDesktop/0.1.7"); + const accountExistsStatus: BuilderStatus = { + configured: false, + builderEnabled: true, + agentNativeProvisioningEnabled: true, + agentNativeProvisioningToken: provisioningToken, + envManaged: false, + connectUrl: signedConnectUrl, + appHost: "https://builder.io", + apiHost: "https://api.builder.io", + publicKeyConfigured: false, + privateKeyConfigured: false, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }; + const invalidConnectUrlStatus: BuilderStatus = { + ...accountExistsStatus, + connectUrl: "https://builder.io/connect", + connectError: null, + }; + const readStatus: BuilderConnectTransport["readStatus"] = async ({ + connectAttemptId, + }) => (connectAttemptId ? invalidConnectUrlStatus : accountExistsStatus); + const openConnectUrl = vi.fn(async () => ({ ok: true as const })); + const transport: BuilderConnectTransport = { + readStatus, + activateAccount: async () => ({ ok: true }), + openConnectUrl, + }; + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + await flushAfterPaint(); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); + + await act(async () => { + container.querySelector("button")?.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); + expect(openConnectUrl).not.toHaveBeenCalled(); + }); + it("falls back to the cached signed URL when the click-time status refresh fails", async () => { setUserAgent("Mozilla/5.0 Chrome/140.0"); const popup = createPopupStub(); diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.ts b/packages/toolkit/src/app/settings/useBuilderStatus.ts index 101b71d1f6b..92e3ec5592e 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.ts +++ b/packages/toolkit/src/app/settings/useBuilderStatus.ts @@ -1507,6 +1507,7 @@ export function useBuilderConnectFlow( ) { connectStartedAtRef.current = null; setConnecting(false); + restoreExistingAccount(); setError(t("agentChat.settingsShell.builder.setupStartFailed")); return; } From aad54acd8ac021cb324f901e50ddfc4b8dc2fd1f Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 16:45:17 -0700 Subject: [PATCH 6/9] fix: trust refreshed Builder account status --- .../onboarding/FirstRunOnboarding.spec.tsx | 11 +- .../app/settings/useBuilderStatus.spec.tsx | 135 ++++++++++-------- .../src/app/settings/useBuilderStatus.ts | 1 - 3 files changed, 84 insertions(+), 63 deletions(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index b20bcff6a30..e7b0c71033e 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -578,7 +578,7 @@ describe("FirstRunOnboarding", () => { ); }); - it("lets users cancel a direct Builder connect and return to setup", () => { + it("offers a return path after a cancelled Builder connect settles", () => { const flow = { hasFetchedStatus: true, statusResolved: true, @@ -590,9 +590,6 @@ describe("FirstRunOnboarding", () => { cancel: vi.fn(), retry: vi.fn(), }; - flow.cancel.mockImplementation(() => { - flow.connecting = false; - }); flow.start.mockImplementation(() => { flow.connecting = true; }); @@ -622,6 +619,12 @@ describe("FirstRunOnboarding", () => { expect(cancelButton?.textContent).toBe("Cancel"); act(() => cancelButton?.click()); expect(flow.cancel).toHaveBeenCalledOnce(); + expect(flow.connecting).toBe(true); + expect( + document.body.querySelector('[data-testid="first-run-back-to-choice"]'), + ).toBeNull(); + + flow.connecting = false; act(() => { root.render( diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx index 6e6c9f0197d..7829cf8b4b3 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx +++ b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx @@ -1856,74 +1856,93 @@ describe("useBuilderConnectFlow", () => { expect(openConnectUrl).not.toHaveBeenCalled(); }); - it("restores existing-account state when the desktop connect URL is invalid", async () => { - setUserAgent("Mozilla/5.0 Electron/41.2.2 AgentNativeDesktop/0.1.7"); - const accountExistsStatus: BuilderStatus = { - configured: false, - builderEnabled: true, - agentNativeProvisioningEnabled: true, - agentNativeProvisioningToken: provisioningToken, - envManaged: false, - connectUrl: signedConnectUrl, - appHost: "https://builder.io", - apiHost: "https://api.builder.io", - publicKeyConfigured: false, - privateKeyConfigured: false, + it.each([ + { + name: "keeps a fresh existing-account state", connectError: { message: "A Builder account already exists for this email. Log in to connect it.", code: "account_exists", at: Date.now(), }, - }; - const invalidConnectUrlStatus: BuilderStatus = { - ...accountExistsStatus, - connectUrl: "https://builder.io/connect", - connectError: null, - }; - const readStatus: BuilderConnectTransport["readStatus"] = async ({ - connectAttemptId, - }) => (connectAttemptId ? invalidConnectUrlStatus : accountExistsStatus); - const openConnectUrl = vi.fn(async () => ({ ok: true as const })); - const transport: BuilderConnectTransport = { - readStatus, - activateAccount: async () => ({ ok: true }), - openConnectUrl, - }; + expectedAccountState: "account-exists", + }, + { + name: "clears a stale existing-account state", + connectError: undefined, + expectedAccountState: "no-account-exists", + }, + ])( + "uses fresh account state when the desktop connect URL is invalid: $name", + async ({ connectError, expectedAccountState }) => { + setUserAgent("Mozilla/5.0 Electron/41.2.2 AgentNativeDesktop/0.1.7"); + const accountExistsStatus: BuilderStatus = { + configured: false, + builderEnabled: true, + agentNativeProvisioningEnabled: true, + agentNativeProvisioningToken: provisioningToken, + envManaged: false, + connectUrl: signedConnectUrl, + appHost: "https://builder.io", + apiHost: "https://api.builder.io", + publicKeyConfigured: false, + privateKeyConfigured: false, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }; + const invalidConnectUrlStatus: BuilderStatus = { + ...accountExistsStatus, + connectUrl: "https://builder.io/connect", + connectError, + }; + const readStatus: BuilderConnectTransport["readStatus"] = async ({ + connectAttemptId, + }) => (connectAttemptId ? invalidConnectUrlStatus : accountExistsStatus); + const openConnectUrl = vi.fn(async () => ({ ok: true as const })); + const transport: BuilderConnectTransport = { + readStatus, + activateAccount: async () => ({ ok: true }), + openConnectUrl, + }; - await act(async () => { - root.render( - , - ); - await Promise.resolve(); - await Promise.resolve(); - }); - await flushAfterPaint(); + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + await flushAfterPaint(); - expect( - container - .querySelector('[data-testid="status"]') - ?.textContent?.replace(/\s+/g, " ") - .trim(), - ).toBe("not-configured idle resolved account-exists"); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); - await act(async () => { - container.querySelector("button")?.click(); - await Promise.resolve(); - await Promise.resolve(); - }); + await act(async () => { + container.querySelector("button")?.click(); + await Promise.resolve(); + await Promise.resolve(); + }); - expect( - container - .querySelector('[data-testid="status"]') - ?.textContent?.replace(/\s+/g, " ") - .trim(), - ).toBe("not-configured idle resolved account-exists"); - expect(openConnectUrl).not.toHaveBeenCalled(); - }); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe(`not-configured idle resolved ${expectedAccountState}`); + expect(openConnectUrl).not.toHaveBeenCalled(); + }, + ); it("falls back to the cached signed URL when the click-time status refresh fails", async () => { setUserAgent("Mozilla/5.0 Chrome/140.0"); diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.ts b/packages/toolkit/src/app/settings/useBuilderStatus.ts index 92e3ec5592e..101b71d1f6b 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.ts +++ b/packages/toolkit/src/app/settings/useBuilderStatus.ts @@ -1507,7 +1507,6 @@ export function useBuilderConnectFlow( ) { connectStartedAtRef.current = null; setConnecting(false); - restoreExistingAccount(); setError(t("agentChat.settingsShell.builder.setupStartFailed")); return; } From 45a83f5aed5e8528c96d6d6c10b97be459f6a2e8 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 16:56:05 -0700 Subject: [PATCH 7/9] fix: preserve refreshed Builder status on launch failure --- .../app/settings/useBuilderStatus.spec.tsx | 129 ++++++++++++++++++ .../src/app/settings/useBuilderStatus.ts | 7 +- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx index 7829cf8b4b3..10249522d04 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx +++ b/packages/toolkit/src/app/settings/useBuilderStatus.spec.tsx @@ -1944,6 +1944,77 @@ describe("useBuilderConnectFlow", () => { }, ); + it("keeps refreshed account state when desktop popup launch fails", async () => { + setUserAgent("Mozilla/5.0 Electron/41.2.2 AgentNativeDesktop/0.1.7"); + const desktopConnectUrl = "https://builder.io/connect?_an_connect=signed"; + const accountExistsStatus: BuilderStatus = { + configured: false, + builderEnabled: true, + agentNativeProvisioningEnabled: true, + agentNativeProvisioningToken: provisioningToken, + envManaged: false, + connectUrl: desktopConnectUrl, + appHost: "https://builder.io", + apiHost: "https://api.builder.io", + publicKeyConfigured: false, + privateKeyConfigured: false, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }; + const refreshedStatus: BuilderStatus = { + ...accountExistsStatus, + connectError: undefined, + }; + const readStatus: BuilderConnectTransport["readStatus"] = async ({ + connectAttemptId, + }) => (connectAttemptId ? refreshedStatus : accountExistsStatus); + const transport: BuilderConnectTransport = { + readStatus, + activateAccount: async () => ({ ok: true }), + openConnectUrl: async () => ({ + ok: false, + error: "Desktop launch failed", + }), + }; + + await act(async () => { + root.render( + , + ); + await Promise.resolve(); + await Promise.resolve(); + }); + await flushAfterPaint(); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); + + await act(async () => { + container.querySelector("button")?.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved no-account-exists"); + expect(container.textContent).toContain("Desktop launch failed"); + }); + it("falls back to the cached signed URL when the click-time status refresh fails", async () => { setUserAgent("Mozilla/5.0 Chrome/140.0"); const popup = createPopupStub(); @@ -3605,6 +3676,64 @@ describe("useBuilderConnectFlow", () => { expect(container.textContent).not.toContain("Allow popups"); }); + it("keeps refreshed account state when the embedded host launch fails", async () => { + setUserAgent("Mozilla/5.0 Chrome/140.0"); + setEmbeddedWindow(true); + vi.mocked(openMcpAppHostLink).mockReturnValueOnce(false); + const accountExistsStatus = { + configured: false, + envManaged: false, + builderEnabled: true, + orgName: null, + connectUrl: signedConnectUrl, + appHost: "https://builder.io", + apiHost: "https://api.builder.io", + publicKeyConfigured: false, + privateKeyConfigured: false, + connectError: { + message: + "A Builder account already exists for this email. Log in to connect it.", + code: "account_exists", + at: Date.now(), + }, + }; + const refreshedStatus = { + ...accountExistsStatus, + connectError: undefined, + }; + vi.mocked(fetch).mockReset(); + vi.mocked(fetch) + .mockResolvedValueOnce(jsonResponse(accountExistsStatus)) + .mockResolvedValueOnce(jsonResponse(refreshedStatus)); + + await act(async () => { + root.render(); + await Promise.resolve(); + }); + await flushAfterPaint(); + + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved account-exists"); + + await act(async () => { + container.querySelector("button")?.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(openMcpAppHostLink).toHaveBeenCalledOnce(); + expect( + container + .querySelector('[data-testid="status"]') + ?.textContent?.replace(/\s+/g, " ") + .trim(), + ).toBe("not-configured idle resolved no-account-exists"); + }); + it("does not open the MCP host after cancelling a pending embedded startup", async () => { setUserAgent("Mozilla/5.0 Chrome/140.0"); setEmbeddedWindow(true); diff --git a/packages/toolkit/src/app/settings/useBuilderStatus.ts b/packages/toolkit/src/app/settings/useBuilderStatus.ts index 101b71d1f6b..4e954ae567e 100644 --- a/packages/toolkit/src/app/settings/useBuilderStatus.ts +++ b/packages/toolkit/src/app/settings/useBuilderStatus.ts @@ -1520,7 +1520,6 @@ export function useBuilderConnectFlow( if (!result?.ok) { connectStartedAtRef.current = null; setConnecting(false); - restoreExistingAccount(); setError( result?.error ?? t("agentChat.settingsShell.builder.setupStartFailed"), @@ -1597,7 +1596,7 @@ export function useBuilderConnectFlow( if (!mountedRef.current || openedByHost) return; connectStartedAtRef.current = null; setConnecting(false); - restoreExistingAccount(); + if (!s) restoreExistingAccount(); setError( t("agentChat.settingsShell.builder.setupHostFailed"), "launch", @@ -1687,7 +1686,7 @@ export function useBuilderConnectFlow( } connectStartedAtRef.current = null; setConnecting(false); - restoreExistingAccount(); + if (!s) restoreExistingAccount(); setError( "Couldn't navigate the Builder popup. Allow popups and try again.", "launch", @@ -1701,7 +1700,7 @@ export function useBuilderConnectFlow( if (!navigateBuilderConnectPopup(opened, trackedFreshUrl)) { connectStartedAtRef.current = null; setConnecting(false); - restoreExistingAccount(); + if (!s) restoreExistingAccount(); setError( "Couldn't navigate the Builder popup. Allow popups and try again.", "launch", From 99ab89baaa0f403c67e241256a3ca542e12d4a19 Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 17:05:04 -0700 Subject: [PATCH 8/9] fix: acknowledge Builder connect cancellation --- .../toolkit/src/app/i18n/catalogs/ar-SA.ts | 1 + .../toolkit/src/app/i18n/catalogs/de-DE.ts | 1 + .../toolkit/src/app/i18n/catalogs/en-US.ts | 1 + .../toolkit/src/app/i18n/catalogs/es-ES.ts | 1 + .../toolkit/src/app/i18n/catalogs/fr-FR.ts | 1 + .../toolkit/src/app/i18n/catalogs/hi-IN.ts | 1 + .../toolkit/src/app/i18n/catalogs/ja-JP.ts | 1 + .../toolkit/src/app/i18n/catalogs/ko-KR.ts | 1 + .../toolkit/src/app/i18n/catalogs/pt-BR.ts | 1 + .../toolkit/src/app/i18n/catalogs/zh-CN.ts | 1 + .../toolkit/src/app/i18n/catalogs/zh-TW.ts | 1 + .../onboarding/FirstRunOnboarding.spec.tsx | 9 ++++- .../src/app/onboarding/FirstRunOnboarding.tsx | 33 +++++++++++++------ 13 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts b/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts index dad82f2d289..d6ad518199b 100644 --- a/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts +++ b/packages/toolkit/src/app/i18n/catalogs/ar-SA.ts @@ -164,6 +164,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "جارٍ إعداد أرصدة Builder.io المجانية", "onboarding.builderProvisioningDescription": "جارٍ إنشاء حساب Builder.io الخاص بك وتفعيل الأرصدة المجانية.", + "onboarding.builderCancelPending": "جارٍ التحقق من حالة الاتصال…", "onboarding.builderConnectionDescription": "أكمل الاتصال بنقرة واحدة في النافذة الجديدة.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/de-DE.ts b/packages/toolkit/src/app/i18n/catalogs/de-DE.ts index e11530a7865..cb6bf16753b 100644 --- a/packages/toolkit/src/app/i18n/catalogs/de-DE.ts +++ b/packages/toolkit/src/app/i18n/catalogs/de-DE.ts @@ -175,6 +175,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "Builder.io-Credits werden eingerichtet", "onboarding.builderProvisioningDescription": "Dein Builder.io-Konto wird erstellt und die kostenlosen Credits werden aktiviert.", + "onboarding.builderCancelPending": "Verbindungsstatus wird überprüft…", "onboarding.builderConnectionDescription": "Schließe die Verbindung mit einem Klick im neuen Fenster ab.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/en-US.ts b/packages/toolkit/src/app/i18n/catalogs/en-US.ts index 6e2bb7d4336..50e76ed1ab1 100644 --- a/packages/toolkit/src/app/i18n/catalogs/en-US.ts +++ b/packages/toolkit/src/app/i18n/catalogs/en-US.ts @@ -166,6 +166,7 @@ const messages = { "onboarding.builderConnecting": "Setting up Builder.io credits", "onboarding.builderProvisioningDescription": "Creating your Builder.io account and activating free credits.", + "onboarding.builderCancelPending": "Checking connection status…", "onboarding.builderConnectionDescription": "Finish the one-click connection in the new window.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/es-ES.ts b/packages/toolkit/src/app/i18n/catalogs/es-ES.ts index 7adba30a6c1..c8cd42d1092 100644 --- a/packages/toolkit/src/app/i18n/catalogs/es-ES.ts +++ b/packages/toolkit/src/app/i18n/catalogs/es-ES.ts @@ -174,6 +174,7 @@ const messages: ToolkitAgentChatTranslation = { "Configurando los créditos gratuitos de Builder.io", "onboarding.builderProvisioningDescription": "Estamos creando tu cuenta de Builder.io y activando los créditos gratis.", + "onboarding.builderCancelPending": "Comprobando el estado de la conexión…", "onboarding.builderConnectionDescription": "Finaliza la conexión con un clic en la nueva ventana.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts b/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts index b529e06ecc2..11d803f06fa 100644 --- a/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts +++ b/packages/toolkit/src/app/i18n/catalogs/fr-FR.ts @@ -174,6 +174,7 @@ const messages: ToolkitAgentChatTranslation = { "Configuration des crédits gratuits de Builder.io", "onboarding.builderProvisioningDescription": "Création de votre compte Builder.io et activation des crédits gratuits.", + "onboarding.builderCancelPending": "Vérification de l’état de la connexion…", "onboarding.builderConnectionDescription": "Terminez la connexion en un clic dans la nouvelle fenêtre.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts b/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts index ace5ce9af97..64d4d1a7161 100644 --- a/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts +++ b/packages/toolkit/src/app/i18n/catalogs/hi-IN.ts @@ -163,6 +163,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "Builder.io के मुफ़्त क्रेडिट सेट अप हो रहे हैं", "onboarding.builderProvisioningDescription": "आपका Builder.io खाता बनाया जा रहा है और मुफ़्त क्रेडिट सक्रिय किए जा रहे हैं।", + "onboarding.builderCancelPending": "कनेक्शन की स्थिति जाँची जा रही है…", "onboarding.builderConnectionDescription": "नई विंडो में एक क्लिक से कनेक्शन पूरा करें।", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts b/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts index 165c5c44f3e..35adac65a3c 100644 --- a/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts +++ b/packages/toolkit/src/app/i18n/catalogs/ja-JP.ts @@ -172,6 +172,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "Builder.io の無料クレジットを設定しています", "onboarding.builderProvisioningDescription": "Builder.io アカウントを作成し、無料クレジットを有効化しています。", + "onboarding.builderCancelPending": "接続状態を確認しています…", "onboarding.builderConnectionDescription": "新しいウィンドウでワンクリック接続を完了してください。", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts b/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts index a4e51d68565..2f7ef19f9bf 100644 --- a/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts +++ b/packages/toolkit/src/app/i18n/catalogs/ko-KR.ts @@ -165,6 +165,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "Builder.io 무료 크레딧 설정 중", "onboarding.builderProvisioningDescription": "Builder.io 계정을 만들고 무료 크레딧을 활성화하고 있습니다.", + "onboarding.builderCancelPending": "연결 상태를 확인하는 중…", "onboarding.builderConnectionDescription": "새 창에서 한 번의 클릭으로 연결을 완료하세요.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts b/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts index acfba66eeae..59edadc848e 100644 --- a/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts +++ b/packages/toolkit/src/app/i18n/catalogs/pt-BR.ts @@ -170,6 +170,7 @@ const messages: ToolkitAgentChatTranslation = { "Configurando os créditos gratuitos do Builder.io", "onboarding.builderProvisioningDescription": "Criando sua conta do Builder.io e ativando os créditos grátis.", + "onboarding.builderCancelPending": "Verificando o status da conexão…", "onboarding.builderConnectionDescription": "Conclua a conexão com um clique na nova janela.", "onboarding.builderReadyWithCodeChanges": diff --git a/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts b/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts index 45416b1c75f..dc8af63dad8 100644 --- a/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts +++ b/packages/toolkit/src/app/i18n/catalogs/zh-CN.ts @@ -159,6 +159,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "正在设置 Builder.io 免费额度", "onboarding.builderProvisioningDescription": "正在创建您的 Builder.io 账户并激活免费额度。", + "onboarding.builderCancelPending": "正在检查连接状态…", "onboarding.builderConnectionDescription": "在新窗口中一键完成连接。", "onboarding.builderReadyWithCodeChanges": "AI 积分和云端代码更改已准备就绪。", "onboarding.builderReadyCreditsOnly": diff --git a/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts b/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts index 7ca8ba0c509..333ebe6b2bf 100644 --- a/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts +++ b/packages/toolkit/src/app/i18n/catalogs/zh-TW.ts @@ -160,6 +160,7 @@ const messages: ToolkitAgentChatTranslation = { "onboarding.builderConnecting": "正在設定 Builder.io 免費額度", "onboarding.builderProvisioningDescription": "正在建立您的 Builder.io 帳戶並啟用免費額度。", + "onboarding.builderCancelPending": "正在檢查連線狀態…", "onboarding.builderConnectionDescription": "在新視窗中按一下即可完成連線。", "onboarding.builderReadyWithCodeChanges": "AI 點數與雲端程式碼變更已準備就緒。", diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index e7b0c71033e..621a09a4cb5 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -578,7 +578,7 @@ describe("FirstRunOnboarding", () => { ); }); - it("offers a return path after a cancelled Builder connect settles", () => { + it("acknowledges cancel while a Builder connect settles", () => { const flow = { hasFetchedStatus: true, statusResolved: true, @@ -620,6 +620,13 @@ describe("FirstRunOnboarding", () => { act(() => cancelButton?.click()); expect(flow.cancel).toHaveBeenCalledOnce(); expect(flow.connecting).toBe(true); + expect( + document.body.querySelector('[data-testid="first-run-cancel-pending"]') + ?.textContent, + ).toBe("Checking connection status…"); + expect( + document.body.querySelector('[data-testid="first-run-cancel-builder"]'), + ).toBeNull(); expect( document.body.querySelector('[data-testid="first-run-back-to-choice"]'), ).toBeNull(); diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index 263a6a5f29e..2d54977fab7 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -196,6 +196,7 @@ export function FirstRunOnboarding({ const [builderConnectionMode, setBuilderConnectionMode] = useState< "existing" | "provision" >("existing"); + const [builderCancelRequested, setBuilderCancelRequested] = useState(false); const extensions = useMemo(() => listFirstRunOnboardingExtensions(), []); const beforeSetupExtensions = useMemo( () => @@ -564,6 +565,7 @@ export function FirstRunOnboarding({ profile.capabilities, ); const handleBuilder = (provisionAccount = canActivateBuilderFreeCredits) => { + setBuilderCancelRequested(false); if (previewMode) { handleFinish(null); return; @@ -1079,16 +1081,27 @@ export function FirstRunOnboarding({ - {connectFlow.connecting && ( - - )} + {connectFlow.connecting && + (builderCancelRequested ? ( +

+ {t("agentChat.onboarding.builderCancelPending")} +

+ ) : ( + + ))} {connectFlow.statusUnavailable && connectFlow.connecting && !connectFlow.terminalError && ( From e83252ca1b43503bc910409c9193283c06ba04ec Mon Sep 17 00:00:00 2001 From: Steve Sewell Date: Sat, 10 Oct 2026 17:07:56 -0700 Subject: [PATCH 9/9] fix: expose Builder setup choices after cancel --- .../onboarding/FirstRunOnboarding.spec.tsx | 42 ++++++++++++++----- .../src/app/onboarding/FirstRunOnboarding.tsx | 31 +++++++++++--- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx index 621a09a4cb5..e2c0b0086a8 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.spec.tsx @@ -578,7 +578,7 @@ describe("FirstRunOnboarding", () => { ); }); - it("acknowledges cancel while a Builder connect settles", () => { + it("returns to setup choices immediately after cancel", () => { const flow = { hasFetchedStatus: true, statusResolved: true, @@ -629,7 +629,32 @@ describe("FirstRunOnboarding", () => { ).toBeNull(); expect( document.body.querySelector('[data-testid="first-run-back-to-choice"]'), - ).toBeNull(); + ).not.toBeNull(); + + act(() => { + document.body + .querySelector( + '[data-testid="first-run-back-to-choice"]', + ) + ?.click(); + }); + expect( + document.body.querySelector('[data-onboarding-screen="choice"]'), + ).toBeTruthy(); + expect( + document.body.querySelector('[data-testid="first-run-cancel-pending"]') + ?.textContent, + ).toBe("Checking connection status…"); + expect( + document.body.querySelector( + '[data-testid="first-run-open-key-settings"]', + ), + ).toBeTruthy(); + expect( + document.body.querySelector( + '[data-testid="first-run-builder-sign-in"]', + )?.disabled, + ).toBe(true); flow.connecting = false; @@ -640,17 +665,14 @@ describe("FirstRunOnboarding", () => {
, ); }); - act(() => { - document.body - .querySelector( - '[data-testid="first-run-back-to-choice"]', - ) - ?.click(); - }); - expect( document.body.querySelector('[data-onboarding-screen="choice"]'), ).toBeTruthy(); + expect( + document.body.querySelector( + '[data-testid="first-run-builder-sign-in"]', + )?.disabled, + ).toBe(false); }); it("keeps Cancel during a failed status poll without offering a fake retry", () => { diff --git a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx index 2d54977fab7..5c831ebe6fb 100644 --- a/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx +++ b/packages/toolkit/src/app/onboarding/FirstRunOnboarding.tsx @@ -801,6 +801,15 @@ export function FirstRunOnboarding({ {t("agentChat.onboarding.builderSignInWithAccount")} + {connectFlow.connecting && builderCancelRequested && ( +

+ {t("agentChat.onboarding.builderCancelPending")} +

+ )} {connectFlow.error && connectFlow.errorKind === "status-read" && (
{connectFlow.connecting && (builderCancelRequested ? ( -

- {t("agentChat.onboarding.builderCancelPending")} -

+
+

+ {t("agentChat.onboarding.builderCancelPending")} +

+ +
) : (