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:
@@ -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"]])
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user