+
+ {label}
+
+ {items.map((item) => (
+
onNavigate(item.path)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') onNavigate(item.path)
+ }}
+ >
+ {item.icon && (
+
+ {item.icon}
+
+ )}
+
+ {item.label}
+ {item.parentApp && (
+
+ {item.parentApp}
+
+ )}
+
+
+
+ ))}
+
+ )
+}
+
+// ── localStorage-backed section collapse ────────────────────────────────────
+
+function usePersistedCollapse(key: string, defaultValue: boolean): [boolean, () => void] {
+ const [value, setValue] = useState(() => {
+ try {
+ const raw = localStorage.getItem(key)
+ if (raw !== null) return raw === 'true'
+ } catch { /* ignore */ }
+ return defaultValue
+ })
+
+ const toggle = () => {
+ setValue((prev) => {
+ const next = !prev
+ try {
+ localStorage.setItem(key, String(next))
+ } catch { /* ignore */ }
+ return next
+ })
+ }
+
+ return [value, toggle]
+}
+
+// ── LayoutShell ─────────────────────────────────────────────────────────────
+
+export function LayoutShell() {
+ const navigate = useNavigate()
+ const location = useLocation()
+ const { starredIds, isStarred, toggleStar } = useStarred()
+
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
+ const [filterQuery, setFilterQuery] = useState('')
+
+ // Section collapse state — persisted to localStorage
+ const [appsCollapsed, toggleAppsCollapsed] = usePersistedCollapse('cameleer:sidebar:apps-collapsed', false)
+ const [agentsCollapsed, toggleAgentsCollapsed] = usePersistedCollapse('cameleer:sidebar:agents-collapsed', false)
+ const [routesCollapsed, toggleRoutesCollapsed] = usePersistedCollapse('cameleer:sidebar:routes-collapsed', false)
+
+ // Tree data — static, so empty deps
+ const appNodes = useMemo(() => buildAppTreeNodes(SIDEBAR_APPS), [])
+ const agentNodes = useMemo(() => buildAgentTreeNodes(SIDEBAR_APPS), [])
+ const routeNodes = useMemo(() => buildRouteTreeNodes(SIDEBAR_APPS), [])
+
+ // Sidebar reveal from Cmd-K navigation
+ const sidebarRevealPath = (location.state as { sidebarReveal?: string } | null)?.sidebarReveal ?? null
+
+ // Auto-uncollapse matching sections when sidebarRevealPath changes
+ useEffect(() => {
+ if (!sidebarRevealPath) return
+
+ if (sidebarRevealPath.startsWith('/apps') && appsCollapsed) {
+ toggleAppsCollapsed()
+ }
+ if (sidebarRevealPath.startsWith('/agents') && agentsCollapsed) {
+ toggleAgentsCollapsed()
+ }
+ if (sidebarRevealPath.startsWith('/routes') && routesCollapsed) {
+ toggleRoutesCollapsed()
+ }
+ }, [sidebarRevealPath]) // eslint-disable-line react-hooks/exhaustive-deps
+
+ const effectiveSelectedPath = sidebarRevealPath ?? location.pathname
+
+ // Starred items — collected and grouped
+ const allStarred = useMemo(
+ () => collectStarredItems(SIDEBAR_APPS, starredIds),
+ [starredIds],
+ )
+
+ const starredApps = allStarred.filter((s) => s.type === 'application')
+ const starredRoutes = allStarred.filter((s) => s.type === 'route')
+ const starredAgents = allStarred.filter((s) => s.type === 'agent')
+ const starredRouteStats = allStarred.filter((s) => s.type === 'routestat')
+ const hasStarred = allStarred.length > 0
+
+ const camelLogo = (
+