diff --git a/apps/web/index.html b/apps/web/index.html index 9f0329b6020..88e1c8b4f23 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -2,7 +2,10 @@ - + diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index b1ce57235a8..d98f30a1e5c 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -54,7 +54,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { }, [navigate]); return ( - + void; } +interface MobileRunContextSelectorProps { + envLocked: boolean; + envModeLocked: boolean; + environmentId: EnvironmentId; + availableEnvironments: readonly EnvironmentOption[] | undefined; + showEnvironmentPicker: boolean; + onEnvironmentChange: ((environmentId: EnvironmentId) => void) | undefined; + effectiveEnvMode: EnvMode; + activeWorktreePath: string | null; + onEnvModeChange: (mode: EnvMode) => void; +} + +const MobileRunContextSelector = memo(function MobileRunContextSelector({ + envLocked, + envModeLocked, + environmentId, + availableEnvironments, + showEnvironmentPicker, + onEnvironmentChange, + effectiveEnvMode, + activeWorktreePath, + onEnvModeChange, +}: MobileRunContextSelectorProps) { + const activeEnvironment = useMemo( + () => availableEnvironments?.find((env) => env.environmentId === environmentId) ?? null, + [availableEnvironments, environmentId], + ); + const environmentLabel = activeEnvironment?.label ?? "Run on"; + const EnvironmentIcon = activeEnvironment?.isPrimary ? MonitorIcon : CloudIcon; + const WorkspaceIcon = + effectiveEnvMode === "worktree" + ? FolderGit2Icon + : activeWorktreePath + ? FolderGitIcon + : FolderIcon; + const workspaceLabel = envModeLocked + ? resolveLockedWorkspaceLabel(activeWorktreePath) + : effectiveEnvMode === "worktree" + ? resolveEnvModeLabel("worktree") + : resolveCurrentWorkspaceLabel(activeWorktreePath); + + return ( + + } + className="min-w-0 max-w-[48%] flex-1 justify-start text-muted-foreground/70 hover:text-foreground/80 md:hidden" + > + {showEnvironmentPicker ? ( + <> + + {environmentLabel} + + ) : ( + <> + + {workspaceLabel} + + )} + + + + {showEnvironmentPicker && availableEnvironments && onEnvironmentChange ? ( + <> + + Run on + onEnvironmentChange(value as EnvironmentId)} + > + {availableEnvironments.map((env) => { + const Icon = env.isPrimary ? MonitorIcon : CloudIcon; + return ( + + + + {env.label} + + + ); + })} + + + + + ) : null} + + Workspace + onEnvModeChange(value as EnvMode)} + > + + + {activeWorktreePath ? ( + + ) : ( + + )} + + {resolveCurrentWorkspaceLabel(activeWorktreePath)} + + + + + + + {resolveEnvModeLabel("worktree")} + + + + + + + ); +}); + export const BranchToolbar = memo(function BranchToolbar({ environmentId, threadId, @@ -74,34 +217,51 @@ export const BranchToolbar = memo(function BranchToolbar({ }); const envModeLocked = envLocked || (serverThread !== undefined && activeWorktreePath !== null); - const showEnvironmentPicker = - availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange; + const showEnvironmentPicker = Boolean( + availableEnvironments && availableEnvironments.length > 1 && onEnvironmentChange, + ); + const isMobile = useIsMobile(); if (!hasActiveThread || !activeProject) return null; return ( -
-
- {showEnvironmentPicker && ( - <> - - - - )} - + {isMobile ? ( + -
+ ) : ( +
+ {showEnvironmentPicker && availableEnvironments && onEnvironmentChange && ( + <> + + + + )} + +
+ )} } - className="text-muted-foreground/70 hover:text-foreground/80" + className={cn("min-w-0 text-muted-foreground/70 hover:text-foreground/80", className)} disabled={(isBranchesSearchPending && branches.length === 0) || isBranchActionPending} > - {triggerLabel} - + {triggerLabel} +
diff --git a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx index 6e1c80f5573..6d06882662f 100644 --- a/apps/web/src/components/BranchToolbarEnvModeSelector.tsx +++ b/apps/web/src/components/BranchToolbarEnvModeSelector.tsx @@ -58,6 +58,7 @@ export const BranchToolbarEnvModeSelector = memo(function BranchToolbarEnvModeSe return ( onEnvironmentChange(value as EnvironmentId)} items={environmentItems} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0f22ba66be3..40cd1b42105 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -3281,14 +3281,14 @@ export default function ChatView(props: ChatViewProps) { {/* Top bar */}
{/* Input bar */} -
+
+ {isGitRepo && ( + + )}
- {isGitRepo && ( - - )} {pullRequestDialogState ? ( { $setSelectionAtComposerOffset(boundedCursor); }); @@ -1621,7 +1621,7 @@ function ComposerPromptEditorInner({ contentEditable={ 0 ? null : ( -
+
{placeholder}
) diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index 3f2c480c546..5d7c9292474 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -16,6 +16,7 @@ import ProjectScriptsControl, { type NewProjectScriptInput } from "../ProjectScr import { Toggle } from "../ui/toggle"; import { SidebarTrigger } from "../ui/sidebar"; import { OpenInPicker } from "./OpenInPicker"; +import { usePrimaryEnvironmentId } from "../../environments/primary"; interface ChatHeaderProps { activeThreadEnvironmentId: EnvironmentId; @@ -68,6 +69,10 @@ export const ChatHeader = memo(function ChatHeader({ onToggleTerminal, onToggleDiff, }: ChatHeaderProps) { + const primaryEnvironmentId = usePrimaryEnvironmentId(); + const isRemoteEnvironment = + primaryEnvironmentId !== null && activeThreadEnvironmentId !== primaryEnvironmentId; + return (
@@ -101,7 +106,7 @@ export const ChatHeader = memo(function ChatHeader({ onDeleteScript={onDeleteProjectScript} /> )} - {activeProjectName && ( + {activeProjectName && !isRemoteEnvironment && ( Sidebar Displays the mobile sidebar. -
{children}
+
+ {children} +
diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 5c568ad5d67..edf71b675bb 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -58,6 +58,22 @@ } } +/* Safe-area inset utilities for surfaces that opt into edge-to-edge rendering. + Insets resolve to 0 when the browser already constrains the layout viewport + to the safe area. */ +@utility pt-safe { + padding-top: max(env(safe-area-inset-top), 0px); +} +@utility pb-safe { + padding-bottom: max(env(safe-area-inset-bottom), 0px); +} +@utility pl-safe { + padding-left: max(env(safe-area-inset-left), 0px); +} +@utility pr-safe { + padding-right: max(env(safe-area-inset-right), 0px); +} + /* Suppress all transitions during theme changes */ .no-transitions, .no-transitions *, @@ -138,19 +154,28 @@ body { sans-serif; margin: 0; padding: 0; - min-height: 100vh; } html, -body, +body { + min-height: calc(100svh + env(safe-area-inset-top)); + overscroll-behavior: none; +} + +body { + height: 100%; + overflow: hidden; +} + #root { height: 100%; width: 100%; max-width: 100%; - overflow-y: hidden; overflow-x: hidden; overflow-x: clip; + overflow-y: hidden; overscroll-behavior-y: none; + padding-top: max(env(safe-area-inset-top), 0px); } body::after { diff --git a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx index ff20673e2de..0309d43d7e3 100644 --- a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx +++ b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx @@ -238,7 +238,7 @@ function ChatThreadRouteView() { if (!shouldUseDiffSheet) { return ( <> - + - + + +