| 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' |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export class PaneBackendExecutor implements TeammateExecutor { |
| readonly type: BackendType |
|
|
| private backend: PaneBackend |
| private context: ToolUseContext | null = null |
|
|
| |
| |
| |
| |
| 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() |
| } |
|
|
| |
| |
| |
| |
| setContext(context: ToolUseContext): void { |
| this.context = context |
| } |
|
|
| |
| |
| |
| async isAvailable(): Promise<boolean> { |
| return this.backend.isAvailable() |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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 { |
| |
| const teammateColor = config.color ?? assignTeammateColor(agentId) |
|
|
| |
| const { paneId, isFirstTeammate } = |
| await this.backend.createTeammatePaneInSwarmView( |
| config.name, |
| teammateColor, |
| ) |
|
|
| |
| const insideTmux = await isInsideTmux() |
|
|
| |
| if (isFirstTeammate && insideTmux) { |
| await this.backend.enablePaneBorderStatus() |
| } |
|
|
| |
| const binaryPath = getTeammateCommand() |
|
|
| |
| 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(' ') |
|
|
| |
| const appState = this.context.getAppState() |
| let inheritedFlags = buildInheritedCliFlags({ |
| planModeRequired: config.planModeRequired, |
| permissionMode: appState.toolPermissionContext.mode, |
| }) |
|
|
| |
| 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 |
|
|
| |
| const envStr = buildInheritedEnvVars() |
|
|
| const spawnCommand = `cd ${quote([workingDir])} && env ${envStr} ${quote([binaryPath])} ${teammateArgs}${flagsStr}` |
|
|
| |
| |
| await this.backend.sendCommandToPane(paneId, spawnCommand, !insideTmux) |
|
|
| |
| this.spawnedTeammates.set(agentId, { paneId, insideTmux }) |
|
|
| |
| 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() |
| }) |
| } |
|
|
| |
| 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, |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| 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}`, |
| ) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| return true |
| } |
| } |
|
|
| |
| |
| |
| export function createPaneBackendExecutor( |
| backend: PaneBackend, |
| ): PaneBackendExecutor { |
| return new PaneBackendExecutor(backend) |
| } |
|
|