mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 09:02:11 +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 { 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
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