A numbered list after a bulleted list is drawn as more bullets: the markdown parser joins lists whose markers differ #133

Closed
opened 2026-10-02 23:34:19 -04:00 by cmoriarty · 1 comment
Owner

Seen while investigating #130, which leaves it for an issue of its own.

parseList in ui/src/lib/markdown.ts adds every list item at the list's indent to the list it is in, whatever its marker. So this answer:

- Prune heartbeat rows older than two days

1. Add the timer
2. Update the query

is parsed as one bulleted list of three items, and drawn as:

- Prune heartbeat rows older than two days
- Add the timer
- Update the query

The numbers are lost, and so is the blank line between the two lists.

Where

Two places in parseList take an item of any kind:

  • After a blank line, continues checks BULLET.exec(next) ?? ORDERED.exec(next), so any list marker at the list's indent continues the list.
  • A new item's line matches BULLET or ORDERED, and joins the current list without its marker being compared with the list's.

Every mix does it

In both directions, and with or without a blank line between. Each source below was parsed with parseMarkdown at 5d67ff9:

Source Parsed today CommonMark
- a, a blank line, 1. b one bulleted list: a, b a bulleted list, then a numbered list
- a, then 1. b one bulleted list: a, b a bulleted list, then a numbered list
1. a, a blank line, - b one numbered list: a, b (b drawn as 2.) a numbered list, then a bulleted list
- a, then * b one bulleted list two bulleted lists
1. a, then 1) b one numbered list two numbered lists
1. foo, 2. bar, 3) baz one numbered list of three a list of two, then a list starting at 3 (the spec's example 302)

CommonMark (5.3, Lists): two list items are of the same type when their markers are the same bullet character, or numbers with the same delimiter (. or )). Changing the bullet character or the delimiter, or switching between bullets and numbers, starts a new list. Items of the same type stay one list across blank lines, so - a, a blank line, - b is one list, as it is today.

Where it shows

Everything drawn through parseMarkdown: an agent's answers, live and settled, subagent reports, thoughts, and a gate's evidence. The fake osfd's streaming answer (ui/dev/fakeosfd.py, ANSWER_TEXT, under ### The plan) has exactly this shape, so in the agent-view spec's answer 1. Add the timer, 2. Update the query and 3. Update the panel are drawn as - items of the bulleted list above them.

Fix

Key a list on the kind of its first marker: the bullet character, or the delimiter after a number. An item of another kind at the list's indent ends the list and starts a new one. Unit tests for bullets then numbers, numbers then bullets, - then *, and 1. then 1): each parses into separate lists, while - a, a blank line, - b stays one list.

#130 changes repairStreaming in the same file and adds a test that walks every prefix of an answer, counting list items as rows. A separate numbered list after a bulleted one adds a blank row between them, so that walk has to pass with this fix too.

Seen while investigating #130, which leaves it for an issue of its own. `parseList` in `ui/src/lib/markdown.ts` adds every list item at the list's indent to the list it is in, whatever its marker. So this answer: ``` - Prune heartbeat rows older than two days 1. Add the timer 2. Update the query ``` is parsed as one bulleted list of three items, and drawn as: ``` - Prune heartbeat rows older than two days - Add the timer - Update the query ``` The numbers are lost, and so is the blank line between the two lists. ### Where Two places in `parseList` take an item of any kind: - After a blank line, `continues` checks `BULLET.exec(next) ?? ORDERED.exec(next)`, so any list marker at the list's indent continues the list. - A new item's line matches `BULLET` or `ORDERED`, and joins the current list without its marker being compared with the list's. ### Every mix does it In both directions, and with or without a blank line between. Each source below was parsed with `parseMarkdown` at 5d67ff9: | Source | Parsed today | CommonMark | |---|---|---| | `- a`, a blank line, `1. b` | one bulleted list: a, b | a bulleted list, then a numbered list | | `- a`, then `1. b` | one bulleted list: a, b | a bulleted list, then a numbered list | | `1. a`, a blank line, `- b` | one numbered list: a, b (b drawn as `2.`) | a numbered list, then a bulleted list | | `- a`, then `* b` | one bulleted list | two bulleted lists | | `1. a`, then `1) b` | one numbered list | two numbered lists | | `1. foo`, `2. bar`, `3) baz` | one numbered list of three | a list of two, then a list starting at 3 (the spec's example 302) | CommonMark (5.3, Lists): two list items are of the same type when their markers are the same bullet character, or numbers with the same delimiter (`.` or `)`). Changing the bullet character or the delimiter, or switching between bullets and numbers, starts a new list. Items of the same type stay one list across blank lines, so `- a`, a blank line, `- b` is one list, as it is today. ### Where it shows Everything drawn through `parseMarkdown`: an agent's answers, live and settled, subagent reports, thoughts, and a gate's evidence. The fake osfd's streaming answer (`ui/dev/fakeosfd.py`, `ANSWER_TEXT`, under `### The plan`) has exactly this shape, so in the agent-view spec's answer `1. Add the timer`, `2. Update the query` and `3. Update the panel` are drawn as `- ` items of the bulleted list above them. ### Fix Key a list on the kind of its first marker: the bullet character, or the delimiter after a number. An item of another kind at the list's indent ends the list and starts a new one. Unit tests for bullets then numbers, numbers then bullets, `-` then `*`, and `1.` then `1)`: each parses into separate lists, while `- a`, a blank line, `- b` stays one list. #130 changes `repairStreaming` in the same file and adds a test that walks every prefix of an answer, counting list items as rows. A separate numbered list after a bulleted one adds a blank row between them, so that walk has to pass with this fix too.
Author
Owner

