105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
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`.
|
|
*
|
|
* Layout:
|
|
* - First subagent: horizontal 60/40 split — main pane on left, subagent on right
|
|
* - Additional subagents: stacked vertically in the right column
|
|
* - Panes close automatically when subagent sessions end
|
|
*
|
|
* 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<string, string>() // sessionId → tmux paneId
|
|
const sourcePaneId = getCurrentPaneId()
|
|
const serverUrl = (ctx.serverUrl?.toString() ?? "").replace(/\/$/, "")
|
|
|
|
// Ordered list of pane IDs in the right column.
|
|
// Empty = no right column yet; length > 0 = right column exists.
|
|
const rightColumnPanes: string[] = []
|
|
|
|
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}`
|
|
|
|
let args: string[]
|
|
if (rightColumnPanes.length === 0) {
|
|
// First subagent: open a horizontal split, giving the new (right)
|
|
// pane 40% of the window width so the main pane keeps 60%.
|
|
args = [
|
|
"tmux",
|
|
"split-window",
|
|
"-h", // horizontal split — new pane appears on the right
|
|
"-p", "40", // new pane gets 40% of window width
|
|
"-d", // don't focus the new pane
|
|
"-P",
|
|
"-F",
|
|
"#{pane_id}",
|
|
...(sourcePaneId ? ["-t", sourcePaneId] : []),
|
|
cmd,
|
|
]
|
|
} else {
|
|
// Additional subagents: stack vertically within the right column
|
|
// by splitting the last right-column pane horizontally.
|
|
const lastRightPane = rightColumnPanes[rightColumnPanes.length - 1]
|
|
args = [
|
|
"tmux",
|
|
"split-window",
|
|
"-v", // vertical split — new pane stacks below the target
|
|
"-d", // don't focus the new pane
|
|
"-P",
|
|
"-F",
|
|
"#{pane_id}",
|
|
"-t", lastRightPane,
|
|
cmd,
|
|
]
|
|
}
|
|
|
|
const proc = spawn(args, { stdout: "pipe", stderr: "pipe" })
|
|
const paneId = (await new Response(proc.stdout).text()).trim()
|
|
const exitCode = await proc.exited
|
|
if (exitCode === 0 && paneId) {
|
|
sessions.set(sessionId, paneId)
|
|
rightColumnPanes.push(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)
|
|
const idx = rightColumnPanes.indexOf(paneId)
|
|
if (idx !== -1) rightColumnPanes.splice(idx, 1)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
export default plugin
|