Files
Chris Tate 4e4dc46a37 Fix/autofix dangling children (#300)
* autoFixSpec prunes children references to undefined elements

Dangling references are the dominant remaining first-attempt validation
failure in benchmarks, and models frequently fail to repair them even given
the exact error (observed: three repair turns, same dangling footer each
time). The renderer already skips missing children at runtime, so pruning
yields the identical rendered output while letting the spec validate. Each
removal is reported in fixes.

* Classify autoFixSpec fixes as lossy or lossless

Pruning a dangling child reference changes what renders; relocating a
misplaced field does not. Callers with a repair loop need to tell these
apart: accept lossless fixes silently, prefer re-prompting over lossy fixes,
and keep the lossy-fixed spec as a last resort. Adds fixDetails alongside the
existing fixes strings (additive, no signature break).

* Validate visible conditions in validateSpec; document filtered-list pattern

Malformed visible conditions (e.g. mixing $state and $item in one object)
silently evaluate to hidden at runtime: evaluateCondition dispatches on the
first recognized key and non-strict parsing strips the rest, so whole regions
of UI disappear with a valid-looking spec. Benchmarked worst case: a kanban
board that rendered zero task cards.

- core: VisibilityConditionStrictSchema (strict objects, exported)
- core: validateSpec rejects malformed visible with a repairable message
  listing the valid forms (code: invalid_visible)
- framework prompts: FILTERED LISTS rule showing the repeat + per-item
  visible pattern models keep reaching for and inventing syntax around

* Support filtered lists: repeat + $item visible on the same element

Models across vendors consistently write {repeat, visible: {$item: ...}} on
one container to mean a filtered list (kanban columns, status sections).
Previously $item had no meaning outside the repeat scope, the condition
evaluated false, and the whole region silently disappeared — the worst
visual failures in benchmarks were boards rendering zero cards this way.

Outside a repeat scope that spelling was always broken, so claiming it is
backward compatible: the renderer now applies such a condition per item,
preserving original indices for item state paths. Container-level $state
conditions and per-child $item conditions behave as before.

- core: conditionUsesItemScope helper (exported)
- react: RepeatChildren filters items by the container's $item condition
- framework prompts: FILTERED LISTS rule teaches the container spelling
- react: repeat-filter test suite (filtering, no-filter, $state container
  visibility, per-child $item)

Other framework renderers (vue, svelte, solid, react-native) still evaluate
the container condition outside scope and should adopt the same semantics.

* Validate repeat containers: require children and matching state arrays

Two silent empty-region failures seen repeatedly in benchmarks, both passing
validation today: a repeat element with no children (nothing to clone per
item) and a repeat statePath pointing at a missing or non-array state value.
Both now fail validateSpec with repairable messages (repeat_without_children,
repeat_state_mismatch). State checks only run when the spec provides state;
runtime-fed state is unaffected.

* Address review: lossy-aware hook repair, react-only filtered-list rule, reuse getByPath

- ink/react-native useUIStream repair loops no longer accept lossy autofixes
  unconditionally: lossless relocations apply immediately, pruned content
  holds back while retries remain (so validation fails and the model repairs
  the missing elements) and applies only as a last resort.
- FILTERED LISTS prompt rule removed from schemas whose renderers do not
  implement the per-item filter yet (everything except react). Renderer
  parity tracked as follow-up.
- repeat_state_mismatch validation reuses getByPath instead of a local JSON
  Pointer lookup that skipped ~0/~1 unescaping.

* Address review: split mixed repeat visibility, apply lossless fixes eagerly

- splitRepeatVisibility (core, exported): AND-composed conditions on a repeat
  container partition into a container gate ($state conjuncts, hides the
  shell) and a per-item filter ($item/$index conjuncts). Mixed $or cannot
  partition soundly and stays fully per-item, documented.
- react renderer uses the split, so {$and: [{$state gate}, {$item filter}]}
  hides the empty shell when the gate is false instead of rendering a husk.
- autoFixSpec gains { lossy?: boolean } (default true, additive): ink and
  react-native repair loops now apply lossless relocations immediately and
  withhold only the pruning until retries are exhausted, matching the stated
  intent.

* Address review: RN final-validation error path, repeat prune guard, docs

- react-native useUIStream mirrors ink: when retries are exhausted and the
  spec still fails validation, report through onError instead of silently
  calling onComplete with an invalid spec.
- autoFixSpec never prunes a repeat container to zero children; that would
  trade missing_child for repeat_without_children and leave the last-resort
  spec unrenderable. The dangling template reference stays visible to repair.
- Docs for the new surface: core README + skill (validateSpec issue codes,
  fixDetails, lossy option), react README + skill and web visibility docs
  (filtered-list pattern, mixed-condition splitting, framework support note).
2026-06-10 22:06:29 -05:00
..
2026-06-10 22:06:29 -05:00
2026-03-27 18:17:20 -07:00
2026-05-06 22:42:42 -05:00

@json-render/ink

Ink terminal renderer for json-render. Turn JSON specs into interactive terminal UIs.

Installation

npm install @json-render/ink @json-render/core ink react

Peer dependencies: ink ^6.0.0 and react ^19.0.0.

Quick Start

1. Create a Catalog

import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/ink/schema";
import {
  standardComponentDefinitions,
  standardActionDefinitions,
} from "@json-render/ink/catalog";

export const catalog = defineCatalog(schema, {
  components: standardComponentDefinitions,
  actions: standardActionDefinitions,
});

2. Render a Spec

import { render } from "ink";
import { createRenderer, standardComponents } from "@json-render/ink";
import { catalog } from "./catalog";

const InkRenderer = createRenderer(catalog, standardComponents);

const spec = {
  root: "heading",
  elements: {
    heading: {
      type: "Heading",
      props: { text: "Hello from the terminal!", level: "h1" },
      children: [],
    },
  },
};

render(<InkRenderer spec={spec} state={{}} />);

Standard Components

Layout

Component Description
Box Flexbox layout container (like a terminal <div>)
Text Text output with optional styling (color, bold, italic, etc.)
Newline Inserts one or more blank lines
Spacer Flexible empty space that expands to fill available room

Content

Component Description
Heading Section heading (h1–h4)
Divider Horizontal separator line with optional title
Badge Small colored inline label for status
Spinner Animated loading spinner with optional label
ProgressBar Horizontal progress bar (0–1)
Sparkline Inline sparkline chart using Unicode blocks
BarChart Horizontal bar chart with labels and values
Table Tabular data display with headers and rows
List Bulleted or numbered list
ListItem Structured list row with title, subtitle, leading/trailing
Card Bordered container with optional title
KeyValue Key-value pair display
Link Clickable URL
StatusLine Status message with colored icon
Markdown Renders markdown-formatted text with terminal styling

Interactive

Component Description
TextInput Text input field with two-way binding
Select Selection menu navigated with arrow keys
MultiSelect Multi-selection with space to toggle, enter to confirm
ConfirmInput Yes/No confirmation prompt
Tabs Tab bar navigation with left/right arrow keys

Generate AI Prompts

const systemPrompt = catalog.prompt({ system: "You are a terminal assistant." });

Streaming

Use useUIStream to progressively render specs from JSONL patch streams:

import { useUIStream } from "@json-render/ink";

const { spec, send, isStreaming } = useUIStream({ api: "/api/generate" });

Key Exports

Export Purpose
createRenderer Create an all-in-one renderer component from a catalog
Renderer Low-level spec renderer
JSONUIProvider Combined provider (state, visibility, validation, actions, focus)
standardComponents Pre-built component implementations for all standard components
schema Ink element tree schema
useStateStore Access state context (state, get, set, update)
useStateValue Get single value from state
useBoundProp Two-way binding for $bindState/$bindItem expressions
useUIStream Stream specs from an API endpoint
createStateStore Create a framework-agnostic in-memory StateStore

Catalog Entry Points

Entry Point Exports
@json-render/ink Components, renderer, hooks, providers
@json-render/ink/schema schema, InkSchema, InkSpec
@json-render/ink/catalog standardComponentDefinitions, standardActionDefinitions
@json-render/ink/server Server-safe re-exports (schema + catalog, no React dependency)

Documentation