@@ -31,11 +31,38 @@ export interface SpawnTarget {
3131}
3232
3333const MAIN_PANE_RATIO = 0.5
34+ const MAX_COLS = 2
35+ const MAX_ROWS = 3
3436const MAX_GRID_SIZE = 4
3537const DIVIDER_SIZE = 1
3638const MIN_SPLIT_WIDTH = 2 * MIN_PANE_WIDTH + DIVIDER_SIZE
3739const MIN_SPLIT_HEIGHT = 2 * MIN_PANE_HEIGHT + DIVIDER_SIZE
3840
41+ export function getColumnCount ( paneCount : number ) : number {
42+ if ( paneCount <= 0 ) return 1
43+ return Math . min ( MAX_COLS , Math . max ( 1 , Math . ceil ( paneCount / MAX_ROWS ) ) )
44+ }
45+
46+ export function getColumnWidth ( agentAreaWidth : number , paneCount : number ) : number {
47+ const cols = getColumnCount ( paneCount )
48+ const dividersWidth = ( cols - 1 ) * DIVIDER_SIZE
49+ return Math . floor ( ( agentAreaWidth - dividersWidth ) / cols )
50+ }
51+
52+ export function isSplittableAtCount ( agentAreaWidth : number , paneCount : number ) : boolean {
53+ const columnWidth = getColumnWidth ( agentAreaWidth , paneCount )
54+ return columnWidth >= MIN_SPLIT_WIDTH
55+ }
56+
57+ export function findMinimalEvictions ( agentAreaWidth : number , currentCount : number ) : number | null {
58+ for ( let k = 1 ; k <= currentCount ; k ++ ) {
59+ if ( isSplittableAtCount ( agentAreaWidth , currentCount - k ) ) {
60+ return k
61+ }
62+ }
63+ return null
64+ }
65+
3966export function canSplitPane ( pane : TmuxPaneInfo , direction : SplitDirection ) : boolean {
4067 if ( direction === "-h" ) {
4168 return pane . width >= MIN_SPLIT_WIDTH
@@ -251,62 +278,96 @@ export function decideSpawnActions(
251278 return { canSpawn : false , actions : [ ] , reason : "no main pane found" }
252279 }
253280
254- const capacity = calculateCapacity ( state . windowWidth , state . windowHeight )
255-
256- if ( capacity . total === 0 ) {
281+ const agentAreaWidth = Math . floor ( state . windowWidth * ( 1 - MAIN_PANE_RATIO ) )
282+ const currentCount = state . agentPanes . length
283+
284+ if ( agentAreaWidth < MIN_PANE_WIDTH ) {
257285 return {
258286 canSpawn : false ,
259287 actions : [ ] ,
260288 reason : `window too small for agent panes: ${ state . windowWidth } x${ state . windowHeight } ` ,
261289 }
262290 }
263291
264- let currentState = state
265- const closeActions : PaneAction [ ] = [ ]
266- const maxIterations = state . agentPanes . length + 1
292+ const oldestPane = findOldestAgentPane ( state . agentPanes , sessionMappings )
293+ const oldestMapping = oldestPane
294+ ? sessionMappings . find ( m => m . paneId === oldestPane . paneId )
295+ : null
267296
268- for ( let i = 0 ; i < maxIterations ; i ++ ) {
269- const spawnTarget = findSplittableTarget ( currentState )
270-
271- if ( spawnTarget ) {
297+ if ( currentCount === 0 ) {
298+ const virtualMainPane : TmuxPaneInfo = { ...state . mainPane , width : state . windowWidth }
299+ if ( canSplitPane ( virtualMainPane , "-h" ) ) {
272300 return {
273301 canSpawn : true ,
274- actions : [
275- ...closeActions ,
276- {
277- type : "spawn" ,
278- sessionId,
279- description,
280- targetPaneId : spawnTarget . targetPaneId ,
281- splitDirection : spawnTarget . splitDirection
282- }
283- ] ,
284- reason : closeActions . length > 0 ? `closed ${ closeActions . length } pane(s) to make room` : undefined ,
302+ actions : [ {
303+ type : "spawn" ,
304+ sessionId,
305+ description,
306+ targetPaneId : state . mainPane . paneId ,
307+ splitDirection : "-h"
308+ } ]
285309 }
286310 }
311+ return { canSpawn : false , actions : [ ] , reason : "mainPane too small to split" }
312+ }
287313
288- const oldestPane = findOldestAgentPane ( currentState . agentPanes , sessionMappings )
289- if ( ! oldestPane ) {
290- break
314+ if ( isSplittableAtCount ( agentAreaWidth , currentCount ) ) {
315+ const spawnTarget = findSplittableTarget ( state )
316+ if ( spawnTarget ) {
317+ return {
318+ canSpawn : true ,
319+ actions : [ {
320+ type : "spawn" ,
321+ sessionId,
322+ description,
323+ targetPaneId : spawnTarget . targetPaneId ,
324+ splitDirection : spawnTarget . splitDirection
325+ } ]
326+ }
291327 }
328+ }
292329
293- const mappingForPane = sessionMappings . find ( m => m . paneId === oldestPane . paneId )
294- closeActions . push ( {
295- type : "close" ,
296- paneId : oldestPane . paneId ,
297- sessionId : mappingForPane ?. sessionId || ""
298- } )
330+ const minEvictions = findMinimalEvictions ( agentAreaWidth , currentCount )
299331
300- currentState = {
301- ...currentState ,
302- agentPanes : currentState . agentPanes . filter ( p => p . paneId !== oldestPane . paneId )
332+ if ( minEvictions === 1 && oldestPane ) {
333+ return {
334+ canSpawn : true ,
335+ actions : [
336+ {
337+ type : "close" ,
338+ paneId : oldestPane . paneId ,
339+ sessionId : oldestMapping ?. sessionId || ""
340+ } ,
341+ {
342+ type : "spawn" ,
343+ sessionId,
344+ description,
345+ targetPaneId : state . mainPane . paneId ,
346+ splitDirection : "-h"
347+ }
348+ ] ,
349+ reason : "closed 1 pane to make room for split"
350+ }
351+ }
352+
353+ if ( oldestPane ) {
354+ return {
355+ canSpawn : true ,
356+ actions : [ {
357+ type : "replace" ,
358+ paneId : oldestPane . paneId ,
359+ oldSessionId : oldestMapping ?. sessionId || "" ,
360+ newSessionId : sessionId ,
361+ description
362+ } ] ,
363+ reason : "replaced oldest pane (no split possible)"
303364 }
304365 }
305366
306367 return {
307368 canSpawn : false ,
308369 actions : [ ] ,
309- reason : "no splittable pane found even after closing all agent panes" ,
370+ reason : "no pane available to replace"
310371 }
311372}
312373
0 commit comments