Fenced code blocks render with no visual container and lose their list indent
Environment
- Claude Code 2.1.237, installed at
~/.local/share/claude/versions/2.1.237 - macOS (Darwin 25.6.0), arm64
- Ghostty 1.3.1, default theme (background #282c34, foreground #ffffff)
TERM=xterm-256color,COLORTERM=truecolor- Dark theme, syntax highlighting active
What happens
A fenced code block in an assistant message is visually indistinguishable from the prose around it. Both render in the same default foreground color, and the block has no border, background, gutter, or left margin. When the block is nested inside a numbered list item, it also renders three columns to the left of that item's own text.
The practical effect: a numbered list of shell commands reads as run-on prose. Each command block sits closer to the item below it than to the item it belongs to.
What I expected
Some persistent visual boundary on the block, in the same spirit as the frame that Bun's own markdown-to-ANSI renderer draws (see below), and an indent matching the list item that contains it.
Measurements
I decoded a screenshot of one assistant message and sampled glyph colors. Line pitch is 17px, cell width 8px.
| Element | Color | Notes |
| --- | --- | --- |
| Prose text | #c1c2c4, #ffffff | Default foreground |
| Code block body (paths, git, pnpm) | #c1c2c4, #ffffff | Identical distribution to prose |
| Code block cd, ls | #87bab3 | highlight.js built_in |
| Code block "…" | #cc6766 | highlight.js string |
| Inline code | #b0b9f9 | Theme permission, rgb(177,185,249) |
| Background, everywhere | #282c34 | Terminal default, including behind inline code |
Horizontal positions in the same message:
- Code block lines start at x=26, column 3.
- Wrapped prose of the containing list item starts at x=49, column 6.
Vertical gaps around each block: 34px above (one blank row), 17px below (no blank row).
Where it comes from
From the bundled JS in the 2.1.237 binary, in the markdown token renderer (c3):
case "code": {
let p = e.lang ?? "",
f = p.match(/^[\w.+#-]+/)?.[0] ?? "",
m = s && p && s.supportsLanguage(p) ? p
: s && f && s.supportsLanguage(f) ? f
: "plaintext",
h = p && !s?.supportsLanguage(p) ? rr.dim(p) + jU : "";
if (!s) return h + e.text + jU;
return h + s.highlight(e.text, { language: m }) + jU
}
case "codespan": return Vo("permission", t)(e.text);
jU is "\n". The code case returns highlighted text plus one newline. Nothing wraps it.
The lost indent has a separate cause. c3 destructures listIndent: c = "", and case "list_item" computes the continuation indent, then exempts several child types from receiving it:
let _ = (e.tokens ?? []).map((S) => {
let v = c3(S, t, { …, listIndent: m, … });
if (S.type === "code" || S.type === "blockquote" || S.type === "hr" ||
S.type === "table" || S.type === "list" || S.type === "space")
return v; // returned verbatim, no indent prefix
…
})
case "code" receives listIndent and never reads c, so nothing re-applies it.
For contrast, the Bun 1.4.0 runtime that the binary embeds ships Bun.markdown.ansi, which handles the same input like this:
1. Run the promote. It places each PDF and rewrites the version constants, including the
map-print reference the first promote missed.
┌─ sh
│ cd /Users/…/packages/bc-e2e
│ pnpm golden:promote
└─
2. Check what it did.
It indents to the item's content margin, draws a dim frame, and labels the language. It does no syntax highlighting, so it isn't a drop-in replacement, but the block is unmistakable.
Suggested fixes
Either one alone would help; the first matters more.
- Give fenced blocks a container. A dim left rail per line would be enough, and the renderer already has one:
case "blockquote"prefixes every line with a dim▎(U+258E). - Apply
listIndentincase "code", or stop exemptingcodefrom the indent prefix incase "list_item". - Emit a blank row after the block, matching the one above it.
Workaround
Wrapping the commands in a blockquote instead of a fence gets the dim ▎ rail and keeps highlight.js coloring on nested code, at the cost of italics on every line.