Skip to content

feat(widget): add redirect support to quick-reply pills#836

Open
amreetkhuntia wants to merge 1 commit into
juspay:releasefrom
amreetkhuntia:feat/quick-reply-redirect
Open

feat(widget): add redirect support to quick-reply pills#836
amreetkhuntia wants to merge 1 commit into
juspay:releasefrom
amreetkhuntia:feat/quick-reply-redirect

Conversation

@amreetkhuntia

@amreetkhuntia amreetkhuntia commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Quick-reply chicklets could only send their value back to the agent. A pill can now optionally carry an action, reusing the catalog's existing ActionUnion: an open_url action makes the pill redirect (honoring a new target: new_tab|same_tab) instead of messaging the agent.

  • ui_catalog: OpenUrlAction gains target; QuickReplyItem gains action
  • ui_prompt: add a redirect example so the LLM can emit redirect pills
  • types: QuickReplyOption (static config chicklets) gains action
  • chat schema: QuickReplyWire carries action to the frontend
  • widget handler: pass action through; only apply the value=label fallback for message pills (redirect pills have null value)

Backward compatible: action defaults to None (existing send-to-agent behavior), target defaults to new_tab. JSONB column, no migration.

Summary by CodeRabbit

  • New Features

    • Quick-reply buttons can now perform URL redirects with configurable target behavior (open in new tab or same tab).
    • Maintains backward compatibility with existing message-based quick replies.
  • Tests

    • Added comprehensive test coverage for quick-reply redirect functionality, including validation and edge cases.

Copilot AI review requested due to automatic review settings June 16, 2026 10:11
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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: 2b20d392-d6d7-46e9-aab0-d1db190d7e20

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

Walkthrough

Adds open_url redirect support to Breeze Buddy quick-reply pills. OpenUrlAction gains a target field (new_tab/same_tab). An optional action: ActionUnion field is added to QuickReplyItem, QuickReplyOption, and documented in QuickReplyWire. The widget handler skips the labelvalue fallback for redirect pills. Tests and a prompt example are updated accordingly.

Changes

Quick-reply open_url redirect feature

Layer / File(s) Summary
OpenUrlAction, QuickReplyItem, QuickReplyOption, and wire schema contracts
app/ai/voice/agents/breeze_buddy/template/ui_catalog.py, app/ai/voice/agents/breeze_buddy/template/types.py, app/schemas/breeze_buddy/chat.py
OpenUrlAction gains target: Literal["new_tab", "same_tab"] defaulting to "new_tab". QuickReplyItem docs and field descriptions are updated for redirect semantics. QuickReplyOption adds action: Optional[ActionUnion]. QuickReplyWire field descriptions and its ActionUnion import are updated to reflect null-value behavior for open_url actions.
Handler redirect detection and value-extraction
app/api/routers/breeze_buddy/widget/handlers.py
Adds _is_redirect_action() to identify open_url pills; modifies _extract_widget_config so redirect pills keep value=None instead of falling back to qr.label.
Tests, prompt example, and .gitignore
tests/test_quick_reply_redirect.py, app/ai/voice/agents/breeze_buddy/template/ui_prompt.py, .gitignore
150-line pytest module covering OpenUrlAction target validation, static model behavior for QuickReplyOption/QuickReplyWire, and handler-level wiring. _EXAMPLES updated to include an open_url action with url and target. temp/ added to .gitignore.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • juspay/clairvoyance#811: Introduced the "dynamic chicklets" feature that established the QuickReplyItem, OpenUrlAction, and action field foundations in ui_catalog.py and ui_prompt.py that this PR directly extends.

Poem

🐰 Hop, hop — a quick-reply pill can fly,
Not just to chat, but to open_url on high!
"new_tab" or "same_tab", your choice to pick,
The label fallback skipped for redirects quick.
No value needed when a URL's the way —
The rabbit ships clean schemas today! 🌐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(widget): add redirect support to quick-reply pills' accurately describes the main feature addition—enabling quick-reply pills to perform redirects via an action property, which is the core enhancement across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.gitignore (1)

53-58: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resolve comment-vs-rule conflict for .claude/settings.json.

Line 53 says settings.json should be shared, but Line 57 now ignores it. Either remove the ignore entry or update the comment to reflect the intended policy.

