From ab040885fb033de5aaf1b946f2e15c28b0caac89 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 12 Jun 2026 08:03:33 -0700 Subject: [PATCH] fix: stop iOS chat keyboard collapse by removing destructive resync The mobile chat keyboard would not reliably stay up on iOS. The cause was the visibilitychange/pageshow 'resync' effect in ChatView: it only ever ran while the composer was already focused (its activeElement guard), and on iOS a programmatic focus() fired from setTimeout has no user-gesture context, so it could not re-raise the keyboard after the blur(). Every spurious iOS visibilitychange (Control Center, notification banners, app switches) therefore dismissed the keyboard mid-session and never restored it. Remove the blur()+focus() resync. The visualViewport half-state it targeted is already handled by useMobileKeyboard, which re-snapshots vv metrics on visibilitychange/pageshow via its settle tail + rAF stability poll without ever touching textarea focus. --- .../dashboard/app/components/ChatView.tsx | 52 ++++++------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index 800e2ead83..aadd154295 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -1675,43 +1675,23 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView }; }, [isMobile, keyboardOpen]); - // On page restore, if iOS thinks the keyboard is up but the textarea - // isn't actually focused (or vice versa), the visualViewport metrics get - // stuck in a half-state — composer pushed up or covered by a blank pane. - // Force a blur+refocus on the textarea to make iOS resync. + // NOTE: a previous iOS-only "resync" effect here force-blurred and + // re-focused the active textarea on visibilitychange/pageshow to nudge + // iOS out of a stuck visualViewport half-state (composer pushed up / + // blank pane). It was removed because it was the cause of the iOS + // "keyboard won't stay up" bug: the effect only ever ran while the + // composer was already focused (its `document.activeElement !== ta` + // guard), and on iOS a programmatic focus() fired from setTimeout has + // no user-gesture context, so it cannot re-raise the keyboard after the + // blur(). In practice it never resynced the keyboard up — it only + // dismissed it whenever iOS emitted a visibilitychange (Control Center, + // notification banners, app switches, etc.) mid-session. // - // This is an iOS-only quirk fix and must stay iOS-only: on Android a - // programmatic focus() after blur() does NOT re-raise the soft keyboard - // (Android only opens the keyboard from a real user gesture), so running - // this there permanently collapses the keyboard whenever a spurious - // visibilitychange fires — including the ones Android browsers emit - // mid-keyboard-transition while the user is typing. We also only resync - // when the document is actually becoming visible; visibilitychange also - // fires on hide, where a blur+refocus is pointless and disruptive. Only - // runs when ChatView holds the active session (avoids stealing focus - // from other views). - useEffect(() => { - if (!isMobile || !activeSession || !isIOS()) return; - const resync = () => { - const ta = inputRef.current; - if (!ta) return; - if (document.activeElement !== ta) return; // only if it was focused - ta.blur(); - window.setTimeout(() => { - ta.focus({ preventScroll: true }); - }, 0); - }; - const resyncOnVisible = () => { - if (document.visibilityState !== "visible") return; - resync(); - }; - document.addEventListener("visibilitychange", resyncOnVisible); - window.addEventListener("pageshow", resync); - return () => { - document.removeEventListener("visibilitychange", resyncOnVisible); - window.removeEventListener("pageshow", resync); - }; - }, [isMobile, activeSession]); + // The visualViewport half-state it targeted is now owned by + // useMobileKeyboard, which re-snapshots vv metrics on + // visibilitychange/pageshow via its settle tail + rAF stability poll — + // without ever touching textarea focus. Do not reintroduce a + // blur()+focus() resync here. useEffect(() => { const previousScope = previousChatScopeRef.current;