diff --git a/.config/fish/functions/c.fish b/.config/fish/functions/c.fish index 3977c5d..a836fd4 100644 --- a/.config/fish/functions/c.fish +++ b/.config/fish/functions/c.fish @@ -1,3 +1,7 @@ -function c --wraps=opencode --description 'alias c opencode' - opencode $argv +function c --wraps=opencode --description 'opencode (auto-starts tmux for visual subagent panes)' + if not set -q TMUX + tmux new-session opencode $argv + else + opencode $argv + end end diff --git a/.config/fish/functions/cc.fish b/.config/fish/functions/cc.fish index f563a07..ff8c4db 100644 --- a/.config/fish/functions/cc.fish +++ b/.config/fish/functions/cc.fish @@ -1,3 +1,7 @@ -function cc --wraps='opencode --continue' --description 'alias cc opencode --continue' - opencode --continue $argv +function cc --wraps='opencode --continue' --description 'opencode --continue (auto-starts tmux for visual subagent panes)' + if not set -q TMUX + tmux new-session opencode --continue $argv + else + opencode --continue $argv + end end diff --git a/.config/opencode/opencode.jsonc b/.config/opencode/opencode.jsonc index 2dcc0f6..7b18dcb 100644 --- a/.config/opencode/opencode.jsonc +++ b/.config/opencode/opencode.jsonc @@ -2,7 +2,7 @@ "$schema": "https://opencode.ai/config.json", "autoupdate": true, "default_agent": "lead", - "plugin": ["@tarquinen/opencode-dcp"], + "plugin": ["@tarquinen/opencode-dcp", "./plugins/tmux-panes.ts"], "agent": { "general": { "disable": true diff --git a/.config/opencode/plugins/tmux-panes.ts b/.config/opencode/plugins/tmux-panes.ts new file mode 100644 index 0000000..72c89fa --- /dev/null +++ b/.config/opencode/plugins/tmux-panes.ts @@ -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() // 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 diff --git a/.config/opencode/skills/tmux-session/SKILL.md b/.config/opencode/skills/tmux-session/SKILL.md new file mode 100644 index 0000000..1ab8f5b --- /dev/null +++ b/.config/opencode/skills/tmux-session/SKILL.md @@ -0,0 +1,100 @@ +--- +name: tmux-session +description: Manage persistent terminal sessions in tmux for long-running processes, dev servers, and interactive tools — load when a task needs a background process or interactive shell +permalink: opencode-config/skills/tmux-session/skill +--- + +## When to Use This Skill + +Load this skill when a task requires: +- Running a **dev server or watcher** that must stay alive across multiple tool calls (e.g. `npm run dev`, `cargo watch`, `pytest --watch`) +- An **interactive REPL or debugger** that needs to persist state between commands +- Running a process **in the background** while working in the main pane +- **Parallel worktree work** where each feature branch gets its own named window + +Do NOT use tmux for one-shot commands that complete and exit — use `bash` directly for those. + +## Naming Convention + +All opencode-managed sessions and windows use the `oc-` prefix: + +| Resource | Name pattern | Example | +|---|---|---| +| Named session | `oc-` | `oc-myapp` | +| Named window | `oc-` | `oc-auth-refactor` | +| Background process window | `oc-bg-` | `oc-bg-dev-server` | + +## Starting a Persistent Session + +```bash +# Check if already inside tmux +echo $TMUX + +# Start a new named session (detached) for a long-running process +tmux new-session -d -s oc-bg-dev-server "npm run dev" + +# Or in a new window within the current session +tmux new-window -n oc-bg-dev-server "npm run dev" +``` + +## Sending Commands to a Running Session + +```bash +# Send a command to a named session +tmux send-keys -t oc-bg-dev-server "npm run build" Enter + +# Read the last N lines of output from a pane +tmux capture-pane -t oc-bg-dev-server -p | tail -20 +``` + +## Checking if a Session/Window Exists + +```bash +# Check for a named session +tmux has-session -t oc-bg-dev-server 2>/dev/null && echo "running" || echo "not running" + +# List all oc- prefixed windows in current session +tmux list-windows -F "#{window_name}" | grep "^oc-" +``` + +## Worktree + Window Workflow + +When working across multiple git worktrees, open each in its own tmux window: + +```bash +# Create worktree and open it in a dedicated window +git worktree add .worktrees/auth-refactor -b auth-refactor +tmux new-window -n oc-auth-refactor -c .worktrees/auth-refactor +``` + +Switch between worktrees by switching windows: +```bash +tmux select-window -t oc-auth-refactor +``` + +## Cleanup + +Always clean up sessions and windows when done: + +```bash +# Kill a specific window +tmux kill-window -t oc-bg-dev-server + +# Kill a detached session +tmux kill-session -t oc-bg-dev-server + +# Kill all oc- prefixed windows in current session +tmux list-windows -F "#{window_name}" | grep "^oc-" | xargs -I{} tmux kill-window -t {} +``` + +## Checking Process Output + +Before assuming a background process is healthy, capture its recent output: + +```bash +# Capture last 30 lines of a pane +tmux capture-pane -t oc-bg-dev-server -p -S -30 + +# Check if process is still running (exit code 0 = alive) +tmux has-session -t oc-bg-dev-server 2>/dev/null +```