mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 09:02:11 +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">
|
<div className="flex h-svh overflow-hidden bg-background text-foreground">
|
||||||
<AppSidebar user={user} />
|
<AppSidebar user={user} />
|
||||||
<main className="flex-1 overflow-y-auto">
|
<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 && (
|
{backTo && (
|
||||||
<Link
|
<Link
|
||||||
to={backTo.to}
|
to={backTo.to}
|
||||||
|
|
@ -83,7 +83,7 @@ export function SettingsRow({
|
||||||
comingSoon,
|
comingSoon,
|
||||||
}: SettingsRowProps) {
|
}: SettingsRowProps) {
|
||||||
return (
|
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}>
|
<label className="flex flex-col gap-0.5" htmlFor={htmlFor}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
|
|
@ -103,7 +103,7 @@ export function SettingsRow({
|
||||||
<span className="text-xs text-muted-foreground">{description}</span>
|
<span className="text-xs text-muted-foreground">{description}</span>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
<div className={`shrink-0 ${comingSoon ? "opacity-50" : ""}`}>{control}</div>
|
<div className={`sm:shrink-0 ${comingSoon ? "opacity-50" : ""}`}>{control}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export function AppSidebar({ user }: { user: SessionUser }) {
|
||||||
<nav className="flex flex-1 flex-col gap-0.5 px-2">
|
<nav className="flex flex-1 flex-col gap-0.5 px-2">
|
||||||
<Link
|
<Link
|
||||||
to="/agents"
|
to="/agents"
|
||||||
|
onClick={layout.closeOnMobile}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs/relaxed text-muted-foreground transition-colors",
|
"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",
|
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
|
@ -65,6 +66,7 @@ export function AppSidebar({ user }: { user: SessionUser }) {
|
||||||
<Link
|
<Link
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
|
onClick={layout.closeOnMobile}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs/relaxed text-muted-foreground transition-colors",
|
"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",
|
"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="min-w-0 flex-1">
|
||||||
<div className="truncate text-sm font-medium text-[var(--ui-text)]">{thread.title}</div>
|
<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 && (
|
{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} />
|
<SourceIcon className="size-3.5" aria-label={source.label} />
|
||||||
{source.label}
|
{source.label}
|
||||||
</span>
|
</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 && (
|
{thread.repo && (
|
||||||
<>
|
<>
|
||||||
<span>·</span>
|
<span className="shrink-0">·</span>
|
||||||
<span>{thread.repo}</span>
|
<span className="min-w-0 truncate" title={thread.repo}>
|
||||||
|
{thread.repo}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span>·</span>
|
<span className="shrink-0">·</span>
|
||||||
<span>{formatRelativeTime(thread.updatedAt)}</span>
|
<span className="shrink-0 whitespace-nowrap">{formatRelativeTime(thread.updatedAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { FolderIcon } from "@phosphor-icons/react";
|
|
||||||
|
|
||||||
import type { SessionUser } from "@/lib/api";
|
import type { SessionUser } from "@/lib/api";
|
||||||
import type { PendingPrompt } from "@/lib/agents/pendingPrompts";
|
import type { PendingPrompt } from "@/lib/agents/pendingPrompts";
|
||||||
|
|
@ -83,17 +82,6 @@ export function AgentThreadView({ user, thread }: AgentThreadViewProps) {
|
||||||
return (
|
return (
|
||||||
<AgentsShell user={user} activeThreadId={thread.id}>
|
<AgentsShell user={user} activeThreadId={thread.id}>
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<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">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
{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">
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
||||||
<div className="px-2 pb-1">
|
<div className="px-2 pb-1">
|
||||||
<Link
|
<Link
|
||||||
to="/agents"
|
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)]"
|
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" />
|
<PlusIcon className="size-4" />
|
||||||
|
|
@ -73,6 +74,7 @@ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
||||||
<Link
|
<Link
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={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)]"
|
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" />
|
<Icon className="size-4" />
|
||||||
|
|
@ -83,9 +85,24 @@ export function AgentsSidebar({ user, activeThreadId }: AgentsSidebarProps) {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
||||||
<ThreadGroup label="Today" threads={groups.today} activeThreadId={activeThreadId} />
|
<ThreadGroup
|
||||||
<ThreadGroup label="Last 30 days" threads={groups.last30} activeThreadId={activeThreadId} />
|
label="Today"
|
||||||
<ThreadGroup label="Older" threads={groups.older} activeThreadId={activeThreadId} />
|
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>
|
||||||
|
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
|
|
@ -99,10 +116,12 @@ function ThreadGroup({
|
||||||
label,
|
label,
|
||||||
threads,
|
threads,
|
||||||
activeThreadId,
|
activeThreadId,
|
||||||
|
onNavigate,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
threads: Array<AgentThread>;
|
threads: Array<AgentThread>;
|
||||||
activeThreadId?: string;
|
activeThreadId?: string;
|
||||||
|
onNavigate?: () => void;
|
||||||
}) {
|
}) {
|
||||||
if (threads.length === 0) return null;
|
if (threads.length === 0) return null;
|
||||||
|
|
||||||
|
|
@ -112,13 +131,26 @@ function ThreadGroup({
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
{threads.map((thread) => (
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ThreadRow({ thread, isActive }: { thread: AgentThread; isActive: boolean }) {
|
function ThreadRow({
|
||||||
|
thread,
|
||||||
|
isActive,
|
||||||
|
onNavigate,
|
||||||
|
}: {
|
||||||
|
thread: AgentThread;
|
||||||
|
isActive: boolean;
|
||||||
|
onNavigate?: () => void;
|
||||||
|
}) {
|
||||||
const deleteThread = useDeleteAgentThread();
|
const deleteThread = useDeleteAgentThread();
|
||||||
const badge =
|
const badge =
|
||||||
thread.diffStats && thread.diffStats.additions > 0
|
thread.diffStats && thread.diffStats.additions > 0
|
||||||
|
|
@ -141,6 +173,7 @@ function ThreadRow({ thread, isActive }: { thread: AgentThread; isActive: boolea
|
||||||
<Link
|
<Link
|
||||||
to="/agents/$threadId"
|
to="/agents/$threadId"
|
||||||
params={{ threadId: thread.id }}
|
params={{ threadId: thread.id }}
|
||||||
|
onClick={onNavigate}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group mb-0.5 flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors",
|
"group mb-0.5 flex items-center gap-2 rounded-lg px-2.5 py-1.5 transition-colors",
|
||||||
isActive
|
isActive
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ function readStoredWidth(): number {
|
||||||
|
|
||||||
function readStoredCollapsed(): boolean {
|
function readStoredCollapsed(): boolean {
|
||||||
if (typeof window === "undefined") return false;
|
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";
|
return window.localStorage.getItem(STORAGE_COLLAPSED) === "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +43,13 @@ export function useSidebarLayout() {
|
||||||
|
|
||||||
const toggle = useCallback(() => setCollapsed(!collapsed), [collapsed, setCollapsed]);
|
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 {
|
interface SidebarFrameProps {
|
||||||
|
|
@ -76,10 +85,16 @@ export function SidebarFrame({
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
style={{ width }}
|
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}
|
{children}
|
||||||
<ResizeHandle width={width} onResize={setWidth} />
|
<div className="max-md:hidden">
|
||||||
|
<ResizeHandle width={width} onResize={setWidth} />
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue