@volter/supercode-terminal
v0.2.57
Published
Optional leak-safe terminal and tmux host adapter for Volter Harness frontends
Readme
@volter/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/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/supercode-terminal/ui and its tokenized
stylesheet from @volter/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.
The terminal canvas and internal layout layers are transparent over one
--sctui-terminal-background backing layer (the Volter brand's terminal.background 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, 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 looks like
Apple's Terminal.app with its Basic profile (the brand's terminal.* roles, font.terminal and
fontSize.terminal: SF Mono 11 where the platform has it, Terminal.app's palette, a steady block
cursor), and like Terminal.app it follows the system's appearance whatever the page's scheme.
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.
<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, cursor and selection through
--sctui-terminal-foreground, --sctui-terminal-cursor and --sctui-terminal-selection-background,
the face and size through --sctui-terminal-font and --sctui-terminal-font-size, 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.
import { dockerExecTerminal } from '@volter/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.
