Terminal
A terminal panel tab, driven by whatever shell profile you give it or register.
terminal adds a terminal to the panel, in a tab beside Problems and Logs. On its own it ships
with no shell: it draws tabs and runs xterm in them, but there is nothing to run inside one,
and the panel says so rather than sitting empty. Pair it with
just-bash (an interpreter over the workbench's own files) or
WebContainer (real node, with npm installs and dev servers), or bring
your own shell.
import { Workbench, FileSystem } from "codelet/workbench";
import { terminal } from "codelet/extensions/terminal";
import { justBash } from "codelet/extensions/terminal/just-bash";
const workbench = new Workbench({
parent: document.getElementById("app")!,
fs: new FileSystem({ "/README.md": "# Hello" }),
extensions: [terminal(), justBash()],
});#Options
| Option | Type | Default | Description |
|---|---|---|---|
shells | ShellProvider[] | [] | Shells this workbench has that no extension brought. Offered alongside every contributed profile, not instead of them. |
cdn | string | "https://esm.sh" | Where xterm, its fit and web-links addons, and xterm.css are fetched from. |
scrollback | number | 100_000 | How many characters of each tab's output are kept, for a second reader — see below. Not what the tab itself shows. |
#Try it
The panel opens on the Terminal tab. Type ls to see the workbench's own files there, then
sh hello.sh to run one of them. xterm and its addons, plus just-bash to run in the tab, load
from a CDN on first open, so the first open takes a moment.
#Bringing your own shell
A tab needs a Pseudoterminal: something with onDidWrite for what to draw, and optionally
handleInput for what the reader typed. There are two ways to hand the terminal one.
Here is a tiny shell, an echo terminal, to see the shape of one:
import { EventEmitter } from "codelet/extensions";
import type { Pseudoterminal } from "codelet/extensions/terminal";
function openEcho(): Pseudoterminal {
const written = new EventEmitter<string>();
let line = "";
return {
onDidWrite: written.event,
open: () => written.fire("echo shell\r\n$ "),
close: () => written.dispose(),
handleInput(data) {
if (data !== "\r") {
line += data;
written.fire(data);
return;
}
written.fire(`\r\n${line}\r\n$ `);
line = "";
},
};
}Pass shells. Each entry is a ShellProvider: a name and an open() that returns a
Pseudoterminal. This is the shape for a shell your page already has, such as a socket to your
own backend.
import { terminal, type ShellProvider } from "codelet/extensions/terminal";
const echoShell: ShellProvider = { name: "Echo", open: openEcho };
terminal({ shells: [echoShell] });Contribute a profile. An extension can offer a shell without the terminal extension knowing
anything about it: declare contributes.terminal.profiles in the manifest, and answer for that
id with window.registerTerminalProfileProvider. This is the route for a shell that ships as its
own extension, the way justBash does.
import { defineExtension } from "codelet/extensions";
const echoTerminal = defineExtension({
manifest: {
name: "echo-terminal",
contributes: { terminal: { profiles: [{ id: "echo.shell", title: "Echo" }] } },
},
activate(context, vscode) {
context.subscriptions.push(
vscode.window.registerTerminalProfileProvider("echo.shell", {
provideTerminalProfile: () => new vscode.TerminalProfile({ name: "Echo", pty: openEcho() }),
}),
);
},
});Either way, what you implement is a Pseudoterminal:
open(dimensions?)— called once, when the tab is created.close()— called when the reader closes the tab, or the extension stops.handleInput?(data)— called with what the reader typed.onDidWrite— anEvent<string>you fire with what the shell has to say. This is the only thing that draws anything in the tab.onDidClose?— fire this if the shell ends on its own; the tab closes with it.setDimensions?(dimensions)— called when the tab is resized.
Both routes are read fresh every time a tab is opened, so an extension stopped from the Extensions view takes its shells out of the "New Terminal" menu, and started again puts them back.
#The tab strip
The tabs run down the right edge, as VSCode places its own, and across the foot of the panel on a
narrow screen. Both are one stop in the page's tab order: Tab reaches the strip, the arrow keys
walk it (up and down as a column, left and right across the foot) with Home and End, and each
tab's close button is reachable from the keyboard rather than appearing only under a pointer.
Note
When there is more than one shell to choose from, "New Terminal" opens a picker instead of opening the only one there is.
#The footer item
The extension puts a terminal button in the status bar. Clicking it opens the panel on the Terminal tab, and clicking it again closes the panel — so a terminal is one click away without the panel being open to find the tab in. When more than one session is running, the count is shown beside the icon.
#What's fetched, and when
xterm, its two addons and xterm.css are fetched from cdn the first time the panel is opened
on the Terminal tab, not when the extension is added. A workbench nobody opens a terminal in
downloads none of it.
The stylesheet goes into a shadow root around the terminals rather than into your page. That
is deliberate: embedding codelet is one import and no CSS build, and a global .xterm rule set
in your document would break that promise. Nothing of xterm's styling — its own, or the
stylesheets it generates for itself at runtime — reaches your page, and nothing in your page's
reset or font stack reaches the terminal.
#Sessions survive everything but closing them
A tab keeps its live terminal — buffer, scrollback, selection, cursor, the lot — across the panel being closed and reopened, the panel strip moving to Problems and back, and a light/dark theme change. Nothing is rebuilt and nothing is replayed: the theme is applied to the running terminal in place. Only the reader closing the tab, or the shell ending itself, closes a session for good.
scrollback is therefore not what you get back. It caps a copy of each tab's output that the
extension keeps for a second reader — live replaying a shared shell to
a guest who joined an hour in — and for tabs opened by another extension before xterm had
finished loading. The cut is taken at a line break near the limit, so a colour never gets cut in
half.