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:
@@ -1,3 +1,7 @@
|
|||||||
function c --wraps=opencode --description 'alias c opencode'
|
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
|
opencode $argv
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
function cc --wraps='opencode --continue' --description 'alias cc opencode --continue'
|
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
|
opencode --continue $argv
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://opencode.ai/config.json",
|
"$schema": "https://opencode.ai/config.json",
|
||||||
"autoupdate": true,
|
"autoupdate": true,
|
||||||
"default_agent": "lead",
|
"default_agent": "lead",
|
||||||
"plugin": ["@tarquinen/opencode-dcp"],
|
"plugin": ["@tarquinen/opencode-dcp", "./plugins/tmux-panes.ts"],
|
||||||
"agent": {
|
"agent": {
|
||||||
"general": {
|
"general": {
|
||||||
"disable": true
|
"disable": true
|
||||||
|
|||||||
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
|
||||||
100
.config/opencode/skills/tmux-session/SKILL.md
Normal file
100
.config/opencode/skills/tmux-session/SKILL.md
Normal file
@@ -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-<project>` | `oc-myapp` |
|
||||||
|
| Named window | `oc-<feature>` | `oc-auth-refactor` |
|
||||||
|
| Background process window | `oc-bg-<process>` | `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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user