diff --git a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts index 96edd3c437..cf2a877827 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts @@ -56,7 +56,7 @@ export function registerTerminalHandlers( (_, id: string, cwd?: string) => { // Use async version to avoid blocking main process during CLI detection terminalManager.invokeClaudeAsync(id, cwd).catch((error) => { - console.error('[terminal-handlers] Failed to invoke Claude:', error); + debugError('[terminal-handlers] Failed to invoke Claude:', error); }); } ); @@ -312,12 +312,17 @@ export function registerTerminalHandlers( ipcMain.handle( IPC_CHANNELS.CLAUDE_PROFILE_INITIALIZE, async (_, profileId: string): Promise => { + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Handler called for profileId:', profileId); try { + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile manager...'); const profileManager = getClaudeProfileManager(); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile...'); const profile = profileManager.getProfile(profileId); if (!profile) { + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile not found!'); return { success: false, error: 'Profile not found' }; } + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile found:', profile.name); // Ensure the config directory exists for non-default profiles if (!profile.isDefault && profile.configDir) { @@ -333,6 +338,7 @@ export function registerTerminalHandlers( const terminalId = `claude-login-${profileId}-${Date.now()}`; const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp'; + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Creating terminal:', terminalId); debugLog('[IPC] Initializing Claude profile:', { profileId, profileName: profile.name, @@ -341,7 +347,9 @@ export function registerTerminalHandlers( }); // Create a new terminal for the login process + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Calling terminalManager.create...'); const createResult = await terminalManager.create({ id: terminalId, cwd: homeDir }); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Terminal created:', createResult.success); // If terminal creation failed, return the error if (!createResult.success) { @@ -352,17 +360,18 @@ export function registerTerminalHandlers( } // Wait a moment for the terminal to initialize + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Waiting 500ms for terminal init...'); await new Promise(resolve => setTimeout(resolve, 500)); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Wait complete'); // Build the login command with the profile's config dir - // Use platform-specific syntax and escaping for environment variables + // Use full path to claude CLI - no need to modify PATH since we have the absolute path + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting Claude CLI invocation...'); let loginCommand: string; - const { command: claudeCmd, env: claudeEnv } = await getClaudeCliInvocationAsync(); - const pathPrefix = claudeEnv.PATH - ? (process.platform === 'win32' - ? `set "PATH=${escapeShellArgWindows(claudeEnv.PATH)}" && ` - : `export PATH=${escapeShellArg(claudeEnv.PATH)} && `) - : ''; + const { command: claudeCmd } = await getClaudeCliInvocationAsync(); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Got Claude CLI:', claudeCmd); + + // Use the full path directly - escaping only needed for paths with spaces const shellClaudeCmd = process.platform === 'win32' ? `"${escapeShellArgWindows(claudeCmd)}"` : escapeShellArg(claudeCmd); @@ -372,24 +381,30 @@ export function registerTerminalHandlers( // SECURITY: Use Windows-specific escaping for cmd.exe const escapedConfigDir = escapeShellArgWindows(profile.configDir); // Windows cmd.exe syntax: set "VAR=value" with %VAR% for expansion - loginCommand = `${pathPrefix}set "CLAUDE_CONFIG_DIR=${escapedConfigDir}" && echo Config dir: %CLAUDE_CONFIG_DIR% && ${shellClaudeCmd} setup-token`; + loginCommand = `set "CLAUDE_CONFIG_DIR=${escapedConfigDir}" && echo Config dir: %CLAUDE_CONFIG_DIR% && ${shellClaudeCmd} setup-token`; } else { // SECURITY: Use POSIX escaping for bash/zsh const escapedConfigDir = escapeShellArg(profile.configDir); // Unix/Mac bash/zsh syntax: export VAR=value with $VAR for expansion - loginCommand = `${pathPrefix}export CLAUDE_CONFIG_DIR=${escapedConfigDir} && echo "Config dir: $CLAUDE_CONFIG_DIR" && ${shellClaudeCmd} setup-token`; + loginCommand = `export CLAUDE_CONFIG_DIR=${escapedConfigDir} && echo "Config dir: $CLAUDE_CONFIG_DIR" && ${shellClaudeCmd} setup-token`; } } else { - loginCommand = `${pathPrefix}${shellClaudeCmd} setup-token`; + // Simple command for default profile - just run setup-token + loginCommand = `${shellClaudeCmd} setup-token`; } + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Built login command, length:', loginCommand.length); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Login command:', loginCommand); debugLog('[IPC] Sending login command to terminal:', loginCommand); // Write the login command to the terminal + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Writing command to terminal...'); terminalManager.write(terminalId, `${loginCommand}\r`); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Command written successfully'); // Notify the renderer that an auth terminal was created // This allows the UI to display the terminal so users can see the OAuth flow + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Notifying renderer of auth terminal...'); const mainWindow = getMainWindow(); if (mainWindow) { mainWindow.webContents.send(IPC_CHANNELS.TERMINAL_AUTH_CREATED, { @@ -399,6 +414,7 @@ export function registerTerminalHandlers( }); } + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Returning success!'); return { success: true, data: { @@ -407,7 +423,7 @@ export function registerTerminalHandlers( } }; } catch (error) { - debugError('[IPC] Failed to initialize Claude profile:', error); + debugError('[IPC:CLAUDE_PROFILE_INITIALIZE] EXCEPTION:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to initialize Claude profile' @@ -640,7 +656,7 @@ export function registerTerminalHandlers( (_, id: string, sessionId?: string) => { // Use async version to avoid blocking main process during CLI detection terminalManager.resumeClaudeAsync(id, sessionId).catch((error) => { - console.error('[terminal-handlers] Failed to resume Claude:', error); + debugError('[terminal-handlers] Failed to resume Claude:', error); }); } ); @@ -651,7 +667,7 @@ export function registerTerminalHandlers( IPC_CHANNELS.TERMINAL_ACTIVATE_DEFERRED_RESUME, (_, id: string) => { terminalManager.activateDeferredResume(id).catch((error) => { - console.error('[terminal-handlers] Failed to activate deferred Claude resume:', error); + debugError('[terminal-handlers] Failed to activate deferred Claude resume:', error); }); } ); diff --git a/apps/frontend/src/main/terminal/pty-manager.ts b/apps/frontend/src/main/terminal/pty-manager.ts index bd38c07a5c..2117917b0c 100644 --- a/apps/frontend/src/main/terminal/pty-manager.ts +++ b/apps/frontend/src/main/terminal/pty-manager.ts @@ -10,6 +10,7 @@ import type { TerminalProcess, WindowGetter } from './types'; import { IPC_CHANNELS } from '../../shared/constants'; import { getClaudeProfileManager } from '../claude-profile-manager'; import { readSettingsFile } from '../settings-utils'; +import { debugLog, debugError } from '../../shared/utils/debug-logger'; import type { SupportedTerminal } from '../../shared/types/settings'; /** @@ -84,7 +85,7 @@ export function spawnPtyProcess( const shellArgs = process.platform === 'win32' ? [] : ['-l']; - console.warn('[PtyManager] Spawning shell:', shell, shellArgs, '(preferred:', preferredTerminal || 'system', ')'); + debugLog('[PtyManager] Spawning shell:', shell, shellArgs, '(preferred:', preferredTerminal || 'system', ')'); // Create a clean environment without DEBUG to prevent Claude Code from // enabling debug mode when the Electron app is run in development mode. @@ -137,7 +138,7 @@ export function setupPtyHandlers( // Handle terminal exit ptyProcess.onExit(({ exitCode }) => { - console.warn('[PtyManager] Terminal exited:', id, 'code:', exitCode); + debugLog('[PtyManager] Terminal exited:', id, 'code:', exitCode); const win = getWindow(); if (win) { @@ -151,11 +152,97 @@ export function setupPtyHandlers( }); } +/** + * Constants for chunked write behavior + * CHUNKED_WRITE_THRESHOLD: Data larger than this (bytes) will be written in chunks + * CHUNK_SIZE: Size of each chunk - smaller chunks yield to event loop more frequently + */ +const CHUNKED_WRITE_THRESHOLD = 1000; +const CHUNK_SIZE = 100; + +/** + * Write queue per terminal to prevent interleaving of concurrent writes. + * Maps terminal ID to the last write Promise in the queue. + */ +const pendingWrites = new Map>(); + +/** + * Internal function to perform the actual write (chunked or direct) + * Returns a Promise that resolves when the write is complete + */ +function performWrite(terminal: TerminalProcess, data: string): Promise { + return new Promise((resolve) => { + // For large commands, write in chunks to prevent blocking + if (data.length > CHUNKED_WRITE_THRESHOLD) { + debugLog('[PtyManager:writeToPty] Large write detected, using chunked write'); + let offset = 0; + let chunkNum = 0; + + const writeChunk = () => { + // Check if terminal is still valid before writing + if (!terminal.pty) { + debugError('[PtyManager:writeToPty] Terminal PTY no longer valid, aborting chunked write'); + resolve(); + return; + } + + if (offset >= data.length) { + debugLog('[PtyManager:writeToPty] Chunked write completed, total chunks:', chunkNum); + resolve(); + return; + } + + const chunk = data.slice(offset, offset + CHUNK_SIZE); + chunkNum++; + try { + terminal.pty.write(chunk); + offset += CHUNK_SIZE; + // Use setImmediate to yield to the event loop between chunks + setImmediate(writeChunk); + } catch (error) { + debugError('[PtyManager:writeToPty] Chunked write FAILED at chunk', chunkNum, ':', error); + resolve(); // Resolve anyway - fire-and-forget semantics + } + }; + + // Start the chunked write after yielding + setImmediate(writeChunk); + } else { + try { + terminal.pty.write(data); + debugLog('[PtyManager:writeToPty] Write completed successfully'); + } catch (error) { + debugError('[PtyManager:writeToPty] Write FAILED:', error); + } + resolve(); + } + }); +} + /** * Write data to a PTY process + * Uses setImmediate to prevent blocking the event loop on large writes. + * Serializes writes per terminal to prevent interleaving of concurrent writes. */ export function writeToPty(terminal: TerminalProcess, data: string): void { - terminal.pty.write(data); + debugLog('[PtyManager:writeToPty] About to write to pty, data length:', data.length); + + // Get the previous write Promise for this terminal (if any) + const previousWrite = pendingWrites.get(terminal.id) || Promise.resolve(); + + // Chain this write after the previous one completes + const currentWrite = previousWrite.then(() => performWrite(terminal, data)); + + // Update the pending write for this terminal + pendingWrites.set(terminal.id, currentWrite); + + // Clean up the Map entry when done to prevent memory leaks + currentWrite.finally(() => { + // Only clean up if this is still the latest write + if (pendingWrites.get(terminal.id) === currentWrite) { + pendingWrites.delete(terminal.id); + } + }); } /** diff --git a/apps/frontend/src/main/terminal/terminal-manager.ts b/apps/frontend/src/main/terminal/terminal-manager.ts index 52b83a01f0..5e8fb4c8b8 100644 --- a/apps/frontend/src/main/terminal/terminal-manager.ts +++ b/apps/frontend/src/main/terminal/terminal-manager.ts @@ -17,6 +17,7 @@ import * as SessionHandler from './session-handler'; import * as TerminalLifecycle from './terminal-lifecycle'; import * as TerminalEventHandler from './terminal-event-handler'; import * as ClaudeIntegration from './claude-integration-handler'; +import { debugLog, debugError } from '../../shared/utils/debug-logger'; export class TerminalManager { private terminals: Map = new Map(); @@ -84,7 +85,7 @@ export class TerminalManager { onResumeNeeded: (terminalId, sessionId) => { // Use async version to avoid blocking main process this.resumeClaudeAsync(terminalId, sessionId).catch((error) => { - console.error('[terminal-manager] Failed to resume Claude session:', error); + debugError('[terminal-manager] Failed to resume Claude session:', error); }); } }, @@ -120,9 +121,14 @@ export class TerminalManager { * Send input to a terminal */ write(id: string, data: string): void { + debugLog('[TerminalManager:write] Writing to terminal:', id, 'data length:', data.length); const terminal = this.terminals.get(id); if (terminal) { + debugLog('[TerminalManager:write] Terminal found, calling writeToPty...'); PtyManager.writeToPty(terminal, data); + debugLog('[TerminalManager:write] writeToPty completed'); + } else { + debugError('[TerminalManager:write] Terminal NOT found:', id); } } @@ -311,7 +317,7 @@ export class TerminalManager { onResumeNeeded: (terminalId, sessionId) => { // Use async version to avoid blocking main process this.resumeClaudeAsync(terminalId, sessionId).catch((error) => { - console.error('[terminal-manager] Failed to resume Claude session:', error); + debugError('[terminal-manager] Failed to resume Claude session:', error); }); } }, diff --git a/apps/frontend/src/renderer/components/RateLimitModal.tsx b/apps/frontend/src/renderer/components/RateLimitModal.tsx index b19c842afd..9d0e2e322d 100644 --- a/apps/frontend/src/renderer/components/RateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/RateLimitModal.tsx @@ -22,6 +22,8 @@ import { Label } from './ui/label'; import { Input } from './ui/input'; import { useRateLimitStore } from '../stores/rate-limit-store'; import { useClaudeProfileStore, loadClaudeProfiles, switchTerminalToProfile } from '../stores/claude-profile-store'; +import { useToast } from '../hooks/use-toast'; +import { debugError } from '../../shared/utils/debug-logger'; const CLAUDE_UPGRADE_URL = 'https://claude.ai/upgrade'; @@ -29,6 +31,7 @@ export function RateLimitModal() { const { t } = useTranslation('common'); const { isModalOpen, rateLimitInfo, hideRateLimitModal, clearPendingRateLimit } = useRateLimitStore(); const { profiles, activeProfileId, isSwitching } = useClaudeProfileStore(); + const { toast } = useToast(); const [selectedProfileId, setSelectedProfileId] = useState(null); const [autoSwitchEnabled, setAutoSwitchEnabled] = useState(false); const [isLoadingSettings, setIsLoadingSettings] = useState(false); @@ -64,7 +67,7 @@ export function RateLimitModal() { setAutoSwitchEnabled(result.data.autoSwitchOnRateLimit); } } catch (err) { - console.error('Failed to load auto-switch settings:', err); + debugError('[RateLimitModal] Failed to load auto-switch settings:', err); } }; @@ -77,7 +80,7 @@ export function RateLimitModal() { }); setAutoSwitchEnabled(enabled); } catch (err) { - console.error('Failed to update auto-switch settings:', err); + debugError('[RateLimitModal] Failed to update auto-switch settings:', err); } finally { setIsLoadingSettings(false); } @@ -116,22 +119,26 @@ export function RateLimitModal() { // Close the modal so user can see the terminal hideRateLimitModal(); - // Alert the user about the terminal - alert( - `A terminal has been opened to authenticate "${profileName}".\n\n` + - `Steps to complete:\n` + - `1. Check the "Agent Terminals" section in the sidebar\n` + - `2. Complete the OAuth login in your browser\n` + - `3. The token will be saved automatically\n\n` + - `Once done, return here and the account will be available.` - ); + // Notify the user about the terminal (non-blocking) + toast({ + title: t('rateLimit.toast.authenticating', { profileName }), + description: t('rateLimit.toast.checkTerminal'), + }); } else { - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('rateLimit.toast.authStartFailed'), + description: initResult.error || t('rateLimit.toast.tryAgain'), + }); } } } catch (err) { - console.error('Failed to add profile:', err); - alert('Failed to add profile. Please try again.'); + debugError('[RateLimitModal] Failed to add profile:', err); + toast({ + variant: 'destructive', + title: t('rateLimit.toast.addProfileFailed'), + description: t('rateLimit.toast.tryAgain'), + }); } finally { setIsAddingProfile(false); } diff --git a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx index 116a091035..ae98edea44 100644 --- a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { AlertCircle, ExternalLink, Clock, RefreshCw, User, ChevronDown, Check, Star, Zap, FileText, ListTodo, Map, Lightbulb, Plus } from 'lucide-react'; import { Dialog, @@ -21,6 +22,8 @@ import { Label } from './ui/label'; import { Input } from './ui/input'; import { useRateLimitStore } from '../stores/rate-limit-store'; import { useClaudeProfileStore, loadClaudeProfiles } from '../stores/claude-profile-store'; +import { useToast } from '../hooks/use-toast'; +import { debugError } from '../../shared/utils/debug-logger'; import type { SDKRateLimitInfo } from '../../shared/types'; const CLAUDE_UPGRADE_URL = 'https://claude.ai/upgrade'; @@ -55,6 +58,8 @@ function getSourceIcon(source: SDKRateLimitInfo['source']) { export function SDKRateLimitModal() { const { isSDKModalOpen, sdkRateLimitInfo, hideSDKRateLimitModal, clearPendingRateLimit } = useRateLimitStore(); const { profiles, isSwitching, setSwitching } = useClaudeProfileStore(); + const { toast } = useToast(); + const { t } = useTranslation('common'); const [selectedProfileId, setSelectedProfileId] = useState(null); const [autoSwitchEnabled, setAutoSwitchEnabled] = useState(false); const [isLoadingSettings, setIsLoadingSettings] = useState(false); @@ -108,7 +113,7 @@ export function SDKRateLimitModal() { setAutoSwitchEnabled(result.data.autoSwitchOnRateLimit); } } catch (err) { - console.error('Failed to load auto-switch settings:', err); + debugError('[SDKRateLimitModal] Failed to load auto-switch settings:', err); } }; @@ -121,7 +126,7 @@ export function SDKRateLimitModal() { }); setAutoSwitchEnabled(enabled); } catch (err) { - console.error('Failed to update auto-switch settings:', err); + debugError('[SDKRateLimitModal] Failed to update auto-switch settings:', err); } finally { setIsLoadingSettings(false); } @@ -160,22 +165,26 @@ export function SDKRateLimitModal() { // Close the modal so user can see the terminal hideSDKRateLimitModal(); - // Alert the user about the terminal - alert( - `A terminal has been opened to authenticate "${profileName}".\n\n` + - `Steps to complete:\n` + - `1. Check the "Agent Terminals" section in the sidebar\n` + - `2. Complete the OAuth login in your browser\n` + - `3. The token will be saved automatically\n\n` + - `Once done, return here and the account will be available.` - ); + // Notify the user about the terminal (non-blocking) + toast({ + title: t('rateLimit.toast.authenticating', { profileName }), + description: t('rateLimit.toast.checkTerminal'), + }); } else { - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('rateLimit.toast.authStartFailed'), + description: initResult.error || t('rateLimit.toast.tryAgain'), + }); } } } catch (err) { - console.error('Failed to add profile:', err); - alert('Failed to add profile. Please try again.'); + debugError('[SDKRateLimitModal] Failed to add profile:', err); + toast({ + variant: 'destructive', + title: t('rateLimit.toast.addProfileFailed'), + description: t('rateLimit.toast.tryAgain'), + }); } finally { setIsAddingProfile(false); } @@ -204,7 +213,7 @@ export function SDKRateLimitModal() { clearPendingRateLimit(); } } catch (err) { - console.error('Failed to retry with profile:', err); + debugError('[SDKRateLimitModal] Failed to retry with profile:', err); } finally { setIsRetrying(false); setSwitching(false); @@ -236,11 +245,11 @@ export function SDKRateLimitModal() { - Claude Code Rate Limit + {t('rateLimit.sdk.title')} - {sourceName} was interrupted due to usage limits. + {t('rateLimit.sdk.interrupted', { source: sourceName })} {currentProfile && ( (Profile: {currentProfile.name}) )} @@ -253,26 +262,26 @@ export function SDKRateLimitModal() { {swapInfo?.wasAutoSwapped ? ( <>

- {swapInfo.swapReason === 'proactive' ? '✓ Proactive Swap' : '⚡ Reactive Swap'} + {swapInfo.swapReason === 'proactive' ? t('rateLimit.sdk.proactiveSwap') : t('rateLimit.sdk.reactiveSwap')}

{swapInfo.swapReason === 'proactive' - ? `Automatically switched from ${swapInfo.swappedFrom} to ${swapInfo.swappedTo} before hitting rate limit.` - : `Rate limit hit on ${swapInfo.swappedFrom}. Automatically switched to ${swapInfo.swappedTo} and restarted.` + ? t('rateLimit.sdk.proactiveSwapDesc', { from: swapInfo.swappedFrom, to: swapInfo.swappedTo }) + : t('rateLimit.sdk.reactiveSwapDesc', { from: swapInfo.swappedFrom, to: swapInfo.swappedTo }) }

- Your work continued without interruption. + {t('rateLimit.sdk.continueWithoutInterruption')}

) : ( <> -

Rate limit reached

+

{t('rateLimit.sdk.rateLimitReached')}

- The operation was stopped because {currentProfile?.name || 'your account'} reached its usage limit. + {t('rateLimit.sdk.operationStopped', { account: currentProfile?.name || 'your account' })} {hasMultipleProfiles - ? ' Switch to another account below to continue.' - : ' Add another Claude account to continue working.'} + ? ' ' + t('rateLimit.sdk.switchBelow') + : ' ' + t('rateLimit.sdk.addAccountToContinue')}

)} @@ -286,7 +295,7 @@ export function SDKRateLimitModal() { onClick={() => window.open(CLAUDE_UPGRADE_URL, '_blank')} > - Upgrade to Pro for Higher Limits + {t('rateLimit.sdk.upgradeToProButton')} {/* Reset time info */} @@ -295,12 +304,12 @@ export function SDKRateLimitModal() {

- Resets {sdkRateLimitInfo.resetTime} + {t('rateLimit.sdk.resetsLabel', { time: sdkRateLimitInfo.resetTime })}

{sdkRateLimitInfo.limitType === 'weekly' - ? 'Weekly limit - resets in about a week' - : 'Session limit - resets in a few hours'} + ? t('rateLimit.sdk.weeklyLimit') + : t('rateLimit.sdk.sessionLimit')}

@@ -310,7 +319,7 @@ export function SDKRateLimitModal() {

- {hasMultipleProfiles ? 'Switch Account & Retry' : 'Use Another Account'} + {hasMultipleProfiles ? t('rateLimit.sdk.switchAccountRetry') : t('rateLimit.useAnotherAccount')}

{hasMultipleProfiles ? ( @@ -379,12 +388,12 @@ export function SDKRateLimitModal() { {isRetrying || isSwitching ? ( <> - Retrying... + {t('rateLimit.sdk.retrying')} ) : ( <> - Retry + {t('rateLimit.sdk.retry')} )} @@ -400,7 +409,7 @@ export function SDKRateLimitModal() { {availableProfiles.length > 0 && (
)} - Add + {t('rateLimit.sdk.add')}

@@ -476,20 +485,19 @@ export function SDKRateLimitModal() { {/* Info about what was interrupted */}

-

What happened:

+

{t('rateLimit.sdk.whatHappened')}

- The {sourceName.toLowerCase()} operation was stopped because your Claude account - ({currentProfile?.name || 'Default'}) reached its usage limit. + {t('rateLimit.sdk.whatHappenedDesc', { source: sourceName.toLowerCase(), account: currentProfile?.name || 'Default' })} {hasMultipleProfiles - ? ' You can switch to another account and retry, or add more accounts above.' - : ' Add another Claude account above to continue working, or wait for the limit to reset.'} + ? ' ' + t('rateLimit.sdk.switchRetryOrAdd') + : ' ' + t('rateLimit.sdk.addOrWait')}

diff --git a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx index 4fad5f3337..e175c0df4d 100644 --- a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx @@ -27,6 +27,7 @@ import { Card, CardContent } from '../ui/card'; import { cn } from '../../lib/utils'; import { loadClaudeProfiles as loadGlobalClaudeProfiles } from '../../stores/claude-profile-store'; import { useClaudeLoginTerminal } from '../../hooks/useClaudeLoginTerminal'; +import { useToast } from '../../hooks/use-toast'; import type { ClaudeProfile } from '../../../shared/types'; interface OAuthStepProps { @@ -42,6 +43,7 @@ interface OAuthStepProps { */ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { const { t } = useTranslation('onboarding'); + const { toast } = useToast(); // Claude Profiles state const [claudeProfiles, setClaudeProfiles] = useState([]); @@ -102,13 +104,16 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { if (info.success && info.profileId) { // Reload profiles to show updated state await loadClaudeProfiles(); - // Show simple success notification - alert(`✅ Profile authenticated successfully!\n\n${info.email ? `Account: ${info.email}` : 'Authentication complete.'}\n\nYou can now use this profile.`); + // Show simple success notification (non-blocking) + toast({ + title: t('oauth.toast.authSuccess'), + description: info.email ? t('oauth.toast.authSuccessWithEmail', { email: info.email }) : t('oauth.toast.authSuccessGeneric'), + }); } }); return unsubscribe; - }, []); + }, [t, toast]); // Profile management handlers - following patterns from IntegrationSettings.tsx const handleAddProfile = async () => { @@ -152,12 +157,20 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { // Users can see the 'claude setup-token' output directly } else { await loadClaudeProfiles(); - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('oauth.toast.authStartFailed'), + description: initResult.error || t('oauth.toast.tryAgain'), + }); } } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to add profile'); - alert('Failed to add profile. Please try again.'); + toast({ + variant: 'destructive', + title: t('oauth.toast.addProfileFailed'), + description: t('oauth.toast.tryAgain'), + }); } finally { setIsAddingProfile(false); } @@ -224,13 +237,21 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { try { const initResult = await window.electronAPI.initializeClaudeProfile(profileId); if (!initResult.success) { - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('oauth.toast.authStartFailed'), + description: initResult.error || t('oauth.toast.tryAgain'), + }); } // Note: If successful, the terminal is now visible in the UI via the onTerminalAuthCreated event // Users can see the 'claude setup-token' output and complete OAuth flow directly } catch (err) { setError(err instanceof Error ? err.message : 'Failed to authenticate profile'); - alert('Failed to start authentication. Please try again.'); + toast({ + variant: 'destructive', + title: t('oauth.toast.authStartFailed'), + description: t('oauth.toast.tryAgain'), + }); } finally { setAuthenticatingProfileId(null); } @@ -267,12 +288,24 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { setManualToken(''); setManualTokenEmail(''); setShowManualToken(false); + toast({ + title: t('oauth.toast.tokenSaved'), + description: t('oauth.toast.tokenSavedDescription'), + }); } else { - alert(`Failed to save token: ${result.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('oauth.toast.tokenSaveFailed'), + description: result.error || t('oauth.toast.tryAgain'), + }); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save token'); - alert('Failed to save token. Please try again.'); + toast({ + variant: 'destructive', + title: t('oauth.toast.tokenSaveFailed'), + description: t('oauth.toast.tryAgain'), + }); } finally { setSavingTokenProfileId(null); } @@ -293,10 +326,10 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {

- Configure Claude Authentication + {t('oauth.configureTitle')}

- Add your Claude accounts to enable AI features + {t('oauth.addAccountsDesc')}

@@ -329,7 +362,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {

- Add multiple Claude subscriptions to automatically switch between them when you hit rate limits. + {t('oauth.multiAccountInfo')}

@@ -359,7 +392,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
{claudeProfiles.length === 0 ? (
-

No accounts configured yet

+

{t('oauth.noAccountsYet')}

) : (
@@ -421,22 +454,22 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
{profile.name} {profile.isDefault && ( - Default + {t('oauth.badges.default')} )} {profile.id === activeProfileId && ( - Active + {t('oauth.badges.active')} )} {(profile.oauthToken || (profile.isDefault && profile.configDir)) ? ( - Authenticated + {t('oauth.badges.authenticated')} ) : ( - Needs Auth + {t('oauth.badges.needsAuth')} )}
@@ -463,7 +496,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { ) : ( )} - Authenticate + {t('oauth.buttons.authenticate')} )} {profile.id !== activeProfileId && ( @@ -474,7 +507,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { className="gap-1 h-7 text-xs" > - Set Active + {t('oauth.buttons.setActive')} )} {/* Toggle token entry button */} @@ -643,7 +676,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { onClick={onBack} className="text-muted-foreground hover:text-foreground" > - Back + {t('oauth.buttons.back')}
diff --git a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx index fbc0dfa0d6..718d1b4b69 100644 --- a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx @@ -29,6 +29,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { SettingsSection } from './SettingsSection'; import { loadClaudeProfiles as loadGlobalClaudeProfiles } from '../../stores/claude-profile-store'; import { useClaudeLoginTerminal } from '../../hooks/useClaudeLoginTerminal'; +import { useToast } from '../../hooks/use-toast'; +import { debugLog, debugError } from '../../../shared/utils/debug-logger'; import type { AppSettings, ClaudeProfile, ClaudeAutoSwitchSettings } from '../../../shared/types'; interface IntegrationSettingsProps { @@ -43,6 +45,7 @@ interface IntegrationSettingsProps { export function IntegrationSettings({ settings, onSettingsChange, isOpen }: IntegrationSettingsProps) { const { t } = useTranslation('settings'); const { t: tCommon } = useTranslation('common'); + const { toast } = useToast(); // Password visibility toggle for global API keys const [showGlobalOpenAIKey, setShowGlobalOpenAIKey] = useState(false); @@ -83,13 +86,16 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte if (info.success && info.profileId) { // Reload profiles to show updated state await loadClaudeProfiles(); - // Show simple success notification - alert(`✅ Profile authenticated successfully!\n\n${info.email ? `Account: ${info.email}` : 'Authentication complete.'}\n\nYou can now use this profile.`); + // Show simple success notification (non-blocking) + toast({ + title: t('integrations.toast.authSuccess'), + description: info.email ? t('integrations.toast.authSuccessWithEmail', { email: info.email }) : t('integrations.toast.authSuccessGeneric'), + }); } }); return unsubscribe; - }, []); + }, [t, toast]); const loadClaudeProfiles = async () => { setIsLoadingProfiles(true); @@ -102,7 +108,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte await loadGlobalClaudeProfiles(); } } catch (err) { - console.error('Failed to load Claude profiles:', err); + debugError('[IntegrationSettings] Failed to load Claude profiles:', err); } finally { setIsLoadingProfiles(false); } @@ -135,12 +141,20 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte // Users can see the 'claude setup-token' output directly } else { await loadClaudeProfiles(); - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('integrations.toast.authStartFailed'), + description: initResult.error || t('integrations.toast.tryAgain'), + }); } } } catch (err) { - console.error('Failed to add profile:', err); - alert('Failed to add profile. Please try again.'); + debugError('[IntegrationSettings] Failed to add profile:', err); + toast({ + variant: 'destructive', + title: t('integrations.toast.addProfileFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { setIsAddingProfile(false); } @@ -154,7 +168,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte await loadClaudeProfiles(); } } catch (err) { - console.error('Failed to delete profile:', err); + debugError('[IntegrationSettings] Failed to delete profile:', err); } finally { setDeletingProfileId(null); } @@ -179,7 +193,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte await loadClaudeProfiles(); } } catch (err) { - console.error('Failed to rename profile:', err); + debugError('[IntegrationSettings] Failed to rename profile:', err); } finally { setEditingProfileId(null); setEditingProfileName(''); @@ -194,23 +208,35 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte await loadGlobalClaudeProfiles(); } } catch (err) { - console.error('Failed to set active profile:', err); + debugError('[IntegrationSettings] Failed to set active profile:', err); } }; const handleAuthenticateProfile = async (profileId: string) => { + debugLog('[IntegrationSettings] handleAuthenticateProfile called for:', profileId); setAuthenticatingProfileId(profileId); try { + debugLog('[IntegrationSettings] Calling initializeClaudeProfile IPC...'); const initResult = await window.electronAPI.initializeClaudeProfile(profileId); + debugLog('[IntegrationSettings] IPC returned:', initResult); if (!initResult.success) { - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('integrations.toast.authStartFailed'), + description: initResult.error || t('integrations.toast.tryAgain'), + }); } // Note: If successful, the terminal is now visible in the UI via the onTerminalAuthCreated event // Users can see the 'claude setup-token' output and complete OAuth flow directly } catch (err) { - console.error('Failed to authenticate profile:', err); - alert('Failed to start authentication. Please try again.'); + debugError('[IntegrationSettings] Failed to authenticate profile:', err); + toast({ + variant: 'destructive', + title: t('integrations.toast.authStartFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { + debugLog('[IntegrationSettings] finally block - clearing authenticatingProfileId'); setAuthenticatingProfileId(null); } }; @@ -245,12 +271,24 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte setManualToken(''); setManualTokenEmail(''); setShowManualToken(false); + toast({ + title: t('integrations.toast.tokenSaved'), + description: t('integrations.toast.tokenSavedDescription'), + }); } else { - alert(`Failed to save token: ${result.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('integrations.toast.tokenSaveFailed'), + description: result.error || t('integrations.toast.tryAgain'), + }); } } catch (err) { - console.error('Failed to save token:', err); - alert('Failed to save token. Please try again.'); + debugError('[IntegrationSettings] Failed to save token:', err); + toast({ + variant: 'destructive', + title: t('integrations.toast.tokenSaveFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { setSavingTokenProfileId(null); } @@ -265,7 +303,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte setAutoSwitchSettings(result.data); } } catch (err) { - console.error('Failed to load auto-switch settings:', err); + debugError('[IntegrationSettings] Failed to load auto-switch settings:', err); } finally { setIsLoadingAutoSwitch(false); } @@ -279,11 +317,19 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte if (result.success) { await loadAutoSwitchSettings(); } else { - alert(`Failed to update settings: ${result.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: t('integrations.toast.settingsUpdateFailed'), + description: result.error || t('integrations.toast.tryAgain'), + }); } } catch (err) { - console.error('Failed to update auto-switch settings:', err); - alert('Failed to update settings. Please try again.'); + debugError('[IntegrationSettings] Failed to update auto-switch settings:', err); + toast({ + variant: 'destructive', + title: t('integrations.toast.settingsUpdateFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { setIsLoadingAutoSwitch(false); } diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index 2cde709475..9b346eaced 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -147,6 +147,40 @@ "ideation": "Ideation", "titleGenerator": "Title Generator", "claude": "Claude" + }, + "toast": { + "authenticating": "Authenticating \"{{profileName}}\"", + "checkTerminal": "Check the Agent Terminals section in the sidebar to complete OAuth login.", + "authStartFailed": "Failed to start authentication", + "addProfileFailed": "Failed to add profile", + "tryAgain": "Please try again." + }, + "sdk": { + "title": "Claude Code Rate Limit", + "interrupted": "{{source}} was interrupted due to usage limits.", + "proactiveSwap": "✓ Proactive Swap", + "reactiveSwap": "⚡ Reactive Swap", + "proactiveSwapDesc": "Automatically switched from {{from}} to {{to}} before hitting rate limit.", + "reactiveSwapDesc": "Rate limit hit on {{from}}. Automatically switched to {{to}} and restarted.", + "continueWithoutInterruption": "Your work continued without interruption.", + "rateLimitReached": "Rate limit reached", + "operationStopped": "The operation was stopped because {{account}} reached its usage limit.", + "switchBelow": "Switch to another account below to continue.", + "addAccountToContinue": "Add another Claude account to continue working.", + "upgradeToProButton": "Upgrade to Pro for Higher Limits", + "resetsLabel": "Resets {{time}}", + "weeklyLimit": "Weekly limit - resets in about a week", + "sessionLimit": "Session limit - resets in a few hours", + "switchAccountRetry": "Switch Account & Retry", + "retrying": "Retrying...", + "retry": "Retry", + "autoSwitchRetryLabel": "Auto-switch & retry on rate limit", + "add": "Add", + "whatHappened": "What happened:", + "whatHappenedDesc": "The {{source}} operation was stopped because your Claude account ({{account}}) reached its usage limit.", + "switchRetryOrAdd": "You can switch to another account and retry, or add more accounts above.", + "addOrWait": "Add another Claude account above to continue working, or wait for the limit to reset.", + "close": "Close" } }, "prReview": { diff --git a/apps/frontend/src/shared/i18n/locales/en/onboarding.json b/apps/frontend/src/shared/i18n/locales/en/onboarding.json index d2b5f77c93..ba48579588 100644 --- a/apps/frontend/src/shared/i18n/locales/en/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/en/onboarding.json @@ -31,8 +31,50 @@ "oauth": { "title": "Claude Authentication", "description": "Connect your Claude account to enable AI features", + "configureTitle": "Configure Claude Authentication", + "addAccountsDesc": "Add your Claude accounts to enable AI features", + "multiAccountInfo": "Add multiple Claude subscriptions to automatically switch between them when you hit rate limits.", + "noAccountsYet": "No accounts configured yet", + "badges": { + "default": "Default", + "active": "Active", + "authenticated": "Authenticated", + "needsAuth": "Needs Auth" + }, + "buttons": { + "authenticate": "Authenticate", + "setActive": "Set Active", + "rename": "Rename", + "delete": "Delete", + "add": "Add", + "adding": "Adding...", + "showToken": "Show Token", + "hideToken": "Hide Token", + "copyToken": "Copy Token", + "back": "Back", + "continue": "Continue", + "skip": "Skip" + }, + "labels": { + "accountName": "Account name", + "namePlaceholder": "Profile name (e.g., Work, Personal)", + "tokenLabel": "OAuth Token", + "tokenPlaceholder": "Enter token here", + "tokenHint": "Paste the token shown in your terminal after completing OAuth login." + }, "keychainTitle": "Secure Storage", - "keychainDescription": "Your tokens are encrypted using your system's keychain. You may see a password prompt from macOS — click \"Always Allow\" to avoid seeing it again." + "keychainDescription": "Your tokens are encrypted using your system's keychain. You may see a password prompt from macOS — click \"Always Allow\" to avoid seeing it again.", + "toast": { + "authSuccess": "Profile authenticated successfully", + "authSuccessWithEmail": "Account: {{email}}", + "authSuccessGeneric": "Authentication complete. You can now use this profile.", + "authStartFailed": "Failed to start authentication", + "addProfileFailed": "Failed to add profile", + "tokenSaved": "Token saved", + "tokenSavedDescription": "Your token has been saved successfully.", + "tokenSaveFailed": "Failed to save token", + "tryAgain": "Please try again." + } }, "memory": { "title": "Memory", diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index c102d26ac2..04d4c0cbcb 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -436,7 +436,19 @@ "apiKeys": "API Keys", "apiKeysInfo": "Keys set here are used as defaults. Individual projects can override these in their settings.", "openaiKey": "OpenAI API Key", - "openaiKeyDescription": "Required for Graphiti memory backend (embeddings)" + "openaiKeyDescription": "Required for Graphiti memory backend (embeddings)", + "toast": { + "authSuccess": "Profile Authenticated", + "authSuccessWithEmail": "Connected as {{email}}", + "authSuccessGeneric": "Authentication complete. You can now use this profile.", + "authStartFailed": "Authentication Failed", + "addProfileFailed": "Failed to Add Profile", + "tokenSaved": "Token Saved", + "tokenSavedDescription": "Your token has been saved successfully.", + "tokenSaveFailed": "Failed to Save Token", + "settingsUpdateFailed": "Failed to Update Settings", + "tryAgain": "Please try again." + } }, "debug": { "title": "Debug & Logs", diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index 7371ec4b82..d2d8ebb804 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -147,6 +147,40 @@ "ideation": "Idéation", "titleGenerator": "Générateur de titre", "claude": "Claude" + }, + "toast": { + "authenticating": "Authentification de « {{profileName}} »", + "checkTerminal": "Vérifiez la section Terminaux Agent dans la barre latérale pour terminer la connexion OAuth.", + "authStartFailed": "Échec du démarrage de l'authentification", + "addProfileFailed": "Échec de l'ajout du profil", + "tryAgain": "Veuillez réessayer." + }, + "sdk": { + "title": "Limite de débit Claude Code", + "interrupted": "{{source}} a été interrompu en raison des limites d'utilisation.", + "proactiveSwap": "✓ Échange proactif", + "reactiveSwap": "⚡ Échange réactif", + "proactiveSwapDesc": "Basculé automatiquement de {{from}} vers {{to}} avant d'atteindre la limite.", + "reactiveSwapDesc": "Limite atteinte sur {{from}}. Basculé automatiquement vers {{to}} et redémarré.", + "continueWithoutInterruption": "Votre travail a continué sans interruption.", + "rateLimitReached": "Limite atteinte", + "operationStopped": "L'opération a été arrêtée car {{account}} a atteint sa limite d'utilisation.", + "switchBelow": "Passez à un autre compte ci-dessous pour continuer.", + "addAccountToContinue": "Ajoutez un autre compte Claude pour continuer à travailler.", + "upgradeToProButton": "Passez à Pro pour des limites plus élevées", + "resetsLabel": "Réinitialisation {{time}}", + "weeklyLimit": "Limite hebdomadaire - se réinitialise dans environ une semaine", + "sessionLimit": "Limite de session - se réinitialise dans quelques heures", + "switchAccountRetry": "Changer de compte et réessayer", + "retrying": "Nouvelle tentative...", + "retry": "Réessayer", + "autoSwitchRetryLabel": "Basculement auto et réessai en cas de limite", + "add": "Ajouter", + "whatHappened": "Ce qui s'est passé :", + "whatHappenedDesc": "L'opération {{source}} a été arrêtée car votre compte Claude ({{account}}) a atteint sa limite d'utilisation.", + "switchRetryOrAdd": "Vous pouvez passer à un autre compte et réessayer, ou ajouter plus de comptes ci-dessus.", + "addOrWait": "Ajoutez un autre compte Claude ci-dessus pour continuer à travailler, ou attendez la réinitialisation de la limite.", + "close": "Fermer" } }, "prReview": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json index c494115c48..f95ef0cb2a 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json @@ -31,8 +31,50 @@ "oauth": { "title": "Authentification Claude", "description": "Connectez votre compte Claude pour activer les fonctionnalités IA", + "configureTitle": "Configurer l'authentification Claude", + "addAccountsDesc": "Ajoutez vos comptes Claude pour activer les fonctionnalités IA", + "multiAccountInfo": "Ajoutez plusieurs abonnements Claude pour basculer automatiquement entre eux lorsque vous atteignez les limites.", + "noAccountsYet": "Aucun compte configuré", + "badges": { + "default": "Par défaut", + "active": "Actif", + "authenticated": "Authentifié", + "needsAuth": "Auth requise" + }, + "buttons": { + "authenticate": "Authentifier", + "setActive": "Définir actif", + "rename": "Renommer", + "delete": "Supprimer", + "add": "Ajouter", + "adding": "Ajout...", + "showToken": "Afficher le jeton", + "hideToken": "Masquer le jeton", + "copyToken": "Copier le jeton", + "back": "Retour", + "continue": "Continuer", + "skip": "Passer" + }, + "labels": { + "accountName": "Nom du compte", + "namePlaceholder": "Nom du profil (ex: Travail, Personnel)", + "tokenLabel": "Jeton OAuth", + "tokenPlaceholder": "Entrez le jeton ici", + "tokenHint": "Collez le jeton affiché dans votre terminal après la connexion OAuth." + }, "keychainTitle": "Stockage sécurisé", - "keychainDescription": "Vos jetons sont chiffrés à l'aide du trousseau de clés de votre système. Une demande de mot de passe macOS peut apparaître — cliquez sur « Toujours autoriser » pour ne plus la revoir." + "keychainDescription": "Vos jetons sont chiffrés à l'aide du trousseau de clés de votre système. Une demande de mot de passe macOS peut apparaître — cliquez sur « Toujours autoriser » pour ne plus la revoir.", + "toast": { + "authSuccess": "Profil authentifié avec succès", + "authSuccessWithEmail": "Compte : {{email}}", + "authSuccessGeneric": "Authentification terminée. Vous pouvez maintenant utiliser ce profil.", + "authStartFailed": "Échec du démarrage de l'authentification", + "addProfileFailed": "Échec de l'ajout du profil", + "tokenSaved": "Jeton enregistré", + "tokenSavedDescription": "Votre jeton a été enregistré avec succès.", + "tokenSaveFailed": "Échec de l'enregistrement du jeton", + "tryAgain": "Veuillez réessayer." + } }, "memory": { "title": "Mémoire", diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index ab972347de..5888c9e774 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -436,7 +436,19 @@ "apiKeys": "Clés API", "apiKeysInfo": "Les clés définies ici sont utilisées par défaut. Les projets individuels peuvent les remplacer dans leurs paramètres.", "openaiKey": "Clé API OpenAI", - "openaiKeyDescription": "Requise pour le backend mémoire Graphiti (embeddings)" + "openaiKeyDescription": "Requise pour le backend mémoire Graphiti (embeddings)", + "toast": { + "authSuccess": "Profil authentifié", + "authSuccessWithEmail": "Connecté en tant que {{email}}", + "authSuccessGeneric": "Authentification terminée. Vous pouvez maintenant utiliser ce profil.", + "authStartFailed": "Échec de l'authentification", + "addProfileFailed": "Échec de l'ajout du profil", + "tokenSaved": "Token enregistré", + "tokenSavedDescription": "Votre token a été enregistré avec succès.", + "tokenSaveFailed": "Échec de l'enregistrement du token", + "settingsUpdateFailed": "Échec de la mise à jour des paramètres", + "tryAgain": "Veuillez réessayer." + } }, "debug": { "title": "Debug & Logs",