// opencode agent-state plugin: mirrors the session state into the tmux pane opencode runs in via // ~/.config/tmux/scripts/agent-state.sh (oc busy|done|input|error). No-op outside tmux. // // Runs in the SERVER process, so TMUX_PANE is only right when the TUI in the pane spawned the // server (plain `opencode`), not for `opencode serve` / `opencode attach`. Under the bwrap // `opencodes` wrapper the tmux socket dir must be bound in, or the script gives up quietly. // Child (subagent) sessions are skipped so a subagent finishing does not flag "done". import type { Plugin } from "@opencode-ai/plugin" export const AgentState: Plugin = async ({ client, $ }) => { const script = `${process.env.HOME}/.config/tmux/scripts/agent-state.sh` const set = async (state: string) => { if (!process.env.TMUX_PANE) return try { await $`${script} oc ${state}`.quiet().nothrow() } catch {} } const childCache = new Map() const isChild = async (sessionID?: string) => { if (!sessionID) return false if (!childCache.has(sessionID)) { try { const r = await client.session.get({ path: { id: sessionID } }) childCache.set(sessionID, Boolean(r.data?.parentID)) } catch { childCache.set(sessionID, false) } } return childCache.get(sessionID) ?? false } return { event: async ({ event }) => { const p = (event as { properties?: Record }).properties ?? {} switch (event.type) { case "message.updated": // a new user message = a prompt went in if (p.info?.role === "user" && !(await isChild(p.info?.sessionID))) await set("busy") break case "permission.asked": if (!(await isChild(p.sessionID))) await set("input") break case "permission.replied": if (!(await isChild(p.sessionID))) await set("busy") break case "session.idle": if (!(await isChild(p.sessionID))) await set("done") break case "session.error": if (!(await isChild(p.sessionID))) await set("error") break } }, } }