diff --git a/ui/src/components/account/add-account-dialog.tsx b/ui/src/components/account/add-account-dialog.tsx index 46e43bcb..49bb0448 100644 --- a/ui/src/components/account/add-account-dialog.tsx +++ b/ui/src/components/account/add-account-dialog.tsx @@ -60,6 +60,11 @@ function normalizeRiskPhrase(value: string): string { return value.trim().replace(/\s+/g, ' ').toUpperCase(); } +interface PowerUserModeSyncOptions { + pendingMessage?: string | null; + disabledMessage?: string | null; +} + export function AddAccountDialog({ open, onClose, @@ -78,6 +83,8 @@ export function AddAccountDialog({ const [kiroAuthMethod, setKiroAuthMethod] = useState(DEFAULT_KIRO_AUTH_METHOD); const { t } = useTranslation(); const wasAuthenticatingRef = useRef(false); + const powerUserModeRequestIdRef = useRef(0); + const powerUserModeLoadErrorShownRef = useRef(false); const authFlow = useCliproxyAuthFlow(); const kiroImportMutation = useKiroImport(); @@ -104,6 +111,53 @@ export function AddAccountDialog({ return data.antigravityAckBypass === true; }, []); + const syncPowerUserModeState = useCallback( + async ({ pendingMessage = null, disabledMessage = null }: PowerUserModeSyncOptions = {}) => { + const requestId = ++powerUserModeRequestIdRef.current; + setPowerUserModeLoading(true); + + if (pendingMessage !== null) { + setLocalError(pendingMessage); + } + + try { + const enabled = await fetchPowerUserModeState(); + if (powerUserModeRequestIdRef.current !== requestId) { + return enabled; + } + + setPowerUserModeEnabled(enabled); + + if (disabledMessage) { + setLocalError(enabled ? null : disabledMessage); + } else if (pendingMessage !== null) { + setLocalError(null); + } + + return enabled; + } catch { + if (powerUserModeRequestIdRef.current !== requestId) { + return false; + } + + setPowerUserModeEnabled(false); + setLocalError(disabledMessage ?? t('addAccountDialog.powerUserLoadFailed')); + + if (!powerUserModeLoadErrorShownRef.current) { + powerUserModeLoadErrorShownRef.current = true; + toast.error(t('addAccountDialog.powerUserLoadFailed')); + } + + return false; + } finally { + if (powerUserModeRequestIdRef.current === requestId) { + setPowerUserModeLoading(false); + } + } + }, + [fetchPowerUserModeState, t] + ); + const resetAndClose = () => { setNickname(''); setCallbackUrl(''); @@ -114,6 +168,8 @@ export function AddAccountDialog({ setPowerUserModeEnabled(false); setPowerUserModeLoading(false); setKiroAuthMethod(DEFAULT_KIRO_AUTH_METHOD); + powerUserModeRequestIdRef.current += 1; + powerUserModeLoadErrorShownRef.current = false; wasAuthenticatingRef.current = false; onClose(); }; @@ -127,38 +183,21 @@ export function AddAccountDialog({ }, [provider, open]); useEffect(() => { - let cancelled = false; + return () => { + powerUserModeRequestIdRef.current += 1; + }; + }, []); + useEffect(() => { if (!open || !supportsPowerUserMode) { + powerUserModeRequestIdRef.current += 1; setPowerUserModeEnabled(false); setPowerUserModeLoading(false); return; } - const loadPowerUserModeState = async () => { - try { - setPowerUserModeLoading(true); - const enabled = await fetchPowerUserModeState(); - if (!cancelled) { - setPowerUserModeEnabled(enabled); - } - } catch { - if (!cancelled) { - setPowerUserModeEnabled(false); - } - } finally { - if (!cancelled) { - setPowerUserModeLoading(false); - } - } - }; - - loadPowerUserModeState(); - - return () => { - cancelled = true; - }; - }, [fetchPowerUserModeState, open, supportsPowerUserMode]); + void syncPowerUserModeState(); + }, [open, provider, supportsPowerUserMode, syncPowerUserModeState]); useEffect(() => { if (!open || provider !== 'agy' || !authFlow.error || !powerUserModeEnabled) { @@ -172,34 +211,11 @@ export function AddAccountDialog({ normalizedError.includes('responsibility checklist'); if (!ackRequired) return; - let cancelled = false; - - const syncBypassState = async () => { - try { - setPowerUserModeLoading(true); - const enabled = await fetchPowerUserModeState(); - if (cancelled) return; - setPowerUserModeEnabled(enabled); - if (!enabled) { - setLocalError('Power user mode is off. Complete the AGY checklist and retry.'); - } - } catch { - if (cancelled) return; - setPowerUserModeEnabled(false); - setLocalError('Power user mode is off. Complete the AGY checklist and retry.'); - } finally { - if (!cancelled) { - setPowerUserModeLoading(false); - } - } - }; - - void syncBypassState(); - - return () => { - cancelled = true; - }; - }, [authFlow.error, fetchPowerUserModeState, open, powerUserModeEnabled, provider]); + void syncPowerUserModeState({ + pendingMessage: t('addAccountDialog.powerUserLoading'), + disabledMessage: t('addAccountDialog.powerUserUnavailableRetry'), + }); + }, [authFlow.error, open, powerUserModeEnabled, provider, syncPowerUserModeState, t]); // When authFlow completes successfully (polling detected success), apply preset and close useEffect(() => { @@ -250,7 +266,7 @@ export function AddAccountDialog({ */ const handleAuthenticate = () => { if (isPowerUserModePending) { - setLocalError('Loading power user safety settings. Please wait a moment and retry.'); + setLocalError(t('addAccountDialog.powerUserLoading')); return; } if (requiresAgyResponsibilityFlow && !isAgyRiskChecklistComplete) { diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 17b08c13..ab0964b3 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -484,6 +484,12 @@ const resources = { powerUserEnabled: 'Power user mode enabled', powerUserSkipped: 'Settings > Proxy power user mode is skipping the AGY responsibility checklist and Gemini dashboard risk phrase. You accept full responsibility for OAuth/account risk.', + powerUserLoadFailed: + 'Failed to load power user mode settings. Check Settings > Proxy and try again.', + powerUserLoading: + 'Loading power user safety settings. Please wait a moment and retry.', + powerUserUnavailableRetry: + 'Power user mode is unavailable. Complete the required provider safety step and retry.', authMethod: 'Auth Method', selectKiroAuthMethod: 'Select Kiro auth method', nicknameRequired: 'Nickname (required)', @@ -1668,6 +1674,9 @@ const resources = { powerUserEnabled: '已启用高级用户模式', powerUserSkipped: '设置 > 代理 中的高级用户模式会跳过 AGY 责任确认清单和 Gemini Dashboard 的风险短语。OAuth / 账号风险需自行承担。', + powerUserLoadFailed: '加载高级用户模式设置失败。请检查“设置 > 代理”后重试。', + powerUserLoading: '正在加载高级用户安全设置。请稍候后重试。', + powerUserUnavailableRetry: '高级用户模式不可用。请完成当前提供商要求的安全步骤后重试。', authMethod: '认证方式', selectKiroAuthMethod: '选择 Kiro 认证方式', nicknameRequired: '昵称(必填)', @@ -2862,6 +2871,12 @@ const resources = { powerUserEnabled: 'Đã bật chế độ power user', powerUserSkipped: 'Chế độ power user trong Cài đặt > Proxy đang bỏ qua danh sách kiểm tra trách nhiệm AGY và bước nhập cụm từ rủi ro của Gemini trên dashboard. Bạn tự chịu hoàn toàn rủi ro OAuth/tài khoản.', + powerUserLoadFailed: + 'Không thể tải cài đặt chế độ power user. Hãy kiểm tra Cài đặt > Proxy rồi thử lại.', + powerUserLoading: + 'Đang tải cài đặt an toàn cho chế độ power user. Vui lòng đợi một chút rồi thử lại.', + powerUserUnavailableRetry: + 'Chế độ power user hiện không khả dụng. Hãy hoàn tất bước an toàn bắt buộc của nhà cung cấp rồi thử lại.', authMethod: 'Phương thức xác thực', selectKiroAuthMethod: 'Chọn phương thức xác thực Kiro', nicknameRequired: 'Biệt danh (bắt buộc)', @@ -4086,6 +4101,12 @@ const resources = { powerUserEnabled: '上級者モードが有効です', powerUserSkipped: '設定 > プロキシのパワーユーザーモードにより、AGY の責任確認チェックと Gemini ダッシュボードのリスク文言入力をスキップしています。OAuth / アカウントに関するリスクはすべて自己責任です。', + powerUserLoadFailed: + 'パワーユーザーモード設定を読み込めませんでした。設定 > プロキシを確認してから再試行してください。', + powerUserLoading: + 'パワーユーザーモードの安全設定を読み込み中です。少し待ってから再試行してください。', + powerUserUnavailableRetry: + 'パワーユーザーモードは利用できません。必要なプロバイダーの安全確認を完了してから再試行してください。', authMethod: '認証方法', selectKiroAuthMethod: 'Kiro の認証方法を選択', nicknameRequired: 'ニックネーム(必須)', diff --git a/ui/tests/unit/components/account/add-account-dialog.test.tsx b/ui/tests/unit/components/account/add-account-dialog.test.tsx index e386a527..b56aa08a 100644 --- a/ui/tests/unit/components/account/add-account-dialog.test.tsx +++ b/ui/tests/unit/components/account/add-account-dialog.test.tsx @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import i18n from '@/lib/i18n'; import { AddAccountDialog } from '@/components/account/add-account-dialog'; import { render, screen, userEvent, waitFor } from '@tests/setup/test-utils'; +import { toast } from 'sonner'; const authMocks = vi.hoisted(() => ({ startAuth: vi.fn(), @@ -35,6 +36,14 @@ vi.mock('@/hooks/use-cliproxy', () => ({ }), })); +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + }, +})); + function createJsonResponse(body: Record): Response { return new Response(JSON.stringify(body), { status: 200, @@ -42,7 +51,7 @@ function createJsonResponse(body: Record): Response { }); } -describe('AddAccountDialog Gemini power user mode', () => { +describe('AddAccountDialog power user mode', () => { const fetchMock = vi.fn(); beforeEach(async () => { @@ -101,4 +110,66 @@ describe('AddAccountDialog Gemini power user mode', () => { expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument(); expect(authMocks.startAuth).not.toHaveBeenCalled(); }); + + it('surfaces a power user mode fetch failure and fails closed for Gemini', async () => { + fetchMock.mockRejectedValue(new Error('network down')); + + render( + + ); + + await waitFor(() => + expect(toast.error).toHaveBeenCalledWith( + 'Failed to load power user mode settings. Check Settings > Proxy and try again.' + ) + ); + + expect( + screen.getByText('Failed to load power user mode settings. Check Settings > Proxy and try again.') + ).toBeInTheDocument(); + expect(screen.getByText(/Type exact phrase to continue/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Authenticate' })).toBeDisabled(); + }); + + it('skips the AGY responsibility checklist when power user mode is enabled', async () => { + fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: true })); + + render(); + + await waitFor(() => + expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk') + ); + + const authenticateButton = screen.getByRole('button', { name: 'Authenticate' }); + await waitFor(() => expect(authenticateButton).toBeEnabled()); + + expect(screen.getByText('Power user mode enabled')).toBeInTheDocument(); + expect(screen.queryByText(/Step 1: I reviewed issue #509/i)).not.toBeInTheDocument(); + + await userEvent.click(authenticateButton); + + expect(authMocks.startAuth).toHaveBeenCalledWith( + 'agy', + expect.objectContaining({ + riskAcknowledgement: undefined, + }) + ); + }); + + it('keeps the AGY responsibility checklist when power user mode is disabled', async () => { + fetchMock.mockResolvedValue(createJsonResponse({ antigravityAckBypass: false })); + + render(); + + await waitFor(() => + expect(fetchMock).toHaveBeenCalledWith('/api/settings/auth/antigravity-risk') + ); + + const authenticateButton = screen.getByRole('button', { name: 'Authenticate' }); + await waitFor(() => expect(authenticateButton).toBeDisabled()); + + expect(screen.getByText(/Step 1: I reviewed issue #509/i)).toBeInTheDocument(); + expect(screen.queryByText('Power user mode enabled')).not.toBeInTheDocument(); + expect(authMocks.startAuth).not.toHaveBeenCalled(); + }); });