diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index 4b35b89f..936075a0 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -7,7 +7,7 @@ * Only rendered in Electron mode. */ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS, @@ -35,13 +35,10 @@ import { } from '@renderer/components/runtime/providerConnectionUi'; import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges'; import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector'; -import { ProviderRuntimeSettingsDialog } from '@renderer/components/runtime/ProviderRuntimeSettingsDialog'; import { getProviderTerminalCommand, getProviderTerminalLogoutCommand, } from '@renderer/components/runtime/providerTerminalCommands'; -import { TerminalLogPanel } from '@renderer/components/terminal/TerminalLogPanel'; -import { TerminalModal } from '@renderer/components/terminal/TerminalModal'; import { useCliInstaller } from '@renderer/hooks/useCliInstaller'; import { loadDashboardCliStatusBannerCollapsed, @@ -113,6 +110,22 @@ const ATLAS_CLOUD_CODING_PLAN_URL = 'https://www.atlascloud.ai/console/coding-pl const ATLAS_CLOUD_DESCRIPTION = "Atlas Cloud is a full-modal AI inference platform that gives developers a single AI API to access video generation, image generation, and LLM APIs. Instead of managing multiple vendor integrations, you connect once and get unified access to 300+ curated models across all modalities. Check out Atlas Cloud's new coding plan promotion for more budget-friendly API access."; +const ProviderRuntimeSettingsDialog = lazy(() => + import('@renderer/components/runtime/ProviderRuntimeSettingsDialog').then((module) => ({ + default: module.ProviderRuntimeSettingsDialog, + })) +); +const TerminalLogPanel = lazy(() => + import('@renderer/components/terminal/TerminalLogPanel').then((module) => ({ + default: module.TerminalLogPanel, + })) +); +const TerminalModal = lazy(() => + import('@renderer/components/terminal/TerminalModal').then((module) => ({ + default: module.TerminalModal, + })) +); + const DashboardRateLimitChips = ({ providerId, items, @@ -1655,50 +1668,58 @@ export const CliStatusBanner = (): React.JSX.Element | null => { const installedAuxiliaryUi = renderCliStatus !== null ? ( <> - provider.providerId === manageProviderId) - ? manageProviderId - : (visibleCliProviders[0]?.providerId ?? 'anthropic') - } - initialRuntimeProviderId={manageRuntimeProviderId} - initialRuntimeProviderAction={manageRuntimeProviderId ? 'connect' : null} - providerStatusLoading={cliProviderStatusLoading} - disabled={isBusy || cliStatusLoading || !renderCliStatus.binaryPath} - codexRuntimeStatus={codexRuntimeStatus} - codexRuntimeStatusLoading={codexRuntimeStatusLoading} - onInstallCodexRuntime={() => installCodexRuntime()} - onSelectBackend={handleProviderBackendChange} - onRefreshProvider={handleProviderRefresh} - onRequestLogin={(providerId) => setProviderTerminal({ providerId, action: 'login' })} - /> + {manageDialogOpen && ( + + provider.providerId === manageProviderId) + ? manageProviderId + : (visibleCliProviders[0]?.providerId ?? 'anthropic') + } + initialRuntimeProviderId={manageRuntimeProviderId} + initialRuntimeProviderAction={manageRuntimeProviderId ? 'connect' : null} + providerStatusLoading={cliProviderStatusLoading} + disabled={isBusy || cliStatusLoading || !renderCliStatus.binaryPath} + codexRuntimeStatus={codexRuntimeStatus} + codexRuntimeStatusLoading={codexRuntimeStatusLoading} + onInstallCodexRuntime={() => installCodexRuntime()} + onSelectBackend={handleProviderBackendChange} + onRefreshProvider={handleProviderRefresh} + onRequestLogin={(providerId) => setProviderTerminal({ providerId, action: 'login' })} + /> + + )} {providerTerminal && renderCliStatus.binaryPath && ( - { - setProviderTerminal(null); - recheckAuthState(); - }} - onExit={() => { - recheckAuthState(); - }} - autoCloseOnSuccessMs={3000} - successMessage={ - providerTerminal.action === 'login' ? 'Authentication updated' : 'Provider logged out' - } - failureMessage={ - providerTerminal.action === 'login' ? 'Authentication failed' : 'Logout failed' - } - /> + + { + setProviderTerminal(null); + recheckAuthState(); + }} + onExit={() => { + recheckAuthState(); + }} + autoCloseOnSuccessMs={3000} + successMessage={ + providerTerminal.action === 'login' + ? 'Authentication updated' + : 'Provider logged out' + } + failureMessage={ + providerTerminal.action === 'login' ? 'Authentication failed' : 'Logout failed' + } + /> + )} ) : null; @@ -1877,7 +1898,9 @@ export const CliStatusBanner = (): React.JSX.Element | null => { Installing {runtimeDisplayName}... - + + + ); } @@ -2250,45 +2273,47 @@ export const CliStatusBanner = (): React.JSX.Element | null => { {installedAuxiliaryUi} {showLoginTerminal && renderCliStatus.binaryPath && ( - { - setShowLoginTerminal(false); - setIsVerifyingAuth(true); - void (async () => { - try { - await invalidateCliStatus(); - if (multimodelEnabled) { - await bootstrapCliStatus({ multimodelEnabled: true }); - } else { - await fetchCliStatus(); + + { + setShowLoginTerminal(false); + setIsVerifyingAuth(true); + void (async () => { + try { + await invalidateCliStatus(); + if (multimodelEnabled) { + await bootstrapCliStatus({ multimodelEnabled: true }); + } else { + await fetchCliStatus(); + } + } finally { + setIsVerifyingAuth(false); } - } finally { - setIsVerifyingAuth(false); - } - })(); - }} - onExit={() => { - setIsVerifyingAuth(true); - void (async () => { - try { - await invalidateCliStatus(); - if (multimodelEnabled) { - await bootstrapCliStatus({ multimodelEnabled: true }); - } else { - await fetchCliStatus(); + })(); + }} + onExit={() => { + setIsVerifyingAuth(true); + void (async () => { + try { + await invalidateCliStatus(); + if (multimodelEnabled) { + await bootstrapCliStatus({ multimodelEnabled: true }); + } else { + await fetchCliStatus(); + } + } finally { + setIsVerifyingAuth(false); } - } finally { - setIsVerifyingAuth(false); - } - })(); - }} - autoCloseOnSuccessMs={4000} - successMessage="Login complete" - failureMessage="Login failed" - /> + })(); + }} + autoCloseOnSuccessMs={4000} + successMessage="Login complete" + failureMessage="Login failed" + /> + )} ); diff --git a/test/renderer/components/cli/CliStatusVisibility.test.ts b/test/renderer/components/cli/CliStatusVisibility.test.ts index 4d516c27..8f96e6e3 100644 --- a/test/renderer/components/cli/CliStatusVisibility.test.ts +++ b/test/renderer/components/cli/CliStatusVisibility.test.ts @@ -173,6 +173,14 @@ vi.mock('@renderer/store', () => { import { CliStatusBanner } from '@renderer/components/dashboard/CliStatusBanner'; import { CliStatusSection } from '@renderer/components/settings/sections/CliStatusSection'; +async function flushLazyImports(): Promise { + await Promise.resolve(); + await Promise.resolve(); + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); +} + function createInstalledCliStatus( overrides?: Partial> ): Record { @@ -450,6 +458,63 @@ describe('CLI status visibility during completed install state', () => { }); }); + it('keeps the dashboard terminal modal unmounted until login is requested', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.cliStatus = createInstalledCliStatus({ + authLoggedIn: false, + }); + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(React.createElement(CliStatusBanner)); + await flushLazyImports(); + }); + + expect(host.querySelector('[data-testid="terminal-modal"]')).toBeNull(); + + const loginButton = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Login' + ); + expect(loginButton).not.toBeUndefined(); + + await act(async () => { + loginButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await flushLazyImports(); + }); + + expect(host.querySelector('[data-testid="terminal-modal"]')).not.toBeNull(); + + await act(async () => { + root.unmount(); + await flushLazyImports(); + }); + }); + + it('loads the installer terminal log only while installation is active', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.cliInstallerState = 'installing'; + storeState.cliInstallerRawChunks = ['installing...\n']; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render(React.createElement(CliStatusBanner)); + await flushLazyImports(); + }); + + expect(host.textContent).toContain('terminal-log'); + + await act(async () => { + root.unmount(); + await flushLazyImports(); + }); + }); + it('shows an OpenCode install action on the dashboard when the OpenCode CLI is missing', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliInstallerState = 'idle'; @@ -1089,6 +1154,15 @@ describe('CLI status visibility during completed install state', () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliInstallerState = 'idle'; storeState.fetchCliProviderStatus = vi.fn(() => Promise.reject(new Error('refresh failed'))); + storeState.cliStatus = createInstalledCliStatus({ + flavor: 'agent_teams_orchestrator', + displayName: 'Multimodel runtime', + supportsSelfUpdate: false, + showVersionDetails: false, + showBinaryPath: false, + authLoggedIn: true, + providers: [createCodexNativeRolloutProvider()], + }); const host = document.createElement('div'); document.body.appendChild(host); @@ -1099,6 +1173,19 @@ describe('CLI status visibility during completed install state', () => { await Promise.resolve(); }); + expect(providerRuntimeSettingsDialogProps).toBeNull(); + + const manageButton = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Manage' + ); + expect(manageButton).not.toBeUndefined(); + + await act(async () => { + manageButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + await Promise.resolve(); + }); + const onSelectBackend = providerRuntimeSettingsDialogProps?.onSelectBackend; expect(onSelectBackend).toBeTypeOf('function');