phase: 180_markdown_inline_links_images
Build and Push Containers / build-and-push-app (push) Successful in 1m51s
Build and Push Containers / build-and-push-db (push) Successful in 15s

All verification complete. Final state summary:

**Report — phase 180 final verification pass**
- Verified (all already implemented in working tree): `inline()` chain = code→strong→em→linked-image→image→link→headings→lists in `frontend/assets/markdown.js`; `allowLinkUrl`/`allowImageUrl` allowlists (A5), shared `mdAnchorOpen` (target/rel only for absolute http(s)); `####`→`<h5>`, `#####`/`######`→`<h6>`, `#`/`##`→`<h3>`/`###`→`<h4>` unchanged; `.md-link`/`.md-img`/h5-h6 scale in `styles.css` (palette vars only). All surfaces (chat, thinking, restore, viewer, modal) route through the one renderer (pinned in `app.js`/`document.js`).
- Completion criteria:
  - `[text](url)`→`a.md-link`, `javascript:`/`data:` inert — ✅ (node probe + unit + E2E)
  - `![alt](src)`→`img.md-img loading="lazy"` with `alt`; bad src inert — ✅
  - `####`/`#####`/`######` ladder; shallow rungs unchanged — ✅
  - Fenced link/image/`####` stay code; cell-level link renders in `.md-table` — ✅ (node probe)
  - Byte-identity fixture + phase-44/138 suites pass unedited (git-verified untouched; 6 + 3 E2E pass in isolation) — ✅
  - `uv run pytest --cov=app --cov-report=term-missing`: **4198 passed, TOTAL 99%**; `uv run pytest tests/e2e/test_markdown_inline_links_images.py -v --no-cov`: **4 passed**; `uv run ruff check .`: clean; `uv run pyright`: **0 errors** — ✅
