Download src/utils/swarm/backends/PaneBackendExecutor.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/chenbhao/codev/resolve/main/src/utils/swarm/backends/PaneBackendExecutor.ts
- Command line
-
hf download hf://chenbhao/codev/src/utils/swarm/backends/PaneBackendExecutor.ts
-
curl -L -o PaneBackendExecutor.ts https://huggingface.co/chenbhao/codev/resolve/main/src/utils/swarm/backends/PaneBackendExecutor.ts
10.9 kB
| import { 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) | |
| } | |