Files
oh-my-opencode/src/features/tmux-subagent/spawn-action-decider.ts
T
YeonGyu-Kim 121a3c45c5 fix(tmux): thread agent_pane_min_width config through pane management
The agent_pane_min_width config value was accepted in the schema and
passed as CapacityConfig.agentPaneWidth but never actually used — the
underscore-prefixed _config parameter in decideSpawnActions was unused,
and all split/capacity calculations used the hardcoded MIN_PANE_WIDTH.

Now decideSpawnActions, canSplitPane, isSplittableAtCount,
findMinimalEvictions, and calculateCapacity all accept and use the
configured minimum pane width, falling back to the default (52) when
not provided.

Closes #1781
2026-02-14 14:58:07 +09:00

136 lines
3.3 KiB
TypeScript

import type {
CapacityConfig,
PaneAction,
SpawnDecision,
TmuxPaneInfo,
WindowState,
} from "./types"
import { MAIN_PANE_RATIO } from "./tmux-grid-constants"
import {
canSplitPane,
findMinimalEvictions,
isSplittableAtCount,
} from "./pane-split-availability"
import { findSpawnTarget } from "./spawn-target-finder"
import { findOldestAgentPane, type SessionMapping } from "./oldest-agent-pane"
export function decideSpawnActions(
state: WindowState,
sessionId: string,
description: string,
config: CapacityConfig,
sessionMappings: SessionMapping[],
): SpawnDecision {
if (!state.mainPane) {
return { canSpawn: false, actions: [], reason: "no main pane found" }
}
const minPaneWidth = config.agentPaneWidth
const agentAreaWidth = Math.floor(state.windowWidth * (1 - MAIN_PANE_RATIO))
const currentCount = state.agentPanes.length
if (agentAreaWidth < minPaneWidth) {
return {
canSpawn: false,
actions: [],
reason: `window too small for agent panes: ${state.windowWidth}x${state.windowHeight}`,
}
}
const oldestPane = findOldestAgentPane(state.agentPanes, sessionMappings)
const oldestMapping = oldestPane
? sessionMappings.find((m) => m.paneId === oldestPane.paneId) ?? null
: null
if (currentCount === 0) {
const virtualMainPane: TmuxPaneInfo = { ...state.mainPane, width: state.windowWidth }
if (canSplitPane(virtualMainPane, "-h", minPaneWidth)) {
return {
canSpawn: true,
actions: [
{
type: "spawn",
sessionId,
description,
targetPaneId: state.mainPane.paneId,
splitDirection: "-h",
},
],
}
}
return { canSpawn: false, actions: [], reason: "mainPane too small to split" }
}
if (isSplittableAtCount(agentAreaWidth, currentCount, minPaneWidth)) {
const spawnTarget = findSpawnTarget(state)
if (spawnTarget) {
return {
canSpawn: true,
actions: [
{
type: "spawn",
sessionId,
description,
targetPaneId: spawnTarget.targetPaneId,
splitDirection: spawnTarget.splitDirection,
},
],
}
}
}
const minEvictions = findMinimalEvictions(agentAreaWidth, currentCount, minPaneWidth)
if (minEvictions === 1 && oldestPane) {
return {
canSpawn: true,
actions: [
{
type: "close",
paneId: oldestPane.paneId,
sessionId: oldestMapping?.sessionId || "",
},
{
type: "spawn",
sessionId,
description,
targetPaneId: state.mainPane.paneId,
splitDirection: "-h",
},
],
reason: "closed 1 pane to make room for split",
}
}
if (oldestPane) {
return {
canSpawn: true,
actions: [
{
type: "replace",
paneId: oldestPane.paneId,
oldSessionId: oldestMapping?.sessionId || "",
newSessionId: sessionId,
description,
},
],
reason: "replaced oldest pane (no split possible)",
}
}
return { canSpawn: false, actions: [], reason: "no pane available to replace" }
}
export function decideCloseAction(
state: WindowState,
sessionId: string,
sessionMappings: SessionMapping[],
): PaneAction | null {
const mapping = sessionMappings.find((m) => m.sessionId === sessionId)
if (!mapping) return null
const paneExists = state.agentPanes.some((pane) => pane.paneId === mapping.paneId)
if (!paneExists) return null
return { type: "close", paneId: mapping.paneId, sessionId }
}