diff --git a/packages/app/index.html b/packages/app/index.html index 8c86360af..ad49b2553 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 93caa6467..8a230cae6 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 331285639..f350ba183 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 21c845d0d..2b7520da1 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 3d9f37ca7..1baa5e40d 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 21b42be48..badc2ae85 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)}