fix(team-mode): stop tmux freeze when many teammates spawn
Drop the per-pane select-pane title shuffle and the cyan border styling that leaked to window scope, then switch from main-vertical to tiled when there are 4 or more teammates so each pane keeps a usable height for the opencode TUI. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -155,7 +155,7 @@ describe("team-layout-tmux", () => {
|
|||||||
expect(literals.some((s) => s.includes("--session 's-m2'"))).toBe(true)
|
expect(literals.some((s) => s.includes("--session 's-m2'"))).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("uses main-vertical layout with leader at 30%", async () => {
|
test("uses main-vertical layout with leader at 30% for up to 3 teammates", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = [
|
||||||
@@ -177,39 +177,48 @@ describe("team-layout-tmux", () => {
|
|||||||
expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2", "m3"])
|
expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2", "m3"])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("sets pane title for each member", async () => {
|
test("#given 4 or more teammates #when createTeamLayout runs #then it switches to tiled layout and stops resizing the leader pane", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = Array.from({ length: 5 }, (_, index) => ({
|
||||||
{ name: "lead", sessionId: "s-lead", worktreePath: "/tmp/lead" },
|
name: `m${index + 1}`,
|
||||||
{ name: "m2", sessionId: "s-m2", worktreePath: "/tmp/m2" },
|
sessionId: `s-m${index + 1}`,
|
||||||
]
|
worktreePath: `/tmp/m${index + 1}`,
|
||||||
|
}))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await createTeamLayout("run-title", members, tmuxMgr as never)
|
await createTeamLayout("run-tiled", members, tmuxMgr as never)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const titleSetters = commands
|
const selectLayoutArgs = commands.filter((args) => args[0] === "select-layout").map((args) => args[args.length - 1])
|
||||||
.filter((args) => args[0] === "select-pane" && args.includes("-T"))
|
expect(selectLayoutArgs).toContain("tiled")
|
||||||
.map((args) => args[args.length - 1])
|
const tiledIndex = selectLayoutArgs.indexOf("tiled")
|
||||||
expect(titleSetters).toContain("lead")
|
const layoutsAfterTiled = selectLayoutArgs.slice(tiledIndex)
|
||||||
expect(titleSetters).toContain("m2")
|
expect(layoutsAfterTiled.every((layout) => layout === "tiled")).toBe(true)
|
||||||
|
const indexOfFirstTiled = commands.findIndex((args) => args[0] === "select-layout" && args[args.length - 1] === "tiled")
|
||||||
|
const resizesAfterTiled = commands
|
||||||
|
.slice(indexOfFirstTiled)
|
||||||
|
.filter((args) => args[0] === "resize-pane" && args.includes("30%"))
|
||||||
|
expect(resizesAfterTiled).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given caller inside tmux #when createTeamLayout runs #then it restores keyboard focus and avoids border status mutation", async () => {
|
test("#given caller inside tmux #when createTeamLayout runs #then it never steals focus or mutates window border options", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [{ name: "m1", sessionId: "s-m1", worktreePath: "/tmp/m1" }]
|
const members = Array.from({ length: 5 }, (_, index) => ({
|
||||||
|
name: `m${index + 1}`,
|
||||||
|
sessionId: `s-m${index + 1}`,
|
||||||
|
worktreePath: `/tmp/m${index + 1}`,
|
||||||
|
}))
|
||||||
|
|
||||||
// when
|
// when
|
||||||
await createTeamLayout("run-options", members, tmuxMgr as never)
|
await createTeamLayout("run-no-focus", members, tmuxMgr as never)
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const focusRestores = commands.filter((args) => args[0] === "select-pane" && args[1] === "-t" && args[2] === "%42")
|
expect(commands.some((args) => args[0] === "select-pane")).toBe(false)
|
||||||
expect(focusRestores.length).toBe(1)
|
expect(commands.some((args) => args[0] === "set-option")).toBe(false)
|
||||||
expect(commands.some((args) => args[0] === "set-option" && args.includes("pane-border-status"))).toBe(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given ownedSession=false, focusWindowId=@10, gridWindowId=@11 #when removeTeamLayout runs #then tmux kill-window is called twice with -t @10 and -t @11 and kill-session is NEVER called", async () => {
|
test("#given ownedSession=false, focusWindowId=@10, gridWindowId=@11 #when removeTeamLayout runs #then tmux kill-window is called twice with -t @10 and -t @11 and kill-session is NEVER called", async () => {
|
||||||
|
|||||||
@@ -68,8 +68,14 @@ async function listPanesInWindow(tmuxPath: string, windowTarget: string): Promis
|
|||||||
async function rebalanceWithLeader(tmuxPath: string, windowTarget: string, leaderPaneId: string): Promise<void> {
|
async function rebalanceWithLeader(tmuxPath: string, windowTarget: string, leaderPaneId: string): Promise<void> {
|
||||||
const panes = await listPanesInWindow(tmuxPath, windowTarget)
|
const panes = await listPanesInWindow(tmuxPath, windowTarget)
|
||||||
if (panes.length <= 1) return
|
if (panes.length <= 1) return
|
||||||
await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowTarget, "main-vertical"])
|
// main-vertical with many teammates collapses each pane to ~10 rows, which the
|
||||||
await runTmuxCommand(tmuxPath, ["resize-pane", "-t", leaderPaneId, "-x", "30%"])
|
// opencode TUI cannot render. Switch to tiled once the column gets too tall.
|
||||||
|
const teammateCount = panes.length - 1
|
||||||
|
const layout = teammateCount >= 4 ? "tiled" : "main-vertical"
|
||||||
|
await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowTarget, layout])
|
||||||
|
if (layout === "main-vertical") {
|
||||||
|
await runTmuxCommand(tmuxPath, ["resize-pane", "-t", leaderPaneId, "-x", "30%"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTeammatePaneInCurrentWindow(
|
async function createTeammatePaneInCurrentWindow(
|
||||||
@@ -105,12 +111,6 @@ async function createTeammatePaneInCurrentWindow(
|
|||||||
if (!splitResult.success || !splitResult.output) return null
|
if (!splitResult.success || !splitResult.output) return null
|
||||||
const paneId = splitResult.output.trim()
|
const paneId = splitResult.output.trim()
|
||||||
|
|
||||||
await runTmuxCommand(tmuxPath, ["select-pane", "-t", paneId, "-T", member.name])
|
|
||||||
await runTmuxCommand(tmuxPath, ["select-pane", "-t", leaderPaneId])
|
|
||||||
await runTmuxCommand(tmuxPath, ["set-option", "-p", "-t", paneId, "pane-border-style", "fg=cyan"])
|
|
||||||
await runTmuxCommand(tmuxPath, ["set-option", "-p", "-t", paneId, "pane-active-border-style", "fg=cyan"])
|
|
||||||
await runTmuxCommand(tmuxPath, ["set-option", "-p", "-t", paneId, "pane-border-format", "#[fg=cyan,bold] #{pane_title} #[default]"])
|
|
||||||
|
|
||||||
await rebalanceWithLeader(tmuxPath, windowTarget, leaderPaneId)
|
await rebalanceWithLeader(tmuxPath, windowTarget, leaderPaneId)
|
||||||
await new Promise((resolve) => setTimeout(resolve, PANE_SHELL_INIT_DELAY_MS))
|
await new Promise((resolve) => setTimeout(resolve, PANE_SHELL_INIT_DELAY_MS))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user