Skip to content

[Detail Bug] Chat markdown: fenced code blocks in numbered lists render empty and break subsequent list items #219

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_1790dd71-9a2b-4692-bedf-77a88bd6cf18

Introduced in #141 by @WilliamAGH on Jul 28, 2026

Summary

  • Context: parseMarkdown in frontend/src/lib/services/markdown.ts preprocesses streamed chat markdown via prepareMarkdownForParsing, which calls nestNumericListFences (in frontend/src/lib/services/numericListFenceNesting.ts) to re-indent fenced code blocks so they stay attached to numeric ordered-list continuations before marked lexes them.
  • Bug: nestNumericListFences computes a single fixed bodyIndentation prefix of max(0, continuation - openingFenceMarkerIndex). When the streamed fence rails are already indented to (or beyond) the list-item continuation column but the literal code body is written at a lower column (e.g. body at column 0 under rails at column 3), bodyIndentation collapses to 0 and the body lines are emitted unchanged.
  • Actual vs. expected: The un-indented body lines fall below the list-item continuation indent, so marked terminates the list item mid-fence: the fenced code block renders empty, the code body leaks out as a loose paragraph, and the following numbered step (e.g. 2. Step two) is rendered as an indented code block instead of an <ol><li>.
  • Impact: User-facing markdown corruption: nested code blocks inside numbered steps can render empty, code leaks into plain paragraphs, and subsequent numbered steps are no longer parsed as list items.

Code with Bug

In frontend/src/lib/services/numericListFenceNesting.ts:

if (fenceCandidate) {
  bodyIndentation = " ".repeat(
    Math.max(0, awaitedContinuationIndentation - fenceCandidate.markerIndex), // <-- BUG 🔴 collapses to 0 when fence rails are already at/beyond continuation column
  );
  targetIndentation = awaitedContinuationIndentation;
  maximumSourceIndentation =
    awaitedContinuationIndentation + COMMONMARK_MAX_FENCE_INDENTATION;
  fenceState.open(fenceCandidate.marker);
  awaitedContinuationIndentation = null;
  nestedLines.push(
    `${" ".repeat(targetIndentation)}${markdownLine.slice(fenceCandidate.markerIndex)}`,
  );
  continue;
}
} else {
  nestedLines.push(`${bodyIndentation}${markdownLine}`); // <-- BUG 🔴 under-indented body lines pass through and drop below list continuation indent
}

Explanation

The preprocessor only computes one bodyIndentation value based on the opening fence’s column. If the rails are already indented to the list continuation column (e.g. 3 spaces under 1.), bodyIndentation becomes 0. Any code body line with fewer leading spaces than the continuation indentation then remains under-indented, causing marked to close the list item before the body. Result: an empty <pre><code> in the list item, the body rendered outside the list, and subsequent numbered items parsed incorrectly.

Codebase Inconsistency

Existing tests already “rescue” a flush-left body when the opening fence is at column 0 (because bodyIndentation = max(0, 3 - 0) = 3), but the identical flush-left body is not rescued when the opening fence is at the continuation column (because bodyIndentation = max(0, 3 - 3) = 0). This asymmetry indicates an incomplete indentation calculation rather than an intentional refusal to handle such input.

Recommended Fix

Re-indent each fence body line based on its own leading spaces so it lands at (at least) targetIndentation, rather than relying on a single fixed prefix derived from the opening fence position:

const lineIndentation = leadingSpaceCount(markdownLine);
const padding = Math.max(0, targetIndentation - lineIndentation);
nestedLines.push(`${" ".repeat(padding)}${markdownLine}`); // <-- FIX 🟢 lifts under-indented body lines into the list item

History

This bug was introduced in commit e57f73b, which added nestNumericListFences and the bodyIndentation = max(0, continuation - markerIndex) calculation. Later commits preserved this calculation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions