oxios 1.10.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { createFileRoute } from '@tanstack/react-router'
import { Network } from 'lucide-react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { AgentCardList } from '@/components/a2a/agent-card-list'
import { AgentInspector } from '@/components/a2a/agent-inspector'
import { InteractiveTopology } from '@/components/a2a/interactive-topology'
import { MessageLog } from '@/components/a2a/message-log'
import { RefreshButton } from '@/components/shared/refresh-button'
import { useA2AAgents, useA2AMessages, useA2ATopology } from '@/hooks/use-a2a'
import { cn } from '@/lib/utils'
import type { A2AMessage } from '@/types/a2a'

export const Route = createFileRoute('/a2a')({ component: A2APage })

type Tab = 'topology' | 'messages' | 'agents'

function A2APage() {
  const { t } = useTranslation()
  const [tab, setTab] = useState<Tab>('topology')
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)

  const agentsQ = useA2AAgents()
  const messagesQ = useA2AMessages()
  const topologyQ = useA2ATopology()

  const isFetching = agentsQ.isFetching || messagesQ.isFetching || topologyQ.isFetching

  const refetchAll = useCallback(() => {
    agentsQ.refetch()
    messagesQ.refetch()
    topologyQ.refetch()
  }, [agentsQ, messagesQ, topologyQ])

  const tabs: { key: Tab; labelKey: string }[] = [
    { key: 'topology', labelKey: 'a2a.topology' },
    { key: 'messages', labelKey: 'a2a.messages' },
    { key: 'agents', labelKey: 'a2a.agents' },
  ]

  // Find the selected node + matching agent card.
  const selectedNode = useMemo(
    () => topologyQ.data?.nodes.find((n) => n.id === selectedNodeId) ?? null,
    [topologyQ.data?.nodes, selectedNodeId],
  )

  const selectedAgentCard = useMemo(() => {
    if (!selectedNodeId) return null
    return agentsQ.data?.find((a) => a.name === selectedNodeId) ?? null
  }, [agentsQ.data, selectedNodeId])

  // Most recent 5 messages involving the selected agent.
  const selectedMessages: A2AMessage[] = useMemo(() => {
    if (!selectedNodeId) return []
    return (Array.isArray(messagesQ.data) ? messagesQ.data : [])
      .filter((m) => m.from_agent === selectedNodeId || m.to_agent === selectedNodeId)
      .slice(0, 5)
  }, [messagesQ.data, selectedNodeId])

  const handleNodeSelect = useCallback((id: string) => {
    setSelectedNodeId(id)
  }, [])

  const handleViewTrace = useCallback(
    (id: string) => {
      // Trace view is not yet implemented — surface the gap with a toast
      // rather than a silent console.info (which made the destructive
      // [Stop agent] button look broken).
      toast(t('a2a.traceNotImplemented'))
      // `id` is captured for the future router hook-up:
      //   navigate({ to: '/agents/$id/trace', params: { id } })
      void id
    },
    [toast, t],
  )

  const handleStopAgent = useCallback(
    (id: string) => {
      // Stop-agent endpoint is not yet implemented. Honest UX: tell the
      // user via toast instead of logging to the console.
      toast.error(t('a2a.stopNotImplemented'))
      // `id` is captured for the future mutation hook-up:
      //   api.stopAgent(id)
      void id
    },
    [toast, t],
  )

  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-2xl font-bold flex items-center gap-2">
            <Network className="h-6 w-6" /> {t('a2a.title')}
          </h1>
          <p className="text-muted-foreground">{t('a2a.subtitle')}</p>
        </div>
        <RefreshButton onClick={refetchAll} isFetching={isFetching} />
      </div>

      {/* Tab switcher */}
      <div className="inline-flex h-9 items-center rounded-lg bg-muted p-1 text-muted-foreground gap-0.5">
        {tabs.map((tb) => (
          <button
            type="button"
            key={tb.key}
            onClick={() => setTab(tb.key)}
            className={cn(
              'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium transition-all',
              tab === tb.key ? 'bg-background text-foreground shadow' : 'hover:bg-background/50',
            )}
          >
            {t(tb.labelKey)}
          </button>
        ))}
      </div>

      {/* Content */}
      {tab === 'topology' && (
        <InteractiveTopology
          nodes={Array.isArray(topologyQ.data?.nodes) ? topologyQ.data.nodes : []}
          edges={Array.isArray(topologyQ.data?.edges) ? topologyQ.data.edges : []}
          isLoading={topologyQ.isLoading}
          isError={topologyQ.isError}
          onRetry={() => topologyQ.refetch()}
          onNodeSelect={handleNodeSelect}
          selectedNodeId={selectedNodeId}
        />
      )}
      {tab === 'messages' && (
        <MessageLog messages={Array.isArray(messagesQ.data) ? messagesQ.data : []} />
      )}
      {tab === 'agents' && (
        <AgentCardList agents={Array.isArray(agentsQ.data) ? agentsQ.data : []} />
      )}

      <AgentInspector
        node={selectedNode}
        open={selectedNode != null}
        onClose={() => setSelectedNodeId(null)}
        agentCard={selectedAgentCard}
        recentMessages={selectedMessages}
        isMessagesLoading={messagesQ.isLoading}
        onViewTrace={handleViewTrace}
        onStopAgent={handleStopAgent}
      />
    </div>
  )
}