diff --git a/auto-claude-ui/src/main/claude-profile/profile-storage.ts b/auto-claude-ui/src/main/claude-profile/profile-storage.ts index 35cafc18..bd5b89c3 100644 --- a/auto-claude-ui/src/main/claude-profile/profile-storage.ts +++ b/auto-claude-ui/src/main/claude-profile/profile-storage.ts @@ -13,10 +13,11 @@ export const STORE_VERSION = 3; // Bumped for encrypted token storage */ export const DEFAULT_AUTO_SWITCH_SETTINGS: ClaudeAutoSwitchSettings = { enabled: false, - sessionThreshold: 85, // Consider switching at 85% session usage - weeklyThreshold: 90, // Consider switching at 90% weekly usage + proactiveSwapEnabled: false, // Proactive monitoring disabled by default + sessionThreshold: 95, // Consider switching at 95% session usage + weeklyThreshold: 99, // Consider switching at 99% weekly usage autoSwitchOnRateLimit: false, // Prompt user by default - usageCheckInterval: 0 // Disabled by default (in ms, e.g., 300000 = 5 min) + usageCheckInterval: 30000 // Check every 30s when enabled (0 = disabled) }; /** diff --git a/auto-claude-ui/src/main/claude-profile/usage-monitor.ts b/auto-claude-ui/src/main/claude-profile/usage-monitor.ts index bc0fd824..1c57b044 100644 --- a/auto-claude-ui/src/main/claude-profile/usage-monitor.ts +++ b/auto-claude-ui/src/main/claude-profile/usage-monitor.ts @@ -221,32 +221,19 @@ export class UsageMonitor extends EventEmitter { /** * Fetch usage via CLI /usage command (fallback) + * Note: This is a fallback method. The API method is preferred. + * CLI-based fetching would require spawning a Claude process and parsing output, + * which is complex. For now, we rely on the API method. */ private async fetchUsageViaCLI( - profileId: string, - profileName: string + _profileId: string, + _profileName: string ): Promise { - const profileManager = getClaudeProfileManager(); - - // Use existing CLI-based usage fetching mechanism - const result = await profileManager.fetchProfileUsage(profileId); - - if (!result.success || !result.data) { - return null; - } - - return { - sessionPercent: result.data.sessionUsagePercent || 0, - weeklyPercent: result.data.weeklyUsagePercent || 0, - sessionResetTime: result.data.sessionResetTime, - weeklyResetTime: result.data.weeklyResetTime, - profileId, - profileName, - fetchedAt: new Date(), - limitType: (result.data.weeklyUsagePercent || 0) > (result.data.sessionUsagePercent || 0) - ? 'weekly' - : 'session' - }; + // CLI-based usage fetching is not implemented yet. + // The API method should handle most cases. If we need CLI fallback, + // we would need to spawn a Claude process with /usage command and parse the output. + console.log('[UsageMonitor] CLI fallback not implemented, API method should be used'); + return null; } /** diff --git a/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts index 11b5f8d9..aa18b5fc 100644 --- a/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/terminal-handlers.ts @@ -1,8 +1,9 @@ import { ipcMain } from 'electron'; import type { BrowserWindow } from 'electron'; import { IPC_CHANNELS } from '../../shared/constants'; -import type { IPCResult, TerminalCreateOptions, ClaudeProfile, ClaudeProfileSettings } from '../../shared/types'; +import type { IPCResult, TerminalCreateOptions, ClaudeProfile, ClaudeProfileSettings, ClaudeUsageSnapshot } from '../../shared/types'; import { getClaudeProfileManager } from '../claude-profile-manager'; +import { getUsageMonitor } from '../claude-profile/usage-monitor'; import { TerminalManager } from '../terminal-manager'; import { projectStore } from '../project-store'; import { terminalNameGenerator } from '../terminal-name-generator'; @@ -558,16 +559,15 @@ export function registerTerminalHandlers( * Call this after mainWindow is created */ export function initializeUsageMonitorForwarding(mainWindow: BrowserWindow): void { - const { getUsageMonitor } = require('../claude-profile/usage-monitor'); const monitor = getUsageMonitor(); // Forward usage updates to renderer - monitor.on('usage-updated', (usage: import('../../shared/types').ClaudeUsageSnapshot) => { + monitor.on('usage-updated', (usage: ClaudeUsageSnapshot) => { mainWindow.webContents.send(IPC_CHANNELS.USAGE_UPDATED, usage); }); // Forward proactive swap notifications to renderer - monitor.on('show-swap-notification', (notification: any) => { + monitor.on('show-swap-notification', (notification: unknown) => { mainWindow.webContents.send(IPC_CHANNELS.PROACTIVE_SWAP_NOTIFICATION, notification); }); diff --git a/auto-claude-ui/src/main/rate-limit-detector.ts b/auto-claude-ui/src/main/rate-limit-detector.ts index f4d7c31c..97257df3 100644 --- a/auto-claude-ui/src/main/rate-limit-detector.ts +++ b/auto-claude-ui/src/main/rate-limit-detector.ts @@ -228,6 +228,17 @@ export interface SDKRateLimitInfo { detectedAt: Date; /** Original error message */ originalError?: string; + + // Auto-swap information + /** Whether this rate limit was automatically handled via account swap */ + wasAutoSwapped?: boolean; + /** Profile that was swapped to (if auto-swapped) */ + swappedToProfile?: { + id: string; + name: string; + }; + /** Why the swap occurred: 'proactive' (before limit) or 'reactive' (after limit hit) */ + swapReason?: 'proactive' | 'reactive'; } /** diff --git a/auto-claude-ui/src/renderer/components/project-settings/EnvironmentSettings.tsx b/auto-claude-ui/src/renderer/components/project-settings/EnvironmentSettings.tsx index cb4b8daa..bf6ca05f 100644 --- a/auto-claude-ui/src/renderer/components/project-settings/EnvironmentSettings.tsx +++ b/auto-claude-ui/src/renderer/components/project-settings/EnvironmentSettings.tsx @@ -167,7 +167,7 @@ export function EnvironmentSettings({ Active - {activeProfile.oauthToken ? ( + {(activeProfile.oauthToken || (activeProfile.isDefault && activeProfile.configDir)) ? ( Authenticated diff --git a/auto-claude-ui/src/renderer/components/settings/IntegrationSettings.tsx b/auto-claude-ui/src/renderer/components/settings/IntegrationSettings.tsx index 74418074..0155e420 100644 --- a/auto-claude-ui/src/renderer/components/settings/IntegrationSettings.tsx +++ b/auto-claude-ui/src/renderer/components/settings/IntegrationSettings.tsx @@ -72,20 +72,11 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte // Listen for OAuth authentication completion useEffect(() => { const unsubscribe = window.electronAPI.onTerminalOAuthToken(async (info) => { - console.log('[IntegrationSettings] OAuth authentication event:', { - terminalId: info.terminalId, - profileId: info.profileId, - email: info.email, - success: info.success - }); - 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.`); - } else if (!info.success) { - console.log('[IntegrationSettings] Authentication detected but not saved:', info.message); } }); @@ -393,7 +384,7 @@ export function IntegrationSettings({ settings, onSettingsChange, isOpen }: Inte Active )} - {profile.oauthToken ? ( + {(profile.oauthToken || (profile.isDefault && profile.configDir)) ? ( Authenticated diff --git a/auto-claude-ui/src/renderer/lib/mocks/claude-profile-mock.ts b/auto-claude-ui/src/renderer/lib/mocks/claude-profile-mock.ts index 2ae8432f..2b471b30 100644 --- a/auto-claude-ui/src/renderer/lib/mocks/claude-profile-mock.ts +++ b/auto-claude-ui/src/renderer/lib/mocks/claude-profile-mock.ts @@ -39,10 +39,11 @@ export const claudeProfileMock = { success: true, data: { enabled: false, - sessionThreshold: 80, - weeklyThreshold: 90, + proactiveSwapEnabled: false, + sessionThreshold: 95, + weeklyThreshold: 99, autoSwitchOnRateLimit: false, - usageCheckInterval: 0 + usageCheckInterval: 30000 } }), @@ -57,5 +58,15 @@ export const claudeProfileMock = { onSDKRateLimit: () => () => {}, - retryWithProfile: async () => ({ success: true }) + retryWithProfile: async () => ({ success: true }), + + // Usage Monitoring (Proactive Account Switching) + requestUsageUpdate: async () => ({ + success: true, + data: null + }), + + onUsageUpdated: () => () => {}, + + onProactiveSwapNotification: () => () => {} }; diff --git a/auto-claude-ui/src/shared/types/ipc.ts b/auto-claude-ui/src/shared/types/ipc.ts index 73fe2566..6ecdf6e6 100644 --- a/auto-claude-ui/src/shared/types/ipc.ts +++ b/auto-claude-ui/src/shared/types/ipc.ts @@ -51,7 +51,8 @@ import type { ClaudeProfileSettings, ClaudeProfile, ClaudeAutoSwitchSettings, - ClaudeAuthResult + ClaudeAuthResult, + ClaudeUsageSnapshot } from './agent'; import type { AppSettings, SourceEnvConfig, SourceEnvCheckResult, AutoBuildSourceUpdateCheck, AutoBuildSourceUpdateProgress } from './settings'; import type { @@ -206,6 +207,19 @@ export interface ElectronAPI { /** Retry a rate-limited operation with a different profile */ retryWithProfile: (request: RetryWithProfileRequest) => Promise; + // Usage Monitoring (Proactive Account Switching) + /** Request current usage snapshot */ + requestUsageUpdate: () => Promise>; + /** Listen for usage data updates */ + onUsageUpdated: (callback: (usage: ClaudeUsageSnapshot) => void) => () => void; + /** Listen for proactive swap notifications */ + onProactiveSwapNotification: (callback: (notification: { + fromProfile: { id: string; name: string }; + toProfile: { id: string; name: string }; + reason: string; + usageSnapshot: ClaudeUsageSnapshot; + }) => void) => () => void; + // App settings getSettings: () => Promise>; saveSettings: (settings: Partial) => Promise; diff --git a/auto-claude/cli/build_commands.py b/auto-claude/cli/build_commands.py index dfd45294..6aa7399e 100644 --- a/auto-claude/cli/build_commands.py +++ b/auto-claude/cli/build_commands.py @@ -184,8 +184,11 @@ def handle_build_command( source_spec_dir = spec_dir working_dir, worktree_manager, localized_spec_dir = setup_workspace( - project_dir, spec_dir.name, workspace_mode, source_spec_dir=spec_dir, - base_branch=base_branch + project_dir, + spec_dir.name, + workspace_mode, + source_spec_dir=spec_dir, + base_branch=base_branch, ) # Use the localized spec directory (inside worktree) for AI access if localized_spec_dir: diff --git a/auto-claude/cli/workspace_commands.py b/auto-claude/cli/workspace_commands.py index ac6b150e..793971ac 100644 --- a/auto-claude/cli/workspace_commands.py +++ b/auto-claude/cli/workspace_commands.py @@ -266,7 +266,11 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: if match: file_path = match.group(1).strip() # Skip .auto-claude files - they should never be merged - if file_path and file_path not in result["conflicting_files"] and not _is_auto_claude_file(file_path): + if ( + file_path + and file_path not in result["conflicting_files"] + and not _is_auto_claude_file(file_path) + ): result["conflicting_files"].append(file_path) # Fallback: if we didn't parse conflicts, use diff to find files changed in both branches @@ -300,7 +304,9 @@ def _check_git_merge_conflicts(project_dir: Path, spec_name: str) -> dict: # Files modified in both = potential conflicts # Filter out .auto-claude files - they should never be merged conflicting = main_files & spec_files - result["conflicting_files"] = [f for f in conflicting if not _is_auto_claude_file(f)] + result["conflicting_files"] = [ + f for f in conflicting if not _is_auto_claude_file(f) + ] debug( MODULE, f"Found {len(conflicting)} files modified in both branches" ) @@ -447,9 +453,7 @@ def handle_merge_preview_command(project_dir: Path, spec_name: str) -> dict: # Filter lock files from the git conflicts list for the response non_lock_conflicting_files = [ - f - for f in git_conflicts.get("conflicting_files", []) - if not is_lock_file(f) + f for f in git_conflicts.get("conflicting_files", []) if not is_lock_file(f) ] result = { diff --git a/auto-claude/core/workspace.py b/auto-claude/core/workspace.py index 72edabc4..eb627196 100644 --- a/auto-claude/core/workspace.py +++ b/auto-claude/core/workspace.py @@ -550,7 +550,11 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: if match: file_path = match.group(1).strip() # Skip .auto-claude files - they should never be merged - if file_path and file_path not in result["conflicting_files"] and not _is_auto_claude_file(file_path): + if ( + file_path + and file_path not in result["conflicting_files"] + and not _is_auto_claude_file(file_path) + ): result["conflicting_files"].append(file_path) # Fallback: if we didn't parse conflicts, use diff to find files changed in both branches @@ -582,7 +586,9 @@ def _check_git_conflicts(project_dir: Path, spec_name: str) -> dict: # Files modified in both = potential conflicts # Filter out .auto-claude files - they should never be merged conflicting = main_files & spec_files - result["conflicting_files"] = [f for f in conflicting if not _is_auto_claude_file(f)] + result["conflicting_files"] = [ + f for f in conflicting if not _is_auto_claude_file(f) + ] except Exception as e: print(muted(f" Error checking git conflicts: {e}")) @@ -735,7 +741,10 @@ def _resolve_git_conflicts_with_ai( # They must be regenerated after merge by running the package manager # (e.g., npm install, pnpm install, uv sync, cargo update) lock_files_excluded.append(file_path) - debug(MODULE, f" {file_path}: lock file (excluded - regenerate after merge)") + debug( + MODULE, + f" {file_path}: lock file (excluded - regenerate after merge)", + ) else: # Regular file - needs AI merge files_needing_ai_merge.append( @@ -928,7 +937,9 @@ def _resolve_git_conflicts_with_ai( if lock_files_excluded: result["lock_files_excluded"] = lock_files_excluded print() - print(muted(f" ℹ {len(lock_files_excluded)} lock file(s) excluded from merge:")) + print( + muted(f" ℹ {len(lock_files_excluded)} lock file(s) excluded from merge:") + ) for lock_file in lock_files_excluded: print(muted(f" - {lock_file}")) print() diff --git a/auto-claude/core/worktree.py b/auto-claude/core/worktree.py index 0e23c410..6afaea67 100644 --- a/auto-claude/core/worktree.py +++ b/auto-claude/core/worktree.py @@ -127,7 +127,9 @@ class WorktreeManager: break if files_to_unstage: - print(f"Unstaging {len(files_to_unstage)} auto-claude/gitignored file(s)...") + print( + f"Unstaging {len(files_to_unstage)} auto-claude/gitignored file(s)..." + ) # Unstage each file for file in files_to_unstage: self._run_git(["reset", "HEAD", "--", file])