-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(auth): add auth failure detection modal for Claude CLI 401 errors #1361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
52f9b65
a37e1ff
a2ae293
f2cd6bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ import { AgentTools } from './components/AgentTools'; | |
| import { WelcomeScreen } from './components/WelcomeScreen'; | ||
| import { RateLimitModal } from './components/RateLimitModal'; | ||
| import { SDKRateLimitModal } from './components/SDKRateLimitModal'; | ||
| import { AuthFailureModal } from './components/AuthFailureModal'; | ||
| import { OnboardingWizard } from './components/onboarding'; | ||
| import { AppUpdateNotification } from './components/AppUpdateNotification'; | ||
| import { ProactiveSwapListener } from './components/ProactiveSwapListener'; | ||
|
|
@@ -1075,6 +1076,9 @@ export function App() { | |
| {/* SDK Rate Limit Modal - shows when SDK/CLI operations hit limits (changelog, tasks, etc.) */} | ||
| <SDKRateLimitModal /> | ||
|
|
||
| {/* Auth Failure Modal - shows when Claude CLI encounters 401/auth errors */} | ||
| <AuthFailureModal onOpenSettings={() => setIsSettingsDialogOpen(true)} /> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Settings button doesn't navigate to Claude profiles sectionMedium Severity The |
||
|
|
||
| {/* Onboarding Wizard - shows on first launch when onboardingCompleted is false */} | ||
| <OnboardingWizard | ||
| open={isOnboardingWizardOpen} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { AlertTriangle, Settings } from 'lucide-react'; | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogDescription, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| DialogTitle | ||
| } from './ui/dialog'; | ||
| import { Button } from './ui/button'; | ||
| import { useAuthFailureStore } from '../stores/auth-failure-store'; | ||
|
|
||
| interface AuthFailureModalProps { | ||
| onOpenSettings?: () => void; | ||
| } | ||
|
|
||
| /** | ||
| * Modal displayed when Claude CLI encounters an authentication failure (401 error). | ||
| * Prompts the user to re-authenticate via Settings > Claude Profiles. | ||
| */ | ||
| export function AuthFailureModal({ onOpenSettings }: AuthFailureModalProps) { | ||
| const { isModalOpen, authFailureInfo, hideAuthFailureModal, clearAuthFailure } = useAuthFailureStore(); | ||
| const { t } = useTranslation('common'); | ||
|
|
||
| if (!authFailureInfo) return null; | ||
|
|
||
| const profileName = authFailureInfo.profileName || 'Unknown Profile'; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Get user-friendly message for the auth failure type | ||
| const getFailureMessage = () => { | ||
| switch (authFailureInfo.failureType) { | ||
| case 'expired': | ||
| return t('auth.failure.tokenExpired', 'Your authentication token has expired.'); | ||
| case 'invalid': | ||
| return t('auth.failure.tokenInvalid', 'Your authentication token is invalid.'); | ||
| case 'missing': | ||
| return t('auth.failure.tokenMissing', 'No authentication token found.'); | ||
| default: | ||
| return t('auth.failure.authFailed', 'Authentication failed.'); | ||
|
Comment on lines
+34
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| } | ||
| }; | ||
|
Comment on lines
+31
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider memoizing The ♻️ Optional: Memoize with useMemo+import { useMemo } from 'react';
...
- const getFailureMessage = () => {
- switch (authFailureInfo.failureType) {
+ const failureMessage = useMemo(() => {
+ switch (authFailureInfo?.failureType) {
case 'expired':
return t('auth.failure.tokenExpired', 'Your authentication token has expired.');
case 'invalid':
return t('auth.failure.tokenInvalid', 'Your authentication token is invalid.');
case 'missing':
return t('auth.failure.tokenMissing', 'No authentication token found.');
default:
return t('auth.failure.authFailed', 'Authentication failed.');
}
- };
-
- const failureMessage = getFailureMessage();
+ }, [authFailureInfo?.failureType, t]);🤖 Prompt for AI Agents |
||
|
|
||
| const failureMessage = getFailureMessage(); | ||
|
|
||
| const handleGoToSettings = () => { | ||
| hideAuthFailureModal(); | ||
| onOpenSettings?.(); | ||
| }; | ||
|
|
||
| const handleDismiss = () => { | ||
| clearAuthFailure(); | ||
| }; | ||
|
|
||
| return ( | ||
| <Dialog open={isModalOpen} onOpenChange={(open) => !open && hideAuthFailureModal()}> | ||
| <DialogContent className="sm:max-w-md"> | ||
| <DialogHeader> | ||
| <div className="flex items-center gap-3"> | ||
| <div className="rounded-full bg-amber-100 dark:bg-amber-900/30 p-2"> | ||
| <AlertTriangle className="h-5 w-5 text-amber-600 dark:text-amber-400" /> | ||
| </div> | ||
| <div> | ||
| <DialogTitle className="text-lg"> | ||
| {t('auth.failure.title', 'Authentication Required')} | ||
| </DialogTitle> | ||
| <DialogDescription className="text-sm text-muted-foreground"> | ||
| {t('auth.failure.profileLabel', 'Profile')}: {profileName} | ||
| </DialogDescription> | ||
| </div> | ||
| </div> | ||
| </DialogHeader> | ||
|
|
||
| <div className="space-y-4 py-4"> | ||
| <p className="text-sm text-foreground"> | ||
| {failureMessage} | ||
| </p> | ||
| <p className="text-sm text-muted-foreground"> | ||
| {t('auth.failure.description', 'Please re-authenticate your Claude profile to continue using Auto Claude.')} | ||
| </p> | ||
|
|
||
| {authFailureInfo.taskId && ( | ||
| <div className="rounded-md bg-muted p-3 text-xs"> | ||
| <p className="text-muted-foreground"> | ||
| {t('auth.failure.taskAffected', 'Task affected')}: <span className="font-mono">{authFailureInfo.taskId}</span> | ||
| </p> | ||
| </div> | ||
| )} | ||
|
|
||
| {authFailureInfo.originalError && ( | ||
| <details className="text-xs"> | ||
| <summary className="cursor-pointer text-muted-foreground hover:text-foreground"> | ||
| {t('auth.failure.technicalDetails', 'Technical details')} | ||
| </summary> | ||
| <pre className="mt-2 rounded-md bg-muted p-2 overflow-x-auto whitespace-pre-wrap break-all"> | ||
| {authFailureInfo.originalError} | ||
| </pre> | ||
| </details> | ||
| )} | ||
| </div> | ||
|
|
||
| <DialogFooter className="flex-col sm:flex-row gap-2"> | ||
| <Button variant="outline" onClick={handleDismiss} className="sm:mr-auto"> | ||
| {t('common.dismiss', 'Dismiss')} | ||
| </Button> | ||
| <Button onClick={handleGoToSettings} className="gap-2"> | ||
| <Settings className="h-4 w-4" /> | ||
| {t('auth.failure.goToSettings', 'Go to Settings')} | ||
| </Button> | ||
| </DialogFooter> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { create } from 'zustand'; | ||
| import type { AuthFailureInfo } from '../../shared/types'; | ||
|
|
||
| interface AuthFailureState { | ||
| // Auth failure modal state | ||
| isModalOpen: boolean; | ||
| authFailureInfo: AuthFailureInfo | null; | ||
|
|
||
| // Track if there's a pending auth failure that needs attention | ||
| hasPendingAuthFailure: boolean; | ||
|
|
||
| // Actions | ||
| showAuthFailureModal: (info: AuthFailureInfo) => void; | ||
| hideAuthFailureModal: () => void; | ||
| clearAuthFailure: () => void; | ||
| } | ||
|
|
||
| export const useAuthFailureStore = create<AuthFailureState>((set) => ({ | ||
| isModalOpen: false, | ||
| authFailureInfo: null, | ||
| hasPendingAuthFailure: false, | ||
|
|
||
| showAuthFailureModal: (info: AuthFailureInfo) => { | ||
| set({ | ||
| isModalOpen: true, | ||
| authFailureInfo: info, | ||
| hasPendingAuthFailure: true, | ||
| }); | ||
| }, | ||
|
|
||
| hideAuthFailureModal: () => { | ||
| // Keep the failure info when closing so user can see it again | ||
| set({ isModalOpen: false }); | ||
| }, | ||
|
|
||
| clearAuthFailure: () => { | ||
| set({ | ||
| isModalOpen: false, | ||
| authFailureInfo: null, | ||
| hasPendingAuthFailure: false, | ||
| }); | ||
| }, | ||
| })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
requireinside a TypeScript file is generally discouraged in favor ofimportstatements. While it might work due to transpilation, usingimportprovides better static analysis, type checking, and aligns with modern TypeScript practices.