TypeScript TUI SDK¶
An experimental TypeScript client for the draft Block protocol: capability negotiation, Context handles, and the five content Operations. It is available as source and a local JavaScript build; the package is unpublished and its API may change.
Layout and scope¶
src/index.tsis the public source entry point.src/client.tsimplements the single-stream client and Context handles.src/ansi.tsconverts ANSI-styled strings intotext/styledspans.test/client.test.tschecks client behavior and interoperability with the terminal protocol module.test/styled-content.test.tschecks styled content encoding and negotiation gating;test/ansi.test.tschecks the ANSI conversion, andtest/ansi-recorded.test.tsruns it over recorded real Pi terminal output.
The SDK imports the shared protocol implementation and implements the semantics in the protocol drafts. Interoperability tests use the terminal module; the PTY examples connect both modules to an experimental renderer.
Local JavaScript build¶
Run pnpm build:sdk from the repository root. It prints a fresh output directory
under .tmp/sdk-build-*, containing ESM JavaScript, TypeScript declarations,
the Apache 2.0 license, and private package metadata. Keep the entire directory:
sdk/src/index.js uses the included node_modules/@tui-protocol/protocol
package through its public name. The distribution contains the SDK and shared
protocol runtime, with declarations and metadata.
The build uses the repository’s pinned compiler and rewrites relative runtime
imports to .js for execution as ordinary JavaScript.
Each build uses a fresh directory instead of merging with potentially stale
output; command-line builds are retained for inspection. The metadata uses
private: true and the local package name @tui-protocol/sdk. The shared build
helper under scripts/ copies the dependency files into the distribution.
The existing distribution-only ./protocol export is retained as a forwarding
entry.
pnpm test includes two artifact checks in test/artifact.test.ts. They copy
only build output outside the checkout and exercise package exports: one runs
a small negotiation/Append/close exchange with a synthetic responder and Node’s
TypeScript support disabled; the other checks an external TypeScript consumer’s
types with the installed compiler. Both remove their own temporary directories.
TUI-side use¶
Create one client for one end-to-end byte stream. Connect its input before starting negotiation. The transport, raw-input mode, ordinary key handling, and fallback renderer belong to the application.
The write callback must synchronously accept the entire batch into an ordered
transport; it must not be an async function. Serialize other writes on the
same stream and report later transport failure through dispose(error).
import { TuiClient } from "@tui-protocol/sdk";
const client = new TuiClient({
write(bytes) { process.stdout.write(bytes); },
timeoutMs: 2000,
});
process.stdin.on("data", bytes => {
for (const event of client.receive(bytes)) {
// Application-provided handlers:
if (event.type === "ordinary") handleInput(event.data);
if (event.type === "error") handleDiagnostic(event);
if (event.type === "message" && event.message.kind === "protocol.error") {
handleOperationError(event.message);
}
}
});
if (await client.negotiate()) {
const context = await client.openContext();
let sentState = context.append("thinking", "Analyzing", "mutable");
sentState = context.extend("thinking", sentState, " input");
sentState = context.replaceSuffix("thinking", sentState, 9, " complete");
context.update("thinking", "Done");
context.seal("thinking");
await context.close();
} else {
startApplicationFallback();
}
The example is schematic: handlers and fallback are application functions. For a runnable application using built JavaScript, with raw-stdin handling and ordinary-text fallback, see the streaming text example.
API behavior¶
API |
Observable result |
|---|---|
|
Resolves |
|
Requires positive confirmation; resolves a handle only after a matched opened response. |
|
Validate, encode, and synchronously hand a byte batch to the transport; return its generated Operation ID. |
|
Stops further sends on that handle while pending; resolves after a matching close response for that Context. |
|
Processes control responses and returns decoded events, including ordinary input, protocol errors, and diagnostics. |
|
Retire the local client and reject pending controls. |
The Context ID is read-only. Request IDs have a per-client namespace followed
by increasing decimal strings; requestIdPrefix can supply an explicit prefix
for deterministic fixtures. Operation IDs are increasing decimal strings. This
keeps independent applications’ control requests distinct even when they share
a persistent terminal byte stream, while preserving retry correlation within
one client. The client does not mirror terminal Block state: an Operation ID
means sent, not accepted or rendered. The application names
the exact base for Extend/ReplaceSuffix and handles asynchronous rejections.
There is no automatic retry, rollback, successful-Operation acknowledgement,
or automatic full Update after an incremental error.
An explicit close-error response makes the handle available again, consistent with the draft’s unchanged remote Context. A close timeout leaves that handle unusable because its remote state is uncertain. An open timeout may leave an unknown remote Context. Same-ID control recovery is not implemented: the application must end/recover the old stream before relying on a fresh client and negotiation.
The client’s response deadline defaults to two seconds and can be configured. A late or unmatched response does not grant support or resolve another request. During a new negotiation, the client blocks sends on existing handles until support is positively confirmed again. Create a fresh client after reconnection. Transport closure and remote-resource cleanup remain the application’s responsibility when retiring the client.
Styled text content¶
client.optionalContentTypes exposes the optional content types from the
latest positive capability response (empty before one, and reset by a fresh
negotiation); client.supportsStyledContent is its text/styled shortcut.
The text argument of append, update, extend, and replaceSuffix accepts
either a string or a readonly StyledTextSpan[]. A string keeps encoding
text/plain exactly as before. A span list encodes text/styled: a content
snapshot for append and update, a span-list fragment for extend and
replaceSuffix. Sending spans only validates and hands bytes to the
transport; as with every Operation, the returned ID means sent, not accepted
or rendered by the terminal.
Passing spans when the latest positive response did not confirm text/styled
throws before any bytes are written; stripping styling and sending plain
strings is the application’s fallback decision, and the SDK never downgrades
spans on its own. An empty span list is valid content for append, update,
and replaceSuffix; extend rejects it locally, matching the protocol’s
non-empty fragment rule.
A non-empty array whose first element has spans is instead a list of
experimental text/lines lines (RFC 0003):
styled text per line plus wrap, continuation, and fill layout
attributes. client.supportsLinesContent reports its confirmation; lines
passed without it throw before any bytes are written, and rendering at the
current width into spans is the application’s fallback. An empty array always
means an empty span list.
ANSI conversion¶
spansFromAnsi(text) converts an ANSI-styled string into a span list suitable
for the content Operations. It interprets only SGR sequences (CSI ... m):
reset (0, including an empty parameter list); bold, faint, italic, underline,
strikethrough, and inverse (1, 2, 3, 4, 9, 7) with their individual clears
(22, 23, 24, 29, 27); the 16-color foreground (30–37, 90–97) and background
(40–47, 100–107) codes as palette indices 0–15, preserving terminal theme
semantics; 256-palette colors (38;5;n, 48;5;n); true colors
(38;2;r;g;b, 48;2;r;g;b); and default-color restores (39, 49), which drop
the attribute rather than emitting the explicit "default" color.
The output merges adjacent runs with equal styles into one span, emits runs without attributes as spans without a style field, and never produces empty spans, so empty or styling-only input yields an empty span list.
Limitations of this first version: every non-SGR escape sequence (other CSI
commands, OSC strings, other ESC sequences) is silently discarded; other
control characters are kept as ordinary text; parameters are split on ;
only, so colon sub-parameters such as underline variants are not
distinguished; unknown SGR attributes and out-of-range color parameters are
ignored.
The conversion is also checked against a recorded sample of real application
output: the raw ConPTY stream of one stock Pi 0.87.1 turn
(test/fixtures/pi-0.87.1-stock-turn.ansi, recorded by the
Pi stock-UI trial).
Besides more than 500 SGR sequences it carries cursor movement, erase, mode,
synchronized-output, and OSC 0/8/11/133 sequences. The tests check that no
sequence residue reaches span text, that the logical text equals the
recording with every escape sequence removed by an independent reference
stripper, that spans are never empty or unmerged, and that Pi’s markdown
heading, bold phrase, and inline code arrive with Pi’s theme styling. The
sample is one application, one theme, and one width; it contains no OSC 8
link targets or colon sub-parameters.
Terminal-side integration¶
The terminal protocol module handles incoming bytes,
state changes, and responses on the receiving side. Its Operation adapter
connects execution to the host’s renderer. The
terminal host example shows this wiring.
The xterm endpoint
and PTY demonstration show the
experimental rendering connection. A supporting host must implement the
protocol’s native history and rendering behavior and recognize the experimental
OSC 9002 carrier.
Verification and remaining limits¶
Run pnpm typecheck and pnpm test from the root. The SDK tests cover the listed
positive/negative negotiation cases, response correlation, all five Operations
against the existing endpoint with byte-split responses, explicit base IDs,
local validation, close errors/timeouts, input diagnostics, and disposal/write
failure, plus the styled content encoding, negotiation gating, and the ANSI
conversion behavior above. The real PTY consumer is verified separately
through its guided browser check.
The client exposes baseline text/plain and negotiated text/styled. Further
optional content types, same-ID control-request recovery, and general
transport/backpressure handling remain future work, along with a
supported-runtime matrix and stable package exports.