1e72c8d773
* refactor: streamline profile management and enhance usage monitoring - Consolidated profile management logic to improve clarity and maintainability. - Enhanced usage monitoring to support both OAuth and API profiles, ensuring accurate data retrieval. - Updated error handling for API requests to provide better feedback and prevent silent failures. - Improved test coverage for profile detection and usage monitoring functionalities. These changes aim to optimize the user experience by ensuring that profile-related data is handled consistently and that usage metrics are accurately reported across different authentication methods. * fix(tests): update tests for new auth badge display behavior - Update AuthStatusIndicator tests to expect "Claude Code"/"API Key" badge labels instead of provider names (Anthropic, z.ai, ZHIPU AI) - Fix usage-monitor tests that relied on console.warn calls (now uses debugLog) - Add missing mock Response headers to prevent TypeError in fetch tests - Convert dynamic require to static import in claude-profile-manager Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Address PR review findings for account swapping - Add `persistenceFailed` flag to EnsureValidTokenResult for callers to detect when token refresh succeeded but keychain write failed - Add collision detection to profile migration to handle cases where two profile names sanitize to the same directory name - Change hardcoded 'default' to 'unknown' in updateTaskSession when no profile assignment exists, and add optional profileInfo parameter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Resolve CodeQL security alerts - Fix TOCTOU race condition in profile migration by using 'wx' flag for atomic file creation instead of existsSync check - Remove unused imports: DEFAULT_CLAUDE_CONFIG_DIR, BrowserWindow, IPC_CHANNELS, User The medium severity alerts for "Network data written to file" and "File data in outbound network request" are expected behavior for credential management and API authentication respectively. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(linux): Add Secret Service support for credential storage Linux credentials now use the Secret Service API (gnome-keyring/kwallet) via the `secret-tool` CLI, matching how macOS uses Keychain and Windows uses Credential Manager. Implementation: - getCredentialsFromLinuxSecretService: Retrieve tokens from Secret Service - getFullCredentialsFromLinuxSecretService: Retrieve full OAuth credentials - updateLinuxSecretServiceCredentials: Store refreshed tokens - Automatic fallback to .credentials.json file if Secret Service unavailable The `secret-tool` command is part of libsecret-tools package, commonly available on most Linux desktop distributions. This provides secure credential storage instead of plaintext file storage. Credentials are stored with: - Label: "Claude Code-credentials" - Attribute: application=claude-code (or claude-code-{hash} for profiles) Fixes security gap where Linux used file storage while macOS and Windows used proper secure credential stores. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: Add guidelines against providing time estimates in development Introduced a critical section in the development guidelines emphasizing the avoidance of time estimates for tasks. This change highlights the misleading nature of traditional time predictions in AI-assisted development and encourages a focus on actionable steps and priority-based ordering instead. Examples of incorrect and correct approaches are provided for clarity. * fix: address PR review findings for account swapping MEDIUM severity fixes: - Implement getBestProfileForTask handler with actual profile selection logic - Refactor credential-utils.ts to eliminate code duplication with shared helpers - Fix PowerShell escaping vulnerabilities with proper character escaping - Use base64 encoding for JSON data passed to PowerShell scripts - Add warning that returned token may be revoked after server-side refresh failure - Document side effect in getBestAvailableProfileEnv function LOW severity fixes: - Reduce token fingerprint logging from 12+4 to 4+2 chars - Document >= threshold as intentional (proactive switching before limits) - Move writeFileSync import to top of file with other fs imports - Remove unnecessary existsSync checks (mkdirSync recursive is idempotent) - Add homedir to top-level imports from 'os' - Add comment noting acceptable TOCTOU race window in profile-storage.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: clear credential cache after platform credential updates Add clearCredentialCache() calls to all platform-specific update functions to prevent stale cached tokens from being returned after successful updates: - updateMacOSKeychainCredentials - updateLinuxSecretServiceCredentials - updateLinuxFileCredentials - updateWindowsCredentialManagerCredentials This ensures callers always get fresh credential values after token refresh, rather than waiting for the 5-minute cache TTL to expire. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle persistenceFailed flag in token refresh calls When token refresh succeeds but fails to persist to keychain, the user needs to be notified to re-authenticate. Previously, the persistenceFailed flag was ignored, which could lead to authentication errors on app restart. Now all three call sites for ensureValidToken/reactiveTokenRefresh properly: - Check the persistenceFailed flag - Add the profile to needsReauthProfiles set when persistence fails - Log a warning about the persistence failure - Clear from needsReauthProfiles only when both refresh AND persistence succeed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: enhance credential management and re-authentication flow - Updated macOS keychain handling to ensure existing credentials are deleted before adding new ones, preventing stale tokens. - Integrated credential checks in profile authentication to verify token presence in the keychain, improving user experience by flagging profiles needing re-authentication. - Enhanced the UsageIndicator component to provide clearer messaging for re-authentication requirements, improving user feedback. - Added new localization strings for re-authentication prompts in both English and French. This update addresses critical issues with credential persistence and user notifications, ensuring a smoother authentication experience across platforms. * fix(auth): prevent false auth failures from file names in logs Change auth failure pattern from \s* to \s+ to require at least one whitespace character between "auth" and "failure/error/failed". This fixes false positives where log lines like "[ParallelOrchestrator] Reading AuthFailureModal.tsx" were incorrectly triggering auth failure modals. The pattern matched "AuthFailure" (zero whitespace) even though the OAuth token was valid. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(auth): use imported homedir instead of inline require Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
566 lines
23 KiB
TypeScript
566 lines
23 KiB
TypeScript
import { ipcRenderer } from 'electron';
|
|
import { IPC_CHANNELS } from '../../shared/constants';
|
|
|
|
// Increase max listeners to accommodate 12 terminals with multiple event types
|
|
// Each terminal can have listeners for: output, exit, titleChange, claudeSession, etc.
|
|
// Default is 10, but with 12 terminals we need more headroom
|
|
ipcRenderer.setMaxListeners(50);
|
|
|
|
import type {
|
|
IPCResult,
|
|
TerminalCreateOptions,
|
|
RateLimitInfo,
|
|
ClaudeProfile,
|
|
ClaudeProfileSettings,
|
|
ClaudeUsageSnapshot,
|
|
CreateTerminalWorktreeRequest,
|
|
TerminalWorktreeConfig,
|
|
TerminalWorktreeResult,
|
|
OtherWorktreeInfo,
|
|
TerminalProfileChangedEvent,
|
|
} from '../../shared/types';
|
|
|
|
/** Type for proactive swap notification events */
|
|
interface ProactiveSwapNotification {
|
|
fromProfile: { id: string; name: string };
|
|
toProfile: { id: string; name: string };
|
|
reason: string;
|
|
usageSnapshot: ClaudeUsageSnapshot;
|
|
}
|
|
|
|
export interface TerminalAPI {
|
|
// Terminal Operations
|
|
createTerminal: (options: TerminalCreateOptions) => Promise<IPCResult>;
|
|
destroyTerminal: (id: string) => Promise<IPCResult>;
|
|
sendTerminalInput: (id: string, data: string) => void;
|
|
resizeTerminal: (id: string, cols: number, rows: number) => void;
|
|
invokeClaudeInTerminal: (id: string, cwd?: string) => void;
|
|
generateTerminalName: (command: string, cwd?: string) => Promise<IPCResult<string>>;
|
|
setTerminalTitle: (id: string, title: string) => void;
|
|
setTerminalWorktreeConfig: (id: string, config: TerminalWorktreeConfig | undefined) => void;
|
|
|
|
// Terminal Session Management
|
|
getTerminalSessions: (projectPath: string) => Promise<IPCResult<import('../../shared/types').TerminalSession[]>>;
|
|
restoreTerminalSession: (
|
|
session: import('../../shared/types').TerminalSession,
|
|
cols?: number,
|
|
rows?: number
|
|
) => Promise<IPCResult<import('../../shared/types').TerminalRestoreResult>>;
|
|
clearTerminalSessions: (projectPath: string) => Promise<IPCResult>;
|
|
resumeClaudeInTerminal: (id: string, sessionId?: string) => void;
|
|
activateDeferredClaudeResume: (id: string) => void;
|
|
getTerminalSessionDates: (projectPath?: string) => Promise<IPCResult<import('../../shared/types').SessionDateInfo[]>>;
|
|
getTerminalSessionsForDate: (
|
|
date: string,
|
|
projectPath: string
|
|
) => Promise<IPCResult<import('../../shared/types').TerminalSession[]>>;
|
|
restoreTerminalSessionsFromDate: (
|
|
date: string,
|
|
projectPath: string,
|
|
cols?: number,
|
|
rows?: number
|
|
) => Promise<IPCResult<import('../../shared/types').SessionDateRestoreResult>>;
|
|
checkTerminalPtyAlive: (terminalId: string) => Promise<IPCResult<{ alive: boolean }>>;
|
|
updateTerminalDisplayOrders: (
|
|
projectPath: string,
|
|
orders: Array<{ terminalId: string; displayOrder: number }>
|
|
) => Promise<IPCResult>;
|
|
|
|
// Terminal Worktree Operations (isolated development)
|
|
createTerminalWorktree: (request: CreateTerminalWorktreeRequest) => Promise<TerminalWorktreeResult>;
|
|
listTerminalWorktrees: (projectPath: string) => Promise<IPCResult<TerminalWorktreeConfig[]>>;
|
|
removeTerminalWorktree: (projectPath: string, name: string, deleteBranch?: boolean) => Promise<IPCResult>;
|
|
listOtherWorktrees: (projectPath: string) => Promise<IPCResult<OtherWorktreeInfo[]>>;
|
|
|
|
// Terminal Event Listeners
|
|
onTerminalOutput: (callback: (id: string, data: string) => void) => () => void;
|
|
onTerminalExit: (callback: (id: string, exitCode: number) => void) => () => void;
|
|
onTerminalTitleChange: (callback: (id: string, title: string) => void) => () => void;
|
|
onTerminalWorktreeConfigChange: (callback: (id: string, config: TerminalWorktreeConfig | undefined) => void) => () => void;
|
|
onTerminalClaudeSession: (callback: (id: string, sessionId: string) => void) => () => void;
|
|
onTerminalRateLimit: (callback: (info: RateLimitInfo) => void) => () => void;
|
|
onTerminalOAuthToken: (
|
|
callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string; needsOnboarding?: boolean }) => void
|
|
) => () => void;
|
|
onTerminalAuthCreated: (
|
|
callback: (info: { terminalId: string; profileId: string; profileName: string }) => void
|
|
) => () => void;
|
|
onTerminalOAuthCodeNeeded: (
|
|
callback: (info: { terminalId: string; profileId: string; profileName: string }) => void
|
|
) => () => void;
|
|
submitOAuthCode: (terminalId: string, code: string) => Promise<IPCResult>;
|
|
onTerminalClaudeBusy: (callback: (id: string, isBusy: boolean) => void) => () => void;
|
|
onTerminalClaudeExit: (callback: (id: string) => void) => () => void;
|
|
onTerminalOnboardingComplete: (
|
|
callback: (info: { terminalId: string; profileId?: string; detectedAt: string }) => void
|
|
) => () => void;
|
|
onTerminalPendingResume: (callback: (id: string, sessionId?: string) => void) => () => void;
|
|
onTerminalProfileChanged: (callback: (event: TerminalProfileChangedEvent) => void) => () => void;
|
|
|
|
// Claude Profile Management
|
|
getClaudeProfiles: () => Promise<IPCResult<ClaudeProfileSettings>>;
|
|
saveClaudeProfile: (profile: ClaudeProfile) => Promise<IPCResult<ClaudeProfile>>;
|
|
deleteClaudeProfile: (profileId: string) => Promise<IPCResult>;
|
|
renameClaudeProfile: (profileId: string, newName: string) => Promise<IPCResult>;
|
|
setActiveClaudeProfile: (profileId: string) => Promise<IPCResult>;
|
|
switchClaudeProfile: (terminalId: string, profileId: string) => Promise<IPCResult>;
|
|
initializeClaudeProfile: (profileId: string) => Promise<IPCResult>;
|
|
setClaudeProfileToken: (profileId: string, token: string, email?: string) => Promise<IPCResult>;
|
|
authenticateClaudeProfile: (profileId: string) => Promise<IPCResult<{ terminalId: string; configDir: string }>>;
|
|
verifyClaudeProfileAuth: (profileId: string) => Promise<IPCResult<{ authenticated: boolean; email?: string }>>;
|
|
getAutoSwitchSettings: () => Promise<IPCResult<import('../../shared/types').ClaudeAutoSwitchSettings>>;
|
|
updateAutoSwitchSettings: (settings: Partial<import('../../shared/types').ClaudeAutoSwitchSettings>) => Promise<IPCResult>;
|
|
getAccountPriorityOrder: () => Promise<IPCResult<string[]>>;
|
|
setAccountPriorityOrder: (order: string[]) => Promise<IPCResult>;
|
|
fetchClaudeUsage: (terminalId: string) => Promise<IPCResult>;
|
|
getBestAvailableProfile: (excludeProfileId?: string) => Promise<IPCResult<import('../../shared/types').ClaudeProfile | null>>;
|
|
onSDKRateLimit: (callback: (info: import('../../shared/types').SDKRateLimitInfo) => void) => () => void;
|
|
onAuthFailure: (callback: (info: import('../../shared/types').AuthFailureInfo) => void) => () => void;
|
|
retryWithProfile: (request: import('../../shared/types').RetryWithProfileRequest) => Promise<IPCResult>;
|
|
|
|
// Usage Monitoring (Proactive Account Switching)
|
|
requestUsageUpdate: () => Promise<IPCResult<import('../../shared/types').ClaudeUsageSnapshot | null>>;
|
|
requestAllProfilesUsage: (forceRefresh?: boolean) => Promise<IPCResult<import('../../shared/types').AllProfilesUsage | null>>;
|
|
onUsageUpdated: (callback: (usage: import('../../shared/types').ClaudeUsageSnapshot) => void) => () => void;
|
|
onAllProfilesUsageUpdated: (callback: (allProfilesUsage: import('../../shared/types').AllProfilesUsage) => void) => () => void;
|
|
onProactiveSwapNotification: (callback: (notification: ProactiveSwapNotification) => void) => () => void;
|
|
}
|
|
|
|
export const createTerminalAPI = (): TerminalAPI => ({
|
|
// Terminal Operations
|
|
createTerminal: (options: TerminalCreateOptions): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_CREATE, options),
|
|
|
|
destroyTerminal: (id: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_DESTROY, id),
|
|
|
|
sendTerminalInput: (id: string, data: string): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_INPUT, id, data),
|
|
|
|
resizeTerminal: (id: string, cols: number, rows: number): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_RESIZE, id, cols, rows),
|
|
|
|
invokeClaudeInTerminal: (id: string, cwd?: string): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_INVOKE_CLAUDE, id, cwd),
|
|
|
|
generateTerminalName: (command: string, cwd?: string): Promise<IPCResult<string>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_GENERATE_NAME, command, cwd),
|
|
|
|
setTerminalTitle: (id: string, title: string): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_SET_TITLE, id, title),
|
|
|
|
setTerminalWorktreeConfig: (id: string, config: TerminalWorktreeConfig | undefined): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_SET_WORKTREE_CONFIG, id, config),
|
|
|
|
// Terminal Session Management
|
|
getTerminalSessions: (projectPath: string): Promise<IPCResult<import('../../shared/types').TerminalSession[]>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_GET_SESSIONS, projectPath),
|
|
|
|
restoreTerminalSession: (
|
|
session: import('../../shared/types').TerminalSession,
|
|
cols?: number,
|
|
rows?: number
|
|
): Promise<IPCResult<import('../../shared/types').TerminalRestoreResult>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_RESTORE_SESSION, session, cols, rows),
|
|
|
|
clearTerminalSessions: (projectPath: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_CLEAR_SESSIONS, projectPath),
|
|
|
|
resumeClaudeInTerminal: (id: string, sessionId?: string): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_RESUME_CLAUDE, id, sessionId),
|
|
|
|
activateDeferredClaudeResume: (id: string): void =>
|
|
ipcRenderer.send(IPC_CHANNELS.TERMINAL_ACTIVATE_DEFERRED_RESUME, id),
|
|
|
|
getTerminalSessionDates: (projectPath?: string): Promise<IPCResult<import('../../shared/types').SessionDateInfo[]>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_GET_SESSION_DATES, projectPath),
|
|
|
|
getTerminalSessionsForDate: (
|
|
date: string,
|
|
projectPath: string
|
|
): Promise<IPCResult<import('../../shared/types').TerminalSession[]>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_GET_SESSIONS_FOR_DATE, date, projectPath),
|
|
|
|
restoreTerminalSessionsFromDate: (
|
|
date: string,
|
|
projectPath: string,
|
|
cols?: number,
|
|
rows?: number
|
|
): Promise<IPCResult<import('../../shared/types').SessionDateRestoreResult>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_RESTORE_FROM_DATE, date, projectPath, cols, rows),
|
|
|
|
checkTerminalPtyAlive: (terminalId: string): Promise<IPCResult<{ alive: boolean }>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_CHECK_PTY_ALIVE, terminalId),
|
|
|
|
updateTerminalDisplayOrders: (
|
|
projectPath: string,
|
|
orders: Array<{ terminalId: string; displayOrder: number }>
|
|
): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_UPDATE_DISPLAY_ORDERS, projectPath, orders),
|
|
|
|
// Terminal Worktree Operations (isolated development)
|
|
createTerminalWorktree: (request: CreateTerminalWorktreeRequest): Promise<TerminalWorktreeResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_WORKTREE_CREATE, request),
|
|
|
|
listTerminalWorktrees: (projectPath: string): Promise<IPCResult<TerminalWorktreeConfig[]>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_WORKTREE_LIST, projectPath),
|
|
|
|
removeTerminalWorktree: (projectPath: string, name: string, deleteBranch: boolean = false): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_WORKTREE_REMOVE, projectPath, name, deleteBranch),
|
|
|
|
listOtherWorktrees: (projectPath: string): Promise<IPCResult<OtherWorktreeInfo[]>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_WORKTREE_LIST_OTHER, projectPath),
|
|
|
|
// Terminal Event Listeners
|
|
onTerminalOutput: (
|
|
callback: (id: string, data: string) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
data: string
|
|
): void => {
|
|
callback(id, data);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_OUTPUT, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_OUTPUT, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalExit: (
|
|
callback: (id: string, exitCode: number) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
exitCode: number
|
|
): void => {
|
|
callback(id, exitCode);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_EXIT, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_EXIT, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalTitleChange: (
|
|
callback: (id: string, title: string) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
title: string
|
|
): void => {
|
|
callback(id, title);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_TITLE_CHANGE, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalWorktreeConfigChange: (
|
|
callback: (id: string, config: TerminalWorktreeConfig | undefined) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
config: TerminalWorktreeConfig | undefined
|
|
): void => {
|
|
callback(id, config);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_WORKTREE_CONFIG_CHANGE, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_WORKTREE_CONFIG_CHANGE, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalClaudeSession: (
|
|
callback: (id: string, sessionId: string) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
sessionId: string
|
|
): void => {
|
|
callback(id, sessionId);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_CLAUDE_SESSION, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_CLAUDE_SESSION, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalRateLimit: (
|
|
callback: (info: RateLimitInfo) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: RateLimitInfo
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_RATE_LIMIT, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_RATE_LIMIT, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalOAuthToken: (
|
|
callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string }) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string }
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalAuthCreated: (
|
|
callback: (info: { terminalId: string; profileId: string; profileName: string }) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: { terminalId: string; profileId: string; profileName: string }
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_AUTH_CREATED, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_AUTH_CREATED, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalOAuthCodeNeeded: (
|
|
callback: (info: { terminalId: string; profileId: string; profileName: string }) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: { terminalId: string; profileId: string; profileName: string }
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_OAUTH_CODE_NEEDED, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_OAUTH_CODE_NEEDED, handler);
|
|
};
|
|
},
|
|
|
|
submitOAuthCode: (terminalId: string, code: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_OAUTH_CODE_SUBMIT, terminalId, code),
|
|
|
|
onTerminalClaudeBusy: (
|
|
callback: (id: string, isBusy: boolean) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
isBusy: boolean
|
|
): void => {
|
|
callback(id, isBusy);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_CLAUDE_BUSY, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_CLAUDE_BUSY, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalClaudeExit: (
|
|
callback: (id: string) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string
|
|
): void => {
|
|
callback(id);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_CLAUDE_EXIT, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_CLAUDE_EXIT, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalOnboardingComplete: (
|
|
callback: (info: { terminalId: string; profileId?: string; detectedAt: string }) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: { terminalId: string; profileId?: string; detectedAt: string }
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalPendingResume: (
|
|
callback: (id: string, sessionId?: string) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
id: string,
|
|
sessionId?: string
|
|
): void => {
|
|
callback(id, sessionId);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_PENDING_RESUME, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_PENDING_RESUME, handler);
|
|
};
|
|
},
|
|
|
|
onTerminalProfileChanged: (
|
|
callback: (event: TerminalProfileChangedEvent) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
data: TerminalProfileChangedEvent
|
|
): void => {
|
|
callback(data);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.TERMINAL_PROFILE_CHANGED, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.TERMINAL_PROFILE_CHANGED, handler);
|
|
};
|
|
},
|
|
|
|
// Claude Profile Management
|
|
getClaudeProfiles: (): Promise<IPCResult<ClaudeProfileSettings>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILES_GET),
|
|
|
|
saveClaudeProfile: (profile: ClaudeProfile): Promise<IPCResult<ClaudeProfile>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_SAVE, profile),
|
|
|
|
deleteClaudeProfile: (profileId: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_DELETE, profileId),
|
|
|
|
renameClaudeProfile: (profileId: string, newName: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_RENAME, profileId, newName),
|
|
|
|
setActiveClaudeProfile: (profileId: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_SET_ACTIVE, profileId),
|
|
|
|
switchClaudeProfile: (terminalId: string, profileId: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_SWITCH, terminalId, profileId),
|
|
|
|
initializeClaudeProfile: (profileId: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_INITIALIZE, profileId),
|
|
|
|
setClaudeProfileToken: (profileId: string, token: string, email?: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_SET_TOKEN, profileId, token, email),
|
|
|
|
authenticateClaudeProfile: (profileId: string): Promise<IPCResult<{ terminalId: string; configDir: string }>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_AUTHENTICATE, profileId),
|
|
|
|
verifyClaudeProfileAuth: (profileId: string): Promise<IPCResult<{ authenticated: boolean; email?: string }>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_VERIFY_AUTH, profileId),
|
|
|
|
getAutoSwitchSettings: (): Promise<IPCResult<import('../../shared/types').ClaudeAutoSwitchSettings>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_AUTO_SWITCH_SETTINGS),
|
|
|
|
updateAutoSwitchSettings: (settings: Partial<import('../../shared/types').ClaudeAutoSwitchSettings>): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_UPDATE_AUTO_SWITCH, settings),
|
|
|
|
getAccountPriorityOrder: (): Promise<IPCResult<string[]>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.ACCOUNT_PRIORITY_GET),
|
|
|
|
setAccountPriorityOrder: (order: string[]): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.ACCOUNT_PRIORITY_SET, order),
|
|
|
|
fetchClaudeUsage: (terminalId: string): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_FETCH_USAGE, terminalId),
|
|
|
|
getBestAvailableProfile: (excludeProfileId?: string): Promise<IPCResult<import('../../shared/types').ClaudeProfile | null>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_PROFILE_GET_BEST_PROFILE, excludeProfileId),
|
|
|
|
onSDKRateLimit: (
|
|
callback: (info: import('../../shared/types').SDKRateLimitInfo) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: import('../../shared/types').SDKRateLimitInfo
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.CLAUDE_SDK_RATE_LIMIT, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.CLAUDE_SDK_RATE_LIMIT, handler);
|
|
};
|
|
},
|
|
|
|
onAuthFailure: (
|
|
callback: (info: import('../../shared/types').AuthFailureInfo) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
info: import('../../shared/types').AuthFailureInfo
|
|
): void => {
|
|
callback(info);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.CLAUDE_AUTH_FAILURE, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.CLAUDE_AUTH_FAILURE, handler);
|
|
};
|
|
},
|
|
|
|
retryWithProfile: (request: import('../../shared/types').RetryWithProfileRequest): Promise<IPCResult> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.CLAUDE_RETRY_WITH_PROFILE, request),
|
|
|
|
// Usage Monitoring (Proactive Account Switching)
|
|
requestUsageUpdate: (): Promise<IPCResult<import('../../shared/types').ClaudeUsageSnapshot | null>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.USAGE_REQUEST),
|
|
|
|
requestAllProfilesUsage: (forceRefresh?: boolean): Promise<IPCResult<import('../../shared/types').AllProfilesUsage | null>> =>
|
|
ipcRenderer.invoke(IPC_CHANNELS.ALL_PROFILES_USAGE_REQUEST, forceRefresh ?? false),
|
|
|
|
onUsageUpdated: (
|
|
callback: (usage: import('../../shared/types').ClaudeUsageSnapshot) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
usage: import('../../shared/types').ClaudeUsageSnapshot
|
|
): void => {
|
|
callback(usage);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.USAGE_UPDATED, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.USAGE_UPDATED, handler);
|
|
};
|
|
},
|
|
|
|
onAllProfilesUsageUpdated: (
|
|
callback: (allProfilesUsage: import('../../shared/types').AllProfilesUsage) => void
|
|
): (() => void) => {
|
|
const handler = (
|
|
_event: Electron.IpcRendererEvent,
|
|
allProfilesUsage: import('../../shared/types').AllProfilesUsage
|
|
): void => {
|
|
callback(allProfilesUsage);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.ALL_PROFILES_USAGE_UPDATED, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.ALL_PROFILES_USAGE_UPDATED, handler);
|
|
};
|
|
},
|
|
|
|
onProactiveSwapNotification: (
|
|
callback: (notification: ProactiveSwapNotification) => void
|
|
): (() => void) => {
|
|
const handler = (_event: Electron.IpcRendererEvent, notification: ProactiveSwapNotification): void => {
|
|
callback(notification);
|
|
};
|
|
ipcRenderer.on(IPC_CHANNELS.PROACTIVE_SWAP_NOTIFICATION, handler);
|
|
return () => {
|
|
ipcRenderer.removeListener(IPC_CHANNELS.PROACTIVE_SWAP_NOTIFICATION, handler);
|
|
};
|
|
}
|
|
});
|