adk-gateway 1.0.0

Multi-channel AI gateway for adk-rust agents — Telegram, Slack, WhatsApp, Discord, Matrix + control panel
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useApi } from '../../hooks/useApi';
import { useWebSocket } from '../../hooks/useWebSocket';
import { api } from '../../api/client';
import ConnectionBadge from './components/ConnectionBadge';
import type { CodingAgentSummary } from '../../types';

function formatTimestamp(iso: string | null): string {
  if (!iso) return 'No tasks yet';
  const date = new Date(iso);
  return date.toLocaleString();
}

interface AgentCardProps {
  agent: CodingAgentSummary;
  onClick: () => void;
}

function AgentCard({ agent, onClick }: AgentCardProps) {
  return (
    <button
      type="button"
      onClick={onClick}
      className="bg-white rounded-xl shadow-sm p-6 text-left w-full hover:shadow-md transition-shadow cursor-pointer"
    >
      <div className="flex items-start justify-between mb-3">
        <h3 className="text-sm font-semibold text-gray-900 truncate">
          {agent.alias || agent.id}
        </h3>
        <ConnectionBadge status={agent.connection_status} />
      </div>
      <p className="text-xs text-gray-500 mb-2">{agent.display_name}</p>
      <p className="text-xs text-gray-400">
        {formatTimestamp(agent.last_task_at)}
      </p>
    </button>
  );
}

function LoadingSkeleton() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {[1, 2, 3].map((i) => (
        <div key={i} className="bg-white rounded-xl shadow-sm p-6 animate-pulse">
          <div className="flex items-start justify-between mb-3">
            <div className="h-4 w-24 bg-gray-200 rounded" />
            <div className="h-5 w-16 bg-gray-200 rounded-full" />
          </div>
          <div className="h-3 w-32 bg-gray-200 rounded mb-2" />
          <div className="h-3 w-20 bg-gray-200 rounded" />
        </div>
      ))}
    </div>
  );
}

export default function AgentListView() {
  const navigate = useNavigate();
  const { data: agents, loading, error, refetch } = useApi<CodingAgentSummary[]>(
    () => api.codingAgents(),
    []
  );
  const { lastEvent } = useWebSocket();
  const [localAgents, setLocalAgents] = useState<CodingAgentSummary[]>([]);

  // Sync local state when API data loads
  useEffect(() => {
    if (agents) {
      setLocalAgents(agents);
    }
  }, [agents]);

  // Handle real-time WebSocket events
  useEffect(() => {
    if (!lastEvent) return;

    if (lastEvent.type === 'coding_agent_status') {
      setLocalAgents(prev =>
        prev.map(a =>
          a.id === lastEvent.agent_id
            ? { ...a, connection_status: lastEvent.status }
            : a
        )
      );
    }

    if (lastEvent.type === 'coding_agent_task') {
      setLocalAgents(prev =>
        prev.map(a =>
          a.id === lastEvent.agent_id
            ? { ...a, last_task_at: lastEvent.task.started_at }
            : a
        )
      );
    }
  }, [lastEvent]);

  if (loading) {
    return (
      <div>
        <div className="flex items-center justify-between mb-5">
          <h2 className="text-2xl font-semibold">Coding Agents</h2>
        </div>
        <LoadingSkeleton />
      </div>
    );
  }

  if (error) {
    return (
      <div>
        <div className="flex items-center justify-between mb-5">
          <h2 className="text-2xl font-semibold">Coding Agents</h2>
        </div>
        <div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center justify-between">
          <p className="text-sm text-red-700">Failed to load agents: {error}</p>
          <button
            onClick={refetch}
            className="px-3 py-1 text-xs font-medium text-red-700 bg-red-100 rounded-lg hover:bg-red-200"
          >
            Retry
          </button>
        </div>
      </div>
    );
  }

  if (localAgents.length === 0) {
    return (
      <div>
        <div className="flex items-center justify-between mb-5">
          <h2 className="text-2xl font-semibold">Coding Agents</h2>
        </div>
        <div className="text-center py-16">
          <p className="text-gray-500 mb-4">No coding agents registered yet.</p>
          <button
            onClick={() => navigate('/ui/coding-agents/new')}
            className="px-4 py-2 text-sm font-medium bg-[var(--color-accent)] text-white rounded-lg hover:bg-[var(--color-accent-hover)]"
          >
            Add Agent
          </button>
        </div>
      </div>
    );
  }

  return (
    <div>
      <div className="flex items-center justify-between mb-5">
        <h2 className="text-2xl font-semibold">Coding Agents</h2>
        <button
          onClick={() => navigate('/ui/coding-agents/new')}
          className="px-4 py-2 text-sm font-medium bg-[var(--color-accent)] text-white rounded-lg hover:bg-[var(--color-accent-hover)]"
        >
          Add Agent
        </button>
      </div>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {localAgents.map((agent) => (
          <AgentCard
            key={agent.id}
            agent={agent}
            onClick={() => navigate(`/ui/coding-agents/${agent.id}`)}
          />
        ))}
      </div>
    </div>
  );
}