fix(ACS-175): Resolve integrations freeze and improve rate limit handling (#839)

* fix(ACS-175): Resolve integrations freeze and improve rate limit handling

* 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

* 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.

* fix: replace console.log with debugLog in main process files

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)

* fix: address Auto Claude PR review findings - console.error→debugError + i18n

- 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).

* 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 <noreply@anthropic.com>
Signed-off-by: Black Circle Sentinel <mludlow000@icloud.com>

---------

Signed-off-by: Black Circle Sentinel <mludlow000@icloud.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
Co-authored-by: Adam Slaker <51129804+aslaker@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Ludlow
2026-01-12 01:09:03 -05:00
committed by GitHub
parent 0c139add1e
commit 3ca15e1c2b
13 changed files with 498 additions and 119 deletions
@@ -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<IPCResult> => {
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);
});
}
);
+90 -3
View File
@@ -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<string, Promise<void>>();
/**
* 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<void> {
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);
}
});
}
/**
@@ -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<string, TerminalProcess> = 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);
});
}
},
@@ -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<string | null>(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);
}
@@ -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<string | null>(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() {
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-warning">
<AlertCircle className="h-5 w-5" />
Claude Code Rate Limit
{t('rateLimit.sdk.title')}
</DialogTitle>
<DialogDescription className="flex items-center gap-2">
<SourceIcon className="h-4 w-4" />
{sourceName} was interrupted due to usage limits.
{t('rateLimit.sdk.interrupted', { source: sourceName })}
{currentProfile && (
<span className="text-muted-foreground"> (Profile: {currentProfile.name})</span>
)}
@@ -253,26 +262,26 @@ export function SDKRateLimitModal() {
{swapInfo?.wasAutoSwapped ? (
<>
<p className="font-medium mb-1">
{swapInfo.swapReason === 'proactive' ? '✓ Proactive Swap' : '⚡ Reactive Swap'}
{swapInfo.swapReason === 'proactive' ? t('rateLimit.sdk.proactiveSwap') : t('rateLimit.sdk.reactiveSwap')}
</p>
<p>
{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 })
}
</p>
<p className="mt-2 text-[10px]">
Your work continued without interruption.
{t('rateLimit.sdk.continueWithoutInterruption')}
</p>
</>
) : (
<>
<p className="font-medium mb-1">Rate limit reached</p>
<p className="font-medium mb-1">{t('rateLimit.sdk.rateLimitReached')}</p>
<p>
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')}
</p>
</>
)}
@@ -286,7 +295,7 @@ export function SDKRateLimitModal() {
onClick={() => window.open(CLAUDE_UPGRADE_URL, '_blank')}
>
<Zap className="h-4 w-4" />
Upgrade to Pro for Higher Limits
{t('rateLimit.sdk.upgradeToProButton')}
</Button>
{/* Reset time info */}
@@ -295,12 +304,12 @@ export function SDKRateLimitModal() {
<Clock className="h-5 w-5 text-muted-foreground shrink-0" />
<div>
<p className="text-sm font-medium text-foreground">
Resets {sdkRateLimitInfo.resetTime}
{t('rateLimit.sdk.resetsLabel', { time: sdkRateLimitInfo.resetTime })}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{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')}
</p>
</div>
</div>
@@ -310,7 +319,7 @@ export function SDKRateLimitModal() {
<div className="rounded-lg border border-accent/50 bg-accent/10 p-4">
<h4 className="text-sm font-medium text-foreground mb-2 flex items-center gap-2">
<User className="h-4 w-4" />
{hasMultipleProfiles ? 'Switch Account & Retry' : 'Use Another Account'}
{hasMultipleProfiles ? t('rateLimit.sdk.switchAccountRetry') : t('rateLimit.useAnotherAccount')}
</h4>
{hasMultipleProfiles ? (
@@ -379,12 +388,12 @@ export function SDKRateLimitModal() {
{isRetrying || isSwitching ? (
<>
<RefreshCw className="h-4 w-4 animate-spin" />
Retrying...
{t('rateLimit.sdk.retrying')}
</>
) : (
<>
<RefreshCw className="h-4 w-4" />
Retry
{t('rateLimit.sdk.retry')}
</>
)}
</Button>
@@ -400,7 +409,7 @@ export function SDKRateLimitModal() {
{availableProfiles.length > 0 && (
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border/50">
<Label htmlFor="sdk-auto-switch" className="text-xs text-muted-foreground cursor-pointer">
Auto-switch & retry on rate limit
{t('rateLimit.sdk.autoSwitchRetryLabel')}
</Label>
<Switch
id="sdk-auto-switch"
@@ -446,7 +455,7 @@ export function SDKRateLimitModal() {
) : (
<Plus className="h-3 w-3" />
)}
Add
{t('rateLimit.sdk.add')}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-2">
@@ -476,20 +485,19 @@ export function SDKRateLimitModal() {
{/* Info about what was interrupted */}
<div className="text-xs text-muted-foreground bg-muted/30 rounded-lg p-3">
<p className="font-medium mb-1">What happened:</p>
<p className="font-medium mb-1">{t('rateLimit.sdk.whatHappened')}</p>
<p>
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')}
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={hideSDKRateLimitModal}>
Close
{t('rateLimit.sdk.close')}
</Button>
</DialogFooter>
</DialogContent>
@@ -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<ClaudeProfile[]>([]);
@@ -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) {
</div>
</div>
<h1 className="text-2xl font-bold text-foreground tracking-tight">
Configure Claude Authentication
{t('oauth.configureTitle')}
</h1>
<p className="mt-2 text-muted-foreground">
Add your Claude accounts to enable AI features
{t('oauth.addAccountsDesc')}
</p>
</div>
@@ -329,7 +362,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
<Info className="h-5 w-5 text-info shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm text-muted-foreground">
Add multiple Claude subscriptions to automatically switch between them when you hit rate limits.
{t('oauth.multiAccountInfo')}
</p>
</div>
</div>
@@ -359,7 +392,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
<div className="rounded-lg bg-muted/30 border border-border p-4">
{claudeProfiles.length === 0 ? (
<div className="rounded-lg border border-dashed border-border p-4 text-center mb-4">
<p className="text-sm text-muted-foreground">No accounts configured yet</p>
<p className="text-sm text-muted-foreground">{t('oauth.noAccountsYet')}</p>
</div>
) : (
<div className="space-y-2 mb-4">
@@ -421,22 +454,22 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-foreground">{profile.name}</span>
{profile.isDefault && (
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">Default</span>
<span className="text-xs bg-muted px-1.5 py-0.5 rounded">{t('oauth.badges.default')}</span>
)}
{profile.id === activeProfileId && (
<span className="text-xs bg-primary/20 text-primary px-1.5 py-0.5 rounded flex items-center gap-1">
<Star className="h-3 w-3" />
Active
{t('oauth.badges.active')}
</span>
)}
{(profile.oauthToken || (profile.isDefault && profile.configDir)) ? (
<span className="text-xs bg-success/20 text-success px-1.5 py-0.5 rounded flex items-center gap-1">
<Check className="h-3 w-3" />
Authenticated
{t('oauth.badges.authenticated')}
</span>
) : (
<span className="text-xs bg-warning/20 text-warning px-1.5 py-0.5 rounded">
Needs Auth
{t('oauth.badges.needsAuth')}
</span>
)}
</div>
@@ -463,7 +496,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
) : (
<LogIn className="h-3 w-3" />
)}
Authenticate
{t('oauth.buttons.authenticate')}
</Button>
)}
{profile.id !== activeProfileId && (
@@ -474,7 +507,7 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
className="gap-1 h-7 text-xs"
>
<Check className="h-3 w-3" />
Set Active
{t('oauth.buttons.setActive')}
</Button>
)}
{/* 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')}
</Button>
<div className="flex gap-4">
<Button
@@ -651,13 +684,13 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) {
onClick={onSkip}
className="text-muted-foreground hover:text-foreground"
>
Skip
{t('oauth.buttons.skip')}
</Button>
<Button
onClick={handleContinue}
disabled={!hasAuthenticatedProfile}
>
Continue
{t('oauth.buttons.continue')}
</Button>
</div>
</div>
@@ -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);
}
@@ -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": {
@@ -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",
@@ -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",
@@ -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": {
@@ -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",
@@ -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",