phux
guides

OpenCode integration

@phux/opencode adds six bounded terminal tools and cache-preserving fleet awareness while an external local phux server owns the terminals.

evolving document
Full source summary

@phux/opencode adds six bounded terminal tools and cache-preserving fleet awareness while an external local phux server owns the terminals. Targets resolve from an explicit argument, the latest created pane, then PHUX_TARGET. The plugin uses public OpenCode hooks; it does not embed a TUI, paste, or connect to remote phux transports.

Requirements

The package requires Node.js 22 or newer, OpenCode V2, a compatible external phux executable, and a running local phux server. It does not bundle or start phux. The current adapter targets phux 0.16.0 and its versioned CLI shapes:

phux --version

The package uses the public OpenCode V2 Promise API and pins the exact @opencode-ai/plugin prerelease it is tested against. V2 remains beta, so each dependency update must pass the package gates and packed-artifact smoke before release.

Install and load

OpenCode documents two plugin-loading paths: npm package names in opencode.json, and JavaScript or TypeScript modules discovered under .opencode/plugins/ or ~/.config/opencode/plugins/. Use the package-name form for a registry release:

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": ["@phux/opencode"]
}

OpenCode installs npm plugins at startup. Do not also add a local shim for the same package; local and npm plugins are separate load sources and would create two plugin instances.

Load from a checkout

Build the integration, install it as a dependency of the project’s OpenCode config directory, and expose it through OpenCode’s documented local-plugin directory:

cd /absolute/path/to/phux/integrations/opencode
npm ci
npm run build

cd /absolute/path/to/your/project
mkdir -p .opencode/plugins
npm install --prefix .opencode --save-exact /absolute/path/to/phux/integrations/opencode
cat > .opencode/plugins/phux.js <<'EOF'
export { default } from "@phux/opencode"
EOF

The named re-export gives OpenCode one local plugin function. The project does not need a plugin entry in opencode.json for an automatically discovered local file.

Load a packed artifact

Packing tests the same files that a registry release contains without claiming that a version is published:

cd /absolute/path/to/phux/integrations/opencode
npm ci
npm pack --pack-destination /tmp

cd /absolute/path/to/your/project
mkdir -p .opencode/plugins
npm install --prefix .opencode --save-exact /tmp/phux-opencode-0.1.0.tgz
cat > .opencode/plugins/phux.js <<'EOF'
export { default } from "@phux/opencode"
EOF

The packed runtime is standalone JavaScript plus declarations and the package README. It still requires the external phux executable.

Runtime configuration

The normal configuration boundary is the environment inherited by OpenCode:

PHUX_SOCKET=/absolute/path/to/phux.sock \
PHUX_TARGET=@42 \
opencode

PHUX_SOCKET chooses a non-default local Unix socket. PHUX_TARGET is an optional initial target and is read when the plugin instance starts. PHUX_TERMINAL_ID, normally inherited automatically inside phux, identifies OpenCode’s own pane in fleet context. Set PHUX_CONTEXT_AWARENESS=0 to disable that context. Restart OpenCode after changing these variables.

The public OpenCode plugin config type also accepts options alongside an npm package entry:

{
  "$schema": "https://opencode.ai/config.json",
  "plugins": [
    {
      "package": "@phux/opencode",
      "options": {
        "executable": "/absolute/path/to/phux",
        "socket": "/absolute/path/to/phux.sock",
        "lifecycleTimeoutMs": 1000,
        "contextAwareness": true,
        "contextTimeoutMs": 1000
      }
    }
  ]
}

executable and socket override the CLI path and socket for that plugin instance. lifecycleTimeoutMs bounds each best-effort lifecycle subprocess. contextAwareness overrides the environment opt-out, and contextTimeoutMs bounds each fleet refresh. Both timeouts default to 1000 ms. These options apply to a configured npm entry. An automatically discovered local shim should use PATH, PHUX_SOCKET, PHUX_TARGET, and PHUX_CONTEXT_AWARENESS instead.

Automatic fleet context

Before each OpenCode V2 model dispatch, the public session-context hook reads the phux agent inventory. The first observation is a bounded checkpoint; later changes become sequenced deltas. When the inventory is unchanged, the plugin reuses the exact same system suffix, preserving the static prompt and tool prefix for provider caching.

A checkpoint carries OpenCode’s inherited Terminal id, the selected target, and up to 64 sorted pane records: canonical Terminal/session/window identity, agent label and kind, lifecycle state, attention, and cwd. Context is capped at 8 KiB and reports omitted panes. Values are marked as untrusted observations. Screen rows, scrollback, titles, detector evidence, explanations, tool output, and credentials are excluded; terminal content remains an explicit tool read.

Because the suffix is reconstructed from plugin state for every dispatch, it also survives compaction without using a private compactor hook. A missing server emits one edge-filtered unavailable checkpoint and does not fail the user turn. Awareness is current at model-dispatch boundaries rather than continuously while a provider response is streaming. The shared rationale is ADR-0067.

The six tools

