diff --git a/apps/web/src/components/AmrLoginPill.tsx b/apps/web/src/components/AmrLoginPill.tsx index f456d2eea..4b0e78516 100644 --- a/apps/web/src/components/AmrLoginPill.tsx +++ b/apps/web/src/components/AmrLoginPill.tsx @@ -196,6 +196,7 @@ export function AmrLoginPill({ const pollRef = useRef(null); const loginStartedAtRef = useRef(null); const loginPendingRef = useRef(false); + const suppressLoginInFlightRef = useRef(false); const stopPolling = useCallback(() => { if (pollRef.current !== null) { @@ -215,6 +216,7 @@ export function AmrLoginPill({ return () => { loginPendingRef.current = false; loginStartedAtRef.current = null; + suppressLoginInFlightRef.current = false; stopPolling(); }; }, [refresh, skipInitialRefresh, stopPolling]); @@ -245,6 +247,7 @@ export function AmrLoginPill({ stopPolling(); loginStartedAtRef.current = null; loginPendingRef.current = false; + suppressLoginInFlightRef.current = false; setPending(null); return; } @@ -257,6 +260,7 @@ export function AmrLoginPill({ } loginStartedAtRef.current = null; loginPendingRef.current = false; + suppressLoginInFlightRef.current = false; setPending(null); setErrorMessage(t('settings.amrLoginErrorCompact')); } @@ -270,6 +274,7 @@ export function AmrLoginPill({ const onStatusChange = (event: Event) => { const reason = amrLoginStatusEventReason(event); if (reason === 'login-started') { + suppressLoginInFlightRef.current = false; const startedAt = Date.now(); loginStartedAtRef.current = startedAt; setErrorMessage(null); @@ -303,12 +308,13 @@ export function AmrLoginPill({ stopPolling(); loginStartedAtRef.current = null; loginPendingRef.current = false; + suppressLoginInFlightRef.current = false; setPending(null); setCanceledVisible(false); setErrorMessage(null); return; } - if (next.loginInFlight) { + if (next.loginInFlight && !suppressLoginInFlightRef.current) { setErrorMessage(null); setPending('login'); startPolling(); @@ -334,6 +340,7 @@ export function AmrLoginPill({ async (event: MouseEvent) => { event.stopPropagation(); if (loginPendingRef.current) return; + suppressLoginInFlightRef.current = false; loginPendingRef.current = true; const startedAt = Date.now(); loginStartedAtRef.current = startedAt; @@ -379,6 +386,7 @@ export function AmrLoginPill({ configPath: '', } )); + suppressLoginInFlightRef.current = true; setPending(null); setCanceledVisible(true); notifyAmrLoginStatusChanged('login-canceled'); @@ -394,6 +402,7 @@ export function AmrLoginPill({ const result = await velaLogout(); loginStartedAtRef.current = null; loginPendingRef.current = false; + suppressLoginInFlightRef.current = false; setPending(null); if (!result.ok) { setErrorMessage(t('settings.amrLoginErrorCompact')); @@ -408,7 +417,11 @@ export function AmrLoginPill({ const loggedIn = status?.loggedIn === true; const userEmail = status?.user?.email ?? ''; const loginInFlight = - pending === 'login' || (status?.loggedIn !== true && status?.loginInFlight === true); + pending === 'login' || ( + status?.loggedIn !== true && + status?.loginInFlight === true && + !suppressLoginInFlightRef.current + ); const logoutInFlight = pending === 'logout'; const cancelInFlight = pending === 'cancel'; const accountStatus: AmrAccountControlStatus = errorMessage diff --git a/apps/web/tests/components/SettingsDialog.execution.test.tsx b/apps/web/tests/components/SettingsDialog.execution.test.tsx index 585654e68..c5f51247a 100644 --- a/apps/web/tests/components/SettingsDialog.execution.test.tsx +++ b/apps/web/tests/components/SettingsDialog.execution.test.tsx @@ -1812,6 +1812,74 @@ describe('SettingsDialog execution settings Local CLI interactions', () => { expect(screen.getByText('late@example.com')).toBeTruthy(); }); + it('does not resurrect Signing in from a stale loginInFlight echo after cancel', async () => { + let statusStage: 'pending' | 'stale-pending' | 'signed-out' = 'pending'; + const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + if (url === '/api/memory') { + return new Response( + JSON.stringify({ enabled: true, memories: [], extraction: null }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ); + } + if (url === '/api/integrations/vela/status') { + const body = + statusStage === 'signed-out' + ? { + loggedIn: false, + loginInFlight: false, + profile: 'local', + user: null, + configPath: '/Users/test/.amr/config.json', + } + : { + loggedIn: false, + loginInFlight: true, + profile: 'local', + user: null, + configPath: '/Users/test/.amr/config.json', + }; + return new Response(JSON.stringify(body), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + } + if (url === '/api/integrations/vela/login/cancel' && init?.method === 'POST') { + statusStage = 'stale-pending'; + return new Response(JSON.stringify({ canceled: true }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + } + throw new Error(`Unexpected fetch: ${url}`); + }); + vi.stubGlobal('fetch', fetchMock); + + renderSettingsDialog( + { mode: 'daemon', agentId: 'amr' }, + { agents: [amrAgent] }, + ); + + fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i })); + const amrCard = screen.getByRole('button', { name: /^Open Design AMR\b/ }).closest('.agent-card') as HTMLElement; + expect(await screen.findByText('Signing in…')).toBeTruthy(); + + fireEvent.mouseEnter(amrCard); + fireEvent.click(await screen.findByRole('button', { name: 'Cancel' })); + expect(await screen.findByText('Canceled')).toBeTruthy(); + + window.dispatchEvent( + new CustomEvent('od:amr-login-status-change', { + detail: { reason: 'login-canceled' }, + }), + ); + + await waitFor(() => { + expect(screen.queryByText('Signing in…')).toBeNull(); + }); + expect(screen.getByRole('button', { name: 'Authorize' })).toBeTruthy(); + }); + it('renders the signed-in AMR account state inside Settings without leaking vela branding', async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = input.toString();