mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 12:43:16 +00:00
fix: make dashboard UI mobile-friendly (#1416)
* Make dashboard UI mobile-friendly - Drop the thread title/repo header on the agent chat view. - Expanded sidebar becomes a full-screen overlay on mobile (<768px) instead of a squished column; resize handle hidden. - Default to collapsed on mobile first load so the chat stays visible; navigating a link/thread auto-collapses the overlay. * Fix settings rows and run cards overflowing on small screens - SettingsRow stacks label/control vertically below sm; reduce AppShell padding on mobile. - Truncate long model/repo names in AgentRunCard metadata so cards stay within bounds. * Keep collapsed-sidebar button from overlapping page titles on mobile * Render page titles below the sidebar button on mobile instead of indenting * Don't persist mobile auto-close to localStorage, preserving desktop preference --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
This commit is contained in:
parent
a26d5d32cb
commit
9754c2d791
6 changed files with 73 additions and 31 deletions
|
|
@ -18,7 +18,7 @@ export function AppShell({ user, title, description, backTo, children }: AppShel
|
|||
<div className="flex h-svh overflow-hidden bg-background text-foreground">
|
||||
<AppSidebar user={user} />
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-3xl px-8 py-10">
|
||||
<div className="mx-auto max-w-3xl px-4 pt-14 pb-6 sm:px-8 sm:py-10">
|
||||
{backTo && (
|
||||
<Link
|
||||
to={backTo.to}
|
||||
|
|
@ -83,7 +83,7 @@ export function SettingsRow({
|
|||
comingSoon,
|
||||
}: SettingsRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-6 border-b border-border px-4 py-3 last:border-b-0">
|
||||
<div className="flex flex-col gap-2 border-b border-border px-4 py-3 last:border-b-0 sm:flex-row sm:items-center sm:justify-between sm:gap-6">
|
||||
<label className="flex flex-col gap-0.5" htmlFor={htmlFor}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span
|
||||
|
|
@ -103,7 +103,7 @@ export function SettingsRow({
|
|||
<span className="text-xs text-muted-foreground">{description}</span>
|
||||
)}
|
||||
</label>
|
||||
<div className={`shrink-0 ${comingSoon ? "opacity-50" : ""}`}>{control}</div>
|
||||
<div className={`sm:shrink-0 ${comingSoon ? "opacity-50" : ""}`}>{control}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export function AppSidebar({ user }: { user: SessionUser }) {
|
|||
<nav className="flex flex-1 flex-col gap-0.5 px-2">
|
||||
<Link
|
||||
to="/agents"
|
||||
onClick={layout.closeOnMobile}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs/relaxed text-muted-foreground transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
|
|
@ -65,6 +66,7 @@ export function AppSidebar({ user }: { user: SessionUser }) {
|
|||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
onClick={layout.closeOnMobile}
|
||||
className={cn(
|
||||
"flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs/relaxed text-muted-foreground transition-colors",
|
||||
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
|
|
|
|||
|
|
@ -69,25 +69,29 @@ export function AgentRunCard({ thread }: AgentRunCardProps) {
|
|||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium text-[var(--ui-text)]">{thread.title}</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-[var(--ui-text-dim)]">
|
||||
<div className="mt-1 flex min-w-0 items-center gap-2 text-xs text-[var(--ui-text-dim)]">
|
||||
{source && SourceIcon && (
|
||||
<>
|
||||
<span className="flex items-center gap-1" title={source.label}>
|
||||
<span className="flex shrink-0 items-center gap-1" title={source.label}>
|
||||
<SourceIcon className="size-3.5" aria-label={source.label} />
|
||||
{source.label}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span className="shrink-0">·</span>
|
||||
</>
|
||||
)}
|
||||
<span>{thread.model}</span>
|
||||
<span className="min-w-0 truncate" title={thread.model}>
|
||||
{thread.model}
|
||||
</span>
|
||||
{thread.repo && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{thread.repo}</span>
|
||||
<span className="shrink-0">·</span>
|
||||
<span className="min-w-0 truncate" title={thread.repo}>
|
||||
{thread.repo}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span>·</span>
|
||||
<span>{formatRelativeTime(thread.updatedAt)}</span>
|
||||
<span className="shrink-0">·</span>
|
||||
<span className="shrink-0 whitespace-nowrap">{formatRelativeTime(thread.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FolderIcon } from "@phosphor-icons/react";
|
||||
|
||||
import type { SessionUser } from "@/lib/api";
|
||||
import type { PendingPrompt } from "@/lib/agents/pendingPrompts";
|
||||
|
|
@ -83,17 +82,6 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
|||
return (
|
||||
<AgentsShell user={user} activeThreadId={thread.id}>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-[var(--ui-border)] px-4 py-3">
|
||||
<span className="truncate text-sm font-medium text-[color:var(--ui-text)]">
|
||||
{thread.title}
|
||||
</span>
|
||||
{thread.repo && (
|
||||
<span className="flex min-w-0 items-center gap-1 text-xs text-[color:var(--ui-text-dim)]">
|
||||
<FolderIcon className="size-3.5 shrink-0" />
|
||||
<span className="truncate">{thread.repoFullName}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{hasMessages ? (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
|||
<div className="px-2 pb-1">
|
||||
<Link
|
||||
to="/agents"
|
||||
onClick={layout.closeOnMobile}
|
||||
className="flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs font-medium text-[var(--ui-text)] transition-colors hover:bg-[var(--ui-sidebar-hover)]"
|
||||
>
|
||||
<PlusIcon className="size-4" />
|
||||
|
|
@ -73,6 +74,7 @@ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
|||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
onClick={layout.closeOnMobile}
|
||||
className="flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs text-[var(--ui-text-muted)] transition-colors hover:bg-[var(--ui-sidebar-hover)] hover:text-[var(--ui-text)]"
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
|
|
@ -83,9 +85,24 @@ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
|||
</nav>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
||||
<ThreadGroup label="Today" threads={groups.today} activeThreadId={activeThreadId} />
|
||||
<ThreadGroup label="Last 30 days" threads={groups.last30} activeThreadId={activeThreadId} />
|
||||
<ThreadGroup label="Older" threads={groups.older} activeThreadId={activeThreadId} />
|
||||
<ThreadGroup
|
||||
label="Today"
|
||||
threads={groups.today}
|
||||
activeThreadId={activeThreadId}
|
||||
onNavigate={layout.closeOnMobile}
|
||||
/>
|
||||
<ThreadGroup
|
||||
label="Last 30 days"
|
||||
threads={groups.last30}
|
||||
activeThreadId={activeThreadId}
|
||||
onNavigate={layout.closeOnMobile}
|
||||
/>
|
||||
<ThreadGroup
|
||||
label="Older"
|
||||
threads={groups.older}
|
||||
activeThreadId={activeThreadId}
|
||||
onNavigate={layout.closeOnMobile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-2">
|
||||
|
|
@ -99,10 +116,12 @@ function ThreadGroup({
|
|||
label,
|
||||
threads,
|
||||
activeThreadId,
|
||||
onNavigate,
|
||||
}: {
|
||||
label: string;
|
||||
threads: Array<AgentThread>;
|
||||
activeThreadId?: string;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
if (threads.length === 0) return null;
|
||||
|
||||
|
|
@ -112,13 +131,26 @@ function ThreadGroup({
|
|||
{label}
|
||||
</div>
|
||||
{threads.map((thread) => (
|
||||
<ThreadRow key={thread.id} thread={thread} isActive={thread.id === activeThreadId} />
|
||||
<ThreadRow
|
||||
key={thread.id}
|
||||
thread={thread}
|
||||
isActive={thread.id === activeThreadId}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ThreadRow({ thread, isActive }: { thread: AgentThread; isActive: boolean }) {
|
||||
function ThreadRow({
|
||||
thread,
|
||||
isActive,
|
||||
onNavigate,
|
||||
}: {
|
||||
thread: AgentThread;
|
||||
isActive: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const deleteThread = useDeleteAgentThread();
|
||||
const badge =
|
||||
thread.diffStats && thread.diffStats.additions > 0
|
||||
|
|
@ -141,6 +173,7 @@ function ThreadRow({ thread, isActive }: { thread: AgentThread; isActive: boolea
|
|||
<Link
|
||||
to="/agents/$threadId"
|
||||
params={{ threadId: thread.id }}
|
||||
onClick={onNavigate}
|
||||
className={cn(
|
||||
"group mb-0.5 flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors",
|
||||
isActive
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ function readStoredWidth(): number {
|
|||
|
||||
function readStoredCollapsed(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
// On mobile the sidebar is a full-screen overlay, so start collapsed to keep
|
||||
// the chat visible regardless of the stored desktop preference.
|
||||
if (window.matchMedia("(max-width: 767px)").matches) return true;
|
||||
return window.localStorage.getItem(STORAGE_COLLAPSED) === "1";
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +43,13 @@ export function useSidebarLayout() {
|
|||
|
||||
const toggle = useCallback(() => setCollapsed(!collapsed), [collapsed, setCollapsed]);
|
||||
|
||||
return { width, collapsed, setWidth, setCollapsed, toggle };
|
||||
const closeOnMobile = useCallback(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
// State-only: don't persist, so the desktop collapsed preference is preserved.
|
||||
if (window.matchMedia("(max-width: 767px)").matches) setCollapsedState(true);
|
||||
}, []);
|
||||
|
||||
return { width, collapsed, setWidth, setCollapsed, toggle, closeOnMobile };
|
||||
}
|
||||
|
||||
interface SidebarFrameProps {
|
||||
|
|
@ -76,10 +85,16 @@ export function SidebarFrame({
|
|||
return (
|
||||
<aside
|
||||
style={{ width }}
|
||||
className={cn("relative flex h-svh shrink-0 flex-col", className)}
|
||||
className={cn(
|
||||
"relative flex h-svh shrink-0 flex-col",
|
||||
"max-md:fixed max-md:inset-0 max-md:z-40 max-md:!w-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<ResizeHandle width={width} onResize={setWidth} />
|
||||
<div className="max-md:hidden">
|
||||
<ResizeHandle width={width} onResize={setWidth} />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue