diff --git a/apps/desktop/src/main/__tests__/ensure-onboarding-complete.test.ts b/apps/desktop/src/main/__tests__/ensure-onboarding-complete.test.ts new file mode 100644 index 00000000..a16c64b7 --- /dev/null +++ b/apps/desktop/src/main/__tests__/ensure-onboarding-complete.test.ts @@ -0,0 +1,227 @@ +/** + * Tests for ensureOnboardingComplete function in cli-integration-handler.ts + * + * Tests the exported ensureOnboardingComplete() which reads/writes .claude.json + * to set hasCompletedOnboarding: true, suppressing Claude's onboarding wizard + * for already-authenticated profiles. + */ + +import { describe, test, expect, vi, beforeEach } from 'vitest'; +import * as path from 'path'; +import * as os from 'os'; + +// ---- fs mock (sync only — the function uses fs, not fs/promises) ---- +const mockFiles: Map = new Map(); + +vi.mock('fs', () => { + const readFileSync = vi.fn((filePath: string, _encoding?: string): string => { + const entry = mockFiles.get(filePath); + if (entry === undefined) { + const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) as NodeJS.ErrnoException; + err.code = 'ENOENT'; + throw err; + } + if (entry instanceof Error) { + throw entry; + } + return entry; + }); + + const writeFileSync = vi.fn(); + const renameSync = vi.fn(); + + return { default: { readFileSync, writeFileSync, renameSync }, readFileSync, writeFileSync, renameSync }; +}); + +// ---- stubs for heavy transitive dependencies ---- +vi.mock('electron', () => ({ + ipcMain: { handle: vi.fn() }, + app: { getPath: vi.fn(() => os.tmpdir()), getAppPath: vi.fn(() => os.tmpdir()) }, + dialog: { showOpenDialog: vi.fn() }, + shell: { openExternal: vi.fn() }, +})); + +vi.mock('@electron-toolkit/utils', () => ({ is: { dev: true } })); + +vi.mock('../../shared/constants', async () => { + const actual = await vi.importActual('../../shared/constants'); + return { ...actual }; +}); + +vi.mock('../claude-profile-manager', () => ({ + getClaudeProfileManager: vi.fn(), + initializeClaudeProfileManager: vi.fn(), +})); + +vi.mock('../claude-profile/credential-utils', () => ({ + getFullCredentialsFromKeychain: vi.fn(), + clearKeychainCache: vi.fn(), + updateProfileSubscriptionMetadata: vi.fn(), +})); + +vi.mock('../claude-profile/usage-monitor', () => ({ + getUsageMonitor: vi.fn(), +})); + +vi.mock('../claude-profile/profile-utils', () => ({ + getEmailFromConfigDir: vi.fn(), +})); + +vi.mock('./output-parser', () => ({})); +vi.mock('./session-handler', () => ({})); + +vi.mock('./pty-manager', () => ({ + writeToPty: vi.fn(), + resizePty: vi.fn(), +})); + +vi.mock('../ipc-handlers/utils', () => ({ + safeSendToRenderer: vi.fn(), +})); + +vi.mock('../../shared/utils/debug-logger', () => ({ + debugLog: vi.fn(), + debugError: vi.fn(), +})); + +vi.mock('../../shared/utils/shell-escape', () => ({ + escapeShellArg: vi.fn((s: string) => s), + escapeForWindowsDoubleQuote: vi.fn((s: string) => s), + buildCdCommand: vi.fn((cwd: string) => `cd ${cwd}`), +})); + +vi.mock('../cli-utils', () => ({ + getClaudeCliInvocation: vi.fn(() => 'claude'), + getClaudeCliInvocationAsync: vi.fn(async () => 'claude'), +})); + +vi.mock('../platform', () => ({ + isWindows: vi.fn(() => false), +})); + +vi.mock('../settings-utils', () => ({ + readSettingsFileAsync: vi.fn(async () => ({})), + readSettingsFile: vi.fn(() => ({})), +})); + +// ---- import the function under test ---- +import { ensureOnboardingComplete } from '../terminal/cli-integration-handler'; +import * as fs from 'fs'; + +// ---- helpers ---- +function claudeJsonPath(configDir: string): string { + const expanded = configDir.startsWith('~') + ? configDir.replace(/^~/, os.homedir()) + : configDir; + return path.join(path.resolve(expanded), '.claude.json'); +} + +const TEST_DIR = '/tmp/test-profile'; + +describe('ensureOnboardingComplete', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockFiles.clear(); + }); + + // ---- ENOENT: file does not exist ---- + test('returns early (no write) when .claude.json does not exist', () => { + // mockFiles is empty → readFileSync will throw ENOENT + ensureOnboardingComplete(TEST_DIR); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + // ---- already set ---- + test('returns early (no write) when hasCompletedOnboarding is already true', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, JSON.stringify({ hasCompletedOnboarding: true })); + + ensureOnboardingComplete(TEST_DIR); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + // ---- missing flag → should write ---- + test('writes hasCompletedOnboarding: true when flag is absent', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, JSON.stringify({ someOtherField: 'value' })); + + ensureOnboardingComplete(TEST_DIR); + + expect(fs.writeFileSync).toHaveBeenCalledOnce(); + const written = JSON.parse((fs.writeFileSync as ReturnType).mock.calls[0][1] as string); + expect(written.hasCompletedOnboarding).toBe(true); + expect(written.someOtherField).toBe('value'); + }); + + // ---- flag is false → should write ---- + test('writes hasCompletedOnboarding: true when flag is false', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, JSON.stringify({ hasCompletedOnboarding: false })); + + ensureOnboardingComplete(TEST_DIR); + + expect(fs.writeFileSync).toHaveBeenCalledOnce(); + const written = JSON.parse((fs.writeFileSync as ReturnType).mock.calls[0][1] as string); + expect(written.hasCompletedOnboarding).toBe(true); + }); + + // ---- non-object JSON (string) → should return silently ---- + test('returns early (no write) when .claude.json contains a JSON string', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, JSON.stringify('just a string')); + + ensureOnboardingComplete(TEST_DIR); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + // ---- array JSON → should return silently ---- + test('returns early (no write) when .claude.json contains a JSON array', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, JSON.stringify([1, 2, 3])); + + ensureOnboardingComplete(TEST_DIR); + + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + // ---- corrupted / invalid JSON → outer catch swallows error ---- + test('handles corrupted JSON gracefully without throwing', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, '{ invalid json }'); + + expect(() => ensureOnboardingComplete(TEST_DIR)).not.toThrow(); + expect(fs.writeFileSync).not.toHaveBeenCalled(); + }); + + // ---- tilde expansion ---- + test('expands leading tilde to home directory', () => { + const tildeDir = '~/myprofile'; + const resolvedDir = path.resolve(tildeDir.replace(/^~/, os.homedir())); + const filePath = path.join(resolvedDir, '.claude.json'); + + mockFiles.set(filePath, JSON.stringify({})); + + ensureOnboardingComplete(tildeDir); + + expect(fs.writeFileSync).toHaveBeenCalledOnce(); + // Writes to a temp file (claudeJsonPath + UUID + .tmp), then renames to target + const writtenPath = (fs.writeFileSync as ReturnType).mock.calls[0][0] as string; + expect(writtenPath).toMatch(new RegExp(`^${filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\..*\\.tmp$`)); + expect(fs.renameSync).toHaveBeenCalledWith(writtenPath, filePath); + }); + + // ---- write error → outer catch swallows error ---- + test('handles write error gracefully without throwing', () => { + const filePath = claudeJsonPath(TEST_DIR); + mockFiles.set(filePath, JSON.stringify({})); + + (fs.writeFileSync as ReturnType).mockImplementationOnce(() => { + throw new Error('EACCES: permission denied'); + }); + + expect(() => ensureOnboardingComplete(TEST_DIR)).not.toThrow(); + }); +}); diff --git a/apps/desktop/src/main/terminal/cli-integration-handler.ts b/apps/desktop/src/main/terminal/cli-integration-handler.ts index b1be0f60..e51834e7 100644 --- a/apps/desktop/src/main/terminal/cli-integration-handler.ts +++ b/apps/desktop/src/main/terminal/cli-integration-handler.ts @@ -27,8 +27,7 @@ import type { TerminalProcess, WindowGetter, RateLimitEvent, - OAuthTokenEvent, - OnboardingCompleteEvent + OAuthTokenEvent } from './types'; // ============================================================================ @@ -564,17 +563,17 @@ export function handleOAuthToken( console.warn('[ClaudeIntegration] Profile credentials verified via Keychain (not caching token):', profileId); - // Set flag to watch for Claude's ready state (onboarding complete) - terminal.awaitingOnboardingComplete = true; + // Mark onboarding complete so future `claude` invocations skip the wizard. + // `claude auth login` creates .claude.json but doesn't set this flag. + if (profile.configDir) { + ensureOnboardingComplete(profile.configDir); + } - // needsOnboarding: true tells the UI to show "complete setup" message - // instead of "success" - user should finish Claude's onboarding before closing safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, { terminalId: terminal.id, profileId, email: emailFromOutput || keychainCreds.email || profile?.email, success: true, - needsOnboarding: true, detectedAt: new Date().toISOString() } as OAuthTokenEvent); } else { @@ -585,17 +584,16 @@ export function handleOAuthToken( if (hasCredentials) { console.warn('[ClaudeIntegration] Profile credentials verified (no Keychain token):', profileId); - // Set flag to watch for Claude's ready state (onboarding complete) - terminal.awaitingOnboardingComplete = true; + // Mark onboarding complete so future `claude` invocations skip the wizard + if (profile.configDir) { + ensureOnboardingComplete(profile.configDir); + } - // needsOnboarding: true tells the UI to show "complete setup" message - // instead of "success" - user should finish Claude's onboarding before closing safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, { terminalId: terminal.id, profileId, email: emailFromOutput || profile?.email, success: true, - needsOnboarding: true, detectedAt: new Date().toISOString() } as OAuthTokenEvent); } else { @@ -718,135 +716,19 @@ export function handleOAuthToken( /** * Handle onboarding complete detection - * Called when terminal output indicates Claude Code is ready after login/onboarding + * Called when terminal output indicates Claude Code is ready after login/onboarding. * - * This detects the Claude Code welcome screen that appears after successful login, - * which includes patterns like "Welcome back", "Claude Code v2.x", or subscription - * tier info like "Claude Max". When detected, it notifies the frontend to auto-close - * the auth terminal. + * Note: This is now a no-op. The onboarding flag is set proactively via + * ensureOnboardingComplete() in handleOAuthToken() and executeProfileCommand(), + * so awaitingOnboardingComplete is never set and this path is never reached. + * Kept as a stub to satisfy the terminal-event-handler callback interface. */ export function handleOnboardingComplete( - terminal: TerminalProcess, - data: string, - getWindow: WindowGetter + _terminal: TerminalProcess, + _data: string, + _getWindow: WindowGetter ): void { - // Only check if we're waiting for onboarding to complete - if (!terminal.awaitingOnboardingComplete) { - return; - } - - // Check if output shows Claude Code welcome screen (onboarding complete indicators) - if (!OutputParser.isOnboardingCompleteOutput(data)) { - return; - } - - console.warn('[ClaudeIntegration] Onboarding complete detected for terminal:', terminal.id); - - // Clear the flag - terminal.awaitingOnboardingComplete = false; - - // Extract profile ID from terminal ID pattern (claude-login-{profileId}-*) - const profileId = extractProfileIdFromAuthTerminalId(terminal.id) || undefined; - - // Try to extract email from the welcome screen (e.g., "user@example.com's Organization") - // Note: extractEmail automatically strips ANSI escape codes internally - let email = OutputParser.extractEmail(data); - if (!email) { - email = OutputParser.extractEmail(terminal.outputBuffer); - } - - // Fallback: If terminal extraction failed or might be corrupt, read directly from Claude's config file - // This is the authoritative source and doesn't suffer from ANSI escape code issues - const profileManager = getClaudeProfileManager(); - const profile = profileId ? profileManager.getProfile(profileId) : null; - - if (!email && profile?.configDir) { - const configEmail = getEmailFromConfigDir(profile.configDir); - if (configEmail) { - console.warn('[ClaudeIntegration] Email not found in terminal output, using config file:', maskEmail(configEmail)); - email = configEmail; - } - } - - // Validate email looks correct (basic sanity check) - // If terminal extraction gave us a truncated email but config file has the correct one, prefer config - if (email && profile?.configDir) { - const configEmail = getEmailFromConfigDir(profile.configDir); - if (configEmail && configEmail !== email) { - // Config file email is different - it's more authoritative - console.warn('[ClaudeIntegration] Terminal email differs from config file, using config file:', { - terminalEmail: maskEmail(email), - configEmail: maskEmail(configEmail) - }); - email = configEmail; - } - } - - console.warn('[ClaudeIntegration] Email extraction attempt:', { - profileId, - foundEmail: maskEmail(email), - dataLength: data.length, - bufferLength: terminal.outputBuffer.length - }); - - // Update profile with email and subscription metadata if found and profile exists - // Always update - the newly extracted email from re-authentication should overwrite any stale/truncated email - if (profileId && email && profile) { - const previousEmail = profile.email; - profile.email = email; - // Also update subscription metadata from Keychain credentials - updateProfileSubscriptionMetadata(profile, profile.configDir); - profileManager.saveProfile(profile); - if (previousEmail !== email) { - console.warn('[ClaudeIntegration] Updated profile email from welcome screen:', profileId, maskEmail(email), '(was:', maskEmail(previousEmail), ')'); - } - } - - // Persist onboarding completion so future invocations skip the wizard - if (profile?.configDir) { - ensureOnboardingComplete(profile.configDir); - } - - safeSendToRenderer(getWindow, IPC_CHANNELS.TERMINAL_ONBOARDING_COMPLETE, { - terminalId: terminal.id, - profileId, - detectedAt: new Date().toISOString() - } as OnboardingCompleteEvent); - - // Trigger immediate usage fetch after successful re-authentication - // This gives the user immediate feedback that their account is working - if (profileId) { - try { - const usageMonitor = getUsageMonitor(); - if (usageMonitor) { - // Clear any auth failure status for this profile since they just re-authenticated - usageMonitor.clearAuthFailedProfile(profileId); - - console.warn('[ClaudeIntegration] Triggering immediate usage fetch after re-authentication:', profileId); - - // Switch to this profile if it's not already active, then fetch usage - const profileManager = getClaudeProfileManager(); - - // Also clear the migration flag if this profile was migrated to an isolated directory - // This prevents the auth failure modal from showing again on next startup - if (profileManager.isProfileMigrated(profileId)) { - profileManager.clearMigratedProfile(profileId); - console.warn('[ClaudeIntegration] Cleared migration flag for re-authenticated profile:', profileId); - } - const activeProfile = profileManager.getActiveProfile(); - if (activeProfile?.id !== profileId) { - profileManager.setActiveProfile(profileId); - } - - // Small delay to allow profile switch to settle, then trigger usage fetch - setTimeout(() => { - usageMonitor.checkNow(); - }, 500); - } - } catch (error) { - console.error('[ClaudeIntegration] Failed to trigger post-auth usage fetch:', error); - } - } + // No-op — onboarding is handled proactively in handleOAuthToken() } /** @@ -903,7 +785,7 @@ export function handleClaudeExit( * When CLAUDE_CONFIG_DIR is set, Claude Code reads .claude.json from that directory. * Without this flag, it triggers the onboarding wizard even for authenticated profiles. */ -function ensureOnboardingComplete(configDir: string): void { +export function ensureOnboardingComplete(configDir: string): void { try { const expandedDir = path.resolve( configDir.startsWith('~') ? configDir.replace(/^~/, os.homedir()) : configDir @@ -932,7 +814,13 @@ function ensureOnboardingComplete(configDir: string): void { } config.hasCompletedOnboarding = true; - fs.writeFileSync(claudeJsonPath, JSON.stringify(config, null, 2), { encoding: 'utf-8' }); + const updatedContent = JSON.stringify(config, null, 2); + + // Write atomically via temp file + rename to avoid partial writes and satisfy CodeQL js/insecure-temporary-file. + // crypto.randomUUID() ensures no collisions; mode 0o600 restricts to owner-only. + const tmpPath = `${claudeJsonPath}.${crypto.randomUUID()}.tmp`; + fs.writeFileSync(tmpPath, updatedContent, { encoding: 'utf-8', mode: 0o600 }); + fs.renameSync(tmpPath, claudeJsonPath); debugLog(`[ClaudeIntegration] Set hasCompletedOnboarding in ${claudeJsonPath}`); } catch (error) { // Non-fatal — worst case the user sees onboarding once diff --git a/apps/desktop/src/main/terminal/types.ts b/apps/desktop/src/main/terminal/types.ts index 8e4cc6c7..ee20a8ff 100644 --- a/apps/desktop/src/main/terminal/types.ts +++ b/apps/desktop/src/main/terminal/types.ts @@ -26,8 +26,6 @@ export interface TerminalProcess { dangerouslySkipPermissions?: boolean; /** Shell type for Windows (affects command chaining syntax) */ shellType?: WindowsShellType; - /** Whether this terminal is waiting for Claude onboarding to complete (login flow) */ - awaitingOnboardingComplete?: boolean; /** Whether PTY has emitted exit; used to avoid writes/resizes on dead PTYs */ hasExited?: boolean; } @@ -55,19 +53,8 @@ export interface OAuthTokenEvent { success: boolean; message?: string; detectedAt: string; - /** If true, user should complete onboarding in terminal before closing */ - needsOnboarding?: boolean; } -/** - * Onboarding complete event data - * Sent when Claude Code shows its ready state after login/onboarding - */ -export interface OnboardingCompleteEvent { - terminalId: string; - profileId?: string; - detectedAt: string; -} /** * Session capture result diff --git a/apps/desktop/src/preload/api/terminal-api.ts b/apps/desktop/src/preload/api/terminal-api.ts index 0f61aab4..81474646 100644 --- a/apps/desktop/src/preload/api/terminal-api.ts +++ b/apps/desktop/src/preload/api/terminal-api.ts @@ -83,7 +83,7 @@ export interface TerminalAPI { 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 + callback: (info: { terminalId: string; profileId?: string; email?: string; success: boolean; message?: string; detectedAt: string }) => void ) => () => void; onTerminalAuthCreated: ( callback: (info: { terminalId: string; profileId: string; profileName: string }) => void diff --git a/apps/desktop/src/renderer/components/ProjectTabBar.tsx b/apps/desktop/src/renderer/components/ProjectTabBar.tsx index d76b34f4..874cf7c4 100644 --- a/apps/desktop/src/renderer/components/ProjectTabBar.tsx +++ b/apps/desktop/src/renderer/components/ProjectTabBar.tsx @@ -33,11 +33,15 @@ export function ProjectTabBar({ // Keyboard shortcuts for tab navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - // Skip if in input fields + // Skip if in input fields (but NOT xterm's hidden textarea — + // xterm already passes through Cmd/Ctrl+1-9 via attachCustomKeyEventHandler) + const target = e.target as HTMLElement; + const isXtermTextarea = target.classList?.contains('xterm-helper-textarea'); if ( - e.target instanceof HTMLInputElement || + !isXtermTextarea && + (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || - (e.target as HTMLElement)?.isContentEditable + target?.isContentEditable) ) { return; } diff --git a/apps/desktop/src/renderer/components/settings/AuthTerminal.tsx b/apps/desktop/src/renderer/components/settings/AuthTerminal.tsx index 5ffee7a3..fc409705 100644 --- a/apps/desktop/src/renderer/components/settings/AuthTerminal.tsx +++ b/apps/desktop/src/renderer/components/settings/AuthTerminal.tsx @@ -51,9 +51,8 @@ export function AuthTerminal({ const loginSentRef = useRef(false); // Track if /login was already sent const loginTimeoutRef = useRef(null); // Track setTimeout for cleanup const successTimeoutRef = useRef(null); // Track success auto-close timeout for cleanup - const authCompletedRef = useRef(false); // Track if auth has already completed to prevent race conditions - const [status, setStatus] = useState<'connecting' | 'ready' | 'onboarding' | 'success' | 'error'>('connecting'); + const [status, setStatus] = useState<'connecting' | 'ready' | 'success' | 'error'>('connecting'); const [authEmail, setAuthEmail] = useState(); const [errorMessage, setErrorMessage] = useState(); @@ -235,7 +234,6 @@ export function AuthTerminal({ thisTerminalId: terminalId, isMatch: info.terminalId === terminalId, success: info.success, - needsOnboarding: info.needsOnboarding, email: info.email, currentStatus: statusRef.current, loginSent: loginSentRef.current @@ -243,16 +241,9 @@ export function AuthTerminal({ if (info.terminalId === terminalId) { if (info.success) { setAuthEmail(info.email); - // If needsOnboarding is true, user should complete setup in terminal - // Otherwise, authentication is fully complete - if (info.needsOnboarding) { - debugLog('Setting status to onboarding', { terminalId }); - setStatus('onboarding'); - } else { - debugLog('Setting status to success (no onboarding needed)', { terminalId }); - setStatus('success'); - onAuthSuccess?.(info.email); - } + debugLog('Setting status to success', { terminalId }); + setStatus('success'); + onAuthSuccess?.(info.email); } else { debugLog('OAuth failed', { terminalId, message: info.message }); setStatus('error'); @@ -272,60 +263,13 @@ export function AuthTerminal({ terminalId, exitCode, currentStatus: statusRef.current, - loginSent: loginSentRef.current, - willTransitionToSuccess: statusRef.current === 'onboarding' && exitCode === 0 + loginSent: loginSentRef.current }); - // If we were in onboarding status and terminal exits with code 0, - // that means the user completed the onboarding successfully - if (statusRef.current === 'onboarding' && exitCode === 0) { - // Prevent race condition with onboarding-complete handler - if (authCompletedRef.current) { - debugLog('SKIPPED exit handler - auth already completed', { terminalId }); - return; - } - authCompletedRef.current = true; - debugLog('Transitioning from onboarding to success', { terminalId }); - setStatus('success'); - onAuthSuccess?.(authEmailRef.current); - } // Don't close automatically - let user see any error messages } }); cleanupFnsRef.current.push(unsubExit); - // Handle onboarding complete (Claude shows ready state after login) - const unsubOnboardingComplete = window.electronAPI.onTerminalOnboardingComplete((info) => { - if (info.terminalId === terminalId) { - console.warn('[AuthTerminal] Onboarding complete:', info); - debugLog('Onboarding complete event', { - terminalId: info.terminalId, - profileId: info.profileId, - currentStatus: statusRef.current - }); - // Only process if we're in onboarding status - if (statusRef.current === 'onboarding') { - // Prevent race condition with terminal exit handler - if (authCompletedRef.current) { - debugLog('SKIPPED onboarding-complete handler - auth already completed', { terminalId }); - return; - } - authCompletedRef.current = true; - debugLog('Auto-closing terminal after onboarding complete', { terminalId }); - setStatus('success'); - onAuthSuccess?.(authEmailRef.current); - // Auto-close after a brief delay to show success UI - successTimeoutRef.current = setTimeout(() => { - if (isCreatedRef.current) { - window.electronAPI.destroyTerminal(terminalId).catch(console.error); - isCreatedRef.current = false; - } - onClose(); - }, 1500); - } - } - }); - cleanupFnsRef.current.push(unsubOnboardingComplete); - return () => { debugLog('Cleaning up event listeners', { terminalId }); inputDisposable.dispose(); @@ -408,9 +352,6 @@ export function AuthTerminal({ {status === 'ready' && (
)} - {status === 'onboarding' && ( -
- )} {status === 'success' && ( )} @@ -420,7 +361,6 @@ export function AuthTerminal({ {status === 'connecting' && t('authTerminal.connecting')} {status === 'ready' && t('authTerminal.authenticate', { profileName })} - {status === 'onboarding' && t('authTerminal.completeSetup', { profileName })} {status === 'success' && (authEmail ? t('authTerminal.authenticatedAs', { email: authEmail }) : t('authTerminal.authenticated'))} {status === 'error' && t('authTerminal.authError')} @@ -446,13 +386,6 @@ export function AuthTerminal({ /> {/* Status bar */} - {status === 'onboarding' && ( -
-

- {t('authTerminal.onboardingMessage')} -

-
- )} {status === 'success' && (

diff --git a/apps/desktop/src/shared/types/ipc.ts b/apps/desktop/src/shared/types/ipc.ts index f4f90ea1..3c25902b 100644 --- a/apps/desktop/src/shared/types/ipc.ts +++ b/apps/desktop/src/shared/types/ipc.ts @@ -300,8 +300,6 @@ export interface ElectronAPILegacy { success: boolean; message?: string; detectedAt: string; - /** If true, user should complete onboarding in terminal before closing */ - needsOnboarding?: boolean; }) => void) => () => void; /** Listen for auth terminal creation - allows UI to display the OAuth terminal */ onTerminalAuthCreated: (callback: (info: {