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
This commit is contained in:
YeonGyu-Kim 2026-02-14 14:49:02 +09:00
parent daf011c616
commit 121a3c45c5
5 changed files with 81 additions and 26 deletions

View File

@ -28,13 +28,13 @@
"typescript": "^5.7.3", "typescript": "^5.7.3",
}, },
"optionalDependencies": { "optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.5.2", "oh-my-opencode-darwin-arm64": "3.5.3",
"oh-my-opencode-darwin-x64": "3.5.2", "oh-my-opencode-darwin-x64": "3.5.3",
"oh-my-opencode-linux-arm64": "3.5.2", "oh-my-opencode-linux-arm64": "3.5.3",
"oh-my-opencode-linux-arm64-musl": "3.5.2", "oh-my-opencode-linux-arm64-musl": "3.5.3",
"oh-my-opencode-linux-x64": "3.5.2", "oh-my-opencode-linux-x64": "3.5.3",
"oh-my-opencode-linux-x64-musl": "3.5.2", "oh-my-opencode-linux-x64-musl": "3.5.3",
"oh-my-opencode-windows-x64": "3.5.2", "oh-my-opencode-windows-x64": "3.5.3",
}, },
}, },
}, },
@ -226,19 +226,19 @@
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.2", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-oIS3lB2F9/N+3mF5wCKk6/EPVSz516XWN+mNdquSSeddw+xqMxGdhKY6K/XeYbHJzeN2Z8IOikNEJ6psR2/a8g=="], "oh-my-opencode-darwin-arm64": ["oh-my-opencode-darwin-arm64@3.5.3", "", { "os": "darwin", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Dq0+PC2dyAqG7c3DUnQmdOkKbKmOsRHwoqgLCQNKN1lTRllF8zbWqp5B+LGKxSPxPqJIPS3mKt+wIR2KvkYJVw=="],
"oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.2", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-OAdXo4ZCCYO4kRWtnyz3tdmaGYPUB3WcXimXAxp+/sEZxAnh7n1RQkpLn6UxWX4AIAdRT9dfrOfRic6VoCYv2g=="], "oh-my-opencode-darwin-x64": ["oh-my-opencode-darwin-x64@3.5.3", "", { "os": "darwin", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Ke45Bv/ygZm3YUSUumIyk647KZ2PFzw30tH597cOpG8MDPGbNVBCM6EKFezcukUPT+gPFVpE1IiGzEkn4JmgZA=="],
"oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-5XXNMFhp1VsyrGNRBoXcOyoaUeVkbrWkBRPDGZfpiq+kRXH3aaSWdR5G7Pl/TadOQv9Bl8/8YaxsuHRTFT1aXw=="], "oh-my-opencode-linux-arm64": ["oh-my-opencode-linux-arm64@3.5.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-aP5S3DngUhFkNeqYM33Ge6zccCWLzB/O3FLXLFXy/Iws03N8xugw72pnMK6lUbIia9QQBKK7IZBoYm9C79pZ3g=="],
"oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.2", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-/woIpqvEI85MgJvEVnz4g5FBLeiQNK7srRsueIFPBmtTahh42HFleCDaIltOl/ndjsE5nCHacQVJHkC9W9/F3Q=="], "oh-my-opencode-linux-arm64-musl": ["oh-my-opencode-linux-arm64-musl@3.5.3", "", { "os": "linux", "cpu": "arm64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-UiD/hVKYZQyX4D5N5SnZT4M5Z/B2SDtJWBW4MibpYSAcPKNCEBKi/5E4hOPxAtTfFGR8tIXFmYZdQJDkVfvluw=="],
"oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-vTL2A+6zzGhi+m7sC8peLDq5OAp2dRR0UEb4RbZAOHtlEruF7qFEmcK3ccWxwc3+Z3G/ITfwn5VNa72ZS4pNTg=="], "oh-my-opencode-linux-x64": ["oh-my-opencode-linux-x64@3.5.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-L9kqwzElGkaQ8pgtv1ZjcHARw9LPaU4UEVjzauByTMi+/5Js/PTsNXBggxSRzZfQ8/MNBPSCiA4K10Kc0YjjvA=="],
"oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.2", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-bOAA55snLsK2QB00IkQy8le0Oqh/GJ7pxEHtm1oUezlQrW/nX5SS/hJ7dPHMmOd9FoiqnqyqWZxNkLmFoG463A=="], "oh-my-opencode-linux-x64-musl": ["oh-my-opencode-linux-x64-musl@3.5.3", "", { "os": "linux", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode" } }, "sha512-Z0fVVih/b2dbNeb9DK9oca5dNYCZyPySBRtxRhDXod5d7fJNgIPrvUoEd3SNfkRGORyFB3hGBZ6nqQ6N8+8DEA=="],
"oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.2", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-fnHiAPYglw3unPckmQBoCT6+VqjSWCE3S3J551mRo0ZFrxuEP2ZKyHZeFMMOtKwDepCvmKgd1W040+KmuVUXOA=="], "oh-my-opencode-windows-x64": ["oh-my-opencode-windows-x64@3.5.3", "", { "os": "win32", "cpu": "x64", "bin": { "oh-my-opencode": "bin/oh-my-opencode.exe" } }, "sha512-ocWPjRs2sJgN02PJnEIYtqdMVDex1YhEj1FzAU5XIicfzQbgxLh9nz1yhHZzfqGJq69QStU6ofpc5kQpfX1LMg=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],

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,