Skip to content

fix(markdown): render nested chat lists with an indentation stack#5582

Open
BrunooMoniz wants to merge 1 commit into
odysseus-dev:devfrom
BrunooMoniz:fix/nested-markdown-lists
Open

fix(markdown): render nested chat lists with an indentation stack#5582
BrunooMoniz wants to merge 1 commit into
odysseus-dev:devfrom
BrunooMoniz:fix/nested-markdown-lists

Conversation

@BrunooMoniz

Copy link
Copy Markdown
Contributor

Summary

The chat markdown renderer built lists with regex passes that anchored on the absolute start of each line and grouped only consecutive marker lines, with no notion of indentation. As a result, any indented item in a nested list (for example - Sub) never matched a list pass, fell through to the paragraph pass, and rendered as flat literal text such as - Sub while the surrounding list fragmented; an interrupted ordered list even restarted its numbering. The ordered-nested case was worse — an indented 1. Sub was wrapped as an <ol> inside a <p>, which is invalid HTML. This PR replaces the flatten-and-group passes with a small _buildLists helper that parses each item's indentation and walks a per-run indentation stack: a deeper item opens a child list inside the still-open parent <li> (<li>...<ul>...</ul></li>) and a dedent closes levels until the indentation matches an open one. Indentation is compared relatively to the parent on the stack, so the inconsistent 2-/3-/4-space and tab widths that models emit all nest the same way, and mixed ordered/unordered nesting produces valid HTML. Task checkboxes keep their task-item class and nest correctly; a small flex-wrap/flex-basis rule drops a sub-list nested under a task item onto its own line instead of sitting inline after the label.

Target branch

  • This PR targets dev, not main. All PRs land in dev; main is curated by the maintainer at each release. If your PR is on main by accident, click "Edit" on this PR and change the base.

Linked Issue

Fixes #5581

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)
  • New feature (non-breaking — adds new behaviour)
  • Breaking change (changes or removes existing behaviour)
  • Refactor / cleanup (behaviour unchanged)
  • Documentation only
  • CI / tooling / configuration

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app (docker compose up or uvicorn app:app) and verified the change works end-to-end. Type-checks and unit tests are not enough.

How to Test

Steps a reviewer can reproduce (full app):

  1. docker compose up -d --build, open the chat.
  2. Get the assistant to reply with an indented nested list, e.g. prompt it to echo exactly:
    - Backend
      - API routes
      - Database models
    - Frontend
    
    1. First
    2. Second
       1. Sub
    
    - [ ] Parent task
      - [ ] Child task
    
  3. On current dev: the indented items render as flat literal - ... / - [ ] ... text, the ordered list restarts at 1. after the sub-item, and DevTools shows an <ol> inside a <p>. With this branch: the sub-items render as indented nested sub-lists, ordered numbering stays continuous, nested checkboxes stay checkboxes, and the HTML is valid.

What was verified for this change:

  • The nested-list rendering was confirmed in a browser at desktop (1280px) and mobile (390px) widths, current dev (before) vs this branch (after) — see the screenshots below.
  • node --check static/js/markdown.js — passes.
  • python -m pytest tests/test_markdown_rendering_js.py — 22 passed (9 new nested-list cases plus the existing flat-list / table / thinking / autolink contracts).
  • node --test tests/streaming/ (streaming-invariant + segmenter) — 126 passed; the fuzz streams every corpus sample token-by-token, including the new nested sample.

Files changed:

  • static/js/markdown.js — new _buildLists indentation-stack builder replaces the ordered/task/unordered flatten-and-group passes in mdToHtml.
  • static/style.cssli.task-item gets flex-wrap: wrap, plus a rule so a nested <ul>/<ol> under a task item takes its own full-width line and the label keeps its row (reuses the existing task-item / task-text classes; no new variables, colors, or fonts).
  • tests/test_markdown_rendering_js.py — nested unordered/ordered/mixed/task/deep cases (and a guard for the flat-list path).
  • tests/streaming/corpus.mjs — a mixed nested-list sample for the streaming-invariant corpus.

Visual / UI changes — REQUIRED if you touched anything that renders

Anything that changes what the UI looks like — buttons, icons, padding, colors, fonts, spacing, layout, CSS, HTML, SVG, or any static/js/ module that draws to the DOM — needs all of the following.

  • Screenshot or short clip of the change in the running app, attached below. Mobile screenshot too if the change affects mobile.
    • How the screenshots were captured: rendered through the app's own chat renderer (mdToHtml) and static/style.css in Chromium (desktop, 1280px) and WebKit (mobile, 390px) at the default dark theme, before (current dev) vs after (this branch).
  • Style match: the change uses Odysseus's existing visual language. Specifically:
    • Reuse existing CSS variables (--red, --fg, --bg, --card, --border, etc.) — do not introduce new color values, font sizes, or spacing units.
    • Reuse existing button/input/card/border classes. Don't invent parallel styling.
    • No Unicode emoji in UI or code. Use inline SVG (matching the monochrome icon style already in static/index.html) or plain text.
    • Monospaced font (Fira Code) for primary UI text. Don't override.
    • Dark theme is the default; any light-mode work must be wired through the existing theme system, not hard-coded.
  • No new component patterns. If a similar widget already exists in the app, extend it instead of writing a parallel one.
  • I am not an LLM agent submitting a bulk PR. If you are, please open an issue describing the problem first — bulk auto-generated PRs that don't match the project's visual style are closed on sight, even when the underlying fix is correct.

Screenshots / clips

Desktop — before (current dev)

nested lists before, desktop

Desktop — after (this PR)

nested lists after, desktop

Mobile — before (current dev)

nested lists before, mobile

Mobile — after (this PR)

nested lists after, mobile

The chat markdown renderer built lists with regex passes anchored on the
absolute start of the line (`^(\d+)\. `, `^(?:- |\* )...`) and grouped only
consecutive sentinel lines, with no notion of depth. An indented item such as
`  - Sub` never matched, fell through to the paragraph pass, and rendered as
literal `- Sub` text while the parent list fragmented. The ordered-nested case
was worse: it emitted an `<ol>` inside a `<p>`, which is invalid HTML.

Replace the flatten-and-group passes with `_buildLists`, which parses each
item's indentation and walks a per-run indentation stack: a deeper item opens a
child list inside the still-open parent `<li>` (`<li>...<ul>...</ul></li>`) and
a dedent closes levels until the indentation matches an open one. Indentation is
compared relatively to the parent on the stack, so inconsistent 2-/3-/4-space
and tab widths nest the same way and mixed ordered/unordered nesting produces
valid HTML. Task-checkbox items keep their `task-item` class and nest correctly;
a small flex-wrap/flex-basis rule drops a sub-list nested under a task item onto
its own line instead of sitting inline after the label.

Adds nested unordered/ordered/mixed/task/deep cases to
tests/test_markdown_rendering_js.py and a mixed-nested sample to the streaming
corpus; the existing flat-list, table, code, and autolink contracts are
unchanged.
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nested markdown lists render flat; an indented ordered sub-item emits <ol> inside <p> (invalid HTML)

1 participant