PYAE1994's picture
feat(phase4): Persistent Workflow OS v4.0.0 β€” queue + checkpoint + workspace memory
cf47145 verified
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
Brain,
Activity,
ListTodo,
GitBranch,
Database,
Settings,
ChevronLeft,
ChevronRight,
Rocket,
Github,
X,
Menu,
} from 'lucide-react';
const NAV_ITEMS = [
{ href: '/', icon: Brain, label: 'Agent', desc: 'Run AI tasks' },
{ href: '/health', icon: Activity, label: 'Health', desc: 'System status' },
{ href: '/queue', icon: ListTodo, label: 'Queue', desc: 'Job queue' },
{ href: '/memory', icon: Database, label: 'Memory', desc: 'Episodic memory' },
{ href: '/workspace', icon: GitBranch, label: 'Workspace', desc: 'Projects' },
];
interface AppShellProps {
children: React.ReactNode;
}
export default function AppShell({ children }: AppShellProps) {
const pathname = usePathname();
const [collapsed, setCollapsed] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
return (
<div className="flex min-h-screen">
{/* ── Mobile overlay ── */}
{mobileOpen && (
<div
className="fixed inset-0 z-30 bg-black/60 lg:hidden"
onClick={() => setMobileOpen(false)}
/>
)}
{/* ── Left Sidebar ── */}
<aside
className={`
fixed top-0 left-0 z-40 h-full flex flex-col
bg-slate-900 border-r border-white/5
transition-all duration-200 ease-in-out
${collapsed ? 'w-16' : 'w-56'}
${mobileOpen ? 'translate-x-0' : '-translate-x-full'}
lg:translate-x-0 lg:static lg:h-auto lg:min-h-screen
`}
>
{/* Logo */}
<div className={`flex items-center gap-2 px-3 py-4 border-b border-white/5 ${collapsed ? 'justify-center' : ''}`}>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-brand-600/30 text-brand-400 shrink-0">
<Rocket className="h-4 w-4" />
</div>
{!collapsed && (
<div className="min-w-0">
<p className="text-sm font-bold truncate">OpenHands</p>
<p className="text-[10px] text-slate-500 truncate">Genspark AI OS</p>
</div>
)}
{/* Collapse toggle β€” desktop only */}
<button
onClick={() => setCollapsed(v => !v)}
className="ml-auto hidden lg:flex items-center justify-center w-6 h-6 rounded hover:bg-white/5 text-slate-500 hover:text-slate-300"
>
{collapsed ? <ChevronRight className="h-3.5 w-3.5" /> : <ChevronLeft className="h-3.5 w-3.5" />}
</button>
{/* Mobile close */}
<button
onClick={() => setMobileOpen(false)}
className="ml-auto lg:hidden flex items-center justify-center w-6 h-6 rounded hover:bg-white/5 text-slate-500"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
{/* Phase badge */}
{!collapsed && (
<div className="mx-3 my-2">
<span className="inline-flex items-center gap-1 text-[10px] bg-brand-600/20 text-brand-300 border border-brand-600/30 px-2 py-0.5 rounded-full w-full justify-center">
βœ… Phase 4 Live
</span>
</div>
)}
{/* Nav items */}
<nav className="flex-1 px-2 py-2 space-y-0.5 overflow-y-auto">
{NAV_ITEMS.map(({ href, icon: Icon, label, desc }) => {
const active = pathname === href || (href !== '/' && pathname.startsWith(href));
return (
<Link
key={href}
href={href}
onClick={() => setMobileOpen(false)}
className={`
flex items-center gap-3 rounded-lg px-2 py-2 text-sm transition-colors
${active
? 'bg-brand-600/20 text-brand-300 border border-brand-600/20'
: 'text-slate-400 hover:text-slate-200 hover:bg-white/5'
}
${collapsed ? 'justify-center' : ''}
`}
title={collapsed ? label : undefined}
>
<Icon className={`h-4 w-4 shrink-0 ${active ? 'text-brand-400' : ''}`} />
{!collapsed && (
<div className="min-w-0">
<p className="font-medium leading-none">{label}</p>
<p className="text-[10px] text-slate-500 mt-0.5 truncate">{desc}</p>
</div>
)}
</Link>
);
})}
</nav>
{/* Bottom β€” Settings link */}
<div className="border-t border-white/5 px-2 py-2 space-y-0.5">
<Link
href="/settings"
onClick={() => setMobileOpen(false)}
className={`
flex items-center gap-3 rounded-lg px-2 py-2 text-sm transition-colors
${pathname === '/settings'
? 'bg-slate-700/50 text-slate-200'
: 'text-slate-500 hover:text-slate-300 hover:bg-white/5'
}
${collapsed ? 'justify-center' : ''}
`}
title={collapsed ? 'Settings' : undefined}
>
<Settings className="h-4 w-4 shrink-0" />
{!collapsed && <span className="font-medium">Settings</span>}
</Link>
{!collapsed && (
<a
href="https://github.com/pyaesonegtckglay-dotcom/Onehands-development"
target="_blank"
rel="noreferrer"
className="flex items-center gap-3 rounded-lg px-2 py-1.5 text-xs text-slate-600 hover:text-slate-400 hover:bg-white/5 transition-colors"
>
<Github className="h-3.5 w-3.5 shrink-0" />
<span>GitHub</span>
</a>
)}
</div>
</aside>
{/* ── Main content area ── */}
<div className="flex-1 flex flex-col min-w-0">
{/* Mobile top bar */}
<header className="lg:hidden flex items-center gap-3 px-4 py-3 border-b border-white/5 bg-slate-900/80 backdrop-blur sticky top-0 z-20">
<button
onClick={() => setMobileOpen(true)}
className="flex items-center justify-center w-8 h-8 rounded-lg hover:bg-white/5 text-slate-400"
>
<Menu className="h-5 w-5" />
</button>
<div className="flex items-center gap-2">
<Rocket className="h-4 w-4 text-brand-400" />
<span className="font-bold text-sm">OpenHands</span>
</div>
<div className="ml-auto">
<span className="text-[10px] text-brand-400 border border-brand-600/30 bg-brand-600/10 px-2 py-0.5 rounded-full">
Phase 4
</span>
</div>
</header>
{/* Page content */}
<main className="flex-1 overflow-auto">
{children}
</main>
</div>
</div>
);
}