Termkit

Concepts

Sessions, the registry, bells, and the two seams.

Sessions and the registry

A session is one shell process living in the daemon, keyed by a stable id you choose. The <TermkitProvider> keeps a registry of sessions and polls the daemon (every pollIntervalMs, default 2s) for the live list.

Because the id you pass to <Terminal> is the daemon's session id, attaching is deterministic: render session="abc" and you connect to shell abc if it exists, or spawn it if it doesn't. There's no per-tab random id to lose, so reloading the page — or closing and reopening the browser — reattaches rather than piling up orphaned shells.

Local vs. global

There are two ways to work with a session, and they coexist:

  • Local. Render <Terminal session="abc" />. It reads like component-local state, but the session outlives the component — unmounting just drops the view.
  • Global. Use the hooks (useSessions, useRingingSessions, useTermkitActions, …) anywhere under the provider to list and act on every session, mounted or not. The provider's poller keeps that list fresh with no terminal on screen.

This is what lets a sidebar show six live sessions, badge the two that are ringing, and let you kill any of them — while only one is actually rendered.

Bells (attention)

Attention is a per-session boolean, not a count: a session either needs you or it doesn't. The daemon sets it when the shell rings the terminal bell (\a), detected via xterm's parser so it never trips on the title-setting escape that shells emit constantly.

Viewing a session clears its bell — that's the inbox model. By default a mounted <Terminal> marks its session seen on attach, on focus, and when a bell arrives while the window is focused and the terminal is showing. Turn that off per terminal with dismissOnView={false} (e.g. an always-mounted background monitor), and clear bells explicitly with dismissBell(id) / dismissAllBells() from useTermkitActions.

The two seams

Termkit is host-agnostic by design. It owns the terminal UI, the session registry, and the reconnect/bell behaviour; you supply two things:

  • connectionUrl({ sessionId, params }) — builds the WebSocket URL for the byte stream. params is an opaque metadata bag forwarded to the daemon (and echoed back on the live list) — put a project slug, a directory, a label, whatever your daemon's working-directory resolution needs.
  • transport — the control plane: list, kill, seen. Three async functions, wired to your backend.

Keeping these external is what keeps domain concepts (projects, directories, groupings) out of termkit. Per-project rollups, for instance, are built in your code on top of useSessions() — termkit only ever sees opaque metadata.

Resilience

The PTY lives in the daemon, which outlives dev-server restarts, so a dropped socket is treated as transient: the terminal reconnects and the daemon replays a snapshot of the current screen. A client-side heartbeat also catches half-open sockets (laptop sleep/wake, Wi-Fi change, a dead proxy) — cases where the socket looks open but nothing flows — and forces a reconnect. A session the daemon deliberately ends (killed, or the shell exited) closes with a terminal code so the client stops retrying instead of resurrecting it. All of this is automatic.

On this page