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:
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user