oxios 1.12.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
import { Bot, Power } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Handle, type NodeProps, Position } from 'reactflow'
import { statusBorder, statusDot } from '@/components/shared/status-palette'
import { cn } from '@/lib/utils'
import type { TopologyNode } from '@/types/a2a'

/** Human-readable "X seconds ago" / "X minutes ago" formatter. */
function formatLastSeen(
  iso: string | null,
  t: (k: string, opts?: { count?: number }) => string,
): string {
  if (!iso) return t('a2a.neverSeen')
  const ts = new Date(iso).getTime()
  if (Number.isNaN(ts)) return t('a2a.neverSeen')
  const diffSec = Math.max(0, Math.floor((Date.now() - ts) / 1000))
  if (diffSec < 60) return t('a2a.lastSeenSeconds', { count: diffSec })
  const min = Math.floor(diffSec / 60)
  if (min < 60) return t('a2a.lastSeenMinutes', { count: min })
  const hr = Math.floor(min / 60)
  if (hr < 24) return t('a2a.lastSeenHours', { count: hr })
  const day = Math.floor(hr / 24)
  return t('a2a.lastSeenDays', { count: day })
}

export interface AgentNodeData extends TopologyNode {
  /** Optional click handler so the parent can open the inspector. */
  onSelect?: (id: string) => void
  /** True when the node is the currently selected one. */
  selected?: boolean
}

/**
 * Custom React Flow node for an A2A agent.
 *
 * Shows the agent name, a colored status dot, a count of
 * capabilities and skills, and the last-seen time. Source and
 * target handles are on the left/right so edges flow horizontally.
 */
export function AgentNode({ data, id }: NodeProps<AgentNodeData>) {
  const { t } = useTranslation()
  const borderClass = statusBorder(data.status)
  const dotClass = statusDot(data.status)

  return (
    <button
      type="button"
      className={cn(
        'min-w-[200px] max-w-[260px] rounded-lg border-2 bg-card p-3 shadow-sm transition focus:outline-none focus:ring-2 focus:ring-ring text-left',
        borderClass,
        data.selected && 'ring-2 ring-ring',
      )}
      aria-label={t('a2a.nodeAriaLabel', { name: data.label, status: data.status })}
      onKeyDown={(e) => {
        if ((e.key === 'Enter' || e.key === ' ') && data.onSelect) {
          e.preventDefault()
          data.onSelect(id)
        }
      }}
      onClick={() => data.onSelect?.(id)}
    >
      <Handle type="target" position={Position.Left} className="!bg-muted-foreground" />
      <div className="flex items-center gap-2">
        <Bot className="h-4 w-4 text-foreground" aria-hidden="true" />
        <span className="font-medium text-sm text-foreground truncate">{data.label}</span>
        <span
          className={cn('ml-auto h-2 w-2 rounded-full', dotClass)}
          aria-hidden="true"
          title={data.status}
        />
      </div>

      <div className="text-xs text-muted-foreground mt-1.5">
        <span>{t('a2a.capabilitiesCount', { count: data.capabilities?.length ?? 0 })}</span>
        <span className="mx-1.5">·</span>
        <span>{t('a2a.skillsCount', { count: data.skills?.length ?? 0 })}</span>
      </div>

      <div className="text-2xs text-muted-foreground/80 mt-0.5 flex items-center gap-1">
        <Power className="h-2.5 w-2.5" aria-hidden="true" />
        {formatLastSeen(data.last_seen, t)}
      </div>
      <Handle type="source" position={Position.Right} className="!bg-muted-foreground" />
    </button>
  )
}