feat(mobile): render inline and block HTML in markdown messages - #5896
Open
iscekic wants to merge 24 commits into
Open
feat(mobile): render inline and block HTML in markdown messages#5896iscekic wants to merge 24 commits into
iscekic wants to merge 24 commits into
Conversation
… deliver-the-work-described-b-c5a9/ux1)
…or (kwf deliver-the-work-described-b-c5a9/ux2)
…ex (kwf deliver-the-work-described-b-c5a9/ux3)
…et (kwf deliver-the-work-described-b-c5a9/ux4)
…k-described-b-c5a9/gr1)
…cribed-b-c5a9/uxs1)
…r-the-work-described-b-c5a9/i18d1)
iscekic
marked this pull request as draft
September 5, 2026 04:13
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (20 files)
Previous Review Summary (commit b1691af)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit b1691af)Status: 3 Issues Found | Recommendation: Address before merge Executive SummaryHTML images without dimensions stay locked to 4:3, Overview
Fix these issues in Kilo Cloud Issue Details (click to expand)WARNING
Files Reviewed (12 files)
Reviewed by grok-4.6 · Input: 120.6K · Output: 22.4K · Cached: 570.5K Review guidance: REVIEW.md from base branch |
…cribed-b-c5a9/uxs2)
…cribed-b-c5a9/uxs3)
iscekic
force-pushed
the
kwf/deliver-the-work-described-b-c5a9
branch
from
September 6, 2026 08:20
f616a34 to
c154ec3
Compare
iscekic
marked this pull request as ready for review
September 6, 2026 08:31
…-c5a9' into kwf/ci-is-red-on-pr-https-github-13e0
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.
Changelog for users
script,style,iframe,object/embed, media, form controls, SVG, canvas, and metadata tags never execute and never load remote resources.HTTPS images onlychip without any network request, and unsupported sources render their alt text.Changelog for maintainers
marked@18.0.6(lexer) andreact-native-render-html@6.3.4(HTML engine) toapps/mobile.ignoredDomTagscover void tags.MarkdownTextsegments content with themarkedlexer and routes styled HTML to a newMarkdownHtmlrenderer; content without HTML continues through the existingreact-native-markedpath unchanged.commoni18n namespace across tabs, code reviewer, agent chat, and KiloClaw screens and components; the feature diff itself touches no locale resource files (the merge withorigin/mainupdates the catalogs — see the merge bullet below).mergeSessionTranscriptgains an optionaldeliveryStatesmap so a failed-delivery message whose text sanitizes to empty stays visible.titleNumberOfLines={1}to the existingreserveTitleSpaceprop for two-line titles.markdown-image-confirmexposes a subscription so multiple mounted image slots react to one Load confirmation viauseSyncExternalStore.origin/main(9e17757) — merge commit19a0ce2a9on top of the PR head356f01e65(no rebase, no squash; nothing from either side dropped). Conflicting files and how each was resolved (per-file detail under "What changed (merge of origin/main)"):apps/mobile/src/i18n/locales/*.json(87 catalogs) — union of the keys both sides hold: main's catalogs are a strict key superset of this branch's, andagentChat.filePart.imageUnavailableWithAlt(the one key main has and the PR had dropped) is restored in every catalog with main's translated{{alt}}template. Shared keys keep main's repo-wide retranslation — the only value-layer change either side made against the merge base, and the one main's own tests pin (e.g. session-row's ES "EN DIRECTO"); this branch's catalog work is key-set restructuring and is fully preserved.node tools/i18n/check-catalogs.mjspasses (87 languages, every catalog matchesen.json).pnpm-lock.yaml— union of both sides: this branch'smarked+react-native-render-htmladditions plus main'sexpo-widgetspatch rework (canonical patch hash1e56973d) and dependency bumps;pnpm install --frozen-lockfilepasses on the merged tree.apps/mobile/src/components/agents/markdown-image.tsx(+markdown-image.test.ts) — this branch's fixed-height failure/retry slot kept; the alt case renders through main's translatedagentChat.filePart.imageUnavailableWithAlttemplate instead of the branch's untranslatedcommon.imageUnavailable+\n${alt}split.apps/mobile/src/components/organization/credit-activity-screen.tsx— main'scommon.loadMorere-points kept; the next-page failure message resolved to this branch'scommon.couldnTLoadMore(this PR's copy-dedupe); main'sorganization.invoices.loadMoreFailedstays in the catalog and used byinvoices-screen.tsx.apps/mobile/src/components/pr-review/pr-review-overview.tsx— this branch'sprReview.connectionExpiredreconnectEmptyStatetitle kept (consistent with sibling call sites), paired with the sharedprReview.reconnectNotice.messagedescription and main'scommon.accessDeniedre-point;prReview.reconnectNotice.titleremains used bypr-review-reconnect-notice.tsx.apps/mobile/src/i18n/label-reference.test.ts—MESSAGE_KEYSconverged to a singleauthErrors.differentOauthentry: main's replacement ofemailAlreadyUsedsurvives, the duplicatedifferentOauthline is de-duplicated.tools/i18n/check-catalogs.mjs— union: this branch's allowlist re-points/removals (common.github/common.gitlab, droppedplatformGithub/platformGitlabduplicates) plus main'sTWO_SENSE_COPYmap and its newagentChat.repoPicker.platformBitbucketallowlist entry.Changes by area
markdown-html.tsx,markdown-html-sanitization.ts,markdown-palette.ts): newMarkdownHtmlcomponent backed byreact-native-render-html, a denylist sanitizer, and shared heading/tag styles so HTML text matches Markdown styling.markdown-text.tsx):splitMarkdownHtmlsplits content into Markdown/HTML segments and routes styled HTML (links, headings, strong, blockquotes, list items) to the HTML engine while keeping fenced code and tables on the existing path.markdown-image.tsx,markdown-image-confirm.ts): fixed-aspect-ratio reserved slots, subscription-based Load confirmation, explicit Load/Retry labels, and linked-image accessibility.message-visibility.ts,message-bubble.tsx,session-transcript.ts): suppress bubbles whose text sanitizes to empty, keep failed-delivery messages, and wrap each bubble and delivery row in one measurable list item.agent-chat/[session-id].tsx,session-detail-content.tsx): adoptreserveTitleSpacefor two-line titles.common.*across roughly 60 files underapps/mobile/src.markdown-html.mounted.test.tsx(runs the real HTML engine), newmarkdown-text.test.tsx(routing and sanitization), plus expandedmarkdown-image.test.ts,message-visibility.test.ts,message-bubble.test.ts, andsession-transcript.test.ts.package.json): addmarkedandreact-native-render-html.E2E proof
Live iOS and Android dev-build verification covered the following scenarios:
script,iframe,style, andformHTML shows no unsafe text, raw tags, remote load, or CTA.Load /.+/reserve one slot and one skeleton without layout movement; the loaded image opens in the viewer.HTTPS images onlyfor blocked sources with no load CTA and no network request.Image unavailable, retry loading; retrying keeps the same slot height.Before/Aftertext stays visible without movement during load, failure, and retry.<strong>inline +<div><h3>block + list response shows all formatted text and no raw tags.Load example.com, the skeleton, and the image in one reserved slot without moving adjacent messages.script/iframe/stylesheet/object/media/form/SVG shows only safe text, no CTA, and no network request.Recording of the verified flow (waits trimmed)
p2-html-link-ios.mp4.trim.mp4
Recording of the verified flow (waits trimmed)
p2-html-link.mp4.trim.mp4
Recording of the verified flow (waits trimmed)
p1-ios-trimmed.mp4.trim.mp4
Recording of the verified flow (waits trimmed)
p8-image-schemes-ios.mp4.trim.mp4
Recording of the verified flow (waits trimmed)
p5-ios-empty.mp4.trim.mp4
Recording of the verified flow (waits trimmed)
p9-ios-schemes.mp4.trim.mp4
Notes
common.*keys resolve to the intended copy in every locale.Pre-existing UX defects observed (not changed here)
Not verified live (each line ends with the reason)
What changed (merge of origin/main)
Merged
origin/main(9e17757) into the PR head 356f01e as merge commit 19a0ce2a9 (parents 356f01e + 9e17757), so CI can run. The single merge commit carries the union-of-keys catalogs (this branch's key-set structure with main's retranslation) and the restoredagentChat.filePart.imageUnavailableWithAltcatalog key with its translated template at its call site. No rebase, no squash; nothing from either side was dropped. Both merge-base gates hold:git merge-base --is-ancestorconfirms9e1775745and356f01e65are each ancestors of the branch tip.Conflicting files and how each was resolved:
apps/mobile/src/components/agents/markdown-image.tsx— this branch'sFixedImageSlotfailure/retry block survives (fixed-height slot, Retry affordance,common.imageUnavailablewhen there is no alt text), and the alt case now uses main's translated sentenceagentChat.filePart.imageUnavailableWithAlt("Image unavailable {{alt}}" and its per-language renditions) instead of this branch's untranslated`${t('common.imageUnavailable')}\n${alt}`split — the union of both intents: the alt text stays visible inside the chip, and it renders through a template translators actually localize (this was main's fix(mobile): translate every catalog against its UI context #5880 intent; this branch had deleted the key as unused, which violated the union-of-keys rule). Main's other change to this file since the merge base — re-pointing the no-alt label tocommon.imageUnavailable— is inside the kept block. (The base's pre-retrysetMeasuredAspectRatio(undefined)reset is deliberately not carried over: this branch'smarkdown-image.test.tspins that the measured ratio is kept through retry, andonLoadre-measures after the retry loads.)apps/mobile/src/components/organization/credit-activity-screen.tsx— kept main's re-points oforganization.creditActivity.loadMore→common.loadMore(both lines), and resolved the conflict line (next-page failure message) to this branch'scommon.couldnTLoadMore, the copy-dedupe this PR delivers. Main's renamedorganization.invoices.loadMoreFailedstays in the catalog and stays used byinvoices-screen.tsx, so no main intent is lost.apps/mobile/src/components/pr-review/pr-review-overview.tsx— kept this branch'sprReview.connectionExpiredas the reconnectEmptyStatetitle (consistent with this branch's sibling call sites inmutation-error-display.ts,pr-merge-sheet.tsx,reply-input.tsx), paired with theprReview.reconnectNotice.messagedescription both sides share, plus main'sprReview.accessDenied→common.accessDeniedre-point.prReview.reconnectNotice.titleremains used bypr-review-reconnect-notice.tsx, so both catalog entries stay live.apps/mobile/src/i18n/label-reference.test.ts— convergedMESSAGE_KEYSto a single'authErrors.differentOauth'entry: main's replacement ofemailAlreadyUsedsurvives, the duplicatedifferentOauthline main's side carried is de-duplicated, and this branch'semailAlreadyUsedremoval stands.tools/i18n/check-catalogs.mjs— union: this branch's allowlist re-points and removals (common.github/common.gitlab, droppedplatformGithub/platformGitlabduplicates) plus main'sTWO_SENSE_COPYmap and its new'agentChat.repoPicker.platformBitbucket'allowlist entry.apps/mobile/src/i18n/locales/*.json(87 files) — every key on this branch exists on main (main's catalogs are a strict key superset; the only key main had that the branch dropped isagentChat.filePart.imageUnavailableWithAlt), so the union of keys is main's catalog set. Values: against the merge base this branch changed no translated value (its catalog work is key renames, drops and restructuring, which the union preserves), while main re-translated roughly 1,100–1,500 keys per locale and pins those renditions in its own tests (e.g.session-row.mounted.test.tsxexpects ES "EN DIRECTO"), so shared keys keep main's retranslation and every merged test pins a value the catalogs actually carry. The merged catalogs restoreagentChat.filePart.imageUnavailableWithAltin all 87 catalogs with main's translations (reversing the earlier dead-key removal, which the union-of-keys rule overrides) and keep the failure chip pointed at it.node tools/i18n/check-catalogs.mjsre-run: 87 languages, every catalog matches en.json.pnpm-lock.yaml— main reworkedpatches/expo-widgets@57.0.11.patch(patch hash 4d6fc209 → 1e56973d; main's patch file is a superset and is what the merge carries) while this branch addedmarkedandreact-native-render-html. Resolved by regenerating withpnpm install --lockfile-onlyon the merged manifests: canonical patch hash 1e56973d (matches the merged patch file's sha256) and this branch's dependency additions.pnpm install --frozen-lockfilepasses on the merged tree ("Lockfile is up to date").Post-merge verification on the final tip (19a0ce2a9): an independent union checker passes (87 locale files keep the union of keys from 356f01e (PR) and 9e17757 (main), with every shared key carrying main's rendition and the grafted
agentChat.filePart.imageUnavailableWithAltcarrying main's per-language template);node tools/i18n/check-catalogs.mjspasses (87 languages, every catalog matches en.json);oxlint --config apps/mobile/.oxlintrc.i18n.json apps/mobile/src packages/app-shared/srcclean (0 warnings, 0 errors on 1081 files);tsgo --noEmit(mobile typecheck) passes;vitest runover the merged-file mobile suites (markdown-image.test.ts,session-row.mounted.test.tsx,pr-review-checks-section.mounted.test.tsx,ios-sink.native.test.ts,ios-sink.test.ts,activity-kit-prompt.test.ts,label-reference.test.ts) passes 395 tests;oxlint(0 warnings, 0 errors) andoxfmt --list-different(clean) on the five hand-resolved source files;pnpm install --frozen-lockfilepasses on the merged tree ("Lockfile is up to date").Why
The branch diverged from
origin/main, so the PR could not merge and CI could not run on it. The merge preserves this PR's HTML-markdown feature work together with main's translation refresh, expo-widgets patch rework, Bitbucket additions, and fixes in one history, with every conflict resolved so both sides' intent survives. The merge commit 19a0ce2a9 is built on the PR's current head (356f01e) with origin/main (9e17757) as the second parent, and the lockfile was resolved as the union of both sides and validated withpnpm install --frozen-lockfile, so CI's install gate passes. The merge commit carries the completed conflict resolutions (union-of-keys locale catalogs carrying main's retranslation, with the restoredagentChat.filePart.imageUnavailableWithAlttranslations at their call site), keeping both sides' intent in every line where the two sides genuinely conflicted.