+ {/* On a narrow workspace view the footer bar takes over the bottom inset: this box clips
+ overflow, so nothing can paint into its padding and the reserved band reads as dead space
+ under the bar. Every other route keeps the inset here because none of them ends in a
+ control row that could reserve its own. The sidebar is position: fixed with its own inset
+ at these widths. */}
+
) : selectedWorkspace ? (
(() => {
- const currentMetadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
+ const currentMetadata = activeWorkspaceMetadata;
// Guard: Don't render AIView if workspace metadata not found.
// This can happen when selectedWorkspace (from localStorage) refers to a
// deleted workspace, or during a race condition on reload before the
From 411632987c884e3cacf71f5d836df93da0567c2a Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Wed, 29 Jul 2026 14:03:56 +0000
Subject: [PATCH 4/5] fix: hand the mobile bottom inset to the chat column, not
the route
---
src/browser/App.tsx | 25 ++++---------------
src/browser/components/ChatPane/ChatPane.tsx | 4 +++
.../stories/App.phoneViewports.stories.tsx | 5 ++++
src/browser/styles/globals.css | 8 ++++++
4 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/src/browser/App.tsx b/src/browser/App.tsx
index da20e8ce97..60acce138a 100644
--- a/src/browser/App.tsx
+++ b/src/browser/App.tsx
@@ -2,7 +2,6 @@ import { useEffect, useCallback, useRef, useState } from "react";
import { useRouter } from "./contexts/RouterContext";
import { useLocation, useNavigate } from "react-router-dom";
import "./styles/globals.css";
-import { cn } from "@/common/lib/utils";
import { useWorkspaceContext, toWorkspaceSelection } from "./contexts/WorkspaceContext";
import { useProjectContext } from "./contexts/ProjectContext";
import type { WorkspaceSelection } from "./components/ProjectSidebar/ProjectSidebar";
@@ -1238,13 +1237,6 @@ function AppInner() {
};
}, [setSelectedWorkspace, workspaceStore]);
- // Non-null only while the workspace view is the visible route, which is also the only route whose
- // last row (WorkspaceFooterBar) can own the bottom safe-area inset.
- const activeWorkspaceMetadata =
- !isAnalyticsOpen && !currentSettingsSection && selectedWorkspace
- ? workspaceMetadata.get(selectedWorkspace.workspaceId)
- : undefined;
-
// Show auth modal if authentication is required
if (status === "auth_required") {
return (
@@ -1259,17 +1251,10 @@ function AppInner() {
return (
<>
- {/* On a narrow workspace view the footer bar takes over the bottom inset: this box clips
- overflow, so nothing can paint into its padding and the reserved band reads as dead space
- under the bar. Every other route keeps the inset here because none of them ends in a
- control row that could reserve its own. The sidebar is position: fixed with its own inset
- at these widths. */}
-
+ {/* mobile-bottom-inset-host: narrow layouts give this box's bottom inset up to a column that
+ reserves its own clearance, keyed off the column rather than the route so surfaces without
+ one still get it here. The sidebar is position: fixed with its own inset at these widths. */}
+
) : selectedWorkspace ? (
(() => {
- const currentMetadata = activeWorkspaceMetadata;
+ const currentMetadata = workspaceMetadata.get(selectedWorkspace.workspaceId);
// Guard: Don't render AIView if workspace metadata not found.
// This can happen when selectedWorkspace (from localStorage) refers to a
// deleted workspace, or during a race condition on reload before the
diff --git a/src/browser/components/ChatPane/ChatPane.tsx b/src/browser/components/ChatPane/ChatPane.tsx
index 169f1947ac..d658d5d92b 100644
--- a/src/browser/components/ChatPane/ChatPane.tsx
+++ b/src/browser/components/ChatPane/ChatPane.tsx
@@ -300,6 +300,10 @@ export const ChatPane: React.FC
= (props) => {
0) {
+ throw new Error("Immersive review left the bottom safe-area inset unowned.");
+ }
},
{ timeout: 10_000 }
);
diff --git a/src/browser/styles/globals.css b/src/browser/styles/globals.css
index 51f090be4a..69e2ad0ae8 100644
--- a/src/browser/styles/globals.css
+++ b/src/browser/styles/globals.css
@@ -1203,6 +1203,14 @@ body,
padding-left: 0 !important;
}
+ /* Hand the bottom inset to a column that reserves its own clearance (see ChatPane and
+ WorkspaceFooterBar). This box clips overflow, so a child can never paint into its padding, and
+ the reserved band reads as dead space under the footer bar. Surfaces without such a column
+ (immersive review, settings, creation, loading shells) keep the inset here. */
+ .mobile-bottom-inset-host:has([data-bottom-inset-owner]) {
+ padding-bottom: 0 !important;
+ }
+
.mobile-overlay {
display: block !important;
}
From 7cbdb293b0850b6b298920fd741c3b0b4400fbfd Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Wed, 29 Jul 2026 14:13:11 +0000
Subject: [PATCH 5/5] tests: pin the phone viewport global on the PR placement
story
---
src/browser/stories/App.phoneViewports.stories.tsx | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/browser/stories/App.phoneViewports.stories.tsx b/src/browser/stories/App.phoneViewports.stories.tsx
index 09e24e5d00..d0c36e8679 100644
--- a/src/browser/stories/App.phoneViewports.stories.tsx
+++ b/src/browser/stories/App.phoneViewports.stories.tsx
@@ -168,6 +168,11 @@ export const IPhone16e: AppStory = {
* viewport selects, so the test-runner exercises the wide placement.
*/
export const IPhone16ePRLinkPlacement: AppStory = {
+ // Mirrors the Pixel phone variant: the fixed-width decorator does not move `window.innerWidth`, so
+ // without this a local reviewer would see the wide placement in a story framed as a phone.
+ globals: {
+ viewport: { value: "mobile2", isRotated: false },
+ },
render: () => (