Skip to content

feat(messages): make task-list checkboxes interactive - #7496

Open
DocStream-Oficial wants to merge 1 commit into
block:mainfrom
DocStream-Oficial:upstream/task-list-checkboxes
Open

feat(messages): make task-list checkboxes interactive#7496
DocStream-Oficial wants to merge 1 commit into
block:mainfrom
DocStream-Oficial:upstream/task-list-checkboxes

Conversation

@DocStream-Oficial

Copy link
Copy Markdown

A GFM task list already renders as checkboxes in messages, but they are inert —
disabled and pointer-events-none in MarkdownInput. This makes them
clickable on desktop and tappable on mobile, and adds a composer button on both
so the syntax need not be typed.

Ticking a box rewrites its marker in the message source and publishes that as an
ordinary message edit. The message text stays the single source of truth, so the
state survives a reload, reads the same on every client, and is visible if you
open the message in the editor. No new event kind, no parallel state.

How a click finds its marker

The rendered <input> carries no source position — remark-gfm generates it
during the mdast→hast conversion, so there is no offset to map a click back to
the - [ ] that produced it.

rehypeTaskIndex stamps each checkbox with its ordinal instead, and
toggleTaskMarker resolves that ordinal against the source.

The invariant this rests on: the renderer and the rewriter must count the
same markers. If they ever disagree, a click flips the wrong line — silently.
Both therefore skip fenced code blocks (a - [ ] inside a fence renders as
code, not as a checkbox) and both require whitespace or end-of-line after the
closing bracket, which is the GFM rule. rehypeTaskIndex.test.mjs renders
through the real plugin pair and asserts the two counts agree on exactly the
input that breaks naive counting.

When the ordinal no longer resolves — the message changed between the render and
the click — toggleTaskMarker returns null and nothing is written.

Authorization and side effects

Gated on the existing canManageMessageForCurrentUser, so the same rule as
editing: the author, or the owner of an agent that posted. That last part is the
useful case — an owner can tick off a checklist their agent published.

The toggle publishes content only, leaving mediaTags/mentionPubkeys unset, so
applyEditTagOverlay passes the original tags through: attachments and mentions
survive, and ticking a box wakes nobody.

Interactivity is opt-in per surface. Without an onToggleTask on the Markdown
runtime the checkbox renders exactly as it does today, so forum posts, profile
bios, project READMEs and previews are unchanged.

Mobile needed the renderer fixed first

On mobile the feature was not merely missing — GFM task lists never rendered as
checkboxes at all. gpt_markdown's own CheckBoxMd matches [ ] text with
no bullet, while UnOrderedList sits earlier in the component list and
claims - anything, so a task list written on desktop arrived as a bullet with a
literal [ ] in the text.

GfmTaskListMd is spliced into the component list at the one position that
works: after CodeBlockMd, so a task inside a fence stays code, and before
UnOrderedList. Both neighbours matter and a test covers the ordering — putting
it first also beat CodeBlockMd and turned code samples into live checkboxes.

Cache safety

The ordinal depends only on the content, so the parsed tree stays a pure function
of its inputs and nodeCache needs no new key segment. The callback travels
through MarkdownRuntimeContext rather than as a prop, which keeps the component
map module-stable.

Testing

  • Desktop: 14 unit tests (toggleTaskMarker, rehypeTaskIndex), 4 Playwright
    specs covering own-message toggle, someone else's message staying inert, a
    task list typed into the composer, and the composer button building a list.
  • Mobile: 16 tests — pure marker logic mirroring the desktop cases one for one,
    plus widget tests that render through gpt_markdown and assert GFM renders as
    checkboxes, ordinals follow document order, and a tap flips the right marker.
  • tsc --noEmit, biome check and flutter analyze clean.

Not verified: rendering on a physical Android device — no emulator was available
in my environment. The mobile widget tests exercise the real render path.

…mobile

A GFM task list already rendered as checkboxes on desktop, but they were
inert: `disabled` and `pointer-events-none`. Tapping one now rewrites its
marker in the message source and publishes that as an ordinary message
edit, so the text stays the single source of truth — the state survives a
reload, reads the same on every client, and is visible in the editor.

The rendered checkbox carries no source position: remark-gfm generates the
`<input>` during the mdast-to-hast conversion, so there is no offset to map
a click back to the `- [ ]` that produced it. `rehypeTaskIndex` stamps each
box with its ordinal instead, and `toggleTaskMarker` resolves that ordinal
against the source. Both count markers the same way — fenced code skipped,
whitespace required after the bracket — because if they ever disagree a tap
flips the wrong line.

Authorization is the existing `canManageMessageForCurrentUser`, so an agent
owner can tick a list their agent posted. The toggle publishes content only,
leaving tags untouched, so attachments and mentions survive and a checkbox
wakes nobody.

Mobile needed the renderer fixed first: `gpt_markdown`'s `CheckBoxMd` matches
`[ ] text` with no bullet, while `UnOrderedList` sits earlier in the component
list and claims `- anything`, so GFM task lists arrived as literal text.
`GfmTaskListMd` is spliced after `CodeBlockMd` and before `UnOrderedList` —
both neighbours matter, and a test covers the ordering.

Composers on both platforms get a button so the syntax need not be typed.

Signed-off-by: DocStream <docstream@docstream.com.mx>
(cherry picked from commit 96e3261360ad054828649d32f900e5e6f83729a2)
@DocStream-Oficial
DocStream-Oficial requested a review from a team as a code owner September 8, 2026 19:51
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 44316ff72f5f7de014c66b01cbf534298a70c249...9188eac1a03cfc03e5f8cf0b0b4fc9493efb8b12.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 9188eac1a03cfc03e5f8cf0b0b4fc9493efb8b12 to authorize a new review.
Any previous review applies only to its recorded range.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant