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 { useDiffOptions } from "@/components/agents/utils/diffUtils"
import { summarizeChangedFiles } from "@/components/agents/ported" import { summarizeChangedFiles } from "@/components/agents/ported"
import { Z } from "@/components/agents/z-index" import { Z } from "@/components/agents/z-index"
import { useIsMobile } from "@/lib/useIsMobile"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
interface AgentGitPanelProps { interface AgentGitPanelProps {
@ -220,6 +221,10 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
) )
const [width, setWidthState] = useState(() => readStoredPanelWidth()) const [width, setWidthState] = useState(() => readStoredPanelWidth())
const [fullScreen, setFullScreen] = useState(false) 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 panelRef = useRef<HTMLDivElement>(null)
const setCollapsed = (next: boolean) => { const setCollapsed = (next: boolean) => {
@ -341,9 +346,9 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
ref={panelRef} ref={panelRef}
className={cn( className={cn(
"relative flex shrink-0 flex-col bg-[var(--ui-bg)]", "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"> <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" /> <SidebarSimpleIcon className="size-4" />
</button> </button>
<button {!isMobile && (
type="button" <button
onClick={() => setFullScreen((v) => !v)} type="button"
aria-label={fullScreen ? "Exit full screen" : "Enter full screen"} onClick={() => setFullScreen((v) => !v)}
className="rounded-md p-1.5 text-[var(--ui-text-dim)] transition-colors hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]" 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" /> {fullScreen ? (
) : ( <ArrowsInIcon className="size-4" />
<ArrowsOutIcon className="size-4" /> ) : (
)} <ArrowsOutIcon className="size-4" />
</button> )}
</button>
)}
</div> </div>
<div <div
className={cn( 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", "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" ? ( {topTab !== "git" ? (
@ -497,7 +504,7 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
)} )}
</div> </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)]"> <div className="w-72 shrink-0 border-l border-[var(--ui-border)] bg-[var(--ui-surface)]">
<FileTreeExplorer <FileTreeExplorer
files={files} files={files}
@ -510,7 +517,7 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
</> </>
)} )}
</div> </div>
{!fullScreen && <PanelResizeHandle width={width} onResize={setWidth} />} {!overlay && <PanelResizeHandle width={width} onResize={setWidth} />}
</aside> </aside>
) )
} }

View file

@ -12,6 +12,7 @@ import { Messages } from "@/components/agents/messages"
import { streamMessagesToUi } from "@/lib/agents/streamMessagesToUi" import { streamMessagesToUi } from "@/lib/agents/streamMessagesToUi"
import { useSubmitAgentMessage } from "@/lib/agents/provider/useSubmitAgentMessage" import { useSubmitAgentMessage } from "@/lib/agents/provider/useSubmitAgentMessage"
import { useModelOptions } from "@/lib/agents/provider/useModelOptions" import { useModelOptions } from "@/lib/agents/provider/useModelOptions"
import { useIsMobile } from "@/lib/useIsMobile"
interface AgentThreadViewProps { interface AgentThreadViewProps {
thread: AgentThread thread: AgentThread
@ -22,6 +23,7 @@ interface AgentThreadViewProps {
export function AgentThreadView({ thread }: AgentThreadViewProps) { export function AgentThreadView({ thread }: AgentThreadViewProps) {
const sendMessage = useSubmitAgentMessage(thread.id) const sendMessage = useSubmitAgentMessage(thread.id)
const stream = useAgentThreadStream() const stream = useAgentThreadStream()
const isMobile = useIsMobile()
const { models, defaultSelection } = useModelOptions() const { models, defaultSelection } = useModelOptions()
const threadSelection = useMemo<ModelSelection | null>(() => { const threadSelection = useMemo<ModelSelection | null>(() => {
@ -62,8 +64,8 @@ export function AgentThreadView({ thread }: AgentThreadViewProps) {
return ( return (
<div className="flex min-w-0 flex-1"> <div className="flex min-w-0 flex-1">
<div <div
className="flex flex-1 flex-col" className="flex min-w-0 flex-1 flex-col"
style={{ minWidth: PANEL_MIN_CHAT_WIDTH }} style={isMobile ? undefined : { minWidth: PANEL_MIN_CHAT_WIDTH }}
> >
{hasMessages ? ( {hasMessages ? (
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden"> <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
}