From 019e5279e9c453bce5692a9a8f656c5fcbb13cb7 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:13:06 +0200 Subject: [PATCH] Show Claude option previews in the question card, readable on Android (#6171) * Show Claude option previews in the question card * Wrap long lines in question option previews * Check option previews under the option that owns them * Show the whole option preview on native so every line is reachable On Android, a slow drag inside a preview capped at 240px did not scroll it, so lines past the cap could not be read. The preview's nested scroll sits inside the option's Pressable; the same drag scrolls it once the preview is outside the Pressable. Native now renders the preview at full height so the chat scroll reaches every line, and a tap on it still selects the option. Web keeps the 240px cap. The mock agent's rollout preview is now taller than the cap so the Android replay can check the last line. --------- Co-authored-by: Gabriel Giordano Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../question-prompt-pagination.spec.ts | 5 ++++ ...e-question-option-preview-preconditions.md | 7 ++++++ .../native-question-option-preview.android.ad | 10 ++++++++ packages/app/e2e/support/helpers/questions.ts | 9 +++++++ .../question-form-card-core.test.ts | 25 +++++++++++++++++++ .../src/components/question-form-card-core.ts | 2 ++ .../app/src/components/question-form-card.tsx | 22 ++++++++++++++++ .../agent/providers/mock-load-test-agent.ts | 16 +++++++++++- 8 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 packages/app/e2e/mobile/agent-device/native-question-option-preview-preconditions.md create mode 100644 packages/app/e2e/mobile/agent-device/native-question-option-preview.android.ad diff --git a/packages/app/e2e/browser/question-prompt-pagination.spec.ts b/packages/app/e2e/browser/question-prompt-pagination.spec.ts index b014b2d1cd..1d75ae2839 100644 --- a/packages/app/e2e/browser/question-prompt-pagination.spec.ts +++ b/packages/app/e2e/browser/question-prompt-pagination.spec.ts @@ -7,6 +7,7 @@ import { expectQuestionDismissEnabled, expectQuestionHidden, expectQuestionNavigationEnabled, + expectQuestionOptionPreview, expectQuestionOptionSelected, expectQuestionPrimaryActionDisabled, expectQuestionPrimaryActionEnabled, @@ -61,6 +62,10 @@ test.describe("Question prompt pagination", () => { await expectQuestionOptionSelected(page, "App"); await openQuestion(page, { index: 2, total: TOTAL_QUESTIONS }); + await expectQuestionOptionPreview(page, { + option: "Behind feature flag", + preview: 'flags.enabled("new-surface")', + }); await chooseQuestionOption(page, "Behind feature flag"); await expectCurrentQuestion(page, { index: 3, diff --git a/packages/app/e2e/mobile/agent-device/native-question-option-preview-preconditions.md b/packages/app/e2e/mobile/agent-device/native-question-option-preview-preconditions.md new file mode 100644 index 0000000000..ac920aa0e7 --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-question-option-preview-preconditions.md @@ -0,0 +1,7 @@ +# Native question option preview + +`native-question-option-preview.android.ad` checks that every line of a long option preview is reachable on Android. Before replay, open a mock agent prompted with `emit synthetic questions` so its question card shows the first question, on a phone-sized emulator (validated on a 1080×1920 API 35 AVD). The flow opens the `rollout` question, drags slowly up from inside the "Behind feature flag" preview, and asserts the preview's last line is visible. + +```sh +agent-device replay packages/app/e2e/mobile/agent-device/native-question-option-preview.android.ad --platform android --serial emulator-5554 +``` diff --git a/packages/app/e2e/mobile/agent-device/native-question-option-preview.android.ad b/packages/app/e2e/mobile/agent-device/native-question-option-preview.android.ad new file mode 100644 index 0000000000..f6cd42bbab --- /dev/null +++ b/packages/app/e2e/mobile/agent-device/native-question-option-preview.android.ad @@ -0,0 +1,10 @@ +context platform=android timeout=60000 retries=0 +env APP_ID=sh.paseo.debug + +open "${APP_ID}" +wait "label=\"rollout\"" 20000 +press "label=\"rollout\"" +wait "id=\"question-form-option-preview\"" 10000 +gesture pan 570 1120 0 -440 600 +is visible "text=\"// LAST PREVIEW LINE\"" +close diff --git a/packages/app/e2e/support/helpers/questions.ts b/packages/app/e2e/support/helpers/questions.ts index ebe13aead5..a26bd9a975 100644 --- a/packages/app/e2e/support/helpers/questions.ts +++ b/packages/app/e2e/support/helpers/questions.ts @@ -43,6 +43,15 @@ export async function expectQuestionOptionSelected(page: Page, option: string): await expect(questionOption(page, option)).toHaveAttribute("aria-checked", "true"); } +export async function expectQuestionOptionPreview( + page: Page, + input: { option: string; preview: string }, +): Promise { + await expect( + questionOption(page, input.option).getByTestId("question-form-option-preview"), + ).toContainText(input.preview); +} + export async function openQuestion( page: Page, input: { index: number; total: number }, diff --git a/packages/app/src/components/question-form-card-core.test.ts b/packages/app/src/components/question-form-card-core.test.ts index 40692d4acf..293bf24ba8 100644 --- a/packages/app/src/components/question-form-card-core.test.ts +++ b/packages/app/src/components/question-form-card-core.test.ts @@ -147,4 +147,29 @@ describe("question form card core", () => { Response: "custom", }); }); + + test("keeps option previews so options can be compared", () => { + const questions = parseQuestionFormQuestions({ + questions: [ + { + question: "Which layout?", + header: "Layout", + options: [ + { + label: "Tabs", + description: "One tab per section", + preview: "\n \n", + }, + { label: "Single page" }, + ], + multiSelect: false, + }, + ], + }); + + expect(questions?.[0]?.options).toEqual([ + { label: "Tabs", description: "One tab per section", preview: "\n \n" }, + { label: "Single page", description: undefined, preview: undefined }, + ]); + }); }); diff --git a/packages/app/src/components/question-form-card-core.ts b/packages/app/src/components/question-form-card-core.ts index 0dea6c706e..78007dba2e 100644 --- a/packages/app/src/components/question-form-card-core.ts +++ b/packages/app/src/components/question-form-card-core.ts @@ -1,6 +1,7 @@ export interface QuestionOption { label: string; description?: string; + preview?: string; } export interface QuestionFormQuestion { @@ -46,6 +47,7 @@ export function parseQuestionFormQuestions(input: unknown): QuestionFormQuestion options.push({ label: o.label, description: typeof o.description === "string" ? o.description : undefined, + preview: readOptionalString(o, "preview"), }); } questions.push({ diff --git a/packages/app/src/components/question-form-card.tsx b/packages/app/src/components/question-form-card.tsx index 5b3b146f4e..655e01d210 100644 --- a/packages/app/src/components/question-form-card.tsx +++ b/packages/app/src/components/question-form-card.tsx @@ -10,6 +10,7 @@ import type { AgentPermissionResponse } from "@getpaseo/protocol/agent-types"; import { isWeb } from "@/constants/platform"; import { EditingTextInput as TextInput } from "@/components/ui/text-input"; import type { EditingTextInputHandle } from "@/components/ui/text-input/types"; +import { ScrollableCodeSurface } from "@/components/ui/scrollable-code-surface"; import { areQuestionsAnswered, buildQuestionFormAnswers, @@ -29,6 +30,9 @@ interface QuestionFormCardProps { } const IS_WEB = isWeb; +// Native previews are uncapped: inside the option's Pressable, Android never hands a +// drag to the preview's nested scroll, so lines past a cap would be unreachable. +const OPTION_PREVIEW_MAX_HEIGHT = IS_WEB ? 240 : undefined; function getQuestionInputPlaceholder({ question, @@ -133,6 +137,20 @@ function QuestionOptionRow({ {option.description ? ( {option.description} ) : null} + {option.preview ? ( + // Shown for every option at once so they can be compared before picking. + // Long lines wrap, as in Claude Code, since most previews are prose. + + {option.preview} + + ) : null} @@ -704,6 +722,10 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.base, lineHeight: 20, }, + optionPreviewText: { + lineHeight: 20, + ...(isWeb ? { whiteSpace: "pre-wrap", overflowWrap: "break-word" } : null), + }, selectionControl: { width: 18, height: 18, diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.ts index 14733e0601..ce9714c5f2 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.ts @@ -232,6 +232,7 @@ type SteeringReplayShape = "claude" | "codex"; interface MockQuestionOption { label: string; description?: string; + preview?: string; } interface MockQuestionPromptQuestion { @@ -271,6 +272,16 @@ function parseSettledAssistantImageMarkdown(prompt: AgentPromptInput): string | return match?.[1] ?? null; } +const ROLLOUT_FLAG_PREVIEW = [ + "Ship it dark behind the new-surface flag, then turn it on for internal users before everyone else.", + "", + 'if (flags.enabled("new-surface")) {', + " render();", + "}", + ...Array.from({ length: 40 }, (_, i) => `// rollout step ${i + 1}`), + "// LAST PREVIEW LINE", +].join("\n"); + function parseMockQuestionPrompt(prompt: AgentPromptInput): MockQuestionPromptRequest | null { const text = promptToText(prompt); if (!/emit\s+(?:a\s+)?synthetic\s+questions?/i.test(text)) { @@ -309,7 +320,10 @@ function parseMockQuestionPrompt(prompt: AgentPromptInput): MockQuestionPromptRe { question: "Which rollout should we use?", header: "rollout", - options: [{ label: "Immediately" }, { label: "Behind feature flag" }], + options: [ + { label: "Immediately" }, + { label: "Behind feature flag", preview: ROLLOUT_FLAG_PREVIEW }, + ], multiSelect: false, }, {