feat(tmux): collapse windows setup steps by default

This commit is contained in:
777genius 2026-04-14 20:20:50 +03:00
parent a9668ff15d
commit dd7b729520
4 changed files with 175 additions and 3 deletions

View file

@ -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:

View file

@ -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', () => {

View file

@ -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 (
<div
className="mb-6 rounded-lg border-l-4 px-4 py-3"
@ -114,6 +132,23 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
Manual guide
</button>
)}
{viewModel.manualHintsCollapsible && (
<button
type="button"
onClick={() => setManualHintsExpanded((current) => !current)}
className="inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors hover:bg-white/5"
style={{ borderColor: 'var(--color-border)' }}
>
{manualHintsExpanded ? (
<ChevronUp className="size-4" />
) : (
<ChevronDown className="size-4" />
)}
{manualHintsExpanded
? 'Hide setup steps'
: `Show setup steps (${viewModel.manualHints.length})`}
</button>
)}
<button
type="button"
onClick={() => void refresh()}
@ -193,7 +228,7 @@ export function TmuxInstallerBannerView(): React.JSX.Element | null {
</div>
)}
{viewModel.manualHints.length > 0 && (
{manualHintsVisible && (
<div className="mt-3 grid gap-2 lg:grid-cols-2">
{viewModel.manualHints.map((hint) => (
<div

View file

@ -0,0 +1,131 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { TmuxInstallerBannerView } from '../TmuxInstallerBannerView';
import type { TmuxInstallerBannerViewModel } from '../../adapters/TmuxInstallerBannerAdapter';
const { mockUseTmuxInstallerBanner } = vi.hoisted(() => ({
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<typeof createRoot>;
} {
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();
});
});
});