feat(messages): make task-list checkboxes interactive - #7496
Open
DocStream-Oficial wants to merge 1 commit into
Open
feat(messages): make task-list checkboxes interactive#7496DocStream-Oficial wants to merge 1 commit into
DocStream-Oficial wants to merge 1 commit into
Conversation
…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)
🔐 Codex Security Review
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A GFM task list already renders as checkboxes in messages, but they are inert —
disabledandpointer-events-noneinMarkdownInput. This makes themclickable 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 itduring the mdast→hast conversion, so there is no offset to map a click back to
the
- [ ]that produced it.rehypeTaskIndexstamps each checkbox with its ordinal instead, andtoggleTaskMarkerresolves 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 ascode, not as a checkbox) and both require whitespace or end-of-line after the
closing bracket, which is the GFM rule.
rehypeTaskIndex.test.mjsrendersthrough 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 —
toggleTaskMarkerreturnsnulland nothing is written.Authorization and side effects
Gated on the existing
canManageMessageForCurrentUser, so the same rule asediting: 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/mentionPubkeysunset, soapplyEditTagOverlaypasses the original tags through: attachments and mentionssurvive, and ticking a box wakes nobody.
Interactivity is opt-in per surface. Without an
onToggleTaskon the Markdownruntime 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 ownCheckBoxMdmatches[ ] textwithno bullet, while
UnOrderedListsits earlier in the component list andclaims
- anything, so a task list written on desktop arrived as a bullet with aliteral
[ ]in the text.GfmTaskListMdis spliced into the component list at the one position thatworks: after
CodeBlockMd, so a task inside a fence stays code, and beforeUnOrderedList. Both neighbours matter and a test covers the ordering — puttingit first also beat
CodeBlockMdand 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
nodeCacheneeds no new key segment. The callback travelsthrough
MarkdownRuntimeContextrather than as a prop, which keeps the componentmap module-stable.
Testing
toggleTaskMarker,rehypeTaskIndex), 4 Playwrightspecs covering own-message toggle, someone else's message staying inert, a
task list typed into the composer, and the composer button building a list.
plus widget tests that render through
gpt_markdownand assert GFM renders ascheckboxes, ordinals follow document order, and a tap flips the right marker.
tsc --noEmit,biome checkandflutter analyzeclean.Not verified: rendering on a physical Android device — no emulator was available
in my environment. The mobile widget tests exercise the real render path.