# План: Hybrid tmux Installer для Desktop App Дата: 2026-04-14 Рекомендуемый подход: `Hybrid installer` Оценка подхода: 🎯 9 🛡️ 8 🧠 6 Примерный объём первой качественной реализации: - `macOS/Linux installer + UI + status/progress + manual fallback` - `900-1400` строк - `Windows WSL wizard + richer status` - ещё `700-1200` строк - `Windows runtime enablement через WSL tmux` - ещё `600-1200` строк Итого полноценный вариант, где Windows не просто умеет "установить", а реально получает пользу от `tmux` - примерно `2200-3800` строк. ## 1. TL;DR Нужно сделать отдельный feature slice `tmux-installer`, где main-side orchestration layer по UX-паттерну похож на существующий `CliInstallerService`, но не копирует его буквально. Ключевая идея: - `macOS` - автодетект `Homebrew`, fallback на `MacPorts`, иначе manual install - `Linux` - автодетект native package manager, установка через `sudo` в PTY, честный step-based progress - `Windows` - не притворяться native installer'ом, а сделать честный `WSL wizard` + проверку/re-check каждого шага Самый важный architectural gotcha: ⚠️ Сейчас Windows-путь в приложении вообще не использует `tmux`, даже если пользователь сам его поставит в WSL. Это видно прямо в коде: - `src/main/services/team/runtimeTeammateMode.ts` жёстко отключает process/tmux path на `win32` - `src/main/ipc/tmux.ts` проверяет только host `tmux`, а не `wsl ... tmux` Следствие: - сделать только installer на Windows недостаточно - если хотим честно обещать "лучший опыт после установки", нужно добавить хотя бы базовую WSL-aware tmux detection - если хотим реальный runtime gain на Windows, нужно отдельно включить WSL tmux path в runtime-решении teammate mode ## 2. Цели ### Продуктовые цели - Пользователь может установить `tmux` максимально удобно из UI - UI честно показывает, что происходит: `checking`, `installing`, `verifying`, `completed`, `error` - Если установка не удалась, пользователь не остаётся в тупике - Manual fallback всегда есть и всегда OS-specific - Никаких фейковых "100% success", если verification не прошло - Ошибки формулируются человеческим языком, а не сырым stack trace ### Технические цели - Не ломать текущий `TmuxStatusBanner` - Сделать фичу по canonical standard из `docs/FEATURE_ARCHITECTURE_STANDARD.md` - source of truth для новой фичи - `src/features/tmux-installer/` - `src/renderer/components/dashboard/TmuxStatusBanner.tsx` должен стать thin wrapper или compatibility entrypoint, который импортирует только public renderer entrypoint фичи - Переиспользовать существующие примитивы: - `CliInstallerService` как референс по progress/event architecture - `PtyTerminalService` как база для PTY lifecycle, но не полагаться на него "как есть" - `TerminalModal` только как визуальный reference, не как источник истины для installer flow - shell env resolution из существующих infra helpers - Сделать state machine, которую можно тестировать unit/integration тестами - Не делать platform-specific хаос прямо в React-компоненте ### Не-цели v1 - Не собирать `tmux` из source автоматически - Не устанавливать Homebrew автоматически - Не поддерживать экспериментальные native Windows forks `tmux` по умолчанию - Не делать "реальный процент" там, где package manager его не даёт - Не пытаться тихо обходить OS security model ## 3. Внешние факты, на которых строится план - tmux official install wiki перечисляет package manager команды для Linux/macOS и отдельно static binaries только для common Linux/macOS platforms: [tmux Installing wiki](https://github.com/tmux/tmux/wiki/Installing) - Homebrew formula для `tmux` сейчас показывает `brew install tmux`, bottle support для macOS и Linux, stable `3.6a`: [Homebrew tmux](https://formulae.brew.sh/formula/tmux) - MacPorts для `tmux` сейчас рекомендует `sudo port install tmux`, версия `3.6a`: [MacPorts tmux](https://ports.macports.org/port/tmux/) - Microsoft для WSL пишет: открыть PowerShell в administrator mode, выполнить `wsl --install`, затем restart machine: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install) - Microsoft также документирует `wsl --list --online`, `wsl --distribution ... --user ...`, `wsl --status`, `wsl --shutdown`: [Basic commands for WSL](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) - Microsoft отдельно держит manual WSL install path для older Windows builds / server-like scenarios: [Manual installation steps for older versions of WSL](https://learn.microsoft.com/en-us/windows/wsl/install-manual) ## 3.1 Самые рискованные места плана после перепроверки Это секции, где изначально была наименьшая уверенность и которые были усилены после дополнительной проверки. ### ⚠️ Windows installer != Windows runtime support Самый критичный момент: - `WSL wizard` сам по себе ещё не даёт реальной выгоды рантайму - пока `runtimeTeammateMode` и `tmux status` не станут `WSL-aware`, Windows-часть будет только подготавливать окружение Именно поэтому в этом плане Windows runtime enablement выделен как обязательный follow-up, а не "nice to have". ### ⚠️ Не надо по умолчанию ставить tmux внутри WSL как `root` Изначальная идея с: ```powershell wsl --distribution Ubuntu --user root -- sh -lc "apt-get install -y tmux" ``` слишком оптимистична для v1. Почему это риск: - distro может быть ещё не bootstrap-ready - поведение imported/custom distros менее предсказуемо - мы можем получить "сработало технически, но пользователь не понимает, что произошло" Более надёжное решение: - сначала довести distro до явного `bootstrapped` состояния - затем запускать install flow через PTY внутри `wsl.exe` - по умолчанию ставить как обычный Linux-user через `sudo`, а не скрыто через root - `--user root` оставить только как optional future optimization, не как default v1 path ### ⚠️ Linux immutable distros надо считать unsupported для v1 auto-install Если это: - `rpm-ostree` - Silverblue / Kinoite - transactional-update / MicroOS то "обычный package manager install в хост" либо не тот путь, либо вообще misleading UX. Надёжнее: - детектить immutable-host признаки - сразу переводить пользователя в `manual-only guidance` - не делать вид, что обычный `dnf install tmux` или `zypper install tmux` там надёжен ### ⚠️ Не надо делать `apt-get update` всегда перед install Изначальный пример с: ```bash apt-get update && apt-get install -y tmux ``` практически рабочий, но хуже как default: - медленнее - больше сетевых точек отказа - лишний шум в логах Надёжнее: - сначала пробовать `install` - если ошибка похожа на stale metadata / package not found due to old index, делать controlled retry с `update` ### ⚠️ Windows elevation надо проектировать как отдельный тип шага, а не как "ещё один child process" Это место долго оставалось самым неформализованным. Проблема: - WSL core install часто требует elevation - типичный способ вызвать UAC это PowerShell `Start-Process -Verb RunAs` - по официальному синтаксису `Start-Process` режим с `-Verb` относится к `Use Shell Execute`, а redirect-параметры живут в другом parameter set, поэтому надёжного "RunAs + live redirected stdout/stderr в тот же процесс" по умолчанию ожидать нельзя. Это мой вывод из PowerShell docs: [Start-Process](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/start-process?view=powershell-7.6) Следствие: - elevated Windows step нельзя моделировать как обычный `spawn child and stream logs` - это должен быть отдельный class of step в installer state machine Надёжнее: - либо делать `external elevated step + fresh probe afterwards` - либо, если очень нужны diagnostics, запускать временный elevated helper script, который пишет итоговый JSON/status file в temp location, а app его потом читает - но даже в варианте с status file финальное решение всё равно принимать только после fresh system probe ### ⚠️ Нельзя строить Windows path вокруг "default distro" как единственного источника истины Это ещё один недооценённый риск. Проблема: - пользователь может установить `tmux` в `Ubuntu` - потом default distro поменяется на `Debian` или custom distro - если приложение смотрит только на default distro, UX станет "tmux снова пропал", хотя на самом деле он установлен в целевом окружении Надёжнее: - default distro использовать только как initial heuristic - после явного wizard choice или успешного install persist'ить `preferred WSL distro` - дальше status/runtime сначала пробуют persisted distro, а уже потом fallback на default ### ⚠️ Windows WSL runtime должен явно выбрать binary model для `claude` Это один из самых недооценённых технических рисков. Проблема: - текущий `ClaudeBinaryResolver` на Windows может вернуть разные executable shapes: `.exe`, `.cmd`, `.bat`, `.com` - runtime через WSL tmux не может бездумно считать, что любой найденный host binary одинаково пригоден - `.cmd` / shell-wrapper path особенно рискованны из-за quoting, shell fallback и cleanup semantics Дополнительный факт из Microsoft docs: - Windows tools from WSL must include executable extension - batch scripts are not direct executables there and need `cmd.exe /C ...` Источник: [Working across Windows and Linux file systems](https://learn.microsoft.com/en-us/windows/wsl/filesystems) Практический вывод для v1 Windows runtime follow-up: - если идём через WSL tmux + Windows interop, prefer native Windows `.exe` binary - не считать `.cmd`/`.bat` автоматически runtime-ready без отдельного validation path - Linux binary inside WSL - это отдельная модель с другими config/auth consequences, её нельзя смешивать с host-binary path без явного решения ## 3.2 Дополнительные design constraints после IOF - Не использовать `pkexec` в v1 как основной Linux privilege path - слишком много variability по desktop environment, polkit agent, headless режимам - для нашего desktop app более предсказуемый путь это `PTY + sudo` - Не строить installer на существующем `PtyTerminalService`/`TerminalModal` без доработки - текущий `PtyTerminalService` стримит output только в renderer и не даёт main-side control surface - текущий `TerminalModal` сам спавнит процесс и потому не подходит как source of truth для service-owned installer state - Не показывать raw terminal output без ограничения размера - нужен ring buffer - нужен redaction policy для чувствительных строк - Не запускать одновременно `auto-install` и `manual terminal` для одной и той же установки - Не собирать install команды строковой конкатенацией - в плане должны фигурировать `command + args + env + requiresPty` - shell string допустим только для заранее заданного inner `sh -lc`, собранного из наших шаблонов, а не из пользовательского ввода - Для Windows WSL core install не обещать live stdout/stderr как обязательную возможность - elevated child process может открываться во внешнем окне/UAC flow - progress для этого шага должен быть step-based, а не "мы всегда покажем все логи" - Для Windows elevated steps нужен отдельный execution contract - `pending_external_elevation` - `waiting_for_external_step` - `external_step_finished` - `external_step_failed` - это не то же самое, что PTY-backed install на Linux/WSL userland - Не требовать WSL 2 там, где `tmux` уже реально работает в WSL 1 - для продукта важнее usable tmux path, чем конкретная WSL version label - Не оставлять в `TmuxStatus` двусмысленное поле `available` - после добавления WSL-aware path нужен явный раздел `host` / `wsl` / `effective` - Все platform-specific решения должны жить в main-process service layer, не в React ## 4. Текущее состояние кодовой базы ### Feature-стандарт, который надо учитывать - authoritative document для этой задачи - `docs/FEATURE_ARCHITECTURE_STANDARD.md` - он задаёт canonical template для medium/large feature: - `src/features//contracts` - `src/features//core/domain` - `src/features//core/application` - `src/features//main/composition` - `src/features//main/adapters/input` - `src/features//main/adapters/output` - `src/features//main/infrastructure` - `src/features//preload` - `src/features//renderer` - эта задача точно подпадает под full slice, потому что: - пересекает больше одной process boundary - вводит свой transport bridge - вводит свой use case и policy logic - имеет main/preload/renderer orchestration - `src/renderer/features/CLAUDE.md` можно использовать только как локальную подсказку для внутренних renderer-паттернов, но не как главный стандарт этой фичи - structural reference implementation для этой фичи - `src/features/recent-projects` - public entrypoints - composition-root wiring - preload bridge pattern - renderer dumb UI + hook orchestration ### Что уже есть - `src/main/ipc/tmux.ts` - простой `getStatus()` - cache TTL - probe через `tmux -V` - `src/shared/types/tmux.ts` - минимальный `TmuxStatus` - `src/renderer/components/dashboard/TmuxStatusBanner.tsx` - баннер на дашборде - OS-specific manual commands - `src/main/services/infrastructure/CliInstallerService.ts` - хороший референс по installer progress events - `setMainWindow()` - `sendProgress()` - `checking/downloading/verifying/installing/completed/error` - `src/main/services/infrastructure/PtyTerminalService.ts` - PTY для interactive terminal workflows - `src/renderer/components/terminal/TerminalModal.tsx` - уже есть UI для живого терминала и status footer - `src/main/ipc/config.ts` - уже есть полезные WSL helpers: - candidate resolution для `wsl.exe` - UTF-16-aware decode WSL output - `src/main/utils/pathDecoder.ts` - уже есть path translation utility для WSL mount paths ### Где уже есть ограничения - `src/main/services/team/runtimeTeammateMode.ts` - на `win32` сейчас всегда возвращает `forceProcessTeammates: false` - `src/main/services/team/TeamProvisioningService.ts` - Windows-пропуски по `ps`-based live process detection - `kill-pane` работает только через host `tmux` - `src/main/services/infrastructure/PtyTerminalService.ts` - сейчас умеет только `spawn/write/resize/kill` - data/exit события уходят напрямую в renderer, но не в installer service - сам `node-pty` там optional native addon, так что installer не должен предполагать его наличие без capability check - `src/renderer/components/terminal/TerminalModal.tsx` - это self-managed terminal UI, который сам запускает PTY - attach к уже идущему installer session сейчас не поддерживается ### Что можно переиспользовать из `agent_teams_orchestrator` - package manager resolver: - `src/utils/nativeInstaller/packageManagers.ts` - WSL-aware tmux execution: - `src/utils/tmuxSocket.ts` Это не код "скопировать как есть", а скорее reference implementation и source of ideas. ## 5. Главный продуктовый вывод ### Что можно обещать пользователю честно #### macOS / Linux Можно обещать: - установка из UI - понятный progress/status - проверка результата - fallback на manual install #### Windows Можно обещать: - удобный guided WSL setup - честный status по каждому шагу - понятный "что делать дальше" Нельзя честно обещать: - silent one-click install без admin/reboot/setup user - что это сработает на каждом корпоративном ноуте без вмешательства IT ## 6. Рекомендуемая архитектура ### 6.1 Новые сущности внутри feature slice Добавить: - `src/features/tmux-installer/core/application/use-cases/GetTmuxStatusUseCase.ts` - `src/features/tmux-installer/core/application/use-cases/InstallTmuxUseCase.ts` - `src/features/tmux-installer/core/application/use-cases/CancelTmuxInstallUseCase.ts` - `src/features/tmux-installer/core/application/use-cases/GetTmuxInstallerSnapshotUseCase.ts` - `src/features/tmux-installer/core/application/ports/TmuxStatusSourcePort.ts` - `src/features/tmux-installer/core/application/ports/TmuxInstallerRunnerPort.ts` - `src/features/tmux-installer/core/application/ports/TmuxInstallerSnapshotPort.ts` - `src/features/tmux-installer/main/composition/createTmuxInstallerFeature.ts` - `src/features/tmux-installer/main/adapters/input/ipc/registerTmuxInstallerIpc.ts` - `src/features/tmux-installer/main/adapters/output/presenters/TmuxInstallerProgressPresenter.ts` - `src/features/tmux-installer/main/adapters/output/sources/TmuxStatusSourceAdapter.ts` - `src/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.ts` - `src/features/tmux-installer/main/infrastructure/installer/TmuxInstallStrategyResolver.ts` - `src/features/tmux-installer/main/infrastructure/installer/TmuxCommandRunner.ts` - `src/features/tmux-installer/main/infrastructure/platform/TmuxPlatformResolver.ts` - `src/features/tmux-installer/main/infrastructure/platform/TmuxPackageManagerResolver.ts` - `src/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.ts` - `src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts` - `src/features/tmux-installer/main/infrastructure/wsl/TmuxWslPathBridge.ts` - `src/features/tmux-installer/main/infrastructure/wsl/TmuxWslPreferenceStore.ts` ### 6.2 Canonical feature slice по стандарту Для `tmux-installer` нужен full feature slice, а не renderer-local feature. Рекомендуемая структура: ```text src/features/tmux-installer/ contracts/ api.ts channels.ts dto.ts index.ts core/ domain/ models/ policies/ application/ models/ ports/ use-cases/ main/ index.ts composition/ createTmuxInstallerFeature.ts adapters/ input/ ipc/ registerTmuxInstallerIpc.ts output/ presenters/ sources/ runtime/ infrastructure/ installer/ platform/ wsl/ preload/ createTmuxInstallerBridge.ts index.ts renderer/ index.ts adapters/ hooks/ ui/ utils/ ``` Почему именно full slice: - feature пересекает `main -> preload -> renderer` - у feature есть собственные transport contracts - у feature есть собственная orchestration logic и policy layer - у feature есть platform-specific runtime/infrastructure детали, которые нельзя размазывать по app shell ### 6.2.1 Slice responsibilities `contracts/` - DTO - API fragment types - IPC channel constants - без store access, Electron-specific wiring и business orchestration `core/domain/` - чистые business rules - capability classification - error normalization policy - manual hint selection rules - completion / retry / fallback invariants - без Electron, child_process, PTY, package manager calls `core/application/` - use cases - source/output/cache ports - response models - orchestration contracts - без Electron, React, Zustand, child process modules `main/composition/` - composition root feature - wiring use cases, adapters, infrastructure - небольшой facade для app shell registration `main/adapters/input/` - IPC handlers - перевод transport input в use case calls `main/adapters/output/` - presenters - adapters для source/cache/runtime ports - тонкий слой между infra и core/application `main/infrastructure/` - `PtyTerminalService` integration - OS/package manager specifics - WSL detection - elevated step runner - binary probing - path bridge `preload/` - thin bridge - зависит от `contracts/` - без composition logic `renderer/` - dumb UI - hooks orchestration - adapters DTO -> view model - небольшие pure utils ### 6.2.2 Renderer sub-structure внутри canonical slice Внутри `src/features/tmux-installer/renderer/`: ```text renderer/ index.ts adapters/ TmuxInstallerBannerAdapter.ts hooks/ useTmuxInstallerBanner.ts ui/ TmuxInstallerBannerView.tsx utils/ formatTmuxInstallerText.ts ``` Правила: - `renderer/ui` не импортирует `@renderer/api`, `@renderer/store`, `@main/*`, Electron APIs - hooks не ходят в `window.electronAPI` напрямую - transport usage идёт через shared renderer API abstraction - `TmuxStatusBanner.tsx` остаётся compatibility wrapper и импортирует только `@features/tmux-installer/renderer` ### 6.2.3 Жёсткие правила соответствия standard doc Обязательные правила: - app shell и другие фичи импортируют только: - `@features/tmux-installer/contracts` - `@features/tmux-installer/main` - `@features/tmux-installer/preload` - `@features/tmux-installer/renderer` - не делать deep-import feature internals снаружи - `core/domain` side-effect free - `core/application` не знает про Electron/React/Zustand/child processes - `renderer/ui` dumb and presentational - transport bridge тонкий и живёт в `preload/` - architecture ориентируется на browser/Tauri-friendly direction Public entrypoints: ```ts // src/features/tmux-installer/contracts/index.ts export * from './api'; export * from './channels'; export * from './dto'; // src/features/tmux-installer/main/index.ts export { createTmuxInstallerFeature } from './composition/createTmuxInstallerFeature'; export { registerTmuxInstallerIpc } from './adapters/input/ipc/registerTmuxInstallerIpc'; // src/features/tmux-installer/renderer/index.ts export { TmuxInstallerBannerView } from './ui/TmuxInstallerBannerView'; // src/features/tmux-installer/preload/index.ts export { createTmuxInstallerBridge } from './createTmuxInstallerBridge'; ``` Чего не делать: ```ts // не импортировать снаружи import { TmuxInstallerBannerAdapter } from '@features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter'; import { InstallTmuxUseCase } from '@features/tmux-installer/core/application/use-cases/InstallTmuxUseCase'; ``` App-shell shim rules: - если трогаем `src/main/ipc/tmux.ts`, он должен остаться только registration/compatibility shim - если трогаем `src/preload/index.ts`, он должен только подключать `@features/tmux-installer/preload` - никакой business logic не должна жить в этих shared shell файлах ### 6.2.4 Renderer error model по standard Внутри feature нужен typed error layer, а не просто `string | null` everywhere. Рекомендуемо: ```ts export class TmuxInstallerFeatureError extends Error { constructor( readonly code: | 'ADAPTER_ERROR' | 'SNAPSHOT_ERROR' | 'PROGRESS_STREAM_ERROR' | 'INVALID_STATUS' | 'INVALID_PROGRESS', message: string, readonly cause?: unknown ) { super(message); this.name = 'TmuxInstallerFeatureError'; } } ``` Где: - adapter ловит IPC/external shape проблемы и заворачивает их в typed error - domain service мапит их в user-facing banner state - UI не работает напрямую с exception shapes ### 6.3 IPC и contracts Расширить `tmux` API: ```ts export interface TmuxAPI { getStatus: () => Promise; install: () => Promise; invalidateStatus: () => Promise; cancelInstall: () => Promise; onProgress: (cb: (event: unknown, data: TmuxInstallerProgress) => void) => () => void; } ``` Новые IPC channels: ```ts export const TMUX_GET_STATUS = 'tmux:getStatus'; export const TMUX_INSTALL = 'tmux:install'; export const TMUX_INVALIDATE_STATUS = 'tmux:invalidateStatus'; export const TMUX_CANCEL_INSTALL = 'tmux:cancelInstall'; export const TMUX_INSTALLER_PROGRESS = 'tmux:progress'; ``` ⚠️ Для feature-based renderer architecture этого недостаточно. Нужен ещё snapshot endpoint, чтобы feature мог восстановиться после remount/reload и не зависеть от того, был ли progress event пойман вживую. Добавить: ```ts export const TMUX_GET_INSTALLER_SNAPSHOT = 'tmux:getInstallerSnapshot'; export interface TmuxAPI { getInstallerSnapshot: () => Promise; } ``` Где: - main process держит актуальный installer state как source of truth - renderer slice при mount сначала делает `getStatus()` + `getInstallerSnapshot()` - затем подписывается на live progress Это должно жить в `src/features/tmux-installer/contracts/`, а не в случайных shared renderer types. ### 6.4 Shared types Рекомендуемое расширение feature contracts/dto в `src/features/tmux-installer/contracts/`: ```ts export type TmuxInstallStrategy = | 'homebrew' | 'macports' | 'apt' | 'dnf' | 'yum' | 'zypper' | 'pacman' | 'wsl' | 'manual' | 'unknown'; export type TmuxInstallerPhase = | 'idle' | 'checking' | 'preparing' | 'requesting_privileges' | 'pending_external_elevation' | 'waiting_for_external_step' | 'installing' | 'verifying' | 'needs_restart' | 'needs_manual_step' | 'completed' | 'error' | 'cancelled'; export interface TmuxInstallHint { title: string; description: string; command?: string; url?: string; } export interface TmuxAutoInstallCapability { supported: boolean; strategy: TmuxInstallStrategy; packageManagerLabel?: string | null; requiresTerminalInput: boolean; requiresAdmin: boolean; requiresRestart: boolean; mayOpenExternalWindow?: boolean; reasonIfUnsupported?: string | null; manualHints: TmuxInstallHint[]; } export interface TmuxWslStatus { wslInstalled: boolean; rebootRequired: boolean; distroName: string | null; distroVersion: 1 | 2 | null; distroBootstrapped: boolean; innerPackageManager: TmuxInstallStrategy | null; tmuxAvailableInsideWsl: boolean; tmuxVersion: string | null; tmuxBinaryPath?: string | null; statusDetail?: string | null; } export interface TmuxWslPreference { preferredDistroName: string | null; source: 'persisted' | 'default' | 'manual' | null; } export interface TmuxBinaryProbe { available: boolean; version: string | null; binaryPath: string | null; error: string | null; } export interface TmuxEffectiveAvailability { available: boolean; location: 'host' | 'wsl' | null; version: string | null; binaryPath: string | null; runtimeReady: boolean; detail?: string | null; } export interface TmuxStatus { platform: TmuxPlatform; nativeSupported: boolean; checkedAt: string; host: TmuxBinaryProbe; effective: TmuxEffectiveAvailability; error: string | null; autoInstall: TmuxAutoInstallCapability; wsl?: TmuxWslStatus | null; wslPreference?: TmuxWslPreference | null; } export interface TmuxInstallerProgress { type: TmuxInstallerPhase | 'status'; detail?: string; percent?: number; rawChunk?: string; error?: string; status?: TmuxStatus; nextManualHint?: TmuxInstallHint | null; externalStepLabel?: string; canRetryNow?: boolean; } ``` Смысл полей: - `host` - что доступно нативно в текущей ОС host - `wsl` - что доступно внутри WSL на Windows - `effective` - какой path приложение реально может использовать - `effective.runtimeReady` важнее простого "binary found", потому что именно он отвечает на вопрос "persistent teammate path действительно можно включать" - `wslPreference` - какой distro приложение считает целевым для tmux path и почему ## 7. UX и state machine ### 7.1 Принципы UX - Не писать "сломано", если app просто работает без `tmux` - Не скрывать platform-specific ограничения - Не показывать фейковый `%`, если это невозможный процент - Всегда давать следующий шаг - Ошибка должна отвечать на 3 вопроса: - что не получилось - почему это могло случиться - что делать дальше ### 7.2 Основные UI состояния - `Not installed, can auto-install` - CTA: `Install tmux` - `Installing` - шаги - status detail - optional raw logs - если `mayOpenExternalWindow === true`, явно предупреждать про отдельное admin/system window - `Waiting for external admin step` - не показывать пустой терминал - показывать понятный текст, что Windows мог открыть отдельное elevated окно - CTA: `Re-check` - `Needs manual step` - CTA: `Open guide` - CTA: `Retry` - `Needs restart` - CTA: `Re-check after restart` - `Completed` - CTA: `Re-check` - `Error` - human-readable error - retry - manual fallback ### 7.3 Что показывать вместо fake percent Для package managers использовать не byte progress, а step progress: ```ts const STEP_WEIGHTS = { checking: 10, preparing: 20, requesting_privileges: 30, installing: 70, verifying: 90, completed: 100, }; ``` То есть progress bar остаётся, но он отражает phase progression, а не сеть. Это честно и UX-wise полезно. ## 8. Платформенная матрица ## 8.1 macOS ### Стратегия Порядок: 1. Если `tmux` уже есть - успех, install не нужен 2. Если найден `brew` - использовать Homebrew 3. Иначе если найден `port` - использовать MacPorts 4. Иначе manual fallback ### Почему так - `brew install tmux` - лучший UX путь для macOS - MacPorts useful fallback, но более niche - автоматическая установка Homebrew сама по себе слишком тяжёлая и рискованная для v1 ### Команды #### Homebrew ```bash brew install tmux ``` #### MacPorts ```bash sudo port install tmux ``` ### Реализация - использовать shell-aware PATH resolution - проверять `brew` не только в текущем `PATH`, но и в common prefixes: - `/opt/homebrew/bin/brew` - `/usr/local/bin/brew` - `brew install tmux` можно запускать как обычный child process со streaming stdout/stderr - не вводить `HOMEBREW_NO_AUTO_UPDATE=1` как default optimisation без отдельной валидации - это может ускорить UX, но также меняет expected brew behavior - если захотим это делать, лучше отдельным tiny spike и только с fallback probe после install - `port install tmux` запускать через PTY, потому что возможен пароль - если `brew` install вдруг начинает требовать interactive input или ведёт себя нестабильно через pipes, разрешён fallback на PTY и для `brew` ### Edge cases - GUI app стартанул не из shell, `brew` не в PATH - на Intel/macOS старый prefix - `brew` есть, но formula tap сломан - bottle download не удался, formula уходит в source build - `xcode-select` / CLT не установлены - MacPorts есть, но пользователь отменил `sudo` - и `brew`, и `port` есть одновременно - `brew` установлен, но prefix permissions повреждены - приложение запущено без полного login-shell PATH, но `brew` реально установлен ### Решение по приоритету Если есть и `brew`, и `port`: - по умолчанию брать `brew` - в detail UI можно написать `Using Homebrew (preferred)` ## 8.2 Linux ### Стратегия Порядок: 1. Если `tmux` уже есть - успех 2. Разобрать `/etc/os-release` 3. Определить distro family 4. Подтвердить наличие package manager binary 5. Сформировать install command 6. Выполнить через PTY ### Почему PTY, а не `execFile` - `sudo` часто требует TTY - пользователю может понадобиться ввести пароль - package manager output полезен как live log - но PTY должен быть owned main-process installer service, а не renderer modal ### Поддерживаемые менеджеры v1 - Debian / Ubuntu -> `apt-get` - Fedora / RHEL -> `dnf` - older RHEL / CentOS -> `yum` - openSUSE -> `zypper` - Arch -> `pacman` ### Явно unsupported для auto-install v1 - immutable rpm-ostree hosts - transactional-update based hosts - нестандартные embedded/container systems без нормального package DB Для них: - auto-install capability = `supported: false` - только manual guidance - reason text должен быть явным, не generic ### Команды #### Debian / Ubuntu ```bash sudo apt-get install -y tmux ``` #### Fedora / RHEL ```bash sudo dnf install -y tmux ``` #### old RHEL / CentOS ```bash sudo yum install -y tmux ``` #### openSUSE ```bash sudo zypper --non-interactive install tmux ``` #### Arch ```bash sudo pacman -S --needed --noconfirm tmux ``` ### Почему именно такие флаги - `apt-get -y` - Debian manpage: `-y, --yes, --assume-yes` делает install non-interactive и abort'ит на unsafe conditions вроде unauthenticated packages: [apt-get(8)](https://manpages.debian.org/bookworm/apt/apt-get.8.en.html) - `dnf -y` - DNF command reference: `-y, --assumeyes` автоматически отвечает yes на вопросы: [DNF Command Reference](https://dnf.readthedocs.io/en/latest/command_ref.html) - `zypper --non-interactive install` - SUSE docs явно рекомендуют `--non-interactive` до команды `install` для scripted usage: [SUSE zypper docs](https://documentation.suse.com/sles/15-SP5/html/SLES-all/cha-sw-cl.html) - `pacman --noconfirm` - Arch manual: bypasses confirmation prompts и предназначен для scripted usage; `--needed` не переустанавливает уже актуальный target: [pacman(8)](https://man.archlinux.org/man/pacman.8.en) ### Почему `apt-get`, а не `apt` Официальная tmux wiki в user-facing тексте показывает `apt install tmux`, но для scripted/background workflow стабильнее использовать `apt-get`. Это design inference, а не quote from source. ### Почему не `pkexec` Хотя `pkexec` теоретически даёт более desktop-native privilege prompt, в v1 он хуже по надёжности: - зависит от polkit integration в системе - отличается по поведению между DE/WM - в некоторых средах отсутствует или ведёт себя нестабильно Поэтому default path: - `PTY + sudo` ### Почему не делаем `apt-get update` always-on Лучший default flow: ```bash sudo apt-get install -y tmux ``` Fallback retry only if needed: ```bash sudo apt-get update && sudo apt-get install -y tmux ``` Причина: - меньше network surface area - быстрее happy-path - проще читать логи ### Linux edge cases - пользователь уже root -> не добавлять `sudo` - `sudo` не установлен - пользователь не в sudoers - package manager lock: - `apt` lock - `pacman` lock - repository metadata stale - offline network - unsupported distro family - host/container environment без нормального package database - `tmux` установился, но verification через `tmux -V` всё равно не проходит из-за PATH/env - immutable distro, где host package install не должен быть auto path - `sudo` требует TTY password prompt, но пользователь закрывает modal - `pacman` keyring / mirror init issues - `dnf` metadata or repo config corruption - `zypper` registration/repo state issues на SUSE - `yum` / enterprise repo family, где `tmux` отсутствует в подключённых repo - `zypper` interactive repo/license conditions, которые не должны auto-accept'иться молча ### Ошибки, которые нужно распознавать отдельно - permission denied / authentication failure - package manager locked - package not found - network / mirror resolution failed - cancelled by user - immutable host unsupported - package manager present, but repository configuration invalid - repository missing required channel / EPEL-like repo not enabled ## 8.3 Windows ### Главная продуктовая позиция Windows в v1 не должен притворяться нативной `tmux` платформой. Путь: - `WSL wizard` - затем установка `tmux` внутри WSL - затем re-check ### Важный architectural caveat ⚠️ Пока runtime Windows не станет WSL-aware, этот wizard даст только "prepared environment", но не реальный runtime win. Поэтому план на Windows нужно делить на два слоя: #### Layer A Installer / wizard / detection / guidance #### Layer B Runtime enablement: - WSL-aware `tmux` detection - WSL-aware teammate mode decision - WSL-aware pane/session ops ### Рекомендуемый flow #### Шаг 1. Проверка WSL core и distro state Пробуем: ```powershell wsl --status ``` Если WSL не установлен: - показываем CTA `Install WSL` - detail: нужен admin и, возможно, restart Если `wsl --status` itself unsupported on older build: - не классифицировать это как обычный "WSL missing" - это отдельный capability signal - дальше либо fallback probe через `wsl --list --verbose`, либо сразу manual Microsoft guidance для older Windows path Если WSL установлен, но дистрибутива ещё нет: - это отдельное состояние, не путать с `WSL missing` - дальше нужен install конкретного distro, а не повторная "общая" диагностика Дополнительное правило: - не блокировать сценарий только потому, что distro на WSL 1 - если inner `tmux` запускается и будущий runtime smoke test проходит, это usable path #### Шаг 2. Установка WSL Если WSL отсутствует полностью, рекомендуемый путь: ```powershell wsl --install --no-distribution ``` Если хотим объединённый flow "WSL + сразу Ubuntu": ```powershell wsl --install -d Ubuntu --no-launch ``` Если install hangs или Store path problematic, дать fallback hint: ```powershell wsl --install --web-download -d Ubuntu ``` Флаги тут выбраны не случайно: - `--no-launch` documented in Microsoft basic commands as "install distro but do not launch it automatically": [Basic commands for WSL](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) - `--web-download` documented in Microsoft install guide как fallback, если install hangs at `0.0%`: [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install) - `--no-distribution` documented in Microsoft basic commands for the case where WSL itself is not installed and you want to skip distro install during the core setup: [Basic commands for WSL](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) ⚠️ Самое важное ограничение этого шага: - `wsl --install` обычно требует elevation - из обычного Electron process нельзя обещать, что мы всегда тихо запустим elevated child и будем стримить его live output назад в UI Поэтому для v1 надёжный UX такой: - step-based progress внутри баннера - явный текст `An administrator window may open` - если нужен внешний elevated PowerShell, это нормально - после завершения шага всегда делать fresh re-check, а не верить только exit code внешнего окна Почему это лучше для нашего плана: - у нас уже есть отдельные UX steps `Install WSL` и `Install Ubuntu` - `--no-distribution` лучше совпадает с этой state machine - меньше путаницы, когда WSL core установился, а distro ещё нет Предпочтительный execution model для этого шага: 1. app создаёт temp marker directory 2. app запускает elevated helper через PowerShell `Start-Process -Verb RunAs -Wait` 3. helper выполняет `wsl --install ...` 4. helper пишет короткий JSON result file в temp directory 5. app читает result file, но всё равно делает fresh probe через `wsl --status` / `wsl --list` Это лучше, чем надеяться на redirected stdout/stderr, который в `RunAs` flow ненадёжен. Implementation note: - helper лучше запускать как temp `.ps1` file, а не как огромную inline command string - так меньше quoting bugs и проще сохранять diagnostics/result file ### Почему `--no-launch` Это даёт нам больше контроля над wizard flow: - сначала установить - потом отдельно проверить reboot requirement - потом отдельно вести пользователя в first-launch/bootstrap ### Шаг 3. Restart detection После WSL install: - если `wsl --status` / `wsl -l -v` всё ещё не готово - или Windows сообщает, что optional components activated but reboot required UI state: - `needs_restart` - кнопка `Re-check after restart` Отдельный edge: - для older Windows builds, где `wsl --install` вообще не поддерживается, сразу переводить в manual guidance на official Microsoft manual install page, а не пытаться эмулировать unsupported flow: [Manual installation steps for older versions of WSL](https://learn.microsoft.com/en-us/windows/wsl/install-manual) ### Шаг 3.5. Установка Linux distro, если WSL core уже есть Если `wsl --status` успешен, но список дистрибутивов пуст: - это не повод повторять full WSL core install - дальше нужен именно install distro Рекомендуемый путь: ```powershell wsl --list --online wsl --install -d Ubuntu --no-launch ``` Если online catalog недоступен или Store path заблокирован: - сразу дать manual Microsoft guidance - не обещать, что приложение само обойдёт Store/corporate restrictions Продуктовое правило: - `Install WSL` и `Install Ubuntu` должны быть разными step labels в UI - это уменьшает путаницу при поддержке и в error analytics Design rule: - `Install Ubuntu` тоже не надо моделировать как гарантированно одинаковый in-app step на всех Windows машинах - где-то он пройдёт как normal command path, где-то уйдёт в Store / external flow, где-то упрётся в policy - поэтому и этот шаг должен завершаться только fresh probe'ом по факту появившегося distro, а не optimistic success UI ### Шаг 4. Distro bootstrap Даже когда WSL установлен, пользователь может ещё не пройти first-launch distro setup. Признаки: - distro установлена, но inner command падает - first launch требует initial decompression или user creation Решение: - отдельный шаг `Complete Linux distro setup` - открываем пользователю команду: ```powershell wsl -d ``` После этого пользователь: - ждёт распаковку - создаёт Linux username/password Затем возвращается в app и нажимает `Re-check`. ### Почему bootstrap должен быть отдельным шагом, а не скрытой автоматикой Это более надёжно и честно: - пользователь видит создание Linux account/password - меньше магии - меньше platform-specific assumptions про state дистрибутива - проще поддержка и диагностика ### Шаг 4.1 Distro selection policy Если default distro уже есть: - используем её Если distro нет: - предлагаем `Ubuntu` как recommended default - но в коде не хардкодим будущие команды на имя `Ubuntu` - реальное имя выбранного/установленного дистрибутива всегда берём из `wsl --list --verbose` / `wsl --list --quiet` Лучший practical rule: - для списка имён дистрибутивов prefer `wsl --list --quiet` - `--list --verbose` использовать в основном для diagnostics и best-effort `distroVersion` - это снижает зависимость от локализованных header/state строк в output. Microsoft docs отдельно показывают `--quiet` как режим "show only distribution names": [Basic commands for WSL](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) - если нужно понять именно default distro, лучше опираться на stable markers вроде `*` prefix / persisted preference, а не на локализованные текстовые суффиксы Если их несколько: - в v1 можно брать default distro - если default не определён, нужна явная UI selection или forced recommendation Нельзя молча выбирать произвольную distro, если их несколько и default неочевиден. Если default distro есть, но его family unsupported для нашего auto-install v1: - не пытаться "угадать" команды - либо даём manual guidance для этого дистрибутива - либо предлагаем отдельно установить supported distro, например Ubuntu - не меняем default distro молча Важное уточнение после IOF: - default distro нужен только как first guess - после user choice или успешной установки приложение должно persist'ить `preferred distro` - дальнейшие `status`, `verify` и будущий runtime follow-up сначала используют persisted distro - если persisted distro исчезла из `wsl --list`, это отдельный recoverable state: - clear stale preference - показать понятный re-select / re-install flow ### Шаг 5. Установка tmux внутри WSL Когда distro доступна, дальше стратегия как на Linux, но команды выполняются внутри WSL. Надёжный v1 default: - запускать install внутри `wsl.exe` через PTY - использовать Linux-user path с `sudo` - не делать hidden root install по умолчанию Например для выбранного distro: ```powershell wsl --distribution -- sh -lc "sudo apt-get install -y tmux" ``` Если distro не Debian-based: - читаем `/etc/os-release` внутри WSL - подбираем inner package manager как для Linux - если family unsupported, останавливаем auto path и даём manual guidance вместо рискованной "магии" Нюанс UX: - пароль здесь обычно не Windows admin password, а Linux password выбранного WSL user - это нужно явно подписать в UI, иначе пользователь будет думать, что приложение просит "не тот пароль" Нюанс надёжности: - если делаем retry вроде `apt-get update && apt-get install`, лучше держать это в том же PTY session - так мы не теряем `sudo` timestamp и не заставляем пользователя вводить пароль дважды без причины Optional future optimization: - после явной проверки bootstrap-ready state можно отдельно исследовать `--user root` - но это не должно быть основным путём v1 ### Шаг 6. Verification ```powershell wsl --distribution -- sh -lc "tmux -V" ``` Но для надёжности этого мало. Рекомендуемая verification ladder: 1. `tmux -V` 2. безопасный smoke test на отдельном socket name, чтобы не трогать пользовательские tmux sessions Например: ```powershell wsl --distribution -- sh -lc "tmux -L codex-smoke -f /dev/null new-session -d true && tmux -L codex-smoke kill-server" ``` Именно smoke test лучше отвечает на вопрос "runtime path реально живой", а не только "binary существует". Для Windows follow-up это особенно важно: - smoke test должен по возможности использовать тот же adapter path, который потом будет использовать runtime - иначе можно случайно проверить "tmux работает в интерактивном `wsl sh -lc`", но не проверить "наш main-process adapter реально умеет работать с ним стабильно" ### Windows edge cases - user denied UAC prompt - WSL install succeeded partially - reboot required - no distro installed - distro exists but not bootstrapped - imported distro без launcher quirks - default distro не Ubuntu - inner distro не Debian-based - inner distro unsupported для v1 auto-install - WSL networking / store download issues - corporate policy blocks virtualization - `wsl.exe` есть, но kernel/update broken - WSL установлен, но default distro не выбрана - `wsl --install` partially succeeded, но distro не зарегистрировалась - пользователь завершил bootstrap partially и закрыл окно - Windows build слишком старый для `wsl --install` - app не elevated и WSL core install ушёл во внешний admin window без live stdout - 32-bit helper process path quirks для `wsl.exe`; defensive note из Microsoft docs - при необходимости `C:\\Windows\\Sysnative\\wsl.exe --command`: [Basic commands for WSL](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) - locale-sensitive WSL output, если где-то случайно полагаться на human-readable headers вместо quiet list / stable markers ## 9. Самое важное решение по Windows runtime Если хотим сделать Windows path не "paper feature", а реально полезным, нужно обязательно сделать follow-up: ### 9.1 `tmux:getStatus` на Windows должен стать WSL-aware Сейчас status проверяет host binary. Это не подходит. Нужно: - сначала probe host tmux - если `win32` и host tmux нет: - resolve `wsl.exe` через candidate list, а не слепо через `'wsl'` - decode output defensively, потому что `wsl.exe` может возвращать UTF-16LE / mixed encoding - probe `wsl --status` - probe persisted/default/selected distro - probe inner `tmux -V` - собирать не один boolean, а нормальный `host / wsl / effective` snapshot - не считать WSL 1 автоматическим fail, если smoke test на `tmux` проходит У нас уже есть полезные reference pieces в кодовой базе: - `src/main/ipc/config.ts` - candidate paths для `wsl.exe` (`System32`, `Sysnative`, fallback `wsl.exe`) - UTF-16-aware decoding output - `listWslDistros()` с несколькими command variants (`--list --quiet`, `-l -q`, `-l`) Их нужно не дублировать ad-hoc, а переиспользовать или вынести в общий helper. Ещё одно правило: - `distroVersion` полезен как diagnostic field, но не должен быть gating factor сам по себе - gating factor это usable adapter path + smoke test, а не просто цифра `1` или `2` ### 9.2 `resolveDesktopTeammateModeDecision()` на Windows Сейчас: ```ts if (process.platform === 'win32') { return { injectedTeammateMode: null, forceProcessTeammates: false, }; } ``` Для полноценного Windows support это надо заменить на WSL-aware decision path. ### 9.3 Team runtime ops Нужно отдельно решить: - как запускать `tmux` команды через `wsl -e tmux ...` - как резолвить `wsl.exe` и его output decoding consistently во всех runtime ops - как хранить pane ids / target ids - как делать cleanup - как переводить Windows `cwd` в WSL path и обратно - как не ломаться на UNC paths вида `\\\\wsl.localhost\\\\...` - как пинить `WSL_INTEROP` для долгоживущего tmux server - как persist'ить целевой distro и не ломаться, если default distro изменился - какой `claude` binary model считается supported внутри WSL tmux runtime Это не детали, а критичные runtime requirements. Отдельное правило: - прямые tmux runtime-команды на Windows должны идти через direct exec path (`wsl -e tmux ...` / `wsl --exec tmux ...`), а не через shell wrapper - shell wrapper оставляем только там, где реально нужен `sh -lc`, например для package-manager install steps Почему это важно: - tmux format strings вроде `#{socket_path}` содержат `#` - если команду отдать login shell'у, shell может интерпретировать это как comment и сломать вызов - этот баг уже отражён в orchestrator reference #### 9.3.1 Path translation Если team runtime на Windows будет реально работать через WSL tmux, то `request.cwd` и любые project-related paths нельзя просто пробрасывать как есть. Нужно: - для Windows host paths делать conversion в WSL path - для UNC WSL paths проверять совпадение distro - иметь fallback manual conversion, если `wslpath` unavailable Reference ideas уже есть в: - `agent_teams_orchestrator/src/utils/idePathConversion.ts` - `src/main/utils/pathDecoder.ts` Без этого можно получить очень неприятные полубаги: - tmux стартует, но в неправильном `cwd` - runtime работает только для `C:\\...`, но ломается на `\\\\wsl.localhost\\...` - путь формально передан, но внутри WSL не существует #### 9.3.1.a Cross-filesystem performance and case semantics Это отдельный риск, который нельзя путать с "путь существует". По Microsoft docs: - при работе из Linux command line fastest path - хранить файлы в WSL filesystem - работа по `/mnt/c/...` возможна, но медленнее - Windows и Linux по-разному ведут себя по case sensitivity Источник: [Working across Windows and Linux file systems](https://learn.microsoft.com/en-us/windows/wsl/filesystems) Следствие для плана: - `translatedCwdExists === true` ещё не означает "runtime path хороший" - если project cwd живёт на Windows filesystem и внутри WSL превращается в `/mnt/c/...`, runtime может быть functional, но slower - это не должно блокировать v1, но это должно попадать: - в diagnostics - в optional UX hint вроде `For best performance on Windows + WSL, store the project inside the WSL filesystem` Ещё один subtle point: - Windows file systems часто case-insensitive - Linux file systems case-sensitive - любые runtime assumptions на равенство путей должны быть нормализованы очень аккуратно Дополнительный факт: - Microsoft документирует `WSLENV` как bridge для env vars между Windows и WSL, включая path translation flags Источник: [Working across Windows and Linux file systems](https://learn.microsoft.com/en-us/windows/wsl/filesystems) Практический вывод: - `WSLENV` можно рассматривать как future optimisation/helper - но для v1 лучше не делать его primary correctness layer - path bridge должен оставаться явным и testable в нашем коде #### 9.3.2 `WSL_INTEROP` pinning Самый скрытый и опасный runtime bug сейчас подсвечен в orchestrator reference: - tmux server, стартовавший через краткоживущий `wsl.exe`, может унаследовать нестабильный interop socket - после detach/exit spawning `wsl.exe` interop перестаёт корректно работать для дальнейших Win32 launches из tmux Reference: - `agent_teams_orchestrator/src/utils/tmuxSocket.ts` Практический вывод для плана: - Windows runtime follow-up должен явно закладывать `WSL_INTEROP=/run/WSL/1_interop` при создании isolated tmux server - и ставить его в tmux global environment для новых sessions Иначе можно получить очень неприятный класс багов: - installer/verify выглядит успешным - tmux server поднимается - но позже реальные команды из persistent teammate path начинают падать на interop/timeouts #### 9.3.3 Isolated socket and `TMUX` env override Ещё один обязательный runtime requirement: - приложение не должно работать в пользовательском tmux server по умолчанию - нужен отдельный isolated socket, как в orchestrator reference Нужно: - создавать свой socket name, например `claude-` или другой deterministic app-specific format - все tmux команды гонять через `-L ` - дочерним процессам внутри teammate runtime прокидывать правильный `TMUX` env, указывающий на наш socket - cleanup всегда делать только по нашему socket name / server pid, не по generic tmux targets Иначе можно словить очень неприятный bug class: - user уже работает в своём tmux внутри WSL - приложение начинает управлять не своим server/session - cleanup и `kill-pane` задевают не тот runtime #### 9.3.4 `claude` binary model inside WSL tmux Нужно принять explicit решение, что именно запускает teammate runtime внутри WSL pane: Вариант A - host Windows `claude.exe` через WSL interop - 🎯 8 🛡️ 7 🧠 6 - `150-350` строк follow-up logic - Плюсы: - ближе к текущей архитектуре desktop app - reuse существующего host-side `ClaudeBinaryResolver` - меньше шансов разъехаться по auth/config flows - Риски: - нужно жёстко prefer `.exe` - `.cmd`/`.bat` wrappers нельзя считать автоматически эквивалентными - interop/runtime smoke test должен проверять именно этот path Вариант B - Linux `claude` внутри WSL distro - 🎯 5 🛡️ 6 🧠 8 - `400-900` строк follow-up logic - Плюсы: - более "нативная" Linux execution model внутри tmux - Риски: - отдельная установка binary внутри WSL - потенциально другой auth/config root - больше drift от текущего Windows desktop runtime - если бинарь/скрипты лежат на mounted Windows filesystem, semantics file permissions в WSL становятся отдельным источником проблем: [File permissions for WSL](https://learn.microsoft.com/en-us/windows/wsl/file-permissions) Рекомендация для v1 follow-up: - идти через вариант A, но только если available binary это пригодный `.exe` - если resolver на Windows дал только `.cmd`/`.bat`, не поднимать `runtimeReady=true` без отдельного validated wrapper path #### 9.3.5 Config/auth root semantics for Windows `claude.exe` from WSL Это ещё один важный слой, который легко пропустить. Факт из Microsoft docs: - Windows executables, запущенные из WSL, работают как Win32 apps активного Windows user и retain WSL working directory for the most part Источник: [Working across Windows and Linux file systems](https://learn.microsoft.com/en-us/windows/wsl/filesystems) Inference для нашего проекта: - если teammate runtime внутри WSL pane запускает host `claude.exe`, нужно явно проверить, какие config/auth/session roots он использует в таком режиме - нельзя автоматически считать, что "working dir внутри WSL" означает "и config root тоже WSL" Практический вывод: - Windows runtime readiness probe должен включать не только запуск бинаря, но и sanity check на auth/config behavior тем же adapter path - для v1 лучше держать модель консервативной: - prefer host Windows auth/config expectations - не объявлять runtime-ready, если probing показывает разъехавшийся config root или странный auth state Самый логичный reference - `agent_teams_orchestrator/src/utils/tmuxSocket.ts`. Если это не входит в текущую итерацию, UI/документация должны честно говорить: - `WSL setup prepares tmux support` - `full Windows persistent teammate runtime will be enabled in follow-up` ## 10. Надёжность: как сделать "без багов" ## 10.1 Installer mutex Нельзя запускать 2 инсталла параллельно. Нужно: ```ts if (this.installing) { this.sendProgress({ type: 'error', error: 'tmux installation is already in progress', }); return; } ``` ## 10.2 Every install ends with verification Нельзя считать install успешным по exit code package manager alone. Успех только если: - install command exit code success - повторный status probe нашёл `tmux` - `tmux -V` реально исполняется - для runtime-critical enablement желательно проходит и лёгкий smoke test на isolated socket/session Для Windows runtime-critical enablement это лучше уточнить ещё жёстче: - smoke test должен по возможности запускать тот же `claude` binary model, который потом реально пойдёт в teammate runtime - тест уровня `tmux ... new-session -d true` полезен, но не доказывает, что interop launch path для реального CLI тоже живой - если binary model это host `claude.exe` from WSL, probe должен подтверждать и auth/config sanity, а не только факт старта процесса ## 10.2.1 Do not gate only on tmux version string Ещё один скрытый риск - слишком доверять `tmux -V`. Почему: - package-manager версии на enterprise / LTS системах могут быть сильно старыми - но для нас важен не номер сам по себе, а наличие конкретных runtime capabilities Практический rule: - installer status может показывать `tmux -V` - но runtime readiness должен опираться на capability probes: - isolated socket create - `has-session` - `display-message -p` - `set-environment -g` - если Windows follow-up использует это - `new-session -e` Иными словами: - `tmux` version string полезен для diagnostics - capability contract важнее, чем числовой version gate, если мы заранее не зафиксировали минимальную supported версию ## 10.3 No fake silent fallback Нельзя: - проглотить ошибку `brew not found` - переключиться на другой strategy без явного detail - показать completed, если verification failed ## 10.4 Structured error taxonomy Нужен нормализатор ошибок: ```ts type TmuxInstallErrorCode = | 'ALREADY_INSTALLED' | 'PACKAGE_MANAGER_NOT_FOUND' | 'UNSUPPORTED_PLATFORM' | 'AUTH_CANCELLED' | 'PERMISSION_DENIED' | 'PTY_UNAVAILABLE' | 'NETWORK_ERROR' | 'PACKAGE_LOCKED' | 'PACKAGE_NOT_FOUND' | 'RESTART_REQUIRED' | 'WSL_NOT_INSTALLED' | 'WSL_COMMAND_UNSUPPORTED' | 'WSL_DISTRO_MISSING' | 'WSL_DISTRO_NOT_READY' | 'EXTERNAL_ELEVATION_CANCELLED' | 'EXTERNAL_ELEVATION_FAILED' | 'VERIFY_FAILED' | 'USER_CANCELLED' | 'UNKNOWN'; ``` И map в user-friendly messages. Важное правило для нормализатора: - не полагаться только на exact English stderr text - где возможно, использовать: - exit code - observable state вроде lock files / missing binary / missing distro - несколько broad regex patterns вместо одного exact message Иначе: - локализованные Linux/Windows системы будут часто падать в `UNKNOWN` - а UI станет выглядеть "случайно нестабильным", хотя root cause классифицируемый ## 10.5 Retry semantics - `Retry` должен re-run plan resolution с нуля - перед retry: - kill active child/pty - invalidate status cache - clear stale progress state ## 10.5.1 Cache semantics during and after install Кэш статуса здесь легко превратить в источник ложных UI состояний. Правила: - пока install active, negative cache entries нельзя считать authoritative - после любого install step, который потенциально меняет system state, нужен explicit invalidate - после Windows external elevated step нужен mandatory fresh probe, даже если helper сообщил success - после app restart UI должен восстанавливаться не из in-memory installer state, а из свежего system probe Итог: - installer flow должен быть idempotent - partial install / app restart / crashed renderer не должны оставлять "залипшее installing" состояние ## 10.5.2 Recovery after app restart or renderer crash Особенно важно для Windows external elevated steps. Правила: - in-memory progress state не считается durable truth - после app relaunch service сначала смотрит на system state, а не пытается "доигрывать" старый progress bar - если остались temp marker/result files от elevated step: - их можно использовать только как diagnostics hint - final UI state всё равно решает fresh probe - старые marker/result dirs нужно периодически cleanup'ить по age, чтобы не копить мусор и не читать stale outcome как новый ## 10.6 Cancellation semantics Нужен `cancelInstall()`: - убивает child/pty - шлёт `cancelled` - не оставляет broken installing flag Но для Windows external elevated step это работает иначе: - после `Start-Process -Verb RunAs` приложение уже не контролирует тот процесс так же, как обычный child - значит `Cancel` в этом состоянии это скорее `stop waiting locally`, а не гарантированное terminate внешнего admin window Надёжный UX contract: - если step ещё не ушёл во внешний elevated flow, `Cancel` действительно отменяет install locally - если step уже ушёл во внешний elevated flow: - app переводит себя в cancelled/abandoned waiting state - явно пишет, что external administrator window may still be open - после этого truth source всё равно fresh probe, а не предположение "мы точно всё остановили" ## 10.7 Diagnostics Добавить diagnostic log рядом по стилю с `CliInstallerService`: - detected platform - chosen strategy - PATH used - package manager path - raw exit code - stderr tail - verify result ### 10.8 Raw log hygiene Нельзя бесконтрольно складывать весь terminal output в renderer/store. Нужно: - ring buffer по строкам или байтам - upper bound на память - redaction rules для очевидно чувствительных фрагментов - не persist raw install terminal logs на диск по умолчанию - не логировать пользовательский input в PTY вообще - пароль/keystrokes пользователя не должны попадать ни в raw chunks, ни в diagnostics Минимум: ```ts const MAX_RAW_CHUNKS = 400; const MAX_RAW_BYTES = 256 * 1024; ``` ## 11. Детальный implementation plan ## Phase 1. Feature contracts and registration Файлы: - `src/features/tmux-installer/contracts/api.ts` - `src/features/tmux-installer/contracts/channels.ts` - `src/features/tmux-installer/contracts/dto.ts` - `src/features/tmux-installer/contracts/index.ts` - `src/features/tmux-installer/main/index.ts` - `src/features/tmux-installer/preload/createTmuxInstallerBridge.ts` - `src/features/tmux-installer/preload/index.ts` - `src/renderer/api/httpClient.ts` - host registration points: - `src/preload/index.ts` - app shell main bootstrap Что делаем: - расширяем типы - добавляем install/invalidate/cancel/progress API - browser-mode stubs возвращают safe no-op behavior ## Phase 2. Main-process status refactor Файлы: - `src/features/tmux-installer/main/index.ts` - `src/features/tmux-installer/main/composition/createTmuxInstallerFeature.ts` - `src/features/tmux-installer/main/adapters/input/ipc/registerTmuxInstallerIpc.ts` - `src/features/tmux-installer/main/adapters/output/sources/TmuxStatusSourceAdapter.ts` - `src/features/tmux-installer/core/application/use-cases/GetTmuxStatusUseCase.ts` - host registration point: - app shell main bootstrap, который импортирует только `@features/tmux-installer/main` - если `src/main/ipc/tmux.ts` сохраняется ради совместимости, то только как thin registration shim Что делаем: - выносим status computation из голого IPC handler в feature use case + adapter chain - status probe должен использовать enriched PATH / interactive shell env, а не только process PATH - для macOS дополнительно проверять common brew prefixes, иначе получим false negative после успешного install - feature facade умеет: - `getStatus()` - `install()` - `cancelInstall()` - `invalidateStatus()` - `setMainWindow()` ## Phase 2.2 WSL preference persistence Новый модуль: - `src/features/tmux-installer/main/infrastructure/wsl/TmuxWslPreferenceStore.ts` Что делаем: - сохраняем `preferred distro` после явного Windows wizard choice или после первого успешного install/verify - status service сначала пробует persisted distro - если persisted distro больше не существует, store self-heals: - mark stale - clear preference - вернуть UI в `re-select distro` / `manual guidance` ## Phase 2.1 PTY plumbing refactor Файлы: - `src/main/services/infrastructure/PtyTerminalService.ts` - feature-local wrapper `src/features/tmux-installer/main/infrastructure/installer/TmuxInstallTerminalSession.ts` - возможно новый attach-style renderer component вместо прямого reuse `TerminalModal` Что делаем: - добавляем main-side control surface для PTY session - installer service должен получать: - raw chunks - exit code - ability to write input - explicit dispose lifecycle - renderer не должен быть владельцем installer process - если attach UI делаем позже, installer всё равно остаётся source of truth ## Phase 3. Strategy resolver Новый модуль: - `src/features/tmux-installer/main/infrastructure/installer/TmuxInstallStrategyResolver.ts` Псевдокод: ```ts export async function resolveTmuxInstallPlan(): Promise { if (process.platform === 'darwin') return resolveMacPlan(); if (process.platform === 'linux') return resolveLinuxPlan(); if (process.platform === 'win32') return resolveWindowsPlan(); return manualOnlyPlan('Unsupported platform'); } ``` Важно: - resolver должен учитывать не только OS/package manager, но и локальные capability flags - пример: если путь требует interactive `sudo`, а PTY capability недоступен, auto-install надо честно выключать и переводить пользователя в manual guidance ## Phase 4. macOS install runner - detect `brew` - else detect `port` - else manual Нерискованный rule: - не auto-install Homebrew - не auto-run remote shell scripts ## Phase 5. Linux install runner - parse `/etc/os-release` - choose command - run command in PTY - stream raw logs - verify - if immutable host detected -> short-circuit to manual fallback - if retry with `update` is needed, prefer doing it in the same PTY session ## Phase 6. Renderer slice inside feature Новые файлы: - `src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.ts` - `src/features/tmux-installer/renderer/hooks/useTmuxInstallerBanner.ts` - `src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx` - `src/features/tmux-installer/renderer/utils/formatTmuxInstallerText.ts` - `src/features/tmux-installer/renderer/index.ts` Не делать primary architecture через новый global slice, пока это не стало реально нужно нескольким независимым surface'ам. Лучше так: - installer state canonical в main process - renderer slice читает snapshot + подписывается на progress events - local React state внутри feature hook достаточно для v1 - если позже появится второй surface с тем же state, можно отдельно решить, нужен ли shared renderer cache Feature hook return shape: ```ts { viewModel: TmuxInstallerBannerViewModel; actions: { install: () => Promise; cancel: () => Promise; refresh: () => Promise; toggleDetails: () => void; }; } ``` Hook boundary rules: - hook не должен импортировать `api` напрямую - hook не должен знать про IPC channel names - hook не должен нормализовать platform-specific ошибки - это ответственность renderer adapter и main-side use case/presenter layers ## Phase 7. Banner UX Создать feature view: - `src/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsx` И оставить `src/renderer/components/dashboard/TmuxStatusBanner.tsx` как thin wrapper: ```tsx import { TmuxInstallerBannerView } from '@features/tmux-installer/renderer'; export function TmuxStatusBanner(): JSX.Element { return ; } ``` Внутри feature view: - `Install tmux` button - status line - progress bar - `Show details` / `Hide details` - error block - manual fallback buttons UI logic: - если `autoInstall.supported === false`, не показывать install CTA - если `needs_manual_step`, показывать step-specific CTA - если `needs_restart`, показывать restart CTA - если live installer session уже идёт, feature должен подхватить её через snapshot и не сбрасывать UI в `idle` ## Phase 7.1 Feature integration points checklist Чтобы реализация реально соответствовала guide, в PR и в финальной implementation notes надо перечислить изменённые host integration points. Ожидаемые integration points для этой задачи: - `src/features/tmux-installer/contracts/*` - `src/features/tmux-installer/core/*` - `src/features/tmux-installer/main/*` - `src/features/tmux-installer/preload/*` - `src/features/tmux-installer/renderer/*` - app shell registration points for main/preload/renderer - `src/renderer/components/dashboard/TmuxStatusBanner.tsx` Если появятся дополнительные host touchpoints, это должно быть явно объяснено, а не "само выросло". ## Phase 7.2 Definition of done по feature standard Перед merge реализация считается готовой только если: - feature живёт в `src/features/tmux-installer/` - структура соответствует canonical template из `docs/FEATURE_ARCHITECTURE_STANDARD.md` - `contracts`, `core`, `main`, `preload`, `renderer` заполнены осмысленно - `core/domain` side-effect free - `core/application` покрывает use case orchestration - public imports идут только через feature entrypoints - shared shell files не содержат business logic, только registration shims - renderer business logic не осталась в `TmuxStatusBanner.tsx` - есть как минимум: - domain policy tests - application use case tests - critical renderer utility tests - one adapter-level mapping test - `pnpm typecheck` проходит - `pnpm build` проходит - import/lint guard rails не позволяют deep-import feature internals снаружи - integration points перечислены в PR notes ## Phase 8. Windows WSL wizard Сделать это отдельным sub-flow внутри feature use case + infrastructure path, а не if/else spaghetti inside banner. Новый helper: - `src/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.ts` Методы: ```ts interface TmuxWslService { getStatus(): Promise; installWsl(): Promise; ensureDistro(): Promise; ensureBootstrapReady(): Promise; installTmuxInsideDistro(): Promise; verifyTmux(): Promise; } ``` Важно: - `installWsl()` должен поддерживать сценарий `external elevated step` - его контракт не должен предполагать "я всегда верну полный live log" - success этого шага всегда подтверждается только последующим fresh probe ## Phase 8.1 Windows elevated runner Новый модуль: - `src/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.ts` Ответственность: - создавать temp working dir для elevated step - писать helper script / args - запускать PowerShell `Start-Process -Verb RunAs -Wait` - читать result JSON / stderr tail file если helper успел их записать - нормализовать outcome в: - `elevated_succeeded` - `elevated_cancelled` - `elevated_failed` - `elevated_unknown_outcome` Ключевое правило: - даже `elevated_succeeded` не завершает installer flow само по себе - после него всегда идёт fresh probe реального system state ## Phase 9. Windows runtime follow-up Это можно вынести в отдельную PR/итерацию, но в этом документе оно считается обязательным follow-up. Файлы: - `src/main/services/team/runtimeTeammateMode.ts` - `src/main/ipc/tmux.ts` - `src/main/services/team/TeamProvisioningService.ts` - `src/features/tmux-installer/main/infrastructure/wsl/TmuxWslPathBridge.ts` Нужно: - сделать WSL-aware tmux detection - перестать hard-disable tmux path на Windows, если WSL tmux реально доступен - добавить отдельный adapter для `tmux` команд через `wsl -e` - добавить path bridge для `cwd` и project-related paths - заложить `WSL_INTEROP` pinning strategy при создании isolated tmux server - ввести explicit policy для supported `claude` binary shape внутри WSL runtime ## 12. Пример скелета feature facade ```ts export class TmuxInstallerFeatureFacade { #mainWindow: BrowserWindow | null = null; #installing = false; #activeChild: ChildProcess | null = null; #activePty: TmuxInstallTerminalSession | null = null; #cachedStatus: { value: TmuxStatus; at: number } | null = null; setMainWindow(window: BrowserWindow | null): void { this.#mainWindow = window; } async getStatus(): Promise { // 1. detect host tmux // 2. detect install capability // 3. on Windows also detect WSL readiness // 4. return unified snapshot } async install(): Promise { if (this.installing) { this.sendProgress({ type: 'error', error: 'tmux installation is already in progress' }); return; } this.installing = true; try { this.sendProgress({ type: 'checking', detail: 'Detecting installation strategy...' }); const plan = await resolveTmuxInstallPlan(); if (!plan.autoInstallSupported) { this.sendProgress({ type: 'error', error: plan.reasonIfUnsupported ?? 'Automatic install is not available on this machine', nextManualHint: plan.manualHints[0] ?? null, }); return; } await this.executePlan(plan); this.sendProgress({ type: 'verifying', detail: 'Verifying tmux...' }); const status = await this.invalidateAndGetFreshStatus(); if (!status.effective.available) { throw new Error('tmux installation finished but verification failed'); } if (!status.effective.runtimeReady) { this.sendProgress({ type: 'needs_manual_step', detail: 'tmux is installed, but runtime integration is not ready yet on this machine', status, }); return; } this.sendProgress({ type: 'completed', detail: `Installed ${status.effective.version ?? 'tmux'} via ${status.effective.location ?? 'unknown path'}`, status, }); } catch (error) { this.sendProgress({ type: 'error', error: normalizeTmuxInstallError(error).userMessage, }); } finally { this.installing = false; this.activeChild = null; this.activePty = null; } } async cancelInstall(): Promise { killProcessTree(this.activeChild); this.activePty?.dispose(); this.sendProgress({ type: 'cancelled', detail: 'Installation cancelled' }); } private sendProgress(progress: TmuxInstallerProgress): void { safeSendToRenderer(this.mainWindow, TMUX_INSTALLER_PROGRESS, progress); } } ``` ## 12.2 Пример runtime readiness rule для Windows follow-up ```ts function isWindowsWslRuntimeReady(ctx: { tmuxSmokeOk: boolean; wslInteropPinned: boolean; targetDistroExists: boolean; translatedCwdExists: boolean; claudeBinaryPath: string | null; configRootSanityOk: boolean; }): boolean { if (!ctx.tmuxSmokeOk) return false; if (!ctx.wslInteropPinned) return false; if (!ctx.targetDistroExists) return false; if (!ctx.translatedCwdExists) return false; if (!ctx.claudeBinaryPath) return false; if (!ctx.configRootSanityOk) return false; const lower = ctx.claudeBinaryPath.toLowerCase(); return lower.endsWith('.exe'); } ``` Это intentionally conservative rule. Смысл: - лучше временно недовключить Windows WSL runtime, чем считать runtime-ready сценарий с `.cmd` wrapper или broken path translation ## 12.1 Пример Windows elevated step runner ```ts interface WindowsElevatedStepResult { outcome: 'elevated_succeeded' | 'elevated_cancelled' | 'elevated_failed' | 'elevated_unknown_outcome'; detail?: string | null; resultFilePath?: string | null; } export class WindowsElevatedStepRunner { async runWslInstall(args: string[]): Promise { const tempDir = await fsp.mkdtemp(join(tmpdir(), 'tmux-wsl-install-')); const resultFile = join(tempDir, 'result.json'); const helperScript = join(tempDir, 'run-wsl-install.ps1'); await fsp.writeFile( helperScript, buildWslInstallScript({ wslArgs: args, resultFile, }), 'utf8' ); const ps = await execFileNoThrow('powershell.exe', [ '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', `Start-Process -FilePath powershell.exe -Verb RunAs -Wait -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File \"${escapePowerShell(helperScript)}\"'`, ]); // Do not trust only PowerShell exit code. UAC cancellation and helper failures // are better inferred from the result file plus a fresh WSL probe. if (await fileExists(resultFile)) { const payload = JSON.parse(await fsp.readFile(resultFile, 'utf8')) as { ok?: boolean; detail?: string; }; return { outcome: payload.ok ? 'elevated_succeeded' : 'elevated_failed', detail: payload.detail ?? null, resultFilePath: resultFile, }; } if (looksLikeElevationCancelled(ps.stderr, ps.stdout, ps.code)) { return { outcome: 'elevated_cancelled', detail: 'Administrator permission request was cancelled', resultFilePath: null, }; } return { outcome: 'elevated_unknown_outcome', detail: ps.stderr || ps.stdout || null, resultFilePath: null, }; } } ``` Это не production-ready код, а reference shape. Главная идея здесь важнее синтаксиса: - elevated Windows step отделён от PTY/Linux install logic - результат helper script это только дополнительная диагностика - truth source всё равно fresh `wsl --status` / `wsl --list` probe после шага ## 13. Пример Linux resolver ```ts export async function resolveLinuxPackageManager(): Promise { const osRelease = await readOsRelease(); if (isDistroFamily(osRelease, ['debian'])) return 'apt'; if (isDistroFamily(osRelease, ['fedora', 'rhel'])) return (await hasBinary('dnf')) ? 'dnf' : 'yum'; if (isDistroFamily(osRelease, ['suse', 'opensuse'])) return 'zypper'; if (isDistroFamily(osRelease, ['arch'])) return 'pacman'; if (await hasBinary('apt-get')) return 'apt'; if (await hasBinary('dnf')) return 'dnf'; if (await hasBinary('yum')) return 'yum'; if (await hasBinary('zypper')) return 'zypper'; if (await hasBinary('pacman')) return 'pacman'; return 'unknown'; } ``` ## 14. Пример Windows WSL status probe ```ts async function getWslStatus(): Promise { const wslStatus = await execWslNoThrow(['--status']); if (wslStatus.code !== 0) { return { wslInstalled: false, rebootRequired: false, distroName: null, distroVersion: null, distroBootstrapped: false, innerPackageManager: null, tmuxAvailableInsideWsl: false, tmuxVersion: null, statusDetail: wslStatus.stderr || wslStatus.stdout || 'WSL not available', }; } const preferredDistro = await wslPreferenceStore.getPreferredDistro(); const list = await execWslNoThrow(['--list', '--verbose']); const distro = resolveTargetDistro({ preferredDistro, listOutput: list.stdout, }); const distroVersion = parseDefaultDistroVersion(list.stdout); if (!distro) { return { wslInstalled: true, rebootRequired: false, distroName: null, distroVersion: null, distroBootstrapped: false, innerPackageManager: null, tmuxAvailableInsideWsl: false, tmuxVersion: null, statusDetail: 'WSL installed but no Linux distribution is configured', }; } const bootstrapProbe = await execWslNoThrow([ '--distribution', distro, '--', 'sh', '-lc', 'printf ready', ]); if (bootstrapProbe.code !== 0 || !bootstrapProbe.stdout.includes('ready')) { return { wslInstalled: true, rebootRequired: false, distroName: distro, distroVersion, distroBootstrapped: false, innerPackageManager: null, tmuxAvailableInsideWsl: false, tmuxVersion: null, statusDetail: bootstrapProbe.stderr || bootstrapProbe.stdout || 'Linux distro bootstrap is not finished', }; } const tmuxProbe = await execWslNoThrow([ '--distribution', distro, '--', 'tmux', '-V', ]); return { wslInstalled: true, rebootRequired: false, distroName: distro, distroVersion, distroBootstrapped: true, innerPackageManager: null, tmuxAvailableInsideWsl: tmuxProbe.code === 0, tmuxVersion: tmuxProbe.code === 0 ? extractTmuxVersion(tmuxProbe.stdout || tmuxProbe.stderr) : null, statusDetail: tmuxProbe.code === 0 ? null : tmuxProbe.stderr || tmuxProbe.stdout || null, }; } ``` Где `execWslNoThrow()`: - сам выбирает подходящий `wsl.exe` candidate - запускает с `encoding: buffer` - затем декодирует stdout/stderr defensively, потому что `wsl.exe` output на Windows не всегда стабильно UTF-8 А `resolveTargetDistro()`: - сначала пробует persisted preference - если её нет или она stale, использует default distro / selected distro heuristic - не должен silently switch runtime target, если preference сломалась и это меняет expected behavior пользователя А `parseDefaultDistroVersion()`: - должен считаться best-effort helper только для diagnostics - если parser не уверен, лучше вернуть `null`, чем принять неправильное runtime decision ## 15. Error UX copy guidelines Плохой текст: - `ENOENT` - `spawn failed` - `exit code 1` Хороший текст: - `Homebrew was not found. Install Homebrew first or use the manual instructions below.` - `The package manager asked for administrator privileges and the request was cancelled.` - `Interactive installation is not available because terminal support is missing in this app build. Use the manual command below.` - `WSL was installed, but Windows restart is still required before tmux can be set up.` - `This Windows version does not support the automatic WSL setup flow used by the app. Follow the Microsoft manual steps below.` - `WSL is available, but no Linux distribution is installed yet. Install Ubuntu first, then continue.` - ` is installed in WSL, but its first-launch setup is not finished yet. Open it once, finish Linux user setup, then re-check.` - `WSL setup needs an administrator step in a separate window. Complete that step, then come back and press Re-check.` ## 16. Тест-план ### Unit tests - status resolver for every platform - package manager resolver - error normalizer - WSL output parsers - WSL executable candidate resolver + UTF-16/mixed output decoding - progress phase mapping - WSL preferred-distro resolver - elevated-step restart recovery logic - locale-robust default-distro resolution - supported Windows `claude` binary-shape policy - locale-robust package-manager error classification - config/auth-root sanity probe for Windows `claude.exe` from WSL - tmux capability probes vs plain version string ### Integration tests with mocks - install success on brew - brew missing -> manual fallback - apt sudo password prompt path - package manager lock errors - verification failure after install - PTY capability missing -> no interactive auto-install path - Windows: - WSL missing - WSL install goes through external elevated step and requires re-check - external elevated step returns no result file -> unknown outcome -> fresh probe decides - WSL install requires restart - `wsl --status` unsupported on older build -> manual guidance - distro missing - distro bootstrap pending - unsupported default distro -> manual or install-supported-distro guidance - tmux install inside WSL success - runtime smoke test passes only when same adapter path is used - persisted preferred distro disappears -> self-heal without misleading "installed" state - app restart during external elevated step -> self-heal from fresh probe, not stale in-memory state - direct `wsl -e tmux ...` path works for tmux format strings with `#` - host Windows resolver returns `.cmd` only -> runtime stays conservative, no false ready - host `claude.exe` launches from WSL but config/auth root behaves unexpectedly -> runtime stays not ready - `tmux -V` succeeds but required runtime capability probe fails -> no false ready ### Manual matrix - macOS Apple Silicon + Homebrew - macOS Intel + Homebrew - macOS + MacPorts only - Ubuntu - Fedora - Arch - openSUSE - Fedora Silverblue / similar immutable host -> manual fallback only - Windows 11 clean machine without WSL - Windows 11 with WSL but no distro bootstrap - Windows 11 with ready supported distro - Windows 11 with WSL 1 distro where tmux still works - Windows 10 older build -> manual Microsoft guidance only - Windows runtime with project on `C:\\...` path -> WSL path translation works - Windows runtime with project on `\\\\wsl.localhost\\\\...` path -> distro match handling works - Windows with multiple distros and changed default distro -> persisted target stays stable - Windows runtime on `/mnt/c/...` project -> works but surfaces performance diagnostic ## 16.1 Остаточные uncertainty points после самого жёсткого IOF Они уже не блокируют план, но их лучше закрыть маленькими spikes до полной реализации. 1. Windows elevated helper UX - 🎯 8 🛡️ 8 🧠 5 - `100-200` строк spike - Нужно проверить на реальной машине, как стабильно работает `Start-Process -Verb RunAs -Wait` + temp result file и какие коды/сообщения приходят при UAC cancel. 2. WSL distro install path under corporate restrictions - 🎯 7 🛡️ 7 🧠 4 - `50-120` строк spike - Нужно подтвердить, когда `wsl --install -d ...` реально помогает, а когда сразу надо переводить пользователя в manual Microsoft docs. 3. PTY attach model для installer UI - 🎯 8 🛡️ 9 🧠 6 - `150-300` строк spike - Нужно быстро выбрать: расширяем существующий `PtyTerminalService` или делаем небольшой отдельный installer-session adapter. 4. WSL runtime path bridge + interop pinning - 🎯 7 🛡️ 9 🧠 7 - `150-350` строк spike - Нужно проверить на реальном Windows path mix: `C:\\...`, `\\\\wsl.localhost\\...`, spaces, different distro names, и подтвердить поведение `WSL_INTEROP=/run/WSL/1_interop` для долгоживущего tmux server. 5. Preferred distro persistence semantics - 🎯 8 🛡️ 8 🧠 5 - `80-180` строк spike - Нужно подтвердить, где и как лучше хранить `preferred WSL distro`, чтобы не получить ложные переключения при смене default distro и при этом не создать лишнюю сложность миграции настроек. 6. Windows `claude` binary model through WSL interop - 🎯 7 🛡️ 8 🧠 6 - `120-260` строк spike - Нужно подтвердить на реальной машине: `claude.exe` path, `.cmd` fallback behavior, quoting, cleanup и минимальный smoke test именно через тот binary shape, который потом пойдёт в teammate runtime. 7. Config/auth root sanity for `claude.exe` launched from WSL - 🎯 6 🛡️ 8 🧠 6 - `100-220` строк spike - Нужно проверить, какой effective config/auth/session root реально использует host `claude.exe`, запущенный из WSL tmux pane, и не появляется ли drift между Windows root и WSL working directory. ## 17. Rollout strategy ### PR 1 - shared types - main service skeleton - macOS/Linux installer - banner UI - manual fallback UX ### PR 2 - Windows WSL wizard - richer status snapshot for WSL ### PR 3 - Windows runtime enablement through WSL tmux Это лучше, чем пытаться протащить всё одним PR. ## 18. Самый важный список "не наступить" - не делать Windows wizard без честного copy, если runtime ещё не использует WSL tmux - не auto-install Homebrew в фоне - не считать install успешным без `tmux -V` - не считать `tmux:getStatus().effective.available === true` синонимом "host tmux есть" - не смешивать service-owned installer PTY с renderer-owned terminal modal - не использовать fake 0-100 network progress для package managers - не терять stderr/stdout - не запускать `sudo` через non-TTY child - не делать default hidden root install внутри WSL - не пытаться auto-manage immutable Linux hosts как обычный `dnf/apt` Linux - не хардкодить PATH без shell env merge - не запускать Windows WSL runtime без path translation layer - не поднимать Windows tmux server в WSL без явного `WSL_INTEROP` pinning strategy - не привязывать Windows runtime только к current default distro - не молча менять целевой WSL distro после успешной установки tmux - не считать `.cmd`/`.bat` эквивалентом `.exe` для Windows WSL runtime без отдельной валидации - не считать успешный старт `claude.exe` из WSL достаточным доказательством корректного config/auth root - не прятать manual fallback, если auto-install unavailable - не делать installer logic inside React component ## 19. Мой итоговый recommendation Делать именно так: 1. Сначала качественный `tmux-installer` feature slice для `macOS/Linux` 2. Сразу же заложить правильный shared contract под Windows WSL 3. На Windows не останавливаться на "installer wizard only" 4. Обязательно follow-up на WSL-aware runtime support, иначе ценность Windows-части будет неполной Если делать по этому плану, получится действительно качественно: - удобно - понятно - с нормальной диагностикой - без дешёвых UX-обманок - с честным fallback для ручной установки