🤖 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 @.gitignore around lines 53 - 58, There is a contradiction in the .gitignore
file between the comment on line 53 and the actual ignore rules. The comment
states that settings.json should be shared with the team, but line 57 contains
.claude/settings.json as an ignore entry, which prevents it from being tracked.
To resolve this conflict, either remove the .claude/settings.json ignore entry
from line 57 to allow the file to be shared as the comment indicates, or update
the comment on line 53 to accurately reflect that settings.json is kept local
and should not be shared. Choose the approach that aligns with your intended
policy for this configuration file.
🧹 Nitpick comments (2)
app/ai/voice/agents/breeze_buddy/template/ui_prompt.py (1)

92-99: ⚡ Quick win

Update the shared action footer to reflect target support (new_tab/same_tab).

This change introduces target, but the footer still documents open_url as “new tab” only. Keeping footer + schema aligned will prevent prompt-level drift.

💡 Suggested fix
 _FOOTER = (
     "Action shape (embedded inside Button/Tile/Handoff):\n"
     '  {type:"to_assistant", msg*: string}  — re-enter the chat as if '
     "the user typed `msg`\n"
-    '  {type:"open_url", url*: HttpUrl}     — open a URL in a new tab\n'
+    '  {type:"open_url", url*: HttpUrl, target?: "new_tab"|"same_tab"}'
+    " — open a URL (default new_tab)\n"
🤖 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 `@app/ai/voice/agents/breeze_buddy/template/ui_prompt.py` around lines 92 - 99,
The code now supports a `target` parameter in the `open_url` action that can be
either `new_tab` or `same_tab`, but the shared action footer documentation still
describes `open_url` as opening in a new tab only. Locate the footer or schema
documentation for the `open_url` action (likely a string constant or docstring
describing available actions) and update it to document the `target` parameter
with both supported values (`new_tab` and `same_tab`) so the documentation stays
aligned with the actual implementation.
tests/test_quick_reply_redirect.py (1)

33-150: ⚡ Quick win

Add return type hints to test function signatures to match repo typing policy.

The new test/helper functions are unannotated; adding explicit return types (e.g., -> None, helper return types) keeps this file aligned with the project-wide typing guideline.

As per coding guidelines, **/*.py: “Add type hints on all function signatures”.

🤖 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 `@tests/test_quick_reply_redirect.py` around lines 33 - 150, All test functions
and helper functions in this file are missing return type hints, which violates
the project's typing guidelines. Add return type annotations to every function
definition: add `-> None` as the return type for all test functions
(test_open_url_defaults_to_new_tab, test_open_url_accepts_same_tab,
test_open_url_rejects_unknown_target,
test_quick_replies_with_redirect_action_validates,
test_quick_reply_item_without_action_is_backward_compatible,
test_quick_reply_item_accepts_to_assistant_action,
test_static_option_redirect_needs_no_value,
test_static_option_without_action_is_backward_compatible,
test_wire_carries_action_with_null_value,
test_handler_message_pill_falls_back_to_label,
test_handler_redirect_pill_has_null_value_and_passes_action, and
test_handler_no_configurations_returns_defaults), and add an appropriate return
type annotation for the _template_with_quick_replies helper function based on
what it returns (a SimpleNamespace object).

Source: Coding guidelines

🤖 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 `@app/api/routers/breeze_buddy/widget/handlers.py`:
- Around line 164-171: The value assignment logic for pills in this code needs
to prioritize redirect action checks. Currently, when qr.value is not None, it
is used regardless of whether the action is a redirect (open_url). To fix this,
reverse the conditional order: first check if the action is a redirect using
_is_redirect_action(qr.action), and if true, always set value to None. Only if
it is not a redirect action should the existing fallback logic apply (use
qr.value if present, otherwise use qr.label). This ensures redirect pills never
leak message value semantics to clients.

In `@tests/test_quick_reply_redirect.py`:
- Around line 44-47: The test file has type checking issues where assertions
access action attributes on optional fields without proper type narrowing. Fix
this by casting the results of validate_props() calls to the specific
QuickReplies type (using cast() from typing), and before accessing any
attributes on the optional .action field, add either isinstance() checks to
narrow the union type or assert statements to ensure the field is not None.
Apply these guards consistently wherever .action.type or .action.target are
accessed in assertions throughout the test file.

---

Outside diff comments:
In @.gitignore:
- Around line 53-58: There is a contradiction in the .gitignore file between the
comment on line 53 and the actual ignore rules. The comment states that
settings.json should be shared with the team, but line 57 contains
.claude/settings.json as an ignore entry, which prevents it from being tracked.
To resolve this conflict, either remove the .claude/settings.json ignore entry
from line 57 to allow the file to be shared as the comment indicates, or update
the comment on line 53 to accurately reflect that settings.json is kept local
and should not be shared. Choose the approach that aligns with your intended
policy for this configuration file.

---

Nitpick comments:
In `@app/ai/voice/agents/breeze_buddy/template/ui_prompt.py`:
- Around line 92-99: The code now supports a `target` parameter in the
`open_url` action that can be either `new_tab` or `same_tab`, but the shared
action footer documentation still describes `open_url` as opening in a new tab
only. Locate the footer or schema documentation for the `open_url` action
(likely a string constant or docstring describing available actions) and update
it to document the `target` parameter with both supported values (`new_tab` and
`same_tab`) so the documentation stays aligned with the actual implementation.

In `@tests/test_quick_reply_redirect.py`:
- Around line 33-150: All test functions and helper functions in this file are
missing return type hints, which violates the project's typing guidelines. Add
return type annotations to every function definition: add `-> None` as the
return type for all test functions (test_open_url_defaults_to_new_tab,
test_open_url_accepts_same_tab, test_open_url_rejects_unknown_target,
test_quick_replies_with_redirect_action_validates,
test_quick_reply_item_without_action_is_backward_compatible,
test_quick_reply_item_accepts_to_assistant_action,
test_static_option_redirect_needs_no_value,
test_static_option_without_action_is_backward_compatible,
test_wire_carries_action_with_null_value,
test_handler_message_pill_falls_back_to_label,
test_handler_redirect_pill_has_null_value_and_passes_action, and
test_handler_no_configurations_returns_defaults), and add an appropriate return
type annotation for the _template_with_quick_replies helper function based on
what it returns (a SimpleNamespace object).
🪄 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: 2cae9095-4a65-4c39-b31b-50e8d56689fb

📥 Commits

Reviewing files that changed from the base of the PR and between 58d3fbc and 5aec56f.

📒 Files selected for processing (7)
  • .gitignore
  • app/ai/voice/agents/breeze_buddy/template/types.py
  • app/ai/voice/agents/breeze_buddy/template/ui_catalog.py
  • app/ai/voice/agents/breeze_buddy/template/ui_prompt.py
  • app/api/routers/breeze_buddy/widget/handlers.py
  • app/schemas/breeze_buddy/chat.py
  • tests/test_quick_reply_redirect.py

Comment on lines +164 to +171
label=qr.label,
# Redirect pills (open_url action) don't message the agent, so the
# label->value fallback only applies to plain message pills.
value=(
qr.value
if qr.value is not None
else (None if _is_redirect_action(qr.action) else qr.label)
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Force redirect pills to always emit value=None.

Line 167 currently keeps qr.value when present, even for open_url actions. That conflicts with the redirect wire behavior in this PR and can leak message semantics back to clients that still key off value.

💡 Suggested fix
         QuickReplyWire(
             label=qr.label,
             # Redirect pills (open_url action) don't message the agent, so the
             # label->value fallback only applies to plain message pills.
-            value=(
-                qr.value
-                if qr.value is not None
-                else (None if _is_redirect_action(qr.action) else qr.label)
-            ),
+            value=(
+                None
+                if _is_redirect_action(qr.action)
+                else (qr.value if qr.value is not None else qr.label)
+            ),
             action=qr.action,
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
label=qr.label,
# Redirect pills (open_url action) don't message the agent, so the
# label->value fallback only applies to plain message pills.
value=(
qr.value
if qr.value is not None
else (None if _is_redirect_action(qr.action) else qr.label)
),
label=qr.label,
# Redirect pills (open_url action) don't message the agent, so the
# label->value fallback only applies to plain message pills.
value=(
None
if _is_redirect_action(qr.action)
else (qr.value if qr.value is not None else qr.label)
),
🤖 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 `@app/api/routers/breeze_buddy/widget/handlers.py` around lines 164 - 171, The
value assignment logic for pills in this code needs to prioritize redirect
action checks. Currently, when qr.value is not None, it is used regardless of
whether the action is a redirect (open_url). To fix this, reverse the
conditional order: first check if the action is a redirect using
_is_redirect_action(qr.action), and if true, always set value to None. Only if
it is not a redirect action should the existing fallback logic apply (use
qr.value if present, otherwise use qr.label). This ensures redirect pills never
leak message value semantics to clients.

Comment thread tests/test_quick_reply_redirect.py
@amreetkhuntia amreetkhuntia force-pushed the feat/quick-reply-redirect branch 2 times, most recently from 810e24f to 1d912d1 Compare June 16, 2026 10:48
Quick-reply chicklets could only send their value back to the agent. A
pill can now optionally carry an action, reusing the catalog's existing
ActionUnion: an open_url action makes the pill redirect (honoring a new
target: new_tab|same_tab) instead of messaging the agent.

- ui_catalog: OpenUrlAction gains target; QuickReplyItem gains action
- ui_prompt: add a redirect example so the LLM can emit redirect pills
- types: QuickReplyOption (static config chicklets) gains action
- chat schema: QuickReplyWire carries action to the frontend
- widget handler: pass action through; only apply the value=label
  fallback for message pills (redirect pills have null value)

Backward compatible: action defaults to None (existing send-to-agent
behavior), target defaults to new_tab. JSONB column, no migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@amreetkhuntia amreetkhuntia force-pushed the feat/quick-reply-redirect branch from 1d912d1 to 9185341 Compare June 16, 2026 10:51
@narsimhaReddyJuspay

Copy link
Copy Markdown
Contributor

PR #836feat(widget): add redirect support to quick-reply pills

Verdict: no blocking issues — looks good. Gates all green: black/isort/autoflake/pyrefly ✅, pytest 441 passed (incl. the new test_quick_reply_redirect.py), field-reference coverage 3 passed. No critical/major functional bugs found — posting no inline comments.

Security — verified clean (the main risk on a "redirect" feature)

  • XSS / dangerous URL schemes: mitigated. The redirect URL is typed url: HttpUrl on OpenUrlAction (ui_catalog.py:98), so Pydantic rejects javascript:, data:, vbscript:, file:, blob:, schemeless, and obfuscated variants at validation — only http/https survive. It's re-validated on the wire path (QuickReplyWire.action: Optional[ActionUnion]OpenUrlAction, chat.py:457).
  • No injection / no SSRF. The URL comes only from template config (merchant/admin at save time) — there is no lead-payload variable interpolation into the URL, and the server never fetches it (purely client-side redirect).
  • Residual (by design, not a regression): a template author can point a pill at an arbitrary https:// host (open redirect / phishing). This matches the existing OpenUrlAction / TileMedia.src precedent (same HttpUrl, no host allowlist). An optional merchant host-allowlist would harden it, but it's consistent with the current trust model.

🟨 One minor worth a look (not blocking)

  • ui_prompt.py action-shape footer omits the new target field and hard-codes "open a URL in a new tab". The LLM reads this footer as the authoritative action reference, so it gets no signal that same_tab exists → it will omit target (defaulting to new_tab) and the same_tab checkout-redirect use case this PR adds won't be reachable via LLM-generated templates. The target literal is shown in the Props/Example lines, but the dedicated reference block is stale. Suggest: {type:"open_url", url*: HttpUrl, target?: "new_tab"|"same_tab"} — open a URL (new_tab default; same_tab navigates the current page).

✅ What's good (verified)

  • Round-trips correctly model → catalog → wire; _extract_widget_config drops value (sets None) for redirect pills and passes action through to QuickReplyWire.
  • Backward compatible — all new fields are Optional with sane defaults; non-redirect pills keep the label-fallback behavior unchanged.
  • Tests genuinely assert the behavior (value is None + action is an OpenUrlAction, incl. the case where a value is set and must be dropped), not just that the field exists.

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