import { getIsNonInteractiveSession } from '../../../bootstrap/state.js' import { logForDebugging } from '../../../utils/debug.js' import { getPlatform } from '../../../utils/platform.js' import { isInITerm2, isInsideTmux, isInsideTmuxSync, isIt2CliAvailable, isTmuxAvailable, } from './detection.js' import { createInProcessBackend } from './InProcessBackend.js' import { getPreferTmuxOverIterm2 } from './it2Setup.js' import { createPaneBackendExecutor } from './PaneBackendExecutor.js' import { getTeammateModeFromSnapshot } from './teammateModeSnapshot.js' import type { BackendDetectionResult, PaneBackend, PaneBackendType, TeammateExecutor, } from './types.js' /** * Cached backend detection result. * Once detected, the backend selection is fixed for the lifetime of the process. */ let cachedBackend: PaneBackend | null = null /** * Cached detection result with additional metadata. */ let cachedDetectionResult: BackendDetectionResult | null = null /** * Flag to track if backends have been registered. */ let backendsRegistered = false /** * Cached in-process backend instance. */ let cachedInProcessBackend: TeammateExecutor | null = null /** * Cached pane backend executor instance. * Wraps the detected PaneBackend to provide TeammateExecutor interface. */ let cachedPaneBackendExecutor: TeammateExecutor | null = null /** * Tracks whether spawn fell back to in-process mode because no pane backend * was available (e.g., iTerm2 without it2 or tmux installed). Once set, * isInProcessEnabled() returns true so UI (banner, teams menu) reflects reality. */ let inProcessFallbackActive = false /** * Placeholder for TmuxBackend - will be replaced with actual implementation. * This allows the registry to compile before the backend implementations exist. */ let TmuxBackendClass: (new () => PaneBackend) | null = null /** * Placeholder for ITermBackend - will be replaced with actual implementation. * This allows the registry to compile before the backend implementations exist. */ let ITermBackendClass: (new () => PaneBackend) | null = null /** * Ensures backend classes are dynamically imported so getBackendByType() can * construct them. Unlike detectAndGetBackend(), this never spawns subprocesses * and never throws — it's the lightweight option when you only need class * registration (e.g., killing a pane by its stored backendType). */ export async function ensureBackendsRegistered(): Promise { if (backendsRegistered) return await import('./TmuxBackend.js') await import('./ITermBackend.js') backendsRegistered = true } /** * Registers the TmuxBackend class with the registry. * Called by TmuxBackend.ts to avoid circular dependencies. */ export function registerTmuxBackend(backendClass: new () => PaneBackend): void { TmuxBackendClass = backendClass } /** * Registers the ITermBackend class with the registry. * Called by ITermBackend.ts to avoid circular dependencies. */ export function registerITermBackend( backendClass: new () => PaneBackend, ): void { logForDebugging( `[registry] registerITermBackend called, class=${backendClass?.name || 'undefined'}`, ) ITermBackendClass = backendClass } /** * Creates a TmuxBackend instance. * Throws if TmuxBackend hasn't been registered. */ function createTmuxBackend(): PaneBackend { if (!TmuxBackendClass) { throw new Error( 'TmuxBackend not registered. Import TmuxBackend.ts before using the registry.', ) } return new TmuxBackendClass() } /** * Creates an ITermBackend instance. * Throws if ITermBackend hasn't been registered. */ function createITermBackend(): PaneBackend { if (!ITermBackendClass) { throw new Error( 'ITermBackend not registered. Import ITermBackend.ts before using the registry.', ) } return new ITermBackendClass() } /** * Detection priority flow: * 1. If inside tmux, always use tmux (even in iTerm2) * 2. If in iTerm2 with it2 available, use iTerm2 backend * 3. If in iTerm2 without it2, return result indicating setup needed * 4. If tmux available, use tmux (creates external session) * 5. Otherwise, throw error with instructions */ export async function detectAndGetBackend(): Promise { // Ensure backends are registered before detection await ensureBackendsRegistered() // Return cached result if available if (cachedDetectionResult) { logForDebugging( `[BackendRegistry] Using cached backend: ${cachedDetectionResult.backend.type}`, ) return cachedDetectionResult } logForDebugging('[BackendRegistry] Starting backend detection...') // Check all environment conditions upfront for logging const insideTmux = await isInsideTmux() const inITerm2 = isInITerm2() logForDebugging( `[BackendRegistry] Environment: insideTmux=${insideTmux}, inITerm2=${inITerm2}`, ) // Priority 1: If inside tmux, always use tmux if (insideTmux) { logForDebugging( '[BackendRegistry] Selected: tmux (running inside tmux session)', ) const backend = createTmuxBackend() cachedBackend = backend cachedDetectionResult = { backend, isNative: true, needsIt2Setup: false, } return cachedDetectionResult } // Priority 2: If in iTerm2, try to use native panes if (inITerm2) { // Check if user previously chose to prefer tmux over iTerm2 const preferTmux = getPreferTmuxOverIterm2() if (preferTmux) { logForDebugging( '[BackendRegistry] User prefers tmux over iTerm2, skipping iTerm2 detection', ) } else { const it2Available = await isIt2CliAvailable() logForDebugging( `[BackendRegistry] iTerm2 detected, it2 CLI available: ${it2Available}`, ) if (it2Available) { logForDebugging( '[BackendRegistry] Selected: iterm2 (native iTerm2 with it2 CLI)', ) const backend = createITermBackend() cachedBackend = backend cachedDetectionResult = { backend, isNative: true, needsIt2Setup: false, } return cachedDetectionResult } } // In iTerm2 but it2 not available - check if tmux can be used as fallback const tmuxAvailable = await isTmuxAvailable() logForDebugging( `[BackendRegistry] it2 not available, tmux available: ${tmuxAvailable}`, ) if (tmuxAvailable) { logForDebugging( '[BackendRegistry] Selected: tmux (fallback in iTerm2, it2 setup recommended)', ) // Return tmux as fallback. Only signal it2 setup if the user hasn't already // chosen to prefer tmux - otherwise they'd be re-prompted on every spawn. const backend = createTmuxBackend() cachedBackend = backend cachedDetectionResult = { backend, isNative: false, needsIt2Setup: !preferTmux, } return cachedDetectionResult } // In iTerm2 with no it2 and no tmux - it2 setup is required logForDebugging( '[BackendRegistry] ERROR: iTerm2 detected but no it2 CLI and no tmux', ) throw new Error( 'iTerm2 detected but it2 CLI not installed. Install it2 with: pip install it2', ) } // Priority 3: Fall back to tmux external session const tmuxAvailable = await isTmuxAvailable() logForDebugging( `[BackendRegistry] Not in tmux or iTerm2, tmux available: ${tmuxAvailable}`, ) if (tmuxAvailable) { logForDebugging('[BackendRegistry] Selected: tmux (external session mode)') const backend = createTmuxBackend() cachedBackend = backend cachedDetectionResult = { backend, isNative: false, needsIt2Setup: false, } return cachedDetectionResult } // No backend available - tmux is not installed logForDebugging('[BackendRegistry] ERROR: No pane backend available') throw new Error(getTmuxInstallInstructions()) } /** * Returns platform-specific tmux installation instructions. */ function getTmuxInstallInstructions(): string { const platform = getPlatform() switch (platform) { case 'macos': return `To use agent swarms, install tmux: brew install tmux Then start a tmux session with: tmux new-session -s claude` case 'linux': case 'wsl': return `To use agent swarms, install tmux: sudo apt install tmux # Ubuntu/Debian sudo dnf install tmux # Fedora/RHEL Then start a tmux session with: tmux new-session -s claude` case 'windows': return `To use agent swarms, you need tmux which requires WSL (Windows Subsystem for Linux). Install WSL first, then inside WSL run: sudo apt install tmux Then start a tmux session with: tmux new-session -s claude` default: return `To use agent swarms, install tmux using your system's package manager. Then start a tmux session with: tmux new-session -s claude` } } /** * Gets a backend by explicit type selection. * Useful for testing or when the user has a preference. * * @param type - The backend type to get * @returns The requested backend instance * @throws If the requested backend type is not available */ export function getBackendByType(type: PaneBackendType): PaneBackend { switch (type) { case 'tmux': return createTmuxBackend() case 'iterm2': return createITermBackend() } } /** * Gets the currently cached backend, if any. * Returns null if no backend has been detected yet. */ export function getCachedBackend(): PaneBackend | null { return cachedBackend } /** * Gets the cached backend detection result, if any. * Returns null if detection hasn't run yet. * Use `isNative` to check if teammates are visible in native panes. */ export function getCachedDetectionResult(): BackendDetectionResult | null { return cachedDetectionResult } /** * Records that spawn fell back to in-process mode because no pane backend * was available. After this, isInProcessEnabled() returns true and subsequent * spawns short-circuit to in-process (the environment won't change mid-session). */ export function markInProcessFallback(): void { logForDebugging('[BackendRegistry] Marking in-process fallback as active') inProcessFallbackActive = true } /** * Gets the teammate mode for this session. * Returns the session snapshot captured at startup, ignoring runtime config changes. */ function getTeammateMode(): 'auto' | 'tmux' | 'in-process' { return getTeammateModeFromSnapshot() } /** * Checks if in-process teammate execution is enabled. * * Logic: * - If teammateMode is 'in-process', always enabled * - If teammateMode is 'tmux', always disabled (use pane backend) * - If teammateMode is 'auto' (default), check environment: * - If inside tmux, use pane backend (return false) * - If inside iTerm2, use pane backend (return false) - detectAndGetBackend() * will pick ITermBackend if it2 is available, or fall back to tmux * - Otherwise, use in-process (return true) */ export function isInProcessEnabled(): boolean { // Force in-process mode for non-interactive sessions (-p mode) // since tmux-based teammates don't make sense without a terminal UI if (getIsNonInteractiveSession()) { logForDebugging( '[BackendRegistry] isInProcessEnabled: true (non-interactive session)', ) return true } const mode = getTeammateMode() let enabled: boolean if (mode === 'in-process') { enabled = true } else if (mode === 'tmux') { enabled = false } else { // 'auto' mode - if a prior spawn fell back to in-process because no pane // backend was available, stay in-process (scoped to auto mode only so a // mid-session Settings change to explicit 'tmux' still takes effect). if (inProcessFallbackActive) { logForDebugging( '[BackendRegistry] isInProcessEnabled: true (fallback after pane backend unavailable)', ) return true } // Check if a pane backend environment is available // If inside tmux or iTerm2, use pane backend; otherwise use in-process const insideTmux = isInsideTmuxSync() const inITerm2 = isInITerm2() enabled = !insideTmux && !inITerm2 } logForDebugging( `[BackendRegistry] isInProcessEnabled: ${enabled} (mode=${mode}, insideTmux=${isInsideTmuxSync()}, inITerm2=${isInITerm2()})`, ) return enabled } /** * Returns the resolved teammate executor mode for this session. * Unlike getTeammateModeFromSnapshot which may return 'auto', this returns * what 'auto' actually resolves to given the current environment. */ export function getResolvedTeammateMode(): 'in-process' | 'tmux' { return isInProcessEnabled() ? 'in-process' : 'tmux' } /** * Gets the InProcessBackend instance. * Creates and caches the instance on first call. */ export function getInProcessBackend(): TeammateExecutor { if (!cachedInProcessBackend) { cachedInProcessBackend = createInProcessBackend() } return cachedInProcessBackend } /** * Gets a TeammateExecutor for spawning teammates. * * Returns either: * - InProcessBackend when preferInProcess is true and in-process mode is enabled * - PaneBackendExecutor wrapping the detected pane backend otherwise * * This provides a unified TeammateExecutor interface regardless of execution mode, * allowing callers to spawn and manage teammates without knowing the backend details. * * @param preferInProcess - If true and in-process is enabled, returns InProcessBackend. * Otherwise returns PaneBackendExecutor. * @returns TeammateExecutor instance */ export async function getTeammateExecutor( preferInProcess: boolean = false, ): Promise { if (preferInProcess && isInProcessEnabled()) { logForDebugging('[BackendRegistry] Using in-process executor') return getInProcessBackend() } // Return pane backend executor logForDebugging('[BackendRegistry] Using pane backend executor') return getPaneBackendExecutor() } /** * Gets the PaneBackendExecutor instance. * Creates and caches the instance on first call, detecting the appropriate pane backend. */ async function getPaneBackendExecutor(): Promise { if (!cachedPaneBackendExecutor) { const detection = await detectAndGetBackend() cachedPaneBackendExecutor = createPaneBackendExecutor(detection.backend) logForDebugging( `[BackendRegistry] Created PaneBackendExecutor wrapping ${detection.backend.type}`, ) } return cachedPaneBackendExecutor } /** * Resets the backend detection cache. * Used for testing to allow re-detection. */ export function resetBackendDetection(): void { cachedBackend = null cachedDetectionResult = null cachedInProcessBackend = null cachedPaneBackendExecutor = null backendsRegistered = false inProcessFallbackActive = false }