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 <hello@gabrielgiordano.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-14 07:13:06 +02:00
committed by GitHub
co-authored by Gabriel Giordano
parent 4a3ff87eec
commit 019e5279e9
8 changed files with 95 additions and 1 deletions
@@ -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,
@@ -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
```
@@ -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
@@ -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<void> {
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 },
@@ -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: "<Tabs>\n <Tab />\n</Tabs>",
},
{ label: "Single page" },
],
multiSelect: false,
},
],
});
expect(questions?.[0]?.options).toEqual([
{ label: "Tabs", description: "One tab per section", preview: "<Tabs>\n <Tab />\n</Tabs>" },
{ label: "Single page", description: undefined, preview: undefined },
]);
});
});
@@ -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({
@@ -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 ? (
<Text style={optionDescriptionStyle}>{option.description}</Text>
) : 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.
<ScrollableCodeSurface
tone="surface0"
maxHeight={OPTION_PREVIEW_MAX_HEIGHT}
horizontal={false}
selectable={false}
textStyle={styles.optionPreviewText}
testID="question-form-option-preview"
>
{option.preview}
</ScrollableCodeSurface>
) : null}
</View>
</View>
</Pressable>
@@ -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,
@@ -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(<NewSurface />);",
"}",
...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,
},
{