From b03e44667dfa45b72fcf1cedb1ac8524b61613da Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Thu, 8 Jan 2026 14:23:38 -0500 Subject: [PATCH 1/6] fix(ACS-175): Resolve integrations freeze and improve rate limit handling --- .../main/ipc-handlers/terminal-handlers.ts | 37 +++++++--- .../frontend/src/main/terminal/pty-manager.ts | 44 +++++++++++- .../src/main/terminal/terminal-manager.ts | 5 ++ .../renderer/components/RateLimitModal.tsx | 28 +++++--- .../renderer/components/SDKRateLimitModal.tsx | 28 +++++--- .../components/onboarding/OAuthStep.tsx | 51 +++++++++++--- .../settings/IntegrationSettings.tsx | 69 +++++++++++++++---- .../src/shared/i18n/locales/en/settings.json | 14 +++- .../src/shared/i18n/locales/fr/settings.json | 14 +++- 9 files changed, 234 insertions(+), 56 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts index 96edd3c437..ba2848f8d4 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts @@ -312,12 +312,17 @@ export function registerTerminalHandlers( ipcMain.handle( IPC_CHANNELS.CLAUDE_PROFILE_INITIALIZE, async (_, profileId: string): Promise => { + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Handler called for profileId:', profileId); try { + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile manager...'); const profileManager = getClaudeProfileManager(); + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile...'); const profile = profileManager.getProfile(profileId); if (!profile) { + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile not found!'); return { success: false, error: 'Profile not found' }; } + console.log('[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'; + console.log('[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 + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Calling terminalManager.create...'); const createResult = await terminalManager.create({ id: terminalId, cwd: homeDir }); + console.log('[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 + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Waiting 500ms for terminal init...'); await new Promise(resolve => setTimeout(resolve, 500)); + console.log('[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 + console.log('[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(); + console.log('[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`; } + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Built login command, length:', loginCommand.length); + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Login command:', loginCommand); debugLog('[IPC] Sending login command to terminal:', loginCommand); // Write the login command to the terminal + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Writing command to terminal...'); terminalManager.write(terminalId, `${loginCommand}\r`); + console.log('[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 + console.log('[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( }); } + console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Returning success!'); return { success: true, data: { @@ -407,6 +423,7 @@ export function registerTerminalHandlers( } }; } catch (error) { + console.error('[IPC:CLAUDE_PROFILE_INITIALIZE] EXCEPTION:', error); debugError('[IPC] Failed to initialize Claude profile:', error); return { success: false, diff --git a/apps/frontend/src/main/terminal/pty-manager.ts b/apps/frontend/src/main/terminal/pty-manager.ts index bd38c07a5c..ac911134bc 100644 --- a/apps/frontend/src/main/terminal/pty-manager.ts +++ b/apps/frontend/src/main/terminal/pty-manager.ts @@ -153,9 +153,51 @@ export function setupPtyHandlers( /** * Write data to a PTY process + * Uses setImmediate to prevent blocking the event loop on large writes */ export function writeToPty(terminal: TerminalProcess, data: string): void { - terminal.pty.write(data); + console.log('[PtyManager:writeToPty] About to write to pty, data length:', data.length); + + // For large commands, write in chunks to prevent blocking + if (data.length > 1000) { + console.log('[PtyManager:writeToPty] Large write detected, using chunked write'); + const chunkSize = 100; // Smaller chunks + let offset = 0; + let chunkNum = 0; + + const writeChunk = () => { + if (offset >= data.length) { + console.log('[PtyManager:writeToPty] Chunked write completed, total chunks:', chunkNum); + return; + } + + const chunk = data.slice(offset, offset + chunkSize); + chunkNum++; + console.log('[PtyManager:writeToPty] Writing chunk', chunkNum, 'offset:', offset, 'size:', chunk.length); + try { + terminal.pty.write(chunk); + console.log('[PtyManager:writeToPty] Chunk', chunkNum, 'written'); + offset += chunkSize; + // Use setImmediate to yield to the event loop between chunks + setImmediate(writeChunk); + } catch (error) { + console.error('[PtyManager:writeToPty] Chunked write FAILED at chunk', chunkNum, 'offset', offset, ':', error); + } + }; + + // Start the chunked write after yielding + console.log('[PtyManager:writeToPty] Scheduling first chunk...'); + setImmediate(writeChunk); + console.log('[PtyManager:writeToPty] First chunk scheduled, returning'); + } else { + try { + terminal.pty.write(data); + console.log('[PtyManager:writeToPty] Write completed successfully'); + } catch (error) { + console.error('[PtyManager:writeToPty] Write FAILED:', error); + throw error; + } + } } /** diff --git a/apps/frontend/src/main/terminal/terminal-manager.ts b/apps/frontend/src/main/terminal/terminal-manager.ts index 52b83a01f0..e3e4808133 100644 --- a/apps/frontend/src/main/terminal/terminal-manager.ts +++ b/apps/frontend/src/main/terminal/terminal-manager.ts @@ -120,9 +120,14 @@ export class TerminalManager { * Send input to a terminal */ write(id: string, data: string): void { + console.log('[TerminalManager:write] Writing to terminal:', id, 'data length:', data.length); const terminal = this.terminals.get(id); if (terminal) { + console.log('[TerminalManager:write] Terminal found, calling writeToPty...'); PtyManager.writeToPty(terminal, data); + console.log('[TerminalManager:write] writeToPty completed'); + } else { + console.error('[TerminalManager:write] Terminal NOT found:', id); } } diff --git a/apps/frontend/src/renderer/components/RateLimitModal.tsx b/apps/frontend/src/renderer/components/RateLimitModal.tsx index b19c842afd..4681f616b6 100644 --- a/apps/frontend/src/renderer/components/RateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/RateLimitModal.tsx @@ -22,6 +22,7 @@ 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'; const CLAUDE_UPGRADE_URL = 'https://claude.ai/upgrade'; @@ -29,6 +30,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); @@ -116,22 +118,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: `Authenticating "${profileName}"`, + description: 'Check the Agent Terminals section in the sidebar to complete OAuth login.', + }); } else { - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: 'Failed to start authentication', + description: initResult.error || 'Please try again.', + }); } } } catch (err) { console.error('Failed to add profile:', err); - alert('Failed to add profile. Please try again.'); + toast({ + variant: 'destructive', + title: 'Failed to add profile', + description: 'Please try again.', + }); } finally { setIsAddingProfile(false); } diff --git a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx index 116a091035..ab0a3205c8 100644 --- a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx @@ -21,6 +21,7 @@ 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 type { SDKRateLimitInfo } from '../../shared/types'; const CLAUDE_UPGRADE_URL = 'https://claude.ai/upgrade'; @@ -55,6 +56,7 @@ function getSourceIcon(source: SDKRateLimitInfo['source']) { export function SDKRateLimitModal() { const { isSDKModalOpen, sdkRateLimitInfo, hideSDKRateLimitModal, clearPendingRateLimit } = useRateLimitStore(); const { profiles, isSwitching, setSwitching } = useClaudeProfileStore(); + const { toast } = useToast(); const [selectedProfileId, setSelectedProfileId] = useState(null); const [autoSwitchEnabled, setAutoSwitchEnabled] = useState(false); const [isLoadingSettings, setIsLoadingSettings] = useState(false); @@ -160,22 +162,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: `Authenticating "${profileName}"`, + description: 'Check the Agent Terminals section in the sidebar to complete OAuth login.', + }); } else { - alert(`Failed to start authentication: ${initResult.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: 'Failed to start authentication', + description: initResult.error || 'Please try again.', + }); } } } catch (err) { console.error('Failed to add profile:', err); - alert('Failed to add profile. Please try again.'); + toast({ + variant: 'destructive', + title: 'Failed to add profile', + description: 'Please try again.', + }); } finally { setIsAddingProfile(false); } diff --git a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx index 4fad5f3337..536a75d14c 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: 'Profile authenticated successfully', + description: info.email ? `Account: ${info.email}` : 'Authentication complete. You can now use this profile.', + }); } }); return unsubscribe; - }, []); + }, [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: 'Failed to start authentication', + description: initResult.error || 'Please try again.', + }); } } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to add profile'); - alert('Failed to add profile. Please try again.'); + toast({ + variant: 'destructive', + title: 'Failed to add profile', + description: 'Please try again.', + }); } 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: 'Failed to start authentication', + description: initResult.error || 'Please try again.', + }); } // 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: 'Failed to start authentication', + description: 'Please try again.', + }); } finally { setAuthenticatingProfileId(null); } @@ -267,12 +288,24 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { setManualToken(''); setManualTokenEmail(''); setShowManualToken(false); + toast({ + title: 'Token saved', + description: 'Your token has been saved successfully.', + }); } else { - alert(`Failed to save token: ${result.error || 'Please try again.'}`); + toast({ + variant: 'destructive', + title: 'Failed to save token', + description: result.error || 'Please try again.', + }); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save token'); - alert('Failed to save token. Please try again.'); + toast({ + variant: 'destructive', + title: 'Failed to save token', + description: 'Please try again.', + }); } finally { setSavingTokenProfileId(null); } diff --git a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx index fbc0dfa0d6..4adc690b81 100644 --- a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx @@ -29,6 +29,7 @@ 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 type { AppSettings, ClaudeProfile, ClaudeAutoSwitchSettings } from '../../../shared/types'; interface IntegrationSettingsProps { @@ -43,6 +44,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 +85,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); @@ -135,12 +140,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.'); + toast({ + variant: 'destructive', + title: t('integrations.toast.addProfileFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { setIsAddingProfile(false); } @@ -199,18 +212,30 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte }; const handleAuthenticateProfile = async (profileId: string) => { + console.log('[IntegrationSettings] handleAuthenticateProfile called for:', profileId); setAuthenticatingProfileId(profileId); try { + console.log('[IntegrationSettings] Calling initializeClaudeProfile IPC...'); const initResult = await window.electronAPI.initializeClaudeProfile(profileId); + console.log('[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.'); + console.error('[IntegrationSettings] Failed to authenticate profile:', err); + toast({ + variant: 'destructive', + title: t('integrations.toast.authStartFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { + console.log('[IntegrationSettings] finally block - clearing authenticatingProfileId'); setAuthenticatingProfileId(null); } }; @@ -245,12 +270,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.'); + toast({ + variant: 'destructive', + title: t('integrations.toast.tokenSaveFailed'), + description: t('integrations.toast.tryAgain'), + }); } finally { setSavingTokenProfileId(null); } @@ -279,11 +316,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.'); + 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/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/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", From 27ae640d1a5fb295039af2e7365f9fba53fef8bb Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Thu, 8 Jan 2026 14:35:12 -0500 Subject: [PATCH 2/6] fix(i18n): replace hardcoded toast strings with translation keys Addresses CodeRabbit review feedback on PR #839: - OAuthStep.tsx: use t() for all toast messages - RateLimitModal.tsx: use t() for toast messages - SDKRateLimitModal.tsx: add useTranslation hook, use t() for toasts - Add toast translation keys to en/fr onboarding.json and common.json --- .../renderer/components/RateLimitModal.tsx | 12 +++---- .../renderer/components/SDKRateLimitModal.tsx | 14 ++++---- .../components/onboarding/OAuthStep.tsx | 32 +++++++++---------- .../src/shared/i18n/locales/en/common.json | 7 ++++ .../shared/i18n/locales/en/onboarding.json | 13 +++++++- .../src/shared/i18n/locales/fr/common.json | 7 ++++ .../shared/i18n/locales/fr/onboarding.json | 13 +++++++- 7 files changed, 68 insertions(+), 30 deletions(-) diff --git a/apps/frontend/src/renderer/components/RateLimitModal.tsx b/apps/frontend/src/renderer/components/RateLimitModal.tsx index 4681f616b6..9c39da250c 100644 --- a/apps/frontend/src/renderer/components/RateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/RateLimitModal.tsx @@ -120,14 +120,14 @@ export function RateLimitModal() { // Notify the user about the terminal (non-blocking) toast({ - title: `Authenticating "${profileName}"`, - description: 'Check the Agent Terminals section in the sidebar to complete OAuth login.', + title: t('rateLimit.toast.authenticating', { profileName }), + description: t('rateLimit.toast.checkTerminal'), }); } else { toast({ variant: 'destructive', - title: 'Failed to start authentication', - description: initResult.error || 'Please try again.', + title: t('rateLimit.toast.authStartFailed'), + description: initResult.error || t('rateLimit.toast.tryAgain'), }); } } @@ -135,8 +135,8 @@ export function RateLimitModal() { console.error('Failed to add profile:', err); toast({ variant: 'destructive', - title: 'Failed to add profile', - description: 'Please try again.', + 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 ab0a3205c8..75ab4e0edf 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, @@ -57,6 +58,7 @@ 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); @@ -164,14 +166,14 @@ export function SDKRateLimitModal() { // Notify the user about the terminal (non-blocking) toast({ - title: `Authenticating "${profileName}"`, - description: 'Check the Agent Terminals section in the sidebar to complete OAuth login.', + title: t('rateLimit.toast.authenticating', { profileName }), + description: t('rateLimit.toast.checkTerminal'), }); } else { toast({ variant: 'destructive', - title: 'Failed to start authentication', - description: initResult.error || 'Please try again.', + title: t('rateLimit.toast.authStartFailed'), + description: initResult.error || t('rateLimit.toast.tryAgain'), }); } } @@ -179,8 +181,8 @@ export function SDKRateLimitModal() { console.error('Failed to add profile:', err); toast({ variant: 'destructive', - title: 'Failed to add profile', - description: 'Please try again.', + title: t('rateLimit.toast.addProfileFailed'), + description: t('rateLimit.toast.tryAgain'), }); } finally { setIsAddingProfile(false); diff --git a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx index 536a75d14c..ed87dadb5c 100644 --- a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx @@ -106,8 +106,8 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { await loadClaudeProfiles(); // Show simple success notification (non-blocking) toast({ - title: 'Profile authenticated successfully', - description: info.email ? `Account: ${info.email}` : 'Authentication complete. You can now use this profile.', + title: t('oauth.toast.authSuccess'), + description: info.email ? t('oauth.toast.authSuccessWithEmail', { email: info.email }) : t('oauth.toast.authSuccessGeneric'), }); } }); @@ -159,8 +159,8 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { await loadClaudeProfiles(); toast({ variant: 'destructive', - title: 'Failed to start authentication', - description: initResult.error || 'Please try again.', + title: t('oauth.toast.authStartFailed'), + description: initResult.error || t('oauth.toast.tryAgain'), }); } } @@ -168,8 +168,8 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { setError(err instanceof Error ? err.message : 'Failed to add profile'); toast({ variant: 'destructive', - title: 'Failed to add profile', - description: 'Please try again.', + title: t('oauth.toast.addProfileFailed'), + description: t('oauth.toast.tryAgain'), }); } finally { setIsAddingProfile(false); @@ -239,8 +239,8 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { if (!initResult.success) { toast({ variant: 'destructive', - title: 'Failed to start authentication', - description: initResult.error || 'Please try again.', + 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 @@ -249,8 +249,8 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { setError(err instanceof Error ? err.message : 'Failed to authenticate profile'); toast({ variant: 'destructive', - title: 'Failed to start authentication', - description: 'Please try again.', + title: t('oauth.toast.authStartFailed'), + description: t('oauth.toast.tryAgain'), }); } finally { setAuthenticatingProfileId(null); @@ -289,22 +289,22 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { setManualTokenEmail(''); setShowManualToken(false); toast({ - title: 'Token saved', - description: 'Your token has been saved successfully.', + title: t('oauth.toast.tokenSaved'), + description: t('oauth.toast.tokenSavedDescription'), }); } else { toast({ variant: 'destructive', - title: 'Failed to save token', - description: result.error || 'Please try again.', + title: t('oauth.toast.tokenSaveFailed'), + description: result.error || t('oauth.toast.tryAgain'), }); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save token'); toast({ variant: 'destructive', - title: 'Failed to save token', - description: 'Please try again.', + title: t('oauth.toast.tokenSaveFailed'), + description: t('oauth.toast.tryAgain'), }); } finally { setSavingTokenProfileId(null); diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index cb4d363ba0..32e3188f4a 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -146,6 +146,13 @@ "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." } }, "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..67dd82ea02 100644 --- a/apps/frontend/src/shared/i18n/locales/en/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/en/onboarding.json @@ -32,7 +32,18 @@ "title": "Claude Authentication", "description": "Connect your Claude account to enable AI features", "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/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index 752e861ff0..4cf10ebe36 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -146,6 +146,13 @@ "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." } }, "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..fc99d87b53 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json @@ -32,7 +32,18 @@ "title": "Authentification Claude", "description": "Connectez votre compte Claude pour activer les fonctionnalités IA", "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", From 56a6beda12e0e0557097d634ff5a5ac5436d14e1 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Thu, 8 Jan 2026 14:39:04 -0500 Subject: [PATCH 3/6] fix: replace console.log with debugLog in IntegrationSettings Addresses CodeRabbit review feedback - use project's debug logging utility for consistent and toggle-able logging in production. --- .../renderer/components/settings/IntegrationSettings.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx index 4adc690b81..b62293f0a8 100644 --- a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx @@ -30,6 +30,7 @@ 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 } from '../../../shared/utils/debug-logger'; import type { AppSettings, ClaudeProfile, ClaudeAutoSwitchSettings } from '../../../shared/types'; interface IntegrationSettingsProps { @@ -212,12 +213,12 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte }; const handleAuthenticateProfile = async (profileId: string) => { - console.log('[IntegrationSettings] handleAuthenticateProfile called for:', profileId); + debugLog('[IntegrationSettings] handleAuthenticateProfile called for:', profileId); setAuthenticatingProfileId(profileId); try { - console.log('[IntegrationSettings] Calling initializeClaudeProfile IPC...'); + debugLog('[IntegrationSettings] Calling initializeClaudeProfile IPC...'); const initResult = await window.electronAPI.initializeClaudeProfile(profileId); - console.log('[IntegrationSettings] IPC returned:', initResult); + debugLog('[IntegrationSettings] IPC returned:', initResult); if (!initResult.success) { toast({ variant: 'destructive', @@ -235,7 +236,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte description: t('integrations.toast.tryAgain'), }); } finally { - console.log('[IntegrationSettings] finally block - clearing authenticatingProfileId'); + debugLog('[IntegrationSettings] finally block - clearing authenticatingProfileId'); setAuthenticatingProfileId(null); } }; From a37e072c6d3c08e70523f54b85ce1d71c89fdbf7 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Thu, 8 Jan 2026 15:30:27 -0500 Subject: [PATCH 4/6] fix: replace console.log with debugLog in main process files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Auto-Claude PR Review feedback: - terminal-handlers.ts: 14 console.log → debugLog - pty-manager.ts: 10 console.log → debugLog/debugError - terminal-manager.ts: 4 console.log → debugLog/debugError Also fixes: - Extract magic numbers to CHUNKED_WRITE_THRESHOLD/CHUNK_SIZE constants - Add terminal validity check before chunked writes - Consistent error handling (no rethrow for fire-and-forget semantics) --- .../main/ipc-handlers/terminal-handlers.ts | 36 +++++++-------- .../frontend/src/main/terminal/pty-manager.ts | 45 ++++++++++++------- .../src/main/terminal/terminal-manager.ts | 9 ++-- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts index ba2848f8d4..e04cf059d7 100644 --- a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts @@ -312,17 +312,17 @@ export function registerTerminalHandlers( ipcMain.handle( IPC_CHANNELS.CLAUDE_PROFILE_INITIALIZE, async (_, profileId: string): Promise => { - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Handler called for profileId:', profileId); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Handler called for profileId:', profileId); try { - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile manager...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile manager...'); const profileManager = getClaudeProfileManager(); - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting profile...'); const profile = profileManager.getProfile(profileId); if (!profile) { - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile not found!'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile not found!'); return { success: false, error: 'Profile not found' }; } - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile found:', profile.name); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Profile found:', profile.name); // Ensure the config directory exists for non-default profiles if (!profile.isDefault && profile.configDir) { @@ -338,7 +338,7 @@ export function registerTerminalHandlers( const terminalId = `claude-login-${profileId}-${Date.now()}`; const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp'; - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Creating terminal:', terminalId); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Creating terminal:', terminalId); debugLog('[IPC] Initializing Claude profile:', { profileId, profileName: profile.name, @@ -347,9 +347,9 @@ export function registerTerminalHandlers( }); // Create a new terminal for the login process - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Calling terminalManager.create...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Calling terminalManager.create...'); const createResult = await terminalManager.create({ id: terminalId, cwd: homeDir }); - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Terminal created:', createResult.success); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Terminal created:', createResult.success); // If terminal creation failed, return the error if (!createResult.success) { @@ -360,16 +360,16 @@ export function registerTerminalHandlers( } // Wait a moment for the terminal to initialize - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Waiting 500ms for terminal init...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Waiting 500ms for terminal init...'); await new Promise(resolve => setTimeout(resolve, 500)); - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Wait complete'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Wait complete'); // Build the login command with the profile's config dir // Use full path to claude CLI - no need to modify PATH since we have the absolute path - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting Claude CLI invocation...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Getting Claude CLI invocation...'); let loginCommand: string; const { command: claudeCmd } = await getClaudeCliInvocationAsync(); - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Got Claude CLI:', claudeCmd); + 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' @@ -393,18 +393,18 @@ export function registerTerminalHandlers( loginCommand = `${shellClaudeCmd} setup-token`; } - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Built login command, length:', loginCommand.length); - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Login command:', loginCommand); + 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 - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Writing command to terminal...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Writing command to terminal...'); terminalManager.write(terminalId, `${loginCommand}\r`); - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Command written successfully'); + 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 - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Notifying renderer of auth terminal...'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Notifying renderer of auth terminal...'); const mainWindow = getMainWindow(); if (mainWindow) { mainWindow.webContents.send(IPC_CHANNELS.TERMINAL_AUTH_CREATED, { @@ -414,7 +414,7 @@ export function registerTerminalHandlers( }); } - console.log('[IPC:CLAUDE_PROFILE_INITIALIZE] Returning success!'); + debugLog('[IPC:CLAUDE_PROFILE_INITIALIZE] Returning success!'); return { success: true, data: { diff --git a/apps/frontend/src/main/terminal/pty-manager.ts b/apps/frontend/src/main/terminal/pty-manager.ts index ac911134bc..cfd1e9778d 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'; /** @@ -151,51 +152,65 @@ 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 data to a PTY process * Uses setImmediate to prevent blocking the event loop on large writes */ export function writeToPty(terminal: TerminalProcess, data: string): void { - console.log('[PtyManager:writeToPty] About to write to pty, data length:', data.length); + debugLog('[PtyManager:writeToPty] About to write to pty, data length:', data.length); // For large commands, write in chunks to prevent blocking - if (data.length > 1000) { - console.log('[PtyManager:writeToPty] Large write detected, using chunked write'); - const chunkSize = 100; // Smaller chunks + 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'); + return; + } + if (offset >= data.length) { - console.log('[PtyManager:writeToPty] Chunked write completed, total chunks:', chunkNum); + debugLog('[PtyManager:writeToPty] Chunked write completed, total chunks:', chunkNum); return; } - const chunk = data.slice(offset, offset + chunkSize); + const chunk = data.slice(offset, offset + CHUNK_SIZE); chunkNum++; - console.log('[PtyManager:writeToPty] Writing chunk', chunkNum, 'offset:', offset, 'size:', chunk.length); + debugLog('[PtyManager:writeToPty] Writing chunk', chunkNum, 'offset:', offset, 'size:', chunk.length); try { terminal.pty.write(chunk); - console.log('[PtyManager:writeToPty] Chunk', chunkNum, 'written'); - offset += chunkSize; + debugLog('[PtyManager:writeToPty] Chunk', chunkNum, 'written'); + offset += CHUNK_SIZE; // Use setImmediate to yield to the event loop between chunks setImmediate(writeChunk); } catch (error) { - console.error('[PtyManager:writeToPty] Chunked write FAILED at chunk', chunkNum, 'offset', offset, ':', error); + debugError('[PtyManager:writeToPty] Chunked write FAILED at chunk', chunkNum, 'offset', offset, ':', error); + // Don't rethrow - match non-chunked behavior for consistency (fire-and-forget semantics) } }; // Start the chunked write after yielding - console.log('[PtyManager:writeToPty] Scheduling first chunk...'); + debugLog('[PtyManager:writeToPty] Scheduling first chunk...'); setImmediate(writeChunk); - console.log('[PtyManager:writeToPty] First chunk scheduled, returning'); + debugLog('[PtyManager:writeToPty] First chunk scheduled, returning'); } else { try { terminal.pty.write(data); - console.log('[PtyManager:writeToPty] Write completed successfully'); + debugLog('[PtyManager:writeToPty] Write completed successfully'); } catch (error) { - console.error('[PtyManager:writeToPty] Write FAILED:', error); - throw error; + debugError('[PtyManager:writeToPty] Write FAILED:', error); + // Don't rethrow - fire-and-forget terminal write semantics } } } diff --git a/apps/frontend/src/main/terminal/terminal-manager.ts b/apps/frontend/src/main/terminal/terminal-manager.ts index e3e4808133..15dbb12ac3 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(); @@ -120,14 +121,14 @@ export class TerminalManager { * Send input to a terminal */ write(id: string, data: string): void { - console.log('[TerminalManager:write] Writing to terminal:', id, 'data length:', data.length); + debugLog('[TerminalManager:write] Writing to terminal:', id, 'data length:', data.length); const terminal = this.terminals.get(id); if (terminal) { - console.log('[TerminalManager:write] Terminal found, calling writeToPty...'); + debugLog('[TerminalManager:write] Terminal found, calling writeToPty...'); PtyManager.writeToPty(terminal, data); - console.log('[TerminalManager:write] writeToPty completed'); + debugLog('[TerminalManager:write] writeToPty completed'); } else { - console.error('[TerminalManager:write] Terminal NOT found:', id); + debugError('[TerminalManager:write] Terminal NOT found:', id); } } From 83280666e6f8243814366b90d3cf2a971d5c2861 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 9 Jan 2026 01:45:51 -0500 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20address=20Auto=20Claude=20PR=20revie?= =?UTF-8?q?w=20findings=20-=20console.error=E2=86=92debugError=20+=20i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace 8 remaining console.error/warn calls with debugError/debugLog: - terminal-handlers.ts: lines 59, 426, 660, 671 - terminal-manager.ts: lines 88, 320 - pty-manager.ts: lines 88, 141 - Fixed duplicate logging in exception handler - Add comprehensive i18n for SDKRateLimitModal.tsx (~20 strings): - Added rateLimit.sdk.* keys with swap notifications, buttons, labels - EN + FR translations in common.json - Add comprehensive i18n for OAuthStep.tsx (~15 strings): - Added oauth.badges.*, oauth.buttons.*, oauth.labels.* keys - EN + FR translations in onboarding.json All MEDIUM severity findings resolved except race condition (deferred). --- .../main/ipc-handlers/terminal-handlers.ts | 9 ++-- .../frontend/src/main/terminal/pty-manager.ts | 4 +- .../src/main/terminal/terminal-manager.ts | 4 +- .../renderer/components/SDKRateLimitModal.tsx | 49 +++++++++---------- .../components/onboarding/OAuthStep.tsx | 26 +++++----- .../src/shared/i18n/locales/en/common.json | 27 ++++++++++ .../shared/i18n/locales/en/onboarding.json | 31 ++++++++++++ .../src/shared/i18n/locales/fr/common.json | 27 ++++++++++ .../shared/i18n/locales/fr/onboarding.json | 31 ++++++++++++ 9 files changed, 161 insertions(+), 47 deletions(-) diff --git a/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts b/apps/frontend/src/main/ipc-handlers/terminal-handlers.ts index e04cf059d7..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); }); } ); @@ -423,8 +423,7 @@ export function registerTerminalHandlers( } }; } catch (error) { - console.error('[IPC:CLAUDE_PROFILE_INITIALIZE] EXCEPTION:', 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' @@ -657,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); }); } ); @@ -668,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 cfd1e9778d..6b82c363a8 100644 --- a/apps/frontend/src/main/terminal/pty-manager.ts +++ b/apps/frontend/src/main/terminal/pty-manager.ts @@ -85,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. @@ -138,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) { diff --git a/apps/frontend/src/main/terminal/terminal-manager.ts b/apps/frontend/src/main/terminal/terminal-manager.ts index 15dbb12ac3..5e8fb4c8b8 100644 --- a/apps/frontend/src/main/terminal/terminal-manager.ts +++ b/apps/frontend/src/main/terminal/terminal-manager.ts @@ -85,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); }); } }, @@ -317,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/SDKRateLimitModal.tsx b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx index 75ab4e0edf..216a1a9ce3 100644 --- a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx @@ -244,11 +244,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}) )} @@ -261,26 +261,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')}

)} @@ -294,7 +294,7 @@ export function SDKRateLimitModal() { onClick={() => window.open(CLAUDE_UPGRADE_URL, '_blank')} > - Upgrade to Pro for Higher Limits + {t('rateLimit.sdk.upgradeToProButton')} {/* Reset time info */} @@ -303,12 +303,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')}

@@ -318,7 +318,7 @@ export function SDKRateLimitModal() {

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

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

@@ -484,20 +484,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 ed87dadb5c..3ce51d1630 100644 --- a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx @@ -326,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')}

@@ -362,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')}

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

No accounts configured yet

+

{t('oauth.noAccountsYet')}

) : (
@@ -454,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')} )}
@@ -496,7 +496,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { ) : ( )} - Authenticate + {t('oauth.buttons.authenticate')} )} {profile.id !== activeProfileId && ( @@ -507,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 */} @@ -676,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/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index 32e3188f4a..37cb41fe3d 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -153,6 +153,33 @@ "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 67dd82ea02..ba48579588 100644 --- a/apps/frontend/src/shared/i18n/locales/en/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/en/onboarding.json @@ -31,6 +31,37 @@ "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.", "toast": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index 4cf10ebe36..cc3888658a 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -153,6 +153,33 @@ "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 fc99d87b53..f95ef0cb2a 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json @@ -31,6 +31,37 @@ "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.", "toast": { From da9b7072a1cc789705495360faec4ed23f128783 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Sat, 10 Jan 2026 16:45:08 -0500 Subject: [PATCH 6/6] fix: address Auto Claude PR review findings - race condition + console.error - Fix race condition in chunked PTY writes by serializing writes per terminal using Promise chaining (prevents interleaving of concurrent large writes) - Fix missing 't' in useEffect dependency array in OAuthStep.tsx - Convert all remaining console.error calls to debugError for consistency: - IntegrationSettings.tsx (9 occurrences) - RateLimitModal.tsx (3 occurrences) - SDKRateLimitModal.tsx (4 occurrences) Co-Authored-By: Claude Opus 4.5 Signed-off-by: Black Circle Sentinel --- .../frontend/src/main/terminal/pty-manager.ts | 118 +++++++++++------- .../renderer/components/RateLimitModal.tsx | 7 +- .../renderer/components/SDKRateLimitModal.tsx | 9 +- .../components/onboarding/OAuthStep.tsx | 2 +- .../settings/IntegrationSettings.tsx | 20 +-- 5 files changed, 94 insertions(+), 62 deletions(-) diff --git a/apps/frontend/src/main/terminal/pty-manager.ts b/apps/frontend/src/main/terminal/pty-manager.ts index 6b82c363a8..2117917b0c 100644 --- a/apps/frontend/src/main/terminal/pty-manager.ts +++ b/apps/frontend/src/main/terminal/pty-manager.ts @@ -161,58 +161,88 @@ const CHUNKED_WRITE_THRESHOLD = 1000; const CHUNK_SIZE = 100; /** - * Write data to a PTY process - * Uses setImmediate to prevent blocking the event loop on large writes + * Write queue per terminal to prevent interleaving of concurrent writes. + * Maps terminal ID to the last write Promise in the queue. */ -export function writeToPty(terminal: TerminalProcess, data: string): void { - debugLog('[PtyManager:writeToPty] About to write to pty, data length:', data.length); +const pendingWrites = new Map>(); - // 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'); - return; - } +/** + * 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; - if (offset >= data.length) { - debugLog('[PtyManager:writeToPty] Chunked write completed, total chunks:', chunkNum); - return; - } + 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 + } + }; - const chunk = data.slice(offset, offset + CHUNK_SIZE); - chunkNum++; - debugLog('[PtyManager:writeToPty] Writing chunk', chunkNum, 'offset:', offset, 'size:', chunk.length); + // Start the chunked write after yielding + setImmediate(writeChunk); + } else { try { - terminal.pty.write(chunk); - debugLog('[PtyManager:writeToPty] Chunk', chunkNum, 'written'); - offset += CHUNK_SIZE; - // Use setImmediate to yield to the event loop between chunks - setImmediate(writeChunk); + terminal.pty.write(data); + debugLog('[PtyManager:writeToPty] Write completed successfully'); } catch (error) { - debugError('[PtyManager:writeToPty] Chunked write FAILED at chunk', chunkNum, 'offset', offset, ':', error); - // Don't rethrow - match non-chunked behavior for consistency (fire-and-forget semantics) + debugError('[PtyManager:writeToPty] Write FAILED:', error); } - }; - - // Start the chunked write after yielding - debugLog('[PtyManager:writeToPty] Scheduling first chunk...'); - setImmediate(writeChunk); - debugLog('[PtyManager:writeToPty] First chunk scheduled, returning'); - } else { - try { - terminal.pty.write(data); - debugLog('[PtyManager:writeToPty] Write completed successfully'); - } catch (error) { - debugError('[PtyManager:writeToPty] Write FAILED:', error); - // Don't rethrow - fire-and-forget terminal write semantics + 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 { + 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/renderer/components/RateLimitModal.tsx b/apps/frontend/src/renderer/components/RateLimitModal.tsx index 9c39da250c..9d0e2e322d 100644 --- a/apps/frontend/src/renderer/components/RateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/RateLimitModal.tsx @@ -23,6 +23,7 @@ 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'; @@ -66,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); } }; @@ -79,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); } @@ -132,7 +133,7 @@ export function RateLimitModal() { } } } catch (err) { - console.error('Failed to add profile:', err); + debugError('[RateLimitModal] Failed to add profile:', err); toast({ variant: 'destructive', title: t('rateLimit.toast.addProfileFailed'), diff --git a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx index 216a1a9ce3..ae98edea44 100644 --- a/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx +++ b/apps/frontend/src/renderer/components/SDKRateLimitModal.tsx @@ -23,6 +23,7 @@ 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'; @@ -112,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); } }; @@ -125,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); } @@ -178,7 +179,7 @@ export function SDKRateLimitModal() { } } } catch (err) { - console.error('Failed to add profile:', err); + debugError('[SDKRateLimitModal] Failed to add profile:', err); toast({ variant: 'destructive', title: t('rateLimit.toast.addProfileFailed'), @@ -212,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); diff --git a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx index 3ce51d1630..e175c0df4d 100644 --- a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx @@ -113,7 +113,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { }); return unsubscribe; - }, [toast]); + }, [t, toast]); // Profile management handlers - following patterns from IntegrationSettings.tsx const handleAddProfile = async () => { diff --git a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx index b62293f0a8..718d1b4b69 100644 --- a/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/IntegrationSettings.tsx @@ -30,7 +30,7 @@ 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 } from '../../../shared/utils/debug-logger'; +import { debugLog, debugError } from '../../../shared/utils/debug-logger'; import type { AppSettings, ClaudeProfile, ClaudeAutoSwitchSettings } from '../../../shared/types'; interface IntegrationSettingsProps { @@ -108,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); } @@ -149,7 +149,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte } } } catch (err) { - console.error('Failed to add profile:', err); + debugError('[IntegrationSettings] Failed to add profile:', err); toast({ variant: 'destructive', title: t('integrations.toast.addProfileFailed'), @@ -168,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); } @@ -193,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(''); @@ -208,7 +208,7 @@ 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); } }; @@ -229,7 +229,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte // 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('[IntegrationSettings] Failed to authenticate profile:', err); + debugError('[IntegrationSettings] Failed to authenticate profile:', err); toast({ variant: 'destructive', title: t('integrations.toast.authStartFailed'), @@ -283,7 +283,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte }); } } catch (err) { - console.error('Failed to save token:', err); + debugError('[IntegrationSettings] Failed to save token:', err); toast({ variant: 'destructive', title: t('integrations.toast.tokenSaveFailed'), @@ -303,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); } @@ -324,7 +324,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte }); } } catch (err) { - console.error('Failed to update auto-switch settings:', err); + debugError('[IntegrationSettings] Failed to update auto-switch settings:', err); toast({ variant: 'destructive', title: t('integrations.toast.settingsUpdateFailed'),