@volter-ai-dev/supercode-client
v0.3.24
Published
Headless state and control layer for Supercode-powered coding-agent frontends
Readme
@volter-ai-dev/supercode-client
@volter-ai-dev/supercode-client is Supercode's framework-neutral, presentation-free state
and control layer for coding-agent frontends. It is the reusable seam between
Supercode's session/runtime machinery and products such as an editor, desktop
app, web client, or terminal dashboard.
It deliberately ships no components, CSS, icons, Markdown renderer, layout, or product copy. A consumer owns all visuals. The package owns the semantics that must stay identical across those visuals.
Why this is separate from @volter-ai-dev/supercode-harness-sdk
@volter-ai-dev/supercode-harness-sdk is the low-level Node transport. It starts
supercode harness serve and exposes discovery, persisted-session, transfer,
and managed-runtime primitives. It intentionally does not decide which session
is active, recover a follower, pair tool calls with results, serialize
concurrent actions, reconcile streamed output with persisted history, or
project capabilities into safe UI actions.
@volter-ai-dev/supercode-client does those things. It accepts the low-level client by
injection and has no Node imports of its own, so the same controller contract
can be hosted behind HTTP/SSE, WebSocket, Electron IPC, or another transport.
import { SupercodeHarnessClient } from '@volter-ai-dev/supercode-harness-sdk';
import { SessionWindowCache, SupercodeController } from '@volter-ai-dev/supercode-client';
const harness = new SupercodeHarnessClient({ cwd: projectRoot });
const mirrorCache = new SessionWindowCache({ maxEntries: 12 });
const agent = new SupercodeController({
client: harness,
workspace: projectRoot,
mirrorCache, // share this instance across short-lived controllers
// A host with its own machine-wide session/activity stream can set
// inventorySubscriptions: false on seeded one-session mirror controllers.
policy: 'yolo', // a host policy decision, not a browser-controlled value
ownsClient: true,
});
const unsubscribe = agent.subscribe(() => render(agent.getSnapshot()));
await agent.initialize();
await agent.dispatch({ type: 'start', harness: 'grok' });
await agent.dispatch({ type: 'send', text: 'Build a timer.' });
if (agent.getSnapshot().availableActions.steer) {
await agent.dispatch({ type: 'steer', text: 'Keep the API; simplify the UI.' });
}The controller implements the external-store contract used by React's
useSyncExternalStore, but does not depend on React:
const snapshot = useSyncExternalStore(
agent.subscribe,
agent.getSnapshot,
agent.getSnapshot,
);Ownership boundary
Supercode owns:
- local harness inventory, authentication posture, runtime capabilities, and repair guidance;
- project-scoped session discovery over JSONL, SQLite, and future stores;
- stable opaque session keys (never ambiguous bare session IDs);
- passive transcript load/follow with sequence-gap recovery and retry;
- live-message delivery receipts plus a revisioned interoperability-control report preserved for the embedding UI;
- opt-in, host-side configuration of the small set of native harness controls
that affect Supercode workflows; disabled unless the embedding host passes
allowHarnessConfiguration: true; - bounded passive mirror reads (120 trailing messages, 16k characters per text field, child transcripts excluded) so a viewport never transports an entire recursive session archive;
- a bounded transcript-window cache that lets hosts paint previously opened chats immediately while their native transcript refresh continues;
- optional inventory subscriptions, so an embedding host with one authoritative machine-wide index can keep seeded one-session mirrors off the redundant scan lane;
- an explicit
initialInventoryconstructor seed, so those hosts can reuse already trusted harness/session descriptors without proxying or intercepting client methods; - normalized, loss-preserving conversation projection, including tool call/result pairing and context-message classification;
- an explicit full-session load path alongside the bounded passive projection, including system context, lineage, parse diagnostics, and nested subagents;
- start, persisted resume, genuine live-process attach, branch, explicit live detach, shared terminal attachment, interrupt, and protocol-response semantics;
- typed load, import, export, translation, and cross-harness handoff utilities resolved through opaque session keys;
- one serialized mutation queue, workspace generations, stale-event rejection, and streamed/persisted completion reconciliation;
- immutable snapshots, capability-derived available actions, structured errors, runtime requests, and a lossless normalized-event side channel.
The embedding product owns:
- HTTP/IPC routes, authentication, origin checks, rate limits, and which host
policy (
defaultoryolo) is allowed; - where the controller lives. For an always-on agent it should live on the host/server, not in a browser tab;
- panel placement, collapse/persistence, icons, colors, fonts, Markdown, composer layout, history presentation, warning copy, and preferences;
- whether context/reasoning entries are visible and how approval requests are rendered.
Semantic model
ACP is a protocol substrate, not the complete product contract. Supercode also supports persisted JSONL/SQLite discovery, passive following, native resume, translation, branch/handoff, and terminal launch instructions. The controller therefore exposes a typed Supercode superset and derives actions from honest capabilities.
Connection modes never collapse into one another:
| Mode | Meaning | Can send? |
|---|---|---:|
| none | No selected transcript or owned runtime | no |
| mirror | Passively following persisted history written elsewhere | only into a live peer |
| control/start | Controller owns a newly-created runtime | yes |
| control/resume | Controller started a new process from persisted state | yes |
| control/attach | Controller joined a genuinely reachable live endpoint | yes |
| control/branch | Controller owns a new session bootstrapped from another | yes |
| control/reduce | Controller owns a continuation bootstrapped from a verified reversible reduction | yes |
In particular, ACP support does not imply live-process attachment. Attachment
is offered only when attach_existing_process is true and the adapter exposes
attachManagedRuntime. Persisted discovery does not guess reachability. A
trusted host may enrich a discovered descriptor with live_endpoint only
after proving that endpoint owns the same harness-native session; alternatively
it may supply baseUrl explicitly in the attach action. Without either, the
controller keeps attach unavailable even when the harness supports it.
A mirror of a session that is running right now is the one place a send
happens without owning a runtime. Claude Code registers its live processes, so
such a session arrives with liveEndpoint starting cc-peer:v1: (and usually
liveStatus); availableActions.send is then true, connection.messaging is
'live_peer', and dispatch({ type: 'send', text }) hands the text to that
session instead of running a turn. Nothing is echoed into the conversation and
the turn is untouched — the message becomes visible when the followed
transcript catches up, which can take seconds. snapshot.delivery records the
hand-off (deliveredToBus) so a view can render "sent to the live session"
until then; a refusal (not_live, identity_mismatch, delivery_failed)
surfaces as a normal controller error. Attachment stays unavailable: a live
peer is messageable, not joinable.
Views persist FrontendSession.identity, never the controller-scoped key or
the reversible locator fingerprint. The identity is an opaque SHA-256 value.
After a host restart, one declarative
restore { identity, connection: 'observe' | 'attach' } command asks the
controller to reselect the exact session and, when requested and still proven
reachable, join its live endpoint. Selection ordering and attach safety stay
inside Supercode rather than being reimplemented by each view.
Leaving or sharing control is explicit. detach applies only to
control/attach; it closes this frontend and returns to the mirror without
touching the owner runtime. openTerminal applies to editor-owned
start/resume/branch/reduce runtimes and returns structured opaque-receipt attachment
instructions without stopping or resuming anything. Both frontends then drive
one Supercode-hosted runtime. A single overloaded “release” operation is
intentionally absent.
Raw terminal display is an optional peer capability, not controller state.
Hosts that want Termfleet-style tmux discovery, creation, capture, and embedded
screen attachment use @volter-ai-dev/supercode-terminal; the controller keeps
owning the semantic transcript and runtime. This separation lets a frontend
show both views without treating ANSI screen bytes as canonical messages or
pulling native PTY dependencies into browser-only/controller consumers.
Turn state is independent from inventory/operation state:
idle— input may be accepted when capabilities allow it;running— a submitted turn is active;interrupting— an interrupt was requested but terminal confirmation has not arrived;reconciling— terminal completion arrived and persisted history is being loaded; new input remains disabled so reconciliation cannot erase it.
A mirrored session reports turn state too, so a frontend can show that the
agent writing the transcript is working. It comes from the live-runtime
registry's reconciled state — never from transcript growth: while that
session's registered Supercode runtime owns a turn the state is running, and
it returns to idle when the turn ends. A session with no registered
Supercode runtime stays idle; a harness running outside Supercode leaves no
receipt, so nothing authoritative can say whether it is working, and the
controller does not guess. A mirrored turn is reported, never a gate: it
belongs to whoever drives that runtime, so it never disables starting,
branching, or detaching a chat of your own — only a turn this controller owns
does that.
For editor-owned Grok ACP runtimes, an explicit interrupt also rotates the native process after persisted reconciliation and resumes the same session. Grok can acknowledge cancellation while a background tool is still alive; rotation fences any late deltas from that cancelled process out of the next turn. A genuine shared attachment is never terminated by this safeguard.
Conversation projection
The source NormalizedSession remains authoritative. conversation is a
render-friendly but loss-preserving projection:
messageentries retain role, original structured content, extracted text, metadata, andvisibility: conversation | context;toolentries pair calls/results by ID and retain raw arguments/content;reasoning,request, andnoticeare distinct semantic entries;- unknown/native runtime events are never guessed into lifecycle state and are
still available through
subscribeEvents.
No source messages are deleted. Products may hide visibility: context, but
the controller preserves it for inspection and lossless reconciliation.
activeSession exposes the complete last-loaded persisted form, including its
recursive subagents; it is null for a brand-new runtime until that harness
has persisted and reconciliation has loaded it. Live deltas appear immediately
in conversation, so consumers should not mistake activeSession for a
real-time streaming model.
A send action may include typed { id, kind, label, detail } context items. The
controller bounds and wraps them in a reversible transport envelope while the
visible conversation retains the user's clean prompt. This keeps editor
selection or diagnostic context out of presentation text without relying on a
product-specific prompt convention. The projected message retains that context,
so a host can recognize an exact kind: "work-item" reference later without
parsing prose.
Every snapshot also includes taskPlan, a normalized read-only projection of
the active session's native planning protocol. Codex update_plan, Claude task
create/update calls, and OpenCode todo writes map to the same pending /
in-progress / completed / cancelled vocabulary; unknown native values remain in
residue instead of being guessed. deriveTaskPlan(session) exposes the same
pure projection for persisted sessions that are not currently active.
Protocol request entries retain a small resolution after response so a view
can show what was selected even though the request is removed from the pending
queue. Native response payloads remain inside the controller.
Session transfer and handoff
State-machine commands use dispatch(). Operations which return a document or
launch plan are separate typed methods so transient artifacts do not pollute
the durable UI snapshot:
const source = agent.getSnapshot().activeSessionKey;
const raw = await agent.loadSession(source);
const artifact = await agent.translateSession(source, 'codex');
const handoff = await agent.handoffSession(source, 'claude-code');
// handoff.launch and handoff.materialize are structured launches, never shell.handoff.artifact.target_harness names the artifact's actual wire format. For
Grok it is claude-code, because stock Grok materializes a resumable bundle
through its official Claude/Codex importer. The artifact gets a fresh target
UUID; read the importer's NDJSON sessionId and substitute it for
{imported_session_id} in handoff.launch.
The complete set is loadSession, importSession, exportSession,
translateSession, and handoffSession. They share the controller's FIFO
queue, structured error state, workspace generation, and full-locator lookup.
Presentation code sees opaque session keys; storage paths and SQLite selectors
remain inside the trusted controller.
Reversible reduction is a state-machine command because it immediately starts and bootstraps the chosen target harness:
await agent.dispatch({ type: 'reduce', sessionKey: source, targetHarness: 'codex' });
const receipt = agent.getSnapshot().reductionReceipt;
// receipt is present only after the service reloaded, verified, and inverted
// the durable sidecar/log/view bundle.availableActions.reduce is false when the adapter lacks reduceSession, the
source is unavailable, a controller-owned turn is active, or no target runtime
can start. There is no client-side approximation.
Concurrency and recovery invariants
- All public mutations and internal completion reconciliation run through one FIFO queue.
- A workspace change increments a generation before closing old resources; late discovery, follow, and runtime events are ignored.
- The controller never marks a turn idle until completion reconciliation has finished or conclusively cannot load a persisted locator.
- A follower ending or throwing retries with bounded exponential backoff until selection/workspace changes or the controller closes.
- Snapshot references are stable between revisions and replaced atomically on every revision.
- Session actions use opaque controller keys mapped to complete locators; identical native session IDs in different harnesses/stores cannot collide.
- Closing a view does nothing to the controller. Only explicit controller
close, workspace change, detach, terminal handoff, or replacement closes owned resources.
Security posture
The package does not expose a network listener. A host must enforce its own authorization and origin policy. The execution policy is fixed in controller construction; it is intentionally absent from browser-dispatchable commands so an untrusted caller cannot upgrade itself to YOLO mode.
Structured terminal launches remain { program, arguments, cwd, env }. The
package never manufactures a shell string. Rendering/copying a platform-specific
command is a host responsibility.
Non-goals
- A competing coding harness or IDE.
- React/Vue/Svelte components.
- A generic design system.
- Terminal emulation or keystroke injection.
- Claiming that persisted resume attaches to an already-running process.
- Hiding unsupported operations behind optimistic UI.
