-
-
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 3 commits
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 |
|---|---|---|
| @@ -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 || t('auth.failure.unknownProfile', 'Unknown Profile'); | ||
|
|
||
| // 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('labels.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,44 @@ | ||
| import { create } from 'zustand'; | ||
| import type { AuthFailureInfo } from '../../shared/types'; | ||
|
|
||
| interface AuthFailureState { | ||
| // Auth failure modal state | ||
| isModalOpen: boolean; | ||
| authFailureInfo: AuthFailureInfo | null; | ||
|
|
||
| // TODO: Use hasPendingAuthFailure to show a badge/indicator in the sidebar | ||
| // when there's an unresolved auth failure (e.g., red dot on Settings icon) | ||
| 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.
Settings button doesn't navigate to Claude profiles section
Medium Severity
The
AuthFailureModal's "Go to Settings" button only callssetIsSettingsDialogOpen(true)without settingsetSettingsInitialSection('integrations'). According to the PR test plan, this button should navigate directly to Claude profiles settings (which are under the 'integrations' section), but currently it opens settings at the default view, leaving users to find Claude profiles manually.