diff --git a/src/features/tmux-subagent/decision-engine.test.ts b/src/features/tmux-subagent/decision-engine.test.ts index b514d555..c0fa2ccc 100644 --- a/src/features/tmux-subagent/decision-engine.test.ts +++ b/src/features/tmux-subagent/decision-engine.test.ts @@ -351,4 +351,47 @@ describe("calculateCapacity", () => { expect(capacity.rows).toBe(4) expect(capacity.total).toBe(12) }) + + it("#given a smaller minPaneWidth #when calculating capacity #then fits more columns", () => { + //#given + const smallMinWidth = 30 + + //#when + const defaultCapacity = calculateCapacity(212, 44) + const customCapacity = calculateCapacity(212, 44, smallMinWidth) + + //#then + expect(customCapacity.cols).toBeGreaterThanOrEqual(defaultCapacity.cols) + }) +}) + +describe("decideSpawnActions with custom agentPaneWidth", () => { + const createWindowState = ( + windowWidth: number, + windowHeight: number, + agentPanes: Array<{ paneId: string; width: number; height: number; left: number; top: number }> = [] + ): WindowState => ({ + windowWidth, + windowHeight, + mainPane: { paneId: "%0", width: Math.floor(windowWidth / 2), height: windowHeight, left: 0, top: 0, title: "main", isActive: true }, + agentPanes: agentPanes.map((p, i) => ({ + ...p, + title: `agent-${i}`, + isActive: false, + })), + }) + + it("#given a smaller agentPaneWidth #when window would be too small for default #then spawns with custom config", () => { + //#given + const smallConfig: CapacityConfig = { mainPaneMinWidth: 120, agentPaneWidth: 25 } + const state = createWindowState(100, 30) + + //#when + const defaultResult = decideSpawnActions(state, "ses1", "test", { mainPaneMinWidth: 120, agentPaneWidth: 52 }, []) + const customResult = decideSpawnActions(state, "ses1", "test", smallConfig, []) + + //#then + expect(defaultResult.canSpawn).toBe(false) + expect(customResult.canSpawn).toBe(true) + }) }) diff --git a/src/features/tmux-subagent/grid-planning.ts b/src/features/tmux-subagent/grid-planning.ts index 9e0fcb91..037b14bc 100644 --- a/src/features/tmux-subagent/grid-planning.ts +++ b/src/features/tmux-subagent/grid-planning.ts @@ -1,10 +1,10 @@ +import { MIN_PANE_HEIGHT, MIN_PANE_WIDTH } from "./types" import type { TmuxPaneInfo } from "./types" import { DIVIDER_SIZE, MAIN_PANE_RATIO, MAX_GRID_SIZE, } from "./tmux-grid-constants" -import { MIN_PANE_HEIGHT, MIN_PANE_WIDTH } from "./types" export interface GridCapacity { cols: number @@ -27,6 +27,7 @@ export interface GridPlan { export function calculateCapacity( windowWidth: number, windowHeight: number, + minPaneWidth: number = MIN_PANE_WIDTH, ): GridCapacity { const availableWidth = Math.floor(windowWidth * (1 - MAIN_PANE_RATIO)) const cols = Math.min( @@ -34,7 +35,7 @@ export function calculateCapacity( Math.max( 0, Math.floor( - (availableWidth + DIVIDER_SIZE) / (MIN_PANE_WIDTH + DIVIDER_SIZE), + (availableWidth + DIVIDER_SIZE) / (minPaneWidth + DIVIDER_SIZE), ), ), ) diff --git a/src/features/tmux-subagent/pane-split-availability.ts b/src/features/tmux-subagent/pane-split-availability.ts index fd9d34ec..65f85247 100644 --- a/src/features/tmux-subagent/pane-split-availability.ts +++ b/src/features/tmux-subagent/pane-split-availability.ts @@ -1,3 +1,4 @@ +import { MIN_PANE_HEIGHT, MIN_PANE_WIDTH } from "./types" import type { SplitDirection, TmuxPaneInfo } from "./types" import { DIVIDER_SIZE, @@ -7,6 +8,10 @@ import { MIN_SPLIT_WIDTH, } from "./tmux-grid-constants" +function minSplitWidthFor(minPaneWidth: number): number { + return 2 * minPaneWidth + DIVIDER_SIZE +} + export function getColumnCount(paneCount: number): number { if (paneCount <= 0) return 1 return Math.min(MAX_COLS, Math.max(1, Math.ceil(paneCount / MAX_ROWS))) @@ -21,26 +26,32 @@ export function getColumnWidth(agentAreaWidth: number, paneCount: number): numbe export function isSplittableAtCount( agentAreaWidth: number, paneCount: number, + minPaneWidth: number = MIN_PANE_WIDTH, ): boolean { const columnWidth = getColumnWidth(agentAreaWidth, paneCount) - return columnWidth >= MIN_SPLIT_WIDTH + return columnWidth >= minSplitWidthFor(minPaneWidth) } export function findMinimalEvictions( agentAreaWidth: number, currentCount: number, + minPaneWidth: number = MIN_PANE_WIDTH, ): number | null { for (let k = 1; k <= currentCount; k++) { - if (isSplittableAtCount(agentAreaWidth, currentCount - k)) { + if (isSplittableAtCount(agentAreaWidth, currentCount - k, minPaneWidth)) { return k } } return null } -export function canSplitPane(pane: TmuxPaneInfo, direction: SplitDirection): boolean { +export function canSplitPane( + pane: TmuxPaneInfo, + direction: SplitDirection, + minPaneWidth: number = MIN_PANE_WIDTH, +): boolean { if (direction === "-h") { - return pane.width >= MIN_SPLIT_WIDTH + return pane.width >= minSplitWidthFor(minPaneWidth) } return pane.height >= MIN_SPLIT_HEIGHT } diff --git a/src/features/tmux-subagent/spawn-action-decider.ts b/src/features/tmux-subagent/spawn-action-decider.ts index 1a279b65..9a2f71ff 100644 --- a/src/features/tmux-subagent/spawn-action-decider.ts +++ b/src/features/tmux-subagent/spawn-action-decider.ts @@ -13,23 +13,23 @@ import { } from "./pane-split-availability" import { findSpawnTarget } from "./spawn-target-finder" import { findOldestAgentPane, type SessionMapping } from "./oldest-agent-pane" -import { MIN_PANE_WIDTH } from "./types" export function decideSpawnActions( state: WindowState, sessionId: string, description: string, - _config: CapacityConfig, + 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 < MIN_PANE_WIDTH) { + if (agentAreaWidth < minPaneWidth) { return { canSpawn: false, actions: [], @@ -44,7 +44,7 @@ export function decideSpawnActions( if (currentCount === 0) { const virtualMainPane: TmuxPaneInfo = { ...state.mainPane, width: state.windowWidth } - if (canSplitPane(virtualMainPane, "-h")) { + if (canSplitPane(virtualMainPane, "-h", minPaneWidth)) { return { canSpawn: true, actions: [ @@ -61,7 +61,7 @@ export function decideSpawnActions( return { canSpawn: false, actions: [], reason: "mainPane too small to split" } } - if (isSplittableAtCount(agentAreaWidth, currentCount)) { + if (isSplittableAtCount(agentAreaWidth, currentCount, minPaneWidth)) { const spawnTarget = findSpawnTarget(state) if (spawnTarget) { return { @@ -79,7 +79,7 @@ export function decideSpawnActions( } } - const minEvictions = findMinimalEvictions(agentAreaWidth, currentCount) + const minEvictions = findMinimalEvictions(agentAreaWidth, currentCount, minPaneWidth) if (minEvictions === 1 && oldestPane) { return { canSpawn: true,