oxios 1.13.0

Oxios Agent OS — Agent Operating System powered by oxi-sdk
/**
 * Monitor node — enhanced React Flow custom node for the unified agent canvas.
 *
 * Shows the agent name, a pulsing status dot, and duration. On hover the
 * card expands to reveal cost, tokens, model, and A2A capability chips.
 * Clicking opens the detail panel (via onSelect callback).
 */

import { memo } from 'react'
import { Handle, Position } from 'reactflow'
import { statusBorder, statusDot } from '@/components/shared/status-palette'
import { cn } from '@/lib/utils'
import type { MonitorNode as MonitorNodeType } from '@/types/agent-monitor'

/** Data injected by the canvas into each React Flow node. */
export interface MonitorNodeData extends MonitorNodeType {
  selected: boolean
  onSelect: (id: string) => void
}

/** Format seconds → human duration (e.g. "3m 12s"). */
function formatDuration(secs: number | null): string {
  if (secs == null) return '—'
  if (secs < 60) return `${Math.floor(secs)}s`
  const m = Math.floor(secs / 60)
  const s = Math.floor(secs % 60)
  if (m < 60) return s > 0 ? `${m}m ${s}s` : `${m}m`
  const h = Math.floor(m / 60)
  return `${h}h ${m % 60}m`
}

/** Compact cost formatter — $0.018, or "—" for zero. */
function formatCost(usd: number): string {
  return usd > 0 ? `$${usd.toFixed(usd < 0.01 ? 4 : 3)}` : '—'
}

/** Compact token formatter — 48.2k, 1.2M. */
function formatTokens(n: number): string {
  if (n === 0) return '—'
  if (n < 1000) return String(n)
  if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`
  return `${(n / 1_000_000).toFixed(1)}M`
}

function MonitorNodeInner({ data }: { data: MonitorNodeData }) {
  const { name, displayStatus, lifecycle, a2a, selected, onSelect, agentId } = data
  const isRunning = displayStatus === 'running'

  return (
    // biome-ignore lint/a11y/useSemanticElements: React Flow custom node container must remain a div to preserve connection handles and DnD
    <div
      className={cn(
        'group relative w-[200px] cursor-pointer rounded-lg border bg-card p-3 shadow-sm',
        'transition-all duration-200 ease-[var(--animate-in-easing)]',
        'hover:shadow-lg hover:border-primary/40',
        statusBorder(displayStatus),
        selected && 'ring-2 ring-primary/50 shadow-md',
      )}
      onClick={() => onSelect(agentId)}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault()
          onSelect(agentId)
        }
      }}
      role="button"
      tabIndex={0}
      aria-label={`Agent ${name}, ${displayStatus}`}
    >
      {/* React Flow connection handles (hidden — edges are A2A-derived) */}
      <Handle type="target" position={Position.Top} className="!h-0 !w-0 !border-0 !opacity-0" />
      <Handle type="source" position={Position.Bottom} className="!h-0 !w-0 !border-0 !opacity-0" />

      {/* Header: status dot + name */}
      <div className="flex items-center gap-2">
        <span
          className={cn('h-2 w-2 shrink-0 rounded-full', statusDot(displayStatus))}
          aria-hidden="true"
        />
        <span className="truncate text-sm font-medium" title={name}>
          {name}
        </span>
      </div>

      {/* Subtitle: status + duration */}
      <div className="mt-1 flex items-center gap-1.5 text-2xs text-muted-foreground">
        <span className={cn('font-medium', isRunning && 'text-success')}>{displayStatus}</span>
        <span aria-hidden="true">·</span>
        <span className="font-mono">
          {isRunning && lifecycle.duration_secs == null ? (
            <span className="text-success">running…</span>
          ) : (
            formatDuration(lifecycle.duration_secs)
          )}
        </span>
      </div>

      {/* Hover-reveal metrics (expand on group-hover) */}
      <div className="grid grid-rows-[0fr] transition-all duration-200 ease-[var(--animate-in-easing)] group-hover:grid-rows-[1fr]">
        <div className="overflow-hidden">
          <div className="mt-2 space-y-1.5 border-t pt-2">
            {/* Cost + tokens */}
            <div className="flex items-center justify-between text-2xs">
              <span className="text-muted-foreground">Cost</span>
              <span className="font-mono">{formatCost(lifecycle.cost_usd)}</span>
            </div>
            <div className="flex items-center justify-between text-2xs">
              <span className="text-muted-foreground">Tokens</span>
              <span className="font-mono">{formatTokens(lifecycle.tokens_used)}</span>
            </div>
            {lifecycle.model_id && (
              <div className="flex items-center justify-between text-2xs">
                <span className="text-muted-foreground">Model</span>
                <span
                  className="truncate font-mono text-muted-foreground"
                  title={lifecycle.model_id}
                >
                  {lifecycle.model_id}
                </span>
              </div>
            )}

            {/* A2A capability chips (if registered) */}
            {a2a && a2a.capabilities.length > 0 && (
              <div className="flex flex-wrap gap-1 pt-1">
                {a2a.capabilities.slice(0, 4).map((cap) => (
                  <span
                    key={cap}
                    className="rounded bg-info-muted px-1.5 py-0.5 text-2xs font-medium text-info"
                  >
                    {cap}
                  </span>
                ))}
                {a2a.capabilities.length > 4 && (
                  <span className="text-2xs text-muted-foreground">
                    +{a2a.capabilities.length - 4}
                  </span>
                )}
              </div>
            )}

            {/* Error indicator */}
            {lifecycle.error && (
              <div className="truncate text-2xs text-error" title={lifecycle.error}>
                ⚠ {lifecycle.error}
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  )
}

export const MonitorNode = memo(MonitorNodeInner)