# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.2.1] - 2026-05-20
### Added
- **Inline image rendering via `@xterm/addon-image`.** The bundled JS hook now loads [`@xterm/addon-image`](https://www.npmjs.com/package/@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](https://github.com/mcass19/ex_ratatui/blob/main/CHANGELOG.md#0100---2026-05-19)) 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](https://hexdocs.pm/ex_ratatui/images.html).
- **`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`](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](guides/configuration.md) 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`](https://hex.pm/packages/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`](https://xtermjs.org/docs/api/terminal/interfaces/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`](https://xtermjs.org/docs/api/terminal/interfaces/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 `c: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`](https://github.com/mcass19/kino_ex_ratatui/blob/main/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`](https://hexdocs.pm/ex_ratatui/ExRatatui.Telemetry.html) 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](guides/telemetry.md) 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
- README demo GIF (`assets/demo.gif`) showing a `Kino.ExRatatui` widget driving an `ExRatatui.App` inside a Livebook notebook.
## [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`](https://hexdocs.pm/ex_ratatui/ExRatatui.Transport.ByteStream.html). 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, handle_connect 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 via `test_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.
[Unreleased]: https://github.com/mcass19/kino_ex_ratatui/compare/v0.2.1...HEAD
[0.2.1]: https://github.com/mcass19/kino_ex_ratatui/releases/tag/v0.2.0...v0.2.1
[0.2.0]: https://github.com/mcass19/kino_ex_ratatui/releases/tag/v0.1.1...v0.2.0
[0.1.1]: https://github.com/mcass19/kino_ex_ratatui/releases/tag/v0.1.0...v0.1.1
[0.1.0]: https://github.com/mcass19/kino_ex_ratatui/releases/tag/v0.1.0