authorgravatar for jappyjan@users.noreply.github.comJan Jaap <jappyjan@users.noreply.github.com> 2026-05-01 01:26:36+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-30 16:26:36-07:00
logd649ccf71598a38aac6ea0e7f2f9c034b1bc78b1
treebb27d77bee4c461133a35b0f29ce62bdbae41407
parentf54f4385e518504f87001afef51bdf3b0be2a5be
signature Signed by PGP key B5690EEEBB952194

fix(web): respect iOS safe areas across mobile chrome & other mobile layout fixes (#2392)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Julius Marminge <julius0216@outlook.com>

13 files changed, 277 insertions(+), 65 deletions(-)

apps/web/index.html+4-1
...@@ -2,7 +2,10 @@...@@ -2,7 +2,10 @@
2<html lang="en">2<html lang="en">
3 <head>3 <head>
4 <meta charset="UTF-8" />4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />5 <meta
6 name="viewport"
7 content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
8 />
6 <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />9 <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
7 <meta name="theme-color" content="#161616" media="(prefers-color-scheme: dark)" />10 <meta name="theme-color" content="#161616" media="(prefers-color-scheme: dark)" />
8 <meta name="theme-color" content="#161616" />11 <meta name="theme-color" content="#161616" />
apps/web/src/components/AppSidebarLayout.tsx+1-1
...@@ -54,7 +54,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {...@@ -54,7 +54,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
54 }, [navigate]);54 }, [navigate]);
5555
56 return (56 return (
57 <SidebarProvider defaultOpen>57 <SidebarProvider className="h-dvh! min-h-0!" defaultOpen>
58 <Sidebar58 <Sidebar
59 side="left"59 side="left"
60 collapsible="offcanvas"60 collapsible="offcanvas"
apps/web/src/components/BranchToolbar.tsx+178-18
...@@ -1,18 +1,41 @@...@@ -1,18 +1,41 @@
1import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";1import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";
2import type { EnvironmentId, ThreadId } from "@t3tools/contracts";2import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
3import {
4 ChevronDownIcon,
5 CloudIcon,
6 FolderGit2Icon,
7 FolderGitIcon,
8 FolderIcon,
9 MonitorIcon,
10} from "lucide-react";
3import { memo, useMemo } from "react";11import { memo, useMemo } from "react";
412
5import { useComposerDraftStore, type DraftId } from "../composerDraftStore";13import { useComposerDraftStore, type DraftId } from "../composerDraftStore";
14import { useIsMobile } from "../hooks/useMediaQuery";
6import { useStore } from "../store";15import { useStore } from "../store";
7import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";16import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";
8import {17import {
9 type EnvMode,18 type EnvMode,
10 type EnvironmentOption,19 type EnvironmentOption,
20 resolveCurrentWorkspaceLabel,
21 resolveEnvModeLabel,
11 resolveEffectiveEnvMode,22 resolveEffectiveEnvMode,
23 resolveLockedWorkspaceLabel,
12} from "./BranchToolbar.logic";24} from "./BranchToolbar.logic";
13import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";25import { BranchToolbarBranchSelector } from "./BranchToolbarBranchSelector";
14import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector";26import { BranchToolbarEnvironmentSelector } from "./BranchToolbarEnvironmentSelector";
15import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector";27import { BranchToolbarEnvModeSelector } from "./BranchToolbarEnvModeSelector";
28import { Button } from "./ui/button";
29import {
30 Menu,
31 MenuGroup,
32 MenuGroupLabel,
33 MenuPopup,
34 MenuRadioGroup,
35 MenuRadioItem,
36 MenuSeparator,
37 MenuTrigger,
38} from "./ui/menu";
16import { Separator } from "./ui/separator";39import { Separator } from "./ui/separator";
1740
18interface BranchToolbarProps {41interface BranchToolbarProps {
...@@ -30,6 +53,126 @@ interface BranchToolbarProps {...@@ -30,6 +53,126 @@ interface BranchToolbarProps {
30 onEnvironmentChange?: (environmentId: EnvironmentId) => void;53 onEnvironmentChange?: (environmentId: EnvironmentId) => void;
31}54}
3255
56interface MobileRunContextSelectorProps {
57 envLocked: boolean;
58 envModeLocked: boolean;
59 environmentId: EnvironmentId;
60 availableEnvironments: readonly EnvironmentOption[] | undefined;
61 showEnvironmentPicker: boolean;
62 onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined;
63 effectiveEnvMode: EnvMode;
64 activeWorktreePath: string | null;
65 onEnvModeChange: (mode: EnvMode) => void;
66}
67
68const MobileRunContextSelector = memo(function MobileRunContextSelector({
69 envLocked,
70 envModeLocked,
71 environmentId,
72 availableEnvironments,
73 showEnvironmentPicker,
74 onEnvironmentChange,
75 effectiveEnvMode,
76 activeWorktreePath,
77 onEnvModeChange,
78}: MobileRunContextSelectorProps) {
79 const activeEnvironment = useMemo(
80 () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null,
81 [availableEnvironments, environmentId],
82 );
83 const environmentLabel = activeEnvironment?.label ?? "Run on";
84 const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon;
85 const WorkspaceIcon =
86 effectiveEnvMode === "worktree"
87 ? FolderGit2Icon
88 : activeWorktreePath
89 ? FolderGitIcon
90 : FolderIcon;
91 const workspaceLabel = envModeLocked
92 ? resolveLockedWorkspaceLabel(activeWorktreePath)
93 : effectiveEnvMode === "worktree"
94 ? resolveEnvModeLabel("worktree")
95 : resolveCurrentWorkspaceLabel(activeWorktreePath);
96
97 return (
98 <Menu>
99 <MenuTrigger
100 render={<Button variant="ghost" size="xs" />}
101 className="min-w-0 max-w-[48%] flex-1 justify-start text-muted-foreground/70 hover:text-foreground/80 md:hidden"
102 >
103 {showEnvironmentPicker ? (
104 <>
105 <EnvironmentIcon className="size-3 shrink-0" />
106 <span className="min-w-0 truncate">{environmentLabel}</span>
107 </>
108 ) : (
109 <>
110 <WorkspaceIcon className="size-3 shrink-0" />
111 <span className="min-w-0 truncate">{workspaceLabel}</span>
112 </>
113 )}
114 <ChevronDownIcon className="size-3 shrink-0 opacity-50" />
115 </MenuTrigger>
116 <MenuPopup align="start" side="top" className="w-64">
117 {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? (
118 <>
119 <MenuGroup>
120 <MenuGroupLabel>Run on</MenuGroupLabel>
121 <MenuRadioGroup
122 value={environmentId}
123 onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)}
124 >
125 {availableEnvironments.map((env) => {
126 const Icon = env.isPrimary ? MonitorIcon : CloudIcon;
127 return (
128 <MenuRadioItem
129 key={env.environmentId}
130 disabled={envLocked}
131 value={env.environmentId}
132 >
133 <span className="flex min-w-0 items-center gap-1.5">
134 <Icon className="size-3" />
135 <span className="min-w-0 truncate">{env.label}</span>
136 </span>
137 </MenuRadioItem>
138 );
139 })}
140 </MenuRadioGroup>
141 </MenuGroup>
142 <MenuSeparator />
143 </>
144 ) : null}
145 <MenuGroup>
146 <MenuGroupLabel>Workspace</MenuGroupLabel>
147 <MenuRadioGroup
148 value={effectiveEnvMode}
149 onValueChange={(value) => onEnvModeChange(value as EnvMode)}
150 >
151 <MenuRadioItem disabled={envModeLocked} value="local">
152 <span className="flex min-w-0 items-center gap-1.5">
153 {activeWorktreePath ? (
154 <FolderGitIcon className="size-3" />
155 ) : (
156 <FolderIcon className="size-3" />
157 )}
158 <span className="min-w-0 truncate">
159 {resolveCurrentWorkspaceLabel(activeWorktreePath)}
160 </span>
161 </span>
162 </MenuRadioItem>
163 <MenuRadioItem disabled={envModeLocked} value="worktree">
164 <span className="flex min-w-0 items-center gap-1.5">
165 <FolderGit2Icon className="size-3" />
166 <span className="min-w-0 truncate">{resolveEnvModeLabel("worktree")}</span>
167 </span>
168 </MenuRadioItem>
169 </MenuRadioGroup>
170 </MenuGroup>
171 </MenuPopup>
172 </Menu>
173 );
174});
175
33export const BranchToolbar = memo(function BranchToolbar({176export const BranchToolbar = memo(function BranchToolbar({
34 environmentId,177 environmentId,
35 threadId,178 threadId,
...@@ -74,34 +217,51 @@ export const BranchToolbar = memo(function BranchToolbar({...@@ -74,34 +217,51 @@ export const BranchToolbar = memo(function BranchToolbar({
74 });217 });
75 const envModeLocked = envLocked || (serverThread !== undefined && activeWorktreePath !== null);218 const envModeLocked = envLocked || (serverThread !== undefined && activeWorktreePath !== null);
76219
77 const showEnvironmentPicker =220 const showEnvironmentPicker = Boolean(
78 availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange;221 availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange,
222 );
223 const isMobile = useIsMobile();
79224
80 if (!hasActiveThread || !activeProject) return null;225 if (!hasActiveThread || !activeProject) return null;
81226
82 return (227 return (
83 <div className="mx-auto flex w-full max-w-208 items-center justify-between px-2.5 pb-3 pt-1 sm:px-3">228 <div className="mx-auto flex w-full max-w-208 items-center gap-2 px-2.5 pb-3 pt-1 sm:px-3">
84 <div className="flex items-center gap-1">229 {isMobile ? (
85 {showEnvironmentPicker && (230 <MobileRunContextSelector
86 <>231 envLocked={envLocked}
87 <BranchToolbarEnvironmentSelector232 envModeLocked={envModeLocked}
88 envLocked={envLocked}233 environmentId={environmentId}
89 environmentId={environmentId}234 availableEnvironments={availableEnvironments}
90 availableEnvironments={availableEnvironments}235 showEnvironmentPicker={showEnvironmentPicker}
91 onEnvironmentChange={onEnvironmentChange}236 onEnvironmentChange={onEnvironmentChange}
92 />
93 <Separator orientation="vertical" className="mx-0.5 h-3.5!" />
94 </>
95 )}
96 <BranchToolbarEnvModeSelector
97 envLocked={envModeLocked}
98 effectiveEnvMode={effectiveEnvMode}237 effectiveEnvMode={effectiveEnvMode}
99 activeWorktreePath={activeWorktreePath}238 activeWorktreePath={activeWorktreePath}
100 onEnvModeChange={onEnvModeChange}239 onEnvModeChange={onEnvModeChange}
101 />240 />
102 </div>241 ) : (
242 <div className="flex min-w-0 shrink-0 items-center gap-1">
243 {showEnvironmentPicker && availableEnvironments && onEnvironmentChange && (
244 <>
245 <BranchToolbarEnvironmentSelector
246 envLocked={envLocked}
247 environmentId={environmentId}
248 availableEnvironments={availableEnvironments}
249 onEnvironmentChange={onEnvironmentChange}
250 />
251 <Separator orientation="vertical" className="mx-0.5 h-3.5!" />
252 </>
253 )}
254 <BranchToolbarEnvModeSelector
255 envLocked={envModeLocked}
256 effectiveEnvMode={effectiveEnvMode}
257 activeWorktreePath={activeWorktreePath}
258 onEnvModeChange={onEnvModeChange}
259 />
260 </div>
261 )}
103262
104 <BranchToolbarBranchSelector263 <BranchToolbarBranchSelector
264 className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none"
105 environmentId={environmentId}265 environmentId={environmentId}
106 threadId={threadId}266 threadId={threadId}
107 {...(draftId ? { draftId } : {})}267 {...(draftId ? { draftId } : {})}
apps/web/src/components/BranchToolbarBranchSelector.tsx+6-3
...@@ -19,6 +19,7 @@ import { readEnvironmentApi } from "../environmentApi";...@@ -19,6 +19,7 @@ import { readEnvironmentApi } from "../environmentApi";
19import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery";19import { gitBranchSearchInfiniteQueryOptions, gitQueryKeys } from "../lib/gitReactQuery";
20import { useGitStatus } from "../lib/gitStatusState";20import { useGitStatus } from "../lib/gitStatusState";
21import { newCommandId } from "../lib/utils";21import { newCommandId } from "../lib/utils";
22import { cn } from "../lib/utils";
22import { parsePullRequestReference } from "../pullRequestReference";23import { parsePullRequestReference } from "../pullRequestReference";
23import { useStore } from "../store";24import { useStore } from "../store";
24import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";25import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";
...@@ -45,6 +46,7 @@ import {...@@ -45,6 +46,7 @@ import {
45import { stackedThreadToast, toastManager } from "./ui/toast";46import { stackedThreadToast, toastManager } from "./ui/toast";
4647
47interface BranchToolbarBranchSelectorProps {48interface BranchToolbarBranchSelectorProps {
49 className?: string;
48 environmentId: EnvironmentId;50 environmentId: EnvironmentId;
49 threadId: ThreadId;51 threadId: ThreadId;
50 draftId?: DraftId;52 draftId?: DraftId;
...@@ -76,6 +78,7 @@ function getBranchTriggerLabel(input: {...@@ -76,6 +78,7 @@ function getBranchTriggerLabel(input: {
76}78}
7779
78export function BranchToolbarBranchSelector({80export function BranchToolbarBranchSelector({
81 className,
79 environmentId,82 environmentId,
80 threadId,83 threadId,
81 draftId,84 draftId,
...@@ -577,11 +580,11 @@ export function BranchToolbarBranchSelector({...@@ -577,11 +580,11 @@ export function BranchToolbarBranchSelector({
577 >580 >
578 <ComboboxTrigger581 <ComboboxTrigger
579 render={<Button variant="ghost" size="xs" />}582 render={<Button variant="ghost" size="xs" />}
580 className="text-muted-foreground/70 hover:text-foreground/80"583 className={cn("min-w-0 text-muted-foreground/70 hover:text-foreground/80", className)}
581 disabled={(isBranchesSearchPending && branches.length === 0) || isBranchActionPending}584 disabled={(isBranchesSearchPending && branches.length === 0) || isBranchActionPending}
582 >585 >
583 <span className="max-w-[240px] truncate">{triggerLabel}</span>586 <span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span>
584 <ChevronDownIcon />587 <ChevronDownIcon className="shrink-0" />
585 </ComboboxTrigger>588 </ComboboxTrigger>
586 <ComboboxPopup align="end" side="top" className="w-80">589 <ComboboxPopup align="end" side="top" className="w-80">
587 <div className="border-b p-1">590 <div className="border-b p-1">
apps/web/src/components/BranchToolbarEnvModeSelector.tsx+1
...@@ -58,6 +58,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe...@@ -58,6 +58,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe
5858
59 return (59 return (
60 <Select60 <Select
61 modal={false}
61 value={effectiveEnvMode}62 value={effectiveEnvMode}
62 onValueChange={(value) => onEnvModeChange(value as EnvMode)}63 onValueChange={(value) => onEnvModeChange(value as EnvMode)}
63 items={envModeItems}64 items={envModeItems}
apps/web/src/components/BranchToolbarEnvironmentSelector.tsx+1
...@@ -54,6 +54,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir...@@ -54,6 +54,7 @@ export const BranchToolbarEnvironmentSelector = memo(function BranchToolbarEnvir
5454
55 return (55 return (
56 <Select56 <Select
57 modal={false}
57 value={environmentId}58 value={environmentId}
58 onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)}59 onValueChange={(value) => onEnvironmentChange(value as EnvironmentId)}
59 items={environmentItems}60 items={environmentItems}
apps/web/src/components/ChatView.tsx+37-30
...@@ -3281,14 +3281,14 @@ export default function ChatView(props: ChatViewProps) {...@@ -3281,14 +3281,14 @@ export default function ChatView(props: ChatViewProps) {
3281 {/* Top bar */}3281 {/* Top bar */}
3282 <header3282 <header
3283 className={cn(3283 className={cn(
3284 "border-b border-border px-3 sm:px-5",3284 "border-b border-border",
3285 isElectron3285 isElectron
3286 ? cn(3286 ? cn(
3287 "drag-region flex h-[52px] items-center wco:h-[env(titlebar-area-height)]",3287 "drag-region flex h-[52px] items-center px-3 sm:px-5 wco:h-[env(titlebar-area-height)]",
3288 reserveTitleBarControlInset &&3288 reserveTitleBarControlInset &&
3289 "wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",3289 "wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
3290 )3290 )
3291 : "py-2 sm:py-3",3291 : "pb-2 pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] pt-2 sm:pb-3 sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)] sm:pt-3",
3292 )}3292 )}
3293 >3293 >
3294 <ChatHeader3294 <ChatHeader
...@@ -3374,7 +3374,14 @@ export default function ChatView(props: ChatViewProps) {...@@ -3374,7 +3374,14 @@ export default function ChatView(props: ChatViewProps) {
3374 </div>3374 </div>
33753375
3376 {/* Input bar */}3376 {/* Input bar */}
3377 <div className={cn("px-3 pt-1.5 sm:px-5 sm:pt-2", isGitRepo ? "pb-1" : "pb-3 sm:pb-4")}>3377 <div
3378 className={cn(
3379 "pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] pt-1.5 sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)] sm:pt-2",
3380 isGitRepo
3381 ? "pb-[calc(env(safe-area-inset-bottom)+0.25rem)]"
3382 : "pb-[calc(env(safe-area-inset-bottom)+0.75rem)] sm:pb-[calc(env(safe-area-inset-bottom)+1rem)]",
3383 )}
3384 >
3378 <ChatComposer3385 <ChatComposer
3379 ref={composerRef}3386 ref={composerRef}
3380 composerDraftTarget={composerDraftTarget}3387 composerDraftTarget={composerDraftTarget}
...@@ -3443,34 +3450,34 @@ export default function ChatView(props: ChatViewProps) {...@@ -3443,34 +3450,34 @@ export default function ChatView(props: ChatViewProps) {
3443 setThreadError={setThreadError}3450 setThreadError={setThreadError}
3444 onExpandImage={onExpandTimelineImage}3451 onExpandImage={onExpandTimelineImage}
3445 />3452 />
3453 {isGitRepo && (
3454 <BranchToolbar
3455 environmentId={activeThread.environmentId}
3456 threadId={activeThread.id}
3457 {...(routeKind === "draft" && draftId ? { draftId } : {})}
3458 onEnvModeChange={onEnvModeChange}
3459 {...(canOverrideServerThreadEnvMode ? { effectiveEnvModeOverride: envMode } : {})}
3460 {...(canOverrideServerThreadEnvMode
3461 ? {
3462 activeThreadBranchOverride: activeThreadBranch,
3463 onActiveThreadBranchOverrideChange: setPendingServerThreadBranch,
3464 }
3465 : {})}
3466 envLocked={envLocked}
3467 onComposerFocusRequest={scheduleComposerFocus}
3468 {...(canCheckoutPullRequestIntoThread
3469 ? { onCheckoutPullRequestRequest: openPullRequestDialog }
3470 : {})}
3471 {...(hasMultipleEnvironments
3472 ? {
3473 availableEnvironments: logicalProjectEnvironments,
3474 onEnvironmentChange,
3475 }
3476 : {})}
3477 />
3478 )}
3446 </div>3479 </div>
34473480
3448 {isGitRepo && (
3449 <BranchToolbar
3450 environmentId={activeThread.environmentId}
3451 threadId={activeThread.id}
3452 {...(routeKind === "draft" && draftId ? { draftId } : {})}
3453 onEnvModeChange={onEnvModeChange}
3454 {...(canOverrideServerThreadEnvMode ? { effectiveEnvModeOverride: envMode } : {})}
3455 {...(canOverrideServerThreadEnvMode
3456 ? {
3457 activeThreadBranchOverride: activeThreadBranch,
3458 onActiveThreadBranchOverrideChange: setPendingServerThreadBranch,
3459 }
3460 : {})}
3461 envLocked={envLocked}
3462 onComposerFocusRequest={scheduleComposerFocus}
3463 {...(canCheckoutPullRequestIntoThread
3464 ? { onCheckoutPullRequestRequest: openPullRequestDialog }
3465 : {})}
3466 {...(hasMultipleEnvironments
3467 ? {
3468 availableEnvironments: logicalProjectEnvironments,
3469 onEnvironmentChange,
3470 }
3471 : {})}
3472 />
3473 )}
3474 {pullRequestDialogState ? (3481 {pullRequestDialogState ? (
3475 <PullRequestThreadDialog3482 <PullRequestThreadDialog
3476 key={pullRequestDialogState.key}3483 key={pullRequestDialogState.key}
apps/web/src/components/ComposerPromptEditor.tsx+3-3
...@@ -1490,7 +1490,7 @@ function ComposerPromptEditorInner({...@@ -1490,7 +1490,7 @@ function ComposerPromptEditorInner({
1490 const rootElement = editor.getRootElement();1490 const rootElement = editor.getRootElement();
1491 if (!rootElement) return;1491 if (!rootElement) return;
1492 const boundedCursor = clampCollapsedComposerCursor(snapshotRef.current.value, nextCursor);1492 const boundedCursor = clampCollapsedComposerCursor(snapshotRef.current.value, nextCursor);
1493 rootElement.focus();1493 rootElement.focus({ preventScroll: true });
1494 editor.update(() => {1494 editor.update(() => {
1495 $setSelectionAtComposerOffset(boundedCursor);1495 $setSelectionAtComposerOffset(boundedCursor);
1496 });1496 });
...@@ -1621,7 +1621,7 @@ function ComposerPromptEditorInner({...@@ -1621,7 +1621,7 @@ function ComposerPromptEditorInner({
1621 contentEditable={1621 contentEditable={
1622 <ContentEditable1622 <ContentEditable
1623 className={cn(1623 className={cn(
1624 "block max-h-[200px] min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent text-base sm:text-[14px] leading-relaxed text-foreground focus:outline-none",1624 "block max-h-[200px] min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent text-[16px] leading-relaxed text-foreground focus:outline-none sm:text-[14px]",
1625 className,1625 className,
1626 )}1626 )}
1627 data-testid="composer-editor"1627 data-testid="composer-editor"
...@@ -1632,7 +1632,7 @@ function ComposerPromptEditorInner({...@@ -1632,7 +1632,7 @@ function ComposerPromptEditorInner({
1632 }1632 }
1633 placeholder={1633 placeholder={
1634 terminalContexts.length > 0 ? null : (1634 terminalContexts.length > 0 ? null : (
1635 <div className="pointer-events-none absolute inset-0 text-base sm:text-[14px] leading-relaxed text-muted-foreground/35">1635 <div className="pointer-events-none absolute inset-0 text-[16px] leading-relaxed text-muted-foreground/35 sm:text-[14px]">
1636 {placeholder}1636 {placeholder}
1637 </div>1637 </div>
1638 )1638 )
apps/web/src/components/chat/ChatHeader.tsx+6-1
...@@ -16,6 +16,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScr...@@ -16,6 +16,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScr
16import { Toggle } from "../ui/toggle";16import { Toggle } from "../ui/toggle";
17import { SidebarTrigger } from "../ui/sidebar";17import { SidebarTrigger } from "../ui/sidebar";
18import { OpenInPicker } from "./OpenInPicker";18import { OpenInPicker } from "./OpenInPicker";
19import { usePrimaryEnvironmentId } from "../../environments/primary";
1920
20interface ChatHeaderProps {21interface ChatHeaderProps {
21 activeThreadEnvironmentId: EnvironmentId;22 activeThreadEnvironmentId: EnvironmentId;
...@@ -68,6 +69,10 @@ export const ChatHeader = memo(function ChatHeader({...@@ -68,6 +69,10 @@ export const ChatHeader = memo(function ChatHeader({
68 onToggleTerminal,69 onToggleTerminal,
69 onToggleDiff,70 onToggleDiff,
70}: ChatHeaderProps) {71}: ChatHeaderProps) {
72 const primaryEnvironmentId = usePrimaryEnvironmentId();
73 const isRemoteEnvironment =
74 primaryEnvironmentId !== null && activeThreadEnvironmentId !== primaryEnvironmentId;
75
71 return (76 return (
72 <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2">77 <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2">
73 <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">78 <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
...@@ -101,7 +106,7 @@ export const ChatHeader = memo(function ChatHeader({...@@ -101,7 +106,7 @@ export const ChatHeader = memo(function ChatHeader({
101 onDeleteScript={onDeleteProjectScript}106 onDeleteScript={onDeleteProjectScript}
102 />107 />
103 )}108 )}
104 {activeProjectName && (109 {activeProjectName && !isRemoteEnvironment && (
105 <OpenInPicker110 <OpenInPicker
106 keybindings={keybindings}111 keybindings={keybindings}
107 availableEditors={availableEditors}112 availableEditors={availableEditors}
apps/web/src/components/ui/sidebar.tsx+8-1
...@@ -245,7 +245,14 @@ function Sidebar({...@@ -245,7 +245,14 @@ function Sidebar({
245 <SheetTitle>Sidebar</SheetTitle>245 <SheetTitle>Sidebar</SheetTitle>
246 <SheetDescription>Displays the mobile sidebar.</SheetDescription>246 <SheetDescription>Displays the mobile sidebar.</SheetDescription>
247 </SheetHeader>247 </SheetHeader>
248 <div className="flex h-full w-full flex-col">{children}</div>248 <div
249 className={cn(
250 "flex h-full w-full flex-col pb-safe pt-safe",
251 side === "left" ? "pl-safe" : "pr-safe",
252 )}
253 >
254 {children}
255 </div>
249 </SheetPopup>256 </SheetPopup>
250 </Sheet>257 </Sheet>
251 </SidebarInstanceContext.Provider>258 </SidebarInstanceContext.Provider>
apps/web/src/index.css+28-3
...@@ -58,6 +58,22 @@...@@ -58,6 +58,22 @@
58 }58 }
59}59}
6060
61/* Safe-area inset utilities for surfaces that opt into edge-to-edge rendering.
62 Insets resolve to 0 when the browser already constrains the layout viewport
63 to the safe area. */
64@utility pt-safe {
65 padding-top: max(env(safe-area-inset-top), 0px);
66}
67@utility pb-safe {
68 padding-bottom: max(env(safe-area-inset-bottom), 0px);
69}
70@utility pl-safe {
71 padding-left: max(env(safe-area-inset-left), 0px);
72}
73@utility pr-safe {
74 padding-right: max(env(safe-area-inset-right), 0px);
75}
76
61/* Suppress all transitions during theme changes */77/* Suppress all transitions during theme changes */
62.no-transitions,78.no-transitions,
63.no-transitions *,79.no-transitions *,
...@@ -138,19 +154,28 @@ body {...@@ -138,19 +154,28 @@ body {
138 sans-serif;154 sans-serif;
139 margin: 0;155 margin: 0;
140 padding: 0;156 padding: 0;
141 min-height: 100vh;
142}157}
143158
144html,159html,
145body,160body {
161 min-height: calc(100svh + env(safe-area-inset-top));
162 overscroll-behavior: none;
163}
164
165body {
166 height: 100%;
167 overflow: hidden;
168}
169
146#root {170#root {
147 height: 100%;171 height: 100%;
148 width: 100%;172 width: 100%;
149 max-width: 100%;173 max-width: 100%;
150 overflow-y: hidden;
151 overflow-x: hidden;174 overflow-x: hidden;
152 overflow-x: clip;175 overflow-x: clip;
176 overflow-y: hidden;
153 overscroll-behavior-y: none;177 overscroll-behavior-y: none;
178 padding-top: max(env(safe-area-inset-top), 0px);
154}179}
155180
156body::after {181body::after {
apps/web/src/routes/_chat.$environmentId.$threadId.tsx+2-2
...@@ -240,7 +240,7 @@ function ChatThreadRouteView() {...@@ -240,7 +240,7 @@ function ChatThreadRouteView() {
240 if (!shouldUseDiffSheet) {240 if (!shouldUseDiffSheet) {
241 return (241 return (
242 <>242 <>
243 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">243 <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh">
244 <ChatView244 <ChatView
245 environmentId={threadRef.environmentId}245 environmentId={threadRef.environmentId}
246 threadId={threadRef.threadId}246 threadId={threadRef.threadId}
...@@ -261,7 +261,7 @@ function ChatThreadRouteView() {...@@ -261,7 +261,7 @@ function ChatThreadRouteView() {
261261
262 return (262 return (
263 <>263 <>
264 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">264 <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh">
265 <ChatView265 <ChatView
266 environmentId={threadRef.environmentId}266 environmentId={threadRef.environmentId}
267 threadId={threadRef.threadId}267 threadId={threadRef.threadId}
apps/web/src/routes/_chat.draft.$draftId.tsx+2-2
...@@ -55,7 +55,7 @@ function DraftChatThreadRouteView() {...@@ -55,7 +55,7 @@ function DraftChatThreadRouteView() {
5555
56 if (canonicalThreadRef) {56 if (canonicalThreadRef) {
57 return (57 return (
58 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">58 <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh">
59 <ChatView59 <ChatView
60 environmentId={canonicalThreadRef.environmentId}60 environmentId={canonicalThreadRef.environmentId}
61 threadId={canonicalThreadRef.threadId}61 threadId={canonicalThreadRef.threadId}
...@@ -70,7 +70,7 @@ function DraftChatThreadRouteView() {...@@ -70,7 +70,7 @@ function DraftChatThreadRouteView() {
70 }70 }
7171
72 return (72 return (
73 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">73 <SidebarInset className="h-svh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground md:h-dvh">
74 <ChatView74 <ChatView
75 draftId={draftId}75 draftId={draftId}
76 environmentId={draftSession.environmentId}76 environmentId={draftSession.environmentId}