Skip to content

web: on mobile, Enter submits the prompt and no newline can be inserted #55549

Closed

Description

@ns0116

Summary

On mobile browsers, pressing Enter in the web UI prompt submits the prompt instead of inserting a newline. Mobile software keyboards (Gboard/Android) have no usable Shift key, so there is no way to compose a multi-line prompt at all. This is still present in v2.0.29.

Environment

  • opencode version: 2.0.29 (npm global, latest)
  • OS: server on Windows 11 Pro (10.0.26300); client is Android + Chrome (Gboard)
  • Terminal: N/A — this happens in the web UI served by the opencode server, not the TUI
  • Shell: N/A (shell is not involved)
  • Install/channel: npm global, release channel
  • Active plugins: local ~/.config/opencode/plugins/ (session-sidebar, statusline, deny-ssh-when-delegated.js, orca-opencode-status.js) — none touch prompt input, so unrelated

Reproduction

  1. Run opencode so its server (web UI) is reachable from a phone.
  2. Open the web UI on Android Chrome and focus the prompt input.
  3. Type a line and press Enter on the virtual keyboard (no Shift available on mobile).
  4. Observe: the prompt is submitted immediately; a newline cannot be inserted.

Expected Behavior

On mobile, Enter should insert a newline so multi-line prompts can be composed, and submission should happen through the send button that is already present in the UI. (Desktop behavior — Enter submits, Shift+Enter inserts a newline — can stay as-is.)

Actual Behavior

Plain Enter submits the prompt. There is no keyboard combination available on a mobile soft keyboard (input.newline defaults to shift+return,ctrl+return,alt+return,ctrl+j), so a multi-line prompt is impossible.

Additional Context

Root cause appears to be in the web prompt input handler (packages/app/src/components/prompt-input.tsx, handleKeyDown): plain Enter is handled as submit unconditionally, with no mobile branch, while newline insertion is gated behind modifier keys that mobile keyboards cannot produce.

This looks like the same problem reported and closed-by-cleanup in #20965 (auto-closed as not planned on inactivity), and related to #15462, #16226 and the umbrella mobile issue #7990. #23430 proposes exposing prompt.submit / prompt.newline in the web Settings → Keyboard Shortcuts UI, which would let this be remapped; it is not merged, and v2.0.29 still behaves the same.

Notes on possible fixes:

  • cli.json keybinds are documented as terminal-only ("CLI settings control the terminal clients only"), and there is no enterToSend-style setting, so this cannot be worked around by config.

Workarounds users currently have to rely on: composing the prompt elsewhere and pasting it, using an external Bluetooth/USB keyboard, or using a third-party mobile client.

It would help to either (a) detect coarse-pointer/mobile input and make Enter insert a newline, or (b) surface prompt.submit / prompt.newline in the web settings so users can swap them.

