source dump of claude code
at main 464 lines 15 kB view raw
1import { getIsNonInteractiveSession } from '../../../bootstrap/state.js' 2import { logForDebugging } from '../../../utils/debug.js' 3import { getPlatform } from '../../../utils/platform.js' 4import { 5 isInITerm2, 6 isInsideTmux, 7 isInsideTmuxSync, 8 isIt2CliAvailable, 9 isTmuxAvailable, 10} from './detection.js' 11import { createInProcessBackend } from './InProcessBackend.js' 12import { getPreferTmuxOverIterm2 } from './it2Setup.js' 13import { createPaneBackendExecutor } from './PaneBackendExecutor.js' 14import { getTeammateModeFromSnapshot } from './teammateModeSnapshot.js' 15import type { 16 BackendDetectionResult, 17 PaneBackend, 18 PaneBackendType, 19 TeammateExecutor, 20} from './types.js' 21 22/** 23 * Cached backend detection result. 24 * Once detected, the backend selection is fixed for the lifetime of the process. 25 */ 26let cachedBackend: PaneBackend | null = null 27 28/** 29 * Cached detection result with additional metadata. 30 */ 31let cachedDetectionResult: BackendDetectionResult | null = null 32 33/** 34 * Flag to track if backends have been registered. 35 */ 36let backendsRegistered = false 37 38/** 39 * Cached in-process backend instance. 40 */ 41let cachedInProcessBackend: TeammateExecutor | null = null 42 43/** 44 * Cached pane backend executor instance. 45 * Wraps the detected PaneBackend to provide TeammateExecutor interface. 46 */ 47let cachedPaneBackendExecutor: TeammateExecutor | null = null 48 49/** 50 * Tracks whether spawn fell back to in-process mode because no pane backend 51 * was available (e.g., iTerm2 without it2 or tmux installed). Once set, 52 * isInProcessEnabled() returns true so UI (banner, teams menu) reflects reality. 53 */ 54let inProcessFallbackActive = false 55 56/** 57 * Placeholder for TmuxBackend - will be replaced with actual implementation. 58 * This allows the registry to compile before the backend implementations exist. 59 */ 60let TmuxBackendClass: (new () => PaneBackend) | null = null 61 62/** 63 * Placeholder for ITermBackend - will be replaced with actual implementation. 64 * This allows the registry to compile before the backend implementations exist. 65 */ 66let ITermBackendClass: (new () => PaneBackend) | null = null 67 68/** 69 * Ensures backend classes are dynamically imported so getBackendByType() can 70 * construct them. Unlike detectAndGetBackend(), this never spawns subprocesses 71 * and never throws — it's the lightweight option when you only need class 72 * registration (e.g., killing a pane by its stored backendType). 73 */ 74export async function ensureBackendsRegistered(): Promise<void> { 75 if (backendsRegistered) return 76 await import('./TmuxBackend.js') 77 await import('./ITermBackend.js') 78 backendsRegistered = true 79} 80 81/** 82 * Registers the TmuxBackend class with the registry. 83 * Called by TmuxBackend.ts to avoid circular dependencies. 84 */ 85export function registerTmuxBackend(backendClass: new () => PaneBackend): void { 86 TmuxBackendClass = backendClass 87} 88 89/** 90 * Registers the ITermBackend class with the registry. 91 * Called by ITermBackend.ts to avoid circular dependencies. 92 */ 93export function registerITermBackend( 94 backendClass: new () => PaneBackend, 95): void { 96 logForDebugging( 97 `[registry] registerITermBackend called, class=${backendClass?.name || 'undefined'}`, 98 ) 99 ITermBackendClass = backendClass 100} 101 102/** 103 * Creates a TmuxBackend instance. 104 * Throws if TmuxBackend hasn't been registered. 105 */ 106function createTmuxBackend(): PaneBackend { 107 if (!TmuxBackendClass) { 108 throw new Error( 109 'TmuxBackend not registered. Import TmuxBackend.ts before using the registry.', 110 ) 111 } 112 return new TmuxBackendClass() 113} 114 115/** 116 * Creates an ITermBackend instance. 117 * Throws if ITermBackend hasn't been registered. 118 */ 119function createITermBackend(): PaneBackend { 120 if (!ITermBackendClass) { 121 throw new Error( 122 'ITermBackend not registered. Import ITermBackend.ts before using the registry.', 123 ) 124 } 125 return new ITermBackendClass() 126} 127 128/** 129 * Detection priority flow: 130 * 1. If inside tmux, always use tmux (even in iTerm2) 131 * 2. If in iTerm2 with it2 available, use iTerm2 backend 132 * 3. If in iTerm2 without it2, return result indicating setup needed 133 * 4. If tmux available, use tmux (creates external session) 134 * 5. Otherwise, throw error with instructions 135 */ 136export async function detectAndGetBackend(): Promise<BackendDetectionResult> { 137 // Ensure backends are registered before detection 138 await ensureBackendsRegistered() 139 140 // Return cached result if available 141 if (cachedDetectionResult) { 142 logForDebugging( 143 `[BackendRegistry] Using cached backend: ${cachedDetectionResult.backend.type}`, 144 ) 145 return cachedDetectionResult 146 } 147 148 logForDebugging('[BackendRegistry] Starting backend detection...') 149 150 // Check all environment conditions upfront for logging 151 const insideTmux = await isInsideTmux() 152 const inITerm2 = isInITerm2() 153 154 logForDebugging( 155 `[BackendRegistry] Environment: insideTmux=${insideTmux}, inITerm2=${inITerm2}`, 156 ) 157 158 // Priority 1: If inside tmux, always use tmux 159 if (insideTmux) { 160 logForDebugging( 161 '[BackendRegistry] Selected: tmux (running inside tmux session)', 162 ) 163 const backend = createTmuxBackend() 164 cachedBackend = backend 165 cachedDetectionResult = { 166 backend, 167 isNative: true, 168 needsIt2Setup: false, 169 } 170 return cachedDetectionResult 171 } 172 173 // Priority 2: If in iTerm2, try to use native panes 174 if (inITerm2) { 175 // Check if user previously chose to prefer tmux over iTerm2 176 const preferTmux = getPreferTmuxOverIterm2() 177 if (preferTmux) { 178 logForDebugging( 179 '[BackendRegistry] User prefers tmux over iTerm2, skipping iTerm2 detection', 180 ) 181 } else { 182 const it2Available = await isIt2CliAvailable() 183 logForDebugging( 184 `[BackendRegistry] iTerm2 detected, it2 CLI available: ${it2Available}`, 185 ) 186 187 if (it2Available) { 188 logForDebugging( 189 '[BackendRegistry] Selected: iterm2 (native iTerm2 with it2 CLI)', 190 ) 191 const backend = createITermBackend() 192 cachedBackend = backend 193 cachedDetectionResult = { 194 backend, 195 isNative: true, 196 needsIt2Setup: false, 197 } 198 return cachedDetectionResult 199 } 200 } 201 202 // In iTerm2 but it2 not available - check if tmux can be used as fallback 203 const tmuxAvailable = await isTmuxAvailable() 204 logForDebugging( 205 `[BackendRegistry] it2 not available, tmux available: ${tmuxAvailable}`, 206 ) 207 208 if (tmuxAvailable) { 209 logForDebugging( 210 '[BackendRegistry] Selected: tmux (fallback in iTerm2, it2 setup recommended)', 211 ) 212 // Return tmux as fallback. Only signal it2 setup if the user hasn't already 213 // chosen to prefer tmux - otherwise they'd be re-prompted on every spawn. 214 const backend = createTmuxBackend() 215 cachedBackend = backend 216 cachedDetectionResult = { 217 backend, 218 isNative: false, 219 needsIt2Setup: !preferTmux, 220 } 221 return cachedDetectionResult 222 } 223 224 // In iTerm2 with no it2 and no tmux - it2 setup is required 225 logForDebugging( 226 '[BackendRegistry] ERROR: iTerm2 detected but no it2 CLI and no tmux', 227 ) 228 throw new Error( 229 'iTerm2 detected but it2 CLI not installed. Install it2 with: pip install it2', 230 ) 231 } 232 233 // Priority 3: Fall back to tmux external session 234 const tmuxAvailable = await isTmuxAvailable() 235 logForDebugging( 236 `[BackendRegistry] Not in tmux or iTerm2, tmux available: ${tmuxAvailable}`, 237 ) 238 239 if (tmuxAvailable) { 240 logForDebugging('[BackendRegistry] Selected: tmux (external session mode)') 241 const backend = createTmuxBackend() 242 cachedBackend = backend 243 cachedDetectionResult = { 244 backend, 245 isNative: false, 246 needsIt2Setup: false, 247 } 248 return cachedDetectionResult 249 } 250 251 // No backend available - tmux is not installed 252 logForDebugging('[BackendRegistry] ERROR: No pane backend available') 253 throw new Error(getTmuxInstallInstructions()) 254} 255 256/** 257 * Returns platform-specific tmux installation instructions. 258 */ 259function getTmuxInstallInstructions(): string { 260 const platform = getPlatform() 261 262 switch (platform) { 263 case 'macos': 264 return `To use agent swarms, install tmux: 265 brew install tmux 266Then start a tmux session with: tmux new-session -s claude` 267 268 case 'linux': 269 case 'wsl': 270 return `To use agent swarms, install tmux: 271 sudo apt install tmux # Ubuntu/Debian 272 sudo dnf install tmux # Fedora/RHEL 273Then start a tmux session with: tmux new-session -s claude` 274 275 case 'windows': 276 return `To use agent swarms, you need tmux which requires WSL (Windows Subsystem for Linux). 277Install WSL first, then inside WSL run: 278 sudo apt install tmux 279Then start a tmux session with: tmux new-session -s claude` 280 281 default: 282 return `To use agent swarms, install tmux using your system's package manager. 283Then start a tmux session with: tmux new-session -s claude` 284 } 285} 286 287/** 288 * Gets a backend by explicit type selection. 289 * Useful for testing or when the user has a preference. 290 * 291 * @param type - The backend type to get 292 * @returns The requested backend instance 293 * @throws If the requested backend type is not available 294 */ 295export function getBackendByType(type: PaneBackendType): PaneBackend { 296 switch (type) { 297 case 'tmux': 298 return createTmuxBackend() 299 case 'iterm2': 300 return createITermBackend() 301 } 302} 303 304/** 305 * Gets the currently cached backend, if any. 306 * Returns null if no backend has been detected yet. 307 */ 308export function getCachedBackend(): PaneBackend | null { 309 return cachedBackend 310} 311 312/** 313 * Gets the cached backend detection result, if any. 314 * Returns null if detection hasn't run yet. 315 * Use `isNative` to check if teammates are visible in native panes. 316 */ 317export function getCachedDetectionResult(): BackendDetectionResult | null { 318 return cachedDetectionResult 319} 320 321/** 322 * Records that spawn fell back to in-process mode because no pane backend 323 * was available. After this, isInProcessEnabled() returns true and subsequent 324 * spawns short-circuit to in-process (the environment won't change mid-session). 325 */ 326export function markInProcessFallback(): void { 327 logForDebugging('[BackendRegistry] Marking in-process fallback as active') 328 inProcessFallbackActive = true 329} 330 331/** 332 * Gets the teammate mode for this session. 333 * Returns the session snapshot captured at startup, ignoring runtime config changes. 334 */ 335function getTeammateMode(): 'auto' | 'tmux' | 'in-process' { 336 return getTeammateModeFromSnapshot() 337} 338 339/** 340 * Checks if in-process teammate execution is enabled. 341 * 342 * Logic: 343 * - If teammateMode is 'in-process', always enabled 344 * - If teammateMode is 'tmux', always disabled (use pane backend) 345 * - If teammateMode is 'auto' (default), check environment: 346 * - If inside tmux, use pane backend (return false) 347 * - If inside iTerm2, use pane backend (return false) - detectAndGetBackend() 348 * will pick ITermBackend if it2 is available, or fall back to tmux 349 * - Otherwise, use in-process (return true) 350 */ 351export function isInProcessEnabled(): boolean { 352 // Force in-process mode for non-interactive sessions (-p mode) 353 // since tmux-based teammates don't make sense without a terminal UI 354 if (getIsNonInteractiveSession()) { 355 logForDebugging( 356 '[BackendRegistry] isInProcessEnabled: true (non-interactive session)', 357 ) 358 return true 359 } 360 361 const mode = getTeammateMode() 362 363 let enabled: boolean 364 if (mode === 'in-process') { 365 enabled = true 366 } else if (mode === 'tmux') { 367 enabled = false 368 } else { 369 // 'auto' mode - if a prior spawn fell back to in-process because no pane 370 // backend was available, stay in-process (scoped to auto mode only so a 371 // mid-session Settings change to explicit 'tmux' still takes effect). 372 if (inProcessFallbackActive) { 373 logForDebugging( 374 '[BackendRegistry] isInProcessEnabled: true (fallback after pane backend unavailable)', 375 ) 376 return true 377 } 378 // Check if a pane backend environment is available 379 // If inside tmux or iTerm2, use pane backend; otherwise use in-process 380 const insideTmux = isInsideTmuxSync() 381 const inITerm2 = isInITerm2() 382 enabled = !insideTmux && !inITerm2 383 } 384 385 logForDebugging( 386 `[BackendRegistry] isInProcessEnabled: ${enabled} (mode=${mode}, insideTmux=${isInsideTmuxSync()}, inITerm2=${isInITerm2()})`, 387 ) 388 return enabled 389} 390 391/** 392 * Returns the resolved teammate executor mode for this session. 393 * Unlike getTeammateModeFromSnapshot which may return 'auto', this returns 394 * what 'auto' actually resolves to given the current environment. 395 */ 396export function getResolvedTeammateMode(): 'in-process' | 'tmux' { 397 return isInProcessEnabled() ? 'in-process' : 'tmux' 398} 399 400/** 401 * Gets the InProcessBackend instance. 402 * Creates and caches the instance on first call. 403 */ 404export function getInProcessBackend(): TeammateExecutor { 405 if (!cachedInProcessBackend) { 406 cachedInProcessBackend = createInProcessBackend() 407 } 408 return cachedInProcessBackend 409} 410 411/** 412 * Gets a TeammateExecutor for spawning teammates. 413 * 414 * Returns either: 415 * - InProcessBackend when preferInProcess is true and in-process mode is enabled 416 * - PaneBackendExecutor wrapping the detected pane backend otherwise 417 * 418 * This provides a unified TeammateExecutor interface regardless of execution mode, 419 * allowing callers to spawn and manage teammates without knowing the backend details. 420 * 421 * @param preferInProcess - If true and in-process is enabled, returns InProcessBackend. 422 * Otherwise returns PaneBackendExecutor. 423 * @returns TeammateExecutor instance 424 */ 425export async function getTeammateExecutor( 426 preferInProcess: boolean = false, 427): Promise<TeammateExecutor> { 428 if (preferInProcess && isInProcessEnabled()) { 429 logForDebugging('[BackendRegistry] Using in-process executor') 430 return getInProcessBackend() 431 } 432 433 // Return pane backend executor 434 logForDebugging('[BackendRegistry] Using pane backend executor') 435 return getPaneBackendExecutor() 436} 437 438/** 439 * Gets the PaneBackendExecutor instance. 440 * Creates and caches the instance on first call, detecting the appropriate pane backend. 441 */ 442async function getPaneBackendExecutor(): Promise<TeammateExecutor> { 443 if (!cachedPaneBackendExecutor) { 444 const detection = await detectAndGetBackend() 445 cachedPaneBackendExecutor = createPaneBackendExecutor(detection.backend) 446 logForDebugging( 447 `[BackendRegistry] Created PaneBackendExecutor wrapping ${detection.backend.type}`, 448 ) 449 } 450 return cachedPaneBackendExecutor 451} 452 453/** 454 * Resets the backend detection cache. 455 * Used for testing to allow re-detection. 456 */ 457export function resetBackendDetection(): void { 458 cachedBackend = null 459 cachedDetectionResult = null 460 cachedInProcessBackend = null 461 cachedPaneBackendExecutor = null 462 backendsRegistered = false 463 inProcessFallbackActive = false 464}