From ff9344c85a8076641ea25dab309a8758ac0d8fe9 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 14 Apr 2026 22:08:10 +0300 Subject: [PATCH] fix(tmux): polish installer banner state --- .../adapters/TmuxInstallerBannerAdapter.ts | 58 +++++++++++++------ .../TmuxInstallerBannerAdapter.test.ts | 43 ++++++++++++++ .../renderer/ui/TmuxInstallerBannerView.tsx | 31 ++++++---- 3 files changed, 102 insertions(+), 30 deletions(-) diff --git a/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts b/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts index 1dd97aca..5bc1c58d 100644 --- a/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts +++ b/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts @@ -50,6 +50,8 @@ interface AdaptInput { detailsOpen: boolean; } +const RESTART_REQUIRED_PATTERNS = ['restart', 'reboot', 'перезагруз', 'требуется перезагрузка']; + export class TmuxInstallerBannerAdapter { static create(): TmuxInstallerBannerAdapter { return new TmuxInstallerBannerAdapter(); @@ -58,19 +60,20 @@ export class TmuxInstallerBannerAdapter { adapt(input: AdaptInput): TmuxInstallerBannerViewModel { const status = input.status; const snapshot = input.snapshot; + const displayPhase = this.#resolveDisplayPhase(snapshot, status); const hasActiveInstallFlow = - snapshot.phase !== 'idle' && snapshot.phase !== 'completed' && snapshot.phase !== 'cancelled'; + displayPhase !== 'idle' && displayPhase !== 'completed' && displayPhase !== 'cancelled'; const tmuxMissing = status ? !status.effective.available : !input.loading; const visible = - hasActiveInstallFlow || (snapshot.phase !== 'completed' && !input.loading && tmuxMissing); + hasActiveInstallFlow || (displayPhase !== 'completed' && !input.loading && tmuxMissing); const title = snapshot.message && - (snapshot.phase === 'pending_external_elevation' || - snapshot.phase === 'waiting_for_external_step' || - snapshot.phase === 'needs_restart' || - snapshot.phase === 'needs_manual_step') + (displayPhase === 'pending_external_elevation' || + displayPhase === 'waiting_for_external_step' || + displayPhase === 'needs_restart' || + displayPhase === 'needs_manual_step') ? snapshot.message - : formatTmuxInstallerTitle(snapshot.phase); + : formatTmuxInstallerTitle(displayPhase); const primaryGuideUrl = status?.autoInstall.manualHints.find((hint) => typeof hint.url === 'string')?.url ?? null; const body = @@ -95,7 +98,7 @@ export class TmuxInstallerBannerAdapter { const manualHints = status?.autoInstall.manualHints ?? []; const manualHintsCollapsible = status?.platform === 'win32' && manualHints.length > 0; const installLabel = - snapshot.phase === 'idle' && + displayPhase === 'idle' && status?.platform === 'win32' && status.autoInstall.strategy === 'wsl' && status.autoInstall.supported @@ -104,16 +107,16 @@ export class TmuxInstallerBannerAdapter { : !status.wsl?.distroName ? 'Install Ubuntu in WSL' : 'Install tmux in WSL' - : formatInstallButtonLabel(snapshot.phase); + : formatInstallButtonLabel(displayPhase); const installDisabled = input.loading || - snapshot.phase === 'preparing' || - snapshot.phase === 'checking' || - snapshot.phase === 'requesting_privileges' || - snapshot.phase === 'pending_external_elevation' || - snapshot.phase === 'waiting_for_external_step' || - snapshot.phase === 'installing' || - snapshot.phase === 'verifying'; + displayPhase === 'preparing' || + displayPhase === 'checking' || + displayPhase === 'requesting_privileges' || + displayPhase === 'pending_external_elevation' || + displayPhase === 'waiting_for_external_step' || + displayPhase === 'installing' || + displayPhase === 'verifying'; const installButtonPrimary = !installDisabled && (installLabel.startsWith('Install') || installLabel.startsWith('Retry')); const showRefreshButton = @@ -131,8 +134,8 @@ export class TmuxInstallerBannerAdapter { locationLabel: formatTmuxLocationLabel(status?.effective.location ?? null), runtimeReadyLabel, versionLabel, - phase: snapshot.phase, - progressPercent: formatTmuxInstallerProgress(snapshot.phase), + phase: displayPhase, + progressPercent: formatTmuxInstallerProgress(displayPhase), logs: snapshot.logs, manualHints, manualHintsCollapsible, @@ -149,4 +152,23 @@ export class TmuxInstallerBannerAdapter { detailsOpen: input.detailsOpen, }; } + + #resolveDisplayPhase( + snapshot: TmuxInstallerSnapshot, + status: TmuxStatus | null + ): TmuxInstallerSnapshot['phase'] { + if (snapshot.phase !== 'waiting_for_external_step') { + return snapshot.phase; + } + + const combinedSignals = [snapshot.message, snapshot.detail, status?.wsl?.statusDetail, ...snapshot.logs] + .filter(Boolean) + .join('\n') + .toLowerCase(); + const restartRequired = + status?.wsl?.rebootRequired === true || + RESTART_REQUIRED_PATTERNS.some((pattern) => combinedSignals.includes(pattern)); + + return restartRequired ? 'needs_restart' : snapshot.phase; + } } diff --git a/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts b/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts index 2dbb9b7e..1939f8ea 100644 --- a/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts +++ b/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts @@ -350,4 +350,47 @@ describe('TmuxInstallerBannerAdapter', () => { expect(result.installDisabled).toBe(true); expect(result.showRefreshButton).toBe(true); }); + + it('shows a restart state when external-step details already require a reboot', () => { + const adapter = TmuxInstallerBannerAdapter.create(); + + const result = adapter.adapt({ + status: { + ...baseStatus, + platform: 'win32', + autoInstall: { + ...baseStatus.autoInstall, + supported: true, + strategy: 'wsl', + }, + wsl: { + wslInstalled: true, + rebootRequired: false, + distroName: null, + distroVersion: null, + distroBootstrapped: false, + innerPackageManager: null, + tmuxAvailableInsideWsl: false, + tmuxVersion: null, + tmuxBinaryPath: null, + statusDetail: null, + }, + }, + snapshot: { + ...idleSnapshot, + phase: 'waiting_for_external_step', + strategy: 'wsl', + message: 'Checking WSL after the administrator step...', + detail: + 'Требуемая операция выполнена успешно. Чтобы сделанные изменения вступили в силу, следует перезагрузить систему.', + }, + loading: false, + error: null, + detailsOpen: false, + }); + + expect(result.phase).toBe('needs_restart'); + expect(result.progressPercent).toBe(96); + expect(result.installLabel).toBe('Re-check after restart'); + }); }); diff --git a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx index 81469306..e40849f9 100644 --- a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx +++ b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx @@ -79,7 +79,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { return (
setExpanded((current) => !current)} - className="flex min-h-[1.75rem] w-full items-center justify-between gap-3 text-left" + className="group flex w-full items-center justify-between gap-3 rounded-md px-1 py-0.5 text-left transition-colors hover:bg-white/[0.03]" > - - + + {viewModel.error ? ( - + ) : ( - + )} {SUMMARY_TITLE} - {expanded ? ( - - ) : ( - - )} + + {expanded ? ( + + ) : ( + + )} + {expanded && (