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] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-17 09:24:28 -07:00 • committed by GitHub
parent b19804536c
commit 0b6806c4ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 19 deletions

View file

@ -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<HTMLDivElement>(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 }}
>
<div className="flex h-11 shrink-0 items-center gap-1 px-3">
{(
@ -379,24 +384,26 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
>
<SidebarSimpleIcon className="size-4" />
</button>
<button
type="button"
onClick={() => setFullScreen((v) => !v)}
aria-label={fullScreen ? "Exit full screen" : "Enter full screen"}
className="rounded-md p-1.5 text-[var(--ui-text-dim)] transition-colors hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
>
{fullScreen ? (
<ArrowsInIcon className="size-4" />
) : (
<ArrowsOutIcon className="size-4" />
)}
</button>
{!isMobile && (
<button
type="button"
onClick={() => setFullScreen((v) => !v)}
aria-label={fullScreen ? "Exit full screen" : "Enter full screen"}
className="rounded-md p-1.5 text-[var(--ui-text-dim)] transition-colors hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
>
{fullScreen ? (
<ArrowsInIcon className="size-4" />
) : (
<ArrowsOutIcon className="size-4" />
)}
</button>
)}
</div>
<div
className={cn(
"flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border border-[var(--ui-border)] bg-[var(--ui-surface)] shadow-sm",
fullScreen ? "mx-3 mb-3" : "mr-4 mb-4 ml-1"
overlay ? "mx-3 mb-3" : "mr-4 mb-4 ml-1"
)}
>
{topTab !== "git" ? (
@ -497,7 +504,7 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
)}
</div>
{fullScreen && files.length > 0 && (
{fullScreen && !isMobile && files.length > 0 && (
<div className="w-72 shrink-0 border-l border-[var(--ui-border)] bg-[var(--ui-surface)]">
<FileTreeExplorer
files={files}
@ -510,7 +517,7 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
</>
)}
</div>
{!fullScreen && <PanelResizeHandle width={width} onResize={setWidth} />}
{!overlay && <PanelResizeHandle width={width} onResize={setWidth} />}
</aside>
)
}

View file

@ -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<ModelSelection | null>(() => {
@ -62,8 +64,8 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
return (
<div className="flex min-w-0 flex-1">
<div
className="flex flex-1 flex-col"
style={{ minWidth: PANEL_MIN_CHAT_WIDTH }}
className="flex min-w-0 flex-1 flex-col"
style={isMobile ? undefined : { minWidth: PANEL_MIN_CHAT_WIDTH }}
>
{hasMessages ? (
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">

25
ui/src/lib/useIsMobile.ts Normal file
View file

@ -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<boolean>(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
}