# PTY support — agents can now drive real terminals

The headline of this release. `bash` now accepts `pty: true` (with `background: true`) to spawn commands inside a real PTY — every interactive program that needed a terminal is now reachable from an agent loop. Python and Node REPLs, `vim`, `htop`, `top`, `less`, `fzf`, build TUIs, even a nested `opencode` session — all work end-to-end.

![Yo dawg, I heard you like OpenCode so I put an OpenCode inside your OpenCode](assets/ocinoc.png)

Yes, really — `opencode` inside `opencode` works. PTY support means the agent can drive any TUI, including a full nested AFT-equipped OpenCode session, complete with sidebar, MCP servers, LSP status, and another agent answering prompts. Recursion all the way down.

### How it works

- **`bash({pty: true, background: true, ptyRows?, ptyCols?})`** — spawn a PTY-backed task. Defaults are 24×80; caps are 60×140 to keep `bash_status` snapshots bounded.
- **`bash_status({taskId, outputMode})`** — read the terminal state.
  - `"screen"` — vt100-rendered visible terminal (rows × cols characters)
  - `"raw"` — uncompressed bytes including ANSI escape sequences
  - `"both"` — separate fields for each
- **`bash_write({taskId, input})`** — send keystrokes. Input is either a verbatim string or an array mixing strings and `{key: "..."}` objects for atomic text + control key sequences:

  ```
  bash_write({taskId, input: [
    "iHello",
    {key: "esc"},
    ":wq",
    {key: "enter"},
  ]})
  ```

  Named keys cover `enter`/`return` (CR), `tab`, `space`, `backspace`, `esc`/`escape`, arrow keys, navigation keys, `delete`, `insert`, `f1`–`f12`, and `ctrl-a` through `ctrl-z`.

PTY tasks run on Unix via `portable-pty` and on Windows via ConPTY.

## bash_watch unifies pattern notifications and sync waits

New `bash_watch` tool replaces ad-hoc wait flags on `bash_status`. Two modes:

**Sync** — `bash_watch({taskId, pattern?, timeoutMs?})` blocks until the pattern matches, the task exits, or timeout. Without a pattern it waits for task exit. Returns the snapshot inline so the agent gets the result without a separate completion reminder.

**Async** — `bash_watch({taskId, pattern, background: true})` registers a pattern watcher and returns immediately. When the pattern matches mid-stream or the task exits, a single `[BG BASH NOTIFY]` reminder fires with the matched line. The default `[BACKGROUND BASH COMPLETED]` reminder is suppressed for that task.

`bash_status` is now a pure snapshot tool — wait/watch semantics live in `bash_watch`.

## URL fetches no longer hang on slow servers

`aft_outline` and `aft_zoom` URL targets now abort with a clear stall error after 15 seconds without a chunk. Previously a slow or stalled server could hang the bridge indefinitely while waiting on `reader.read()`.

## Other

- `bash` schema rejects `pty: true` without `background: true` and `ptyRows`/`ptyCols` without `pty: true`.
- OpenCode subagent sessions silently convert `background: true` to foreground bash unless `bash.subagent_background = true` in config.
- `bash_status` and `bash_kill` are always registered when `bash` is registered (no longer gated on `experimental.bash.background`).
- Background bash completion delivery now persists `completion_delivered` across plugin restarts, so previously-delivered tasks no longer replay as fresh reminders after restart.
- Async `bash_watch` exit notifications render as `task X exited` instead of the prior `matched "exited (exit 0)"` framing.
- The release script blocks minor-version releases when the in-plugin `ANNOUNCEMENT_VERSION` is stale relative to the release tag.
