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.paramsis 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.