tuispec-0.2.0.0: SERVER.md
# `tuispec server` Reference
This document describes the implemented `tuispec server` JSON-RPC behavior.
## Goals
- Run interactive PTY-backed TUI sessions over JSON-RPC 2.0.
- Keep one active session per server process.
- Provide launch/input/wait/snapshot APIs plus utility methods for batching,
notifications, and JSONL recording/replay.
## Transport
- Framing: newline-delimited JSON objects (one JSON-RPC message per line).
- Input: stdin.
- Output: stdout.
- Mode: request/response plus optional server notifications.
## CLI
```bash
tuispec server --artifact-dir PATH [--cols N] [--rows N] [--timeout-seconds N] [--ambiguity-mode fail|first-visible|last-visible]
```
Defaults:
- `cols = 134`
- `rows = 40`
- `timeout-seconds = 5`
- `ambiguity-mode = fail`
## Session Model
- Single active session at a time.
- Initialize with `initialize`.
- Non-lifecycle methods require an active session unless noted.
## Coordinate Rules
- Row/column coordinates in server APIs are 1-based.
- `0` is reserved as a wildcard for row/column range selectors in `currentView`
filters (`entireRow`, `entireCol`, and `region`/`rows` wildcard forms).
- Selector `at`/`within.rect` coordinates must be `>= 1`.
## Method Surface
### Lifecycle
- `initialize`
- params:
- `name` (optional, default `"session"`)
- optional run overrides:
- `timeoutSeconds`
- `terminalCols`
- `terminalRows`
- `ambiguityMode` (`fail|first|first-visible|last|last-visible`)
- `snapshotTheme`
- `updateSnapshots`
- result:
- `sessionName`
- `artifactRoot`
- `rows`
- `cols`
- `version`
- `launch`
- params:
- `command` (required)
- `args` (optional, default `[]`)
- `env` (optional object of `string -> string|null`)
- string value: set/override
- `null`: unset inherited env var
- `cwd` (optional)
- `readySelector` (optional selector)
- `readyTimeoutMs` (optional)
- `readyPollIntervalMs` (optional)
- result:
- `ok`
### Input / Wait
- `sendKey`
- params: `key`
- `sendText`
- params: `text`
- `sendLine`
- params:
- `text`
- optional `expectAfter` selector
- optional `timeoutMs`
- optional `pollIntervalMs`
- optional `ambiguityMode`
- result: `ok`
- `waitForText`
- params:
- `selector`
- optional `timeoutMs`
- optional `pollIntervalMs`
- optional `ambiguityMode`
- result: `ok`
- `waitUntil`
- params:
- `pattern` (regex-like pattern over full `currentView.text`)
- optional `timeoutMs`
- optional `pollIntervalMs`
- result: `ok`
- `waitForStable`
- params:
- `debounceMs` (required): viewport must be unchanged for this many milliseconds
- optional `timeoutMs`: overall timeout (defaults to session `timeoutSeconds × 1000`)
- optional `pollIntervalMs`: poll interval (default `100`)
- result: `ok`
- Polls the viewport at `pollIntervalMs` intervals and returns once the visible
text has remained identical for at least `debounceMs` consecutive milliseconds.
Use this instead of fixed sleeps to wait for TUI output to settle.
### View / Snapshot
- `currentView`
- optional params (choose exactly one filter mode):
- `rows`: `{ "start": Int, "end": Int }`
- `region`: `{ "col": Int, "row": Int, "width": Int, "height": Int }`
- `entireRow`: `Int`
- `entireCol`: `Int`
- result:
- `text`
- `rows`
- `cols`
- `dumpView`
- params:
- `name`
- optional `format`: `ansi|png|both` (default `ansi`)
- optional render overrides for PNG mode:
- `theme`
- `font`
- `rows`
- `cols`
- result:
- `snapshotPath` (canonical)
- `metaPath` (canonical)
- `artifactRoot` (canonical)
- optional `pngPath`
- `renderView`
- params:
- `name`
- optional render overrides: `theme`, `font`, `rows`, `cols`
- result:
- `snapshotPath`
- `metaPath`
- `pngPath`
- `artifactRoot`
- `diffView`
- params:
- `leftPath`
- `rightPath`
- optional `mode`: `text|styled` (default `text`)
- result:
- `changed`
- `changedLines`
- `summary`
- `expectSnapshot`
- params: `name`
- result:
- `ok`
- `actualPath`
- `baselinePath`
- `baselineExists`
### Assertions
- `expectVisible`
- params: `selector`
- result: `ok`
- `expectNotVisible`
- params: `selector`
- result: `ok`
### Notifications
- `viewSubscribe`
- params (optional):
- `debounceMs` (default `100`)
- `includeText` (default `false`)
- result:
- `ok`
- `debounceMs`
- `includeText`
- `viewUnsubscribe`
- params: none
- result: `ok`
When subscribed, the server emits JSON-RPC notifications:
```json
{"jsonrpc":"2.0","method":"view.changed","params":{"rows":40,"cols":134}}
```
If `includeText=true`, notification params also include `text`.
### Batch
- `batch`
- params:
- `steps`: array of `{ "method": Text, "params": Value }`
- execution model:
- sequential
- non-atomic
- stop on first failure
- result:
- on success: `{ "ok": true, "completed": N, "results": [...] }`
- on failure: `{ "ok": false, "completed": N, "results": [...], "errorStep": K, "error": {...} }`
### Recording / Replay (JSONL)
- `recording.start`
- params:
- `path` (required): output JSONL path
- `frameIntervalMs` (optional, default `200`): viewport sampling interval in
milliseconds. Set to `0` to disable frame capture. Default 200ms = 5 Hz.
- result: `{ "ok": true, "path": FilePath, "frameIntervalMs": Int }`
- `recording.stop`
- params: none
- result: `{ "ok": true }`
- `recording.status`
- params: none
- result: `{ "active": Bool, "path": FilePath|null }`
- `replay`
- params:
- `path`
- optional `speed`: `as-fast-as-possible|real-time`
- result:
- `ok`
- `replayedRequests`
- `path`
- `speed`
Recording files are JSONL with one event per line and include:
- `timestampMicros`
- `direction` (`request|response|notification|frame|frame-delta`)
- `line` (raw JSON-RPC line, viewport text, or delta payload)
When `frameIntervalMs > 0` (the default), the server spawns a background thread
that samples the viewport at the configured rate. Only frames that differ from
the previous sample are written. Two frame event types are used:
- `frame` — full keyframe, emitted roughly every second. `line` contains the
complete rendered viewport text.
- `frame-delta` — compact delta, emitted between keyframes. `line` contains a
JSON array of `[lineIndex, "new line text"]` pairs for each changed line.
The replay CLI reconstructs full frames by applying deltas to the most recent
keyframe.
### Server Utility
- `server.ping`
- result:
- `pong: true`
- `version`
- `server.shutdown`
- result:
- `shuttingDown: true`
- then hard-shutdown:
- send `SIGKILL` to active child process group
- exit process immediately
## Selector Encoding
- exact:
- `{"type":"exact","text":"Ready"}`
- regex:
- `{"type":"regex","pattern":"Ready|Done"}`
- at (1-based):
- `{"type":"at","col":10,"row":2}`
- within (1-based origin):
- `{"type":"within","rect":{"col":1,"row":1,"width":40,"height":10},"selector":{...}}`
- nth:
- `{"type":"nth","index":1,"selector":{...}}`
## Error Contract
JSON-RPC errors:
- parse failures: `pARSE_ERROR`
- invalid request: `iNVALID_REQUEST`
- unknown method: `mETHOD_NOT_FOUND`
- bad params: `iNVALID_PARAMS`
Server-domain codes:
- `-32001` no active session
- `-32002` session already started
- `-32004` method failed
## Signal Behavior
On `SIGHUP`, server does hard-shutdown:
- send `SIGKILL` to active child process group
- close recording handle
- exit immediately