diff --git a/src/main/ipc/cliInstaller.ts b/src/main/ipc/cliInstaller.ts index 64806f83..de4d5fda 100644 --- a/src/main/ipc/cliInstaller.ts +++ b/src/main/ipc/cliInstaller.ts @@ -10,6 +10,7 @@ import { CLI_INSTALLER_GET_STATUS, CLI_INSTALLER_INSTALL, + CLI_INSTALLER_INVALIDATE_STATUS, // eslint-disable-next-line boundaries/element-types -- IPC channel constants shared between main and preload } from '@preload/constants/ipcChannels'; import { getErrorMessage } from '@shared/utils/errorHandling'; @@ -39,6 +40,7 @@ export function initializeCliInstallerHandlers(installerService: CliInstallerSer export function registerCliInstallerHandlers(ipcMain: IpcMain): void { ipcMain.handle(CLI_INSTALLER_GET_STATUS, handleGetStatus); ipcMain.handle(CLI_INSTALLER_INSTALL, handleInstall); + ipcMain.handle(CLI_INSTALLER_INVALIDATE_STATUS, handleInvalidateStatus); logger.info('CLI installer handlers registered'); } @@ -49,6 +51,7 @@ export function registerCliInstallerHandlers(ipcMain: IpcMain): void { export function removeCliInstallerHandlers(ipcMain: IpcMain): void { ipcMain.removeHandler(CLI_INSTALLER_GET_STATUS); ipcMain.removeHandler(CLI_INSTALLER_INSTALL); + ipcMain.removeHandler(CLI_INSTALLER_INVALIDATE_STATUS); logger.info('CLI installer handlers removed'); } @@ -105,3 +108,8 @@ async function handleInstall(_event: IpcMainInvokeEvent): Promise { + cachedStatus = null; + return { success: true, data: undefined }; +} diff --git a/src/main/services/infrastructure/PtyTerminalService.ts b/src/main/services/infrastructure/PtyTerminalService.ts index 7e50e3de..d1828bc9 100644 --- a/src/main/services/infrastructure/PtyTerminalService.ts +++ b/src/main/services/infrastructure/PtyTerminalService.ts @@ -7,6 +7,7 @@ import crypto from 'node:crypto'; +import { buildEnrichedEnv } from '@main/utils/cliEnv'; import { getHomeDir } from '@main/utils/pathDecoder'; // eslint-disable-next-line boundaries/element-types -- IPC channel constants shared between main and preload import { TERMINAL_DATA, TERMINAL_EXIT } from '@preload/constants/ipcChannels'; @@ -65,9 +66,7 @@ export class PtyTerminalService { rows: options?.rows ?? 24, cwd: options?.cwd ?? home, env: { - ...process.env, - HOME: home, - USERPROFILE: home, + ...buildEnrichedEnv(), ...options?.env, } as Record, }); diff --git a/src/preload/constants/ipcChannels.ts b/src/preload/constants/ipcChannels.ts index 19131baf..3f4b47db 100644 --- a/src/preload/constants/ipcChannels.ts +++ b/src/preload/constants/ipcChannels.ts @@ -405,6 +405,9 @@ export const CLI_INSTALLER_INSTALL = 'cliInstaller:install'; /** CLI installer progress events (main -> renderer) */ export const CLI_INSTALLER_PROGRESS = 'cliInstaller:progress'; +/** Invalidate cached CLI status (forces fresh check on next getStatus) */ +export const CLI_INSTALLER_INVALIDATE_STATUS = 'cliInstaller:invalidateStatus'; + // ============================================================================= // Terminal API Channels // ============================================================================= diff --git a/src/preload/index.ts b/src/preload/index.ts index 3aa647db..6a5eda34 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -10,6 +10,7 @@ import { APP_RELAUNCH, CLI_INSTALLER_GET_STATUS, CLI_INSTALLER_INSTALL, + CLI_INSTALLER_INVALIDATE_STATUS, CLI_INSTALLER_PROGRESS, CONTEXT_CHANGED, CONTEXT_GET_ACTIVE, @@ -1293,6 +1294,9 @@ const electronAPI: ElectronAPI = { install: async (): Promise => { return invokeIpcWithResult(CLI_INSTALLER_INSTALL); }, + invalidateStatus: async (): Promise => { + return invokeIpcWithResult(CLI_INSTALLER_INVALIDATE_STATUS); + }, onProgress: (callback: (event: unknown, data: CliInstallerProgress) => void): (() => void) => { ipcRenderer.on( CLI_INSTALLER_PROGRESS, diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index a73fd8cb..a33577a2 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -1045,6 +1045,7 @@ export class HttpAPIClient implements ElectronAPI { install: async (): Promise => { console.warn('[HttpAPIClient] CLI installer not available in browser mode'); }, + invalidateStatus: async (): Promise => {}, onProgress: (): (() => void) => { return () => {}; }, diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index b59ecc40..c5e490cd 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -272,11 +272,13 @@ export const CliStatusBanner = (): React.JSX.Element | null => { installerRawChunks, completedVersion, fetchCliStatus, + invalidateCliStatus, installCli, isBusy, } = useCliInstaller(); const [showLoginTerminal, setShowLoginTerminal] = useState(false); + const [isVerifyingAuth, setIsVerifyingAuth] = useState(false); useEffect(() => { if (!isElectron) return; @@ -526,6 +528,22 @@ export const CliStatusBanner = (): React.JSX.Element | null => { // Installed but not logged in — yellow warning banner if (cliStatus.installed && !cliStatus.authLoggedIn) { + if (isVerifyingAuth) { + return ( +
+ +

+ Verifying authentication... +

+
+ ); + } return ( <>
{ args={['auth', 'login']} onClose={() => { setShowLoginTerminal(false); - void fetchCliStatus(); + setIsVerifyingAuth(true); + void (async () => { + try { + await invalidateCliStatus(); + await fetchCliStatus(); + } finally { + setIsVerifyingAuth(false); + } + })(); }} onExit={() => { - void fetchCliStatus(); + setIsVerifyingAuth(true); + void (async () => { + try { + await invalidateCliStatus(); + await fetchCliStatus(); + } finally { + setIsVerifyingAuth(false); + } + })(); }} autoCloseOnSuccessMs={4000} successMessage="Login complete" diff --git a/src/renderer/hooks/useCliInstaller.ts b/src/renderer/hooks/useCliInstaller.ts index 679d5e23..a7831d6d 100644 --- a/src/renderer/hooks/useCliInstaller.ts +++ b/src/renderer/hooks/useCliInstaller.ts @@ -29,6 +29,7 @@ export function useCliInstaller(): { installerRawChunks: string[]; completedVersion: string | null; fetchCliStatus: () => Promise; + invalidateCliStatus: () => Promise; installCli: () => void; isBusy: boolean; } { @@ -44,6 +45,7 @@ export function useCliInstaller(): { const installerRawChunks = useStore((s) => s.cliInstallerRawChunks); const completedVersion = useStore((s) => s.cliCompletedVersion); const fetchCliStatus = useStore((s) => s.fetchCliStatus); + const invalidateCliStatus = useStore((s) => s.invalidateCliStatus); const installCli = useStore((s) => s.installCli); const isBusy = installerState !== 'idle' && installerState !== 'error'; @@ -61,6 +63,7 @@ export function useCliInstaller(): { installerRawChunks, completedVersion, fetchCliStatus, + invalidateCliStatus, installCli, isBusy, }; diff --git a/src/renderer/store/slices/cliInstallerSlice.ts b/src/renderer/store/slices/cliInstallerSlice.ts index 92b25fdb..929b5938 100644 --- a/src/renderer/store/slices/cliInstallerSlice.ts +++ b/src/renderer/store/slices/cliInstallerSlice.ts @@ -42,6 +42,7 @@ export interface CliInstallerSlice { // Actions fetchCliStatus: () => Promise; + invalidateCliStatus: () => Promise; installCli: () => void; } @@ -90,6 +91,10 @@ export const createCliInstallerSlice: StateCreator { + await api.cliInstaller?.invalidateStatus(); + }, + installCli: () => { set({ cliInstallerState: 'checking', diff --git a/src/shared/types/cliInstaller.ts b/src/shared/types/cliInstaller.ts index 91b4bc68..9149ead1 100644 --- a/src/shared/types/cliInstaller.ts +++ b/src/shared/types/cliInstaller.ts @@ -83,6 +83,8 @@ export interface CliInstallerAPI { getStatus: () => Promise; /** Start install/update flow. Progress sent via onProgress events. */ install: () => Promise; + /** Invalidate cached status (forces fresh check on next getStatus) */ + invalidateStatus: () => Promise; /** Subscribe to progress events. Returns cleanup function. */ onProgress: (cb: (event: unknown, data: CliInstallerProgress) => void) => () => void; }