Skip to content

fix(app): question options and submit controls are cut off on mobile #51770

Closed

Description

@ysm-dev

Description

In the V2 web app, long Question tool prompts or many options push Next and Submit below the mobile viewport. Long question text can also leave the options inaccessible. Users cannot finish answering the agent.

Reported on an iPhone 15 Pro and reproduced at a 393 × 852 browser viewport. The question and options should scroll while the navigation buttons stay visible.

Related reports: #23730 and #38286. Both are closed; #38286 was closed for inactivity. This still reproduces on v2.

Plugins

Reproduced with the production question component and fixture data; no plugin required.

OpenCode version

2.0.18, v2 at 0caae608a2

Steps to reproduce

  1. Open the V2 web app on an iPhone 15 Pro or use a 393 × 852 mobile viewport.
  2. Ask the agent to use the Question tool with 12 options, each with a long label and description.
  3. Try to reach the last option and tap Next or Submit.
  4. Repeat with question text longer than the screen height.

Screenshot and/or share link

Agent-browser capture of the production question component in a session story at 393 × 852. The footer is below the visible screen.

Question options extend below the mobile screen and Next is missing

Operating System

iOS on iPhone 15 Pro, reported by the user. Browser reproduction on macOS with mobile viewport emulation.

Terminal

Web app.

Activity

  1. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. ysm-dev commented on Sep 28, 2026

    @ysm-dev
    ContributorAuthor

    Fix and mobile regression coverage in #51771. Verified scrolling, navigation, and submission in Chromium and WebKit.

  3. Bearmancer commented on Sep 30, 2026

    @Bearmancer

    Data point from a real mobile browser against a remote instance, still reproducing on 2.0.20 (this report is 2.0.18 at 393x852).

    Client: Firefox for Android, Motorola Edge 70. Server: Windows 11, reached over tailscale serve (tailnet HTTPS proxying the loopback server port).

    Observed here: the option rows exist in the card but are unreachable — the card does not scroll, so Dismiss / Submit Ctrl+Enter stay visible while the options behind them cannot be brought into view or tapped. Same end state as reported (question cannot be answered on a phone), reached slightly differently: in this capture the buttons are visible and the options are the unreachable part.

    Worth noting for #51771: the fix was verified in Chromium and WebKit. This is a remote instance served over HTTPS to a phone browser, where the composer area is additionally squeezed by the mobile browser's own URL bar — if the scroll container is sized with vh rather than dvh, the bottom of the card can sit under the browser chrome and remain unreachable even when the container scrolls. Filed as #52264 with the screenshot; happy to close as duplicate if #51771 lands covering this path.

  4. opencodejr commented on Oct 28, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I reproduced it by rendering the production question dock at 393 × 852 (mobile emulation in headless Chromium) with test data: 12 options with long labels and descriptions, and a separate question with text taller than the screen.

    • On 2.0.18, both problems show. With many options the dock runs past the bottom of the screen and Next is off screen. With long question text the options box shrinks to about 17 px.
    • On the latest dev (86ce19d), the many-options case is fixed: the options scroll inside the dock and Next stays visible.
    • Long question text still breaks on dev. The options box collapses to 0 px, the question text doesn't scroll and runs under the Dismiss/Next buttons, and none of the options can be reached.

    The screenshots below show 2.0.18 with many options, dev with many options after scrolling, and dev with long question text. The script below takes the measurements.

    Related: #23730 and #38286, which you mentioned (both closed).

    Screenshot

    Screenshot

    Screenshot

    Reproduction script (check.mjs)
    // Needs packages/app/src/session/zz-repro-question.stories.tsx (12 long options; a 40-sentence question) and the
    // story server: `cd packages/stories && bunx vite --port 6006` on dev (on v2.0.18: storybook, BASE=http://127.0.0.1:6007 IFRAME=1).
    // Renders the production question dock in a 393x852 viewport and checks whether
    // the last option and the Next/Submit controls can be reached.
    import { chromium } from "/tmp/opencode/pw/node_modules/playwright/index.mjs"
    const base = process.env.BASE ?? "http://127.0.0.1:6006"
    const browser = await chromium.launch()
    const page = await browser.newPage({ viewport: { width: 393, height: 852 }, isMobile: true, hasTouch: true, deviceScaleFactor: 2 })
    for (const name of ["many-options", "long-text"]) {
      await page.goto(`${base}${process.env.IFRAME ? "/iframe.html" : "/"}?id=repro-question-mobile--${name}&viewMode=story`)
      const q = page.locator('[data-component="dock-prompt"][data-kind="question"]')
      await q.waitFor({ timeout: 60000 })
      await page.waitForTimeout(1500)
      await page.screenshot({ path: `/workspace/repro/${process.env.TAG ?? "dev"}-${name}-before.png` })
      const next = q.getByRole("button", { name: "Next", exact: true })
      const last = q.getByText("Option 12:", { exact: false }).first()
      const info = async () => ({
        dock: await q.evaluate((e) => { const r = e.getBoundingClientRect(); return { top: r.top, bottom: r.bottom } }),
        next: await next.evaluate((e) => { const r = e.getBoundingClientRect(); return { top: r.top, bottom: r.bottom } }),
        last: await last.evaluate((e) => { const r = e.getBoundingClientRect(); return { top: r.top, bottom: r.bottom } }),
        scrollers: await q.evaluate((e) => [...e.querySelectorAll("*")].filter((x) => x.scrollHeight > x.clientHeight + 2 && ["auto", "scroll"].includes(getComputedStyle(x).overflowY)).map((x) => `${x.tagName}.${x.className.toString().slice(0, 40)} ${x.clientHeight}/${x.scrollHeight}`)),
        page: await page.evaluate(() => ({ inner: innerHeight, docScroll: document.scrollingElement.scrollHeight })),
      })
      console.log(name, "initial", JSON.stringify(await info()))
      // try to reach the last option by scrolling it into view and by wheel-scrolling over the dock
      await last.scrollIntoViewIfNeeded().catch(() => {})
      await page.mouse.move(196, 600); for (let i = 0; i < 20; i++) await page.mouse.wheel(0, 400)
      await page.waitForTimeout(500)
      console.log(name, "after scroll", JSON.stringify(await info()))
      await page.screenshot({ path: `/workspace/repro/${process.env.TAG ?? "dev"}-${name}-after.png` })
    }
    await browser.close()

    Run it: bun check.mjs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions