fix(tmux-subagent): drain terminal probe replies during delegated pane startup (#2887)

When oh-my-opencode created a delegated tmux pane, terminal
capability/color probe replies emitted by tmux or the freshly
attaching opencode session could end up in the caller pane's
input buffer instead of being consumed by the delegated pane,
appearing as literal text in the main OpenCode chat (e.g.
"414/21212a2/...").

Root cause: buildSplitArgs in team-layout-tmux/layout.ts called
split-window without the -d (detached/don't-switch-focus) flag.
Without -d, tmux briefly grants focus to the new pane during
creation; the outer terminal then sends DA1/DA2 and OSC color
probe replies into what it believes is the active pane, but the
focus handoff races and those bytes land in the caller pane's
stdin buffer instead.

Fix: add -d to every split-window call in buildSplitArgs, matching
the same flag already used in pane-spawn.ts for inline subagent
panes. This keeps the caller pane focused throughout the delegated
pane lifecycle so probe replies are consumed by the correct target.

Existing tests pass; one new test asserts -d is present on every
split-window call to guard this invariant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ZeyuFu
2026-05-16 01:41:25 -04:00
parent 1723a8b74c
commit e25f3ca736
2 changed files with 27 additions and 2 deletions
@@ -255,6 +255,30 @@ describe("team-layout-tmux", () => {
expect(commands.some((args) => args[0] === "set-option")).toBe(false)
})
test("#given delegated pane startup #when split-window is called #then -d flag is always present to prevent terminal probe replies leaking into caller pane (fix #2887)", async () => {
// given
const { createTeamLayout } = await loadLayoutModule()
const members = [
{ name: "m1", sessionId: "s-m1", worktreePath: "/tmp/m1" },
{ name: "m2", sessionId: "s-m2", worktreePath: "/tmp/m2" },
{ name: "m3", sessionId: "s-m3", worktreePath: "/tmp/m3" },
]
// when
await createTeamLayout("run-probe-drain", members, tmuxMgr as never)
// then: every split-window call must carry -d so focus never bounces to the
// new pane; without -d, terminal capability probe replies (DA1/DA2, OSC color)
// emitted during pane startup are misrouted to the caller pane's stdin and
// appear as literal garbage text in the main OpenCode chat input.
const commands = getCommands()
const splitWindowCalls = commands.filter((args) => args[0] === "split-window")
expect(splitWindowCalls.length).toBeGreaterThan(0)
for (const splitArgs of splitWindowCalls) {
expect(splitArgs).toContain("-d")
}
})
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 () => {
// given
const { removeTeamLayout } = await loadLayoutModule()
@@ -392,7 +416,7 @@ describe("team-layout-tmux", () => {
const commands = getCommands()
const splitCalls = commands.filter((args) => args[0] === "split-window")
expect(splitCalls).toEqual([
["split-window", "-t", process.env.TMUX_PANE ?? "", "-h", "-l", "70%", "-P", "-F", "#{pane_id}", "-c", "/tmp/m1"],
["split-window", "-t", process.env.TMUX_PANE ?? "", "-h", "-d", "-l", "70%", "-P", "-F", "#{pane_id}", "-c", "/tmp/m1"],
])
expect(commands.filter((args) => args[0] === "new-window").length).toBe(0)
})
@@ -61,7 +61,7 @@ function selectExistingTeammatePane(teammatePanes: Array<string>, 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", callerPaneId, "-h", "-d", "-l", "70%", "-P", "-F", "#{pane_id}", "-c", getPaneWorkingDirectory(member)]
}
return [
@@ -69,6 +69,7 @@ function buildSplitArgs(callerPaneId: string, teammatePanes: Array<string>, memb
"-t",
selectExistingTeammatePane(teammatePanes, callerPaneId),
teammatePanes.length % 2 === 1 ? "-v" : "-h",
"-d",
"-P",
"-F",
"#{pane_id}",