agent-ecosystem/docs/research/tmux-hybrid-installer-plan.md
2026-04-14 20:07:57 +03:00

107 KiB
Raw Blame History

План: 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
  • Homebrew formula для tmux сейчас показывает brew install tmux, bottle support для macOS и Linux, stable 3.6a: Homebrew tmux
  • MacPorts для tmux сейчас рекомендует sudo port install tmux, версия 3.6a: MacPorts tmux
  • Microsoft для WSL пишет: открыть PowerShell в administrator mode, выполнить wsl --install, затем restart machine: Install WSL
  • Microsoft также документирует wsl --list --online, wsl --distribution ... --user ..., wsl --status, wsl --shutdown: Basic commands for WSL
  • Microsoft отдельно держит manual WSL install path для older Windows builds / server-like scenarios: Manual installation steps for older versions of WSL

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

Изначальная идея с:

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

Изначальный пример с:

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

Следствие:

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

Практический вывод для 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/<feature>/contracts
    • src/features/<feature>/core/domain
    • src/features/<feature>/core/application
    • src/features/<feature>/main/composition
    • src/features/<feature>/main/adapters/input
    • src/features/<feature>/main/adapters/output
    • src/features/<feature>/main/infrastructure
    • src/features/<feature>/preload
    • src/features/<feature>/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.

Рекомендуемая структура:

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

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:

// 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';

Чего не делать:

// не импортировать снаружи
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.

Рекомендуемо:

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:

export interface TmuxAPI {
  getStatus: () => Promise<TmuxStatus>;
  install: () => Promise<void>;
  invalidateStatus: () => Promise<void>;
  cancelInstall: () => Promise<void>;
  onProgress: (cb: (event: unknown, data: TmuxInstallerProgress) => void) => () => void;
}

Новые IPC channels:

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 пойман вживую.

Добавить:

export const TMUX_GET_INSTALLER_SNAPSHOT = 'tmux:getInstallerSnapshot';

export interface TmuxAPI {
  getInstallerSnapshot: () => Promise<TmuxInstallerSnapshot>;
}

Где:

  • 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/:

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:

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

brew install tmux

MacPorts

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

sudo apt-get install -y tmux

Fedora / RHEL

sudo dnf install -y tmux

old RHEL / CentOS

sudo yum install -y tmux

openSUSE

sudo zypper --non-interactive install tmux

Arch

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)
  • dnf -y
    • DNF command reference: -y, --assumeyes автоматически отвечает yes на вопросы: DNF Command Reference
  • zypper --non-interactive install
    • SUSE docs явно рекомендуют --non-interactive до команды install для scripted usage: SUSE zypper docs
  • pacman --noconfirm
    • Arch manual: bypasses confirmation prompts и предназначен для scripted usage; --needed не переустанавливает уже актуальный target: pacman(8)

Почему 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:

sudo apt-get install -y tmux

Fallback retry only if needed:

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

Пробуем:

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 отсутствует полностью, рекомендуемый путь:

wsl --install --no-distribution

Если хотим объединённый flow "WSL + сразу Ubuntu":

wsl --install -d Ubuntu --no-launch

Если install hangs или Store path problematic, дать fallback hint:

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
  • --web-download documented in Microsoft install guide как fallback, если install hangs at 0.0%: Install WSL
  • --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

⚠️ Самое важное ограничение этого шага:

  • 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

Шаг 3.5. Установка Linux distro, если WSL core уже есть

Если wsl --status успешен, но список дистрибутивов пуст:

  • это не повод повторять full WSL core install
  • дальше нужен именно install distro

Рекомендуемый путь:

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
  • открываем пользователю команду:
wsl -d <SelectedDistro>

После этого пользователь:

  • ждёт распаковку
  • создаёт 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
  • если нужно понять именно 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:

wsl --distribution <SelectedDistro> -- 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

wsl --distribution <SelectedDistro> -- sh -lc "tmux -V"

Но для надёжности этого мало.

Рекомендуемая verification ladder:

  1. tmux -V
  2. безопасный smoke test на отдельном socket name, чтобы не трогать пользовательские tmux sessions

Например:

wsl --distribution <SelectedDistro> -- 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
  • 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

Сейчас:

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\\<distro>\\...
  • как пинить 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

Следствие для плана:

  • 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 на равенство путей должны быть нормализованы очень аккуратно

Дополнительный факт:

Практический вывод:

  • 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-<pid> или другой deterministic app-specific format
  • все tmux команды гонять через -L <socket>
  • дочерним процессам внутри 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

Рекомендация для 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

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 инсталла параллельно.

Нужно:

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

Нужен нормализатор ошибок:

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

Минимум:

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

Псевдокод:

export async function resolveTmuxInstallPlan(): Promise<TmuxInstallPlan> {
  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:

{
  viewModel: TmuxInstallerBannerViewModel;
  actions: {
    install: () => Promise<void>;
    cancel: () => Promise<void>;
    refresh: () => Promise<void>;
    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:

import { TmuxInstallerBannerView } from '@features/tmux-installer/renderer';

export function TmuxStatusBanner(): JSX.Element {
  return <TmuxInstallerBannerView />;
}

Внутри 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

Методы:

interface TmuxWslService {
  getStatus(): Promise<TmuxWslStatus>;
  installWsl(): Promise<TmuxWslStepResult>;
  ensureDistro(): Promise<TmuxWslStepResult>;
  ensureBootstrapReady(): Promise<TmuxWslStepResult>;
  installTmuxInsideDistro(): Promise<TmuxWslStepResult>;
  verifyTmux(): Promise<TmuxWslStepResult>;
}

Важно:

  • 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

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<TmuxStatus> {
    // 1. detect host tmux
    // 2. detect install capability
    // 3. on Windows also detect WSL readiness
    // 4. return unified snapshot
  }

  async install(): Promise<void> {
    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<void> {
    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

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

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<WindowsElevatedStepResult> {
    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

export async function resolveLinuxPackageManager(): Promise<TmuxInstallStrategy> {
  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

async function getWslStatus(): Promise<TmuxWslStatus> {
  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.
  • <SelectedDistro> 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\\<distro>\\... 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 для ручной установки