AkurAI Build
Menu

popagent

public

Latest change 2fb6f198c4c71ef37dffc8ac5dca8482068a7bfc - Add governed AkurAI Build maintenance by AkurAI Build

import { useEffect, useRef, useState } from "react";
import { AlertTriangle, Bot, CheckCircle2, CircleDashed, LoaderCircle, RotateCcw, TerminalSquare, Wrench, X, XCircle } from "lucide-react";
import type { AgentActivity, AgentTask, AgentTaskStatus, AgentToolActivity } from "../api-types";
import { useFocusTrap } from "./use-focus-trap";

export type TaskKanbanLane = "queued" | "active" | "attention" | "done";

export function taskKanbanLane(status: AgentTaskStatus): TaskKanbanLane {
  if (status === "queued") return "queued";
  if (status === "running" || status === "cancelling") return "active";
  if (status === "completed") return "done";
  return "attention";
}

const LANES: Array<{
  id: TaskKanbanLane;
  title: string;
  empty: string;
  accent: string;
  icon: typeof CircleDashed;
}> = [
  { id: "queued", title: "Queued", empty: "No work waiting.", accent: "bg-sky-400", icon: CircleDashed },
  { id: "active", title: "In progress", empty: "No agents working.", accent: "bg-accent", icon: LoaderCircle },
  { id: "attention", title: "Needs attention", empty: "No blocked or failed work.", accent: "bg-amber-400", icon: AlertTriangle },
  { id: "done", title: "Done", empty: "No finished work yet.", accent: "bg-emerald-500", icon: CheckCircle2 },
];

function taskTitle(prompt: string): string {
  const firstLine = prompt.split("\n").find((line) => line.trim())?.trim() ?? "Untitled task";
  return firstLine.length > 120 ? `${firstLine.slice(0, 117)}…` : firstLine;
}

function statusLabel(status: AgentTaskStatus): string {
  return status === "dead-letter" ? "Dead-lettered" : status[0]!.toUpperCase() + status.slice(1);
}

