Merge pull request #1844 from liu-qingyuan/fix/tmux-split-defer-fifo

fix(tmux): prefer split-or-defer with FIFO deferred attach
This commit is contained in:
YeonGyu-Kim
2026-02-20 11:55:32 +09:00
committed by GitHub
14 changed files with 1184 additions and 237 deletions
+44
View File
@@ -0,0 +1,44 @@
import { afterEach, describe, expect, it, mock } from "bun:test"
const spawnCalls: string[][] = []
const spawnMock = mock((args: string[]) => {
spawnCalls.push(args)
return { exited: Promise.resolve(0) }
})
describe("applyLayout", () => {
afterEach(() => {
spawnCalls.length = 0
spawnMock.mockClear()
})
it("applies main-vertical with main-pane-width option", async () => {
const { applyLayout } = await import("./layout")
await applyLayout("tmux", "main-vertical", 60, { spawnCommand: spawnMock })
expect(spawnCalls).toEqual([
["tmux", "select-layout", "main-vertical"],
["tmux", "set-window-option", "main-pane-width", "60%"],
])
})
it("applies main-horizontal with main-pane-height option", async () => {
const { applyLayout } = await import("./layout")
await applyLayout("tmux", "main-horizontal", 55, { spawnCommand: spawnMock })
expect(spawnCalls).toEqual([
["tmux", "select-layout", "main-horizontal"],
["tmux", "set-window-option", "main-pane-height", "55%"],
])
})
it("does not set main pane option for non-main layouts", async () => {
const { applyLayout } = await import("./layout")
await applyLayout("tmux", "tiled", 50, { spawnCommand: spawnMock })
expect(spawnCalls).toEqual([["tmux", "select-layout", "tiled"]])
})
})
+52 -10
View File
@@ -2,14 +2,52 @@ import { spawn } from "bun"
import type { TmuxLayout } from "../../../config/schema"
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
type TmuxSpawnCommand = (
args: string[],
options: { stdout: "ignore"; stderr: "ignore" },
) => { exited: Promise<number> }
interface LayoutDeps {
spawnCommand?: TmuxSpawnCommand
}
interface MainPaneWidthOptions {
mainPaneSize?: number
mainPaneMinWidth?: number
agentPaneMinWidth?: number
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value))
}
function calculateMainPaneWidth(
windowWidth: number,
options?: MainPaneWidthOptions,
): number {
const dividerWidth = 1
const sizePercent = clamp(options?.mainPaneSize ?? 50, 20, 80)
const minMainPaneWidth = options?.mainPaneMinWidth ?? 0
const minAgentPaneWidth = options?.agentPaneMinWidth ?? 0
const desiredMainPaneWidth = Math.floor(
(windowWidth - dividerWidth) * (sizePercent / 100),
)
const maxMainPaneWidth = Math.max(
0,
windowWidth - dividerWidth - minAgentPaneWidth,
)
return clamp(Math.max(desiredMainPaneWidth, minMainPaneWidth), 0, maxMainPaneWidth)
}
export async function applyLayout(
tmux: string,
layout: TmuxLayout,
mainPaneSize: number,
deps?: LayoutDeps,
): Promise<void> {
const tmux = await getTmuxPath()
if (!tmux) return
const layoutProc = spawn([tmux, "select-layout", layout], {
const spawnCommand: TmuxSpawnCommand = deps?.spawnCommand ?? spawn
const layoutProc = spawnCommand([tmux, "select-layout", layout], {
stdout: "ignore",
stderr: "ignore",
})
@@ -18,7 +56,7 @@ export async function applyLayout(
if (layout.startsWith("main-")) {
const dimension =
layout === "main-horizontal" ? "main-pane-height" : "main-pane-width"
const sizeProc = spawn(
const sizeProc = spawnCommand(
[tmux, "set-window-option", dimension, `${mainPaneSize}%`],
{ stdout: "ignore", stderr: "ignore" },
)
@@ -29,15 +67,17 @@ export async function applyLayout(
export async function enforceMainPaneWidth(
mainPaneId: string,
windowWidth: number,
mainPaneSize: number,
mainPaneSizeOrOptions?: number | MainPaneWidthOptions,
): Promise<void> {
const { log } = await import("../../logger")
const tmux = await getTmuxPath()
if (!tmux) return
const dividerWidth = 1
const boundedMainPaneSize = Math.max(20, Math.min(80, mainPaneSize))
const mainWidth = Math.floor(((windowWidth - dividerWidth) * boundedMainPaneSize) / 100)
const options: MainPaneWidthOptions =
typeof mainPaneSizeOrOptions === "number"
? { mainPaneSize: mainPaneSizeOrOptions }
: mainPaneSizeOrOptions ?? {}
const mainWidth = calculateMainPaneWidth(windowWidth, options)
const proc = spawn([tmux, "resize-pane", "-t", mainPaneId, "-x", String(mainWidth)], {
stdout: "ignore",
@@ -49,6 +89,8 @@ export async function enforceMainPaneWidth(
mainPaneId,
mainWidth,
windowWidth,
mainPaneSize: boundedMainPaneSize,
mainPaneSize: options?.mainPaneSize,
mainPaneMinWidth: options?.mainPaneMinWidth,
agentPaneMinWidth: options?.agentPaneMinWidth,
})
}