ctx.terminals
Open and manage terminal tabs through TerminalService. The terminal is a core feature — a built-in DockKind like the editor — so this mirrors ctx.editors: create opens a terminal in a workspace, and closeWorkspace reaps a workspace's terminals. The tab renders from the workspace's terminal records; the PTY session lives on ctx.process.
ctx.terminals: TerminalServiceExample
// open a shell terminal in the active workspace, rooted at a folder
ctx.terminals.create({ cwd: "/path/to/project" });
// open a specific kind in a specific workspace
ctx.terminals.create({ kind: "claude", workspaceId });Methods
On ctx.terminals. Method names link to the full signature.
| Method | What it does |
|---|---|
create(input?) | Open a new terminal in a workspace (defaults to the active one). Returns its record. |
closeWorkspace(id) | Close and kill every terminal in a workspace (e.g. on workspace delete). |
focus(terminalId) | Switch to the workspace containing this terminal and activate its tab in the center dock. No-ops for unknown ids. |
getTabMenuItems(terminalId) | The terminal's tab context-menu rows — Rename… plus terminal/tab contributions. |
Tab context menu
getTabMenuItems(terminalId) returns the rows of that terminal's tab context menu — Rename…, then any "terminal/tab"-surface contributions.
Use it when your own UI lists terminals (an agent list, a session picker) so right-clicking a row offers the same actions as right-clicking the tab, contributions included, instead of a menu that drifts from it.
ctx.ui.showMenu({
items: [
{ label: "Mark as seen", run: () => ctx.agents.acknowledge(id) },
{ type: "separator" },
...ctx.terminals.getTabMenuItems(id),
],
at: { x: e.clientX, y: e.clientY },
});Returns an empty array for a terminal no workspace owns. The owner is resolved for you, so a surface spanning every workspace can pass any terminal id.
Tab adornments
Leading icons and trailing Phosphor indicators on terminal tabs. Prefer the adorn verbs (setIndicator / bindIndicator / …) — full guide: Tab adornments.
registerTabDecoration remains as a deprecated shim over bindIndicator.
ctx.subscriptions.push(
ctx.terminals.bindActivity({
id: "my-ext.tab",
provide(terminalId) {
if (!isBusy(terminalId)) return null;
return { activity: "working", tooltip: "Working" };
},
}),
);
ctx.terminals.invalidateTabAdornments();| Method | What it does |
|---|---|
setActivity / clearActivity / flashActivity / bindActivity | Host-owned Activity (ADR 0030) |
setIndicator / clearIndicator / flashIndicator / bindIndicator | Trailing static Phosphor indicators |
setIcon / clearIcon / bindIcon | Leading ReactNode icons |
invalidateTabAdornments | Re-query binders |
registerTabDecoration (deprecated) | Shim → bindIndicator |
OSC events
Subscribe to raw OSC (Operating System Command) escape sequences emitted by a terminal's PTY. Unlike the title that appears on the tab, this fires from the raw output stream regardless of whether the terminal's panel is mounted — making it reliable for background workspace monitoring.
Common OSC codes:
| Code | Meaning |
|---|---|
0 | Set window/tab title |
7 | Working directory (file://…) |
9 | iTerm2 notification (attention, progress) |
133 | Shell prompt marker (semantic shell) |
const BRAILLE_START = 0x2800;
const BRAILLE_END = 0x28ff;
const IDLE_CHAR = "\u2733"; // ✳ — Claude Code idle/waiting signal
ctx.subscriptions.push(
ctx.terminals.subscribeOsc(terminalId, ({ code, payload }) => {
if (code !== 0) return;
const first = payload.charCodeAt(0);
if (first >= BRAILLE_START && first <= BRAILLE_END) {
setStatus(terminalId, "busy"); // agent is running
} else if (payload.startsWith(IDLE_CHAR)) {
setStatus(terminalId, "idle"); // agent is waiting for input
}
}),
);| Method | What it does |
|---|---|
subscribeOsc(terminalId, handler, options?) | Subscribe to parsed OSC sequences from a terminal's PTY stream. Live output only unless { includeReplay: true } — see Replayed scrollback. Returns a Disposable. |
Each event is an OscEvent.
Raw output
Subscribe to the raw PTY output stream of a terminal. The handler receives every chunk of bytes the PTY produces — ANSI escape sequences, OSC sequences, plain text — exactly as they arrive, before any parsing. This fires regardless of whether the terminal's panel is visible, making it suitable for background activity monitoring (for example, confirming an agent is still producing output between OSC signals).
Keep handlers lightweight: they execute synchronously on every PTY chunk, which can be multiple times per second while a program is running.
// Track the last time any output arrived to confirm agent activity.
let lastOutputAt = 0;
ctx.subscriptions.push(
ctx.terminals.subscribeOutput(terminalId, () => {
lastOutputAt = Date.now();
}),
);| Method | What it does |
|---|---|
subscribeOutput(terminalId, handler, options?) | Subscribe to raw PTY output chunks from a terminal. Returns a Disposable. |
Replayed scrollback
A terminal session outlives the app. Attaching to one that is already running makes the session host replay its recent scrollback, and those bytes look exactly like output arriving right now.
Both subscribeOutput and subscribeOsc deliver live output only by default, so you never have to think about this: re-attaching to a terminal cannot make your extension believe a burst of activity just happened. That default is what you want for anything that treats output as a signal — "the agent is working", "something changed", a notification.
Pass { includeReplay: true } when you want the history: rendering scrollback, or working out which program is running in a terminal you have only just attached to. Each chunk then arrives with an OutputOrigin saying which kind it is.
ctx.subscriptions.push(
ctx.terminals.subscribeOutput(
terminalId,
(chunk, { replay }) => {
identifyProgram(chunk); // history is evidence…
if (!replay) noteActivity(); // …but only live output is activity
},
{ includeReplay: true },
),
);Active terminal
Track which terminal tab the user is looking at. "Active" is the center dock's single active panel of the active workspace — null when an editor tab (or nothing) is active, and transiently during workspace switches before the incoming workspace's active tab is published. A terminal that is merely visible in a non-active split does not count.
// clear a "needs attention" marker once the user views the terminal
ctx.subscriptions.push(
ctx.terminals.subscribeActive((terminalId) => {
if (terminalId) attention.delete(terminalId);
}),
);| Method | What it does |
|---|---|
getActive() | The record id of the active center-dock terminal tab, or null. |
subscribeActive(listener) | Subscribe to active-terminal changes (tab activation, group activation, workspace switch). Returns a Disposable. |
Types
Pass TerminalService.
Related: CreateTerminalInput · TerminalRecord · TerminalKind · TerminalTabDecoration · TerminalTabDecorationProvider · OscEvent.
See also
Persistent sessions live on ctx.process. Other State members on ctx.