Skip to content

Commit 32d3207

Browse files
committed
fix: cleanup attach windows when removing jobs
Tag tmux windows created by mc_attach with @mc_job_id so mc_cleanup can find and close them. Previously attach windows were orphaned after cleanup, requiring manual removal.
1 parent c63ba10 commit 32d3207

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

src/lib/tmux.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,58 @@ export async function createWindow(opts: {
116116
}
117117
}
118118

119+
/**
120+
* Set a user option on a tmux window for ownership tracking
121+
*/
122+
export async function setWindowOption(target: string, option: string, value: string): Promise<void> {
123+
try {
124+
const proc = spawn(["tmux", "set-option", "-w", "-t", target, option, value], { stderr: "pipe" });
125+
const exitCode = await proc.exited;
126+
if (exitCode !== 0) {
127+
throw new Error(`tmux set-option failed with exit code ${exitCode}`);
128+
}
129+
} catch (error) {
130+
throw new Error(
131+
`Failed to set tmux window option: ${error instanceof Error ? error.message : String(error)}`
132+
);
133+
}
134+
}
135+
136+
/**
137+
* Find and kill all tmux windows tagged with a specific job ID
138+
*/
139+
export async function killTaggedWindows(jobId: string): Promise<number> {
140+
try {
141+
const proc = spawn(
142+
["tmux", "list-windows", "-a", "-F", "#{session_name}:#{window_index} #{@mc_job_id}"],
143+
{ stderr: "pipe" },
144+
);
145+
const output = await new Response(proc.stdout).text();
146+
const exitCode = await proc.exited;
147+
if (exitCode !== 0) {
148+
return 0;
149+
}
150+
151+
let killed = 0;
152+
for (const line of output.trim().split('\n')) {
153+
if (!line.trim()) continue;
154+
const [target, taggedId] = line.split(' ', 2);
155+
if (taggedId === jobId && target) {
156+
try {
157+
const killProc = spawn(["tmux", "kill-window", "-t", target], { stderr: "pipe" });
158+
await killProc.exited;
159+
killed++;
160+
} catch {
161+
// Best-effort: window may already be gone
162+
}
163+
}
164+
}
165+
return killed;
166+
} catch {
167+
return 0;
168+
}
169+
}
170+
119171
/**
120172
* Check if a tmux session exists
121173
*/

src/tools/attach.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { tool, type ToolDefinition } from '@opencode-ai/plugin';
22
import { getJobByName } from '../lib/job-state';
3-
import { isInsideTmux, createWindow, getCurrentSession } from '../lib/tmux';
3+
import { isInsideTmux, createWindow, getCurrentSession, setWindowOption } from '../lib/tmux';
44

55
export const mc_attach: ToolDefinition = tool({
66
description: 'Get instructions for attaching to a job\'s terminal',
@@ -37,6 +37,12 @@ export const mc_attach: ToolDefinition = tool({
3737
command: attachCommand,
3838
});
3939

40+
try {
41+
await setWindowOption(`${currentSession}:${windowName}`, '@mc_job_id', job.id);
42+
} catch {
43+
// Best-effort: tagging failure should not block attach
44+
}
45+
4046
return `Opened TUI for job '${job.name}' in new tmux window`;
4147
} else {
4248
// Not inside tmux, return the command to run

src/tools/cleanup.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tool, type ToolDefinition } from '@opencode-ai/plugin';
22
import { getJobByName, removeJob, getRunningJobs, loadJobState } from '../lib/job-state';
33
import { removeWorktree } from '../lib/worktree';
4-
import { killSession, sessionExists } from '../lib/tmux';
4+
import { killSession, sessionExists, killTaggedWindows } from '../lib/tmux';
55
import { gitCommand } from '../lib/git';
66
import { removeReport } from '../lib/reports';
77
import { releasePort } from '../lib/port-allocator';
@@ -59,6 +59,8 @@ async function cleanupJobs(
5959
try { await killSession(job.tmuxTarget); } catch {}
6060
}
6161

62+
try { await killTaggedWindows(job.id); } catch {}
63+
6264
if (job.port) {
6365
try { await releasePort(job.port); } catch {}
6466
}

0 commit comments

Comments
 (0)