Merge pull request #1835 from code-yeongyu/fix/issue-1781-tmux-pane-width

fix(tmux): thread agent_pane_min_width config through pane management
This commit is contained in:
YeonGyu-Kim 2026-02-14 15:01:21 +09:00 committed by GitHub
commit affefee12f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 67 additions and 12 deletions

View File

@ -351,4 +351,47 @@ describe("calculateCapacity", () => {
expect(capacity.rows).toBe(4) expect(capacity.rows).toBe(4)
expect(capacity.total).toBe(12) 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)
})
}) })

View File

@ -1,10 +1,10 @@
import { MIN_PANE_HEIGHT, MIN_PANE_WIDTH } from "./types"
import type { TmuxPaneInfo } from "./types" import type { TmuxPaneInfo } from "./types"
import { import {
DIVIDER_SIZE, DIVIDER_SIZE,
MAIN_PANE_RATIO, MAIN_PANE_RATIO,
MAX_GRID_SIZE, MAX_GRID_SIZE,
} from "./tmux-grid-constants" } from "./tmux-grid-constants"
import { MIN_PANE_HEIGHT, MIN_PANE_WIDTH } from "./types"
export interface GridCapacity { export interface GridCapacity {
cols: number cols: number
@ -27,6 +27,7 @@ export interface GridPlan {
export function calculateCapacity( export function calculateCapacity(
windowWidth: number, windowWidth: number,
windowHeight: number, windowHeight: number,
minPaneWidth: number = MIN_PANE_WIDTH,
): GridCapacity { ): GridCapacity {
const availableWidth = Math.floor(windowWidth * (1 - MAIN_PANE_RATIO)) const availableWidth = Math.floor(windowWidth * (1 - MAIN_PANE_RATIO))
const cols = Math.min( const cols = Math.min(
@ -34,7 +35,7 @@ export function calculateCapacity(
Math.max( Math.max(
0, 0,
Math.floor( Math.floor(
(availableWidth + DIVIDER_SIZE) / (MIN_PANE_WIDTH + DIVIDER_SIZE), (availableWidth + DIVIDER_SIZE) / (minPaneWidth + DIVIDER_SIZE),
), ),
), ),
) )

View File

@ -1,3 +1,4 @@
import { MIN_PANE_HEIGHT, MIN_PANE_WIDTH } from "./types"
import type { SplitDirection, TmuxPaneInfo } from "./types" import type { SplitDirection, TmuxPaneInfo } from "./types"
import { import {
DIVIDER_SIZE, DIVIDER_SIZE,
@ -7,6 +8,10 @@ import {
MIN_SPLIT_WIDTH, MIN_SPLIT_WIDTH,
} from "./tmux-grid-constants" } from "./tmux-grid-constants"
function minSplitWidthFor(minPaneWidth: number): number {
return 2 * minPaneWidth + DIVIDER_SIZE
}
export function getColumnCount(paneCount: number): number { export function getColumnCount(paneCount: number): number {
if (paneCount <= 0) return 1 if (paneCount <= 0) return 1
return Math.min(MAX_COLS, Math.max(1, Math.ceil(paneCount / MAX_ROWS))) 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( export function isSplittableAtCount(
agentAreaWidth: number, agentAreaWidth: number,
paneCount: number, paneCount: number,
minPaneWidth: number = MIN_PANE_WIDTH,
): boolean { ): boolean {
const columnWidth = getColumnWidth(agentAreaWidth, paneCount) const columnWidth = getColumnWidth(agentAreaWidth, paneCount)
return columnWidth >= MIN_SPLIT_WIDTH return columnWidth >= minSplitWidthFor(minPaneWidth)
} }
export function findMinimalEvictions( export function findMinimalEvictions(
agentAreaWidth: number, agentAreaWidth: number,
currentCount: number, currentCount: number,
minPaneWidth: number = MIN_PANE_WIDTH,
): number | null { ): number | null {
for (let k = 1; k <= currentCount; k++) { for (let k = 1; k <= currentCount; k++) {
if (isSplittableAtCount(agentAreaWidth, currentCount - k)) { if (isSplittableAtCount(agentAreaWidth, currentCount - k, minPaneWidth)) {
return k return k
} }
} }
return null 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") { if (direction === "-h") {
return pane.width >= MIN_SPLIT_WIDTH return pane.width >= minSplitWidthFor(minPaneWidth)
} }
return pane.height >= MIN_SPLIT_HEIGHT return pane.height >= MIN_SPLIT_HEIGHT
} }

View File

@ -13,23 +13,23 @@ import {
} from "./pane-split-availability" } from "./pane-split-availability"
import { findSpawnTarget } from "./spawn-target-finder" import { findSpawnTarget } from "./spawn-target-finder"
import { findOldestAgentPane, type SessionMapping } from "./oldest-agent-pane" import { findOldestAgentPane, type SessionMapping } from "./oldest-agent-pane"
import { MIN_PANE_WIDTH } from "./types"
export function decideSpawnActions( export function decideSpawnActions(
state: WindowState, state: WindowState,
sessionId: string, sessionId: string,
description: string, description: string,
_config: CapacityConfig, config: CapacityConfig,
sessionMappings: SessionMapping[], sessionMappings: SessionMapping[],
): SpawnDecision { ): SpawnDecision {
if (!state.mainPane) { if (!state.mainPane) {
return { canSpawn: false, actions: [], reason: "no main pane found" } return { canSpawn: false, actions: [], reason: "no main pane found" }
} }
const minPaneWidth = config.agentPaneWidth
const agentAreaWidth = Math.floor(state.windowWidth * (1 - MAIN_PANE_RATIO)) const agentAreaWidth = Math.floor(state.windowWidth * (1 - MAIN_PANE_RATIO))
const currentCount = state.agentPanes.length const currentCount = state.agentPanes.length
if (agentAreaWidth < MIN_PANE_WIDTH) { if (agentAreaWidth < minPaneWidth) {
return { return {
canSpawn: false, canSpawn: false,
actions: [], actions: [],
@ -44,7 +44,7 @@ export function decideSpawnActions(
if (currentCount === 0) { if (currentCount === 0) {
const virtualMainPane: TmuxPaneInfo = { ...state.mainPane, width: state.windowWidth } const virtualMainPane: TmuxPaneInfo = { ...state.mainPane, width: state.windowWidth }
if (canSplitPane(virtualMainPane, "-h")) { if (canSplitPane(virtualMainPane, "-h", minPaneWidth)) {
return { return {
canSpawn: true, canSpawn: true,
actions: [ actions: [
@ -61,7 +61,7 @@ export function decideSpawnActions(
return { canSpawn: false, actions: [], reason: "mainPane too small to split" } return { canSpawn: false, actions: [], reason: "mainPane too small to split" }
} }
if (isSplittableAtCount(agentAreaWidth, currentCount)) { if (isSplittableAtCount(agentAreaWidth, currentCount, minPaneWidth)) {
const spawnTarget = findSpawnTarget(state) const spawnTarget = findSpawnTarget(state)
if (spawnTarget) { if (spawnTarget) {
return { return {
@ -79,7 +79,7 @@ export function decideSpawnActions(
} }
} }
const minEvictions = findMinimalEvictions(agentAreaWidth, currentCount) const minEvictions = findMinimalEvictions(agentAreaWidth, currentCount, minPaneWidth)
if (minEvictions === 1 && oldestPane) { if (minEvictions === 1 && oldestPane) {
return { return {
canSpawn: true, canSpawn: true,