107 KiB
План: 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 installLinux- автодетект native package manager, установка черезsudoв PTY, честный step-based progressWindows- не притворяться native installer'ом, а сделать честныйWSL wizard+ проверку/re-check каждого шага
Самый важный architectural gotcha:
⚠️ Сейчас Windows-путь в приложении вообще не использует tmux, даже если пользователь сам его поставит в WSL.
Это видно прямо в коде:
src/main/services/team/runtimeTeammateMode.tsжёстко отключает process/tmux path наwin32src/main/ipc/tmux.tsпроверяет только hosttmux, а не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 фичи
- source of truth для новой фичи -
- Переиспользовать существующие примитивы:
CliInstallerServiceкак референс по progress/event architecturePtyTerminalServiceкак база для 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, stable3.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:
- 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
Практический вывод для v1 Windows runtime follow-up:
- если идём через WSL tmux + Windows interop, prefer native Windows
.exebinary - не считать
.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_elevationwaiting_for_external_stepexternal_step_finishedexternal_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
- после добавления WSL-aware path нужен явный раздел
- Все platform-specific решения должны жить в main-process service layer, не в React
4. Текущее состояние кодовой базы
Feature-стандарт, который надо учитывать
- authoritative document для этой задачи -
docs/FEATURE_ARCHITECTURE_STANDARD.md - он задаёт canonical template для medium/large feature:
src/features/<feature>/contractssrc/features/<feature>/core/domainsrc/features/<feature>/core/applicationsrc/features/<feature>/main/compositionsrc/features/<feature>/main/adapters/inputsrc/features/<feature>/main/adapters/outputsrc/features/<feature>/main/infrastructuresrc/features/<feature>/preloadsrc/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работает только через hosttmux
- Windows-пропуски по
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.tssrc/features/tmux-installer/core/application/use-cases/InstallTmuxUseCase.tssrc/features/tmux-installer/core/application/use-cases/CancelTmuxInstallUseCase.tssrc/features/tmux-installer/core/application/use-cases/GetTmuxInstallerSnapshotUseCase.tssrc/features/tmux-installer/core/application/ports/TmuxStatusSourcePort.tssrc/features/tmux-installer/core/application/ports/TmuxInstallerRunnerPort.tssrc/features/tmux-installer/core/application/ports/TmuxInstallerSnapshotPort.tssrc/features/tmux-installer/main/composition/createTmuxInstallerFeature.tssrc/features/tmux-installer/main/adapters/input/ipc/registerTmuxInstallerIpc.tssrc/features/tmux-installer/main/adapters/output/presenters/TmuxInstallerProgressPresenter.tssrc/features/tmux-installer/main/adapters/output/sources/TmuxStatusSourceAdapter.tssrc/features/tmux-installer/main/adapters/output/runtime/TmuxInstallerRunnerAdapter.tssrc/features/tmux-installer/main/infrastructure/installer/TmuxInstallStrategyResolver.tssrc/features/tmux-installer/main/infrastructure/installer/TmuxCommandRunner.tssrc/features/tmux-installer/main/infrastructure/platform/TmuxPlatformResolver.tssrc/features/tmux-installer/main/infrastructure/platform/TmuxPackageManagerResolver.tssrc/features/tmux-installer/main/infrastructure/wsl/TmuxWslService.tssrc/features/tmux-installer/main/infrastructure/wsl/WindowsElevatedStepRunner.tssrc/features/tmux-installer/main/infrastructure/wsl/TmuxWslPathBridge.tssrc/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/
PtyTerminalServiceintegration- 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/domainside-effect freecore/applicationне знает про Electron/React/Zustand/child processesrenderer/uidumb 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- что доступно нативно в текущей ОС hostwsl- что доступно внутри WSL на Windowseffective- какой 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
- CTA:
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
- CTA:
Needs restart- CTA:
Re-check after restart
- CTA:
Completed- CTA:
Re-check
- CTA:
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
Стратегия
Порядок:
- Если
tmuxуже есть - успех, install не нужен - Если найден
brew- использовать Homebrew - Иначе если найден
port- использовать MacPorts - Иначе 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, потому что возможен пароль- если
brewinstall вдруг начинает требовать 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
Стратегия
Порядок:
- Если
tmuxуже есть - успех - Разобрать
/etc/os-release - Определить distro family
- Подтвердить наличие package manager binary
- Сформировать install command
- Выполнить через 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)
- Debian manpage:
dnf -y- DNF command reference:
-y, --assumeyesавтоматически отвечает yes на вопросы: DNF Command Reference
- DNF command reference:
zypper --non-interactive install- SUSE docs явно рекомендуют
--non-interactiveдо командыinstallдля scripted usage: SUSE zypper docs
- SUSE docs явно рекомендуют
pacman --noconfirm- Arch manual: bypasses confirmation prompts и предназначен для scripted usage;
--neededне переустанавливает уже актуальный target: pacman(8)
- Arch manual: bypasses confirmation prompts и предназначен для scripted usage;
Почему 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:
aptlockpacmanlock
- 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, но пользователь закрывает modalpacmankeyring / mirror init issuesdnfmetadata or repo config corruptionzypperregistration/repo state issues на SUSEyum/ enterprise repo family, гдеtmuxотсутствует в подключённых repozypperinteractive 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
tmuxdetection - 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-launchdocumented in Microsoft basic commands as "install distro but do not launch it automatically": Basic commands for WSL--web-downloaddocumented in Microsoft install guide как fallback, если install hangs at0.0%: Install WSL--no-distributiondocumented 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 для этого шага:
- app создаёт temp marker directory
- app запускает elevated helper через PowerShell
Start-Process -Verb RunAs -Wait - helper выполняет
wsl --install ... - helper пишет короткий JSON result file в temp directory
- app читает result file, но всё равно делает fresh probe через
wsl --status/wsl --list
Это лучше, чем надеяться на redirected stdout/stderr, который в RunAs flow ненадёжен.
Implementation note:
- helper лучше запускать как temp
.ps1file, а не как огромную 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-effortdistroVersion- это снижает зависимость от локализованных 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 - так мы не теряем
sudotimestamp и не заставляем пользователя вводить пароль дважды без причины
Optional future optimization:
- после явной проверки bootstrap-ready state можно отдельно исследовать
--user root - но это не должно быть основным путём v1
Шаг 6. Verification
wsl --distribution <SelectedDistro> -- sh -lc "tmux -V"
Но для надёжности этого мало.
Рекомендуемая verification ladder:
tmux -V- безопасный 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 --installpartially 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
- resolve
- собирать не один boolean, а нормальный
host / wsl / effectivesnapshot - не считать WSL 1 автоматическим fail, если smoke test на
tmuxпроходит
У нас уже есть полезные reference pieces в кодовой базе:
src/main/ipc/config.ts- candidate paths для
wsl.exe(System32,Sysnative, fallbackwsl.exe) - UTF-16-aware decoding output
listWslDistros()с несколькими command variants (--list --quiet,-l -q,-l)
- candidate paths для
Их нужно не дублировать 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 изменился
- какой
claudebinary 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, если
wslpathunavailable
Reference ideas уже есть в:
agent_teams_orchestrator/src/utils/idePathConversion.tssrc/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 на равенство путей должны быть нормализованы очень аккуратно
Дополнительный факт:
- Microsoft документирует
WSLENVкак bridge для env vars между Windows и WSL, включая path translation flags
Источник: Working across Windows and Linux file systems
Практический вывод:
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.exeinterop перестаёт корректно работать для дальнейших 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 прокидывать правильный
TMUXenv, указывающий на наш 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/.batwrappers нельзя считать автоматически эквивалентными- interop/runtime smoke test должен проверять именно этот path
- нужно жёстко prefer
Вариант 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 supportfull 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 должен по возможности запускать тот же
claudebinary model, который потом реально пойдёт в teammate runtime - тест уровня
tmux ... new-session -d trueполезен, но не доказывает, что interop launch path для реального CLI тоже живой - если binary model это host
claude.exefrom 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-sessiondisplay-message -pset-environment -g- если Windows follow-up использует это -
new-session -e
Иными словами:
tmuxversion 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.tssrc/features/tmux-installer/contracts/channels.tssrc/features/tmux-installer/contracts/dto.tssrc/features/tmux-installer/contracts/index.tssrc/features/tmux-installer/main/index.tssrc/features/tmux-installer/preload/createTmuxInstallerBridge.tssrc/features/tmux-installer/preload/index.tssrc/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.tssrc/features/tmux-installer/main/composition/createTmuxInstallerFeature.tssrc/features/tmux-installer/main/adapters/input/ipc/registerTmuxInstallerIpc.tssrc/features/tmux-installer/main/adapters/output/sources/TmuxStatusSourceAdapter.tssrc/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
- app shell main bootstrap, который импортирует только
Что делаем:
- выносим 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
updateis needed, prefer doing it in the same PTY session
Phase 6. Renderer slice inside feature
Новые файлы:
src/features/tmux-installer/renderer/adapters/TmuxInstallerBannerAdapter.tssrc/features/tmux-installer/renderer/hooks/useTmuxInstallerBanner.tssrc/features/tmux-installer/renderer/ui/TmuxInstallerBannerView.tsxsrc/features/tmux-installer/renderer/utils/formatTmuxInstallerText.tssrc/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 tmuxbutton- 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/domainside-effect freecore/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_succeededelevated_cancelledelevated_failedelevated_unknown_outcome
Ключевое правило:
- даже
elevated_succeededне завершает installer flow само по себе - после него всегда идёт fresh probe реального system state
Phase 9. Windows runtime follow-up
Это можно вынести в отдельную PR/итерацию, но в этом документе оно считается обязательным follow-up.
Файлы:
src/main/services/team/runtimeTeammateMode.tssrc/main/ipc/tmux.tssrc/main/services/team/TeamProvisioningService.tssrc/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_INTEROPpinning strategy при создании isolated tmux server - ввести explicit policy для supported
claudebinary 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 сценарий с
.cmdwrapper или 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 --listprobe после шага
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.execandidate - запускает с
encoding: buffer - затем декодирует stdout/stderr defensively, потому что
wsl.exeoutput на 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
Плохой текст:
ENOENTspawn failedexit 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
claudebinary-shape policy - locale-robust package-manager error classification
- config/auth-root sanity probe for Windows
claude.exefrom 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 --statusunsupported 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
.cmdonly -> runtime stays conservative, no false ready - host
claude.exelaunches from WSL but config/auth root behaves unexpectedly -> runtime stays not ready tmux -Vsucceeds 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 до полной реализации.
-
Windows elevated helper UX
- 🎯 8 🛡️ 8 🧠 5
100-200строк spike- Нужно проверить на реальной машине, как стабильно работает
Start-Process -Verb RunAs -Wait+ temp result file и какие коды/сообщения приходят при UAC cancel.
-
WSL distro install path under corporate restrictions
- 🎯 7 🛡️ 7 🧠 4
50-120строк spike- Нужно подтвердить, когда
wsl --install -d ...реально помогает, а когда сразу надо переводить пользователя в manual Microsoft docs.
-
PTY attach model для installer UI
- 🎯 8 🛡️ 9 🧠 6
150-300строк spike- Нужно быстро выбрать: расширяем существующий
PtyTerminalServiceили делаем небольшой отдельный installer-session adapter.
-
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.
-
Preferred distro persistence semantics
- 🎯 8 🛡️ 8 🧠 5
80-180строк spike- Нужно подтвердить, где и как лучше хранить
preferred WSL distro, чтобы не получить ложные переключения при смене default distro и при этом не создать лишнюю сложность миграции настроек.
-
Windows
claudebinary model through WSL interop- 🎯 7 🛡️ 8 🧠 6
120-260строк spike- Нужно подтвердить на реальной машине:
claude.exepath,.cmdfallback behavior, quoting, cleanup и минимальный smoke test именно через тот binary shape, который потом пойдёт в teammate runtime.
-
Config/auth root sanity for
claude.exelaunched 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/aptLinux - не хардкодить PATH без shell env merge
- не запускать Windows WSL runtime без path translation layer
- не поднимать Windows tmux server в WSL без явного
WSL_INTEROPpinning 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
Делать именно так:
- Сначала качественный
tmux-installerfeature slice дляmacOS/Linux - Сразу же заложить правильный shared contract под Windows WSL
- На Windows не останавливаться на "installer wizard only"
- Обязательно follow-up на WSL-aware runtime support, иначе ценность Windows-части будет неполной
Если делать по этому плану, получится действительно качественно:
- удобно
- понятно
- с нормальной диагностикой
- без дешёвых UX-обманок
- с честным fallback для ручной установки