Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
13 kB · 370 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371import type { AgentColorName } from '../../../tools/AgentTool/agentColorManager.js'import { logForDebugging } from '../../../utils/debug.js'import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'import { IT2_COMMAND, isInITerm2, isIt2CliAvailable } from './detection.js'import { registerITermBackend } from './registry.js'import type { CreatePaneResult, PaneBackend, PaneId } from './types.js'
// Track session IDs for teammatesconst teammateSessionIds: string[] = []
// Track whether the first pane has been usedlet firstPaneUsed = false
// Lock mechanism to prevent race conditions when spawning teammates in parallellet paneCreationLock: Promise<void> = Promise.resolve()
/** * Acquires a lock for pane creation, ensuring sequential execution. * Returns a release function that must be called when done. */function acquirePaneCreationLock(): Promise<() => void> { let release: () => void const newLock = new Promise<void>(resolve => { release = resolve })
const previousLock = paneCreationLock paneCreationLock = newLock
return previousLock.then(() => release!)}
/** * Runs an it2 CLI command and returns the result. */function runIt2( args: string[],): Promise<{ stdout: string; stderr: string; code: number }> { return execFileNoThrow(IT2_COMMAND, args)}
/** * Parses the session ID from `it2 session split` output. * Format: "Created new pane: <session-id>" * * NOTE: This UUID is only valid when splitting from a specific session * using the -s flag. When splitting from the "active" session, the UUID * may not be accessible if the split happened in a different window. */function parseSplitOutput(output: string): string { const match = output.match(/Created new pane:\s*(.+)/) if (match && match[1]) { return match[1].trim() } return ''}
/** * Gets the leader's session ID from ITERM_SESSION_ID env var. * Format: "wXtYpZ:UUID" - we extract the UUID part after the colon. * Returns null if not in iTerm2 or env var not set. */function getLeaderSessionId(): string | null { const itermSessionId = process.env.ITERM_SESSION_ID if (!itermSessionId) { return null } const colonIndex = itermSessionId.indexOf(':') if (colonIndex === -1) { return null } return itermSessionId.slice(colonIndex + 1)}
/** * ITermBackend implements pane management using iTerm2's native split panes * via the it2 CLI tool. */export class ITermBackend implements PaneBackend { readonly type = 'iterm2' as const readonly displayName = 'iTerm2' readonly supportsHideShow = false
/** * Checks if iTerm2 backend is available (in iTerm2 with it2 CLI installed). */ async isAvailable(): Promise<boolean> { const inITerm2 = isInITerm2() logForDebugging(`[ITermBackend] isAvailable check: inITerm2=${inITerm2}`) if (!inITerm2) { logForDebugging('[ITermBackend] isAvailable: false (not in iTerm2)') return false } const it2Available = await isIt2CliAvailable() logForDebugging( `[ITermBackend] isAvailable: ${it2Available} (it2 CLI ${it2Available ? 'found' : 'not found'})`, ) return it2Available }
/** * Checks if we're currently running inside iTerm2. */ async isRunningInside(): Promise<boolean> { const result = isInITerm2() logForDebugging(`[ITermBackend] isRunningInside: ${result}`) return result }
/** * Creates a new teammate pane in the swarm view. * Uses a lock to prevent race conditions when multiple teammates are spawned in parallel. */ async createTeammatePaneInSwarmView( name: string, color: AgentColorName, ): Promise<CreatePaneResult> { logForDebugging( `[ITermBackend] createTeammatePaneInSwarmView called for ${name} with color ${color}`, ) const releaseLock = await acquirePaneCreationLock()
try { // Layout: Leader on left, teammates stacked vertically on the right // - First teammate: vertical split (-v) from leader's session // - Subsequent teammates: horizontal split from last teammate's session // // We explicitly target the session to split from using -s flag to ensure // correct layout even if user clicks on different panes. // // At-fault recovery: If a targeted teammate session is dead (user closed // the pane via Cmd+W / X, or process crashed), prune it and retry with // the next-to-last. Cheaper than a proactive 'it2 session list' on every spawn. // Bounded at O(N+1) iterations: each continue shrinks teammateSessionIds by 1; // when empty → firstPaneUsed resets → next iteration has no target → throws. // eslint-disable-next-line no-constant-condition while (true) { const isFirstTeammate = !firstPaneUsed logForDebugging( `[ITermBackend] Creating pane: isFirstTeammate=${isFirstTeammate}, existingPanes=${teammateSessionIds.length}`, )
let splitArgs: string[] let targetedTeammateId: string | undefined if (isFirstTeammate) { // Split from leader's session (extracted from ITERM_SESSION_ID env var) const leaderSessionId = getLeaderSessionId() if (leaderSessionId) { splitArgs = ['session', 'split', '-v', '-s', leaderSessionId] logForDebugging( `[ITermBackend] First split from leader session: ${leaderSessionId}`, ) } else { // Fallback to active session if we can't get leader's ID splitArgs = ['session', 'split', '-v'] logForDebugging( '[ITermBackend] First split from active session (no leader ID)', ) } } else { // Split from the last teammate's session to stack vertically targetedTeammateId = teammateSessionIds[teammateSessionIds.length - 1] if (targetedTeammateId) { splitArgs = ['session', 'split', '-s', targetedTeammateId] logForDebugging( `[ITermBackend] Subsequent split from teammate session: ${targetedTeammateId}`, ) } else { // Fallback to active session splitArgs = ['session', 'split'] logForDebugging( '[ITermBackend] Subsequent split from active session (no teammate ID)', ) } }
const splitResult = await runIt2(splitArgs)
if (splitResult.code !== 0) { // If we targeted a teammate session, confirm it's actually dead before // pruning — 'session list' distinguishes dead-target from systemic // failure (Python API off, it2 removed, transient socket error). // Pruning on systemic failure would drain all live IDs → state corrupted. if (targetedTeammateId) { const listResult = await runIt2(['session', 'list']) if ( listResult.code === 0 && !listResult.stdout.includes(targetedTeammateId) ) { // Confirmed dead — prune and retry with next-to-last (or leader). logForDebugging( `[ITermBackend] Split failed targeting dead session ${targetedTeammateId}, pruning and retrying: ${splitResult.stderr}`, ) const idx = teammateSessionIds.indexOf(targetedTeammateId) if (idx !== -1) { teammateSessionIds.splice(idx, 1) } if (teammateSessionIds.length === 0) { firstPaneUsed = false } continue } // Target is alive or we can't tell — don't corrupt state, surface the error. } throw new Error( `Failed to create iTerm2 split pane: ${splitResult.stderr}`, ) }
if (isFirstTeammate) { firstPaneUsed = true }
// Parse the session ID from split output // This works because we're splitting from a specific session (-s flag), // so the new pane is in the same window and the UUID is valid. const paneId = parseSplitOutput(splitResult.stdout)
if (!paneId) { throw new Error( `Failed to parse session ID from split output: ${splitResult.stdout}`, ) } logForDebugging( `[ITermBackend] Created teammate pane for ${name}: ${paneId}`, )
teammateSessionIds.push(paneId)
// Set pane color and title // Skip color and title for now - each it2 call is slow (Python process + API) // The pane is functional without these cosmetic features // TODO: Consider batching these or making them async/fire-and-forget
return { paneId, isFirstTeammate } } } finally { releaseLock() } }
/** * Sends a command to a specific pane. */ async sendCommandToPane( paneId: PaneId, command: string, _useExternalSession?: boolean, ): Promise<void> { // Use it2 session run to execute command (adds newline automatically) // Always use -s flag to target specific session - this ensures the command // goes to the right pane even if user switches windows const args = paneId ? ['session', 'run', '-s', paneId, command] : ['session', 'run', command]
const result = await runIt2(args)
if (result.code !== 0) { throw new Error( `Failed to send command to iTerm2 pane ${paneId}: ${result.stderr}`, ) } }
/** * No-op for iTerm2 - tab colors would require escape sequences but we skip * them for performance (each it2 call is slow). */ async setPaneBorderColor( _paneId: PaneId, _color: AgentColorName, _useExternalSession?: boolean, ): Promise<void> { // Skip for performance - each it2 call spawns a Python process }
/** * No-op for iTerm2 - titles would require escape sequences but we skip * them for performance (each it2 call is slow). */ async setPaneTitle( _paneId: PaneId, _name: string, _color: AgentColorName, _useExternalSession?: boolean, ): Promise<void> { // Skip for performance - each it2 call spawns a Python process }
/** * No-op for iTerm2 - pane titles are shown in tabs automatically. */ async enablePaneBorderStatus( _windowTarget?: string, _useExternalSession?: boolean, ): Promise<void> { // iTerm2 doesn't have the concept of pane border status like tmux // Titles are shown in tabs automatically }
/** * No-op for iTerm2 - pane balancing is handled automatically. */ async rebalancePanes( _windowTarget: string, _hasLeader: boolean, ): Promise<void> { // iTerm2 handles pane balancing automatically logForDebugging( '[ITermBackend] Pane rebalancing not implemented for iTerm2', ) }
/** * Kills/closes a specific pane using the it2 CLI. * Also removes the pane from tracked session IDs so subsequent spawns * don't try to split from a dead session. */ async killPane( paneId: PaneId, _useExternalSession?: boolean, ): Promise<boolean> { // -f (force) is required: without it, iTerm2 respects the "Confirm before // closing" preference and either shows a dialog or refuses when the session // still has a running process (the shell always is). tmux kill-pane has no // such prompt, which is why this was only broken for iTerm2. const result = await runIt2(['session', 'close', '-f', '-s', paneId]) // Clean up module state regardless of close result — even if the pane is // already gone (e.g., user closed it manually), removing the stale ID is correct. const idx = teammateSessionIds.indexOf(paneId) if (idx !== -1) { teammateSessionIds.splice(idx, 1) } if (teammateSessionIds.length === 0) { firstPaneUsed = false } return result.code === 0 }
/** * Stub for hiding a pane - not supported in iTerm2 backend. * iTerm2 doesn't have a direct equivalent to tmux's break-pane. */ async hidePane( _paneId: PaneId, _useExternalSession?: boolean, ): Promise<boolean> { logForDebugging('[ITermBackend] hidePane not supported in iTerm2') return false }
/** * Stub for showing a hidden pane - not supported in iTerm2 backend. * iTerm2 doesn't have a direct equivalent to tmux's join-pane. */ async showPane( _paneId: PaneId, _targetWindowOrPane: string, _useExternalSession?: boolean, ): Promise<boolean> { logForDebugging('[ITermBackend] showPane not supported in iTerm2') return false }}
// Register the backend with the registry when this module is imported.// This side effect is intentional - the registry needs backends to self-register to avoid circular dependencies.// eslint-disable-next-line custom-rules/no-top-level-side-effectsregisterITermBackend(ITermBackend)