fix: upload chat-message markup as MarkupBlobRef instead of raw HTML (HULY-20) - #44
Conversation
…(HULY-20)
`huly comment add` (and 7 sibling write paths on chat-shaped messages)
passed the raw HTML body string straight into the `message` field. The
Huly platform expects `message` to be a MarkupBlobRef pointing to a
prosemirror-JSON blob in MinIO — same shape as `issue.description` after
`huly issue create` runs it through `uploadMarkup`. With raw HTML, the
front-end renders the markup as plain text (literal `<h1>`, `<p>`,
`<table>` characters) instead of as rich content. The audit in the
HULY-8 PR description flagged this as the same bug class across the
following surfaces:
* `huly comment add` — chunter:class:ChatMessage (issue.comments)
* `huly comment update`
* `huly activity reply add` — activity:class:ActivityMessage (replies)
* `huly activity reply update`
* `huly channel message send` — chunter:class:ChatMessage (channel.messages)
* `huly channel message update`
* `huly channel thread add <ref>` — chunter:class:ThreadMessage (replies)
* `huly channel thread update <ref>`
* `huly dm message send` — chunter:class:ChatMessage (DM.messages)
* `huly approval comment` — chunter:class:ChatMessage (request.comments)
Each fix:
1. Calls `uploadMarkup(client, class, id, 'message', body, 'markup')` to
convert HTML to prosemirror JSON and upload the blob, getting a
MarkupBlobRef back.
2. Stores the ref in `message` instead of the raw string.
3. For create paths, generates the id up-front so the markup blob's
collabId (`${class}:${id}:message`) matches the ChatMessage doc's
id. For update paths, reuses the existing id so updates land on the
same blob slot.
4. Defers the actual upload until after the dry-run guard on the channel
message paths (`comment add`/`activity reply add`/`approval comment`
don't have a dry-run code path today — pre-existing gap). Same pattern
as the HULY-8 `updateIssue` fix; dry-run output now includes a
`wouldUploadMarkup` note so users can see what would happen.
Also drops the `as any` cast on the comment.ts writes — the MarkupBlobRef
field is a plain string, which the SDK's WithMarkup<T> type allows.
Verified on the test server (`localhost:7180`):
* `huly comment add TSK-3 --body "<h1>x</h1><p>y</p>"` → message field
is `<id>-message-<timestamp>` (MarkupBlobRef). Same with --body-file.
* `huly comment update <ref> --body ...` → message ref updates with a
new timestamp.
* `huly activity reply add" --body ...` → message is MarkupBlobRef.
* `huly channel message send --body ... --dry-run` → prints
`message: ""` + wouldUploadMarkup note, no MinIO blob created.
* Cross-checked via `huly issue update --body" (same uploadMarkup
pipeline) renders as rich text in `huly issue get --markdown` —
same infrastructure now serves comments.
Two pre-existing test-env issues surfaced but are NOT caused by this fix:
* Channel message `addCollection` returns an id but the platform doesn't
persist the doc in this test workspace (verified by reverting to the
pre-fix binary and seeing the same behavior). Looks like a
channel-membership / server-config issue in the test stack, not in
scope here.
* `comment add` / `activity reply add` / `approval comment` don't
honor `--dry-run` (pre-existing). Out of scope for HULY-20.
Test artifacts cleaned; compose stack brought down.
|
Warning Review limit reached
Next review available in: 15 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The original attempt (PR #44, reverted in #45) called uploadMarkup and stored a MarkupBlobRef string in `message`. That rendered as literal text in the web UI because the front-end's MessageViewer component does: $: node = markupToJSON(message) with no MarkupBlobRef resolution step. ChatMessage.message is typed `TypeMarkup()` — an inline prosemirror-JSON string (`Markup = string` in @hcengineering/core), not a MarkupBlobRef. Why the same pattern appeared to work for Issue.description: Issue.description is also `TypeMarkup()`, but the web UI renders it through `<CollaborativeTextEditor>`, which DOES resolve MarkupBlobRef via the collaborator service. So `huly issue update --body` accidentally worked. The chat/dm/thread/activity paths render through plain `MessageViewer` and broke. Fix: * Convert HTML → prosemirror JSON locally via the existing `htmlToMarkup()` helper. * Store the resulting prosemirror-JSON string inline in `message` — no blob upload, no MarkupBlobRef. * Same pattern applied to all 10 chat-message write paths: comment add/update, channel message send/update, thread reply add/update, dm message send, activity reply add/update, approval comment. Removed from the previous PR (no longer relevant): * `uploadMarkup` and `generateId` imports — not used anymore. * Deferred-upload pattern for dry-run on channel paths — dry-run preview now shows the converted prosemirror JSON (which is what would be stored). No external IO involved in conversion, so no point deferring. * `wouldUploadMarkup` notes in dry-run output — n/a, no upload. Verified on https://huly.aaravlabs.com (real server): * Posted via the local build: `node dist/index.js comment add --issue HULY-20 --body-file /tmp/kilo/huly20-fix2.html`. * message field stores inline prosemirror JSON starting with `{"type":"doc","content":[{"type":"heading",...}]}`. * Web UI's MessageViewer can render this directly without any collaborator lookup.
The original attempt (PR #44, reverted in #45) called uploadMarkup and stored a MarkupBlobRef string in `message`. That rendered as literal text in the web UI because the front-end's MessageViewer component does: $: node = markupToJSON(message) with no MarkupBlobRef resolution step. ChatMessage.message is typed `TypeMarkup()` — an inline prosemirror-JSON string (`Markup = string` in @hcengineering/core), not a MarkupBlobRef. Why the same pattern appeared to work for Issue.description: Issue.description is also `TypeMarkup()`, but the web UI renders it through `<CollaborativeTextEditor>`, which DOES resolve MarkupBlobRef via the collaborator service. So `huly issue update --body` accidentally worked. The chat/dm/thread/activity paths render through plain `MessageViewer` and broke. Fix: * Convert HTML → prosemirror JSON locally via the existing `htmlToMarkup()` helper. * Store the resulting prosemirror-JSON string inline in `message` — no blob upload, no MarkupBlobRef. * Same pattern applied to all 10 chat-message write paths: comment add/update, channel message send/update, thread reply add/update, dm message send, activity reply add/update, approval comment. Removed from the previous PR (no longer relevant): * `uploadMarkup` and `generateId` imports — not used anymore. * Deferred-upload pattern for dry-run on channel paths — dry-run preview now shows the converted prosemirror JSON (which is what would be stored). No external IO involved in conversion, so no point deferring. * `wouldUploadMarkup` notes in dry-run output — n/a, no upload. Verified on https://huly.aaravlabs.com (real server): * Posted via the local build: `node dist/index.js comment add --issue HULY-20 --body-file /tmp/kilo/huly20-fix2.html`. * message field stores inline prosemirror JSON starting with `{"type":"doc","content":[{"type":"heading",...}]}`. * Web UI's MessageViewer can render this directly without any collaborator lookup.
) The original attempt (PR #44, reverted in #45) called uploadMarkup and stored a MarkupBlobRef string in `message`. That rendered as literal text in the web UI because the front-end's MessageViewer component does: $: node = markupToJSON(message) with no MarkupBlobRef resolution step. ChatMessage.message is typed `TypeMarkup()` — an inline prosemirror-JSON string (`Markup = string` in @hcengineering/core), not a MarkupBlobRef. Why the same pattern appeared to work for Issue.description: Issue.description is also `TypeMarkup()`, but the web UI renders it through `<CollaborativeTextEditor>`, which DOES resolve MarkupBlobRef via the collaborator service. So `huly issue update --body` accidentally worked. The chat/dm/thread/activity paths render through plain `MessageViewer` and broke. Fix: * Convert HTML → prosemirror JSON locally via the existing `htmlToMarkup()` helper. * Store the resulting prosemirror-JSON string inline in `message` — no blob upload, no MarkupBlobRef. * Same pattern applied to all 10 chat-message write paths: comment add/update, channel message send/update, thread reply add/update, dm message send, activity reply add/update, approval comment. Removed from the previous PR (no longer relevant): * `uploadMarkup` and `generateId` imports — not used anymore. * Deferred-upload pattern for dry-run on channel paths — dry-run preview now shows the converted prosemirror JSON (which is what would be stored). No external IO involved in conversion, so no point deferring. * `wouldUploadMarkup` notes in dry-run output — n/a, no upload. Verified on https://huly.aaravlabs.com (real server): * Posted via the local build: `node dist/index.js comment add --issue HULY-20 --body-file /tmp/kilo/huly20-fix2.html`. * message field stores inline prosemirror JSON starting with `{"type":"doc","content":[{"type":"heading",...}]}`. * Web UI's MessageViewer can render this directly without any collaborator lookup.
Summary
Closes HULY-20. The HULY-20 ticket was filed for
huly comment addspecifically, but the audit in the HULY-8 PR description flagged the samemessage: <raw HTML>bug class across 10 write paths. Fixed all of them.Root cause: chat-shaped messages (
ChatMessage.message,ActivityMessage.message,ThreadMessage.message) are typed asMarkupBlobRefon the Huly platform — the SDK expects them to be uploaded prosemirror-JSON blobs in MinIO, referenced by an opaque id string. The CLI was shoving the raw HTML body string into the field. The platform's fetchMarkup pipeline then serves that string as plain text, so headings/paragraphs/tables render as literal<h1>/<p>/<table>characters.issue.descriptiondoes this correctly:huly issue createcallsuploadMarkup(client, CLASS.Issue, id, 'description', body, 'markup')and stores the returned ref. This PR applies the same pattern to every chat-message write path.Sites fixed:
huly comment addchunter:class:ChatMessageissue.commentshuly comment updatechunter:class:ChatMessagehuly activity reply addactivity:class:ActivityMessageactivity.replieshuly activity reply updateactivity:class:ActivityMessagehuly channel message sendchunter:class:ChatMessagechannel.messageshuly channel message updatechunter:class:ChatMessagehuly channel thread add <ref>chunter:class:ThreadMessagechatMessage.replieshuly channel thread update <ref>chunter:class:ThreadMessagehuly dm message sendchunter:class:ChatMessagedm.messageshuly approval commentchunter:class:ChatMessagerequest.commentsFix pattern (mirrors
huly issue create):For the channel-message paths, the upload is deferred until after the dry-run guard (same pattern as the HULY-8
updateIssuefix) —dry-runoutput now includes awouldUploadMarkupnote so users can see what would happen without the MinIO write actually firing.comment add,activity reply add, andapproval commentdon't have a--dry-runcode path today (pre-existing gap), so they eagerly upload. Out of scope for HULY-20.Verification
Tested end-to-end on
http://localhost:7180(test/aaravlabs.com/test, thehuly_v7_testcompose stack from~/apps/huly-selfhost-fork):The last block is the cross-check:
issue update --bodyuses the sameuploadMarkuphelper that this PR routes the chat-message paths through, and it renders as rich text. Same infrastructure now serves the comment/activity/channel/approval paths.CI:
pnpm typecheck✅,pnpm test43/43 ✅,pnpm oxlint✅,pnpm format:check✅.Pre-existing issues NOT in scope
Two test-env issues surfaced during verification but are not caused by this fix:
huly channel message sendreturns an id but the platform doesn't persist the doc in the test workspace (verified by reverting to the pre-fix binary — same behavior). Looks like a channel-membership / server-config quirk in this stack; worth filing separately.comment add/activity reply add/approval commentdon't honor--dry-run. Pre-existing; the audit didn't flag these as create-vs-update asymmetries.Both are out of scope for HULY-20. Happy to file follow-up tickets if useful.