ToolOpenCode-facing behavior
phux_listLists sessions without changing phux focus.
phux_createCreates a named session without attaching and selects its seed pane for this plugin instance. Its optional command is argv.
phux_snapshotReads a bounded screen projection without attaching or resizing.
phux_send_keysSends named or literal key items. It is not paste.
phux_runRuns one shell command string through the phux sentinel and returns its result.
phux_waitWaits for visible text, idleness, or indefinitely when all conditions and deadlines are omitted.

The headless CLI owns selector syntax, command semantics, JSON shapes, and exit codes. Use the agent CLI guide for that contract rather than inferring a second CLI from these tool descriptions.

until and idle_ms are mutually exclusive. timeout_seconds is the phux operation deadline; local_timeout_ms is the adapter subprocess deadline. Short operations default the local deadline to 10 seconds. phux_run and phux_wait have no implicit local deadline, preserving their documented indefinite forms. OpenCode’s tool abort signal is passed to every subprocess. Snapshot, run, and wait output sent to the model is limited to the newest 200 lines and 12 KiB, with an explicit notice when the adapter or phux truncated it.

Target selection and concurrency

Every targeted tool resolves its pane in this exact order:

  1. the tool’s explicit target argument;
  2. the seed pane selected by the latest successful phux_create in this plugin instance;
  3. PHUX_TARGET captured at plugin startup.

The tool fails when all three are absent. It never silently uses phux focus. An explicit target overrides both selected and environment targets.

Selection belongs to the plugin instance, not to an OpenCode session. Two concurrent OpenCode sessions using the same instance therefore share one mutable selected target, and concurrent creates are last-completion-wins. Use explicit targets when sessions or tool calls can overlap.

The plugin does not lock the PTY, serialize terminal tools, reserve a prompt, or make snapshot followed by input transactional. A human attach, another agent, and phux_send_keys or phux_run can interleave. Lifecycle metadata is queued to avoid overlapping metadata writes, but it is status, not an input lock. Coordinate writers and prefer discrete phux_run calls where possible.

Lifecycle metadata and gaps

Lifecycle reporting is best effort and uses only the public OpenCode server plugin surface.

Public signalBehavior
session.status with busyDeclares identity for the current target, if not already declared there.
session.status with idleSame; a turn boundary declares nothing new and writes nothing.
session.idleSame.
successful phux_createDeclares identity for that tool’s public OpenCode session.
session.deletedOwnership-checks and clears that session’s declaration.
plugin disposeBest-effort ownership-checks and clears declarations known to this instance.

Records use name=opencode, kind=opencode, and owner opencode:<public OpenCode session id>identity only, never a state. A declared state outranks the server’s own derivation for the record’s whole lifetime (../spec/L3.md §3.7, ADR-0046 point 8), so reporting one would stand the shipped rules/opencode.toml detector down on every pane running this plugin. The server derives working and blocked from that manifest instead.

The record is written once per session and target, not once per event. A whole-record write carries state: "unknown", so rewriting identity at a turn boundary would clobber the server’s derivation and publish a working -> unknown edge that phux agent wait reads as the agent departing.

Before clearing, the plugin reads the current declaration and requires its name, kind, and owner to still match. It therefore preserves metadata replaced by another owner.

Retry status, session.created, session.error, and unrelated events do not invent transitions. OpenCode 1.18.1 has no documented event that distinguishes a plugin reload from final disposal, so reload preservation is not claimed. A process crash, SIGKILL, or other forced termination cannot run disposal. Metadata failures and local deadlines do not fail terminal tools.

Shared CLI boundary and other adapters

The source reuses the host-independent PhuxCli adapter maintained with the Pi integration. The OpenCode build bundles that adapter, its schema validation, and the tool runtime into the artifact. The packed runtime has no dependency on @phux/pi; it retains an exact production dependency on the public OpenCode V2 plugin API and still executes the external phux CLI. This shared implementation boundary does not make Pi target persistence, commands, or lifecycle behavior part of the OpenCode contract.

Use Pi when Pi-native target persistence and human commands are the needed host surface. Use phux-mcp when a client speaks MCP over stdio and needs that adapter’s broader catalog. Those guides own their own contracts; this page does not duplicate them.

Human attach and current safety boundaries

To join a session, construct argv for a separate real terminal, for example:

["phux", "attach", "--socket", "/absolute/path/to/phux.sock", "work"]

Treat this as argv, not a shell string to eval. The OpenCode plugin does not execute attach, open a nested terminal, or navigate the human client. A human attach is a live writer, not a read-only monitor, so coordinate it with agent input.

Current boundaries are explicit:

  • There is no TUI embedding and no dependency on OpenCode TUI internals.
  • There is no WASM build; this is a Node adapter around an external native process.
  • There is no paste tool. phux_send_keys sends key items and must not be presented as clipboard or bracketed-paste support.
  • There is no remote pairing or remote transport configuration. The plugin accepts a local Unix socket, not QUIC, WebSocket, bearer-token, certificate, or pairing arguments.
  • Never place pairing tokens, certificate material, or other remote credentials in tool arguments, targets, lifecycle records, attach guidance, or smoke output.

Package-development commands and the opt-in real smoke live in ../../integrations/opencode/README.md.

View exact source