Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
11 kB · 354 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355import { getSessionId } from '../../../bootstrap/state.js'import type { ToolUseContext } from '../../../Tool.js'import { formatAgentId, parseAgentId } from '../../../utils/agentId.js'import { quote } from '../../../utils/bash/shellQuote.js'import { registerCleanup } from '../../../utils/cleanupRegistry.js'import { logForDebugging } from '../../../utils/debug.js'import { jsonStringify } from '../../../utils/slowOperations.js'import { writeToMailbox } from '../../../utils/teammateMailbox.js'import { buildInheritedCliFlags, buildInheritedEnvVars, getTeammateCommand,} from '../spawnUtils.js'import { assignTeammateColor } from '../teammateLayoutManager.js'import { isInsideTmux } from './detection.js'import type { BackendType, PaneBackend, TeammateExecutor, TeammateMessage, TeammateSpawnConfig, TeammateSpawnResult,} from './types.js'
/** * PaneBackendExecutor adapts a PaneBackend to the TeammateExecutor interface. * * This allows pane-based backends (tmux, iTerm2) to be used through the same * TeammateExecutor abstraction as InProcessBackend, making getTeammateExecutor() * return a meaningful executor regardless of execution mode. * * The adapter handles: * - spawn(): Creates a pane and sends the Claude CLI command to it * - sendMessage(): Writes to the teammate's file-based mailbox * - terminate(): Sends a shutdown request via mailbox * - kill(): Kills the pane via the backend * - isActive(): Checks if the pane is still running */export class PaneBackendExecutor implements TeammateExecutor { readonly type: BackendType
private backend: PaneBackend private context: ToolUseContext | null = null
/** * Track spawned teammates by agentId -> paneId mapping. * This allows us to find the pane for operations like kill/terminate. */ private spawnedTeammates: Map<string, { paneId: string; insideTmux: boolean }> private cleanupRegistered = false
constructor(backend: PaneBackend) { this.backend = backend this.type = backend.type this.spawnedTeammates = new Map() }
/** * Sets the ToolUseContext for this executor. * Must be called before spawn() to provide access to AppState and permissions. */ setContext(context: ToolUseContext): void { this.context = context }
/** * Checks if the underlying pane backend is available. */ async isAvailable(): Promise<boolean> { return this.backend.isAvailable() }
/** * Spawns a teammate in a new pane. * * Creates a pane via the backend, builds the CLI command with teammate * identity flags, and sends it to the pane. */ async spawn(config: TeammateSpawnConfig): Promise<TeammateSpawnResult> { const agentId = formatAgentId(config.name, config.teamName)
if (!this.context) { logForDebugging( `[PaneBackendExecutor] spawn() called without context for ${config.name}`, ) return { success: false, agentId, error: 'PaneBackendExecutor not initialized. Call setContext() before spawn().', } }
try { // Assign a unique color to this teammate const teammateColor = config.color ?? assignTeammateColor(agentId)
// Create a pane in the swarm view const { paneId, isFirstTeammate } = await this.backend.createTeammatePaneInSwarmView( config.name, teammateColor, )
// Check if we're inside tmux to determine how to send commands const insideTmux = await isInsideTmux()
// Enable pane border status on first teammate when inside tmux if (isFirstTeammate && insideTmux) { await this.backend.enablePaneBorderStatus() }
// Build the command to spawn Claude Code with teammate identity const binaryPath = getTeammateCommand()
// Build teammate identity CLI args const teammateArgs = [ `--agent-id ${quote([agentId])}`, `--agent-name ${quote([config.name])}`, `--team-name ${quote([config.teamName])}`, `--agent-color ${quote([teammateColor])}`, `--parent-session-id ${quote([config.parentSessionId || getSessionId()])}`, config.planModeRequired ? '--plan-mode-required' : '', ] .filter(Boolean) .join(' ')
// Build CLI flags to propagate to teammate const appState = this.context.getAppState() let inheritedFlags = buildInheritedCliFlags({ planModeRequired: config.planModeRequired, permissionMode: appState.toolPermissionContext.mode, })
// If teammate has a custom model, add --model flag (or replace inherited one) if (config.model) { inheritedFlags = inheritedFlags .split(' ') .filter( (flag, i, arr) => flag !== '--model' && arr[i - 1] !== '--model', ) .join(' ') inheritedFlags = inheritedFlags ? `${inheritedFlags} --model ${quote([config.model])}` : `--model ${quote([config.model])}` }
const flagsStr = inheritedFlags ? ` ${inheritedFlags}` : '' const workingDir = config.cwd
// Build environment variables to forward to teammate const envStr = buildInheritedEnvVars()
const spawnCommand = `cd ${quote([workingDir])} && env ${envStr} ${quote([binaryPath])} ${teammateArgs}${flagsStr}`
// Send the command to the new pane // Use swarm socket when running outside tmux (external swarm session) await this.backend.sendCommandToPane(paneId, spawnCommand, !insideTmux)
// Track the spawned teammate this.spawnedTeammates.set(agentId, { paneId, insideTmux })
// Register cleanup to kill all panes on leader exit (e.g., SIGHUP) if (!this.cleanupRegistered) { this.cleanupRegistered = true registerCleanup(async () => { for (const [id, info] of this.spawnedTeammates) { logForDebugging( `[PaneBackendExecutor] Cleanup: killing pane for ${id}`, ) await this.backend.killPane(info.paneId, !info.insideTmux) } this.spawnedTeammates.clear() }) }
// Send initial instructions to teammate via mailbox await writeToMailbox( config.name, { from: 'team-lead', text: config.prompt, timestamp: new Date().toISOString(), }, config.teamName, )
logForDebugging( `[PaneBackendExecutor] Spawned teammate ${agentId} in pane ${paneId}`, )
return { success: true, agentId, paneId, } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) logForDebugging( `[PaneBackendExecutor] Failed to spawn ${agentId}: ${errorMessage}`, ) return { success: false, agentId, error: errorMessage, } } }
/** * Sends a message to a pane-based teammate via file-based mailbox. * * All teammates (pane and in-process) use the same mailbox mechanism. */ async sendMessage(agentId: string, message: TeammateMessage): Promise<void> { logForDebugging( `[PaneBackendExecutor] sendMessage() to ${agentId}: ${message.text.substring(0, 50)}...`, )
const parsed = parseAgentId(agentId) if (!parsed) { throw new Error( `Invalid agentId format: ${agentId}. Expected format: agentName@teamName`, ) }
const { agentName, teamName } = parsed
await writeToMailbox( agentName, { text: message.text, from: message.from, color: message.color, timestamp: message.timestamp ?? new Date().toISOString(), }, teamName, )
logForDebugging( `[PaneBackendExecutor] sendMessage() completed for ${agentId}`, ) }
/** * Gracefully terminates a pane-based teammate. * * For pane-based teammates, we send a shutdown request via mailbox and * let the teammate process handle exit gracefully. */ async terminate(agentId: string, reason?: string): Promise<boolean> { logForDebugging( `[PaneBackendExecutor] terminate() called for ${agentId}: ${reason}`, )
const parsed = parseAgentId(agentId) if (!parsed) { logForDebugging( `[PaneBackendExecutor] terminate() failed: invalid agentId format`, ) return false }
const { agentName, teamName } = parsed
// Send shutdown request via mailbox const shutdownRequest = { type: 'shutdown_request', requestId: `shutdown-${agentId}-${Date.now()}`, from: 'team-lead', reason, }
await writeToMailbox( agentName, { from: 'team-lead', text: jsonStringify(shutdownRequest), timestamp: new Date().toISOString(), }, teamName, )
logForDebugging( `[PaneBackendExecutor] terminate() sent shutdown request to ${agentId}`, )
return true }
/** * Force kills a pane-based teammate by killing its pane. */ async kill(agentId: string): Promise<boolean> { logForDebugging(`[PaneBackendExecutor] kill() called for ${agentId}`)
const teammateInfo = this.spawnedTeammates.get(agentId) if (!teammateInfo) { logForDebugging( `[PaneBackendExecutor] kill() failed: teammate ${agentId} not found in spawned map`, ) return false }
const { paneId, insideTmux } = teammateInfo
// Kill the pane via the backend // Use external session socket when we spawned outside tmux const killed = await this.backend.killPane(paneId, !insideTmux)
if (killed) { this.spawnedTeammates.delete(agentId) logForDebugging(`[PaneBackendExecutor] kill() succeeded for ${agentId}`) } else { logForDebugging(`[PaneBackendExecutor] kill() failed for ${agentId}`) }
return killed }
/** * Checks if a pane-based teammate is still active. * * For pane-based teammates, we check if the pane still exists. * This is a best-effort check - the pane may exist but the process inside * may have exited. */ async isActive(agentId: string): Promise<boolean> { logForDebugging(`[PaneBackendExecutor] isActive() called for ${agentId}`)
const teammateInfo = this.spawnedTeammates.get(agentId) if (!teammateInfo) { logForDebugging( `[PaneBackendExecutor] isActive(): teammate ${agentId} not found`, ) return false }
// For now, assume active if we have a record of it // A more robust check would query the backend for pane existence // but that would require adding a new method to PaneBackend return true }}
/** * Creates a PaneBackendExecutor wrapping the given PaneBackend. */export function createPaneBackendExecutor( backend: PaneBackend,): PaneBackendExecutor { return new PaneBackendExecutor(backend)}