Skip to content

fix(web): restore chat replies and grounding widget layout - #471

Merged
equationalapplications merged 2 commits into
stagingfrom
fixes
Jun 25, 2026
Merged

equationalapplications merged 2 commits into
stagingfrom
fixes

Conversation

@equationalapplications

Copy link
Copy Markdown
Owner

Description

Fixes production web chat failures and the grounding Search Suggestions widget layout on web. Web chat was returning 400s from generateReply, and wide suggestion chips were clipped with no horizontal scroll and extra vertical space below the widget.

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

  • 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; log finishReason on failure
  • 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

Testing

Platforms Tested

  • iOS
  • Android
  • Web

Test Steps

  1. Open http://localhost:8081/chat 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 tan/white space below the dark widget bar
  5. Run npx jest src/utils/__tests__/groundingShadowContent.test.ts
  6. Run functions tests for generateReply null unsyncedHistory handling

Screenshots

Before

Wide suggestion chips clipped; no horizontal scroll; extra space below widget; web chat intermittently failing.

After

Horizontal scroll works; widget height fits content; web 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

Native grounding rendering (WebView) is unchanged. Shadow DOM follows Google's recommended embed path for renderedContent.

Made with Cursor

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>
Copilot AI review requested due to automatic review settings June 25, 2026 22:04
@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ff24c74-31ae-43d2-b67b-4eeb83a00d9b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

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

This PR fixes production web chat failures caused by undefined optional fields being encoded as null in Firebase callable payloads, and improves the web grounding “Search Suggestions” widget layout by moving rendering into shadow DOM and enabling reliable horizontal scrolling.

Changes:

  • Adjusts web client callable payload construction to omit undefined optional fields and adds a regression test.
  • Updates generateReply to treat null unsyncedHistory as absent, and retries once when Gemini returns empty text (with added logging).
  • Reworks web grounding HTML rendering to mount sanitized content into a shadow root, measure content width, and route wheel gestures to horizontal scrolling; constrains grounded bubbles on web.

Reviewed changes

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

Show a summary per file
File Description
src/utils/groundingShadowContent.ts Adds shadow-DOM mounting, sanitization, layout measurement, and wheel-to-horizontal-scroll helpers for grounding widgets.
src/utils/tests/groundingShadowContent.test.ts Adds jsdom unit tests for the new grounding shadow DOM utilities.
src/services/chatReplyService.ts Omits undefined optional fields from callable payload to avoid Firebase web encoding them as null.
src/components/GroundingHtml.web.tsx Switches grounding widget rendering from sandboxed iframe to shadow DOM with measured-width scrollport behavior.
src/components/GroundingHtml.tsx Updates documentation comment to reflect the web implementation change (shadow DOM).
src/components/ChatView.tsx Constrains web chat bubble width/overflow and disables bubble press for grounded messages to preserve scroll gestures.
functions/src/generateReply.ts Retries once on empty model text response and treats null unsyncedHistory as absent during parsing.
functions/src/generateReply.test.ts Adds test coverage for null unsyncedHistory being accepted as absent.
tests/chatReplyService.test.ts Adds test ensuring undefined optional fields are omitted from the callable payload.

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

Comment thread src/utils/groundingShadowContent.ts Outdated
Comment thread src/components/GroundingHtml.web.tsx

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
functions/src/generateReply.ts (1)

365-371: 🚀 Performance & Scalability | 🔵 Trivial

Optional: skip the retry for deterministic finish reasons.

This reissues the same request even when the empty response is already explained by a non-retryable finish reason. Consider retrying only for transient/unspecified cases; otherwise it just adds latency and model cost.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@functions/src/generateReply.ts` around lines 365 - 371, The retry in
generateReply is unconditional after an empty model response, which can resend
the same request even when the finish reason is already clearly non-retryable.
Update the retry gate in generateReply to inspect candidates.map((c) =>
c.finishReason) and only continue on transient or unspecified cases, while
skipping the retry for deterministic finish reasons such as
length/content-filter style outcomes. Keep the existing logging, but make the
retry decision depend on the finish reasons instead of attempt === 0 alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/ChatView.tsx`:
- Around line 172-186: The `ChatView` `renderMessageText` block is passing
`MessageText.textStyle` as an array, but `MessageTextProps.textStyle` expects a
left/right map so the themed colors can be preserved. Update the `MessageText`
usage to keep the existing left/right structure and apply the web wrapping
styles inside each side entry rather than replacing the prop shape.

