From 0b6806c4ba9f332b4db444a64b6097b3b2005455 Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Wed, 17 Jun 2026 09:24:28 -0700 Subject: [PATCH] fix: make git panel a full-screen overlay on mobile widths (#1559) At mobile widths the resizable git panel collided with the chat column's 360px min-width, overflowing the viewport. Treat mobile (<768px) like the sidebar: the git panel becomes a full-screen overlay the user navigates to and back from, while desktop keeps the inline resizable panel. Co-authored-by: open-swe[bot] --- ui/src/components/agents/AgentGitPanel.tsx | 41 ++++++++++++-------- ui/src/components/agents/AgentThreadView.tsx | 6 ++- ui/src/lib/useIsMobile.ts | 25 ++++++++++++ 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 ui/src/lib/useIsMobile.ts diff --git a/ui/src/components/agents/AgentGitPanel.tsx b/ui/src/components/agents/AgentGitPanel.tsx index 6b27004c..fa24286e 100644 --- a/ui/src/components/agents/AgentGitPanel.tsx +++ b/ui/src/components/agents/AgentGitPanel.tsx @@ -23,6 +23,7 @@ import { buttonVariants } from "@/components/ui/button" import { useDiffOptions } from "@/components/agents/utils/diffUtils" import { summarizeChangedFiles } from "@/components/agents/ported" import { Z } from "@/components/agents/z-index" +import { useIsMobile } from "@/lib/useIsMobile" import { cn } from "@/lib/utils" interface AgentGitPanelProps { @@ -220,6 +221,10 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) { ) const [width, setWidthState] = useState(() => readStoredPanelWidth()) const [fullScreen, setFullScreen] = useState(false) + const isMobile = useIsMobile() + // On mobile the panel is never an inline resizable column — it's a full-screen + // overlay that the user navigates to (and back from), like the sidebar. + const overlay = fullScreen || isMobile const panelRef = useRef(null) const setCollapsed = (next: boolean) => { @@ -341,9 +346,9 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) { ref={panelRef} className={cn( "relative flex shrink-0 flex-col bg-[var(--ui-bg)]", - fullScreen ? "fixed inset-0 !w-full" : "h-full" + overlay ? "fixed inset-0 !w-full" : "h-full" )} - style={fullScreen ? { zIndex: Z.MODAL } : { width }} + style={overlay ? { zIndex: Z.MODAL } : { width }} >
{( @@ -379,24 +384,26 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) { > - + {!isMobile && ( + + )}
{topTab !== "git" ? ( @@ -497,7 +504,7 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) { )}
- {fullScreen && files.length > 0 && ( + {fullScreen && !isMobile && files.length > 0 && (
)}
- {!fullScreen && } + {!overlay && } ) } diff --git a/ui/src/components/agents/AgentThreadView.tsx b/ui/src/components/agents/AgentThreadView.tsx index 570e7758..9397e7ef 100644 --- a/ui/src/components/agents/AgentThreadView.tsx +++ b/ui/src/components/agents/AgentThreadView.tsx @@ -12,6 +12,7 @@ import { Messages } from "@/components/agents/messages" import { streamMessagesToUi } from "@/lib/agents/streamMessagesToUi" import { useSubmitAgentMessage } from "@/lib/agents/provider/useSubmitAgentMessage" import { useModelOptions } from "@/lib/agents/provider/useModelOptions" +import { useIsMobile } from "@/lib/useIsMobile" interface AgentThreadViewProps { thread: AgentThread @@ -22,6 +23,7 @@ interface AgentThreadViewProps { export function AgentThreadView({ thread }: AgentThreadViewProps) { const sendMessage = useSubmitAgentMessage(thread.id) const stream = useAgentThreadStream() + const isMobile = useIsMobile() const { models, defaultSelection } = useModelOptions() const threadSelection = useMemo(() => { @@ -62,8 +64,8 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) { return (
{hasMessages ? (
diff --git a/ui/src/lib/useIsMobile.ts b/ui/src/lib/useIsMobile.ts new file mode 100644 index 00000000..a887a68f --- /dev/null +++ b/ui/src/lib/useIsMobile.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from "react" + +// Matches the `max-md:` Tailwind breakpoint (md = 768px) used across the UI, so +// JS-driven layout decisions stay in sync with the CSS responsive utilities. +export const MOBILE_MEDIA_QUERY = "(max-width: 767px)" + +function readIsMobile(): boolean { + if (typeof window === "undefined") return false + return window.matchMedia(MOBILE_MEDIA_QUERY).matches +} + +/** Reactive flag that tracks whether the viewport is at mobile width. */ +export function useIsMobile(): boolean { + const [isMobile, setIsMobile] = useState(readIsMobile) + + useEffect(() => { + const media = window.matchMedia(MOBILE_MEDIA_QUERY) + const onChange = () => setIsMobile(media.matches) + onChange() + media.addEventListener("change", onChange) + return () => media.removeEventListener("change", onChange) + }, []) + + return isMobile +}