= (props) => {
e.currentTarget.value = ""
}}
/>
-
+
-
@@ -1654,7 +1654,7 @@ export const PromptInput: Component
= (props) => {
disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
- class="h-6 w-4.5"
+ class="size-8 md:h-6 md:w-4.5"
/>
diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx
index 28741098c8e..2514ca930fb 100644
--- a/packages/app/src/entry.tsx
+++ b/packages/app/src/entry.tsx
@@ -4,6 +4,11 @@ import { AppBaseProviders, AppInterface } from "@/app"
import { Platform, PlatformProvider } from "@/context/platform"
import pkg from "../package.json"
+// Register service worker for PWA support
+if ("serviceWorker" in navigator && import.meta.env.PROD) {
+ navigator.serviceWorker.register("/sw.js").catch(() => {})
+}
+
const root = document.getElementById("root")
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error(
diff --git a/packages/app/src/hooks/use-virtual-keyboard.ts b/packages/app/src/hooks/use-virtual-keyboard.ts
new file mode 100644
index 00000000000..40f2479d466
--- /dev/null
+++ b/packages/app/src/hooks/use-virtual-keyboard.ts
@@ -0,0 +1,68 @@
+import { createSignal, onCleanup, onMount } from "solid-js"
+
+// Minimum height difference to consider keyboard visible (accounts for browser chrome changes)
+const KEYBOARD_VISIBILITY_THRESHOLD = 150
+
+export function useVirtualKeyboard() {
+ const [height, setHeight] = createSignal(0)
+ const [visible, setVisible] = createSignal(false)
+
+ onMount(() => {
+ // Initialize CSS property to prevent stale values from previous mounts
+ document.documentElement.style.setProperty("--keyboard-height", "0px")
+
+ // Use visualViewport API if available (iOS Safari 13+, Chrome, etc.)
+ const viewport = window.visualViewport
+ if (!viewport) return
+
+ // Track baseline height, reset on orientation change
+ let baselineHeight = viewport.height
+
+ const updateBaseline = () => {
+ // Only update baseline when keyboard is likely closed (viewport near window height)
+ // This handles orientation changes correctly
+ if (Math.abs(viewport.height - window.innerHeight) < 100) {
+ baselineHeight = viewport.height
+ }
+ }
+
+ const handleResize = () => {
+ const currentHeight = viewport.height
+ const keyboardHeight = Math.max(0, baselineHeight - currentHeight)
+
+ // Consider keyboard visible if it takes up more than threshold
+ const isVisible = keyboardHeight > KEYBOARD_VISIBILITY_THRESHOLD
+
+ // If keyboard just closed, update baseline for potential orientation change
+ if (!isVisible && visible()) {
+ baselineHeight = currentHeight
+ }
+
+ setHeight(keyboardHeight)
+ setVisible(isVisible)
+
+ // Update CSS custom property for use in styles
+ document.documentElement.style.setProperty("--keyboard-height", `${keyboardHeight}px`)
+ }
+
+ // Handle orientation changes - reset baseline after orientation settles
+ const handleOrientationChange = () => {
+ // Delay to let viewport settle after orientation change
+ setTimeout(updateBaseline, 300)
+ }
+
+ viewport.addEventListener("resize", handleResize)
+ window.addEventListener("orientationchange", handleOrientationChange)
+
+ onCleanup(() => {
+ viewport.removeEventListener("resize", handleResize)
+ window.removeEventListener("orientationchange", handleOrientationChange)
+ document.documentElement.style.removeProperty("--keyboard-height")
+ })
+ })
+
+ return {
+ height,
+ visible,
+ }
+}
diff --git a/packages/app/src/index.css b/packages/app/src/index.css
index e40f0842b15..f72c8214634 100644
--- a/packages/app/src/index.css
+++ b/packages/app/src/index.css
@@ -1,6 +1,12 @@
@import "@opencode-ai/ui/styles/tailwind";
:root {
+ /* Safe area insets for notched devices (iPhone X+) */
+ --safe-area-inset-top: env(safe-area-inset-top, 0px);
+ --safe-area-inset-right: env(safe-area-inset-right, 0px);
+ --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
+ --safe-area-inset-left: env(safe-area-inset-left, 0px);
+
a {
cursor: default;
}
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index cffefd5634d..8ead4815d41 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -76,6 +76,11 @@ export default function Layout(props: ParentProps) {
let scrollContainerRef: HTMLDivElement | undefined
const xlQuery = window.matchMedia("(min-width: 1280px)")
+ const mdQuery = window.matchMedia("(min-width: 768px)")
+ const [isMobile, setIsMobile] = createSignal(!mdQuery.matches)
+ const handleMdChange = (e: MediaQueryListEvent) => setIsMobile(!e.matches)
+ mdQuery.addEventListener("change", handleMdChange)
+ onCleanup(() => mdQuery.removeEventListener("change", handleMdChange))
const [isLargeViewport, setIsLargeViewport] = createSignal(xlQuery.matches)
const handleViewportChange = (e: MediaQueryListEvent) => setIsLargeViewport(e.matches)
xlQuery.addEventListener("change", handleViewportChange)
@@ -852,29 +857,50 @@ export default function Layout(props: ParentProps) {
const status = sessionStore.session_status[props.session.id]
return status?.type === "busy" || status?.type === "retry"
})
+
return (
- <>
-
)
}
@@ -1005,7 +1032,13 @@ export default function Layout(props: ParentProps) {
-
+ layout.mobileSidebar.hide()}
+ />
@@ -1032,6 +1065,7 @@ export default function Layout(props: ParentProps) {