Skip to content

Commit 2b1fd2a

Browse files
centdixclaude
andcommitted
Fix set-window-status from sandbox containers via RPC branch resolution
The workmux stub inside containers had no tmux context (no TMUX_PANE), so set-window-status silently failed. Pass WORKMUX_BRANCH env to containers, include it in the RPC payload, and resolve the tmux pane ID on the host before spawning the workmux binary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4af6180 commit 2b1fd2a

2 files changed

Lines changed: 49 additions & 3 deletions

File tree

backend/src/docker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ args = sys.argv[2:]
6666
host = os.environ.get("WORKMUX_RPC_HOST", "host.docker.internal")
6767
port = os.environ.get("WORKMUX_RPC_PORT", "5111")
6868
token = os.environ.get("WORKMUX_RPC_TOKEN", "")
69+
branch = os.environ.get("WORKMUX_BRANCH", "")
6970
70-
payload = {"command": cmd, "args": args}
71+
payload = {"command": cmd, "args": args, "branch": branch}
7172
data = json.dumps(payload).encode()
7273
req = urllib.request.Request(
7374
f"http://{host}:{port}/rpc/workmux",
@@ -240,6 +241,7 @@ export function buildDockerRunArgs(
240241
args.push("-e", `WORKMUX_RPC_HOST=host.docker.internal`);
241242
args.push("-e", `WORKMUX_RPC_PORT=${rpcPort}`);
242243
args.push("-e", `WORKMUX_RPC_TOKEN=${rpcSecret}`);
244+
args.push("-e", `WORKMUX_BRANCH=${opts.branch}`);
243245

244246
// Image + command.
245247
args.push(sandboxConfig.image, "sleep", "infinity");
@@ -331,7 +333,7 @@ export async function launchContainer(opts: LaunchContainerOpts): Promise<string
331333
// Inject workmux stub so agents inside the container can call host-side workmux.
332334
const stub = buildWorkmuxStub();
333335
const injectProc = Bun.spawn(
334-
["docker", "exec", "-i", name, "sh", "-c",
336+
["docker", "exec", "-u", "root", "-i", name, "sh", "-c",
335337
"cat > /usr/local/bin/workmux && chmod +x /usr/local/bin/workmux"],
336338
{ stdin: "pipe", stdout: "pipe", stderr: "pipe" },
337339
);

backend/src/rpc.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,43 @@ import { jsonResponse } from "./http";
44
interface RpcRequest {
55
command: string;
66
args?: string[];
7+
branch?: string;
78
}
89

910
type RpcResponse = { ok: true; output: string } | { ok: false; error: string }
1011

12+
/** Build env with TMUX set so workmux can resolve agent states outside tmux. */
13+
function tmuxEnv(): Record<string, string | undefined> {
14+
if (Bun.env.TMUX) return Bun.env;
15+
const tmpdir = Bun.env.TMUX_TMPDIR || "/tmp";
16+
const uid = process.getuid?.() ?? 1000;
17+
return { ...Bun.env, TMUX: `${tmpdir}/tmux-${uid}/default,0,0` };
18+
}
19+
20+
/**
21+
* Resolve the tmux pane ID for a worktree window (wm-{branch}).
22+
* Returns the first pane ID, or null if the window doesn't exist.
23+
*/
24+
async function resolvePaneId(branch: string): Promise<string | null> {
25+
const proc = Bun.spawn(
26+
["tmux", "list-panes", "-a", "-F", "#{window_name}\t#{pane_id}"],
27+
{ stdout: "pipe", stderr: "pipe", env: tmuxEnv() },
28+
);
29+
const [stdout, , exitCode] = await Promise.all([
30+
new Response(proc.stdout).text(),
31+
new Response(proc.stderr).text(),
32+
proc.exited,
33+
]);
34+
if (exitCode !== 0) return null;
35+
36+
const target = `wm-${branch}`;
37+
for (const line of stdout.trim().split("\n")) {
38+
const [windowName, paneId] = line.split("\t");
39+
if (windowName === target && paneId) return paneId;
40+
}
41+
return null;
42+
}
43+
1144
export async function handleWorkmuxRpc(req: Request): Promise<Response> {
1245
const secret = await loadRpcSecret();
1346
const authHeader = req.headers.get("Authorization");
@@ -23,15 +56,26 @@ export async function handleWorkmuxRpc(req: Request): Promise<Response> {
2356
return jsonResponse({ ok: false, error: "Invalid JSON" } satisfies RpcResponse, 400);
2457
}
2558

26-
const { command, args = [] } = raw;
59+
const { command, args = [], branch } = raw;
2760
if (!command) {
2861
return jsonResponse({ ok: false, error: "Missing command" } satisfies RpcResponse, 400);
2962
}
3063

3164
try {
65+
// Build spawn environment. For set-window-status from a container,
66+
// resolve the tmux pane ID so the workmux binary can target the right window.
67+
const env = tmuxEnv();
68+
if (command === "set-window-status" && branch) {
69+
const paneId = await resolvePaneId(branch);
70+
if (paneId) {
71+
env.TMUX_PANE = paneId;
72+
}
73+
}
74+
3275
const proc = Bun.spawn(["workmux", command, ...args], {
3376
stdout: "pipe",
3477
stderr: "pipe",
78+
env,
3579
});
3680
const [stdout, stderr, exitCode] = await Promise.all([
3781
new Response(proc.stdout).text(),

0 commit comments

Comments
 (0)