Skip to content

Terminal Channel ​

A terminal channel carries the state of a single pseudo-terminal (pty) process — a shell, dev server, build task, or other long-running command. Terminals live independently of any session that may have created them, and may be claimed by clients or by sessions.

For a hands-on walkthrough of terminal flows, see the Terminals guide.

URI ​

<scheme>:/<id>

The scheme and path of a terminal URI are server-defined. The lightweight terminal catalogue carried on RootState.terminals advertises each live terminal's URI; clients subscribe to a terminal URI to get its full state.

State ​

Subscribers receive a TerminalState snapshot:

typescript
TerminalState {
  title: string
  cwd?: URI
  cols?: number
  rows?: number
  content: TerminalContentPart[]
  lifecycle: TerminalLifecycleState
  claim: TerminalClaim
  supportsCommandDetection?: boolean
}

content is an ordered array of typed parts. Each part is either unclassified (raw VT output) or a structured command part carrying the command line, accumulated output, exit code, and duration. See the Terminals guide for the part shapes.

A terminal is always owned — the claim field records whether it belongs to a client or a session. Its lifecycle is { status: "running" } while active and { status: "exited", exitCode?: number } after exit.

Historical terminal results ​

A terminal URI referenced by ToolResultTerminalContent.resource remains the single subscription identity after its command completes. While the terminal resource's lifecycle is running, subscriptions return its current running TerminalState. When its lifecycle is exited, subscriptions MUST return an exited TerminalState with the retained TerminalContentPart[]. The server MAY reconstruct this state lazily from persisted output; retaining a live process or an in-memory terminal instance is not required.

The retained terminal resource follows the lifecycle of its owning chat or session. Once that owner and its retained data are removed, subscribing may return NotFound.

Lifecycle ​

Creation ​

createTerminal is a JSON-RPC request that allocates a new terminal with a required initial claim plus optional name, cwd, and dimensions:

jsonc
// Client → Server
{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "createTerminal",
  "params": {
    "channel": "ahp-terminal:/<id>",
    "claim": { "kind": "client", "clientId": "client-abc" },
    "name": "build",
    "cwd": "file:///workspace",
    "cols": 120,
    "rows": 30
  }
}

After creation, the server dispatches root/terminalsChanged so that subscribers of ahp-root:// see the new entry in the terminal catalogue, and the client SHOULD subscribe to the terminal URI to receive the full state.

Disposal ​

disposeTerminal kills the underlying pty (if running) and removes the terminal. The server dispatches root/terminalsChanged to reflect the new catalogue. There is no "release without disposal" — when a terminal is no longer needed, it is disposed.

Methods and events on this channel ​

This section lists wire methods that are interpreted in the context of a terminal URI (ahp-terminal:/<id>).

Commands (params.channel = "ahp-terminal:/<id>") ​

MethodKindPurpose
createTerminalrequestCreate a terminal at the chosen URI with an initial claim.
disposeTerminalrequestDispose this terminal and kill its pty if still running.

Notifications (params.channel = "ahp-terminal:/<id>") ​

MethodKindMeaning
actionserver → client notificationTerminal action envelope (terminal/* action payloads).
dispatchActionclient → server notificationDispatch terminal client actions (terminal/input, terminal/resized, terminal/claimed, terminal/titleChanged, terminal/cleared).
unsubscribeclient → server notificationStop receiving messages for this terminal channel.

Actions ​

All terminal-scoped action envelopes carry channel: "<terminal-uri>". Action payloads do NOT carry their own terminal URI.

Data flow ​

ActionClient-dispatchableReducer effect
terminal/dataNoAppends to the tail content part
terminal/inputYesNo-op (server forwards to the pty)

terminal/data is server-only. terminal/input is a side-effect-only client action — the client dispatches keyboard input and the server forwards it to the pty process. The reducer does not modify state on terminal/input; any resulting output arrives later via terminal/data.

Why two separate actions?

Terminal I/O is intentionally split into terminal/input (client → pty) and terminal/data (pty → client) because standard write-ahead reconciliation is not safe for terminals. A pty is a stateful, mutable process — optimistically applying input or predicting output would produce incorrect state. By keeping input as a side-effect-only action and output as server-authoritative, clients avoid the reconciliation pitfalls that would arise from treating terminal I/O like normal state actions.

Command detection ​

Shell-integrated terminals can announce command boundaries:

ActionClient-dispatchableReducer effect
terminal/commandDetectionAvailableNoSets supportsCommandDetection: true
terminal/commandExecutedNoAppends a command part, sets supportsCommandDetection: true
terminal/commandFinishedNoMarks the matching command part complete with exit code & duration

The server MUST NOT include shell integration escape sequences in terminal/data — they MUST be stripped before dispatch. Clients MUST check supportsCommandDetection before relying on command boundaries.

Control ​

ActionClient-dispatchableReducer effect
terminal/resizedYesSets cols, rows
terminal/claimedYesSets claim
terminal/titleChangedYesSets title
terminal/cwdChangedNoSets cwd
terminal/exitedNoSets lifecycle to exited with the optional exitCode
terminal/clearedYesResets content to []

Commands ​

Catalogue Notifications ​

The lightweight terminal catalogue on RootState.terminals is kept in sync via the server-side root/terminalsChanged action on the Root Channel. Terminal channels themselves emit no protocol notifications — only action envelopes.

Released under the MIT License.