Widget Display Logic¶
This document provides a comprehensive overview of the decision flow that determines whether the Rose Widget displays on a given page. Understanding this flow is essential for debugging visibility issues and configuring the widget correctly.
For configuration options, see Client Configuration. For the technical implementation, see Widget Technical Knowledge.
Overview¶
The widget display decision involves multiple layers of checks, split between DisplayController (show/hide decision) and RoseWidget (display mode and layout resolution):
| Layer | Component | Purpose | Configuration |
|---|---|---|---|
| Force Display | DisplayController | Bypass all checks (demo/testing) | Runtime forceDisplay param |
| Traffic Control | DisplayController | Gradual rollout percentage | traffic_control.traffic_percentage |
| URL Whitelist | DisplayController | Only show on specific URLs | traffic_control.display_only_on_urls |
| Page Display Rules (hidden) | DisplayController | Never show on specific URLs | traffic_control.page_display_rules |
| Device Check | DisplayController | Mobile device handling | traffic_control.enable_mobile |
| Display Mode | DisplayController | Hidden zone vs normal | traffic_control.widget_display_mode |
| Form Assistant | DisplayController | Derive form-assistant context and forced copilot presentation | form_assistant feature + form_assistant.pages |
| Expanded Layout Resolution | RoseWidget | Resolve expanded vs copilot layout | appearance.enable_copilot_layout + stored viewLayout |
| Page Display Rules (minimized/displayed) | RoseWidget | Per-URL display mode override | traffic_control.page_display_rules |
Decision Flowchart¶
Two independent systems can set minimized state:
- Page display rules — any URL pattern can be
hidden,minimized, ordisplayed - Form assistant initial state — matching form assistant pages start minimized on CTA/form pages
When a page rule sets minimized, it is applied once per URL. The user can click to expand, and the widget won't be forced back to minimized. On SPA navigation away from a minimized-rule page, the minimized state resets.
Page rules override form assistant minimized: A displayed page rule takes priority over the form assistant initial minimized state. If a CTA page has both form assistant enabled and a displayed page rule, the widget shows collapsed (search bar) instead of minimized. This override is applied once per URL — the user can still manually minimize afterward.
Page rules vs hidden_zone: A
displayedorminimizedpage rule overrides hidden_zone mode. If no page rule matches, hidden_zone behavior applies normally.
Expanded Layout Resolution¶
After DisplayController decides that the widget should render, RoseWidget resolves the expanded layout separately from the top-level widget state:
This distinction is important:
viewLayoutis widget UI state for the expanded view onlyisFormAssistantis derived page/context behavior, not reducer stateformFactoris the runtime presentation input that can force copilot in testing or special integrations
When the copilot layout toggle is disabled, the widget normalizes viewLayout back to 'expanded' unless form assistant or a forced copilot formFactor explicitly locks the side panel.
SPA Navigation: URL Change Handling¶
Note:
subscribeToUrlChangesalways runs regardless ofspa_widget_handling. The SPA gate only controls whether DisplayController recomputes its full display decision (traffic control, form factor, etc.). Page display rules are always resolved by RoseWidget via thecurrentUrlprop.
Layer 1: Force Display Override¶
Runtime parameter: forceDisplay
When forceDisplay is set to true, all other checks are bypassed and the widget always displays. This is used for:
- Demo pages
- Local development testing
- Client preview environments
InboundXLoader.init({
forceDisplay: true, // Bypasses all display restrictions
// ... other config
});
Layer 2: Traffic Control¶
Configuration: traffic_control.traffic_percentage (0-100)
Controls what percentage of visitors see the widget using deterministic bucketing.
| Value | Behavior |
|---|---|
0 |
Widget never shows (domain disabled) |
1-99 |
Gradual rollout (e.g., 20 = 20% of visitors) |
100 |
Widget always shows (default) |
How Bucketing Works¶
- Generate a stable client ID (PostHog
distinct_idor localStorage UUID) - Hash the client ID to assign a bucket (0-99)
- If
bucket < traffic_percentage, show the widget
Key benefit: Same user always gets the same result across sessions. Increasing traffic from 20% to 80% keeps the original 20% enabled.
Code location: frontend/widget/src/lib/widgetInit/trafficControl.ts
Layer 3: URL Whitelist¶
Configuration: traffic_control.display_only_on_urls (string array)
If defined, the widget only displays on URLs matching these patterns.
Matching Logic¶
- Uses simple substring matching (
includes()) - Trailing slashes are normalized before comparison
- If array is empty or undefined, no whitelist restriction applies
- Wildcards (
*) are NOT supported (unlikepage_display_rules)
Code location: frontend/shared/src/context/SiteConfigContext.tsx (isCurrentURLAllowed)
Layer 4: Page Display Rules¶
Configuration: custom_config.page_display_rules (array of { pattern, display })
Per-URL display mode control. Replaces the old exclude_url_patterns and hide_search_bar_on_cta_pages configurations.
{
"custom_config": {
"page_display_rules": [
{ "pattern": "www.example.com/blog*", "display": "hidden" },
{ "pattern": "www.example.com/book-demo*", "display": "minimized" },
{ "pattern": "www.example.com/pricing", "display": "displayed" }
]
}
}
Display Modes¶
| Value | Behavior | Where Checked |
|---|---|---|
hidden |
Widget not rendered at all | DisplayController (URL check) + RoseWidget (early return) |
minimized |
Shows MinimizedChatView (small button) — applied once per URL, user can expand | RoseWidget |
displayed |
Widget shows normally (overrides hidden_zone) | RoseWidget |
Matching Logic¶
- Uses wildcard pattern matching via
resolvePageDisplayRule() - Supports
*wildcards (e.g.,*.example.com/internal*) - First match wins — rules are evaluated in order
- If no rule matches, widget behaves normally (no override)
Minimized Behavior Details¶
When a page rule sets display: 'minimized':
- Widget shows as
MinimizedChatView(small button) on first navigation to the URL - User can click to expand — the rule does not re-apply (applied once per URL)
- If user collapses, widget stays collapsed (search bar), not forced back to minimized
- Navigating to a different page resets the minimized state
- Navigating back to the same URL re-applies minimized
Implementation: pageRuleAppliedForUrl ref in RoseWidget tracks which URL the minimized rule was applied for, preventing re-application on the same URL. Similarly, displayedRuleAppliedForUrl tracks which URL a displayed override was applied for, preventing it from fighting user-initiated minimize actions.
Page Rules vs Hidden Zone¶
When widget_display_mode = 'hidden_zone_click' is active, page rules take priority:
| Page Rule | Hidden Zone | Result |
|---|---|---|
displayed |
Active | Widget shows normally (page rule overrides) |
minimized |
Active | Widget shows minimized (page rule overrides) |
hidden |
Active | Widget hidden (both agree) |
| No match | Active | HiddenZoneButton shown (hidden zone behavior) |
Page Rules vs Form Assistant¶
Form assistant sets isExplicitlyMinimized: true on matching CTA/form pages. Page rules interact as follows:
| Page Rule | Form Assistant | Result |
|---|---|---|
minimized |
Active | Both set minimized — forced copilot state is preserved on navigation |
displayed |
Active | Widget shows normally while keeping the form assistant copilot layout |
hidden |
Active | Widget hidden (page rule wins) |
| No match | Active | Form assistant minimized state is preserved (page rules don't touch it) |
Code locations:
- Rule resolution: frontend/shared/src/utils/domain/pageDisplayRules.ts (resolvePageDisplayRule)
- URL check (hidden): frontend/shared/src/utils/domain/siteRestrictions.ts (isUrlAllowed)
- Display mode (minimized/displayed): frontend/shared/src/components/RoseWidget.tsx (pageRuleDisplay useMemo + sync effect)
Layer 5: Device Check¶
Configuration: custom_config.enable_mobile (boolean)
| Value | Behavior |
|---|---|
false (default) |
Widget hidden on mobile devices |
true |
Widget visible on mobile devices |
Mobile Detection¶
Mobile detection uses User Agent string matching only:
Note: The 1236px breakpoint in constants is used for responsive layout adjustments within the widget, not for display decisions.
Code location: frontend/shared/src/utils/device/mobileDetection.ts
Layer 6: Display Mode (Hidden Zone)¶
Configuration: traffic_control.widget_display_mode
| Value | Behavior |
|---|---|
'normal' (default) |
Widget shows immediately |
'hidden_zone_click' |
Widget shows a subtle button; full widget appears after click |
Hidden zone is used for soft launches and client testing. Page display rules can override hidden zone behavior (see Layer 4).
Code location: frontend/widget/src/components/DisplayController.tsx (resolveDisplayMode)
Layer 7: Form Assistant¶
Configuration: unified form_assistant feature plus page rules in form_assistant.pages
| Configuration | Purpose |
|---|---|
form_assistant feature |
Enables form assistant for the domain |
form_assistant.pages |
Page patterns and questions for matching CTA/form pages |
| Value | Behavior |
|---|---|
false (default) |
Widget always follows the regular collapsed → expanded flow |
true |
Matching CTA/form pages enter form assistant mode and force copilot layout |
What is Form Assistant?¶
Form assistant is a feature that forces the widget into the copilot side-panel layout on CTA destination pages to assist users in completing forms.
It is not the same thing as the optional copilot layout toggle:
- Copilot layout is a visual presentation for the expanded chat
- Form assistant is a behavior mode that forces copilot layout and adds form-specific UX rules
When enabled, matching CTA/form pages provide:
- Minimized view: Small button in the bottom-right corner (instead of bottom-center search bar)
- Expanded view: Right-side panel (instead of centered chat popup)
- Welcome message: Initial assistant message with suggested questions related to the current page
- Form assistance: Helps users understand form fields and complete their tasks
- Behavior overrides: No layout toggle, CTA suppression inside the chat, and fresh conversation bootstrapping when entering form assistant pages
When is Form Assistant Used?¶
Form assistant is activated when ALL of the following conditions are true:
form_assistantis enabled (site override if set, otherwise global default)- Current device is NOT mobile (the forced copilot side panel is never used on mobile devices)
- Current page is a form tracking page (CTA destination, additional form tracking page, or thank-you page)
What Counts as a "Form Tracking Page"?¶
| Source | Configuration | Description |
|---|---|---|
| CTA Destinations | ctas.ctas[].url[lang] |
URLs from CTA button definitions |
| Additional Destinations | Not used in unified config | No separate track-only destination list |
| Form Tracking Pages | analytics.form_tracking_pages |
Extra patterns for form tracking |
| Thank-You Pages | analytics.thank_you_pages |
Confirmation page patterns |
Mobile Restriction¶
Form assistant is never enabled on mobile devices. On mobile, the widget always uses the default form factor regardless of the form_assistant feature flag setting.
Mode Transition: Copilot to Default¶
When a user navigates from a form assistant page (CTA page) to a default mode page (non-CTA page), the widget handles the transition gracefully:
| Scenario | Behavior |
|---|---|
| User interacted with copilot (sent a message) | Conversation persists, widget shows in default mode with history |
| User only saw welcome message (no interaction) | Conversation is cleared, widget shows in collapsed state |
Code location: frontend/shared/src/config/SiteConfigManager.ts (isFormAssistantEnabled, shouldUseFormAssistant)
Layer 8: Expanded Layout Resolution¶
Configuration: appearance.enable_copilot_layout
| Value | Behavior |
|---|---|
false (default) |
Visitors cannot toggle into copilot layout; expanded view resolves to centered layout unless form assistant or a forced copilot formFactor applies |
true |
Desktop visitors can toggle between centered expanded view and copilot side panel |
What viewLayout controls¶
viewLayout only affects the expanded/chat chrome presentation:
- expanded view width and position
- follow-up question spacing
- question bubble sizing
- minimized docking position after close
It does not enable form assistant behavior by itself.
Persistence¶
- The selected
viewLayoutis stored in session storage - Form assistant always resolves the layout to
'copilot' - When the copilot layout toggle is disabled, RoseWidget normalizes both storage and in-memory state back to
'expanded'
Code locations:
- frontend/shared/src/utils/widget/viewLayout.ts (resolveExpandedLayout)
- frontend/shared/src/components/RoseWidget.tsx (effectiveViewLayout resolution + sync effect)
- frontend/shared/src/services/chatStorage.ts (getViewLayout, setViewLayout)
Key Functions Reference¶
| Function | Location | Purpose |
|---|---|---|
canInitializeWidget() |
widget/src/lib/widgetInit/initializationCheck.ts |
Orchestrates all initialization checks |
checkTrafficAllocation() |
widget/src/lib/widgetInit/trafficControl.ts |
Deterministic traffic bucketing |
isUrlAllowed() |
shared/src/utils/domain/siteRestrictions.ts |
Checks URL against page_display_rules (hidden) |
resolvePageDisplayRule() |
shared/src/utils/domain/pageDisplayRules.ts |
Resolves first matching page display rule |
isUrlAllowedByPageRules() |
shared/src/utils/domain/pageDisplayRules.ts |
Checks if URL is hidden by page rules |
isCurrentDeviceAllowed() |
shared/src/utils/domain/siteRestrictions.ts |
Checks mobile device rules |
shouldEnableFormTracking() |
shared/src/config/SiteConfigManager.ts |
Determines if page needs form tracking |
isFormAssistantEnabled() |
shared/src/config/SiteConfigManager.ts |
Checks form_assistant feature flag |
shouldUseFormAssistant() |
shared/src/config/SiteConfigManager.ts |
Determines if form assistant should be used on current page |
resolveExpandedLayout() |
shared/src/utils/widget/viewLayout.ts |
Resolves the effective expanded layout from config, context, and stored preference |
shouldEnableSpaHandling() |
shared/src/config/SiteConfigManager.ts |
Checks if SPA widget handling is enabled |
subscribeToUrlChanges() |
shared/src/utils/domain/urlChangeDetection.ts |
Subscribes to SPA URL changes |
Debugging Display Issues¶
1. Check Browser Console¶
Enable debug logging to see the decision flow. The loader's debug option only enables loader logs. To see widget display-decision logs, enable debug mode after the widget loads:
// Option 1: Enable after widget is ready
window.InboundXWidget.onReady(() => {
window.InboundXWidget.enableDebug();
});
// Option 2: Check status and enable manually
window.InboundXWidget.getStatus(); // See current state
await window.InboundXWidget.enableDebug(); // Enable debug logs
Look for these log patterns:
- 🎯 shouldDisplayOnCurrentUrl - URL pattern evaluation
- 🚦 Traffic control - Traffic allocation check
- 📱 Mobile device detected - Device type detection
- 📐 Page display rule - Page rule minimized/displayed application
- 🚫 Page display rule: widget hidden - Page rule hidden
2. Verify Configuration¶
Check the domain overrides in Supabase:
SELECT
domain,
config_slug,
config
FROM config.client_configs
WHERE domain = 'example.com'
AND config_slug IN ('traffic_control', 'analytics', 'ctas', 'form_assistant')
ORDER BY config_slug;
3. Common Issues¶
| Symptom | Likely Cause | Check |
|---|---|---|
| Widget never shows | traffic_percentage = 0 |
traffic_control override in Supabase |
| Widget missing on some pages | Page display rule hidden |
page_display_rules config |
| Widget missing on mobile | Mobile not enabled | enable_mobile in custom_config |
| Widget shows as small button | Page display rule minimized |
page_display_rules config |
| Widget shows but only after click | Hidden zone mode | widget_display_mode |
| Widget shows inconsistently | Traffic bucketing | traffic_allocated < 100 |
| Widget stuck minimized on page rule page | Bug — should allow expand | Check pageRuleAppliedForUrl ref |
Widget minimized on CTA page despite displayed rule |
Page rule not overriding form assistant minimized state | Check displayedRuleAppliedForUrl ref + form assistant initial state effect |
| Minimized state persists after navigation | Page rule reset not firing | Check SPA handling + currentUrl prop |
4. Force Display for Testing¶
To bypass all restrictions temporarily:
Related Documentation¶
- Client Configuration - All configuration options
- Traffic Control - Detailed traffic allocation docs
- Widget Technical Knowledge - Implementation details
- Widget Development Setup - Local development guide