Skip to content

feat(chat): unify markdown rendering + related-surfaces footer + new-tool chips - #139

Merged
thomasluizon merged 2 commits into
mainfrom
feature/wave2-chat-rendering
Jun 6, 2026
Merged

feat(chat): unify markdown rendering + related-surfaces footer + new-tool chips#139
thomasluizon merged 2 commits into
mainfrom
feature/wave2-chat-rendering

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

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 the marked engine and renders all markdown — chat messages AND habit/goal descriptions — through one renderer per platform.

  • Web: new components/ui/markdown.tsx reusing the exact marked+DOMPurify allowlist from description-viewer. Used in chat + descriptions. format-chat-message.ts (+ test) deleted.
  • Mobile: added react-native-marked (same marked engine → parsing parity with web) behind a themed components/ui/markdown.tsx. It rejects javascript:/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)

  • Shared: optional relatedSurfaces on chatResponseSchema + chatMessageSchema; new chat/related-surfaces.ts mapping the 5 emitted surface IDs (today, gamification, notifications, subscriptions, ai-settings) → { labelKey, webRoute, mobileRoute } with a getRelatedSurfaces() filter (unknown IDs dropped).
  • Both platforms render an optional "Related" deep-link footer below AI bubbles.

3. New mutating-tool chips (#96)

  • ACTION_LABELS gains CreateTag/UpdateTag/DeleteTag/ReorderGoals/ReorderHabits (snake + Pascal forms) on both platforms; chat.action.* + chat.related.* i18n keys added to both en.json and pt-BR.json.
  • CreateTag/UpdateTag/DeleteTag marked NON_NAVIGABLE (no tag route).
  • actionResultSchema.type widened from the 18-value enum to z.string() (backend sends PascalCase types outside the enum; the chat path never .parse()s).
  • Tag/reorder turns now refetch the relevant lists (selectActionInvalidations gains a tags flag; tag ops also invalidate habit lists since rows show tag chips). delete_tag stays immediate-execute.

Markdown library note

Used react-native-marked@^8.1.0 (not the plan's tentative react-native-markdown-display). The brief locked react-native-marked for parsing parity; 8.1.0 is current and maintained, depends on marked@18 (matches web's marked@^18), and satisfies all peers (react, react-native ≥0.76, react-native-svg). It installed cleanly and did not perturb fix-hoisting.js / the overrides block — the pinned RN transitive deps and the root react-native junction all survived npm install.

Tests / validation

  • shared: npm test 848 pass (new related-surfaces.test.ts, widened-type + relatedSurfaces schema cases, updated invalidation cases); type-check clean.
  • web: full suite 1460 pass; lint + type-check clean. New ui/markdown.test.tsx; message-bubble/action-chips tests updated (footer + new labels + non-navigable tags); format-chat-message.test.ts removed.
  • mobile: full suite 394 pass; lint + type-check clean. New ui/markdown.test.tsx (asserts http(s)/mailto open, javascript:/data: refused, muted tone); message-bubble/action-chips tests 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_surfaces onto ChatResponse): thomasluizon/orbit-api#187

Closes #95
Closes #96

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jun 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Jun 6, 2026 12:49am

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud

sonarqubecloud Bot commented Jun 6, 2026

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit ed00dfe into main Jun 6, 2026
8 checks passed
@thomasluizon
thomasluizon deleted the feature/wave2-chat-rendering branch June 6, 2026 00:54
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.

UI rendering for new chat tool responses (web + mobile) UI: render describe_feature markdown responses

1 participant