+
{(file) => {
From 77522697b0648b2b0a5eff4c78b101df048efebd Mon Sep 17 00:00:00 2001
From: "opencode-agent[bot]"
Date: Tue, 23 Jun 2026 09:00:19 +0000
Subject: [PATCH 12/15] chore: generate
---
packages/app/src/pages/session.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index b797a854d2b..21b42be48a4 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1622,9 +1622,7 @@ export default function Page() {
"shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
}}
>
-
- {mobileTabs(true)}
-
+ {mobileTabs(true)}
From b0063709e6107be835569c103024cec3d8ce6dcf Mon Sep 17 00:00:00 2001
From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Date: Tue, 23 Jun 2026 17:29:22 +0800
Subject: [PATCH 13/15] feat(app): add mobile bottom navigation (#32797)
---
packages/app/index.html | 5 +++-
.../src/components/settings-v2/general.tsx | 16 +++++++++++
packages/app/src/components/titlebar.tsx | 22 ++++++++++++---
packages/app/src/context/settings.tsx | 9 +++++++
packages/app/src/i18n/en.ts | 3 +++
packages/app/src/pages/session.tsx | 27 +++++++++++++++----
6 files changed, 72 insertions(+), 10 deletions(-)
diff --git a/packages/app/index.html b/packages/app/index.html
index 8c86360af3d..ad49b255324 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -2,7 +2,10 @@
-
+
OpenCode
diff --git a/packages/app/src/components/settings-v2/general.tsx b/packages/app/src/components/settings-v2/general.tsx
index 93caa646739..8a230cae684 100644
--- a/packages/app/src/components/settings-v2/general.tsx
+++ b/packages/app/src/components/settings-v2/general.tsx
@@ -1,4 +1,5 @@
import { Component, Show, createMemo, createResource, onMount } from "solid-js"
+import { createMediaQuery } from "@solid-primitives/media"
import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2"
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
@@ -89,6 +90,7 @@ export const SettingsGeneralV2: Component = () => {
const dialog = useDialog()
const params = useParams()
const settings = useSettings()
+ const mobile = createMediaQuery("(max-width: 767px)")
const updater = useUpdaterAction()
@@ -345,6 +347,20 @@ export const SettingsGeneralV2: Component = () => {
/>
+
+
+
+
+ settings.general.setMobileTitlebarPosition(checked ? "bottom" : "top")}
+ />
+
+
+
)
diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx
index 3312856391f..f350ba183e6 100644
--- a/packages/app/src/components/titlebar.tsx
+++ b/packages/app/src/components/titlebar.tsx
@@ -35,6 +35,7 @@ import { displayName, getProjectAvatarSource, projectForSession } from "@/pages/
import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
+import { createMediaQuery } from "@solid-primitives/media"
import { readSessionTabsRemovedDetail, SESSION_TABS_REMOVED_EVENT } from "@/components/titlebar-session-events"
import { useGlobal } from "@/context/global"
import { ServerConnection, useServer } from "@/context/server"
@@ -87,6 +88,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const location = useLocation()
const params = useParams()
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
+ const mobile = createMediaQuery("(max-width: 767px)")
+ const bottom = createMemo(
+ () => useV2Titlebar() && mobile() && settings.general.mobileTitlebarPosition() === "bottom",
+ )
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
@@ -98,6 +103,10 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1)
const minHeight = () => {
const height = useV2Titlebar() ? v2TitlebarHeight : legacyTitlebarHeight
+ if (useV2Titlebar() && mobile()) {
+ const inset = bottom() ? "env(safe-area-inset-bottom, 0px)" : "env(safe-area-inset-top, 0px)"
+ return `calc(${height}px + ${inset})`
+ }
if (mac()) return `${height / zoom()}px`
if (windows()) return `${height / Math.min(titlebarZoom(), 1)}px`
return undefined
@@ -235,10 +244,13 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
"shrink-0 relative flex flex-row": true,
"h-9 bg-v2-background-bg-deep overflow-visible": useV2Titlebar(),
"h-10 bg-background-base overflow-hidden": !useV2Titlebar(),
+ "order-last": bottom(),
}}
style={{
"min-height": minHeight(),
- "padding-left": mac() ? `${84 / zoom()}px` : 0,
+ "padding-top": useV2Titlebar() && mobile() && !bottom() ? "env(safe-area-inset-top, 0px)" : undefined,
+ "padding-bottom": bottom() ? "env(safe-area-inset-bottom, 0px)" : undefined,
+ "padding-left": mac() && !mobile() ? `${84 / zoom()}px` : 0,
width: electronWindows() ? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))` : undefined,
"max-width": electronWindows()
? `env(titlebar-area-width, calc(100vw - ${windowsControlsWidth()}))`
@@ -426,10 +438,12 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
return (
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index 21c845d0d1e..2b7520da12a 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -33,6 +33,7 @@ export interface Settings {
editToolPartsExpanded: boolean
showSessionProgressBar: boolean
showCustomAgents: boolean
+ mobileTitlebarPosition: "top" | "bottom"
newLayoutDesigns?: boolean
}
appearance: {
@@ -118,6 +119,7 @@ const defaultSettings: Settings = {
editToolPartsExpanded: false,
showSessionProgressBar: true,
showCustomAgents: false,
+ mobileTitlebarPosition: "top",
},
appearance: {
fontSize: 14,
@@ -248,6 +250,13 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setShowCustomAgents(value: boolean) {
setStore("general", "showCustomAgents", value)
},
+ mobileTitlebarPosition: withFallback(
+ () => store.general?.mobileTitlebarPosition,
+ defaultSettings.general.mobileTitlebarPosition,
+ ),
+ setMobileTitlebarPosition(value: "top" | "bottom") {
+ setStore("general", "mobileTitlebarPosition", value)
+ },
newLayoutDesigns,
setNewLayoutDesigns(value: boolean) {
setStore("general", "newLayoutDesigns", value)
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 3d9f37ca73b..1baa5e40da1 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -841,6 +841,9 @@ export const dict = {
"settings.general.row.showTerminal.description": "Show the terminal button in the desktop title bar",
"settings.general.row.showStatus.title": "Server status",
"settings.general.row.showStatus.description": "Show the server status button in the title bar",
+ "settings.general.row.mobileTitlebarBottom.title": "Bottom navigation",
+ "settings.general.row.mobileTitlebarBottom.description":
+ "Place the title bar and session tabs at the bottom of the screen on mobile",
"settings.general.row.showCustomAgents.title": "Custom agents",
"settings.general.row.showCustomAgents.description": "Show the agent picker in the composer",
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 21b42be48a4..badc2ae8541 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -1568,12 +1568,20 @@ export default function Page() {
/>
)
- const mobileTabs = (compact = false) => (
+ const mobileTabs = (compact = false, bottom = false) => (
-
+
setStore("mobileTab", "session")}
>
@@ -1581,7 +1589,10 @@ export default function Page() {
setStore("mobileTab", "changes")}
>
@@ -1592,6 +1603,9 @@ export default function Page() {
)
+ const mobileTabsBottom = createMemo(
+ () => !isDesktop() && settings.general.newLayoutDesigns() && settings.general.mobileTitlebarPosition() === "bottom",
+ )
return (
@@ -1622,7 +1636,9 @@ export default function Page() {
"shadow-[var(--v2-elevation-raised)]": settings.general.newLayoutDesigns() && !!params.id,
}}
>
-
{mobileTabs(true)}
+
+ {mobileTabs(true)}
+
@@ -1688,6 +1704,7 @@ export default function Page() {
{composerRegion("dock")}
+
{mobileTabs(true, true)}
From fc95a84b42f909e4202a070978fc939e4be7a6ee Mon Sep 17 00:00:00 2001
From: "opencode-agent[bot]"
Date: Tue, 23 Jun 2026 09:30:58 +0000
Subject: [PATCH 14/15] chore: generate
---
packages/app/src/components/titlebar.tsx | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx
index f350ba183e6..8144aec66ac 100644
--- a/packages/app/src/components/titlebar.tsx
+++ b/packages/app/src/components/titlebar.tsx
@@ -89,9 +89,7 @@ export function Titlebar(props: { update?: TitlebarUpdate }) {
const params = useParams()
const useV2Titlebar = createMemo(() => settings.general.newLayoutDesigns())
const mobile = createMediaQuery("(max-width: 767px)")
- const bottom = createMemo(
- () => useV2Titlebar() && mobile() && settings.general.mobileTitlebarPosition() === "bottom",
- )
+ const bottom = createMemo(() => useV2Titlebar() && mobile() && settings.general.mobileTitlebarPosition() === "bottom")
const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos")
const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows")
From a21e74773f281b1f414a543a5584b36585d28f30 Mon Sep 17 00:00:00 2001
From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
Date: Tue, 23 Jun 2026 17:59:30 +0800
Subject: [PATCH 15/15] fix(app): improve iOS PWA shell (#32798)
---
packages/app/index.html | 11 +++++++----
packages/app/public/oc-theme-preload.js | 3 ++-
packages/ui/src/assets/favicon/site.webmanifest | 7 +++++--
packages/ui/src/theme/context.tsx | 3 ++-
4 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/packages/app/index.html b/packages/app/index.html
index ad49b255324..d71535182c8 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -1,5 +1,5 @@
-
+
-
+
+
+
+
-
+
-
+
diff --git a/packages/app/public/oc-theme-preload.js b/packages/app/public/oc-theme-preload.js
index 18846fceb6b..11c9b39ec4c 100644
--- a/packages/app/public/oc-theme-preload.js
+++ b/packages/app/public/oc-theme-preload.js
@@ -15,10 +15,11 @@
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
+ document.documentElement.style.backgroundColor = isDark ? "#080808" : "#fafafa"
// Update theme-color meta tag to match app color scheme
var metas = document.querySelectorAll("meta[name='theme-color']")
- if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#131010" : "#F8F7F7")
+ if (metas.length > 0) metas[0].setAttribute("content", isDark ? "#080808" : "#fafafa")
if (themeId === "oc-2") return
diff --git a/packages/ui/src/assets/favicon/site.webmanifest b/packages/ui/src/assets/favicon/site.webmanifest
index 41290e840c3..427c3adf423 100644
--- a/packages/ui/src/assets/favicon/site.webmanifest
+++ b/packages/ui/src/assets/favicon/site.webmanifest
@@ -1,6 +1,9 @@
{
"name": "OpenCode",
"short_name": "OpenCode",
+ "id": "/",
+ "start_url": "/",
+ "scope": "/",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
@@ -15,7 +18,7 @@
"purpose": "maskable"
}
],
- "theme_color": "#ffffff",
- "background_color": "#ffffff",
+ "theme_color": "#080808",
+ "background_color": "#080808",
"display": "standalone"
}
diff --git a/packages/ui/src/theme/context.tsx b/packages/ui/src/theme/context.tsx
index a1505a0e39d..1051345fbe2 100644
--- a/packages/ui/src/theme/context.tsx
+++ b/packages/ui/src/theme/context.tsx
@@ -152,10 +152,11 @@ function applyThemeCss(theme: DesktopTheme, themeId: string, mode: "light" | "da
ensureThemeStyleElement().textContent = fullCss
document.documentElement.dataset.theme = themeId
document.documentElement.dataset.colorScheme = mode
+ document.documentElement.style.backgroundColor = isDark ? "#080808" : "#fafafa"
// Update theme-color meta tag to match light/dark mode
const meta = document.querySelector('meta[name="theme-color"]')
- if (meta) meta.setAttribute("content", isDark ? "#131010" : "#F8F7F7")
+ if (meta) meta.setAttribute("content", isDark ? "#080808" : "#fafafa")
}
function cacheThemeVariants(theme: DesktopTheme, themeId: string) {