#15841 · @BYK · opened Mar 3, 2026 at 1:03 PM UTC · last updated Mar 21, 2026 at 10:24 AM UTC

fix(app): resize layout viewport when mobile keyboard appears

appfix
58
+444 files

Score breakdown

Impact

9.0

Clarity

7.0

Urgency

8.0

Ease Of Review

6.0

Guidelines

4.0

Readiness

5.0

Size

7.0

Trust

5.0

Traction

2.0

Summary

This PR addresses a critical mobile UX bug where the virtual keyboard obstructs the input composer. It introduces a viewport meta tag change to resize the layout viewport, but also includes unrelated API client changes.

Open in GitHub

Description

Issue for this PR

Closes #15842

Type of change

  • [x] Bug fix
  • [ ] New feature
  • [ ] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

The viewport meta tag defaults to interactive-widget=resizes-visual, which means the virtual keyboard overlays the visual viewport without resizing the layout viewport. Since the root element uses h-dvh and dvh tracks the layout viewport, the full flex layout stays at its original height behind the keyboard — pushing the composer (shrink-0 at the bottom of a flex-col) under it.

Adding interactive-widget=resizes-content tells the browser to resize the layout viewport (and thus the ICB) when the keyboard appears. dvh then shrinks to the space above the keyboard, the message timeline (flex-1) absorbs the reduction, and the composer stays visible.

Desktop is unaffected (no virtual keyboard = no viewport change). Browser support: Chrome 108+, Safari 15+, Edge 108+.

How did you verify your code works?

Tested on iOS Safari and Chrome Android — the composer stays visible above the keyboard after the change.

Screenshots / recordings

N/A

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

Linked Issues

#15842 Mobile keyboard covers prompt input in web UI

View issue

Comments

PR comments

BYK

@adamdotdevin any luck?

Changed Files

packages/app/index.html

+11
@@ -2,7 +2,7 @@
<html lang="en" style="background-color: var(--background-base)">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content" />
<title>OpenCode</title>
<link rel="icon" type="image/png" href="/favicon-96x96-v3.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon-v3.svg" />

packages/app/src/components/dialog-connect-provider.tsx

+11
@@ -383,7 +383,7 @@ export function DialogConnectProvider(props: { provider: string }) {
setFormStore("error", undefined)
await globalSDK.client.auth.set({
providerID: props.provider,
auth: {
body: {
type: "api",
key: apiKey,
},

packages/app/src/components/dialog-custom-provider.tsx

+11
@@ -131,7 +131,7 @@ export function DialogCustomProvider(props: Props) {
const auth = result.key
? globalSDK.client.auth.set({
providerID: result.providerID,
auth: {
body: {
type: "api",
key: result.key,
},

packages/opencode/src/cli/cmd/tui/component/dialog-provider.tsx

+11
@@ -265,7 +265,7 @@ function ApiMethod(props: ApiMethodProps) {
if (!value) return
await sdk.client.auth.set({
providerID: props.providerID,
auth: {
body: {
type: "api",
key: value,
},