Activity

  1. opencode-agent commented on Oct 14, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I can confirm this on v2.0.29, and it still happens on the latest dev (3943b84).

    What I did: I ran opencode serve and opened the web UI in Chromium emulating an Android phone (touch, mobile, pointer: coarse). In a session, I tapped the prompt, typed "first line", pressed Enter and typed "second line". Enter sent "first line" as a prompt right away, and the box was left holding only "second line". No newline was inserted. The script below reproduces it, and the screenshots show the result.

    The composer's key handler (packages/app/src/composer/editor/editor.tsx, onKeyDown) submits on any Enter without Shift, with no special case for touch or mobile.

    Related issues: #20965 (an earlier report, closed for inactivity), #43088 (configurable prompt shortcuts in the app) and #55341 (configurable Enter behaviour on desktop).

    Screenshot

    Screenshot

    Reproduction script (mobile-enter.mjs)
    // Reproduces anomalyco/opencode#55549: on a mobile (touch) browser, Enter in the
    // web composer submits the prompt instead of inserting a newline.
    // Usage: start `opencode serve --hostname 127.0.0.1 --port 4096` in a project dir,
    // then: OC_URL=http://127.0.0.1:4096 OC_PASSWORD=<server password> node mobile-enter.mjs
    import { chromium, devices } from "playwright"
    
    const url = process.env.OC_URL ?? "http://127.0.0.1:4096"
    const browser = await chromium.launch()
    const context = await browser.newContext({
      ...devices["Pixel 7"], // Android Chrome: touch, isMobile, coarse pointer
      httpCredentials: { username: "opencode", password: process.env.OC_PASSWORD ?? "" },
    })
    const page = await context.newPage()
    const prompts = []
    // Intercept prompt submissions so nothing reaches a model.
    await page.route(/\/api\/session\/[^/]+\/prompt/, (route) => {
      if (route.request().method() === "POST") {
        prompts.push(route.request().postData())
        return route.fulfill({ status: 200, contentType: "application/json", body: "{}" })
      }
      return route.continue()
    })
    
    if (process.env.OC_PAIR) {
      await page.goto(process.env.OC_PAIR) // one-time link from `opencode pair --url <url>`
    } else {
      await page.goto(url)
      // Connect with the server password via the "Use password" flow.
      await page.getByText("Use password").click()
      await page.waitForTimeout(500)
      const inputs = page.locator("input:visible")
      console.log("login inputs:", await inputs.count(), await page.locator("input:visible").evaluateAll((els) => els.map((e) => e.outerHTML)))
      for (const el of await inputs.all()) {
        const type = await el.getAttribute("type")
        const v = await el.inputValue()
        if (type === "password") await el.fill(process.env.OC_PASSWORD ?? "")
        else if (!v) await el.fill(url)
      }
      await page.screenshot({ path: "/workspace/repro/0-login.png" })
      await page.keyboard.press("Enter")
    }
    await page.waitForTimeout(4000)
    // Create a session through the API and open it.
    const auth = "Basic " + Buffer.from("opencode:" + (process.env.OC_PASSWORD ?? "")).toString("base64")
    const created = await fetch(new URL("/api/session?directory=" + encodeURIComponent(process.env.OC_DIR ?? process.cwd()), url), {
      method: "POST",
      headers: { authorization: auth, "content-type": "application/json" },
      body: "{}",
    }).then((r) => r.json())
    const sessionID = created.data.id
    await page.goto(new URL(`/server/${Buffer.from(url).toString("base64")}/session/${sessionID}`, url).href)
    await page.waitForTimeout(4000)
    await page.screenshot({ path: "/workspace/repro/0-loaded.png" })
    
    const editor = page.getByRole("textbox", { name: "Prompt", exact: true })
    await editor.waitFor({ timeout: 20000 })
    await editor.tap()
    await page.keyboard.type("first line")
    await page.screenshot({ path: "/workspace/repro/1-typed.png" })
    await page.keyboard.press("Enter") // the soft keyboard's Enter key
    await page.keyboard.type("second line")
    await page.waitForTimeout(1500)
    await page.screenshot({ path: "/workspace/repro/2-after-enter.png" })
    
    const text = await editor.innerText().catch(() => "<editor gone>")
    console.log("matchMedia(pointer: coarse):", await page.evaluate(() => matchMedia("(pointer: coarse)").matches))
    console.log("prompt POSTs after Enter:", prompts.length, prompts)
    console.log("editor text after Enter + 'second line':", JSON.stringify(text))
    console.log(
      prompts.length > 0 || !text.includes("first line")
        ? "BUG REPRODUCED: Enter submitted the prompt instead of inserting a newline"
        : "Not reproduced: Enter inserted a newline",
    )
    await browser.close()

    Run it: bun mobile-enter.mjs

  2. kanswer commented on Oct 18, 2026

    @kanswer

    I ran into this too and opened a PR: #56664

    Plain Enter now inserts a line break in the composer when the device reports no hover pointer (phones/tablets), while the send button submits the whole prompt. It uses the same insertion path as desktop Shift+Enter, so multi-line prompts come through as a single prompt; desktop and Mod+Enter behavior are unchanged.

  3. joelrbrandt commented on Nov 1, 2026

    @joelrbrandt

    Just a heads up: on iOS 27 Safari with the default iOS keyboard, new lines can be entered in the composer by toggling "shift" on and then tapping enter.

    It is possible that other iOS keyboards don't pass shift state through on sending the enter key.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions