All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Unreleased

0.2.1 - 2026-05-20

Added

  • Inline image rendering via @xterm/addon-image. The bundled JS hook now loads @xterm/addon-image 0.9.0 in both the live (new/2) and static (frame/2) init paths, registering Sixel (DCS q ...) and iTerm2 inline-image (ESC ] 1337 ; File=... ^G) parser handlers on the xterm.js terminal. Without it those escape sequences were silently swallowed and ExRatatui.Widgets.Image (introduced in ex_ratatui 0.10) rendered nothing in Livebook. Construct images with ExRatatui.Image.new/2 passing protocol: :sixel or protocol: :iterm2 — xterm.js does not implement the Kitty graphics protocol. Bundle grows ~60 KB minified; no Elixir API change, no per-instance opt change. Moduledoc gained an Inline images section pointing at the ex_ratatui Images guide.

  • examples/new_widgets.livemd — tour of the three widgets introduced in ex_ratatui 0.10. One consolidated Livebook notebook covering ExRatatui.Widgets.Image (a static frame with the new image addon plus an interactive App that cycles :sixel / :iterm2 at runtime), ExRatatui.Widgets.CodeBlock (syntect-highlighted snippet with :solarized_dark, line numbers, and emphasis on lines 3..5), and ExRatatui.Widgets.BigText (slide-deck banner showing :full and :quadrant density variants). Closes with a Kino.Layout.grid/2 side-by-side comparison of all three. Catalogue entry added to examples/README.md. Mix.install pins ~> 0.3 so the notebook picks up the addon-image bundle as soon as 0.3.0 is published.

0.2.0 - 2026-05-07

Added

  • Accessible stopped-state DOM overlay. When the runtime server exits ({:stop, _}, mount/1 failure, crash) the widget no longer paints a dim ANSI message into the xterm buffer — it broadcasts a "stopped" Kino event with %{message: stopped_message}, and the JS hook anchors a role="status" aria-live="polite" <div> over the xterm container. Screen readers announce the message via aria-live; sighted users see a centered italic line in the configured theme's foreground/background instead of a dead cursor sitting on the frozen final frame. The overlay is pointer-events: none, applied at z-index: 1, and uses textContent so a user-supplied :stopped_message is never interpreted as HTML. Idempotent — refires of the event are ignored once an overlay is present. The :stopped_message knob from the per-instance display options is unchanged; this is purely a wire-protocol + presentation switch from "ANSI bytes the user reads off the dead xterm buffer" to "structured payload an accessible DOM overlay renders".

Changed

  • "stopped" Kino event replaces the in-buffer ANSI message on server :DOWN. The wire protocol changed: Kino.JS.Live.broadcast_event(ctx, "ansi", {:binary, %{}, "\\e[2J… App stopped …\\e[0m"}) is now broadcast_event(ctx, "stopped", %{message: stopped_message}). The build_stopped_screen/1 private helper is removed. Existing tests updated; the assert_broadcast_event(kino, "stopped", %{message: _}) shape pins the new contract, plus a defensive test that the payload is a plain map (not {:binary, _, _}) so a regression to byte-stream-shaped messages would fail loudly. End-user :stopped_message API is unchanged.

  • Kino.ExRatatui.configure/1 — global display defaults. Writes to the :kino_ex_ratatui Application environment so every subsequent new/2 and frame/2 call picks them up without ceremony. Accepts the same seven display keys as new/2. Validation runs at the call site (matches new/2's shape) so a typo in configure/1 fails fast rather than producing a working-but-wrong widget. Calling twice merges into the prior config rather than replacing it, so related settings can split across cells. Merge order is per-instance opts > configure/1 > module defaults, key-by-key — partial config sticks. The same env is reachable via Application.get_all_env(:kino_ex_ratatui) so a release config/runtime.exs works equivalently. New Configuration guide walks through the merge order, the atom theme shorthands, and an "when not to use it" pointer for runtime-dependent values.

  • Property-based test pass over the display-options surface. New stream_data dependency and test/kino/ex_ratatui/property_test.exs (async: true) with six properties: every valid display value lands in the display map unchanged across all seven keys; the :dark / :light / :livebook atom shorthands round-trip; setting one display key never disturbs the other six (default fall-through invariant); reserved keys never appear in mount_opts after new/2 regardless of input shape; non-reserved keys are preserved in mount_opts in their original order; and frame/2 with any opt outside its supported set always raises ArgumentError. Two more properties in configure_test.exs (async: false) cover global-state behavior: configure/1 stores each {key, value} pair under Application.get_env(:kino_ex_ratatui, key), and per-instance opts always win over configure/1 for the same key (precedence invariant). Generators per key, uniq_list_of to keep keyword-key uniqueness deterministic, and explicit Application.delete_env cleanup between iterations so the global-state properties stay order-independent.

  • :theme atom shorthands — :dark, :light, :livebook. In addition to a full xterm.js ITheme map, :theme now accepts three atoms resolved in the JS hook. :dark picks a bundled Catppuccin Mocha-flavored palette (the same colors as the no-opts default); :light picks Catppuccin Latte. :livebook reads the user's OS-level prefers-color-scheme and live-switches between :dark / :light whenever that preference changes — no cell re-eval needed. The previous map-only validator is unchanged; passing an unknown atom (e.g. theme: :neon) still raises ArgumentError with a message naming the offending option. JS-side: a subscribeLivebookTheme/2 helper on the live and static paths registers a matchMedia("(prefers-color-scheme: dark)") listener for the iframe's lifetime; iframes torn down on cell re-eval take the listener with them. Theming example extended with sections 4–5 walking through :livebook autoswitch and configure/1.

  • Per-instance display options on new/2 and frame/2. Seven reserved opts now configure the xterm.js iframe per cell: :theme (full xterm.js ITheme map — background, foreground, cursor, cursorAccent, selectionBackground, the 16-color ANSI palette, etc.), :font_family (CSS string), :font_size (px integer), :height (CSS length applied to the xterm container — accepts "600px", "60vh", "calc(100vh - 200px)", …), :cursor_blink (boolean), :scrollback (non-negative integer), and :stopped_message (string painted into the iframe when the runtime exits via {:stop, _} or a mount failure). Defaults preserve the previous look exactly. Reserved keys are stripped from the keyword list before reaching ExRatatui.App.mount/1, so apps never see them as mount opts. Each value is validated at the call site — bad shapes raise ArgumentError with a message naming the offending option, never silently. frame/2 accepts the static-friendly subset (:theme, :font_family, :font_size); live-only opts and unknown keys raise so typos like col: instead of cols: no longer go through silently. The display payload flows to the JS bundle via handle_connect/1 (live mode) and the static info map (frame mode); the JS hook merges it onto its own copy of the defaults so out-of-band callers (custom payloads, future smart-cell variants) still get sensible values when only some opts are supplied. New examples/theming.livemd walks through every option — One Dark, Solarized Light, custom Fira Code at 16px, no-blink + custom stopped message, and a side-by-side static gallery via Kino.Layout.grid/1 of three theme variants on the same widget tree.

  • Kino.ExRatatui.Telemetry:telemetry integration. Mirrors the shape of ExRatatui.Telemetry one layer up, emitting events at the boundaries this widget controls so consumers can plug in logging, metrics, or distributed tracing without reaching into the runtime. Two span events ([:kino_ex_ratatui, :transport, :connect] with :mod/:width/:height — wraps the lazy Session + Transport.start_server/1 boot triggered by the first "resize"; [:kino_ex_ratatui, :render, :frame] with :mod/:byte_count — wraps the IO.iodata_to_binary/1 + Kino-bridge broadcast per-frame work) and three single events ([:kino_ex_ratatui, :transport, :disconnect] with :mod/:reason — fires exactly once per session, either from the runtime server's :DOWN or from the widget's terminate/2 if the runtime is still alive; [:kino_ex_ratatui, :input, :forward] with :mod/:byte_count — fires when bytes from xterm.js are forwarded to ByteStream.forward_input/3; [:kino_ex_ratatui, :resize] with :mod/:width/:height — fires on resizes after the boot one). Public helpers: span/3, execute/3, attach_default_logger/1, detach_default_logger/0. New Telemetry guide walks through the full event catalogue and a Telemetry.Metrics wiring example. Added {:telemetry, "~> 1.0"} as an explicit dependency and to extra_applications so the handler registry is available wherever the kino runs.

0.1.1 - 2026-04-30

Added

0.1.0 - 2026-04-29

Added

  • First release. kino_ex_ratatui runs an ExRatatui.App inside a Livebook notebook via xterm.js, implemented as a ~150-line Kino.JS.Live widget on top of ExRatatui.Transport.ByteStream. Two entry points: Kino.ExRatatui.new/2 for live App-driven kinos, Kino.ExRatatui.frame/2 for one-shot static frames suitable for docs and Kino.Layout.grid/1 side-by-side comparisons.
  • JS bundle under assets/@xterm/xterm 5.5 + @xterm/addon-fit 0.10 bundled with esbuild 0.28 to lib/assets/kino_ex_ratatui/{main.js,main.css}. The bundle is committed so installing the hex package needs no Node toolchain. mix assets.install and mix assets.build aliases are provided for contributors.
  • Lazy lifecycle. The runtime server and ExRatatui.Session are created on the first "resize" event from the iframe, so dimensions always come from xterm.js's FitAddon rather than a hardcoded default. Subsequent resize events flow through ByteStream.forward_resize/4. When the App returns {:stop, _} (or mount/1 fails), the widget broadcasts the canonical alt-screen leave sequence so xterm.js restores its cursor and main buffer.
  • Test suite — 22 tests via Kino.Test's configure_livebook_bridge + push_event/3 + assert_broadcast_event/3, covering: lazy boot, mount-opts pass-through, handleconnect payload, first/subsequent resize, input round-trip, input arriving before first resize, server :DOWN, terminate cleanup, mount failure, unrelated handle_info messages, and `_assets_info/0. Runs async in 0.2s, 100% line coverage (test fixtures excluded viatest_coverage: [ignore_modules: [...]]`).
  • Three bundled example notebooks under examples/system_monitor.livemd (callback-runtime dashboard porting ex_ratatui/examples/system_monitor.exs with Gauge, Table, /proc polling), chat_interface.livemd (callback-runtime AI-chat mock exercising Markdown, Textarea, Throbber, Scrollbar, and /-prefixed SlashCommands autocomplete via Popup — ported from the original imperative ExRatatui.run/1 loop in ex_ratatui/examples/chat_interface.exs), and reducer_counter.livemd (reducer-runtime counter with a Subscription.interval plus a Kino.ExRatatui.frame/2 static-frame demo). Each notebook cross-references the other two and links to the relevant runtime guide so any one of them is a complete jumping-off point.