Raxol. UI. Components. Harness. BodyProvider
(Raxol v2.6.1)
View Source
The T5 seam: an explicit per-kind content-map contract, plus the mapping
from a %Raxol.UI.Components.Harness.Block{}'s kind to the merged
component (or pair of components) that renders its EXPANDED body.
Per docs/proposals/in-flight/harness-ui-STATE.md's binding advisory
note, this contract is defined BEFORE mounting: Block.content (T4) is
a plain, kind-shaped map (see Block's moduledoc, "Rendering"), and
every known kind's shape is documented here as the schema a body map
must satisfy to mount safely. Raxol.UI.Components.Harness.BlockBody
(T5's fold-aware entry point) is the only caller in production code;
this module is also directly test-facing so the schema and the
kind-to-component mapping are independently exercisable.
Per-kind content-map schema
Required keys (validated by validate/2; optional keys are read with a
safe default and never fail validation):
:message-- required:text(the Markdown/plain body); optional:role(:user | :assistant, defaults to:assistant--Block's own extraction never populates:roletoday, a documented gap, not a bug: a future projection unit can add it, contract-only-grows).:reasoning-- required:text.:tool_call-- required:name,:args; optional:result(nilwhile the call has no paired result yet) and:tainted(boolean, defaultsfalse). Mirrors exactly whatBlock.extract_content(:tool_call, ...)already produces.:diff-- required:path,:old,:new; optional:language. MirrorsRaxol.UI.Components.Harness.DiffViewer's own prop names -- seeBlock'sextract_diff_content/1(T5 extension: no prior producer resolved:diffkind, so there was no existing shape to preserve).:approval-- required:action,:blast_radius,:options.:blast_radiusmust be shaped likeRaxol.UI.Components.Harness.BlastRadiusPreview.blast_radius(), ornilwhen no producer ever supplied one --nilis a real, distinct value here, not a validation failure:BlastRadiusPreviewrenders it as an explicit "not declared, treat as unsafe" warning, never as its%{}"No tracked effects." line (a producer-declared empty blast radius is a different, calmer claim than one nobody declared at all).:optionsmust be shaped likeRaxol.UI.Components.Harness.ApprovalPrompt.option()for the mounted render to be meaningful --validate/2only checks key PRESENCE (a cheap, always-safe check), not the value's inner shape; a wrongly-shaped value is the producer's bug, not something this seam can catch without becoming a second type-checker for every component's props.
:opaque (Block's forward-compat fallback for an unrecognised kind) has
no schema and no mountable component -- component_for/1 and mount/2
both refuse it; callers fall back to Block.render/2's own opaque
render, same as BlockBody does for any other mount failure.
Fold-aware sizing
This module only knows how to render the EXPANDED body. Folded
rendering (one-line summary + outcome row) is Block.render/2's own
proven code path -- BlockBody reuses it directly rather than
reimplementing a second summary renderer here.
:reasoning stays plain through this path, by design
Unlike :message, a :reasoning body is never threaded through
Harness.MarkdownBody here -- ReasoningBlock renders dim plain text
on purpose (reasoning is meant to read as quieter, secondary content).
Styled Markdown reasoning is an explicit opt-in that lives entirely in
Block.render/2's own context[:markdown] path, not in this seam.
Summary
Functions
The canonical merged component for kind's expanded body. :tool_call
composes a second component (ToolResultBlock, only when a result is
present) internally in mount/2 -- this always names the primary one.
Every kind this seam has a schema + mountable component for (excludes :opaque).
Mounts body (a kind-shaped content map) through its real component,
returning the rendered view map ({:ok, view}), or {:error, reason}
when
The content-map keys kind requires. An unknown/:opaque kind has no
schema -- an empty list, since component_for/1 refuses it separately
and validate/2 on it always returns :ok (nothing to check, nothing
to mount).
Validates body's keys against kind's schema. Presence-only (never
inspects a value's shape -- see the moduledoc's :approval note): a
content map that has every required key always passes, regardless of
what those keys hold. Missing keys are named explicitly, in schema
order, never a bare "invalid".
Types
@type body() :: map()
@type kind() :: Raxol.UI.Components.Harness.Block.kind()
Functions
The canonical merged component for kind's expanded body. :tool_call
composes a second component (ToolResultBlock, only when a result is
present) internally in mount/2 -- this always names the primary one.
@spec known_kinds() :: [kind()]
Every kind this seam has a schema + mountable component for (excludes :opaque).
Mounts body (a kind-shaped content map) through its real component,
returning the rendered view map ({:ok, view}), or {:error, reason}
when:
kindhas no known component (:opaque, or anything else outsideknown_kinds/0);opts[:component]is given and disagrees withcomponent_for/1-- refuses rather than silently renderingkind's content through the wrong component (the "wrong-kind mount" guard: a caller asking to render a:diffbody throughApprovalPromptis a programming error, not a fallback case);bodyfailsvalidate/2.
Options
:context-- render context passed to every mounted component'srender/2(default%{});:widthinside it sizes:message,:reasoning, and:diffbodies.:seal--:live | :sealed(default:sealed), the block's own seal state. Threaded only into the:messagebody, asMessageBlock's render:mode(:live->:streaming,:sealed->:sealed) -- a live message renders with the provisional-close streaming treatment, a sealed one as a plain full parse. Defaulting to:sealedmeans existing direct callers ofmount/3see zero behavior change;BlockBodyis the one caller that passes the block's realseal. Every other kind accepts and ignores it.:outcome-- aBlock.outcome()map (default%{}), consulted only for:tool_callto derive the mounted status glyph (:pendingwith no result yet,:failedon a non-zeroexit_code,:doneotherwise) --Block.contentitself carries no status field of its own.:component-- override the component actually mounted (for testing the wrong-kind refusal above); defaults tocomponent_for(kind).
What {:ok, _} | {:error, _} does NOT cover
The two-tuple result above covers this seam's OWN guard failures
(unknown kind, wrong-kind override, schema validation) -- it does not
catch an exception raised inside the mounted component's own init/1
or render/2 (a schema-valid but wrong-SHAPED prop reaching a
component's internal guard/pattern match, see the :approval schema
note above). This module keeps no try/rescue of its own, by design --
see Raxol.UI.Components.Harness.BlockBody's moduledoc ("the rescue
lives HERE, not inside BodyProvider.mount/3"). Production code never
calls mount/3 directly for exactly that reason: only
BlockBody.render/2 does, and it wraps the call so a component raise
recovers to the same {:error, reason} shape this function returns for
its own failures. A caller that reaches mount/3 directly (as this
module's own tests do) gets an uncaught raise on that path, not an
{:error, _} tuple.
The content-map keys kind requires. An unknown/:opaque kind has no
schema -- an empty list, since component_for/1 refuses it separately
and validate/2 on it always returns :ok (nothing to check, nothing
to mount).
Validates body's keys against kind's schema. Presence-only (never
inspects a value's shape -- see the moduledoc's :approval note): a
content map that has every required key always passes, regardless of
what those keys hold. Missing keys are named explicitly, in schema
order, never a bare "invalid".