feat(chat): unify markdown rendering + related-surfaces footer + new-tool chips - #139
Conversation
…tool chips Standardize on the `marked` engine across both platforms and render all markdown (chat messages AND habit/goal descriptions) through one renderer per platform. - Web: extract a shared `Markdown` component reusing the `marked`+`DOMPurify` pattern from description-viewer; use it in chat + descriptions; delete the custom `formatChatMessage`. - Mobile: add `react-native-marked` (same `marked` engine → parsing parity), wrap it in a themed `Markdown` component that rejects javascript:/data: link schemes and renders no raw HTML; use it in chat + descriptions; delete the hand-rolled `<Text>`-segment parser. This also fixes the live bug where the mobile description viewer showed markdown literally. - related_surfaces footer (#95): add optional `relatedSurfaces` to the shared chat schemas, a shared surface→route/label map for the 5 emitted IDs, and an optional "Related" deep-link footer in chat on both platforms. - New-tool chips (#96): add CreateTag/UpdateTag/DeleteTag/ReorderGoals/ ReorderHabits labels to ACTION_LABELS + i18n (en + pt-BR), mark the tag mutations NON_NAVIGABLE, widen actionResultSchema.type to a string, and wire tag/reorder turns to refetch the relevant lists. Closes #95 Closes #96 Paired API PR (threads related_surfaces onto ChatResponse): thomasluizon/orbit-api feature/wave2-chat-rendering Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
There was a problem hiding this comment.
Three coherent features land together with full cross-platform parity and good test coverage.
Markdown unification replaces the hand-rolled HTML segment parser on mobile and the inline marked+DOMPurify block in description-viewer on web with a single Markdown component per platform. The mobile renderer's SafeLinkRenderer correctly gates Linking.openURL to https?: and mailto: schemes, and the web renderer relies on DOMPurify with a fixed allowlist — both surfaces are XSS-clean. The two components intentionally diverge only on their API shape (content vs children) and rendering layer (DOM vs RN primitives), which is the right cross-platform adapter boundary.
Related-surfaces footer is well-contained in packages/shared/chat/related-surfaces.ts: a typed map, a getRelatedSurfaces guard that drops unknown IDs and dedupes, and i18n keys added to both locales in the same commit. The footer only renders on AI messages with a non-empty resolved list, and the deep-link routes are correctly split by platform (webRoute / mobileRoute).
New-tool chips follow the existing dual snake_case/PascalCase backward-compat pattern already present in ACTION_LABELS. Tag mutations are correctly marked NON_NAVIGABLE in both action-chips files; ReorderGoals and ReorderHabits are naturally non-navigable via the !!action.entityId guard and don't need to be added to the set. The CHAT_TAG_ACTION_TYPES set in chat-composer-core.ts only carries PascalCase entries, which is correct since these are new API actions that were always emitted in PascalCase. The tags || habits invalidation logic in selectActionInvalidations correctly propagates tag changes to the habit list cache.
There was a problem hiding this comment.
Three well-scoped features land together with full cross-platform parity, clean i18n, and solid test coverage throughout.
Markdown unification correctly replaces the hand-rolled HTML segment parser on mobile and the inline marked+DOMPurify block in description-viewer on web with a single Markdown component per platform. The mobile SafeLinkRenderer correctly gates Linking.openURL to https?: and mailto: schemes; the web renderer relies on DOMPurify with a fixed allowlist — both surfaces are XSS-clean. The deliberate prop-name divergence (content vs children) stays within the allowed platform-adapter boundary.
Related-surfaces footer is well-contained in packages/shared/chat/related-surfaces.ts: typed map, a getRelatedSurfaces guard that drops unknown IDs and dedupes, i18n keys added to both locales in the same commit, and footer only renders on AI messages with a non-empty resolved list. Deep-link routes are correctly split by platform.
New-tool chips follow the existing snake_case/PascalCase backward-compat pattern. Tag mutations are correctly marked NON_NAVIGABLE in both platforms; ReorderGoals/ReorderHabits are naturally non-navigable via the !!action.entityId guard. The CHAT_TAG_ACTION_TYPES→habits cross-invalidation logic is sound and well-tested.
No high-signal issues found.
|



What
Wave 2 PR-B. Three coordinated changes across web + mobile (+ a paired API slice).
1. One markdown renderer per platform (#95)
Today chat renders through a hand-rolled
formatChatMessage(only**bold**/*italic*). This standardizes on themarkedengine and renders all markdown — chat messages AND habit/goal descriptions — through one renderer per platform.components/ui/markdown.tsxreusing the exactmarked+DOMPurifyallowlist fromdescription-viewer. Used in chat + descriptions.format-chat-message.ts(+ test) deleted.react-native-marked(samemarkedengine → parsing parity with web) behind a themedcomponents/ui/markdown.tsx. It rejectsjavascript:/data:link schemes and renders no raw HTML (pure RN primitives → New-Architecture safe). Used in chat + descriptions; the hand-rolled<Text>-segment parser deleted. This also fixes a live bug: the mobile description viewer previously rendered<Text>{description}</Text>, so markdown showed literally.2. related_surfaces footer (#95, cross-repo)
relatedSurfacesonchatResponseSchema+chatMessageSchema; newchat/related-surfaces.tsmapping the 5 emitted surface IDs (today,gamification,notifications,subscriptions,ai-settings) →{ labelKey, webRoute, mobileRoute }with agetRelatedSurfaces()filter (unknown IDs dropped).3. New mutating-tool chips (#96)
ACTION_LABELSgains CreateTag/UpdateTag/DeleteTag/ReorderGoals/ReorderHabits (snake + Pascal forms) on both platforms;chat.action.*+chat.related.*i18n keys added to bothen.jsonandpt-BR.json.NON_NAVIGABLE(no tag route).actionResultSchema.typewidened from the 18-value enum toz.string()(backend sends PascalCase types outside the enum; the chat path never.parse()s).selectActionInvalidationsgains atagsflag; tag ops also invalidate habit lists since rows show tag chips).delete_tagstays immediate-execute.Markdown library note
Used
react-native-marked@^8.1.0(not the plan's tentativereact-native-markdown-display). The brief lockedreact-native-markedfor parsing parity; 8.1.0 is current and maintained, depends onmarked@18(matches web'smarked@^18), and satisfies all peers (react, react-native ≥0.76, react-native-svg). It installed cleanly and did not perturbfix-hoisting.js/ theoverridesblock — the pinned RN transitive deps and the rootreact-nativejunction all survivednpm install.Tests / validation
npm test848 pass (newrelated-surfaces.test.ts, widened-type +relatedSurfacesschema cases, updated invalidation cases); type-check clean.lint+type-checkclean. Newui/markdown.test.tsx;message-bubble/action-chipstests updated (footer + new labels + non-navigable tags);format-chat-message.test.tsremoved.lint+type-checkclean. Newui/markdown.test.tsx(asserts http(s)/mailto open,javascript:/data:refused, muted tone);message-bubble/action-chipstests updated.Browser/device visual verification of rendered markdown is a manual follow-up (covered by unit/RTL tests here).
Cross-repo
Paired API PR (threads
related_surfacesontoChatResponse): thomasluizon/orbit-api#187Closes #95
Closes #96
🤖 Generated with Claude Code