diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7a7ea52..21243c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,15 @@
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
until 1.0, minor versions may contain breaking changes.
+## [Unreleased]
+
+### Fixed
+
+- **Phone: page stayed shifted after the keyboard closed** – the top bar (menu) was out of
+ reach. On Android the keyboard now shrinks the page instead of shifting it
+ (`interactive-widget=resizes-content`; the chat stays at its latest message); browsers that
+ still shift the page (iOS Safari) get it scrolled back once the keyboard closes.
+
## [v202611-00015] – 2026-11-10
### Fixed
diff --git a/web/index.html b/web/index.html
index b0226d0..a2943c9 100644
--- a/web/index.html
+++ b/web/index.html
@@ -2,7 +2,9 @@
-
+
+
diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx
index ad49a5a..0bda1f0 100644
--- a/web/src/components/ChatView.tsx
+++ b/web/src/components/ChatView.tsx
@@ -202,6 +202,17 @@ export function ChatView({ sessionId, onMenu, onClosePane }: { sessionId: string
const el = scrollRef.current;
if (el && stick.current) el.scrollTop = el.scrollHeight;
});
+ // … also when the pane gets smaller (on-screen keyboard opens) without a render.
+ const hasSession = !!session;
+ useEffect(() => {
+ const el = scrollRef.current;
+ if (!el || typeof ResizeObserver === "undefined") return;
+ const ro = new ResizeObserver(() => {
+ if (stick.current) el.scrollTop = el.scrollHeight;
+ });
+ ro.observe(el);
+ return () => ro.disconnect();
+ }, [hasSession, sessionId]);
if (loadError) return
{loadError}
;
if (!session) return
{t("common.loading")}
;
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 3ac17f7..8198779 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -10,6 +10,43 @@ import { captureShare } from "./share";
applyTheme();
captureShare();
registerSW({ immediate: true });
+undoKeyboardShift();
+
+/**
+ * Browsers that do not resize the page for the on-screen keyboard (iOS Safari, older
+ * Android) scroll the whole page up to show the input – and do not always scroll it back
+ * when the keyboard closes, leaving the top bar (menu) out of reach. The app never
+ * scrolls the page itself (only its panes), so any page offset is undone once the
+ * visible area grows again.
+ */
+function undoKeyboardShift() {
+ const vv = window.visualViewport;
+ if (!vv) return;
+ let last = vv.height;
+ const reset = () => {
+ const el = document.scrollingElement ?? document.documentElement;
+ if (window.scrollY !== 0 || el.scrollTop !== 0 || document.body.scrollTop !== 0) {
+ window.scrollTo(0, 0);
+ el.scrollTop = 0;
+ document.body.scrollTop = 0;
+ }
+ };
+ vv.addEventListener("resize", () => {
+ // Grown back (keyboard closed or closing): after the browser's own adjustment.
+ if (vv.height > last + 1) {
+ requestAnimationFrame(reset);
+ setTimeout(reset, 350);
+ }
+ last = vv.height;
+ });
+ // Keyboard closed by leaving the input (no resize on some browsers).
+ document.addEventListener("focusout", () =>
+ setTimeout(() => {
+ const a = document.activeElement;
+ if (!a || !(a instanceof HTMLInputElement || a instanceof HTMLTextAreaElement || (a as HTMLElement).isContentEditable)) reset();
+ }, 300),
+ );
+}
createRoot(document.getElementById("root")!).render(