feat: add tmux integration with visual subagent panes
- plugins/tmux-panes.ts: opencode plugin that hooks into session.created/ session.deleted events to spawn a tmux pane per subagent running 'opencode attach', giving live visual TUI for each background agent - opencode.jsonc: load the local plugin alongside @tarquinen/opencode-dcp - skills/tmux-session/SKILL.md: teach agents to manage persistent tmux sessions (dev servers, watchers, worktree windows) with oc- naming - c.fish / cc.fish: auto-start a tmux session when invoked outside tmux so the visual panes plugin can always activate
This commit is contained in:
72
.config/opencode/plugins/tmux-panes.ts
Normal file
72
.config/opencode/plugins/tmux-panes.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
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<string, string>() // 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
|
||||
Reference in New Issue
Block a user