OpenClawDashboard/dashboard/src/components/agent-strip.tsx
Manohar 4ee0517345 feat(dashboard): agents page, knowledge, schedule/digest cards, dual-source tasks
- app/agents/: new Agents page showing per-agent status + workspace files
- app/knowledge/: Knowledge base viewer
- app/api/tiger/: proxy routes for cron, file-tasks, file-projects, keys, agents
- components/agent-strip.tsx: agent status bar for dashboard home
- components/command-bar.tsx: command palette
- components/digest-card.tsx + schedule-card.tsx: weekly digest + cron schedule
- components/status-footer.tsx: system status footer
- tasks page: dual-source (TASKS.md JSON block + SQLite) with fallback
- projects page: PROJECTS.md reader with kanban board integration
2026-05-02 20:11:43 +00:00

148 lines
4.1 KiB
TypeScript

"use client"
/**
* agent-strip.tsx — Live state of Tiger + sub-agents
*
* Replaces the old "Agent Model" card on the home page. Now you see ALL 5
* agents at once with status, last activity, and file count.
*
* DATA SOURCE: /api/tiger/agents — already exists, returns:
* { ok: true, agents: [{ id, name, emoji, role, fileCount, lastActivity }] }
*/
import * as React from "react"
import useSWR from "swr"
import { cn } from "@/lib/utils"
interface Agent {
id: string
name: string
emoji: string
role: string
fileCount: number
lastActivity: number
}
interface AgentsResponse {
ok: boolean
agents: Agent[]
}
const fetcher = (url: string) => fetch(url).then((r) => r.json())
function relativeTime(ts: number): string {
if (!ts) return "—"
const diff = Date.now() - ts
const m = Math.floor(diff / 60_000)
if (m < 1) return "now"
if (m < 60) return `${m}m`
const h = Math.floor(m / 60)
if (h < 24) return `${h}h`
const d = Math.floor(h / 24)
return `${d}d`
}
function statusOf(ts: number): "active" | "recent" | "idle" {
if (!ts) return "idle"
const diff = Date.now() - ts
if (diff < 5 * 60_000) return "active"
if (diff < 60 * 60_000) return "recent"
return "idle"
}
const STATUS_DOT: Record<"active" | "recent" | "idle", string> = {
active: "bg-green-500 animate-pulse",
recent: "bg-amber-500",
idle: "bg-zinc-500",
}
export function AgentStrip() {
const { data, error, isLoading } = useSWR<AgentsResponse>(
"/api/tiger/agents",
fetcher,
{ refreshInterval: 30_000 }
)
if (isLoading) {
return (
<div>
<SectionLabel>Agents</SectionLabel>
<div className="flex gap-3 overflow-x-auto pb-2 snap-x">
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="min-w-[140px] h-[80px] rounded-lg border border-border/50 bg-card/30 animate-pulse"
/>
))}
</div>
</div>
)
}
if (error || !data?.ok) {
return (
<div>
<SectionLabel>Agents</SectionLabel>
<div className="text-sm text-muted-foreground p-3 rounded-lg border border-border/50">
Could not load agents. Bridge unreachable?
</div>
</div>
)
}
const agents = data.agents ?? []
return (
<div>
<div className="flex items-baseline justify-between mb-2">
<SectionLabel>Agents</SectionLabel>
<a href="/agents" className="text-xs text-primary hover:underline">
View all
</a>
</div>
<div className="flex gap-3 overflow-x-auto pb-2 snap-x snap-mandatory">
{agents.map((agent) => {
const status = statusOf(agent.lastActivity)
return (
<a
key={agent.id}
href={`/agents?id=${agent.id}`}
className="snap-start shrink-0 min-w-[140px] p-3 rounded-lg border border-border/50 bg-card/40 hover:bg-card/60 hover:border-primary/30 transition-colors cursor-pointer"
>
<div className="flex items-center gap-2 mb-1">
<span className="text-lg leading-none">{agent.emoji}</span>
<span className="text-sm font-medium truncate flex-1">
{agent.name}
</span>
<span
className={cn(
"h-2 w-2 rounded-full shrink-0",
STATUS_DOT[status]
)}
/>
</div>
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">
{agent.role}
</div>
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
<span>last: {relativeTime(agent.lastActivity)}</span>
<span className="tabular-nums">{agent.fileCount} files</span>
</div>
</a>
)
})}
</div>
</div>
)
}
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<span className="text-[11px] uppercase tracking-wider text-muted-foreground/80">
{children}
</span>
)
}