Skip to content

fix: upload chat-message markup as MarkupBlobRef instead of raw HTML (HULY-20) - #44

Merged
IamCoder18 merged 1 commit into
mainfrom
fix/huly-20-markup-content-for-chat-messages
Aug 11, 2026
Merged

fix: upload chat-message markup as MarkupBlobRef instead of raw HTML (HULY-20)#44
IamCoder18 merged 1 commit into
mainfrom
fix/huly-20-markup-content-for-chat-messages

Conversation

@IamCoder18

Copy link
Copy Markdown
Owner

Summary

Closes HULY-20. The HULY-20 ticket was filed for huly comment add specifically, but the audit in the HULY-8 PR description flagged the same message: <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 as MarkupBlobRef on 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.description does this correctly: huly issue create calls uploadMarkup(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:

# CLI command Class Collection
1 huly comment add chunter:class:ChatMessage issue.comments
2 huly comment update chunter:class:ChatMessage (doc itself)
3 huly activity reply add activity:class:ActivityMessage activity.replies
4 huly activity reply update activity:class:ActivityMessage (doc itself)
5 huly channel message send chunter:class:ChatMessage channel.messages
6 huly channel message update chunter:class:ChatMessage (collection)
7 huly channel thread add <ref> chunter:class:ThreadMessage chatMessage.replies
8 huly channel thread update <ref> chunter:class:ThreadMessage (collection)
9 huly dm message send chunter:class:ChatMessage dm.messages
10 huly approval comment chunter:class:ChatMessage request.comments

Fix pattern (mirrors huly issue create):

// create paths: generate id up-front so the markup blob's collabId matches the doc's id
const newMessageId = generateId()
const messageRef = await uploadMarkup(client, CLASS, newMessageId, 'message', body, 'markup')
await client.addCollection(CLASS, ..., { message: messageRef }, newMessageId)

// update paths: reuse the existing id
const messageRef = await uploadMarkup(client, CLASS, existingId, 'message', body, 'markup')
await client.updateDoc(CLASS, ..., existingId, { message: messageRef, editedOn: ... })

For the channel-message paths, the upload is deferred until after the dry-run guard (same pattern as the HULY-8 updateIssue fix) — dry-run output now includes a wouldUploadMarkup note so users can see what would happen without the MinIO write actually firing.

comment add, activity reply add, and approval comment don't have a --dry-run code 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, the huly_v7_test compose stack from ~/apps/huly-selfhost-fork):

$ huly comment add TSK-3 --body "<h1>x</h1><p>y</p>"
$ huly comment list --issue TSK-3 --json | jq '.[0].message'
"6a7a6822be5535c8d4927090-message-1786406946311"   ← MarkupBlobRef

$ huly comment update <ref> --body "<h1>Updated</h1><p>New body</p>"
$ huly comment list --issue TSK-3 --json | jq '.[0].message'
"6a7a6822be5535c8d4927090-message-1786406948806"   ← new timestamp on same id

$ huly activity reply add <activity> --body "<h1>Reply</h1>"
$ huly activity reply list <activity> --json | jq '.[0].message'
"6a7a68d3d4c27ea5cc48296d-message-1786407123663"  ← MarkupBlobRef

$ huly channel message send <chan> --body "<h1>Dry run</h1>" --dry-run --json
{ "data": { "message": "" }, "wouldUploadMarkup": { ... } }   ← no MinIO write

$ huly issue update TSK-3 --body "<h1>x</h1><p>y</p>"
$ huly issue get TSK-3 --markdown
# x
y

The last block is the cross-check: issue update --body uses the same uploadMarkup helper 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 test 43/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:

  1. huly channel message send returns 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.
  2. comment add / activity reply add / approval comment don'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.

…(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.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@IamCoder18, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee8ae3b6-68ea-44cf-9680-7551f9db886d

📥 Commits

Reviewing files that changed from the base of the PR and between 75a7c26 and 3e14fc9.

📒 Files selected for processing (4)
  • packages/cli/src/resources/activity.ts
  • packages/cli/src/resources/approvals.ts
  • packages/cli/src/resources/channel.ts
  • packages/cli/src/resources/comment.ts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@IamCoder18
IamCoder18 merged commit 0a16ceb into main Aug 11, 2026
3 checks passed
IamCoder18 added a commit that referenced this pull request Aug 11, 2026
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.
IamCoder18 added a commit that referenced this pull request Aug 11, 2026
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.
IamCoder18 added a commit that referenced this pull request Aug 11, 2026
)

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.
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