Refactor code for improved readability and maintainability
- Updated function calls in build_commands.py for better parameter alignment. - Enhanced merge conflict checks in workspace_commands.py and workspace.py for clarity. - Improved logging and output formatting in worktree.py and other files. - Added proactive swap information to rate-limit-detector.ts and profile-storage.ts. - Updated usage monitoring methods in usage-monitor.ts and IPC types for better integration.
This commit is contained in:
@@ -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)
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<ClaudeUsageSnapshot | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -167,7 +167,7 @@ export function EnvironmentSettings({
|
||||
<Star className="h-3 w-3" />
|
||||
Active
|
||||
</span>
|
||||
{activeProfile.oauthToken ? (
|
||||
{(activeProfile.oauthToken || (activeProfile.isDefault && activeProfile.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
|
||||
|
||||
@@ -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
|
||||
</span>
|
||||
)}
|
||||
{profile.oauthToken ? (
|
||||
{(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
|
||||
|
||||
@@ -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: () => () => {}
|
||||
};
|
||||
|
||||
@@ -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<IPCResult>;
|
||||
|
||||
// Usage Monitoring (Proactive Account Switching)
|
||||
/** Request current usage snapshot */
|
||||
requestUsageUpdate: () => Promise<IPCResult<ClaudeUsageSnapshot | null>>;
|
||||
/** 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<IPCResult<AppSettings>>;
|
||||
saveSettings: (settings: Partial<AppSettings>) => Promise<IPCResult>;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user