import type { Plugin } from "@opencode-ai/plugin" import { spawn } from "bun" /** * tmux-panes plugin * * When opencode spawns a background subagent, this plugin automatically opens * a new tmux pane showing that subagent's live TUI via `opencode attach`. * The pane closes when the subagent session ends. * * Only activates when running inside a tmux session (TMUX env var is set). */ const isInsideTmux = () => Boolean(process.env.TMUX) const getCurrentPaneId = () => process.env.TMUX_PANE const plugin: Plugin = async (ctx) => { if (!isInsideTmux()) return {} const sessions = new Map() // sessionId → tmux paneId const sourcePaneId = getCurrentPaneId() const serverUrl = ctx.serverUrl.toString() return { event: async ({ event }) => { // Spawn a new pane when a subagent session is created if (event.type === "session.created") { const info = (event as any).properties?.info // parentID presence distinguishes subagents from the root session if (!info?.id || !info?.parentID) return const sessionId: string = info.id if (sessions.has(sessionId)) return const cmd = `opencode attach ${serverUrl} --session ${sessionId}` const proc = spawn( [ "tmux", "split-window", "-h", // horizontal split "-d", // don't focus the new pane "-P", "-F", "#{pane_id}", // print the new pane's ID ...(sourcePaneId ? ["-t", sourcePaneId] : []), cmd, ], { stdout: "pipe", stderr: "pipe" }, ) const paneId = (await new Response(proc.stdout).text()).trim() if ((await proc.exited) === 0 && paneId) { sessions.set(sessionId, paneId) } } // Kill the pane when the subagent session ends if (event.type === "session.deleted") { const info = (event as any).properties?.info const paneId = sessions.get(info?.id) if (paneId) { spawn(["tmux", "kill-pane", "-t", paneId], { stdout: "ignore", stderr: "ignore", }) sessions.delete(info.id) } } }, } } export default plugin