# Extension GUI compatibility (bundled extensions)

This page answers one question per bundled extension command: when a session runs through an Agent Client
Protocol (ACP) host such as T3 Code, is there anything the user can see? The path is
`T3 Code → selesai-acp → Selesai RPC child` (`src/modes/acp/adapter.ts`, `src/modes/rpc/rpc-mode.ts`). The
interactive TUI is the reference behavior, not something this bridge replaces.

**Scope.** Every entry of `src/extensions/package.json` → `pi.extensions` (22) and every inline builtin in
`src/extensions/index.ts` → `builtInExtensions` (5), with the 52 slash commands those entries actually register.
`src/extensions/pi-rewind-hook/` is vendored but in neither registry, so it is not loaded and not covered.
Extensions you install yourself (npm, git, a local path) are outside this inventory. Their behavior here is
**unknown**, not compatible.

**Evidence labels.**

| Label | Meaning |
| --- | --- |
| `src` | Read from source: the handler reaches that path. No wire run for that row. |
| `proto` | Exercised through the built bridge (`dist/acp-entry.js` with a real RPC child and a loopback model) or the RPC child directly. The frames and values were observed; no window was rendered. |
| `host` | Rendered in a real T3 Code window. **No row on this page has `host` evidence.** |

The `proto` rows lean on `test/acp-stdio.test.ts` and the focused mapper, history, RPC and Ponytail tests. The
per-command source inventory and the probes behind the remaining rows are recorded in the task findings that
produced this page. Nothing here proves how a given T3 version draws a frame.

## What can carry extension feedback

