From 6a62b068ff9357de913c24a4c45d6a92156cc289 Mon Sep 17 00:00:00 2001
From: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Date: Sun, 30 Aug 2026 09:18:52 +1000
Subject: [PATCH 1/2] fix(app): reserve the iOS PWA status bar
---
.../app/e2e/regression/pwa-navigation.spec.ts | 64 +++++++++++++++++++
packages/app/index.html | 3 +-
2 files changed, 66 insertions(+), 1 deletion(-)
create mode 100644 packages/app/e2e/regression/pwa-navigation.spec.ts
diff --git a/packages/app/e2e/regression/pwa-navigation.spec.ts b/packages/app/e2e/regression/pwa-navigation.spec.ts
new file mode 100644
index 000000000000..d20e825e1497
--- /dev/null
+++ b/packages/app/e2e/regression/pwa-navigation.spec.ts
@@ -0,0 +1,64 @@
+import { expect, test } from "@playwright/test"
+import { mockOpenCodeServer } from "../utils/mock-server"
+
+const directory = "C:/OpenCode/PwaNavigation"
+const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
+
+// Browser smoke coverage only: native status-bar hit testing requires an installed iOS PWA.
+for (const viewport of [
+ { width: 390, height: 844 },
+ { width: 844, height: 390 },
+ { width: 1280, height: 800 },
+]) {
+ test.describe(`${viewport.width}x${viewport.height}`, () => {
+ test.use({ viewport, hasTouch: true })
+
+ test("keeps home and new-session navigation usable", async ({ page }) => {
+ await mockOpenCodeServer(page, {
+ directory,
+ project: {
+ id: "proj_pwa_navigation",
+ worktree: directory,
+ vcs: "git",
+ name: "pwa-navigation",
+ time: { created: 1700000000000, updated: 1700000000000 },
+ sandboxes: [],
+ },
+ provider: { all: [], connected: [], default: {} },
+ sessions: [],
+ pageMessages: () => ({ items: [] }),
+ })
+ await page.addInitScript(
+ ({ directory, server }) => {
+ localStorage.setItem(
+ "opencode.global.dat:server",
+ JSON.stringify({
+ projects: { [server]: [{ worktree: directory, expanded: true }] },
+ lastProject: { [server]: directory },
+ }),
+ )
+ },
+ { directory, server },
+ )
+
+ await page.goto("/")
+ await expect(page.locator('meta[name="apple-mobile-web-app-status-bar-style"]')).toHaveAttribute(
+ "content",
+ "default",
+ )
+ const titlebar = page.locator('[data-slot="titlebar-v2"]')
+ const home = titlebar.getByRole("button", { name: "Home", exact: true })
+ const create = titlebar.getByRole("button", { name: "New session", exact: true })
+ await expect(home).toHaveAttribute("aria-pressed", "true")
+ await expect(page.getByText("pwa-navigation", { exact: true })).toBeVisible()
+ await expect(create).toBeInViewport({ ratio: 1 })
+ await create.tap()
+ await expect(page).toHaveURL(/\/new-session\?draftId=.+/)
+ await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
+ await expect(home).toHaveAttribute("aria-pressed", "false")
+ await home.tap()
+ await expect(page).toHaveURL(/\/$/)
+ await expect(home).toHaveAttribute("aria-pressed", "true")
+ })
+ })
+}
diff --git a/packages/app/index.html b/packages/app/index.html
index e07a15b4121e..4054d6c3f943 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -13,7 +13,8 @@
-
+
+
From 3b59db1abdc4153364dc3cf35a6ad1302032274b Mon Sep 17 00:00:00 2001
From: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Date: Sun, 30 Aug 2026 10:38:44 +1000
Subject: [PATCH 2/2] fix(app): inset PWA navigation below native chrome
---
.../app/e2e/regression/pwa-navigation.spec.ts | 71 +++++++++++++++----
packages/app/index.html | 3 +-
packages/app/src/shell/shell.tsx | 9 ++-
3 files changed, 68 insertions(+), 15 deletions(-)
diff --git a/packages/app/e2e/regression/pwa-navigation.spec.ts b/packages/app/e2e/regression/pwa-navigation.spec.ts
index d20e825e1497..26e2e33d21a8 100644
--- a/packages/app/e2e/regression/pwa-navigation.spec.ts
+++ b/packages/app/e2e/regression/pwa-navigation.spec.ts
@@ -4,16 +4,44 @@ import { mockOpenCodeServer } from "../utils/mock-server"
const directory = "C:/OpenCode/PwaNavigation"
const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}`
-// Browser smoke coverage only: native status-bar hit testing requires an installed iOS PWA.
-for (const viewport of [
- { width: 390, height: 844 },
- { width: 844, height: 390 },
- { width: 1280, height: 800 },
-]) {
- test.describe(`${viewport.width}x${viewport.height}`, () => {
- test.use({ viewport, hasTouch: true })
+const portrait = { width: 390, height: 844 }
+const empty = { top: 0, right: 0, bottom: 0, left: 0 }
- test("keeps home and new-session navigation usable", async ({ page }) => {
+// Insets and iOS standalone are emulated; native status-bar rendering still needs a device check.
+for (const input of [
+ { name: "browser", viewport: portrait, insets: empty, ios: false, standalone: false },
+ { name: "desktop", viewport: { width: 1280, height: 800 }, insets: empty, ios: false, standalone: false },
+ { name: "Android PWA", viewport: portrait, insets: { ...empty, top: 24, bottom: 24 }, ios: false },
+ { name: "iOS PWA", viewport: portrait, insets: { ...empty, top: 47, bottom: 34 }, ios: true },
+ { name: "iOS zero inset", viewport: portrait, insets: empty, ios: true },
+ { name: "iOS Dynamic Island", viewport: portrait, insets: { ...empty, top: 59, bottom: 34 }, ios: true },
+ {
+ name: "iOS bottom navigation",
+ viewport: portrait,
+ insets: { ...empty, top: 47, bottom: 34 },
+ ios: true,
+ bottom: true,
+ },
+ {
+ name: "iOS landscape RTL",
+ viewport: { width: 844, height: 390 },
+ insets: { top: 0, right: 47, bottom: 21, left: 47 },
+ ios: true,
+ rtl: true,
+ },
+].map((input) => ({ standalone: true, bottom: false, rtl: false, ...input }))) {
+ test.describe(input.name, () => {
+ test.use({ viewport: input.viewport, hasTouch: true })
+
+ test("keeps navigation outside the reserved edges and accepts taps", async ({ page, browserName }) => {
+ const insets = browserName === "chromium" ? input.insets : empty
+ if (browserName === "chromium") {
+ const cdp = await page.context().newCDPSession(page)
+ await cdp.send("Emulation.setSafeAreaInsetsOverride", { insets })
+ await cdp.send("Emulation.setEmulatedMedia", {
+ features: [{ name: "display-mode", value: input.standalone ? "standalone" : "browser" }],
+ })
+ }
await mockOpenCodeServer(page, {
directory,
project: {
@@ -29,7 +57,12 @@ for (const viewport of [
pageMessages: () => ({ items: [] }),
})
await page.addInitScript(
- ({ directory, server }) => {
+ ({ directory, server, ios, bottom }) => {
+ Object.defineProperty(navigator, "standalone", { value: ios })
+ localStorage.setItem(
+ "settings.v3",
+ JSON.stringify({ general: { mobileTitlebarPosition: bottom ? "bottom" : "top" } }),
+ )
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
@@ -38,20 +71,33 @@ for (const viewport of [
}),
)
},
- { directory, server },
+ { directory, server, ios: input.ios, bottom: input.bottom },
)
await page.goto("/")
await expect(page.locator('meta[name="apple-mobile-web-app-status-bar-style"]')).toHaveAttribute(
"content",
- "default",
+ "black-translucent",
)
+ if (input.rtl) await page.locator("html").evaluate((element) => element.setAttribute("dir", "rtl"))
+ const shell = page.locator('[data-slot="app-shell"]')
+ await expect(shell).toHaveCSS("padding-top", `${insets.top + (input.ios ? 32 : 0)}px`)
+ await expect(shell).toHaveCSS("padding-right", `${insets.right}px`)
+ await expect(shell).toHaveCSS("padding-bottom", `${insets.bottom}px`)
+ await expect(shell).toHaveCSS("padding-left", `${insets.left}px`)
const titlebar = page.locator('[data-slot="titlebar-v2"]')
const home = titlebar.getByRole("button", { name: "Home", exact: true })
const create = titlebar.getByRole("button", { name: "New session", exact: true })
await expect(home).toHaveAttribute("aria-pressed", "true")
await expect(page.getByText("pwa-navigation", { exact: true })).toBeVisible()
await expect(create).toBeInViewport({ ratio: 1 })
+ const bounds = await titlebar.boundingBox()
+ expect(bounds).not.toBeNull()
+ expect(bounds!.y).toBeGreaterThanOrEqual(insets.top + (input.ios ? 32 : 0))
+ expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(input.viewport.height - insets.bottom)
+ expect(bounds!.x).toBeGreaterThanOrEqual(insets.left)
+ expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(input.viewport.width - insets.right)
+ if (input.bottom) expect(bounds!.y + bounds!.height).toBe(input.viewport.height - insets.bottom - 1)
await create.tap()
await expect(page).toHaveURL(/\/new-session\?draftId=.+/)
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
@@ -59,6 +105,7 @@ for (const viewport of [
await home.tap()
await expect(page).toHaveURL(/\/$/)
await expect(home).toHaveAttribute("aria-pressed", "true")
+ expect(await titlebar.boundingBox()).toEqual(bounds)
})
})
}
diff --git a/packages/app/index.html b/packages/app/index.html
index 4054d6c3f943..e07a15b4121e 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -13,8 +13,7 @@
-
-
+
diff --git a/packages/app/src/shell/shell.tsx b/packages/app/src/shell/shell.tsx
index c97c80be449b..e2f33dd3edd5 100644
--- a/packages/app/src/shell/shell.tsx
+++ b/packages/app/src/shell/shell.tsx
@@ -39,10 +39,17 @@ export default function Layout(props: ParentProps) {
return (