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, }, {