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:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user