# Terminal adapters — `adapters/tty` and `adapters/ink`

Two adapters render a served tree in a terminal. The TTY adapter is a pure
text renderer for line hosts (a REPL, a pipe, an agent); the Ink adapter is
a full-screen TUI kit riding the React adapter's walker. They share one
idea: every interactive carries a numbered `[n]` marker, and the numbering
is computed once (by the TTY walker) so `[7]` is the same interactive in
both. The shared adapter contract is [ADAPTER.md](ADAPTER.md); the host
that binds these to a live server is `@niscorp/moss` (`terminal/tty`,
`terminal/ink`).

---

## `@niscorp/nova/adapters/tty`

Pure functions — no framework, no I/O, no node builtins. Importable
anywhere.

### `createTtyView(registry, api, options?): TtyView`

- `registry: ComponentRegistry<TtyComponent>` — the kit.
- `api: TtyRenderApi` — core's `RenderApi` (`frame()`, `canvasTree(id)`,
  `dispatch(id, event)`, `publish(channel, payload?)`).
- `options.fallback?: TtyComponent` — renderer for unregistered names;
  omit for strict `COMPONENT_NOT_FOUND` markers.
- `view.render(): TtyFrame` — walk the current snapshot. Call on every
  host update; the view holds no state.

### `TtyFrame`

```ts
{ text: string; interactives: TtyInteractive[] }
```

`text` is the whole screen: one rule line per non-empty canvas
(`── main ───`), component lines below, blank runs collapsed. Empty
canvases collapse entirely.

### `TtyInteractive`

```ts
{ index, kind, ref, canvas, origin?, label, value?, path? }
```

- `index` — the printed `[n]`, 1-based, registration order.
- `kind` — `'click' | 'row' | 'model' | 'toggle'`. The host maps these to
  events: click/row → `ui:click` (payload `value`), toggle → `ui:model`
  with the negated boolean, model → the host's own input flow.
- `canvas` — whose dispatch this belongs to; `''` is frame chrome
  (dispatches nothing).
- `origin` — the action instance it sits in (the enclosing `ActionSlot`'s
  `instanceId`; a `CanvasSlot` starts outside any). A list canvas renders
  several live instances that share refs — a tab bar's every button is
  `open` — so the host stamps this as the event's `origin`, as the DOM
  adapter does, or the server's card-deck fallback picks one of them.
- `label` — a name to list it by: the first line a `ref`'d or `model`'d
  node printed, cut at 48 characters (the ref when that line is empty); a
  component that registers its own passes one.
- `value` — click/row: the dispatch payload; model/toggle: the current
  value. `path` — the model path, display only.

The walker applies the conventions itself: any `ref`'d node gets a click
marker (payload = its `value` prop), any `model`'d node gets a model
marker (`toggle` when its current value is boolean). Components never
handle events; data-driven internals (a table's rows, a panel's `✕`)
register through `ctx.register`.

### `TtyComponent`

```ts
(ctx: { props; children: TtyBlock[]; register }) => TtyBlock   // TtyBlock = { lines: string[] }
```

`register({ kind, ref, label, value?, path? })` adds an interactive and
returns its index; the component prints that `[n]` itself, where it wants
it.

Kit (`/adapters/tty/components`): `defaultRegistry()` with `Box, Row,
Stack, Grid, Text, Button, Input, Select, Textarea, Checkbox, Switch,
Table, Badge, Panel, JsonTree, ActionSlot` (`Select` is `Input`, `Switch`
is `Checkbox`; the components are also exported by name), plus `fallback`
(unknown names
render their children; childless ones surface `label`/`title` + `count`)
and the helpers (`stack`, `inline`, `inlineText`, `pad`, `truncate`,
`indent`, `str`, `num`, `bool`, `iconGlyph`).

`Box` and `Stack` stack their children. `Row` and `Grid` call `inline`: the
children join on one line, two spaces apart, while each is a single line,
and stack as soon as one is not.

---

## `@niscorp/nova/adapters/ink`

An Ink component kit for the React adapter's walker — ESM-only, `ink` and
`ink-text-input` are optional peers. There is no separate walker: mount
`NovaRenderProvider` + `RenderTree` and register this kit.

### Host obligations

- Pass the three host leaf renderers to `NovaRenderProvider`: `fallback`
  (this kit's), `textWrapper` (`TextWrap` — ink forbids bare strings
  outside `<Text>`), `errorMarker` (`ErrorMarker` — the DOM default
  `<span>` crashes ink).
- For `[n]` markers, provide `CanvasMarkersContext` per canvas with a
  resolver `(ref, { value?, occurrence? }) => number | undefined` and
  provide `FrameControlsContext` at the root (`setTyping` — a focused
  input claims typed digits as text, so the host's number handling must
  stand down). Without providers the kit renders markerless and Tab-only.
- Marker identity: a pressable passes its `value` (a list's rows share one
  ref and differ by payload), a `Table` row passes its `value` and its
  `occurrence` (its place among the rows), and an `Input` or a `Checkbox`
  passes neither and resolves by its ref alone. `markerFocusId(n)` is the ink focus id a marked
  component adopts — `useFocusManager().focus(markerFocusId(n))` jumps to
  it.

### Interaction conventions

Tab/Shift+Tab cycle ink's focus ring; Enter activates (Space too on a
Checkbox and on a Panel's header buttons; `useActionable` answers Enter
only); a focused Input holds a local draft, honours `debounce`, submits `ui:key
Enter`, and forwards ↑/↓ to the server as `ui:key ArrowDown/ArrowUp` (a
served palette moves its own highlight). Typed-digit handling is the
host's job — the kit only displays markers and adopts their focus ids.
A `ref` on a container (`Box`, `Stack`, `Row`, `Grid`) makes it one focus
stop with a marker: Enter on it dispatches `ui:click` with its `value` prop
as payload. Enter in an Input sends a value `debounce` is still holding
before the `ui:key`.

### Exports

`defaultRegistry()` — `Box, Row, Stack, Grid, Text, Button, Input, Select,
Textarea, Checkbox, Switch, Table, Badge, Panel, JsonTree, ActionSlot`
(`Select` and `Textarea` are `Input`, `Switch` is `Checkbox`; the
components are also exported by name);
`fallback` (ref'd unknowns are focusable and click on Enter; childless
ones surface `label`/`title` + `count`); `TextWrap`,
`ErrorMarker`; `useActionable(novaRef, value)` — the full interactive
convention (marker + focus + Enter-click) for an app's own components;
`useMarker`, `Mark`, `markerFocusId`, `CanvasMarkersContext`,
`FrameControlsContext`; types `MarkerResolve`, `InkFrameControls`.

Layout mapping: Grid honours `weights` (flex ratios) and `columns`
(wrapping tracks); Row honours `justify`/`align`; containers honour
`border` (incl. sided `'r'`/`'l'`/`'t'`/`'b'`), `padding`, `w`/`width`
(px→cells at ~8px/cell, percent passthrough), `grow`. What has no
terminal analog (bg, radius, glow) is absent, never faked. `JsonTree`
prints the TTY kit's lines, which do not fold: four levels are open, and
each object or array lists its first 20 entries.