| Channel | What the host receives | Limits |
| --- | --- | --- |
| `ctx.ui.notify(text, severity)` | A fixed severity title (`info`, `warning` or `error`), only if the client negotiated `session.notices` and a turn is started and routed | The extension's own text is never forwarded: it can hold secrets, provider errors or private paths. This is a policy, not a bug. |
| `pi.sendMessage({ display: true, content })` | `agent_message_chunk` text, prefixed with the fixed `Extension output:` label, once per completed message | Text blocks only. 8 KiB per message including label and truncation marker; 32 KiB of projected text per request plus one fixed overflow marker. Hidden messages, `details`, images and resources are not forwarded. ANSI/control sequences are stripped. |
| `pi.sendUserMessage(...)` / `sendMessage(message, { triggerTurn: true })` | A normal model run whose assistant output is the answer | Not a display event. A pure command ends the prompt with `end_turn` and no model request. |
| `ctx.ui.select / confirm / input / editor` | An ACP form, only during a started turn | Command-time dialogs (a slash command's own handler, before its turn is accepted) are cancelled with one fixed line. `multiselect` and `inputKind: "secret"` are never representable. |
| `ctx.ui.custom()`, status/widget/footer/header/editor/theme/terminal-input/autocomplete | Nothing | TUI chrome is stubbed or dropped. `custom()` returns `undefined`, so a command whose only action is a component does nothing. |
| `pi.registerTool` | Normal tool calls | Works; tools can work while the same extension's settings command cannot. |
| Clipboard and editor writes | Nothing in the browser | Clipboard operations run on the machine that runs the Selesai child. A browser clipboard is never touched. |

`ctx.hasUI` is `true` in RPC mode, and ACP children are RPC children. Every `if (!ctx.hasUI)` fallback is
therefore dead code on a GUI surface: the command takes its interactive branch and then blocks on a dialog or
returns silently. `ctx.mode` (`"tui"`, `"rpc"`, `"json"`, `"print"`) is the only reliable interactivity test.

**Display output is publish intent, not a secrecy check.** `display: true` says "this text belongs in the
transcript". It does not make arbitrary strings safe, and there is no sanitizer that removes credentials. Publish
fixed wording or values from a closed set, keep the normal notify for diagnostics, and never convert arbitrary
notify bodies into displayed text.

## The safe output pattern

```typescript
// Terminal UI keeps its normal notice; headless surfaces publish explicit display text.
if (ctx.mode !== "tui") {
  pi.sendMessage({
    customType: "my-status",
    content: "Deploy mode: staging", // fixed text or a validated enum — never a raw error or path
    display: true,
  });
  return;
}
ctx.ui.notify("Deploy mode: staging", "info");
```

- One completed message becomes one transcript update. Do not also send a notify carrying the same text.
- A report longer than 8 KiB is truncated with a visible marker; the TUI can show what ACP cannot.
- Displayed custom messages stay in model context under the existing custom-message semantics. The `display`
  flag controls presentation, not whether the model sees the entry.
- Correlated custom output from an earlier request is refused while another request owns the active run, before
  it can steer or contaminate that run. Explicit `nextTurn` context remains a deliberate future-turn operation.
  Uncorrelated legacy delivery retains its previous behavior; this is not universal background-job isolation.
- Terminal-only commands use `interactiveOnly: true` (see `pi.registerCommand` in
  [Extensions](extensions.md)). They stay listed and runnable in the TUI, are omitted from the ACP command list, and
  are refused with fixed text before their handler when typed in any non-TUI mode. The refusal covers every
  argument branch, so text-only branches of those commands (`/tool-display show`, `/zentui preset …`) are refused
  too, even though they would have worked headless.

## Command-level results

`visible` means the GUI shows output; `private` means the handler runs but its report is a notify body the host
intentionally does not forward; `no-op` means every channel it uses is dropped; `terminal-only` means it is refused
outside the TUI on purpose; `model run` means it starts or queues a model turn; `unsafe` means it mutates session or
branch state the host owns.

| Command | Entry | GUI result | Notes and alternative |
| --- | --- | --- | --- |
| `/ponytail status` | `ponytail/index.js` | `visible` (`proto`) | `Ponytail: current <mode> • default <mode>`, enum only. |
| `/ponytail`, `/ponytail <mode>`, `/ponytail default <mode>` | same | `visible` (`src`; mode change `proto`) | Fixed confirmation; bare `/ponytail` selects the configured default (or full if default is off), not status. The current or default mode is stored in Selesai settings on the host. |
| `/ponytail <junk>`, `/ponytail default <junk>` | same | `visible` (`proto`) | `Unknown or unsupported /ponytail mode.`; the argument is not echoed. |
| `/ponytail-review`, `/ponytail-audit`, `/ponytail-gain`, `/ponytail-debt`, `/ponytail-help` | same | `model run` (`src`; review alias `proto`) | Sends the literal `/skill:<alias>` text as a user message. Skills are not expanded for extension-sent messages, so the model receives that literal text. |
| `/subagents` | `pi-subagents` | `no-op` | Bare admin is an action menu. Use `/subagents <agent> details`, which writes a displayed report. |
| `/run` | same | `model run` (`src`) | Launches a subagent; usage and error text is notify-only. |
| `/subagent-cost` | same | `visible` (`src`, channel `proto`) | A displayed text report; the command itself was observed over RPC, not re-run through the built bridge. |
| `/subagents-doctor`, `/subagents-guide`, `/subagents-refine`, `/subagents-models`, `/subagents-profiles`, `/subagents-load-profile`, `/subagents-refresh-provider-models`, `/subagents-generate-profiles`, `/subagents-check-profile` | same | `visible` (`src`) | Reports use the displayed text channel; errors stay notify-only. |
| `/subagents-inspect-rpc` | same | `no-op` | Replies through `setWidget`, which the bridge drops; nothing reaches the wire. |
| `/subagents-fleet` | same | `visible` (`src`) | Falls back to the text `status` report outside the TUI instead of the fleet component. |
| `/subagents-detach`, `/subagents-stop`, `/subagents-steer` | same | `visible` (`src`) | Result text is displayed; usage errors are notify-only. |
| `/subagents-watchdog` | same | `visible` (`src`) | Status and toggle report through displayed text. |
| `/prompt-workflow` | same | `visible` (`src`) | Empty/list sends a displayed list of discovered prompt files, including host-local paths. Named workflows launch child runs; unknown names notify. |
| `/memory-consolidate`, `/memory-insights`, `/memory-sync-markdown`, `/memory-preview-context`, `/memory-pin`, `/memory-index-sessions`, `/memory-switch-project` | `pi-hermes-memory` | `private` (`src`) | The handlers run, including their mutations; the report is notify-only. |
| `/memory-recover` | same | `private` (`src`) | Dry run prints a notify report. `--apply` asks for confirmation, which is cancelled outside a started turn, so no mutation happens. |
| `/memory-skills` | same | `private` (`src`) | Has an explicit non-TUI read-only list branch, still notify-only. |
| `/memory-interview` | same | `model run` (`src`) | Sends a user message; the run is owned by the submitting prompt. |
| `/learn-memory-tool` | same | `no-op` (`src`) | The selection dialog is cancelled before a choice, so no guide is shown. |
| `/intercom` | `pi-intercom/index.ts` | `no-op` (`src`) | Overlay-only; it returns silently outside the TUI. |
| `/intercom-id` | same | `private` (`src`) | Notifies the session id; the editor insertion is dropped. |
| `/intercom-pool`, `/intercom-pool <name>`, `/intercom-pool default` | same | `private` (`src`) | Pool scope changes happen; text is notify-only. |
| `/alias <name>` | same | `private` (`src`) | Sets the session name; there is no confirmation on the wire. |
| `/alias`, `/alias menu` | same | `no-op` (`src`) | Missing-argument prompt is an input dialog, cancelled outside a started turn. |
| `/handover` | same | `private` (`src`) | Refuses outside the TUI, but the refusal is notify-only: the command has no visible report (generic notices also require a started turn). Use the intercom handover tool action instead. |
| `Alt+M` shortcut | same | `no-op` (`src`) | Shortcuts never fire outside the TUI. |
| `/undo` | `undo.ts` | `unsafe` (`src`) | Restores a checkpoint and navigates the session branch the ACP host owns; result lines are notify-only. Use the TUI. |
| `/undo-status` | same | `private` (`src`) | Runs and reports over notify only. |
| `/setup-agent-browser` | `agent-browser.ts` | `private` (`src`) | Feedback is notify-only; the npm install it may run happens on the agent host. |
| `/cp` | `copy-turn.ts` | `private` (`src`) | Copies on the agent host. `/cp` with no argument, `/cp list`, `/cp <hash>` and errors are notify-only. The browser clipboard is never involved. |
| `/handoff-new` | `handoff-new.ts` | `unsafe` (`src`) | Replaces the Selesai session identity. ACP fails the session closed with a stable error and refuses the next prompt. Use the TUI or a new thread. |
| `/tool-display` | `pi-tool-display` | `terminal-only` (`proto`) | Absent from the ACP command list; every argument branch is refused with fixed text, no handler and no model. |
| `/web-agent` (bare), `/web-agent settings` | `pi-web-agent` | `no-op` (`src`) | The settings modal is a component, so nothing happens. |
| `/web-agent doctor`, `/web-agent show` | same | `private` (`src`) | Reports include runtime versions and absolute config paths; only a fixed notice is sent. |
| `/web-agent changelog`, `/web-agent reset`, `/web-agent mode ...`, invalid arguments | same | `private` (`src`) | These run headless (reset and mode write config) but their text is notify-only. |
| `/zentui` | `pi-zentui/extensions/zentui/index.ts` | `terminal-only` (`proto`) | Absent from the ACP command list; every argument branch is refused with fixed text, no handler and no model. |
| `/tokenin add`, `/tokenin switch`, `/tokenin remove` | `tokenin-onboarding.ts` | `unsupported` (`src`) | Command-time dialogs are cancelled before answering. Token entry is never offered as a masked field; `add` then notifies "No token entered" (notify-only). |
| `/tokenin`, `/tokenin usage` | same | `private` (`src`) | The balance/usage lookup runs; the formatted report is notify-only. |
| `/llama` | `llama.cpp` builtin | `private` (`src`) | Refuses outside the TUI by checking `ctx.mode`, but the reason is a notify body: a handled command has no visible report; generic notices require a started turn. Router management stays in the TUI. |
| `/mcp` | `mcp` builtin | `private` (`src`) | Manager output is notify-only and includes absolute agent-directory paths. MCP tools themselves work. |

Startup and background output follows the same rules and is not a slash command: `Ponytail loaded: …`,
auto-session-name results, compaction reminders, `sai-deci` warnings and subagent background notices are either
notify-only or subject to the display ownership rules above. Correlated stale custom output cannot join another
active run; uncorrelated legacy events and deliberate nextTurn context retain their existing behavior.
The `/tokenin` startup onboarding also returns early when the mode is RPC, but that guard is about startup only: it
does not change the behavior of the `/tokenin` subcommands above.

## Entries without slash commands

| Entry | Surface | GUI result |
| --- | --- | --- |
| `auto-session-name.ts` | `input` hook | Runs; it can add its own naming model request before the busy check. No feedback. |
| `context-compaction-reminder.ts` | hooks | Post-compaction reminder is notify-only. |
| `cost-reconcile.ts` | hooks | Silent settings/usage bookkeeping. |
| `question` | tool | Dialogs work as forms during a started turn (`select` verified `proto`). `multiselect` is cancelled with fixed text; there is no dismiss control, so Stop ends the turn. |
| `grep-app` | tools | Work. |
| `herdr-agent-state.ts` | hooks | Environment-gated external state; silent. |
| `inline-skills.ts` | hooks and autocomplete | Input expansion still runs; the autocomplete provider is TUI-only. |
| `sai-deci` | tools and hooks | Tools work. Advisory notes use the displayed channel when a request owns them; the unavailable warning stays a fixed notice. |
| `rtk.ts` | `tool_call` hook | Silent; runs the external `rtk` binary. |
| `skills-index` | tool | Works. |
| `codemode` builtin | tools | Works. |
| `tool-search` builtin | tools | Works. |
| `todo` builtin | tool and plan update | The todo list reaches the host as an ACP plan update; fresh host rendering was not measured. |

## Dialogs

- `select`, `confirm`, `input` and `editor` become forms only while a turn is started and routed. `question`
  inside a normal tool-driven turn is the working example.
- `multiselect`, `inputKind: "secret"` and empty forms are not representable and are always cancelled. There is no
  boolean-per-option workaround and no unmasked replacement for a secret field.
- A slash command runs before its prompt is acknowledged, so a dialog it raises has no started turn and is
  cancelled. The extension sees the cancellation (no selection, `false` for a confirm) and must fail closed. A
  refusal or a fixed instruction line it writes afterwards is subject to the channel rules above: notify-only
  text is invisible.
- Supported forms are unchanged by session cancellation: Stop cancels a pending form and the extension receives the
  cancellation answer.

## Not delivered

Deferred or unsupported, by decision rather than oversight:

- Generic notify bodies, raw provider or filesystem errors, private `details`, hidden messages and images.
- Interactive components and TUI chrome: settings modals for web-agent, agent-browser, tool-display and zentui;
  memory, intercom and token-onboarding overlays; subagent admin, fleet and inspector widgets; status widgets,
  footers, headers, editor components, terminal input handlers, themes and message renderers.
- `multiselect` and secret input forms.
- Session replacement and branch mutation from inside a command (`/handoff-new`, `/undo`), browser-clipboard and
  editor-restoration effects, and browser widgets.
- Extension packages installed outside this repository. Unknown third-party commands that omit `interactiveOnly`
  keep working exactly as before, including their display and notify behavior.

## Not measured

- Rendering in any T3 Code window. All bridge evidence is protocol-level.
- Live models or providers; the protocol runs use an isolated loopback model.
- End-to-end delivery of displayed reports from bundled commands other than Ponytail. The generic channel and the
  terminal-only refusals were run through the built bridge; each individual report command was not.
- Off-branch replay through a forked session: the history filter is unit-tested, not driven through a built fork.

## See also

- [Selesai in T3 Code (ACP)](acp.md) for the host-facing contract, approvals and replay limits.
- [Extensions](extensions.md) for `pi.sendMessage`, `pi.registerCommand` and `ctx.mode`.
- [RPC Mode](rpc.md) for the child protocol behind this bridge.
