π File detail
utils/swarm/backends/PaneBackendExecutor.ts
π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes PaneBackendExecutor and createPaneBackendExecutor β mainly functions, hooks, or classes. It composes internal code from bootstrap, Tool, utils, spawnUtils, and teammateLayoutManager (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
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'
π€ Exports (heuristic)
PaneBackendExecutorcreatePaneBackendExecutor
π₯οΈ Source preview
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)
}