Skip to content

fix(chat): grounding and web chat reply fixes (#470, #471) - #472

Merged
equationalapplications merged 13 commits into
mainfrom
staging
Jun 25, 2026
Merged

equationalapplications merged 13 commits into
mainfrom
staging

Conversation

@equationalapplications

Copy link
Copy Markdown
Owner

Description

Promotes staging to main with grounding metadata and web chat fixes from #470 and #471.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test update
  • Build/CI update

Related Issue

Fixes #

Changes Made

Native grounding (#470)

  • Wrap native GroundingHtml WebView in a clipped container with opacity: 0.99 to prevent overlap with sibling list rows
  • Auto-measure widget height via injected JS so suggestion chips are not squashed into a fixed box
  • Wrap Google's HTML snippet in a minimal document shell (viewport meta) without modifying the snippet
  • Disable removeClippedSubviews on GiftedChat's message list to avoid FlatList clipping WebViews incorrectly
  • Add overflow: 'hidden' on the grounding container in ChatView
  • Strip executable markup (scripts, inline event handlers) before native WebView render

Web chat and grounding (#471)

  • Omit undefined optional fields in generateChatReply payload so Firebase web does not encode them as null
  • Treat null unsyncedHistory as absent in generateReply parsing
  • Retry once when Gemini returns an empty text response; skip retries for deterministic finish reasons
  • Render grounding Search Suggestions in shadow DOM on web with probe-width measurement and outer horizontal scrollport
  • Let widget height follow content naturally to remove empty space below the row
  • Constrain grounded chat bubbles on web (maxWidth: 80%) and disable bubble press so scroll gestures are not swallowed
  • Harden shadow DOM HTML sanitization with an allowlist; guard layout sync on unmount

Tests

  • groundingShadowContent.test.ts, sanitizeGroundingHtml.test.ts
  • chatReplyService.test.ts, generateReply.test.ts

Testing

Platforms Tested

  • iOS
  • Android
  • Web

Test Steps

Native (#470)

  1. Trigger a grounded chat response on iOS or Android
  2. Confirm Search Suggestions appear below message text inside the bubble
  3. Confirm chips are readable and do not overlap the message above
  4. Tap a suggestion chip and confirm it opens in the system browser

Web (#471)

  1. Open chat on web and send a message that triggers Google Search grounding
  2. Confirm chat replies succeed (no unsyncedHistory 400)
  3. Confirm Search Suggestions row scrolls horizontally when chips overflow
  4. Confirm no extra space below the dark widget bar
  5. Run npm run test (grounding and generateReply coverage)

Screenshots

Before

Native: grounding metadata overlapped adjacent bubbles and was vertically compressed. Web: wide chips clipped with no horizontal scroll; web chat intermittently failing with 400s.

After

Native: widget contained within bubble with readable chips. Web: horizontal scroll works, widget height fits content, chat replies succeed.

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Notes

Made with Cursor

equationalapplications and others added 7 commits June 25, 2026 15:40
Grounding metadata overlapped adjacent chat bubbles on native because
WebViews in GiftedChat's inverted list paint outside their bounds. Clip
the widget, auto-size to content, and disable FlatList subview clipping.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sanitize script tags and inline event handlers before enabling JS for
height measurement, align web iframe sanitization, and type list props
with FlatListProps instead of an assertion.

Co-authored-by: Cursor <cursoragent@cursor.com>
fix(grounding): contain search suggestions WebView on iOS and Android
Co-authored-by: Cursor <cursoragent@cursor.com>
Firebase web was sending null for omitted optional generateReply fields,
breaking web chat. Grounding search suggestions now use shadow DOM with
probe-width horizontal scrolling and natural height on web.

Co-authored-by: Cursor <cursoragent@cursor.com>
Harden shadow DOM HTML sanitization with an allowlist, guard layout sync
on unmount, preserve MessageText left/right styling, and skip empty-response
retries for deterministic Gemini finish reasons.

Co-authored-by: Cursor <cursoragent@cursor.com>
fix(web): restore chat replies and grounding widget layout
Copilot AI review requested due to automatic review settings June 25, 2026 22:23
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@equationalapplications, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 28 minutes and 14 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

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

🚦 How do rate 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 see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fbe57bae-1656-43f8-a716-92015d390178

📥 Commits

Reviewing files that changed from the base of the PR and between ebc2b88 and bc8bc90.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • __tests__/chatReplyService.test.ts
  • functions/src/generateReply.test.ts
  • functions/src/generateReply.ts
  • src/components/ChatView.tsx
  • src/components/GroundingHtml.tsx
  • src/components/GroundingHtml.web.tsx
  • src/services/chatReplyService.ts
  • src/utils/__tests__/groundingShadowContent.test.ts
  • src/utils/__tests__/sanitizeGroundingHtml.test.ts
  • src/utils/groundingShadowContent.ts
  • src/utils/sanitizeGroundingHtml.ts

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

Comment thread src/utils/sanitizeGroundingHtml.ts Fixed
Comment thread src/utils/sanitizeGroundingHtml.ts Fixed
Comment thread src/utils/sanitizeGroundingHtml.ts Fixed
Comment thread src/utils/sanitizeGroundingHtml.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Promotes fixes from staging to main to stabilize grounding (Google Search Suggestions widget) rendering across native + web, harden HTML sanitization, and address web chat 400s / empty-model-response handling in generateReply.

Changes:

  • Added native + web grounding HTML sanitizers, including a shadow-DOM render path on web with measurement utilities for horizontal scrolling and natural height.
  • Updated chat UI rendering (bubble constraints, message wrapping, grounding container styles) and native WebView grounding sizing/containment behavior.
  • Hardened callable payload construction and backend parsing (unsyncedHistory: null), plus introduced a one-time retry for empty Gemini responses with added tests.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils/sanitizeGroundingHtml.ts Adds native-focused regex sanitization (strip scripts/inline handlers + unsafe links).
src/utils/groundingShadowContent.ts Implements web shadow-DOM sanitizer, mounting, and layout/scroll measurement helpers.
src/utils/tests/sanitizeGroundingHtml.test.ts Unit tests for native sanitization helpers.
src/utils/tests/groundingShadowContent.test.ts Unit tests for shadow DOM sanitization + layout/scroll helpers.
src/services/chatReplyService.ts Omits undefined optional callable fields to avoid Firebase web encoding them as null.
src/components/GroundingHtml.web.tsx Switches web grounding rendering to shadow DOM + horizontal scrollport with measurement.
src/components/GroundingHtml.tsx Updates native grounding rendering to sanitize + auto-measure WebView height.
src/components/ChatView.tsx Adjusts bubble/layout constraints for web grounding and disables list clipping to prevent native WebView overdraw.
package-lock.json Lockfile metadata change (fsevents dev flag removal).
functions/src/generateReply.ts Adds empty-response retry logic + treats unsyncedHistory: null as absent.
functions/src/generateReply.test.ts Adds coverage for unsyncedHistory: null behavior.
tests/chatReplyService.test.ts Adds coverage ensuring undefined optional callable fields are omitted.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread functions/src/generateReply.ts
Comment thread src/utils/groundingShadowContent.ts Outdated
Comment thread src/utils/groundingShadowContent.ts
Comment thread src/components/ChatView.tsx Outdated
Use iterative replaceUntilStable for script and event-handler stripping,
and allow whitespace before script closing tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: 2cc1bdc9

Review resolution

  • Bad HTML filtering regexp (</script >): Fixed — SCRIPT_TAG_RE now uses <\/script\s*> for the closing tag (file: src/utils/sanitizeGroundingHtml.ts)
  • Incomplete multi-character sanitization (<script): Fixed — script tag and self-closing script patterns run through replaceUntilStable() so nested/obfuscated <scr<script>ipt> payloads are removed iteratively (file: src/utils/sanitizeGroundingHtml.ts)
  • Incomplete multi-character sanitization (on event handlers): Fixed — inline event-handler stripping also uses replaceUntilStable() to avoid single-pass bypasses (file: src/utils/sanitizeGroundingHtml.ts)

Verification

  • typecheck — pass (npm run typecheck)
  • lint — pass (npm run lint)
  • tests — pass (npm run test -- --testPathPattern=sanitizeGroundingHtml; full suite 723 passed)

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 25, 2026
Comment thread src/utils/sanitizeGroundingHtml.ts Fixed
Skip retry when empty responses have non-retryable finish reasons,
remove script/style entirely in shadow sanitizer, fix svg viewBox
allowlist casing, and limit removeClippedSubviews to native only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings June 25, 2026 22:32
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: d0c0be43

Review resolution

  • generateReply retry on non-retryable finish reason: Fixed — break after the error log so the loop does not make a second generateContent call when shouldRetry is false (file: functions/src/generateReply.ts)
  • SVG viewBox stripped: Fixed — allowlist entry lowercased to viewbox to match attribute comparison (file: src/utils/groundingShadowContent.ts)
  • <script>/<style> unwrapped into body: Fixed — disallowed script and style tags are removed entirely via GROUNDING_REMOVE_ENTIRELY_TAGS (file: src/utils/groundingShadowContent.ts)
  • removeClippedSubviews={false} on web: Fixed — prop applied only on native (Platform.OS !== 'web'); web keeps GiftedChat default clipping (file: src/components/ChatView.tsx)

Verification

  • typecheck — pass (npm run typecheck)
  • lint — pass (npm run lint)
  • tests — pass (npm run test; functions npm test — 300 passed; app suite 724 passed)

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 25, 2026
Allow arbitrary non-> characters after </script so closers like
</script\t\n bar> are stripped per CodeQL bad-tag-filter.

Co-authored-by: Cursor <cursoragent@cursor.com>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: d8713716

Review resolution

  • Bad HTML filtering regexp (</script\t\n bar>): Fixed — SCRIPT_TAG_RE now uses <\/script[^>]*> (and matching negative lookahead) so malformed closers with arbitrary junk before > are stripped (file: src/utils/sanitizeGroundingHtml.ts)

Verification

  • typecheck — pass (npm run typecheck)
  • tests — pass (npm run test -- --testPathPattern=sanitizeGroundingHtml; full suite 725 passed)

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.

Comment thread src/utils/groundingShadowContent.ts Outdated
Comment thread src/components/GroundingHtml.web.tsx Outdated
Comment thread src/utils/sanitizeGroundingHtml.ts
Copilot AI review requested due to automatic review settings June 25, 2026 22:35
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 25, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 25, 2026
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Kurt VanDusen <info@equationalapplications.com>
@equationalapplications
equationalapplications merged commit b9ffb85 into main Jun 25, 2026
6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +47 to +50
/** Defense-in-depth sanitization before rendering grounding HTML on native WebViews. */
export function sanitizeGroundingHtmlForNative(html: string): string {
return sanitizeGroundingHtmlLinksRegex(stripExecutableGroundingMarkup(html))
}
Comment on lines +74 to +83
function isAllowedGroundingAttribute(tagName: string, attributeName: string): boolean {
const lowerName = attributeName.toLowerCase()
if (lowerName.startsWith('on') || lowerName.startsWith('data-') || lowerName.startsWith('aria-')) {
return lowerName.startsWith('aria-') || lowerName.startsWith('data-')
}
if (GROUNDING_GLOBAL_ALLOWED_ATTRS.has(lowerName)) {
return true
}
return GROUNDING_TAG_ALLOWED_ATTRS[tagName]?.has(lowerName) ?? false
}
Copilot AI review requested due to automatic review settings June 25, 2026 22:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +102 to +117
useEffect(() => {
const scrollEl = scrollRef.current
if (!scrollEl) {
return
}

const onWheel = (event: WheelEvent) => {
if (applyHorizontalWheelToScrollport(event, scrollEl)) {
event.preventDefault()
event.stopPropagation()
}
}

scrollEl.addEventListener('wheel', onWheel, { capture: true, passive: false })
return () => scrollEl.removeEventListener('wheel', onWheel, { capture: true })
}, [contentWidth])
Comment on lines +74 to +83
function isAllowedGroundingAttribute(tagName: string, attributeName: string): boolean {
const lowerName = attributeName.toLowerCase()
if (lowerName.startsWith('on') || lowerName.startsWith('data-') || lowerName.startsWith('aria-')) {
return lowerName.startsWith('aria-') || lowerName.startsWith('data-')
}
if (GROUNDING_GLOBAL_ALLOWED_ATTRS.has(lowerName)) {
return true
}
return GROUNDING_TAG_ALLOWED_ATTRS[tagName]?.has(lowerName) ?? false
}
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.

3 participants