mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
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:
parent
b19804536c
commit
0b6806c4ba
3 changed files with 53 additions and 19 deletions
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
25
ui/src/lib/useIsMobile.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue