From 733f138dae2f50293dfbadb9c1b5ebe3daa91959 Mon Sep 17 00:00:00 2001
From: "opencode-agent[bot]"
<219766164+opencode-agent[bot]@users.noreply.github.com>
Date: Mon, 7 Sep 2026 13:41:44 +1000
Subject: [PATCH] fix(app): remove bottom safe area while keyboard is open
(#47712)
---
packages/app/src/entry.tsx | 2 ++
packages/app/src/runtime/platform/keyboard.ts | 33 +++++++++++++++++++
packages/app/src/shell/mobile-drawer.css | 2 +-
packages/app/src/shell/shell.tsx | 15 ++++++---
packages/app/src/shell/titlebar/titlebar.tsx | 6 ++--
5 files changed, 50 insertions(+), 8 deletions(-)
create mode 100644 packages/app/src/runtime/platform/keyboard.ts
diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx
index 22ab7eb514f..cc866e12a3a 100644
--- a/packages/app/src/entry.tsx
+++ b/packages/app/src/entry.tsx
@@ -7,6 +7,7 @@ import { loadInitialLocale } from "@/runtime/i18n/language"
import { PlatformProvider } from "@/runtime/platform/platform"
import { createWebPlatform } from "@/runtime/platform/web"
import { isStandalone, PwaRoutePersistence, restorePwaRoute } from "@/runtime/platform/pwa"
+import { KeyboardInsets } from "@/runtime/platform/keyboard"
import en from "@/runtime/i18n/en"
import zh from "@/runtime/i18n/zh"
import { authFromToken } from "@/runtime/server/api"
@@ -92,6 +93,7 @@ if (root instanceof HTMLElement && root.dataset.opencodeMounted === undefined) {
canonicalLocalServer={ServerConnection.key(server)}
servers={[server]}
>
+
{standalone && }
diff --git a/packages/app/src/runtime/platform/keyboard.ts b/packages/app/src/runtime/platform/keyboard.ts
new file mode 100644
index 00000000000..cd065005426
--- /dev/null
+++ b/packages/app/src/runtime/platform/keyboard.ts
@@ -0,0 +1,33 @@
+import { onCleanup, onMount } from "solid-js"
+
+export function KeyboardInsets() {
+ onMount(() => {
+ const viewport = window.visualViewport
+ if (!viewport) return
+ const root = document.documentElement
+ const sync = () => {
+ const active = document.activeElement
+ const editing =
+ active instanceof HTMLElement &&
+ (active.isContentEditable || active.matches("input:not([readonly]), textarea:not([readonly])"))
+ // iOS retains the home-indicator inset above its keyboard. Ignore pinch zoom
+ // and small viewport changes from browser chrome, not just editor focus.
+ const keyboard = editing && root.clientHeight - viewport.height * viewport.scale > 100
+ if (keyboard) root.style.setProperty("--safe-area-inset-bottom", "0px")
+ if (!keyboard) root.style.removeProperty("--safe-area-inset-bottom")
+ }
+ sync()
+ viewport.addEventListener("resize", sync)
+ window.addEventListener("resize", sync)
+ document.addEventListener("focusin", sync)
+ document.addEventListener("focusout", sync)
+ onCleanup(() => {
+ viewport.removeEventListener("resize", sync)
+ window.removeEventListener("resize", sync)
+ document.removeEventListener("focusin", sync)
+ document.removeEventListener("focusout", sync)
+ root.style.removeProperty("--safe-area-inset-bottom")
+ })
+ })
+ return null
+}
diff --git a/packages/app/src/shell/mobile-drawer.css b/packages/app/src/shell/mobile-drawer.css
index 34e7577fd7d..255e3e087c8 100644
--- a/packages/app/src/shell/mobile-drawer.css
+++ b/packages/app/src/shell/mobile-drawer.css
@@ -19,7 +19,7 @@
display: flex;
flex-direction: column;
max-height: min(75dvh, calc(100dvh - env(safe-area-inset-top, 0px) - 16px));
- padding: 0 12px max(12px, env(safe-area-inset-bottom, 0px));
+ padding: 0 12px max(12px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px)));
padding-left: max(12px, env(safe-area-inset-left, 0px));
padding-right: max(12px, env(safe-area-inset-right, 0px));
border-radius: 16px 16px 0 0;
diff --git a/packages/app/src/shell/shell.tsx b/packages/app/src/shell/shell.tsx
index f8a98cfd84a..d02237ab73e 100644
--- a/packages/app/src/shell/shell.tsx
+++ b/packages/app/src/shell/shell.tsx
@@ -47,7 +47,9 @@ export default function Layout(props: ParentProps) {
: platform.platform === "desktop" && platform.os === "windows"
? "1px"
: "8px",
- "--shell-bottom-inset": bottomTitlebar() ? "8px" : "max(0px, calc(8px - env(safe-area-inset-bottom, 0px)))",
+ "--shell-bottom-inset": bottomTitlebar()
+ ? "8px"
+ : "max(0px, calc(8px - var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))",
}}
>
diff --git a/packages/app/src/shell/titlebar/titlebar.tsx b/packages/app/src/shell/titlebar/titlebar.tsx
index c7f52036d27..83a4b5c62b7 100644
--- a/packages/app/src/shell/titlebar/titlebar.tsx
+++ b/packages/app/src/shell/titlebar/titlebar.tsx
@@ -155,11 +155,11 @@ export function Titlebar(props: {
height:
platform.platform === "web"
? bottom()
- ? "calc(28px + max(8px, env(safe-area-inset-bottom, 0px)))"
+ ? "calc(28px + max(8px, var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))))"
: "calc(28px + max(8px, env(safe-area-inset-top, 0px)))"
: undefined,
"padding-top": bottom() ? "0px" : "env(safe-area-inset-top, 0px)",
- "padding-bottom": bottom() ? "env(safe-area-inset-bottom, 0px)" : "0px",
+ "padding-bottom": bottom() ? "var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px))" : "0px",
"min-height": minHeight(),
// Keep native macOS traffic lights clear even when the desktop window is narrow.
"padding-left": macTrafficLights() ? `${macTrafficLightsBaseWidth / zoom()}px` : 0,
@@ -440,7 +440,7 @@ export function Titlebar(props: {
class="h-full flex-1 overflow-hidden flex flex-row items-center gap-1.5 px-2 md:pe-3"
classList={{
"pt-[max(0px,calc(8px-env(safe-area-inset-top,0px)))]": !bottom() && !windows(),
- "pb-[max(0px,calc(8px-env(safe-area-inset-bottom,0px)))]": bottom(),
+ "pb-[max(0px,calc(8px-var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px))))]": bottom(),
"pl-4": macTrafficLights(),
// Center the 20px app icon over the sidebar's 16px icon column.
"ps-3.5": windows(),