
# Terminal

`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](/extensions/just-bash) (an interpreter over the workbench's own files) or
[WebContainer](/extensions/webcontainer) (real node, with npm installs and dev servers), or bring
your own shell.

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

::codelet-playground{mode="workbench" extensions="terminal" view="terminal" height="480"}

```sh [hello.sh]
#!/bin/sh
echo "hello from just-bash"
```

```md [README.md]
# terminal demo

A workbench with a Terminal tab.
```

::

## 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:

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

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

```ts
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` — an `Event<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`](/extensions/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.

:read-more{to="/extensions/just-bash"}
