diff --git a/bun.lock b/bun.lock index 0175f65a5..36c9e59dd 100644 --- a/bun.lock +++ b/bun.lock @@ -28,13 +28,13 @@ "typescript": "^5.7.3", }, "optionalDependencies": { - "oh-my-opencode-darwin-arm64": "3.5.2", - "oh-my-opencode-darwin-x64": "3.5.2", - "oh-my-opencode-linux-arm64": "3.5.2", - "oh-my-opencode-linux-arm64-musl": "3.5.2", - "oh-my-opencode-linux-x64": "3.5.2", - "oh-my-opencode-linux-x64-musl": "3.5.2", - "oh-my-opencode-windows-x64": "3.5.2", + "oh-my-opencode-darwin-arm64": "3.5.3", + "oh-my-opencode-darwin-x64": "3.5.3", + "oh-my-opencode-linux-arm64": "3.5.3", + "oh-my-opencode-linux-arm64-musl": "3.5.3", + "oh-my-opencode-linux-x64": "3.5.3", + "oh-my-opencode-linux-x64-musl": "3.5.3", + "oh-my-opencode-windows-x64": "3.5.3", }, }, }, @@ -226,19 +226,19 @@ "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=="], diff --git a/src/features/tmux-subagent/decision-engine.test.ts b/src/features/tmux-subagent/decision-engine.test.ts index b514d5556..c0fa2ccc4 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 9e0fcb91d..037b14bc1 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 fd9d34ec7..65f852474 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 1a279b65b..9a2f71ff5 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,