AkurAI Build
Menu

popagent

public

Latest change b61deab8cc219052959fa9b2e7525335a79519f8 - Isolate tests and remove terminal tasks by AkurAI Build

import { AlertTriangle, CheckCircle2, CircleDashed, LoaderCircle, RotateCcw, XCircle } from "lucide-react";
import type { AgentTask, AgentTaskStatus } from "../api-types";

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 }: { task: AgentTask; stepLimit?: number; workspaceName?: string; onCancel: (task: AgentTask) => void; onRemove: (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 === "self-update" ? "Self-update" : "User"}</span>
    </div>

    <details className="mt-2 border-t border-line pt-2 text-xs">
      <summary className="min-h-10 cursor-pointer select-none py-2 font-medium text-ink-muted outline-none hover:text-ink focus-visible:text-accent">Open full task</summary>
      <div className="space-y-3 pb-1">
        <div><h4 className="text-[10px] font-semibold uppercase tracking-widest text-ink-dim">Request</h4><p className="mt-1 whitespace-pre-wrap leading-relaxed text-ink-muted">{task.prompt}</p></div>
        {task.output ? <div><h4 className="text-[10px] font-semibold uppercase tracking-widest text-ink-dim">Evidence and output</h4><pre className="mt-1 max-h-72 overflow-auto whitespace-pre-wrap rounded-md bg-panel-2 p-3 font-sans text-xs leading-relaxed text-ink-muted">{task.output}</pre></div> : null}
        {task.recoveryCount ? <p className="text-amber-500">Recovered {task.recoveryCount} time{task.recoveryCount === 1 ? "" : "s"} after restart.</p> : null}
        {task.lastErrorClass && !task.error ? <p className="text-ink-dim">Last error class: {task.lastErrorClass}</p> : null}
      </div>
    </details>

    {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>;
}

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;
}) {
  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} />) : <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>
  </section>;
}