# `@volter-ai-dev/supercode-terminal`

## Native Claude rollover evidence

`NativeTerminalSessionHost` (also used by `SupercodeTerminalController`) follows
Claude `/clear` transcript successors while the process keeps its original argv
session ID. iTerm and Terminal observations share one ownership batch. Ambiguous
co-located predecessors and successors claimed by another pane are not adopted;
later predecessor activity or a missing successor revokes the mapping.

The rollover store defaults to `CLAUDE_CONFIG_DIR`, then `~/.claude`. An explicit
`nativeHost.home` isolates discovery to that home's `.claude`; an explicit
`nativeHost.claudeConfigDir` takes precedence. Both rollover evidence and the
existing PID-only Claude fallback use this same native store.

Embedders can gather the same evidence with `createSessionRollTracker` from the
existing `./substrate/session-lifecycle.js` export and supply its map as
`ObserveIo.sessionRolls` (or pass the tracker to `gatherLifecycleObservation`).

Evidence is asynchronous: 16 KiB transcript head/tail reads, a 50-entry head
inspection bound, 500-entry edge caches, and a 15-second successor-scan cadence.
Directory enumeration still scales with the native project inventory. No new
watcher, timer, subprocess, transcript copy, or persistent state is introduced.
Closed-pane chains are pruned; over-capacity evidence fails closed. These are
conservative append-oriented observations, not proof of arbitrary interior
transcript rewrites. Native routing tests use isolated command fixtures, not
live user terminals.

## Overview

Optional terminal/tmux hosting for Volter Harness frontends. It complements
Volter Harness's semantic transcript and runtime APIs with the familiar live terminal
screen; it does not turn terminal bytes into canonical conversation history.

This package owns the shared terminal substrate. `src/substrate/` is its source;
`substrate/` contains the compiled runtime and declarations. The existing host,
controller, and browser client consume these modules directly, without a
the fleet tool package dependency. Advanced hosts and legacy adapters may import
`/substrate/attach.js`, `/substrate/client.js`, `/substrate/local-terminal.js`,
`/substrate/native-host.js`, `/substrate/session-lifecycle.js`,
`/substrate/tmux-stream.js`, and `/substrate/tmux.js`; the two internal helper
subpaths are exported only for compatibility. Prefer the controller/client APIs
above this layer for new applications.

The substrate was moved from a retired package without behavioral changes.
Its existing home paths, environment variables, and tmux ownership markers stay
compatible. Those files retain Apache-2.0 licensing; see `NOTICE-SUBSTRATE` and
`LICENSE-SUBSTRATE`.

The server-side `TmuxTerminalHost` can discover existing tmux sessions, create a
session from a structured command, capture its display, submit input to a
session it owns, prepare a local `tmux
attach-session` launch, open that launch through the trusted local-terminal
adapter, and issue a one-use embedded attachment grant. Existing
sessions are read-only by default. Control and close are allowed only for
sessions bearing this host's ownership mark unless the embedding host explicitly
opts into unowned control.

Most Volter Harness frontends should use `SupercodeTerminalController`, which composes that low-level
host with the loopback bridge, persistent context recovery, native Claude Code/Codex handoff, and
harness-aware initial-input delivery. The embedding product supplies allowed browser origins and
optional display-path formatting; it should not create a second terminal registry or scrape agent
prompts itself. `TmuxTerminalHost` remains public for applications that deliberately need only the
lower-level primitive.

An embedding host may pass an opaque `contextKey` when it creates a session.
The host stores that association in a namespaced tmux user option and returns it
with future catalog reads, so a semantic conversation can recover its terminal
after the browser or native bridge restarts. The value is application-defined;
native harness session ids, tmux targets, sockets, and process handles should
never be used as context keys.

For a brand-new interactive launch, the native session does not exist when tmux is created.
`conversationDiscovery` records the pre-launch opaque catalog keys and first user prompt;
`reconcileConversationBindings` later binds the terminal only when exactly one new descriptor has
the same harness, workspace, and user prompt. Ambiguous matches remain unbound. This recovery
state is bounded, expires after ten minutes, and never exposes a tmux target to the product.

Browser and extension hosts can pair it with `TerminalWebSocketBridge`, which binds an ephemeral
loopback port, requires an exact configured origin on every WebSocket upgrade, and accepts only
one-use attachment grants. The embedding product supplies its allowed origin and never handles a
tmux socket or target. `normalizeTerminalUiState` performs the matching untrusted-wire projection
before state reaches the reusable terminal components.

Browser code imports `@volter-ai-dev/supercode-terminal/client`. It contains only
the terminal WebSocket client and protocol types; it never imports tmux,
`node-pty`, filesystem, or process APIs.

Frontends that want the default terminal experience can import the optional
Preact components from `@volter-ai-dev/supercode-terminal/ui` and its tokenized
stylesheet from `@volter-ai-dev/supercode-terminal/ui/styles.css`. `TerminalWorkspace`
is the terminal-app surface: retained live tabs, one-click session attachment,
keyboard tab switching, direct terminal focus, connection state, and separate detach/open/stop
actions. `TerminalPanel`, `TerminalViewer`, and `TerminalPath` remain independently reusable for
constrained hosts. The surfaces take
callbacks rather than a transport implementation, so a host can map its own
validated intents without exposing native handles. The heavy xterm runtime is a
dynamic import and is loaded only when a viewer is actually opened.