function TaskCard({ task, stepLimit, workspaceName, onCancel, onRemove, onOpen }: { task: AgentTask; stepLimit?: number; workspaceName?: string; onCancel: (task: AgentTask) => void; onRemove: (task: AgentTask) => void; onOpen: (task: AgentTask) => void }) {
  const active = task.status === "running" || task.status === "cancelling";
  const cancellable = task.status === "queued" || task.status === "running";
  const failed = task.status === "failed" || task.status === "dead-letter";
  const removable = task.status === "completed" || task.status === "failed" || task.status === "cancelled" || task.status === "dead-letter";
  const timestamp = task.completedAt ?? task.startedAt ?? task.createdAt;

  return <article className="group rounded-lg border border-line bg-bg p-3.5 shadow-[0_1px_0_rgba(255,255,255,0.03)] transition-colors hover:border-ink-dim/40" aria-label={`${statusLabel(task.status)} task: ${taskTitle(task.prompt)}`}>
    <div className="flex items-start justify-between gap-3">
      <div className="min-w-0">
        <h3 className="line-clamp-2 text-sm font-medium leading-snug text-ink">{taskTitle(task.prompt)}</h3>
        <p className="mt-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-ink-dim">{statusLabel(task.status)} · attempt {task.attemptCount}/{task.maxAttempts}</p>
        {workspaceName ? <p className="mt-1 truncate text-[11px] text-accent">{workspaceName}</p> : null}
      </div>
      {task.status === "running" ? <LoaderCircle aria-hidden="true" className="mt-0.5 size-4 shrink-0 animate-spin text-accent motion-reduce:animate-none" /> : task.status === "completed" ? <CheckCircle2 aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-emerald-500" /> : failed ? <XCircle aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-red-400" /> : null}
    </div>

    {active ? <div className="mt-3 rounded-md border border-accent/15 bg-accent-soft/40 p-2.5">
      <div className="flex items-start justify-between gap-3 text-xs">
        <span className="min-w-0 leading-relaxed text-ink-muted">{task.progress ?? (task.status === "cancelling" ? "Stopping safely…" : "Starting work…")}</span>
        <span className="shrink-0 font-mono text-[10px] text-ink-dim">{stepLimit ? `${task.stepsCompleted} of ${stepLimit} steps` : `${task.stepsCompleted} steps`}</span>
      </div>
      {stepLimit ? <progress aria-label={`Progress for ${taskTitle(task.prompt)}`} max={stepLimit} value={Math.min(task.stepsCompleted, stepLimit)} className="mt-2 h-1.5 w-full accent-accent" /> : null}
    </div> : null}

    {task.nextAttemptAt ? <div className="mt-3 flex gap-2 rounded-md border border-amber-500/20 bg-amber-500/5 p-2.5 text-xs text-amber-500"><RotateCcw aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" /><span>Retry {new Date(task.nextAttemptAt).toLocaleString()}{task.lastErrorClass ? ` · ${task.lastErrorClass}` : ""}</span></div> : null}
    {task.error ? <p role="alert" className="mt-3 rounded-md border border-red-500/20 bg-red-500/5 p-2.5 text-xs leading-relaxed text-red-400">{task.error}</p> : null}

    <div className="mt-3 flex items-center justify-between gap-3 border-t border-line pt-2.5 text-[10px] text-ink-dim">
      <span>{new Date(timestamp).toLocaleString()}</span>
      <span className="uppercase tracking-wider">{task.source === "build-maintenance" ? "Build maintenance" : task.source === "self-update" ? "Self-update" : "User"}</span>
    </div>

    <button type="button" onClick={() => onOpen(task)} className="mt-2 flex min-h-10 w-full items-center justify-center gap-2 rounded-md border border-line px-3 text-xs text-ink-muted hover:border-accent/30 hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"><TerminalSquare aria-hidden="true" className="size-3.5" />Open task run</button>

    {cancellable ? <button type="button" onClick={() => onCancel(task)} className="mt-2 min-h-10 w-full rounded-md border border-line px-3 text-xs text-ink-muted hover:border-red-500/30 hover:text-red-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50">Cancel task</button> : null}
    {removable ? <button type="button" onClick={() => onRemove(task)} className="mt-2 min-h-10 w-full rounded-md border border-line px-3 text-xs text-ink-muted hover:border-red-500/30 hover:text-red-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50">Remove from board</button> : null}
  </article>;
}

function jsonPreview(value: unknown): string {
  if (value === undefined) return "";
  try {
    return JSON.stringify(value, null, 2);
  } catch {
    return String(value);
  }
}

function ActivityLine({ activity }: { activity: AgentActivity }) {
  const toolCalls: AgentToolActivity[] = activity.toolCalls ?? activity.tools.map((name, index) => ({
    id: `${activity.runId}-${activity.iteration}-${index}`,
    name,
    args: {},
    status: "complete",
  }));
  const failed = activity.finishReason === "error";
  return <li className="flex gap-3 px-4 py-4 sm:px-6">
    <div className={`mt-0.5 flex size-8 shrink-0 items-center justify-center rounded border ${failed ? "border-red-500/30 bg-red-500/10 text-red-400" : "border-accent/25 bg-accent-soft text-accent"}`}><Bot aria-hidden="true" className="size-4" /></div>
    <div className="min-w-0 flex-1">
      <div className="flex flex-wrap items-center gap-2 text-xs">
        <strong className="text-ink">{activity.agentName}</strong>
        <span className="text-ink-dim">iteration {activity.iteration}{activity.maxIterations ? ` of ${activity.maxIterations}` : ""}</span>
        <span className={`rounded-sm px-1.5 py-0.5 text-[10px] ${failed ? "bg-red-500/10 text-red-400" : activity.isFinal ? "bg-emerald-500/10 text-emerald-500" : "bg-accent-soft text-accent"}`}>{activity.isFinal ? activity.finishReason : activity.text.trim() ? "thinking" : activity.finishReason}</span>
      </div>
      {activity.text.trim() ? <div className="md-content mt-2 whitespace-pre-wrap break-words text-sm leading-relaxed text-ink-muted">{activity.text}</div> : null}
      {toolCalls.map((tool) => {
        const details = [
          ["Arguments", jsonPreview(tool.args)],
          ["Result", jsonPreview(tool.result)],
          ["Error", tool.error ?? ""],
        ].filter((entry) => entry[1]);
        return <details key={tool.id} className={`mt-2 rounded border bg-panel-2 text-xs ${tool.status === "error" ? "border-red-500/30" : "border-line"}`} open={tool.status === "error"}>
          <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-ink-dim focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/50">
            <Wrench aria-hidden="true" className="size-3.5 text-accent" />
            <code className="min-w-0 flex-1 truncate text-ink">{tool.name}</code>
            <span className={tool.status === "error" ? "text-red-400" : ""}>{tool.status}</span>
          </summary>
          {details.length ? <div className="space-y-2 border-t border-line px-3 py-2">{details.map(([label, value]) => <div key={label}><div className="mb-1 font-medium text-ink-dim">{label}</div><pre className="max-h-52 overflow-auto whitespace-pre-wrap break-all rounded bg-bg p-2 text-[11px] text-ink">{value}</pre></div>)}</div> : null}
        </details>;
      })}
    </div>
  </li>;
}

export function TaskTerminalDialog({ task, stepLimit, onClose }: { task?: AgentTask; stepLimit?: number; onClose: () => void }) {
  const dialogRef = useRef<HTMLDivElement>(null);
  const transcriptRef = useRef<HTMLElement>(null);
  useFocusTrap(dialogRef, Boolean(task));
  useEffect(() => {
    if (!task) return;
    dialogRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
    const close = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [onClose, task]);
  useEffect(() => {
    if (task?.status === "running" || task?.status === "cancelling") transcriptRef.current?.scrollTo({ top: transcriptRef.current.scrollHeight, behavior: "smooth" });
  }, [task?.activity?.length, task?.status]);
  if (!task) return null;
  const activity = task.activity ?? [];
  return <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-2 backdrop-blur-sm sm:p-5">
    <div ref={dialogRef} role="dialog" aria-modal="true" aria-labelledby="task-run-title" className="flex h-[min(92vh,58rem)] w-full max-w-6xl flex-col overflow-hidden rounded-xl border border-line bg-bg text-ink shadow-2xl">
      <header className="liquid-header flex items-center gap-3 border-b border-line px-4 py-3">
        <Bot aria-hidden="true" className="size-4 text-accent" />
        <div className="min-w-0 flex-1"><h2 id="task-run-title" className="truncate text-sm font-semibold">Task run</h2><p className="truncate text-[11px] text-ink-dim">{taskTitle(task.prompt)}</p></div>
        <span className="hidden text-xs text-ink-dim sm:block">{task.stepsCompleted}{stepLimit ? ` of ${stepLimit}` : ""} steps · attempt {task.attemptCount}/{task.maxAttempts}</span>
        <button type="button" aria-label="Close task run" onClick={onClose} className="liquid-control flex size-10 items-center justify-center rounded text-ink-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/50"><X className="size-4" /></button>
      </header>
      <div className="flex min-h-0 flex-1 flex-col md:grid md:grid-cols-[17rem_1fr]">
        <aside className="border-b border-line bg-panel/60 p-4 text-xs md:border-b-0 md:border-r">
          <dl className="grid grid-cols-2 gap-3 md:grid-cols-1"><div><dt className="text-ink-dim">Status</dt><dd className="mt-1 text-ink">{statusLabel(task.status)}</dd></div><div><dt className="text-ink-dim">Model</dt><dd className="mt-1 break-all font-mono text-ink-muted">{task.model}</dd></div><div><dt className="text-ink-dim">Current activity</dt><dd className="mt-1 text-ink-muted">{task.progress ?? "Waiting for activity"}</dd></div><div><dt className="text-ink-dim">Started</dt><dd className="mt-1 text-ink-muted">{new Date(task.startedAt ?? task.createdAt).toLocaleString()}</dd></div></dl>
        </aside>
        <section ref={transcriptRef} aria-label="Run activity" aria-live="polite" className="min-h-0 overflow-y-auto bg-bg">
          <div className="flex justify-end px-4 py-4 sm:px-6"><div className="user-bubble max-w-[85%] rounded rounded-br-sm border px-4 py-2.5"><div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-ink-dim">You</div><div className="whitespace-pre-wrap text-sm leading-relaxed">{task.prompt}</div></div></div>
          {activity.length ? <ol className="divide-y divide-line">{activity.map((entry, index) => <ActivityLine key={`${entry.runId}-${entry.iteration}-${index}`} activity={entry} />)}</ol> : <div className="flex min-h-48 items-center justify-center px-6 text-center text-sm text-ink-dim">{task.status === "queued" ? "Waiting for an agent…" : "No detailed activity was recorded for this run."}</div>}
          {task.error ? <div role="alert" className="m-4 rounded border border-red-500/30 bg-red-500/5 p-3 text-xs text-red-400">{task.error}</div> : null}
          {task.output ? <div className="flex gap-3 border-t border-line px-4 py-4 sm:px-6"><div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded border border-accent/25 bg-accent-soft"><Bot className="size-4 text-accent" /></div><div className="min-w-0 flex-1"><strong className="text-xs">Orchistrator</strong><div className="md-content mt-2 whitespace-pre-wrap break-words text-sm leading-relaxed text-ink-muted">{task.output}</div></div></div> : null}
          {(task.status === "running" || task.status === "cancelling") ? <div className="flex items-center gap-2 border-t border-line px-6 py-3 text-xs text-ink-dim"><LoaderCircle className="size-3.5 animate-spin text-accent motion-reduce:animate-none" />Thinking and tool activity update live</div> : null}
        </section>
      </div>
    </div>
  </div>;
}

export function TaskKanbanBoard({
  tasks,
  stepLimit,
  workspaceNames,
  heading = "Work board",
  onCancel,
  onRemove,
}: {
  tasks: AgentTask[];
  stepLimit?: number;
  workspaceNames?: Record<string, string>;
  heading?: string;
  onCancel: (task: AgentTask) => void;
  onRemove: (task: AgentTask) => void;
}) {
  const [openTask, setOpenTask] = useState<AgentTask>();
  const currentOpenTask = openTask ? tasks.find((task) => task.id === openTask.id) ?? openTask : undefined;
  return <section aria-label="Task Kanban board" className="mt-5">
    <div className="mb-3 flex items-end justify-between gap-4">
      <div><h2 className="font-medium">{heading}</h2><p className="mt-1 text-xs text-ink-dim">Live agent work, grouped by execution state.</p></div>
      <span className="text-xs tabular-nums text-ink-dim">{tasks.length} task{tasks.length === 1 ? "" : "s"}</span>
    </div>
    <div className="kanban-scroll -mx-4 flex items-start snap-x snap-mandatory gap-3 overflow-x-auto px-4 pb-3 md:mx-0 md:grid md:snap-none md:grid-cols-2 md:overflow-visible md:px-0 xl:grid-cols-4">
      {LANES.map((lane) => {
        const laneTasks = tasks.filter((task) => taskKanbanLane(task.status) === lane.id);
        const Icon = lane.icon;
        return <section key={lane.id} aria-labelledby={`task-lane-${lane.id}`} className="w-[85vw] max-w-[22rem] shrink-0 snap-start rounded-xl border border-line bg-panel/70 p-2.5 md:w-auto md:max-w-none">
          <header className="mb-2.5 flex items-center gap-2 px-1 py-1">
            <span className={`h-5 w-1 rounded-full ${lane.accent}`} aria-hidden="true" />
            <Icon aria-hidden="true" className={`size-4 text-ink-dim ${lane.id === "active" && laneTasks.length ? "animate-pulse motion-reduce:animate-none" : ""}`} />
            <h3 id={`task-lane-${lane.id}`} className="text-xs font-semibold uppercase tracking-[0.12em] text-ink-muted">{lane.title}</h3>
            <span className="ml-auto rounded-full border border-line bg-bg px-2 py-0.5 text-[10px] tabular-nums text-ink-dim">{laneTasks.length}</span>
          </header>
          <div className="max-h-[32rem] space-y-2.5 overflow-y-auto pr-1">{laneTasks.length ? laneTasks.map((task) => <TaskCard key={task.id} task={task} stepLimit={stepLimit} workspaceName={workspaceNames?.[task.workspaceId]} onCancel={onCancel} onRemove={onRemove} onOpen={setOpenTask} />) : <p className="rounded-lg border border-dashed border-line px-3 py-8 text-center text-xs text-ink-dim">{lane.empty}</p>}</div>
        </section>;
      })}
    </div>
    <TaskTerminalDialog task={currentOpenTask} stepLimit={stepLimit} onClose={() => setOpenTask(undefined)} />
  </section>;
}