fix(team-mode): keep tmux visualization in caller window
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -114,7 +114,7 @@ describe("team-layout-tmux", () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sessionId: displaySessionId }
|
return { sessionId: displaySessionId, paneId: process.env.TMUX_PANE, windowTarget: "test-session:0" }
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ describe("team-layout-tmux", () => {
|
|||||||
expect(runTmuxCommandMock).toHaveBeenCalledTimes(0)
|
expect(runTmuxCommandMock).toHaveBeenCalledTimes(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("creates detached focus and grid windows and sends attach via send-keys", async () => {
|
test("creates teammate panes in the caller window and sends attach via send-keys", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = [
|
||||||
@@ -162,12 +162,8 @@ describe("team-layout-tmux", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const newWindowCalls = commands.filter((args) => args[0] === "new-window")
|
expect(commands.some((args) => args[0] === "new-window")).toBe(false)
|
||||||
expect(newWindowCalls.length).toBe(2)
|
expect(commands.filter((args) => args[0] === "split-window")).toHaveLength(2)
|
||||||
expect(newWindowCalls.map((args) => args[args.indexOf("-n") + 1])).toEqual([
|
|
||||||
"team-run-attach-focus",
|
|
||||||
"team-run-attach-grid",
|
|
||||||
])
|
|
||||||
|
|
||||||
const sendKeysCalls = commands.filter((args) => args[0] === "send-keys")
|
const sendKeysCalls = commands.filter((args) => args[0] === "send-keys")
|
||||||
const literals = sendKeysCalls.map((args) => args.join(" "))
|
const literals = sendKeysCalls.map((args) => args.join(" "))
|
||||||
@@ -175,7 +171,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 focus main-vertical and grid tiled windows", async () => {
|
test("uses caller window main-vertical layout with caller pane as primary", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = [
|
||||||
@@ -191,14 +187,14 @@ describe("team-layout-tmux", () => {
|
|||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const selectLayoutArgs = commands.filter((args) => args[0] === "select-layout").map((args) => args[args.length - 1])
|
const selectLayoutArgs = commands.filter((args) => args[0] === "select-layout").map((args) => args[args.length - 1])
|
||||||
expect(selectLayoutArgs).toContain("main-vertical")
|
expect(selectLayoutArgs).toContain("main-vertical")
|
||||||
expect(selectLayoutArgs).toContain("tiled")
|
expect(selectLayoutArgs).not.toContain("tiled")
|
||||||
expect(commands).toContainEqual(["set-window-option", "-t", "@1", "main-pane-width", "60%"])
|
expect(commands).toContainEqual(["resize-pane", "-t", process.env.TMUX_PANE ?? "", "-x", "30%"])
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2", "m3"])
|
expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2", "m3"])
|
||||||
expect(Object.keys(result?.gridPanesByMember ?? {}).sort()).toEqual(["m1", "m2", "m3"])
|
expect(Object.keys(result?.gridPanesByMember ?? {})).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given 4 or more teammates #when createTeamLayout runs #then it still keeps separate focus and grid windows", async () => {
|
test("#given 4 or more teammates #when createTeamLayout runs #then it keeps every teammate in the caller window", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = Array.from({ length: 5 }, (_, index) => ({
|
const members = Array.from({ length: 5 }, (_, index) => ({
|
||||||
@@ -212,13 +208,11 @@ describe("team-layout-tmux", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const newWindowNames = commands
|
expect(commands.some((args) => args[0] === "new-window")).toBe(false)
|
||||||
.filter((args) => args[0] === "new-window")
|
expect(commands.filter((args) => args[0] === "split-window")).toHaveLength(5)
|
||||||
.map((args) => args[args.indexOf("-n") + 1])
|
|
||||||
expect(newWindowNames).toEqual(["team-run-tiled-focus", "team-run-tiled-grid"])
|
|
||||||
const selectLayoutArgs = commands.filter((args) => args[0] === "select-layout").map((args) => args[args.length - 1])
|
const selectLayoutArgs = commands.filter((args) => args[0] === "select-layout").map((args) => args[args.length - 1])
|
||||||
expect(selectLayoutArgs).toContain("main-vertical")
|
expect(selectLayoutArgs).toContain("main-vertical")
|
||||||
expect(selectLayoutArgs).toContain("tiled")
|
expect(selectLayoutArgs).not.toContain("tiled")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given caller inside tmux #when createTeamLayout runs #then it never steals focus or mutates window border options", async () => {
|
test("#given caller inside tmux #when createTeamLayout runs #then it never steals focus or mutates window border options", async () => {
|
||||||
@@ -333,7 +327,7 @@ describe("team-layout-tmux", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("createTeamLayout - focus/grid window topology", () => {
|
describe("createTeamLayout - focus/grid window topology", () => {
|
||||||
test("#given caller inside tmux #when createTeamLayout runs #then creates focus and grid windows without a new session", async () => {
|
test("#given caller inside tmux #when createTeamLayout runs #then uses the caller window without a new session", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = [
|
||||||
@@ -347,8 +341,8 @@ describe("team-layout-tmux", () => {
|
|||||||
// then
|
// then
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
expect(commands.some((args) => args[0] === "new-session")).toBe(false)
|
expect(commands.some((args) => args[0] === "new-session")).toBe(false)
|
||||||
expect(commands.filter((args) => args[0] === "new-window").length).toBe(2)
|
expect(commands.filter((args) => args[0] === "new-window").length).toBe(0)
|
||||||
expect(commands.some((args) => args[0] === "split-window" && args.includes(process.env.TMUX_PANE ?? ""))).toBe(false)
|
expect(commands.some((args) => args[0] === "split-window" && args.includes(process.env.TMUX_PANE ?? ""))).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given caller session resolved #when createTeamLayout runs #then ownedSession is false", async () => {
|
test("#given caller session resolved #when createTeamLayout runs #then ownedSession is false", async () => {
|
||||||
@@ -364,7 +358,7 @@ describe("team-layout-tmux", () => {
|
|||||||
expect(result?.ownedSession).toBe(false)
|
expect(result?.ownedSession).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given first teammate #when layout runs #then it creates focus and grid windows without splitting the leader pane", async () => {
|
test("#given first teammate #when layout runs #then it splits the caller pane horizontally for teammate area", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [{ name: "m1", sessionId: "s-m1", worktreePath: "/tmp/m1" }]
|
const members = [{ name: "m1", sessionId: "s-m1", worktreePath: "/tmp/m1" }]
|
||||||
@@ -375,8 +369,10 @@ describe("team-layout-tmux", () => {
|
|||||||
// then
|
// then
|
||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
const splitCalls = commands.filter((args) => args[0] === "split-window")
|
const splitCalls = commands.filter((args) => args[0] === "split-window")
|
||||||
expect(splitCalls).toEqual([])
|
expect(splitCalls).toEqual([
|
||||||
expect(commands.filter((args) => args[0] === "new-window").length).toBe(2)
|
["split-window", "-t", process.env.TMUX_PANE ?? "", "-h", "-l", "70%", "-P", "-F", "#{pane_id}", "-c", "/tmp/m1"],
|
||||||
|
])
|
||||||
|
expect(commands.filter((args) => args[0] === "new-window").length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given 3 members #when createTeamLayout runs #then focusPanesByMember contains 3 distinct pane ids", async () => {
|
test("#given 3 members #when createTeamLayout runs #then focusPanesByMember contains 3 distinct pane ids", async () => {
|
||||||
@@ -397,7 +393,7 @@ describe("team-layout-tmux", () => {
|
|||||||
expect(new Set(Object.values(result?.focusPanesByMember ?? {})).size).toBe(3)
|
expect(new Set(Object.values(result?.focusPanesByMember ?? {})).size).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given layout created #when createTeamLayout runs #then it keeps separate focus and grid pane maps", async () => {
|
test("#given layout created #when createTeamLayout runs #then it records focus panes only", async () => {
|
||||||
// given
|
// given
|
||||||
const { createTeamLayout } = await loadLayoutModule()
|
const { createTeamLayout } = await loadLayoutModule()
|
||||||
const members = [
|
const members = [
|
||||||
@@ -412,9 +408,10 @@ describe("team-layout-tmux", () => {
|
|||||||
const commands = getCommands()
|
const commands = getCommands()
|
||||||
expect(result).not.toBeNull()
|
expect(result).not.toBeNull()
|
||||||
expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2"])
|
expect(Object.keys(result?.focusPanesByMember ?? {}).sort()).toEqual(["m1", "m2"])
|
||||||
expect(Object.keys(result?.gridPanesByMember ?? {}).sort()).toEqual(["m1", "m2"])
|
expect(Object.keys(result?.gridPanesByMember ?? {})).toEqual([])
|
||||||
expect(result?.focusWindowId).not.toBe(result?.gridWindowId)
|
expect(result?.focusWindowId).toBe("test-session:0")
|
||||||
expect(commands.filter((args) => args[0] === "new-window").length).toBe(2)
|
expect(result?.gridWindowId).toBeUndefined()
|
||||||
|
expect(commands.filter((args) => args[0] === "new-window").length).toBe(0)
|
||||||
expect(commands.some((args) => args[0] === "send-keys" && args.includes("Enter"))).toBe(true)
|
expect(commands.some((args) => args[0] === "send-keys" && args.includes("Enter"))).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type TeamLayoutMember = { name: string; sessionId: string; worktreePath?: string
|
|||||||
|
|
||||||
export type TeamLayoutResult = {
|
export type TeamLayoutResult = {
|
||||||
focusWindowId: string
|
focusWindowId: string
|
||||||
gridWindowId: string
|
gridWindowId?: string
|
||||||
focusPanesByMember: Record<string, string>
|
focusPanesByMember: Record<string, string>
|
||||||
gridPanesByMember: Record<string, string>
|
gridPanesByMember: Record<string, string>
|
||||||
targetSessionId: string
|
targetSessionId: string
|
||||||
@@ -34,60 +34,63 @@ function buildAttachCommand(member: TeamLayoutMember, serverUrl: string): string
|
|||||||
return `opencode attach ${shellSingleQuote(serverUrl)} --session ${shellSingleQuote(member.sessionId)} --dir ${shellSingleQuote(getPaneWorkingDirectory(member))}`
|
return `opencode attach ${shellSingleQuote(serverUrl)} --session ${shellSingleQuote(member.sessionId)} --dir ${shellSingleQuote(getPaneWorkingDirectory(member))}`
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listPanesInWindow(tmuxPath: string, windowId: string): Promise<Array<string>> {
|
async function listPanesInWindow(tmuxPath: string, windowTarget: string): Promise<Array<string>> {
|
||||||
const result = await runTmuxCommand(tmuxPath, ["list-panes", "-t", windowId, "-F", "#{pane_id}"])
|
const result = await runTmuxCommand(tmuxPath, ["list-panes", "-t", windowTarget, "-F", "#{pane_id}"])
|
||||||
if (!result.success || !result.output) return []
|
if (!result.success || !result.output) return []
|
||||||
return result.output.trim().split("\n").filter(Boolean)
|
return result.output.trim().split("\n").filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTeamWindow(
|
function selectExistingTeammatePane(teammatePanes: Array<string>, callerPaneId: string): string {
|
||||||
|
return teammatePanes[Math.floor(teammatePanes.length / 2)] ?? teammatePanes[teammatePanes.length - 1] ?? callerPaneId
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSplitArgs(callerPaneId: string, teammatePanes: Array<string>, member: TeamLayoutMember): Array<string> {
|
||||||
|
if (teammatePanes.length === 0) {
|
||||||
|
return ["split-window", "-t", callerPaneId, "-h", "-l", "70%", "-P", "-F", "#{pane_id}", "-c", getPaneWorkingDirectory(member)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"split-window",
|
||||||
|
"-t",
|
||||||
|
selectExistingTeammatePane(teammatePanes, callerPaneId),
|
||||||
|
teammatePanes.length % 2 === 1 ? "-v" : "-h",
|
||||||
|
"-P",
|
||||||
|
"-F",
|
||||||
|
"#{pane_id}",
|
||||||
|
"-c",
|
||||||
|
getPaneWorkingDirectory(member),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTeamLayoutInCallerWindow(
|
||||||
tmuxPath: string,
|
tmuxPath: string,
|
||||||
targetSessionId: string,
|
callerPaneId: string,
|
||||||
windowName: string,
|
windowTarget: string,
|
||||||
layout: "main-vertical" | "tiled",
|
|
||||||
members: Array<TeamLayoutMember>,
|
members: Array<TeamLayoutMember>,
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
): Promise<{ windowId: string; panesByMember: Record<string, string> } | null> {
|
): Promise<{ focusWindowId: string; focusPanesByMember: Record<string, string> } | null> {
|
||||||
const [firstMember, ...restMembers] = members
|
const panesByMember: Record<string, string> = {}
|
||||||
if (!firstMember) return null
|
const existingPanes = await listPanesInWindow(tmuxPath, windowTarget)
|
||||||
|
let teammatePanes = existingPanes.filter((paneId) => paneId !== callerPaneId)
|
||||||
const created = await runTmuxCommand(tmuxPath, [
|
|
||||||
"new-window", "-d", "-P", "-F", "#{window_id}", "-t", targetSessionId, "-n", windowName,
|
|
||||||
"-c", getPaneWorkingDirectory(firstMember),
|
|
||||||
])
|
|
||||||
if (!created.success || !created.output) return null
|
|
||||||
|
|
||||||
const windowId = created.output.trim()
|
|
||||||
const initialPanes = await listPanesInWindow(tmuxPath, windowId)
|
|
||||||
const firstPaneId = initialPanes[0]
|
|
||||||
if (!firstPaneId) return null
|
|
||||||
|
|
||||||
const panesByMember: Record<string, string> = { [firstMember.name]: firstPaneId }
|
|
||||||
for (const member of restMembers) {
|
|
||||||
const split = await runTmuxCommand(tmuxPath, [
|
|
||||||
"split-window", "-d", "-P", "-F", "#{pane_id}", "-t", firstPaneId,
|
|
||||||
"-c", getPaneWorkingDirectory(member),
|
|
||||||
])
|
|
||||||
if (!split.success || !split.output) return null
|
|
||||||
panesByMember[member.name] = split.output.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const layoutResult = await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowId, layout])
|
|
||||||
if (!layoutResult.success) return null
|
|
||||||
|
|
||||||
if (layout === "main-vertical") {
|
|
||||||
await runTmuxCommand(tmuxPath, ["set-window-option", "-t", windowId, "main-pane-width", "60%"])
|
|
||||||
await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowId, layout])
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
const paneId = panesByMember[member.name]
|
const split = await runTmuxCommand(tmuxPath, buildSplitArgs(callerPaneId, teammatePanes, member))
|
||||||
if (!paneId) return null
|
if (!split.success || !split.output) return null
|
||||||
|
|
||||||
|
const paneId = split.output.trim()
|
||||||
|
teammatePanes = [...teammatePanes, paneId]
|
||||||
|
panesByMember[member.name] = paneId
|
||||||
await runTmuxCommand(tmuxPath, ["select-pane", "-t", paneId, "-T", member.name])
|
await runTmuxCommand(tmuxPath, ["select-pane", "-t", paneId, "-T", member.name])
|
||||||
await runTmuxCommand(tmuxPath, ["send-keys", "-t", paneId, buildAttachCommand(member, serverUrl), "Enter"])
|
await runTmuxCommand(tmuxPath, ["send-keys", "-t", paneId, buildAttachCommand(member, serverUrl), "Enter"])
|
||||||
}
|
}
|
||||||
|
|
||||||
return { windowId, panesByMember }
|
const layoutResult = await runTmuxCommand(tmuxPath, ["select-layout", "-t", windowTarget, "main-vertical"])
|
||||||
|
if (!layoutResult.success) return null
|
||||||
|
|
||||||
|
const resizeResult = await runTmuxCommand(tmuxPath, ["resize-pane", "-t", callerPaneId, "-x", "30%"])
|
||||||
|
if (!resizeResult.success) return null
|
||||||
|
|
||||||
|
return { focusWindowId: windowTarget, focusPanesByMember: panesByMember }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createTeamLayout(teamRunId: string, members: Array<TeamLayoutMember>, tmuxMgr: TmuxSessionManager): Promise<TeamLayoutResult | null> {
|
export async function createTeamLayout(teamRunId: string, members: Array<TeamLayoutMember>, tmuxMgr: TmuxSessionManager): Promise<TeamLayoutResult | null> {
|
||||||
@@ -111,27 +114,21 @@ export async function createTeamLayout(teamRunId: string, members: Array<TeamLay
|
|||||||
}
|
}
|
||||||
|
|
||||||
const callerSession = await resolveCallerTmuxSession(tmuxPath)
|
const callerSession = await resolveCallerTmuxSession(tmuxPath)
|
||||||
const fallbackSessionName = `omo-team-${teamRunId}`
|
if (!callerSession) {
|
||||||
const ownedSession = callerSession === null
|
log("tmux visualization requires a resolvable caller tmux pane, skipping", { teamRunId })
|
||||||
const targetSessionId = callerSession?.sessionId ?? fallbackSessionName
|
return null
|
||||||
|
|
||||||
if (ownedSession) {
|
|
||||||
log("falling back to detached team session because caller tmux session could not be resolved", { teamRunId })
|
|
||||||
const created = await runTmuxCommand(tmuxPath, ["new-session", "-d", "-s", fallbackSessionName, "-P", "-F", "#{window_id}"])
|
|
||||||
if (!created.success || !created.output) return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const focus = await createTeamWindow(tmuxPath, targetSessionId, `team-${teamRunId}-focus`, "main-vertical", members, serverUrl)
|
const focus = await createTeamLayoutInCallerWindow(tmuxPath, callerSession.paneId, callerSession.windowTarget, members, serverUrl)
|
||||||
const grid = await createTeamWindow(tmuxPath, targetSessionId, `team-${teamRunId}-grid`, "tiled", members, serverUrl)
|
if (!focus) return null
|
||||||
if (!focus || !grid) return null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
focusWindowId: focus.windowId,
|
focusWindowId: focus.focusWindowId,
|
||||||
gridWindowId: grid.windowId,
|
gridWindowId: undefined,
|
||||||
focusPanesByMember: focus.panesByMember,
|
focusPanesByMember: focus.focusPanesByMember,
|
||||||
gridPanesByMember: grid.panesByMember,
|
gridPanesByMember: {},
|
||||||
targetSessionId,
|
targetSessionId: callerSession.sessionId,
|
||||||
ownedSession,
|
ownedSession: false,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("tmux visualization unavailable, skipping", { error: String(error) })
|
log("tmux visualization unavailable, skipping", { error: String(error) })
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type TmuxManagerLike = {
|
|||||||
|
|
||||||
type TeamLayoutResultLike = {
|
type TeamLayoutResultLike = {
|
||||||
focusWindowId: string
|
focusWindowId: string
|
||||||
gridWindowId: string
|
gridWindowId?: string
|
||||||
focusPanesByMember: Record<string, string>
|
focusPanesByMember: Record<string, string>
|
||||||
gridPanesByMember: Record<string, string>
|
gridPanesByMember: Record<string, string>
|
||||||
targetSessionId: string
|
targetSessionId: string
|
||||||
@@ -79,7 +79,7 @@ function isTeamLayoutResultLike(value: unknown): value is TeamLayoutResultLike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return typeof value.focusWindowId === "string"
|
return typeof value.focusWindowId === "string"
|
||||||
&& typeof value.gridWindowId === "string"
|
&& (value.gridWindowId === undefined || typeof value.gridWindowId === "string")
|
||||||
&& isRecord(value.focusPanesByMember)
|
&& isRecord(value.focusPanesByMember)
|
||||||
&& isRecord(value.gridPanesByMember)
|
&& isRecord(value.gridPanesByMember)
|
||||||
&& typeof value.targetSessionId === "string"
|
&& typeof value.targetSessionId === "string"
|
||||||
@@ -210,6 +210,7 @@ async function invokeRemoveTeamLayout(
|
|||||||
targetSessionId,
|
targetSessionId,
|
||||||
focusWindowId: layoutResult.focusWindowId,
|
focusWindowId: layoutResult.focusWindowId,
|
||||||
gridWindowId: layoutResult.gridWindowId,
|
gridWindowId: layoutResult.gridWindowId,
|
||||||
|
paneIds: Object.values(layoutResult.focusPanesByMember),
|
||||||
},
|
},
|
||||||
tmuxManager,
|
tmuxManager,
|
||||||
]))
|
]))
|
||||||
@@ -272,13 +273,11 @@ describe("team-mode live tmux smoke", () => {
|
|||||||
await rm(state.tempRoot, { recursive: true, force: true })
|
await rm(state.tempRoot, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
test.skipIf(!LIVE)("#given a real caller tmux session and two mock members #when createTeamLayout runs #then two new windows appear in the caller session AND removeTeamLayout deletes exactly those two windows leaving the caller session intact", async () => {
|
test.skipIf(!LIVE)("#given a real caller tmux session and two mock members #when createTeamLayout runs #then teammate panes appear in the caller window and cleanup leaves the session intact", async () => {
|
||||||
// given
|
// given
|
||||||
const state = requireLiveTestState()
|
const state = requireLiveTestState()
|
||||||
const layoutModule = await loadLayoutModule()
|
const layoutModule = await loadLayoutModule()
|
||||||
const teamRunId = randomUUID()
|
const teamRunId = randomUUID()
|
||||||
const shortTeamRunId = teamRunId.slice(0, 8)
|
|
||||||
const expectedWindowNames = [`focus-${shortTeamRunId}`, `grid-${shortTeamRunId}`]
|
|
||||||
const initialWindows = await listWindows(state.callerSessionId)
|
const initialWindows = await listWindows(state.callerSessionId)
|
||||||
const members: TeamLayoutMemberLike[] = [
|
const members: TeamLayoutMemberLike[] = [
|
||||||
{
|
{
|
||||||
@@ -295,25 +294,33 @@ describe("team-mode live tmux smoke", () => {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
const layoutResult = await invokeCreateTeamLayout(layoutModule, teamRunId, members, state.tmuxManager)
|
const layoutResult = await invokeCreateTeamLayout(layoutModule, teamRunId, members, state.tmuxManager)
|
||||||
const windowsAppeared = await waitForCondition(async () => {
|
const panesAppeared = await waitForCondition(async () => {
|
||||||
|
const panes = await runTmuxCommand(["list-panes", "-t", state.callerSessionId, "-F", "#{pane_id}"])
|
||||||
|
return panes.success && Object.values(layoutResult.focusPanesByMember).every((paneId) => panes.stdout.split("\n").includes(paneId))
|
||||||
|
})
|
||||||
|
const windowsUnchangedBeforeCleanup = await waitForCondition(async () => {
|
||||||
const windows = await listWindows(state.callerSessionId)
|
const windows = await listWindows(state.callerSessionId)
|
||||||
return expectedWindowNames.every((windowName) => windows.some((window) => window.name === windowName))
|
return windows.map((window) => window.id).join(",") === initialWindows.map((window) => window.id).join(",")
|
||||||
})
|
})
|
||||||
|
|
||||||
await invokeRemoveTeamLayout(layoutModule, teamRunId, state.tmuxManager, layoutResult, state.callerSessionId)
|
await invokeRemoveTeamLayout(layoutModule, teamRunId, state.tmuxManager, layoutResult, state.callerSessionId)
|
||||||
const windowsRemoved = await waitForCondition(async () => {
|
const panesRemoved = await waitForCondition(async () => {
|
||||||
|
const panes = await runTmuxCommand(["list-panes", "-t", state.callerSessionId, "-F", "#{pane_id}"])
|
||||||
|
return panes.success && Object.values(layoutResult.focusPanesByMember).every((paneId) => !panes.stdout.split("\n").includes(paneId))
|
||||||
|
})
|
||||||
|
const windowsUnchangedAfterCleanup = await waitForCondition(async () => {
|
||||||
const windows = await listWindows(state.callerSessionId)
|
const windows = await listWindows(state.callerSessionId)
|
||||||
const noExpectedWindowsRemain = expectedWindowNames.every((windowName) => windows.every((window) => window.name !== windowName))
|
return windows.map((window) => window.id).join(",") === initialWindows.map((window) => window.id).join(",")
|
||||||
const sameWindowIds = windows.map((window) => window.id).join(",") === initialWindows.map((window) => window.id).join(",")
|
|
||||||
return noExpectedWindowsRemain && sameWindowIds
|
|
||||||
})
|
})
|
||||||
const callerSessionStillAlive = await runTmuxCommand(["has-session", "-t", state.callerSessionId])
|
const callerSessionStillAlive = await runTmuxCommand(["has-session", "-t", state.callerSessionId])
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(layoutResult.focusWindowId.length).toBeGreaterThan(0)
|
expect(layoutResult.focusWindowId.length).toBeGreaterThan(0)
|
||||||
expect(layoutResult.gridWindowId.length).toBeGreaterThan(0)
|
expect(layoutResult.gridWindowId).toBeUndefined()
|
||||||
expect(windowsAppeared).toBe(true)
|
expect(panesAppeared).toBe(true)
|
||||||
expect(windowsRemoved).toBe(true)
|
expect(windowsUnchangedBeforeCleanup).toBe(true)
|
||||||
|
expect(panesRemoved).toBe(true)
|
||||||
|
expect(windowsUnchangedAfterCleanup).toBe(true)
|
||||||
expect(callerSessionStillAlive.success).toBe(true)
|
expect(callerSessionStillAlive.success).toBe(true)
|
||||||
expect(process.env.TMUX_PANE).toBe(state.callerPaneId)
|
expect(process.env.TMUX_PANE).toBe(state.callerPaneId)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user