- Docs: full README/AGENTS.md review vs the whole project — env-example↔README table complete, page routes match `app/main.py`, AGENTS quick-ref artifacts exist. Fixed 1 inconsistency: README understated the image allowlist (impl per task 02 accepts first segments ≤2 chars, `^.{0,2}/`, not only `./`/`../`); phase-180 paragraphs otherwise accurate.
- Notable: image allowlist `.{0,2}/` is broader than the overview's `^\.{1,2}/` but is what task 02 explicitly specifies and the docstring documents — not a deviation from the tasks; SVG sizing deliberately deferred to phase 181.
- Next pending phase: `181_svg_document_preview`.
This commit is contained in:
2026-10-14 16:03:36 -04:00
parent 3d82a72b6f
commit 6880bd0866
39 changed files with 2952 additions and 1 deletions
+114
View File
@@ -587,6 +587,120 @@ body::before {
quote stays distinct. */
.msg.user .bubble .md-quote { color: var(--bg); }
/* Markdown inline links (phase 180, task 04, TODO.md L4 — "Markdown
[links](some-address) aren't being render correctly and show up as
plain text"): the shared renderer (markdown.js inline chain) turns
[text](url) — and a linked image, locked A6 — into
<a class="md-link">, and an element with no rule anywhere in this file
would paint as unmarked body text. ONE rule set serves every rendering
context the renderer feeds (the chat .bubble brain + user, the
details.thinking / details.reasoning scratchpad, the shared page's
.msg/.bubble copy, the viewer's .doc-md and the document modal, which
shares the renderDocument core) — all of them sit on --surface, and the
deflected bubble's --accent-bg holds the pairing too (8.3:1), so a
single AA pairing covers them all. Palette vars only, no hex/rgb/hsl
literal (the :root-drift test parses the built-ins out of this file — a
literal here would be a second source of truth, and a new hue would
break the monochrome invariant):
color --brand-ink — 9.9:1 on --surface, the :root-documented
AA-safe BRAND TEXT token (the same rose the
details.thinking summary and .msg.brain .avatar use);
underline unconditional — a link in running text must not rely on
color alone (WCAG 1.4.1, PLAN §7.2 "text + color, never
color alone"), and it is what keeps the link marked inside
the user bubble, where the color inverts to the bubble's
own ink below;
offset 2px — lifts the rule off the descenders (g/y/p stay open).
The focus state is the GLOBAL `:focus-visible` 3px --brand ring above
(do NOT restate or reset it here — one ring for the whole app).
Static content — no animation (nothing for prefers-reduced-motion to
still). */
.md-link {
color: var(--brand-ink); /* 9.9:1 on --surface (AA) — :root's brand text token */
text-decoration: underline;
text-underline-offset: 2px;
}
/* :hover brightens one notch on the same rose — --brand-hover is --brand
mixed with 14% white in :root: 5.9:1 on --surface and 5.0:1 on the
deflected bubble's --accent-bg, so the state change never drops under
AA on any canvas the renderer writes into (brightening is the safe
direction on a dark palette — every darker step fails there). */
.md-link:hover { color: var(--brand-hover); }
/* The USER bubble runs the question through the same renderer on the
--brand fill (the .md-quote inversion above is the precedent):
--brand-ink on --brand is 1.9:1 and --brand-hover on --brand is 1.2:1 —
both AA failures — so the link keeps the bubble's own ink, --bg on
--brand at 5.2:1 (the :root pairing). The unconditional underline is
what marks it a link here, never the color. */
.msg.user .bubble .md-link { color: var(--bg); }
/* …and the SAME inversion on :hover: the default --brand-hover would
erase the word (1.2:1 on the fill). A color-mix toward --bg cannot
help — the text is ALREADY --bg — so the hover state moves into the
DECORATION (a 2px rule at a deeper offset), which is a visible state
change that needs no new pair, keeps 5.2:1 at every theme, and stays
inside the palette (no new hue — the monochrome invariant). */
.msg.user .bubble .md-link:hover {
color: var(--bg);
text-decoration-thickness: 2px;
text-underline-offset: 3px;
}
/* Markdown inline images (phase 180, task 04, TODO.md L3 — the in-answer
half of "Need SVG render support on the front end"): ![alt](src) and
[![alt](src)](url) render <img class="md-img"> through the shared
renderer, so the painted diagram has to stay INSIDE the reading column
instead of pushing a horizontal scrollbar (the TODO's complaint is a
reference that prints; the fix is only useful when what replaces it
fits). No fixed width/height — max-width:100% + height:auto keep any
aspect ratio at 375px and at 1440px (the .doc-image-img / .source-image-img
contract), object-fit:contain never crops, and the card language
(--surface plate, --line hairline, --radius-sm) is the treatment every
other image in the app already carries, so a diagram inside an answer
reads as an image rather than a stray artifact. inline-block so the
block-ish margin behaves and the image sits as its own box inside a
wrapping <a> (locked A6) instead of on a text baseline. Palette vars
only (no literal). Static content — no animation (nothing for
prefers-reduced-motion to still). Known limit (phase 181): an SVG that
declares a viewBox but no width/height paints at the replaced-element
default box, constrained by max-width — vector sizing is phase 181's
contract, so NO .md-img--vector modifier here. */
.md-img {
display: inline-block;
max-width: 100%;
height: auto;
object-fit: contain;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
margin: 0.4rem 0;
}
/* The deep rungs of the heading ladder (phase 180, task 04, TODO.md L5 —
"Quad-level headers (####) aren't being rendered"): the renderer now
emits <h5> for #### and <h6> for #####/###### (locked A4) and neither
tag had a rule ANYWHERE in this file, so a deep heading in an answer
inherited the UA defaults (h5 0.83em / h6 0.67em with 1.67em / 2.33em
block margins — ~1.4rem / 1.6rem on this scale) — gaps wider than the
paragraphs around it while the type itself went SMALLER than the answer
text it sits in. ONE rule set per level for every renderer context (the
viewer's .doc-md, the chat AND shared-page .bubble, the thinking
scratchpad) — no chat-specific duplication — extending the existing
.doc-md h3 1.15rem / .doc-md h4 1rem scale MONOTONICALLY
(1.15 → 1.0 → 0.95 → 0.88): every level stays a visible step from the
1rem body text and from its neighbor, and the margins
tighten with the depth on the same h3 1.1/0.4 → h4 0.9/0.35 rhythm.
Sizes are rem (never px), so the ladder survives a browser
font-size/zoom change; no color is set, so the scale adds no new
contrast pair (it inherits each context's own AA ink — ink on --surface
15.3:1, ink-soft on --surface 8.2:1 in the scratchpad). Known, accepted:
the 0.875rem scratchpad owns no h3/h4 rules of its own (UA sizes there),
so the fixed 0.95rem h5 rides a hair above its body text — a heading
stays a heading, and re-sizing the shallower rungs is out of this
phase's scope. Static content — no animation (nothing for
prefers-reduced-motion to still). */
.doc-md h5, .msg .bubble h5, .thinking-text h5 { margin: 0.8rem 0 0.3rem; font-size: 0.95rem; }
.doc-md h6, .msg .bubble h6, .thinking-text h6 { margin: 0.7rem 0 0.3rem; font-size: 0.88rem; }
.msg.user { justify-content: flex-end; }
.msg.user .msg-body { align-items: flex-end; }
.msg.user .bubble {