Repository navigation
fix(web): restore chat replies and grounding widget layout - #471
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
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
undefinedoptional fields and adds a regression test. - Updates
generateReplyto treatnullunsyncedHistoryas 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.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
functions/src/generateReply.ts (1)
365-371: 🚀 Performance & Scalability | 🔵 TrivialOptional: 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
📒 Files selected for processing (9)
__tests__/chatReplyService.test.tsfunctions/src/generateReply.test.tsfunctions/src/generateReply.tssrc/components/ChatView.tsxsrc/components/GroundingHtml.tsxsrc/components/GroundingHtml.web.tsxsrc/services/chatReplyService.tssrc/utils/__tests__/groundingShadowContent.test.tssrc/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>
/fix-pr follow-upCommit: Review resolution
Verification
|
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
Related Issue
Fixes #
Changes Made
generateChatReplypayload so Firebase web does not encode them asnullnullunsyncedHistoryas absent ingenerateReplyparsingfinishReasonon failuremaxWidth: 80%) and disable bubble press so scroll gestures are not swallowedTesting
Platforms Tested
Test Steps
http://localhost:8081/chatand send a message that triggers Google Search groundingunsyncedHistory400)npx jest src/utils/__tests__/groundingShadowContent.test.tsgenerateReplynullunsyncedHistoryhandlingScreenshots
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
Additional Notes
Native grounding rendering (WebView) is unchanged. Shadow DOM follows Google's recommended embed path for
renderedContent.Made with Cursor