diff --git a/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts b/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts index 7c5978bb..16cb83d2 100644 --- a/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts +++ b/src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts @@ -26,6 +26,7 @@ export interface TmuxInstallerBannerViewModel { progressPercent: number | null; logs: string[]; manualHints: TmuxInstallHint[]; + manualHintsCollapsible: boolean; primaryGuideUrl: string | null; installSupported: boolean; installDisabled: boolean; @@ -79,6 +80,8 @@ export class TmuxInstallerBannerAdapter { : null; const versionLabel = status?.effective.version ?? status?.host.version ?? status?.wsl?.tmuxVersion ?? null; + const manualHints = status?.autoInstall.manualHints ?? []; + const manualHintsCollapsible = status?.platform === 'win32' && manualHints.length > 0; const installLabel = snapshot.phase === 'idle' && status?.platform === 'win32' && @@ -104,7 +107,8 @@ export class TmuxInstallerBannerAdapter { phase: snapshot.phase, progressPercent: formatTmuxInstallerProgress(snapshot.phase), logs: snapshot.logs, - manualHints: status?.autoInstall.manualHints ?? [], + manualHints, + manualHintsCollapsible, primaryGuideUrl, installSupported: status?.autoInstall.supported ?? false, installDisabled: 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 20b89577..742807b4 100644 --- a/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts +++ b/src/features/tmux-installer/renderer/adapters/__tests__/TmuxInstallerBannerAdapter.test.ts @@ -71,6 +71,7 @@ describe('TmuxInstallerBannerAdapter', () => { expect(result.primaryGuideUrl).toBeNull(); expect(result.progressPercent).toBeNull(); expect(result.manualHints).toHaveLength(1); + expect(result.manualHintsCollapsible).toBe(false); expect(result.body).toContain('persistent teammate reliability'); }); @@ -143,6 +144,7 @@ describe('TmuxInstallerBannerAdapter', () => { expect(result.platformLabel).toBe('Windows'); expect(result.primaryGuideUrl).toBe('https://learn.microsoft.com/en-us/windows/wsl/install'); expect(result.progressPercent).toBe(100); + expect(result.manualHintsCollapsible).toBe(true); }); it('keeps the banner visible when tmux is installed but runtime is not ready yet', () => { diff --git a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx index d9fc7621..9b5f2c62 100644 --- a/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx +++ b/src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx @@ -1,6 +1,14 @@ import React from 'react'; -import { AlertTriangle, ExternalLink, RefreshCw, Wrench, XCircle } from 'lucide-react'; +import { + AlertTriangle, + ChevronDown, + ChevronUp, + ExternalLink, + RefreshCw, + Wrench, + XCircle, +} from 'lucide-react'; import { useTmuxInstallerBanner } from '../hooks/useTmuxInstallerBanner'; @@ -28,6 +36,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { const { viewModel, install, cancel, submitInput, refresh, toggleDetails, openExternal } = useTmuxInstallerBanner(); const [inputValue, setInputValue] = React.useState(''); + const [manualHintsExpanded, setManualHintsExpanded] = React.useState(false); React.useEffect(() => { if (!viewModel.acceptsInput) { @@ -35,10 +44,19 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null { } }, [viewModel.acceptsInput]); + React.useEffect(() => { + if (!viewModel.manualHintsCollapsible) { + setManualHintsExpanded(false); + } + }, [viewModel.manualHintsCollapsible]); + if (!viewModel.visible) { return null; } + const manualHintsVisible = + viewModel.manualHints.length > 0 && (!viewModel.manualHintsCollapsible || manualHintsExpanded); + return (
)} + {viewModel.manualHintsCollapsible && ( + + )}
)} - {viewModel.manualHints.length > 0 && ( + {manualHintsVisible && (
{viewModel.manualHints.map((hint) => (
({ + mockUseTmuxInstallerBanner: vi.fn(), +})); + +vi.mock('../../hooks/useTmuxInstallerBanner', () => ({ + useTmuxInstallerBanner: mockUseTmuxInstallerBanner, +})); + +const baseViewModel: TmuxInstallerBannerViewModel = { + visible: true, + loading: false, + title: 'tmux is not installed', + body: 'WSL is available, but no Linux distribution is installed yet.', + error: null, + platformLabel: 'Windows', + locationLabel: null, + runtimeReadyLabel: null, + versionLabel: null, + phase: 'idle', + progressPercent: null, + logs: [], + manualHints: [ + { + title: 'Install WSL', + description: 'Install Windows Subsystem for Linux.', + command: 'wsl --install --no-distribution', + }, + { + title: 'Install Ubuntu', + description: 'Recommended WSL distro.', + command: 'wsl --install -d Ubuntu --no-launch', + }, + ], + manualHintsCollapsible: true, + primaryGuideUrl: 'https://example.com/guide', + installSupported: true, + installDisabled: false, + installLabel: 'Install Ubuntu in WSL', + canCancel: false, + acceptsInput: false, + inputPrompt: null, + inputSecret: false, + detailsOpen: false, +}; + +function renderBanner(viewModel: TmuxInstallerBannerViewModel): { + host: HTMLDivElement; + root: ReturnType; +} { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + mockUseTmuxInstallerBanner.mockReturnValue({ + viewModel, + install: vi.fn(), + cancel: vi.fn(), + submitInput: vi.fn(), + refresh: vi.fn(), + toggleDetails: vi.fn(), + openExternal: vi.fn(), + }); + + act(() => { + root.render(React.createElement(TmuxInstallerBannerView)); + }); + + return { host, root }; +} + +describe('TmuxInstallerBannerView', () => { + beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + }); + + afterEach(() => { + mockUseTmuxInstallerBanner.mockReset(); + document.body.innerHTML = ''; + }); + + it('keeps Windows setup steps collapsed by default and expands them on demand', async () => { + const { host, root } = renderBanner(baseViewModel); + + expect(host.textContent).toContain('Show setup steps (2)'); + expect(host.textContent).not.toContain('wsl --install --no-distribution'); + + const toggleButton = [...host.querySelectorAll('button')].find((button) => + button.textContent?.includes('Show setup steps') + ); + expect(toggleButton).toBeDefined(); + + await act(async () => { + toggleButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('Hide setup steps'); + expect(host.textContent).toContain('wsl --install --no-distribution'); + expect(host.textContent).toContain('wsl --install -d Ubuntu --no-launch'); + + act(() => { + root.unmount(); + }); + }); + + it('shows setup hints immediately on non-Windows platforms', () => { + const { host, root } = renderBanner({ + ...baseViewModel, + platformLabel: 'macOS', + manualHintsCollapsible: false, + manualHints: [ + { title: 'Homebrew', description: 'Recommended', command: 'brew install tmux' }, + ], + }); + + expect(host.textContent).toContain('brew install tmux'); + expect(host.textContent).not.toContain('Show setup steps'); + + act(() => { + root.unmount(); + }); + }); +});