From 8136abd48424235eb507b71ff55fdda180e50006 Mon Sep 17 00:00:00 2001 From: "[1000i100] Millicent Billette" Date: Fri, 21 Aug 2026 13:56:33 +0200 Subject: [PATCH 1/3] feat(chat): show checkboxes on multi-select question options A multi-select question looked exactly like a single-select one: the same row of options, the same trailing check on whatever was picked last. Nothing on screen said "you may pick several", so users answered in free text instead of selecting. Multi-select options now carry a leading checkbox and keep their number shortcut on screen while the answer is assembled; single-select keeps its trailing check, so the two modes no longer look alike. Both clients render it: web option buttons expose `role="checkbox"` with `aria-checked`, and the mobile pills get the matching `accessibilityRole`/`accessibilityState`. Co-Authored-By: Claude Opus 5 (1M context) --- .../features/threads/PendingUserInputCard.tsx | 34 ++++++++++++- .../ComposerPendingUserInputPanel.test.tsx | 48 +++++++++++++++++-- .../chat/ComposerPendingUserInputPanel.tsx | 32 ++++++++++++- 3 files changed, 109 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index ddb625f9b219..158021395fa9 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -88,6 +88,7 @@ const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200); export function PendingUserInputCard(props: PendingUserInputCardProps) { const iconSubtle = useThemeColor("--color-icon-subtle"); + const checkboxTint = useThemeColor("--color-primary-foreground"); const questionCount = props.pendingUserInput.questions.length; const cardCoverage = props.cardCoverage; @@ -263,8 +264,17 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { return ( + {/* A pill that can stay lit alongside its neighbours needs + to say so before it is tapped: the box marks the answer + as additive rather than exclusive. */} + {question.multiSelect ? ( + + {selected ? ( + + ) : null} + + ) : null} = {}, +) { return renderToStaticMarkup( {}} onAdvance={() => {}} @@ -58,4 +78,26 @@ describe("ComposerPendingUserInputPanel", () => { expect(markup).toContain("Incremental"); expect(markup).toContain("Big bang"); }); + + it("announces multi-select options as checkboxes and keeps their shortcuts", () => { + const markup = renderPanel(multiSelectPrompt, { + "question-2": { selectedOptionLabels: ["Web"] }, + }); + + const optionButtons = markup.match(/]*role="checkbox"[^>]*>/g) ?? []; + expect(optionButtons).toHaveLength(2); + expect(optionButtons[0]).toContain('aria-checked="true"'); + expect(optionButtons[1]).toContain('aria-checked="false"'); + // The trailing number shortcut survives selection, so every option stays + // reachable from the keyboard while the answer is being assembled. + expect(markup).toContain(" { + const markup = renderPanel(prompt, { "question-1": { selectedOptionLabels: ["Incremental"] } }); + + expect(markup).not.toContain('role="checkbox"'); + expect(markup).not.toContain("Select one or more options."); + }); }); diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index d50a0c264549..239d98020950 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -242,13 +242,37 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( ); const content = ( <> + {/* Multi-select is the only mode where more than one option + can stay lit, so it is the only one that gets a box: a + leading checkbox reads as "toggle me", where the trailing + check of a single-select reads as "answered". */} + {activeQuestion.multiSelect ? ( + + ) : null}
{option.label} {option.description && option.description !== option.label ? ( {option.description} ) : null}
- {isSelected ? ( + {/* Multi-select keeps every number shortcut on screen while + options are being picked: the checkbox already carries the + state, and the keys stay live for the whole list. */} + {isSelected && !activeQuestion.multiSelect ? ( ) : shortcutKey !== null ? ( { handleOptionSelection(activeQuestion.id, option.label); From 61f002e5218def1dad61f76f01bf869515eddeca Mon Sep 17 00:00:00 2001 From: "[1000i100] Millicent Billette" Date: Fri, 21 Aug 2026 14:44:58 +0200 Subject: [PATCH 2/3] docs(pr): add before/after screenshots for #7803 Co-Authored-By: Claude Opus 5 (1M context) --- .github/pr-assets/7803-after-dark.svg | 1 + .github/pr-assets/7803-after-light.svg | 1 + .github/pr-assets/7803-before-dark.svg | 1 + .github/pr-assets/7803-before-light.svg | 1 + 4 files changed, 4 insertions(+) create mode 100644 .github/pr-assets/7803-after-dark.svg create mode 100644 .github/pr-assets/7803-after-light.svg create mode 100644 .github/pr-assets/7803-before-dark.svg create mode 100644 .github/pr-assets/7803-before-light.svg diff --git a/.github/pr-assets/7803-after-dark.svg b/.github/pr-assets/7803-after-dark.svg new file mode 100644 index 000000000000..10473a0c9c13 --- /dev/null +++ b/.github/pr-assets/7803-after-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/pr-assets/7803-after-light.svg b/.github/pr-assets/7803-after-light.svg new file mode 100644 index 000000000000..a04da67be9ed --- /dev/null +++ b/.github/pr-assets/7803-after-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/pr-assets/7803-before-dark.svg b/.github/pr-assets/7803-before-dark.svg new file mode 100644 index 000000000000..37bfdec8b44a --- /dev/null +++ b/.github/pr-assets/7803-before-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.github/pr-assets/7803-before-light.svg b/.github/pr-assets/7803-before-light.svg new file mode 100644 index 000000000000..58826b81ed71 --- /dev/null +++ b/.github/pr-assets/7803-before-light.svg @@ -0,0 +1 @@ + \ No newline at end of file From 2c66fffe149abb9fb84ab43674dd022aadd42674 Mon Sep 17 00:00:00 2001 From: "[1000i100] Millicent Billette" Date: Fri, 21 Aug 2026 15:14:37 +0200 Subject: [PATCH 3/3] fix(chat): mount the multi-select check only when the option is picked Hiding the glyph with `text-transparent` breaks down in forced-colors mode: the OS overrides both the text colour and `bg-primary`, so every unchecked box would render a visible check and read as selected. Mounting it conditionally matches the shared checkbox primitive and the mobile pill in this same change. Co-Authored-By: Claude Opus 5 (1M context) --- .../components/chat/ComposerPendingUserInputPanel.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx index 239d98020950..8a7b9fd79526 100644 --- a/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx +++ b/apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx @@ -257,10 +257,15 @@ const ComposerPendingUserInputCard = memo(function ComposerPendingUserInputCard( // borrows the shortcut key's ink rather than the // hairline border token, which fades to ~4% white in // dark mode and disappears. - "border-secondary-label/70 bg-background/35 text-transparent group-hover:border-foreground", + "border-secondary-label/70 bg-background/35 group-hover:border-foreground", )} > - + {/* Mounted only when checked, like the shared checkbox and + the mobile pill: a glyph hidden by `text-transparent` + comes back in forced-colors mode, where the OS + overrides both the text and the background, and every + empty box would then read as selected. */} + {isSelected ? : null} ) : null}