oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { Outlet, useRouterState } from '@tanstack/react-router'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { InfoPanel } from '@/components/knowledge/info-panel'
import { MoveModal } from '@/components/knowledge/move-modal'
import { SearchModal } from '@/components/knowledge/search-modal'
import { useApprovalWatcher, useGlobalEvents } from '@/hooks/use-global-events'
import { useKnowledgeShortcuts } from '@/hooks/use-knowledge-shortcuts'
import { cn } from '@/lib/utils'
import { useEventStore } from '@/stores/events'
import { useKnowledgeStore } from '@/stores/knowledge'
import { useSidebarStore } from '@/stores/sidebar'
import { Header } from './header'
import { Sidebar } from './sidebar'

/**
 * AppLayout — single sidebar, mode-adaptive.
 *
 * The Sidebar component internally switches between Console/Knowledge/Chat
 * nav content based on the current route. No sidebar replacement needed.
 */
export function AppLayout() {
  const { t } = useTranslation()
  const { mobileOpen, setMobileOpen } = useSidebarStore()

  const router = useRouterState()
  const pathname = router.location.pathname
  const isKnowledge = pathname.startsWith('/knowledge')
  const isKnowledgeSubRoute = isKnowledge && pathname !== '/knowledge' && pathname !== '/knowledge/'
  const isChat = pathname === '/chat'

  const { infoPanelOpen } = useKnowledgeStore()

  useKnowledgeShortcuts()
  useGlobalEvents()
  useApprovalWatcher()

  // Bootstrap singleton SSE connection on first mount
  const connectEvents = useEventStore((s) => s.connect)
  React.useState(() => {
    connectEvents()
  })

  // Close the mobile drawer on Escape — keyboard accessibility
  useEffect(() => {
    if (!mobileOpen) return
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setMobileOpen(false)
    }
    document.addEventListener('keydown', onKey)
    return () => document.removeEventListener('keydown', onKey)
  }, [mobileOpen, setMobileOpen])

  return (
    <div className="flex h-[100vh] h-dvh overflow-hidden">
      {/* ── Desktop sidebar — persistent, width-collapsible ── */}
      <div className="hidden lg:flex">
        <Sidebar />
      </div>

      {/* ── Mobile sidebar — slide-in drawer (animated enter + exit) ── */}
      {/* Backdrop: always mounted, opacity + pointer-events toggled */}
      <div
        role="presentation"
        aria-hidden={!mobileOpen}
        onClick={() => setMobileOpen(false)}
        className={cn(
          'fixed inset-0 z-40 bg-black/50 backdrop-blur-sm lg:hidden',
          'transition-opacity duration-300 ease-[var(--animate-in-easing)]',
          mobileOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
        )}
      />
      {/* Drawer panel: always mounted, translateX toggled so closing slides out */}
      <div
        role="dialog"
        aria-modal="true"
        aria-label={t('common.closeMenu')}
        className={cn(
          'fixed inset-y-0 left-0 z-50 flex lg:hidden',
          'transition-transform duration-300 ease-[var(--animate-in-easing)] will-change-transform',
          mobileOpen ? 'translate-x-0' : 'pointer-events-none -translate-x-full',
        )}
      >
        <Sidebar />
      </div>

      {/* ── Main content area ── */}
      <div className="flex flex-1 flex-col min-w-0 overflow-hidden">
        <Header />

        {isKnowledge ? (
          <div className="flex flex-1 min-h-0 overflow-hidden">
            <div className="flex flex-1 min-w-0 overflow-hidden">
              <Outlet />
            </div>
            {/* InfoPanel only on main knowledge route, not sub-routes */}
            {!isKnowledgeSubRoute && infoPanelOpen && <InfoPanel />}
          </div>
        ) : isChat ? (
          /* Chat: no padding, full height */
          <main className="flex-1 min-h-0 overflow-hidden">
            <Outlet />
          </main>
        ) : (
          <main className="flex-1 overflow-y-auto p-3 lg:p-4 min-h-0">
            <Outlet />
          </main>
        )}
      </div>

      {isKnowledge && (
        <>
          <SearchModal />
          <MoveModal />
        </>
      )}
    </div>
  )
}