In `@src/components/GroundingHtml.web.tsx`:
- Around line 119-120: The style handling in GroundingHtml.web.tsx is unsafe
because StyleSheet.flatten(style) can return undefined, so destructuring
flattenedStyle may throw when style is omitted. Update the flattenedStyle
assignment in the GroundingHtml component to default to an empty object before
destructuring, then keep the existing minHeight/height extraction and
containerStyle creation intact.

In `@src/utils/groundingShadowContent.ts`:
- Around line 24-48: The sanitizer in sanitizeGroundingHtmlLinks is still too
permissive for content that will be inserted into Shadow DOM, since it only
strips scripts, inline event handlers, and unsafe hrefs while leaving other
active/embed-capable elements and URL-bearing attributes intact. Tighten this
function to use a strict allowlist for the expected Google widget HTML shape,
removing or rejecting elements like iframe, object, embed, meta/http-equiv, and
any non-approved URL attributes before returning bodyHtml and headMarkup. Also
review the related headMarkup collection so only explicitly allowed head
elements are preserved.
- Around line 172-177: The wheel handler in getHorizontalWheelDelta usage should
also return false when the scrollport cannot move any further at either
horizontal edge, not just when there is no overflow. Update the scrolling logic
around scrollEl.scrollLeft so it checks the current position against the
left/right bounds before consuming the event, and only return true when
scrollLeft actually changes; otherwise let the event pass through.

---

Nitpick comments:
In `@functions/src/generateReply.ts`:
- Around line 365-371: The retry in generateReply is unconditional after an
empty model response, which can resend the same request even when the finish
reason is already clearly non-retryable. Update the retry gate in generateReply
to inspect candidates.map((c) => c.finishReason) and only continue on transient
or unspecified cases, while skipping the retry for deterministic finish reasons
such as length/content-filter style outcomes. Keep the existing logging, but
make the retry decision depend on the finish reasons instead of attempt === 0
alone.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3466b1c4-8dfd-4875-9b7b-a04e3ff1e731

📥 Commits

Reviewing files that changed from the base of the PR and between e0c239b and 6c81baf.

📒 Files selected for processing (9)
  • __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/groundingShadowContent.ts

Comment thread src/components/ChatView.tsx Outdated
Comment thread src/components/GroundingHtml.web.tsx Outdated
Comment thread src/utils/groundingShadowContent.ts Outdated
Comment thread src/utils/groundingShadowContent.ts
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>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: 5c9959b1

Review resolution

  • sanitizeGroundingHtmlLinks permissive sanitizer (Copilot + CodeRabbit): Fixed — replaced denylist with a strict tag/attribute allowlist for Google widget HTML; strips iframe/object/embed/link/meta and unsafe URL attributes (src/utils/groundingShadowContent.ts)
  • scheduleSync rAF after unmount (Copilot): Fixed — added mounted guard, cancel pending rAF on cleanup (src/components/GroundingHtml.web.tsx)
  • MessageText.textStyle left/right shape (CodeRabbit): Fixed — apply web wrap styles per side while preserving left/right map (src/components/ChatView.tsx)
  • StyleSheet.flatten undefined destructuring (CodeRabbit): Fixed — default flattened style to {} before destructuring (src/components/GroundingHtml.web.tsx)
  • Wheel handler at horizontal edge (CodeRabbit): Fixed — return false when scrollLeft cannot move further (src/utils/groundingShadowContent.ts)
  • Empty-response retry on deterministic finish reasons (CodeRabbit nitpick): Fixed — retry only when finish reason is unspecified/transient (functions/src/generateReply.ts)

Verification

  • typecheck — pass (npm run typecheck)
  • lint — pass (npm run lint:check)
  • tests — pass (npm run test -- src/utils/__tests__/groundingShadowContent.test.ts __tests__/chatReplyService.test.ts; cd functions && npm run test)

@equationalapplications
equationalapplications merged commit 265a94a into staging Jun 25, 2026
3 checks passed
@equationalapplications
equationalapplications deleted the fixes branch June 25, 2026 22:20
equationalapplications added a commit that referenced this pull request Jun 25, 2026
fix(chat): grounding and web chat reply fixes (#470, #471)
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.

2 participants