Shipped in 457f581, deployed on 2026-10-03 at 01:16 (Actions run 96, together with #130, #132 and #134). Archived in 81e567f as openspec/changes/archive/2026-10-03-marker-change-starts-new-list.

What changed

  • parseList keys a list on the kind of its first marker: the bullet character (-, * or +), or the delimiter after a number (. or )), as CommonMark does (0.31.2, section 5.3). An item with another kind of marker, at the list's indent, ends the list and starts a new one, with or without a blank line between them.
  • A new numbered list counts from its own first number: 3) baz after 2. bar starts at 3.
  • Items with one kind of marker stay one list across blank lines, as before. A nested list follows the same rule inside its item.
  • It applies wherever the console draws markdown: an agent's answers, live and settled, subagent reports, thoughts and a gate's evidence. The fixture answer's plan is now four bullets, then 1., 2. and 3. one line below them.
  • agent-transcript has a new requirement, "A change of list marker starts a new list".

Tested

  • Unit tests in ui/src/lib/markdown.test.ts (block lists) cover bullets then numbers, numbers then bullets, - then *, and 1. then 1), each with and without a blank line. They also cover the start number, a nested list and the rendered markup. All 7 split cases failed before the fix. - a, a blank line, - b stays one list.
  • #130's walk over every prefix of an answer passes with this change. In the five shapes it alters, the row count first differs where the new list's marker arrives, one row higher for the gap, and never falls.
  • In Chromium against the fixture, every sampled frame that showed a numbered step, 213 of them, drew it in an ol. The settled plan is a ul of four items, then an ol of three with decimal markers, 22.4 px (one line) below. agent-view.spec.ts passed 8 of 8.
  • The fast lane and the full lane pass. The full lane took 165 s: backend 2,363, UI unit 533, browser 153. Its first run failed agent-view.spec.ts:202 while the machine was loaded. That spec passes by only 2 to 18 characters, with this change or without it, and is now #135.

Verified on production

  • /api/status reports 7b6a101, which contains this change. /healthz has a new boot_id, boot_01M4031RD5W1RT9B98G2MNF1EE.
  • The served index-DPU8Rmow.js is byte for byte the bundle a local build of 264202d produces.
  • Not checked on production: a real agent transcript with this shape. The rendering was checked in Chromium against the fixture, with the same bundle.

Left over

  • Nesting is still decided by indent past the list's marker rather than past the item's content. So - a then 1. b (one space in) is still a nested list, where CommonMark starts a new one.
Shipped in 457f581, deployed on 2026-10-03 at 01:16 (Actions run 96, together with #130, #132 and #134). Archived in 81e567f as `openspec/changes/archive/2026-10-03-marker-change-starts-new-list`. ### What changed - `parseList` keys a list on the kind of its first marker: the bullet character (`-`, `*` or `+`), or the delimiter after a number (`.` or `)`), as CommonMark does (0.31.2, section 5.3). An item with another kind of marker, at the list's indent, ends the list and starts a new one, with or without a blank line between them. - A new numbered list counts from its own first number: `3) baz` after `2. bar` starts at 3. - Items with one kind of marker stay one list across blank lines, as before. A nested list follows the same rule inside its item. - It applies wherever the console draws markdown: an agent's answers, live and settled, subagent reports, thoughts and a gate's evidence. The fixture answer's plan is now four bullets, then `1.`, `2.` and `3.` one line below them. - `agent-transcript` has a new requirement, "A change of list marker starts a new list". ### Tested - Unit tests in `ui/src/lib/markdown.test.ts` (block `lists`) cover bullets then numbers, numbers then bullets, `-` then `*`, and `1.` then `1)`, each with and without a blank line. They also cover the start number, a nested list and the rendered markup. All 7 split cases failed before the fix. `- a`, a blank line, `- b` stays one list. - #130's walk over every prefix of an answer passes with this change. In the five shapes it alters, the row count first differs where the new list's marker arrives, one row higher for the gap, and never falls. - In Chromium against the fixture, every sampled frame that showed a numbered step, 213 of them, drew it in an `ol`. The settled plan is a `ul` of four items, then an `ol` of three with decimal markers, 22.4 px (one line) below. `agent-view.spec.ts` passed 8 of 8. - The fast lane and the full lane pass. The full lane took 165 s: backend 2,363, UI unit 533, browser 153. Its first run failed `agent-view.spec.ts:202` while the machine was loaded. That spec passes by only 2 to 18 characters, with this change or without it, and is now #135. ### Verified on production - `/api/status` reports 7b6a101, which contains this change. `/healthz` has a new boot_id, `boot_01M4031RD5W1RT9B98G2MNF1EE`. - The served `index-DPU8Rmow.js` is byte for byte the bundle a local build of 264202d produces. - Not checked on production: a real agent transcript with this shape. The rendering was checked in Chromium against the fixture, with the same bundle. ### Left over - Nesting is still decided by indent past the list's marker rather than past the item's content. So `- a` then ` 1. b` (one space in) is still a nested list, where CommonMark starts a new one.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cmoriarty/braid#133
No description provided.