Skip to content

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.

ts
ctx.terminals: TerminalService

Example

tsx
// 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.

MethodWhat 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.

ts
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.

ts
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();
MethodWhat it does
setActivity / clearActivity / flashActivity / bindActivityHost-owned Activity (ADR 0030)
setIndicator / clearIndicator / flashIndicator / bindIndicatorTrailing static Phosphor indicators
setIcon / clearIcon / bindIconLeading ReactNode icons
invalidateTabAdornmentsRe-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:

CodeMeaning
0Set window/tab title
7Working directory (file://…)
9iTerm2 notification (attention, progress)
133Shell prompt marker (semantic shell)
ts
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
    }
  }),
);
MethodWhat 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.

ts
// Track the last time any output arrived to confirm agent activity.
let lastOutputAt = 0;
ctx.subscriptions.push(
  ctx.terminals.subscribeOutput(terminalId, () => {
    lastOutputAt = Date.now();
  }),
);
MethodWhat 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.

ts
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 },
  ),
);

See SubscribeOutputOptions.

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.

ts
// clear a "needs attention" marker once the user views the terminal
ctx.subscriptions.push(
  ctx.terminals.subscribeActive((terminalId) => {
    if (terminalId) attention.delete(terminalId);
  }),
);
MethodWhat 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.