The workspace deliberately leaves browser-owned Command/Ctrl shortcuts alone. While it has focus,
its default namespace is Option/Alt+Shift: `T` opens the session menu, `D` detaches the active tab,
digits 1–9 select tabs, and brackets rotate between tabs.

Hosts with both a fixed simulated viewport and a responsive viewport can pass `viewportMode` and
`onViewportModeChange`. The workspace then exposes one compact scale/fit control for writable
attachments; read-only mirrors never offer a control that could reflow the source pane. The full,
evidence-scoped parity boundary is recorded in [CAPABILITIES.md](CAPABILITIES.md).

The terminal canvas and internal layout layers are transparent over one
`--sctui-terminal-background` backing layer (the Volter brand's `surface.media` by default). Embedders
can set that token to any valid transparent or opaque CSS color without forking the component; the
color is intentionally applied once so nested alpha does not compound back into an opaque card.

Colours and fonts default to the [Volter brand's roles](https://brand.volter.ai/tokens), read as
`var(--volter-<role>, <brand value>)`: a host that sets `--volter-*` (the brand's `tokens.css`)
governs them, and `--sctui-*` or `--scui-*` overrides the one it sets. The terminal is a dark media
surface in either page scheme, so its inks (the ANSI palette, foreground, cursor and the viewer's
own controls) use the brand's dark values. `styles.css` is generated from `src/styles.css`, which
names each role as `brand(<role>)`; `npm run build` resolves them.

Safety properties:

- browser actions use opaque catalog ids rather than tmux targets;
- embedded attachment grants are random, short-lived, and consumed once;
- the tmux socket and session target never appear in an embedded grant;
- detaching disposes the ephemeral PTY/WebSocket resources but cannot stop the
  durable tmux session;
- closing is a separate explicit operation and checks the ownership mark;
- structured commands and argument arrays are passed without shell-string
  construction.

## Use the xterm UI with a browser runtime

The optional `/stream` and `/react/stream` entries export `TerminalStreamViewer`.
They use the same search, links, paste controls, download, status, and keyboard
behavior as `TerminalViewer`, with a caller-supplied connector instead of a
WebSocket. Import `/ui/styles.css` and `@xterm/xterm/css/xterm.css`; install the
xterm, fit, search, and web-links peer packages for this surface.

```jsx
<TerminalStreamViewer
  attachment={{ id: 'workspace-shell', sessionId: 'workspace', baseUrl: '', mode: 'control' }}
  connect={(attachment, events, { cols, rows }) => {
    // Subscribe to process output, report readiness with events.onOpen(),
    // forward output through events.onOutput(data), and report exit/close.
    return { write, resize, close: unsubscribe };
  }}
  convertEol={true}
  onReady={({ focus }) => { /* retain focus for the host's terminal tab */ }}
/>
```

`connect` returns synchronously and must keep its function identity stable for
one attachment. Unmount closes the attachment; the host retains ownership of
process termination. Use `convertEol` for streams without PTY newline processing;
the default preserves terminal bytes. Theme foreground and cursor through
`--sctui-terminal-foreground` and `--sctui-terminal-cursor`, and the ANSI palette through
`--sctui-terminal-black`, `-red`, `-green`, `-yellow`, `-blue`, `-magenta`, `-cyan`, `-white` and
their `-bright-*` forms (defaults: the brand's `terminal.*` roles). xterm reads the resolved colours
when the viewer mounts. `/stream` does not
load the tmux or WebSocket implementation. Existing `TerminalViewer` consumers
keep the default WebSocket connector, also exported as `/client`'s
`connectTerminal`.

## Shells over a Docker exec wire

`/docker-exec` exports `dockerExecTerminal`, a controller for an environment that
offers Docker's Engine API and no session service: a container, a Conduit
`docker/engine-api` binding, or a browser tab's container. Each New terminal is
an exec of `sh` with a TTY. The controller holds the exec's stream and a bounded
buffer of its output, so a view that detaches leaves its shell running and one
that attaches replays the newest output. `stop` ends a shell or dismisses one
that exited. Nothing outlives the page: Docker offers no way back into an exec's
stream once its socket closes, so say so where the shells are listed.

```js
import { dockerExecTerminal } from '@volter-ai-dev/supercode-terminal/docker-exec';

const shells = dockerExecTerminal({
  base: 'https://agent.example/v1.51', containerId,
  headers: { authorization: `Bearer ${token}` },
  // in a tab, the page's own socket onto the exec's stream
  socket: (url) => daemon.socket(url),
});
// shells.getState() and shells.subscribe() feed TerminalWorkspace;
// shells.connector is the viewer's `connect`.
const id = await shells.create();
```

It loads neither xterm nor a UI framework.
