Skip to content

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

flowchart TD A[Page Loads] --> B{forceDisplay?} B -->|Yes| RENDER[Render RoseWidget] B -->|No| C{traffic_allocated = 0?} C -->|Yes| HIDE1[HIDE: Domain disabled] C -->|No| D{User bucket < traffic_allocated?} D -->|No| HIDE2[HIDE: Traffic bucket] D -->|Yes| E{display_only_on_urls defined?} E -->|Yes| F{URL matches whitelist?} E -->|No| G{URL hidden by page_display_rules?} F -->|No| HIDE3[HIDE: Not whitelisted] F -->|Yes| G G -->|Yes| HIDE4[HIDE: page_display_rules] G -->|No| H{Mobile device?} H -->|Yes| I{enable_mobile?} H -->|No| MODE I -->|No| HIDE5[HIDE: Mobile blocked] I -->|Yes| MODE[Resolve display mode + form factor] MODE --> M1{widget_display_mode?} M1 -->|hidden_zone_click| MODE_HZ[displayMode = hidden_zone] M1 -->|normal| MODE_N[displayMode = normal] MODE_HZ --> FF MODE_N --> FF FF{form_assistant enabled + page matches + not mobile?} FF -->|Yes| RENDER_C[Render RoseWidget — form assistant context] FF -->|No| RENDER_D[Render RoseWidget — default context] RENDER --> PR RENDER_C --> PR RENDER_D --> PR %% === RoseWidget internal === PR{page_display_rules match?} PR -->|hidden| HIDE_PR[HIDE: page rule] PR -->|minimized| WIDGET_MIN[SHOW: MinimizedChatView] PR -->|displayed| WIDGET_SHOW[SHOW: Widget displayed] PR -->|No match| HZ{displayMode = hidden_zone?} HZ -->|Yes, user has not clicked| HZ_BTN[SHOW: HiddenZoneButton — user must click to reveal] HZ -->|Yes, user clicked| FORM_ASSISTANT_INIT HZ -->|No| FORM_ASSISTANT_INIT FORM_ASSISTANT_INIT{forced copilot + form assistant + first load?} FORM_ASSISTANT_INIT -->|Yes| COPILOT_MIN[SHOW: MinimizedChatView — form assistant initial state] FORM_ASSISTANT_INIT -->|No| WIDGET_SHOW %% Styles style HIDE1 fill:#f87171,color:#fff style HIDE2 fill:#f87171,color:#fff style HIDE3 fill:#f87171,color:#fff style HIDE4 fill:#f87171,color:#fff style HIDE5 fill:#f87171,color:#fff style HIDE_PR fill:#f87171,color:#fff style WIDGET_SHOW fill:#4ade80,color:#000 style HZ_BTN fill:#fbbf24,color:#000 style WIDGET_MIN fill:#60a5fa,color:#000 style COPILOT_MIN fill:#60a5fa,color:#000 style RENDER fill:#e5e7eb,color:#000 style RENDER_C fill:#e5e7eb,color:#000 style RENDER_D fill:#e5e7eb,color:#000

Two independent systems can set minimized state:

  1. Page display rules — any URL pattern can be hidden, minimized, or displayed
  2. 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 displayed or minimized page 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:

flowchart TD A[RoseWidget] --> B{isFormAssistant OR formFactor = copilot?} B -->|Yes| C[effectiveViewLayout = copilot] B -->|No| D{enable_copilot_layout?} D -->|No| E[effectiveViewLayout = expanded] D -->|Yes| F[effectiveViewLayout = stored viewLayout]

This distinction is important:

  • viewLayout is widget UI state for the expanded view only
  • isFormAssistant is derived page/context behavior, not reducer state
  • formFactor is 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

flowchart TD SPA[URL changes on SPA navigation] --> DC_SUB[DisplayController: subscribeToUrlChanges] DC_SUB --> DC_URL[Update currentUrl state] DC_URL --> SPA_GATE{spa_widget_handling.enabled?} SPA_GATE -->|Yes| RECOMPUTE[Recompute display decision] SPA_GATE -->|No, was enabled before| RECOMPUTE SPA_GATE -->|No, never enabled| SKIP_DC[Skip DisplayController recomputation] RECOMPUTE --> DC_UPDATE[Update displayMode + formFactor] DC_URL --> RW_PROP[Pass currentUrl prop to RoseWidget] RW_PROP --> RW_MEMO[useMemo: resolve pageRuleDisplay] RW_MEMO --> RW_RENDER[Re-render with new page rule]

Note: subscribeToUrlChanges always runs regardless of spa_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 the currentUrl prop.


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

  1. Generate a stable client ID (PostHog distinct_id or localStorage UUID)
  2. Hash the client ID to assign a bucket (0-99)
  3. 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.

{
  "display_only_on_urls": [
    "https://www.example.com/pricing",
    "https://www.example.com/demo"
  ]
}

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 (unlike page_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':

  1. Widget shows as MinimizedChatView (small button) on first navigation to the URL
  2. User can click to expand — the rule does not re-apply (applied once per URL)
  3. If user collapses, widget stays collapsed (search bar), not forced back to minimized
  4. Navigating to a different page resets the minimized state
  5. 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:

/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i

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:

  1. form_assistant is enabled (site override if set, otherwise global default)
  2. Current device is NOT mobile (the forced copilot side panel is never used on mobile devices)
  3. 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 viewLayout is 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:

InboundXLoader.init({
  forceDisplay: true,
  // ... other config
});