Repository navigation
@W-24333476 - Add opener question pills (pdp-questions attribute) - #14
Merged
sf-samarth-urs merged 10 commits intoOct 5, 2026
Merged
sf-samarth-urs merged 10 commits into
sf-samarth-urs merged 10 commits into
Conversation
chintandesai-cimulate
left a comment
Collaborator
There was a problem hiding this comment.
overall LGTM, tests are missing for other classes, can we add them
Render a pill row above the input on PDPs when the host provides a JSON- encoded array of opener questions via the new `pdp-questions` attribute (SCAPI `c_pdpQuestions` is the intended source). Tapping a pill routes through the same `sendMessage` path as typed input, so product-context prefixing, readiness gating, and welcome suppression all apply uniformly. Pills auto-hide once the shopper has engaged (asked a question or seen an error) and re-appear on SPA-navigation between PDPs via the existing `RESET_CONVERSATION` path, when the host swaps the list. - `parsePdpQuestions` — defensive JSON parse, non-array/non-string reject, trim, dedupe, cap at MAX_PDP_QUESTIONS (5). Never throws. - `<QuestionPills>` — accessible list-of-buttons, `prepareConnection` on hover/focus to hide handshake latency behind pill intent. - New CSS class + 4 themable `--iaw-pill-*` custom properties. - 9 new unit tests, all 142 existing tests still green, clean typecheck, UMD + ESM bundles build.
Hover is a bystander signal: a shopper scrolling past the pills has no intent to use Ask Assistant, but prepareConnection on mouseEnter would still spin up a SCRT2 session that sits unused against the ~11k concurrent session cap. Rely on sendMessage's own ensureConnected guard so the first click does the handshake inline, same as InputBar on a cold start.
Addresses Chintan's PR review: new/changed reducer branches for the
opener-pills work had no direct coverage. Adds 20 cases hitting:
- SET_PDP_QUESTIONS (new) — replaces list, accepts [], no cross-field
bleed into conversation state
- RESET_CONVERSATION (modified) — preserves pdpQuestions so SPA
PDP→PDP nav doesn't flicker the pills, while clearing conversation
state and leaving status/connectionReady alone
- ASK_QUESTION — pill-click doesn't wipe sibling pills
- SET_CONNECTION_READY — gates the pill `disabled` binding
- SET_ERROR — clears conversation on error, preserves on error:null
- plus smoke for SET_STATUS, APPEND_STREAMING_TOKEN, SET_ANSWER,
SET_AGENT_TYPING and the unknown-action default branch
Match the Alexa for shopping / Shiseido-Sandbox north-star interaction model: the pre-generated opener-question menu is a persistent surface so the shopper can ask one question, read the answer, then ask another from the same list without having to retype anything or navigate away. Previously the pills hid entirely once the shopper engaged, which forced them to type manually or SPA-navigate away and back to see the remaining questions. - Drop the `hasEngaged` gate — pills stay rendered as long as the product has a non-empty pdp-questions list. - Disable all pills while a question is in-flight (agentTyping or streamingText present) so the shopper can't stack sends; re-enables on SET_ANSWER / SET_ERROR. - Mark the clicked pill with `aria-pressed` and style it in the inverted accent color so the shopper can see which pill produced the answer below. Selection is derived from state.lastQuestion so it clears naturally on RESET_CONVERSATION (SPA PDP→PDP nav) and stays empty when the shopper free-types. Follow-up questions (dynamic from pageContext) and routing pill clicks into the main-chat window are deliberate phase-2 items per PRD and not part of this change.
The agent emits an auto-welcome the moment a conversation is created. We drop that welcome via hasUserSentRef + a skip-ticket so the UI doesn't surface it before the shopper has engaged. The ticket was a plain boolean (`welcomePendingRef`), so sendMessage never waited on it — on a cold first send (first pill click, or first Send on an untyped InputBar), if the agent emitted the REAL answer before the welcome, the first Chatbot `message` event was eaten as "the welcome" and the shopper saw nothing. Repro from screenshots: 1st pill click → user bubble, 45s of silence, no SSE answer; 2nd click → full streamed answer. The InputBar hid this because the shopper spent seconds typing, which gave the welcome time to arrive and clear the ticket before Send fired. Pills had no warm window. Fix: replace the boolean with an awaitable gate. sendMessage now awaits the gate between ensureConnected() and client.sendMessage(). The gate closes on any definitive signal — welcome dropped, streaming token started, 5s welcome-timeout, SESSION_EXPIRED, or remount cleanup — so the user POST is held only until the welcome has unambiguously been resolved. Warm sends see pending=false and skip the await in one microtask, so there's zero latency hit on later clicks or typed sends. Factored into src/provider/welcome-gate.ts to match the architecture of ensure-connected.ts, with 12 unit tests covering the three states, the multi-waiter + re-open cycles, and the sendMessage-race contract. All previously-passing tests continue to pass (174/174).
sf-samarth-urs
force-pushed
the
feature/pdp-opener-pills
branch
from
October 5, 2026 09:39
f34ca4d to
83ff845
Compare
The clicked pill is already visible and highlighted above the answer, so echoing the same text as a <p class="query"> below it is noise. Chintan flagged this in the PRD review (shoe PDP design keeps pills on top + answer below, no echo). Fix: in Response.tsx, derive cameFromPill = state.pdpQuestions.includes( state.lastQuestion) and gate the echo on !cameFromPill. Typed messages still echo (matches the conversational pattern shoppers expect for freeform input). A typed message whose text happens to match a pill verbatim is also suppressed — acceptable edge case. No new tests: this is a one-line pure-view derivation on existing state; the repo doesn't yet have a jsdom + testing-library setup for component tests, and dragging those in for a single conditional isn't earning anything. Reducer tests already cover lastQuestion / pdpQuestions state transitions.
… drain shelf
Aligns the PDP widget to the Shiseido-Sandbox design and fixes two
rough edges called out in local testing:
- Layout: InputBar → Response → QuestionPills. Pills now live BELOW
the answer (not above it), and the "which question am I looking at?"
acknowledgement is a right-aligned dark pill-shaped user bubble —
not a highlighted pill. Removes the duplicate "clicked pill + echoed
query text" that read as repetitive.
- InputBar placeholder templates the current PDP product name
("Ask me anything about <productName>") when the host passes one.
Explicit `placeholder` config still wins; generic fallback preserved
for non-PDP contexts. productName sourced from SCAPI's `name` field.
- Pill shelf drains: a clicked pill is hidden (askedQuestions filter)
so the shopper never looks at a question whose answer is already
above them. Remaining pills stay available until exhausted; shelf
then renders nothing and the InputBar carries the shopper forward
until Shilpi's follow-up pills land in a later PR.
- Welcome-gate fix: streaming_token handler no longer closes the
welcome gate pre-send. Agentforce streams the welcome message too,
so welcome tokens were slipping the gate open and letting the
welcome's final `message` event flash on screen before being
overwritten. Now gated on hasUserSentRef.
- Dead-air fix on cold first click: dispatch SET_AGENT_TYPING on send
intent (not on SSE `typing_started`). The ~1-2s handshake window
was previously silent; the dots are the shopper's "I heard you"
acknowledgement. Cleared on all error-return paths.
6 new reducer tests cover askedQuestions behavior (append, dedup,
accumulate across sends, clear on RESET_CONVERSATION, initial state).
Follow-ups parked for the next PR with Shilpi: regenerate pill +
dynamically-generated follow-up questions sourced from the latest
answer.
The welcome-gate was a send-side guard only: it held the user POST
until the welcome resolved OR the 5s safety timeout fired. If
Agentforce was slow enough that the welcome `message` arrived AFTER
the timeout AND after the user POST, the receive-side handler saw
hasUserSentRef=true + gate.pending=false and happily rendered the
welcome ("Welcome to NTO!") as if it were the real answer.
Reproduced in an incognito cold load — screenshot in PR.
Fix: a durable `welcomeDroppedRef` that outlives the gate's 5s
timeout. The message handler now treats the FIRST Chatbot message
as the welcome regardless of gate state, flipping the ref so
subsequent Chatbot messages render as answers. streaming_token
mirrors the same guard so welcome tokens from a slow agent don't
paint streamingText as the answer either.
- New ref initialized false on every fresh client; reset false on
SESSION_EXPIRED and on sendMessage's retry prelude
- Pre-set true on successful tryRestore (no welcome on restore)
- Flipped true the first time any Chatbot message is consumed,
pre-send OR post-send
Trade-off: if Agentforce ever ships the real answer BEFORE the
welcome (SSE out-of-order), we'd drop the real answer. In practice
Agentforce queues welcome first, so this is safe. If that ever
breaks, we add a lastEventId / correlation check.
180/180 tests still green (existing suite covers the send-side
gate; the receive-side flag is pure WidgetProvider ref logic).
…DK errors Two bugs observed in an incognito cold load with a slow Agentforce: 1. Welcome arrives in a specific window — after the 5s send-side gate timeout fires (gate closes, user POST goes out) but BEFORE the user POST completes (hasUserSentRef still false). Prior iteration only flipped welcomeDroppedRef when the gate was still pending; this window missed, welcomeDroppedRef stayed false, then the real answer arriving post-send was treated as "the welcome" and swallowed. Fix: pre-send, ANY Chatbot message is the welcome — no gate-state dependency. Flip welcomeDroppedRef unconditionally in that branch; do the gate-close + SET_CONNECTION_READY only if the gate is still pending (otherwise it's already handled by the timeout). The post-send branch is unchanged — it catches the case where the welcome arrives even later, after the POST completes. 2. The error handler was dispatching SET_ERROR for every SDK error, including recoverable ones (SSE reconnects, transient drops). The SET_ERROR reducer branch clears lastQuestion + answer, so a mid-flight reconnect visibly swallowed the shopper's bubble and the in-flight answer with a red "Something went wrong" flash. The SDK was going to recover silently anyway. Fix: only surface SET_ERROR when e.recoverable === false. Log recoverable errors when enableLogging is on. sendMessage()'s own retry path still catches terminal failures of the user's POST. 180/180 tests still green. Follow-up: WidgetProvider.tsx is not currently exercised by any test (no jsdom/testing-library). Welcome- gate wiring is subtle enough that this bug keeps biting; test harness for Provider-level handshake flow is the clearest remaining gap.
…module
The welcome-greeting classification logic has broken twice in two days
across three fix iterations — once leaking the welcome as the answer,
once swallowing the real answer as "the welcome". Each time the bug
was in a window the previous hand-rolled branch didn't cover. The
logic itself is simple (3 timing windows); what makes it brittle is
that it was expressed as nested `if`s inside a long-lived SSE
handler, with no test harness around it.
Fix the shape, not just the symptom:
- New module `src/provider/welcome-tracker.ts` holds the entire
decision table as two pure functions — `classifyMessage` for SSE
`message` events, `shouldAppendStreamingToken` for the token
stream. Both take plain booleans; no DOM, no SDK, no refs.
- 12 unit tests in `tests/provider/welcome-tracker.test.ts` cover
every documented window. The two regressions from this week each
have a named test ("drops a Chatbot message AFTER the gate has
already timed out", "drops a Chatbot message as the (late)
welcome, not the answer") so a future refactor that reintroduces
either bug fails loudly.
- `WidgetProvider.tsx` now translates classifier verdicts into
ref writes + reducer dispatches. The handler is the thin edge;
behavior is the module. Semantics unchanged from the last fix.
Known coverage gap called out separately: WidgetProvider.tsx itself
is still untested (no jsdom harness). This refactor doesn't close
that gap, but it moves the hardest-to-reason-about logic out of
WidgetProvider and into a module that IS fully covered.
192/192 tests green (was 180; +12 welcome-tracker).
sf-samarth-urs
merged commit Oct 5, 2026
c90ec6b
into
SalesforceCommerceCloud:main
1 of 2 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opener question pills UI to the inline agent widget, driven by a new
pdp-questionsattribute on the custom element. On a PDP where the merchant has published questions (via Content Studio → SCAPIc_pdpQuestions), the widget renders up to 5 clickable pills, aligned to the Shiseido-Sandbox north-star design.North-star layout
InputBar → Response → QuestionPills. The pill shelf lives BELOW the answer."Ask me anything about <productName>"(via a newproduct-nameattribute).askedQuestionsfilter); when empty the shelf disappears and the InputBar carries the shopper forward.RESET_CONVERSATIONclearsaskedQuestions; the new product's full shelf is shown.Welcome-handoff state machine
Agentforce auto-emits a "welcome" greeting before the shopper sends anything; the widget must drop it so an idle PDP shows no unsolicited bot text. The handoff spans several timing windows (fast agent, slow agent, gate-timeout) and misidentifying the welcome leaks it as the answer OR drops the real answer as "the welcome". The rules are extracted into a pure classifier module (
src/provider/welcome-tracker.ts) covered by 12 unit tests — one per window, two named for regressions caught during verification.WidgetProvider.tsxis a thin dispatcher over the classifier verdict. Separately, recoverable SDK reconnect errors no longer reachSET_ERROR(which was clearinglastQuestion+answerand causing the red "Something went wrong" flash).Contract
pdp-questions— JSON-encoded string array, e.g.'["Q1?","Q2?"]'. Parser trims, drops empties, caps at 5 (MAX_PDP_QUESTIONS); invalid input → empty list.product-name— plain string; absent → generic fallback.Companion PR in
pwa-kit: @W-24333476 - Add opener question pills to inline PDP widget (plumbsc_pdpQuestions+namethrough the PDP shim into these attributes).Changes
src/provider/pdp-questions.ts—parsePdpQuestions+MAX_PDP_QUESTIONS = 5src/provider/welcome-gate.ts— awaitableWelcomeGatestate machine (send-side guard)src/provider/welcome-tracker.ts— pure classifier for inbound Agentforce events (classifyMessage,shouldAppendStreamingToken)src/provider/types.ts—productName?,pdpQuestions?onWidgetConfig;pdpQuestions,askedQuestionsonWidgetState;SET_PDP_QUESTIONSactionsrc/provider/reducer.ts—ASK_QUESTIONdedup-appends toaskedQuestions,RESET_CONVERSATIONclears it,SET_PDP_QUESTIONScasesrc/provider/WidgetProvider.tsx— gate wiring +welcomeDroppedRef;message/streaming_tokendispatch off classifier verdicts;errorsurfaces only non-recoverable errors;SET_AGENT_TYPINGon send intentsrc/custom-element.ts— observespdp-questions+product-namesrc/components/InputBar.tsx— placeholder precedence:config.placeholder > product-name template > genericsrc/components/QuestionPills.tsx— filtersaskedQuestions; no selection/highlightsrc/components/Response.tsx— right-aligned user bubble markupsrc/App.tsx— layout:InputBar → Response → QuestionPillssrc/styles.css—.user-bubble+.user-bubble-row; drops.pill[aria-pressed]selected state;--iaw-user-bubble-*theme varspdp-questions.test.ts(9),welcome-gate.test.ts(12),welcome-tracker.test.ts(12),reducer.test.tsupdatesTest plan
npm test— 192 tests passnpm run build— clean build; UMD 346 KB / 108 KB gzip<name>"askedQuestionsclearspdp-questions: safe fallbackFollow-ups (not in this PR)
inline_pdp_pill_click