oxios 1.5.2

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { Bot, ExternalLink, Power, Square, X } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { statusDot } from '@/components/shared/status-palette'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import type { A2AAgentCard, A2AMessage, TopologyNode } from '@/types/a2a'

interface Props {
  /** The currently selected node (from topology). */
  node: TopologyNode | null
  /** Open/closed state. */
  open: boolean
  /** Close handler. */
  onClose: () => void
  /** Full agent card detail (for capabilities/skills list). */
  agentCard?: A2AAgentCard | null
  /** Recent messages involving this agent. */
  recentMessages?: A2AMessage[]
  /** Loading state for the messages. */
  isMessagesLoading?: boolean
  /** Stop-agent handler (placeholder — wired in a follow-up). */
  onStopAgent?: (id: string) => void
  /** View-trace handler (placeholder — wired in a follow-up). */
  onViewTrace?: (id: string) => void
}

/**
 * Slide-in inspector for a single A2A agent.
 *
 * Slides from the right when `open` is true. Closes on backdrop click,
 * Esc key, or close button. Renders the agent's capabilities, skills,
 * status, and the last 5 messages.
 */
export function AgentInspector({
  node,
  open,
  onClose,
  agentCard,
  recentMessages = [],
  isMessagesLoading,
  onStopAgent,
  onViewTrace,
}: Props) {
  const { t } = useTranslation()
  const panelRef = useRef<HTMLDivElement>(null)
  const closeButtonRef = useRef<HTMLButtonElement>(null)

  // Esc to close
  useEffect(() => {
    if (!open) return
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.stopPropagation()
        onClose()
      }
    }
    document.addEventListener('keydown', onKey)
    return () => document.removeEventListener('keydown', onKey)
  }, [open, onClose])

  // Focus trap: apply `inert` to the rest of the page so Tab cycles
  // inside the dialog instead of leaking out. The inspector is a
  // fixed-position overlay (sibling of <main>), so toggling `inert`
  // on <main> is sufficient — React 19's types support it natively.
  useEffect(() => {
    const main = document.querySelector('main')
    if (!main) return
    if (open) {
      // Save the previous value so we can restore on close.
      const prev = main.getAttribute('inert')
      main.setAttribute('inert', '')
      return () => {
        if (prev === null) {
          main.removeAttribute('inert')
        } else {
          main.setAttribute('inert', prev)
        }
      }
    }
    return undefined
  }, [open])

  // Focus close button when opened.
  // Skip if focus is already inside the panel (e.g. user clicked
  // a tab in the tab switcher) to avoid stealing focus mid-task.
  useEffect(() => {
    if (!open) return
    const id = requestAnimationFrame(() => {
      const panel = panelRef.current
      if (!panel) return
      const alreadyInside = panel.contains(document.activeElement)
      if (!alreadyInside) {
        closeButtonRef.current?.focus()
      }
    })
    return () => cancelAnimationFrame(id)
  }, [open])

  if (!node) return null

  const dot = statusDot(node.status)
  const capList = Array.isArray(agentCard?.capabilities)
    ? agentCard.capabilities
    : Array.isArray(node.capabilities)
      ? node.capabilities
      : []
  const skillList = Array.isArray(agentCard?.skills)
    ? agentCard.skills
    : Array.isArray(node.skills)
      ? node.skills
      : []

  return (
    <div
      aria-hidden={!open}
      className={cn(
        'fixed inset-0 z-40 transition-opacity',
        open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none',
      )}
    >
      {/* Backdrop */}
      <div className="absolute inset-0 bg-black/30" onClick={onClose} aria-hidden="true" />
      {/* Panel */}
      <aside
        ref={panelRef}
        role="dialog"
        aria-modal="true"
        aria-label={t('a2a.inspectorTitle', { name: node.label })}
        data-testid="a2a-agent-inspector"
        className={cn(
          'absolute right-0 top-0 h-full w-full max-w-md bg-card border-l shadow-xl',
          'transition-transform duration-200 ease-out',
          open ? 'translate-x-0' : 'translate-x-full',
        )}
      >
        <div className="flex h-full flex-col">
          {/* Header */}
          <header className="flex items-start justify-between border-b p-4">
            <div className="flex items-start gap-3">
              <div className="rounded-md bg-muted p-2">
                <Bot className="h-5 w-5" aria-hidden="true" />
              </div>
              <div>
                <h2 className="text-base font-semibold">{node.label}</h2>
                <div className="flex items-center gap-1.5 mt-0.5">
                  <span className={cn('h-2 w-2 rounded-full', dot)} aria-hidden="true" />
                  <span className="text-xs capitalize text-muted-foreground">{node.status}</span>
                </div>
              </div>
            </div>
            <Button
              ref={closeButtonRef}
              variant="ghost"
              size="icon"
              onClick={onClose}
              aria-label={t('a2a.inspectorClose')}
            >
              <X className="h-4 w-4" />
            </Button>
          </header>

          {/* Body */}
          <div className="flex-1 overflow-y-auto p-4 space-y-5">
            <section>
              <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                {t('a2a.inspectorCapabilities')}
              </h3>
              {capList.length === 0 ? (
                <p className="text-sm text-muted-foreground mt-2">{t('a2a.inspectorNoCaps')}</p>
              ) : (
                <div className="flex flex-wrap gap-1.5 mt-2">
                  {capList.map((c) => (
                    <Badge key={c} variant="outline" className="text-xs">
                      {c}
                    </Badge>
                  ))}
                </div>
              )}
            </section>

            <section>
              <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                {t('a2a.inspectorSkills')}
              </h3>
              {skillList.length === 0 ? (
                <p className="text-sm text-muted-foreground mt-2">{t('a2a.inspectorNoSkills')}</p>
              ) : (
                <div className="flex flex-wrap gap-1.5 mt-2">
                  {skillList.map((s) => (
                    <Badge key={s} variant="secondary" className="text-xs">
                      {s}
                    </Badge>
                  ))}
                </div>
              )}
            </section>

            <section>
              <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                {t('a2a.inspectorLastMessages')}
              </h3>
              {isMessagesLoading ? (
                <p className="text-sm text-muted-foreground mt-2">{t('a2a.inspectorLoading')}</p>
              ) : recentMessages.length === 0 ? (
                <p className="text-sm text-muted-foreground mt-2">{t('a2a.inspectorNoMessages')}</p>
              ) : (
                <ul className="mt-2 space-y-2" data-testid="a2a-inspector-messages">
                  {recentMessages.slice(0, 5).map((m) => (
                    <li
                      key={m.request_id}
                      className="rounded-md border bg-background p-2 text-xs space-y-1"
                    >
                      <div className="flex items-center justify-between">
                        <span className="font-mono text-2xs text-muted-foreground">
                          {new Date(m.timestamp).toLocaleTimeString()}
                        </span>
                        <Badge variant="outline" className="text-2xs">
                          {m.message_type}
                        </Badge>
                      </div>
                      <div>
                        <span className="font-medium">{m.from_agent}</span>
                        <span className="text-muted-foreground mx-1"></span>
                        <span className="font-medium">{m.to_agent}</span>
                      </div>
                      {m.payload_summary && (
                        <p className="text-muted-foreground truncate">{m.payload_summary}</p>
                      )}
                    </li>
                  ))}
                </ul>
              )}
            </section>
          </div>

          {/* Footer actions */}
          <footer className="border-t p-4 flex items-center gap-2">
            <Button
              variant="outline"
              size="sm"
              onClick={() => onViewTrace?.(node.id)}
              className="flex-1"
            >
              <ExternalLink className="h-3.5 w-3.5" />
              {t('a2a.inspectorViewTrace')}
            </Button>
            <Button
              variant="destructive"
              size="sm"
              onClick={() => onStopAgent?.(node.id)}
              className="flex-1"
            >
              <Square className="h-3.5 w-3.5" />
              {t('a2a.inspectorStopAgent')}
            </Button>
          </footer>

          <div className="px-4 pb-4 text-2xs text-muted-foreground flex items-center gap-1">
            <Power className="h-3 w-3" aria-hidden="true" />
            {node.last_seen
              ? `${t('a2a.inspectorLastSeen')}: ${new Date(node.last_seen).toLocaleString()}`
              : t('a2a.neverSeen')}
          </div>
        </div>
      </aside>
    </div>
  )
}