Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
15 kB · 464 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465import { 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<void> { 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<BackendDetectionResult> { // 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 tmuxThen 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/RHELThen 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 tmuxThen 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<TeammateExecutor> { 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<TeammateExecutor> { 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}