fix(tmux): prefer split-or-defer with FIFO deferred attach

This commit is contained in:
liu-qingyuan
2026-02-15 03:26:39 +08:00
parent f3c8b0d098
commit 541f0d354d
14 changed files with 1156 additions and 227 deletions
@@ -1,13 +1,40 @@
import type { SplitDirection, TmuxPaneInfo, WindowState } from "./types"
import type { CapacityConfig, SplitDirection, TmuxPaneInfo, WindowState } from "./types"
import { computeMainPaneWidth } from "./tmux-grid-constants"
import { computeGridPlan, mapPaneToSlot } from "./grid-planning"
import { canSplitPane, getBestSplitDirection } from "./pane-split-availability"
import { MIN_PANE_WIDTH } from "./types"
import { canSplitPane } from "./pane-split-availability"
export interface SpawnTarget {
targetPaneId: string
splitDirection: SplitDirection
}
function isStrictMainVertical(config: CapacityConfig): boolean {
return config.layout === "main-vertical"
}
function isStrictMainHorizontal(config: CapacityConfig): boolean {
return config.layout === "main-horizontal"
}
function isStrictMainLayout(config: CapacityConfig): boolean {
return isStrictMainVertical(config) || isStrictMainHorizontal(config)
}
function getInitialSplitDirection(config: CapacityConfig): SplitDirection {
return isStrictMainHorizontal(config) ? "-v" : "-h"
}
function getStrictFollowupSplitDirection(config: CapacityConfig): SplitDirection {
return isStrictMainHorizontal(config) ? "-h" : "-v"
}
function sortPanesForStrictLayout(panes: TmuxPaneInfo[], config: CapacityConfig): TmuxPaneInfo[] {
if (isStrictMainHorizontal(config)) {
return [...panes].sort((a, b) => a.left - b.left || a.top - b.top)
}
return [...panes].sort((a, b) => a.top - b.top || a.left - b.left)
}
function buildOccupancy(
agentPanes: TmuxPaneInfo[],
plan: ReturnType<typeof computeGridPlan>,
@@ -37,16 +64,29 @@ function findFirstEmptySlot(
function findSplittableTarget(
state: WindowState,
minPaneWidth: number,
config: CapacityConfig,
_preferredDirection?: SplitDirection,
): SpawnTarget | null {
if (!state.mainPane) return null
const existingCount = state.agentPanes.length
const minAgentPaneWidth = config.agentPaneWidth
const initialDirection = getInitialSplitDirection(config)
if (existingCount === 0) {
const virtualMainPane: TmuxPaneInfo = { ...state.mainPane, width: state.windowWidth }
if (canSplitPane(virtualMainPane, "-h", minPaneWidth)) {
return { targetPaneId: state.mainPane.paneId, splitDirection: "-h" }
if (canSplitPane(virtualMainPane, initialDirection, minAgentPaneWidth)) {
return { targetPaneId: state.mainPane.paneId, splitDirection: initialDirection }
}
return null
}
if (isStrictMainLayout(config)) {
const followupDirection = getStrictFollowupSplitDirection(config)
const panesByPriority = sortPanesForStrictLayout(state.agentPanes, config)
for (const pane of panesByPriority) {
if (canSplitPane(pane, followupDirection, minAgentPaneWidth)) {
return { targetPaneId: pane.paneId, splitDirection: followupDirection }
}
}
return null
}
@@ -55,34 +95,44 @@ function findSplittableTarget(
state.windowWidth,
state.windowHeight,
existingCount + 1,
state.mainPane.width,
minPaneWidth,
config,
)
const mainPaneWidth = state.mainPane.width
const mainPaneWidth = computeMainPaneWidth(state.windowWidth, config)
const occupancy = buildOccupancy(state.agentPanes, plan, mainPaneWidth)
const targetSlot = findFirstEmptySlot(occupancy, plan)
const leftPane = occupancy.get(`${targetSlot.row}:${targetSlot.col - 1}`)
if (leftPane && canSplitPane(leftPane, "-h", minPaneWidth)) {
if (
!isStrictMainVertical(config) &&
leftPane &&
canSplitPane(leftPane, "-h", minAgentPaneWidth)
) {
return { targetPaneId: leftPane.paneId, splitDirection: "-h" }
}
const abovePane = occupancy.get(`${targetSlot.row - 1}:${targetSlot.col}`)
if (abovePane && canSplitPane(abovePane, "-v", minPaneWidth)) {
if (abovePane && canSplitPane(abovePane, "-v", minAgentPaneWidth)) {
return { targetPaneId: abovePane.paneId, splitDirection: "-v" }
}
const splittablePanes = state.agentPanes
.map((pane) => ({ pane, direction: getBestSplitDirection(pane, minPaneWidth) }))
.filter(
(item): item is { pane: TmuxPaneInfo; direction: SplitDirection } =>
item.direction !== null,
)
.sort((a, b) => b.pane.width * b.pane.height - a.pane.width * a.pane.height)
const panesByPosition = [...state.agentPanes].sort(
(a, b) => a.left - b.left || a.top - b.top,
)
const best = splittablePanes[0]
if (best) {
return { targetPaneId: best.pane.paneId, splitDirection: best.direction }
for (const pane of panesByPosition) {
if (canSplitPane(pane, "-v", minAgentPaneWidth)) {
return { targetPaneId: pane.paneId, splitDirection: "-v" }
}
}
if (isStrictMainVertical(config)) {
return null
}
for (const pane of panesByPosition) {
if (canSplitPane(pane, "-h", minAgentPaneWidth)) {
return { targetPaneId: pane.paneId, splitDirection: "-h" }
}
}
return null
@@ -90,7 +140,7 @@ function findSplittableTarget(
export function findSpawnTarget(
state: WindowState,
minPaneWidth: number = MIN_PANE_WIDTH,
config: CapacityConfig,
): SpawnTarget | null {
return findSplittableTarget(state, minPaneWidth)
return findSplittableTarget(state, config)
}