Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
383 changes: 338 additions & 45 deletions Cargo.lock

Large diffs are not rendered by default.

118 changes: 27 additions & 91 deletions src/app/agent-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { Link } from "react-router-dom";
import { useQuery } from "@/lib/graphql/client";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { DaemonDocument } from "@/lib/graphql/generated/graphql";
import type { DaemonQuery } from "@/lib/graphql/generated/graphql";
import { StatusDot, PageLoading, PageError, SectionHeading } from "./shared";

const PHASE_OUTPUT_QUERY = `query PhaseOutput($workflowId: ID!, $phaseId: String, $tail: Int) { phaseOutput(workflowId: $workflowId, phaseId: $phaseId, tail: $tail) { lines phaseId hasMore } }`;
// NOTE(E-followup): the kernel schema exposes no per-agent phase-output stream
// (only `daemonAgents` session metadata + the `workflowEvents`/`daemonEvents`
// subscriptions), so agent cards show session metadata only. A richer per-agent
// output view requires a transport-graphql schema extension.
type AgentInfo = NonNullable<DaemonQuery["daemonAgents"]>[number];

function useElapsedTime(startedAt: string | null | undefined): string {
const [, setTick] = useState(0);
Expand All @@ -22,7 +26,7 @@ function useElapsedTime(startedAt: string | null | undefined): string {
}

function formatDuration(ms: number): string {
if (ms < 0) return "0s";
if (ms < 0 || Number.isNaN(ms)) return "0s";
const s = Math.floor(ms / 1000);
if (s < 60) return `${s}s`;
const m = Math.floor(s / 60);
Expand All @@ -31,70 +35,25 @@ function formatDuration(ms: number): string {
return `${h}h ${m % 60}m`;
}

function AgentOutputPreview({ workflowId, phaseId }: { workflowId: string; phaseId: string | null | undefined }) {
const [result] = useQuery<{ phaseOutput: { lines: string[]; phaseId: string; hasMore: boolean } }>({
query: PHASE_OUTPUT_QUERY,
variables: { workflowId, phaseId: phaseId ?? undefined, tail: 10 },
pause: !workflowId,
});

const lines = result.data?.phaseOutput?.lines ?? [];

if (result.fetching && lines.length === 0) {
return (
<div className="rounded border border-border/30 bg-background/40 p-3">
<p className="text-[11px] text-muted-foreground/40 font-mono">Loading output...</p>
</div>
);
}

if (lines.length === 0) {
return (
<div className="rounded border border-border/30 bg-background/40 p-3">
<p className="text-[11px] text-muted-foreground/40 font-mono">No output yet</p>
</div>
);
}

return (
<div className="rounded border border-border/30 bg-background/40 p-3 max-h-40 overflow-y-auto">
<div className="space-y-0.5">
{lines.map((line, i) => (
<div key={i} className="font-mono text-[11px] text-foreground/60 whitespace-pre-wrap break-all">
{line}
</div>
))}
<span className="inline-block w-1.5 h-3.5 bg-primary/60 animate-pulse" />
</div>
</div>
);
}

function AgentCard({ agent }: { agent: { runId: string; taskId?: string | null; taskTitle?: string | null; workflowId?: string | null; phaseId?: string | null; status: string } }) {
const elapsed = useElapsedTime(new Date().toISOString());
function AgentCard({ agent }: { agent: AgentInfo }) {
const elapsed = useElapsedTime(agent.startedAt);

return (
<Card className="border-border/40 bg-card/60">
<CardHeader className="pb-2 pt-3 px-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<StatusDot status={agent.status} />
<span className="font-mono text-sm text-foreground/80">agent-{agent.runId}</span>
<StatusDot status="running" />
<span className="font-mono text-sm text-foreground/80">{agent.sessionId}</span>
</div>
<Badge variant="secondary" className="text-[10px] h-5 px-2">{agent.status}</Badge>
<Badge variant="secondary" className="text-[10px] h-5 px-2">{agent.provider}</Badge>
</div>
</CardHeader>
<CardContent className="px-4 pb-4 space-y-3">
<div className="space-y-1 text-xs">
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Task:</span>
{agent.taskId ? (
<Link to={`/tasks/${agent.taskId}`} className="text-primary/80 hover:text-primary transition-colors">
{agent.taskId} {agent.taskTitle ? `"${agent.taskTitle}"` : ""}
</Link>
) : (
<span className="text-muted-foreground/40">-</span>
)}
<span className="text-muted-foreground">Model:</span>
<span className="font-mono text-foreground/70">{agent.model}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Workflow:</span>
Expand All @@ -115,43 +74,20 @@ function AgentCard({ agent }: { agent: { runId: string; taskId?: string | null;
<span className="font-mono text-foreground/70">{elapsed || "-"}</span>
</div>
</div>

{agent.workflowId && (
<div className="space-y-1.5">
<SectionHeading>Output (last 10 lines)</SectionHeading>
<AgentOutputPreview workflowId={agent.workflowId} phaseId={agent.phaseId} />
</div>
)}

<div className="flex items-center gap-2 pt-1">
{agent.workflowId && (
<>
<Button size="sm" variant="outline" className="h-6 text-[11px] px-2" asChild>
<Link to={`/workflows/${agent.workflowId}`}>View Full Output</Link>
</Button>
<Button size="sm" variant="ghost" className="h-6 text-[11px] px-2" asChild>
<Link to={`/workflows/${agent.workflowId}`}>View Workflow</Link>
</Button>
</>
)}
</div>
</CardContent>
</Card>
);
}

export function AgentManagementPage() {
const [result] = useQuery({ query: DaemonDocument });
const [result] = useQuery<DaemonQuery>({ query: DaemonDocument });
const { data, fetching, error } = result;

if (fetching) return <PageLoading />;
if (error) return <PageError message={error.message} />;

const status = data?.daemonStatus;
const health = data?.daemonHealth;
const agents = data?.agentRuns ?? [];
const activeCount = agents.filter((a: { status: string }) => a.status.toLowerCase() === "running").length;
const maxAgents = status?.maxAgents ?? health?.activeAgents ?? 0;
const agents = data?.daemonAgents ?? [];

const overallHealth = agents.length > 0 ? "running" : health?.healthy ? "healthy" : "error";

Expand All @@ -160,7 +96,7 @@ export function AgentManagementPage() {
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold tracking-tight">Agents</h1>
<span className="text-sm text-muted-foreground/70">
{activeCount} active / {maxAgents} capacity
{agents.length} active
</span>
<StatusDot status={overallHealth} />
</div>
Expand All @@ -185,37 +121,37 @@ export function AgentManagementPage() {
<SectionHeading>Active Agents</SectionHeading>
<div className="grid md:grid-cols-2 gap-4">
{agents.map((a) => (
<AgentCard key={a.runId} agent={a} />
<AgentCard key={a.sessionId} agent={a} />
))}
</div>
</div>
)}

<div className="space-y-2">
<SectionHeading>System Capacity</SectionHeading>
<SectionHeading>Daemon</SectionHeading>
<Card className="border-border/40 bg-card/60">
<CardContent className="space-y-1 px-4 py-3 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">Max Agents</span>
<span className="font-mono text-foreground/70">{status?.maxAgents ?? "-"}</span>
<span className="text-muted-foreground">Status</span>
<span className="font-mono text-foreground/70">{health?.status ?? "-"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Runner Connected</span>
<span className="text-muted-foreground">Healthy</span>
<span className="font-mono text-foreground/70">
{health?.runnerConnected ? (
{health?.healthy ? (
<Badge variant="outline" className="text-[10px] h-4 px-1.5 border-primary/20 text-primary/70">yes</Badge>
) : (
"no"
)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Runner PID</span>
<span className="font-mono text-foreground/70">{health?.runnerPid ?? "-"}</span>
<span className="text-muted-foreground">Running</span>
<span className="font-mono text-foreground/70">{data?.daemon?.running ? "yes" : "no"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Daemon PID</span>
<span className="font-mono text-foreground/70">{health?.daemonPid ?? "-"}</span>
<span className="text-muted-foreground">PID</span>
<span className="font-mono text-foreground/70">{data?.daemon?.pid ?? "-"}</span>
</div>
</CardContent>
</Card>
Expand Down
Loading
Loading