A source the Markdown renderer refuses now goes through the same element
with no src rather than a separate light-DOM span, so a blocked source and
a source that fails to load are one piece of UI instead of two. That drops
the span's stylesheet rule and the only reason the renderer built elements
just to serialise them.
Decoding what marked escaped no longer runs untrusted markup through a
detached element's innerHTML; the entities marked actually emits are
decoded in one pass, and attributes are escaped on the way out. With the
DOM dependency gone the renderer is directly unit-testable, so the escaping
and the protocol allowlist now have coverage that does not need Electron.
The element's shadow styles also stop hard-coding values that already exist
as tokens -- custom properties cross the shadow boundary, so --muted and
--hairline-strong are now the single source of truth -- and the height cap
becomes --session-image-max-block, which compact Markdown surfaces (subagent
panes, memory rows, tool results) lower from a viewport fraction to 240px.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
marked <= 14 calls renderer.image(href, title, text); marked >= 15 passes
the token instead. With only the positional form handled, an upgrade of the
pinned CDN build would turn every session image into fallback text without
any error, so normalise both shapes and cover them with a unit test.
The tag name moves into session-image-contract.js so the Markdown renderer
no longer reaches it through the module that imports the .vue component,
which is what kept it out of Node's test runner.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>