AkurAI Build
Menu

popagent

public

Latest change 5e361abf2b93831addde65d670bfbec2a201b11c - feat: run autonomous work when idle by AkurAI Build

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
  AlertTriangle,
  Ban,
  CheckCircle2,
  CircleDashed,
  Clock3,
  Flag,
  LoaderCircle,
  RotateCcw,
  ShieldCheck,
  SkipForward,
  XCircle,
} from "lucide-react";
import { AUTONOMY_WORKFLOW_PHASES } from "../api-types";
import type {
  AgentTask,
  AgentWorkspace,
  AgentWorkflowDetail,
  AgentWorkflowPhaseDetail,
  AgentWorkflowPhaseId,
  AgentWorkflowPhaseState,
  AgentWorkflowPhaseSummary,
  AgentWorkflowSummary,
  AutonomySettings,
  AutomationStatus,
  EvolutionRevision,
  EvolutionRevisionListResponse,
  EvolutionSignal,
  EvolutionSignalListResponse,
  TaskListResponse,
} from "../api-types";
import { apiFetch } from "./api";
import { toast } from "./toast";
import { useFocusTrap } from "./use-focus-trap";

const HISTORY_LIMIT = 100;
const HISTORY_REFRESH_MS = 2_000;

const WORKFLOW_PHASES: readonly { id: AgentWorkflowPhaseId; label: string; purpose: string }[] = AUTONOMY_WORKFLOW_PHASES.map((id) => ({
  id,
  label: id === "inspect-change" ? "Inspect change" : id[0]!.toUpperCase() + id.slice(1),
  purpose: id === "prepare"
    ? "Scope the request and establish the operator-approved boundaries."
    : id === "research"
      ? "Collect bounded repository and external evidence."
      : id === "decide"
        ? "Choose one useful change or a clean no-op."
        : id === "implement"
          ? "Make the smallest complete change in the contained checkout."
          : id === "inspect-change"
            ? "Inspect the resulting diff and changed-file evidence."
            : id === "verify"
              ? "Run focused checks and capture their observable result."
              : id === "review"
                ? "Independently assess correctness, safety, and residual risk."
                : "Commit the reviewed result on the prepared branch.",
}));

const WORKFLOW_STATE_META: Record<AgentWorkflowPhaseState, {
  label: string;
  Icon: typeof CircleDashed;
  tone: string;
}> = {
  waiting: { label: "Waiting", Icon: CircleDashed, tone: "text-ink-dim" },
  active: { label: "Active", Icon: LoaderCircle, tone: "text-accent" },
  complete: { label: "Complete", Icon: CheckCircle2, tone: "text-emerald-500" },
  failed: { label: "Failed", Icon: XCircle, tone: "text-red-400" },
  cancelled: { label: "Cancelled", Icon: Ban, tone: "text-amber-500" },
  skipped: { label: "Skipped", Icon: SkipForward, tone: "text-ink-dim" },
};

const WORKFLOW_STATE_LABEL: Record<AgentWorkflowSummary["state"], string> = {
  running: "Running",
  completed: "Completed",
  failed: "Failed",
  cancelled: "Cancelled",
};

const WORKFLOW_PHASE_BY_ID = new Map(WORKFLOW_PHASES.map((phase) => [phase.id, phase]));

function phaseSummary(workflow: AgentWorkflowSummary | AgentWorkflowDetail | undefined, id: AgentWorkflowPhaseId): AgentWorkflowPhaseSummary | AgentWorkflowPhaseDetail | undefined {
  return workflow?.phases.find((phase) => phase.id === id);
}

export function workflowPhaseLabel(id: AgentWorkflowPhaseId): string {
  return WORKFLOW_PHASE_BY_ID.get(id)?.label ?? id;
}

export function workflowPhaseStateLabel(state: AgentWorkflowPhaseState): string {
  return WORKFLOW_STATE_META[state].label;
}

function workflowPhaseCopy(id: AgentWorkflowPhaseId, state: AgentWorkflowPhaseState): string {
  if (state === "failed") return "This phase failed. Review the reported evidence and failure reason before taking another action.";
  if (state === "cancelled") return "This phase was cancelled by the operator. No later phase was started.";
  if (state === "skipped") return id === "decide"
    ? "No-op: the evidence did not justify a change, so the workflow stopped without implementation."
    : "No-op: this phase was not needed for the selected outcome.";
  if (id === "commit") return "Commit evidence is local to the prepared branch. Publication and deployment remain governed separately.";
  return WORKFLOW_PHASE_BY_ID.get(id)?.purpose ?? "Bounded workflow evidence for this phase.";
}

function WorkflowState({ state }: { state: AgentWorkflowPhaseState }) {
  const { Icon, label, tone } = WORKFLOW_STATE_META[state];
  return <span className={`inline-flex items-center gap-1.5 text-xs font-medium ${tone}`}><Icon aria-hidden="true" className={`size-4 ${state === "active" ? "animate-spin motion-reduce:animate-none" : ""}`} />{label}</span>;
}

export function AutonomyWorkflowRail({
  workflow,
  selectedPhase,
  onSelect,
}: {
  workflow?: AgentWorkflowSummary | AgentWorkflowDetail;
  selectedPhase: AgentWorkflowPhaseId;
  onSelect: (phase: AgentWorkflowPhaseId) => void;
}) {
  return <nav aria-label="Autonomous workflow phases">
    <ol className="relative space-y-1 before:absolute before:bottom-5 before:left-5 before:top-5 before:w-px before:bg-line">
      {WORKFLOW_PHASES.map((phase) => {
        const summary = phaseSummary(workflow, phase.id);
        const state = summary?.state ?? "waiting";
        const selected = selectedPhase === phase.id;
        const label = phase.label;
        const { Icon, tone } = WORKFLOW_STATE_META[state];
        return <li key={phase.id} className="relative">
          <button type="button" aria-current={selected ? "step" : undefined} aria-label={`${label}: ${workflowPhaseStateLabel(state)}`} onClick={() => onSelect(phase.id)} className={`flex min-h-11 w-full items-center gap-3 rounded px-2.5 text-left text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-accent/60 ${selected ? "bg-accent/10 text-ink" : "text-ink-muted hover:bg-panel-2"}`}>
            <span className={`z-10 flex size-6 shrink-0 items-center justify-center rounded-full border bg-panel ${selected ? "border-accent/60" : "border-line"} ${tone}`}><Icon aria-hidden="true" className={`size-4 ${state === "active" ? "animate-spin motion-reduce:animate-none" : ""}`} /></span>
            <span className="min-w-0 flex-1"><span className="block truncate font-medium">{label}</span><span className="block text-xs text-ink-dim">{workflowPhaseStateLabel(state)}</span></span>
          </button>
        </li>;
      })}
    </ol>
  </nav>;
}

export function AutonomyWorkflowPhaseDetail({
  workflow,
  selectedPhase,
}: {
  workflow?: AgentWorkflowSummary | AgentWorkflowDetail;
  selectedPhase: AgentWorkflowPhaseId;
}) {
  const summary = phaseSummary(workflow, selectedPhase);
  const state = summary?.state ?? "waiting";
  const detail = summary && "startedAt" in summary ? summary : undefined;
  return <article aria-labelledby="autonomy-phase-detail-title" className="min-w-0 rounded border border-line bg-panel p-5">
    <div className="flex flex-wrap items-start justify-between gap-3">
      <div className="min-w-0"><p className="text-xs font-semibold uppercase tracking-[0.12em] text-ink-dim">Selected phase</p><h3 id="autonomy-phase-detail-title" className="mt-1 text-lg font-semibold">{summary?.label ?? workflowPhaseLabel(selectedPhase)}</h3></div>
      <WorkflowState state={state} />
    </div>
    <p className="mt-3 text-sm leading-relaxed text-ink-muted">{workflowPhaseCopy(selectedPhase, state)}</p>
    {detail?.error ? <p role="alert" className="mt-4 break-words rounded border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-red-400">{detail.error}</p> : null}
    {detail?.startedAt || detail?.completedAt ? <p className="mt-4 text-xs text-ink-dim">{detail.startedAt ? `Started ${dateTime(detail.startedAt)}` : ""}{detail.startedAt && detail.completedAt ? " · " : ""}{detail.completedAt ? `Finished ${dateTime(detail.completedAt)}` : ""}</p> : null}
    <div className="mt-5">
      <h4 className="text-xs font-semibold uppercase tracking-[0.12em] text-ink-dim">Evidence</h4>
      {summary?.evidence.length ? <ul className="mt-2 space-y-2">{summary.evidence.map((item, index) => <li key={`${selectedPhase}-${index}`} className="break-words rounded border border-line bg-bg px-3 py-2 text-sm leading-relaxed">{item}</li>)}</ul> : <p className="mt-2 text-sm text-ink-dim">No evidence has been recorded for this phase yet.</p>}
    </div>
  </article>;
}

function workflowTaskState(task: AgentTask): AgentWorkflowSummary["state"] | undefined {
  return task.workflow?.state;
}
function workflowOverallPhaseState(state: AgentWorkflowSummary["state"]): AgentWorkflowPhaseState {
  if (state === "running") return "active";
  if (state === "failed") return "failed";
  if (state === "cancelled") return "cancelled";
  return "complete";
}

export function AutonomyWorkflowHistory({
  tasks,
  workspaces,
  selectedTaskId,
  onSelect,
  stateFilter,
  workspaceFilter,
  onStateFilter,
  onWorkspaceFilter,
}: {
  tasks: AgentTask[];
  workspaces: AgentWorkspace[];
  selectedTaskId?: string;
  onSelect: (task: AgentTask) => void;
  stateFilter: "all" | AgentWorkflowSummary["state"];
  workspaceFilter: string;
  onStateFilter: (value: "all" | AgentWorkflowSummary["state"]) => void;
  onWorkspaceFilter: (value: string) => void;
}) {
  const workspaceNames = new Map(workspaces.map((workspace) => [workspace.id, workspace.name]));
  const filtered = tasks.filter((task) =>
    task.workflow &&
    (stateFilter === "all" || workflowTaskState(task) === stateFilter) &&
    (workspaceFilter === "all" || task.workspaceId === workspaceFilter)
  );
  return <section aria-labelledby="autonomy-workflow-history-title" className="mt-5 rounded border border-line bg-panel p-5">
    <div className="flex items-center gap-2"><Clock3 aria-hidden="true" className="size-4 text-accent" /><h2 id="autonomy-workflow-history-title" className="font-medium">Runs</h2></div>
    <div className="mt-4 flex flex-wrap gap-2" aria-label="Workflow history filters">
      {(["all", "running", "completed", "failed", "cancelled"] as const).map((value) => <button key={value} type="button" aria-pressed={stateFilter === value} onClick={() => onStateFilter(value)} className={`min-h-11 rounded border px-3 text-xs font-medium capitalize ${stateFilter === value ? "border-accent/50 bg-accent/10 text-ink" : "border-line text-ink-dim hover:text-ink"}`}>{value}</button>)}
      {workspaces.length ? <select aria-label="Filter workflow history by workspace" value={workspaceFilter} onChange={(event) => onWorkspaceFilter(event.currentTarget.value)} className="min-h-11 max-w-full rounded border border-line bg-bg px-3 text-xs text-ink outline-none focus:border-accent/50"><option value="all">All workspaces</option>{workspaces.map((workspace) => <option key={workspace.id} value={workspace.id}>{workspace.name}</option>)}</select> : null}
    </div>
    {filtered.length ? <ol className="mt-4 divide-y divide-line border-y border-line">{filtered.map((task) => {
      const current = task.workflow?.phases.find((phase) => phase.state === "active") ?? task.workflow?.phases.find((phase) => phase.state === "failed") ?? task.workflow?.phases.at(-1);
      const selected = task.id === selectedTaskId;
      return <li key={task.id}><button type="button" aria-current={selected ? "true" : undefined} onClick={() => onSelect(task)} className={`flex min-h-16 w-full min-w-0 items-center gap-3 py-3 text-left outline-none focus-visible:ring-2 focus-visible:ring-accent/60 ${selected ? "bg-accent/5" : ""}`}><span className="min-w-0 flex-1"><span className="block truncate text-sm font-medium">{task.prompt}</span><span className="mt-1 block truncate text-xs text-ink-dim">{workspaceNames.get(task.workspaceId) ?? task.workspaceId} · {current?.label ?? (current ? workflowPhaseLabel(current.id) : "Prepare")} · {task.createdAt ? dateTime(task.createdAt) : "Unknown time"}</span></span>{task.workflow ? <WorkflowState state={current?.state ?? workflowOverallPhaseState(task.workflow.state)} /> : null}</button></li>;
    })}</ol> : <p className="mt-5 rounded border border-dashed border-line px-3 py-8 text-center text-sm text-ink-dim">No workflow runs match these filters.</p>}
  </section>;
}
export type AutonomyHistoryPoll = {
  signal: AbortSignal;
  stop: () => void;
};

export function startAutonomyHistoryPolling(
  refresh: (signal: AbortSignal, initial: boolean) => Promise<void>,
  intervalMs = HISTORY_REFRESH_MS,
): AutonomyHistoryPoll {
  const controller = new AbortController();
  let timer: Timer | undefined;
  let initial = true;
  const run = async () => {
    if (controller.signal.aborted) return;
    try {
      await refresh(controller.signal, initial);
    } catch {
      // Preserve the last rendered history and retry after transient refresh failures.
    }
    initial = false;
    if (!controller.signal.aborted) timer = setTimeout(() => void run(), intervalMs);
  };
  void run();
  return {
    signal: controller.signal,
    stop: () => {
      controller.abort();
      clearTimeout(timer);
    },
  };
}

function dateTime(value: string): string {
  return new Date(value).toLocaleString();
}

const STATUS_CLASSES: Record<string, string> = {
  applied: "text-emerald-500",
  "dead-letter": "text-red-400",
  reverted: "text-amber-500",
  ignored: "text-amber-500",
};
const REVISION_LABEL_BY_TARGET: Record<EvolutionRevision["targetType"], string> = {
  overlay: "Learned instruction overlay",
  skill: "Agent-created skill",
  fact: "Learned durable fact",
};
const REVISION_NOUN_BY_TARGET: Record<EvolutionRevision["targetType"], string> = {
  overlay: "instruction overlay",
  skill: "skill content",
  fact: "durable fact",
};


export function countAutonomySignalHealth(signals: EvolutionSignal[]) {
  const counts = { pending: 0, processing: 0, applied: 0, ignored: 0, retried: 0, deadLetters: 0 };
  for (const signal of signals) {
    if (signal.status === "pending") counts.pending++;
    else if (signal.status === "processing") counts.processing++;
    else if (signal.status === "applied") counts.applied++;
    else if (signal.status === "ignored") counts.ignored++;
    else counts.deadLetters++;
    if (signal.attempts > 1) counts.retried++;
  }
  return counts;
}

export function AutonomySignalList({ signals }: { signals: EvolutionSignal[] }) {
  if (!signals.length) return <p className="text-sm text-ink-dim">No evolution signals have been recorded.</p>;
  return <ol className="space-y-2">
    {signals.map((signal) => <li key={signal.id} className="rounded border border-line bg-bg p-3">
      <div className="flex flex-wrap items-start justify-between gap-2">
        <div className="min-w-0">
          <p className="text-sm leading-relaxed">{signal.summary}</p>
          <p className="mt-1 text-xs text-ink-dim">{signal.kind} · {signal.agentId}{signal.workspaceId ? ` · ${signal.workspaceId}` : ""} · {dateTime(signal.createdAt)}</p>
        </div>
        <span className={`text-xs font-semibold uppercase tracking-wide ${STATUS_CLASSES[signal.status] ?? "text-ink-dim"}`}>{signal.status}</span>
      </div>
      <p className="mt-2 text-xs text-ink-dim">{signal.attempts === 1 ? "1 attempt" : `${signal.attempts} attempts`}{signal.nextAttemptAt ? ` · retry ${dateTime(signal.nextAttemptAt)}` : ""}{signal.turnId ? ` · turn ${signal.turnId}` : ""}{signal.traceId ? ` · trace ${signal.traceId}` : ""}</p>
      {signal.error ? <p className="mt-2 rounded border border-red-500/20 bg-red-500/5 px-3 py-2 text-xs text-red-400">{signal.error}</p> : null}
    </li>)}
  </ol>;
}

export function AutonomyRevisionList({
  revisions,
  busyId,
  onRevert,
}: {
  revisions: EvolutionRevision[];
  busyId?: string;
  onRevert: (revision: EvolutionRevision) => void;
}) {
  if (!revisions.length) return <p className="text-sm text-ink-dim">No learned changes have been applied.</p>;
  const latestTargets = new Set<string>();
  const revertedRevisionIds = new Set(revisions.flatMap((revision) =>
    revision.revertsRevisionId ? [revision.revertsRevisionId] : []
  ));
  const revertibleIds = new Set<string>();
  for (const revision of revisions) {
    const target = `${revision.agentId}\0${revision.targetType}\0${revision.targetKey}`;
    if (latestTargets.has(target)) continue;
    latestTargets.add(target);
    if (revision.status === "applied" && !revertedRevisionIds.has(revision.id)) {
      revertibleIds.add(revision.id);
    }
  }
  return <div className="space-y-3">
    {revisions.map((revision) => <article key={revision.id} className="rounded border border-line bg-bg p-4">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div className="min-w-0">
          <p className="font-medium">{REVISION_LABEL_BY_TARGET[revision.targetType]}</p>
          <p className="mt-1 break-all font-mono text-xs text-ink-dim">{revision.agentId} / {revision.targetKey}</p>
        </div>
        <span className={`text-xs font-semibold uppercase tracking-wide ${STATUS_CLASSES[revision.status] ?? "text-ink-dim"}`}>{revision.status}</span>
      </div>
      <p className="mt-3 text-sm leading-relaxed text-ink-muted">{revision.rationale}</p>
      <p className="mt-2 break-all text-xs text-ink-dim">{dateTime(revision.appliedAt)} · {revision.evidenceIds.length} evidence {revision.evidenceIds.length === 1 ? "item" : "items"}{revision.revertsRevisionId ? ` · reverts ${revision.revertsRevisionId}` : ""}</p>
      <details className="mt-3 border-t border-line pt-3 text-sm">
        <summary className="cursor-pointer text-ink-muted outline-none focus:text-ink">Inspect learned change</summary>
        <div className="mt-3 grid gap-3 lg:grid-cols-2">
          <div><h3 className="text-xs font-semibold uppercase tracking-wide text-ink-dim">Before</h3><pre className="mt-1 max-h-64 overflow-auto whitespace-pre-wrap rounded bg-panel-2 p-3 text-xs">{revision.beforeContent || "Empty"}</pre></div>
          <div><h3 className="text-xs font-semibold uppercase tracking-wide text-ink-dim">After</h3><pre className="mt-1 max-h-64 overflow-auto whitespace-pre-wrap rounded bg-panel-2 p-3 text-xs">{revision.afterContent || "Empty"}</pre></div>
        </div>
      </details>
      {revertibleIds.has(revision.id) ? <div className="mt-3 flex justify-end"><button type="button" disabled={Boolean(busyId)} onClick={() => onRevert(revision)} className="flex min-h-11 items-center gap-2 rounded border border-amber-500/30 px-3 text-sm text-amber-500 disabled:opacity-50"><RotateCcw className="size-4" />{busyId === revision.id ? "Reverting…" : "Revert change"}</button></div> : null}
    </article>)}
  </div>;
}

function NumberField({ label, value, min, max, step = 1, onChange }: { label: string; value: number; min: number; max: number; step?: number; onChange: (value: number) => void }) {
  return <label className="text-sm font-medium">{label}<input type="number" aria-label={label} value={value} min={min} max={max} step={step} onChange={(event) => { const next = event.currentTarget.valueAsNumber; if (Number.isFinite(next)) onChange(next); }} className="mt-2 min-h-11 w-full rounded border border-line bg-bg px-3 text-ink outline-none focus:border-accent/50" /></label>;
}

export function AutonomySettingsPanel() {
  const [settings, setSettings] = useState<AutonomySettings>();
  const [signals, setSignals] = useState<EvolutionSignal[]>();
  const [revisions, setRevisions] = useState<EvolutionRevision[]>();
  const [workspaces, setWorkspaces] = useState<AgentWorkspace[]>([]);
  const [status, setStatus] = useState<AutomationStatus>();
  const [workflowTasks, setWorkflowTasks] = useState<AgentTask[]>([]);
  const [workflowLoading, setWorkflowLoading] = useState(true);
  const [workflowError, setWorkflowError] = useState("");
  const [selectedTaskId, setSelectedTaskId] = useState<string>();
  const [selectedPhase, setSelectedPhase] = useState<AgentWorkflowPhaseId>("prepare");
  const [workflowDetail, setWorkflowDetail] = useState<AgentWorkflowSummary | AgentWorkflowDetail>();
  const [workflowDetailLoading, setWorkflowDetailLoading] = useState(false);
  const [workflowDetailError, setWorkflowDetailError] = useState("");
  const [historyStateFilter, setHistoryStateFilter] = useState<"all" | AgentWorkflowSummary["state"]>("all");
  const [historyWorkspaceFilter, setHistoryWorkspaceFilter] = useState("all");
  const [loadError, setLoadError] = useState("");
  const [saving, setSaving] = useState(false);
  const [reverting, setReverting] = useState<string>();
  const [cancelling, setCancelling] = useState(false);
  const [confirmRevision, setConfirmRevision] = useState<EvolutionRevision>();
  const confirmRef = useRef<HTMLDivElement>(null);
  const cancelRef = useRef<HTMLButtonElement>(null);
  const lifecycleSignalRef = useRef<AbortSignal | null>(null);
  const historyRequestRef = useRef(0);
  const historyAppliedRef = useRef(0);
  useFocusTrap(confirmRef, Boolean(confirmRevision));

  const fetchHistory = useCallback(async (signal?: AbortSignal) => {
    const [signalsResponse, revisionsResponse] = await Promise.all([
      apiFetch(`/api/autonomy/signals?limit=${HISTORY_LIMIT}`, { signal }),
      apiFetch(`/api/autonomy/revisions?limit=${HISTORY_LIMIT}`, { signal }),
    ]);
    if (!signalsResponse.ok || !revisionsResponse.ok) {
      throw new Error("Unable to load autonomy state.");
    }
    const [signalHistory, revisionHistory] = await Promise.all([
      signalsResponse.json() as Promise<EvolutionSignalListResponse>,
      revisionsResponse.json() as Promise<EvolutionRevisionListResponse>,
    ]);
    return { signals: signalHistory.signals, revisions: revisionHistory.revisions };
  }, []);
  const fetchWorkflowTasks = useCallback(async (signal?: AbortSignal) => {
    const response = await apiFetch("/api/tasks?workflow=true", { signal });
    if (!response.ok) throw new Error(`Unable to load workflow history (HTTP ${response.status}).`);
    const payload = await response.json() as TaskListResponse;
    if (signal?.aborted) return;
    setWorkflowTasks(payload.tasks.filter((task) => Boolean(task.workflow)));
    setWorkflowError("");
  }, []);
  const refreshWorkflowTasks = useCallback(async (signal?: AbortSignal) => {
    setWorkflowLoading(true);
    try {
      await fetchWorkflowTasks(signal);
    } catch (error) {
      if (!signal?.aborted) setWorkflowError(error instanceof Error ? error.message : "Unable to load workflow history.");
    } finally {
      if (!signal?.aborted) setWorkflowLoading(false);
    }
  }, [fetchWorkflowTasks]);

  const selectedTask = useMemo(
    () => workflowTasks.find((task) => task.id === selectedTaskId),
    [selectedTaskId, workflowTasks],
  );

  useEffect(() => {
    const active = status?.activeTask?.workflow ? status.activeTask : undefined;
    const next = active ?? (selectedTaskId ? workflowTasks.find((task) => task.id === selectedTaskId) : undefined) ?? workflowTasks[0];
    if (!next) return;
    if (next.id !== selectedTaskId) setSelectedTaskId(next.id);
    if (next.workflow?.currentPhase) setSelectedPhase(next.workflow.currentPhase);
  }, [selectedTaskId, status, workflowTasks]);

  useEffect(() => {
    if (!selectedTaskId) {
      setWorkflowDetail(undefined);
      return;
    }
    const summary = selectedTask?.workflow;
    if (!summary) {
      setWorkflowDetail(undefined);
      setWorkflowDetailError("");
      return;
    }
    const controller = new AbortController();
    setWorkflowDetail(summary);
    setWorkflowDetailLoading(true);
    setWorkflowDetailError("");
    void apiFetch(`/api/tasks/${encodeURIComponent(selectedTaskId)}/workflow`, { signal: controller.signal })
      .then(async (response) => {
        if (!response.ok) throw new Error(response.status === 404 ? "Workflow details are no longer available." : `Unable to load phase details (HTTP ${response.status}).`);
        const detail = await response.json() as AgentWorkflowDetail;
        if (!controller.signal.aborted) {
          setWorkflowDetail(detail);
          if (detail.currentPhase) setSelectedPhase(detail.currentPhase);
        }
      })
      .catch((error) => {
        if (!controller.signal.aborted) setWorkflowDetailError(error instanceof Error ? error.message : "Unable to load phase details.");
      })
      .finally(() => {
        if (!controller.signal.aborted) setWorkflowDetailLoading(false);
      });
    return () => controller.abort();
  }, [selectedTask, selectedTaskId]);
  const load = useCallback(async (signal?: AbortSignal) => {
    const historyRequest = ++historyRequestRef.current;
    setLoadError("");
    try {
      const [settingsResponse, workspacesResponse, statusResponse, history] = await Promise.all([
        apiFetch("/api/settings/autonomy", { signal }),
        apiFetch("/api/workspaces", { signal }),
        apiFetch("/api/autonomy/status", { signal }),
        fetchHistory(signal),
      ]);
      if (!settingsResponse.ok || !workspacesResponse.ok || !statusResponse.ok) throw new Error("Unable to load autonomy state.");
      const [nextSettings, workspaceResult, nextStatus] = await Promise.all([
        settingsResponse.json() as Promise<AutonomySettings>,
        workspacesResponse.json() as Promise<{ workspaces: AgentWorkspace[] }>,
        statusResponse.json() as Promise<AutomationStatus>,
      ]);
      if (signal?.aborted) return;
      setSettings(nextSettings);
      setWorkspaces(workspaceResult.workspaces);
      setStatus(nextStatus);
      if (historyRequest < historyAppliedRef.current) return;
      historyAppliedRef.current = historyRequest;
      setSignals(history.signals);
      setRevisions(history.revisions);
    } catch (error) {
      if (signal?.aborted || historyRequest < historyAppliedRef.current) return;
      setLoadError(error instanceof Error ? error.message : "Unable to load autonomy state.");
    }
  }, [fetchHistory]);

  useEffect(() => {
    const poll = startAutonomyHistoryPolling(async (signal, initial) => {
      if (initial) {
        await Promise.all([load(signal), refreshWorkflowTasks(signal)]);
        return;
      }
      const historyRequest = ++historyRequestRef.current;
      const history = await fetchHistory(signal);
      void refreshWorkflowTasks(signal);
      if (signal.aborted || historyRequest < historyAppliedRef.current) return;
      historyAppliedRef.current = historyRequest;
      setSignals(history.signals);
      setRevisions(history.revisions);
    });
    lifecycleSignalRef.current = poll.signal;
    return () => {
      poll.stop();
      if (lifecycleSignalRef.current === poll.signal) lifecycleSignalRef.current = null;
    };
  }, [fetchHistory, load, refreshWorkflowTasks]);
  useEffect(() => {
    if (!confirmRevision) return;
    cancelRef.current?.focus();
    const close = (event: KeyboardEvent) => {
      if (event.key === "Escape" && !reverting) setConfirmRevision(undefined);
    };
    document.addEventListener("keydown", close);
    return () => document.removeEventListener("keydown", close);
  }, [confirmRevision, reverting]);

  const update = <Key extends keyof AutonomySettings>(key: Key, value: AutonomySettings[Key]) => {
    setSettings((current) => current ? { ...current, [key]: value } : current);
  };
  const save = async () => {
    if (!settings) return;
    setSaving(true);
    try {
      const { updatedAt, ...input } = settings;
      void updatedAt;
      const response = await apiFetch("/api/settings/autonomy", { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify(input) });
      if (!response.ok) throw new Error("Unable to save autonomy settings.");
      setSettings(await response.json() as AutonomySettings);
      toast("Autonomy settings saved.");
    } catch (error) {
      toast(error instanceof Error ? error.message : "Unable to save autonomy settings.");
    } finally {
      setSaving(false);
    }
  };
  const revert = async () => {
    if (!confirmRevision) return;
    const id = confirmRevision.id;
    setReverting(id);
    try {
      const response = await apiFetch(`/api/autonomy/revisions/${encodeURIComponent(id)}/revert`, { method: "POST" });
      if (!response.ok) throw new Error(response.status === 404 ? "This revision can no longer be reverted." : "Unable to revert revision.");
      setConfirmRevision(undefined);
      await load(lifecycleSignalRef.current ?? undefined);
      toast("Revision reverted.");
    } catch (error) {
      toast(error instanceof Error ? error.message : "Unable to revert revision.");
    } finally {
      setReverting(undefined);
    }
  };
  const refreshWorkflow = async () => {
    await refreshWorkflowTasks(lifecycleSignalRef.current ?? undefined);
  };
  const cancelWorkflow = async () => {
    const task = selectedTask;
    if (!task || task.status !== "running") return;
    setCancelling(true);
    try {
      const response = await apiFetch(`/api/tasks/${encodeURIComponent(task.id)}/cancel`, { method: "POST" });
      if (!response.ok) throw new Error(response.status === 409 ? "This workflow can no longer be cancelled." : "Unable to cancel workflow.");
      toast("Workflow cancellation requested.");
      await refreshWorkflow();
    } catch (error) {
      toast(error instanceof Error ? error.message : "Unable to cancel workflow.");
    } finally {
      setCancelling(false);
    }
  };
  const activeWorkflowTask = status?.activeTask?.workflow
    ? status.activeTask
    : workflowTasks.find((task) => task.workflow?.state === "running");
  const selectedWorkflow = workflowDetail ?? selectedTask?.workflow;

  if (loadError) return <div className="w-full max-w-5xl"><h1 className="text-xl font-semibold">Autonomy</h1><div role="alert" className="mt-6 rounded border border-red-500/20 bg-panel p-5"><div className="flex items-start gap-3"><AlertTriangle className="mt-0.5 size-5 text-red-400" /><div><p className="font-medium">Autonomy state unavailable</p><p className="mt-1 text-sm text-ink-dim">{loadError}</p><button type="button" onClick={() => void load(lifecycleSignalRef.current ?? undefined)} className="mt-4 min-h-11 rounded border border-line px-4 text-sm">Try again</button></div></div></div></div>;
  if (!settings || !signals || !revisions) return <div className="w-full max-w-5xl"><h1 className="text-xl font-semibold">Autonomy</h1><p role="status" className="mt-6 text-sm text-ink-dim">Loading autonomy state and history…</p></div>;

  const health = countAutonomySignalHealth(signals);
  return <div className="w-full max-w-5xl">
    <h1 className="text-xl font-semibold">Autonomy</h1>
    <p className="mt-1 text-sm text-ink-dim">Control bounded self-improvement, inspect its evidence, and undo learned changes without altering base instructions or capabilities.</p>
    <section className="mt-6 rounded border border-line bg-panel p-5" aria-labelledby="autonomy-workflow-title">
      <header className="flex flex-wrap items-start justify-between gap-3">
        <div className="min-w-0"><div className="flex flex-wrap items-center gap-2"><Flag aria-hidden="true" className="size-5 text-accent" /><h2 id="autonomy-workflow-title" className="font-medium">Autonomous improvement workflow</h2>{activeWorkflowTask?.workflow ? <span className="rounded-full border border-accent/30 bg-accent/10 px-2 py-0.5 text-xs text-accent">{WORKFLOW_STATE_LABEL[activeWorkflowTask.workflow.state]}</span> : null}</div><p className="mt-1 text-xs leading-relaxed text-ink-dim">Follow one bounded run from preparation through review and its local commit.</p></div>
        <div className="flex flex-wrap gap-2">
          <button type="button" disabled={workflowLoading} onClick={() => void refreshWorkflow()} className="min-h-11 rounded border border-line px-3 text-xs font-medium disabled:opacity-50">{workflowLoading ? "Refreshing…" : "Refresh workflow"}</button>
          {selectedTask?.status === "running" ? <button type="button" disabled={cancelling} onClick={() => void cancelWorkflow()} className="min-h-11 rounded border border-red-500/30 px-3 text-xs font-medium text-red-400 disabled:opacity-50">{cancelling ? "Cancelling…" : "Cancel run"}</button> : null}
        </div>
      </header>
      <p role="status" aria-live="polite" className="sr-only">{activeWorkflowTask?.workflow ? `Workflow ${WORKFLOW_STATE_LABEL[activeWorkflowTask.workflow.state].toLowerCase()}. Current phase ${activeWorkflowTask.workflow.currentPhase ? workflowPhaseLabel(activeWorkflowTask.workflow.currentPhase) : "Prepare"}.` : "No autonomous workflow is running."}</p>
      {workflowError ? <div role="alert" className="mt-5 flex items-start gap-3 rounded border border-red-500/20 bg-red-500/5 p-4"><AlertTriangle aria-hidden="true" className="mt-0.5 size-5 shrink-0 text-red-400" /><div className="min-w-0"><p className="text-sm font-medium">Workflow history unavailable</p><p className="mt-1 text-sm text-ink-dim">{workflowError}</p><button type="button" onClick={() => void refreshWorkflow()} className="mt-3 min-h-11 rounded border border-line px-3 text-xs">Try again</button></div></div> : null}
      {workflowLoading ? <p role="status" className="sr-only">Loading runs</p> : workflowTasks.length === 0 ? <p className="mt-5 rounded border border-dashed border-line px-3 py-8 text-center text-sm text-ink-dim">No autonomous workflow runs yet. Enable a repository below to let the next contained cycle appear here.</p> : selectedWorkflow ? <div className="mt-5 grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(0,2fr)]">
        <div className="min-w-0 xl:sticky xl:top-4 xl:self-start"><AutonomyWorkflowRail workflow={selectedWorkflow} selectedPhase={selectedPhase} onSelect={setSelectedPhase} /></div>
        <div className="min-w-0">{workflowDetailError ? <p role="alert" className="mb-3 rounded border border-red-500/20 bg-red-500/5 px-3 py-2 text-sm text-red-400">{workflowDetailError}</p> : null}{workflowDetailLoading ? <p role="status" className="mb-3 text-sm text-ink-dim">Loading selected phase details…</p> : null}<AutonomyWorkflowPhaseDetail workflow={selectedWorkflow} selectedPhase={selectedPhase} /></div>
      </div> : <p className="mt-5 text-sm text-ink-dim">Select a workflow run from history to inspect its phases.</p>}
    </section>

    <AutonomyWorkflowHistory tasks={workflowTasks} workspaces={workspaces} selectedTaskId={selectedTaskId} onSelect={(task) => { setSelectedTaskId(task.id); if (task.workflow?.currentPhase) setSelectedPhase(task.workflow.currentPhase); }} stateFilter={historyStateFilter} workspaceFilter={historyWorkspaceFilter} onStateFilter={setHistoryStateFilter} onWorkspaceFilter={setHistoryWorkspaceFilter} />

    <section className="mt-6 rounded border border-line bg-panel p-5" aria-labelledby="autonomy-controls-title">
      <div className="flex items-start gap-3"><ShieldCheck className="mt-0.5 size-5 text-accent" /><div><h2 id="autonomy-controls-title" className="font-medium">Safety-controlled learning</h2><p className="mt-1 text-xs leading-relaxed text-ink-dim">Learning is limited to a separate instruction overlay, agent-created skills, and bounded durable facts. Workspace access, tools, secrets, hooks, authentication, and execution bounds remain fixed.</p></div></div>
      <div className="mt-5 grid gap-3 sm:grid-cols-2">
        {([
          ["enabled", "Enable self-improvement", "Reflect on bounded signals and propose learned changes."],
          ["autoApplyStrategies", "Auto-apply strategies", "Apply eligible learned instruction overlays automatically."],
          ["autoCreateSkills", "Auto-create skills", "Create agent-owned skills when repeated evidence supports one."],
          ["autoRetainFacts", "Auto-retain facts", "Store stable operational facts established by cited execution evidence."],
          ["selfUpdateEnabled", "Enable source self-update", "Queue contained reviewed repository improvements."],
          ["idleImprovementEnabled", "Enable idle improvements", "Run one selected repository only after the idle grace period."],
          ["idleDeploymentEnabled", "Publish and deploy", "After review, use the repository's maintained publish and deploy path."],
        ] as const).map(([key, label, description]) => <label key={key} className="flex min-h-20 items-start justify-between gap-4 rounded border border-line bg-bg p-3"><span><span className="block text-sm font-medium">{label}</span><span className="mt-1 block text-xs leading-relaxed text-ink-dim">{description}</span></span><input type="checkbox" aria-label={label} checked={settings[key]} onChange={(event) => update(key, event.currentTarget.checked)} className="mt-1 size-5 shrink-0 accent-accent" /></label>)}
      </div>
      <div className="mt-5 grid gap-4 sm:grid-cols-3">
        <NumberField label="Reflection interval (ms)" value={settings.reflectionIntervalMs} min={60_000} max={86_400_000} step={60_000} onChange={(value) => update("reflectionIntervalMs", value)} />
        <NumberField label="Signals per reflection" value={settings.batchSize} min={1} max={100} onChange={(value) => update("batchSize", value)} />
        <NumberField label="Maximum attempts" value={settings.maxAttempts} min={1} max={10} onChange={(value) => update("maxAttempts", value)} />
      </div>
      <fieldset className="mt-5"><legend className="text-sm font-medium">Autonomous repositories</legend><div className="mt-2 grid gap-2 sm:grid-cols-2">{workspaces.map((workspace) => <label key={workspace.id} className="flex min-h-11 items-center gap-3 rounded border border-line bg-bg px-3 text-sm"><input type="checkbox" checked={settings.idleWorkspaceIds.includes(workspace.id)} onChange={(event) => update("idleWorkspaceIds", event.currentTarget.checked ? [...settings.idleWorkspaceIds, workspace.id] : settings.idleWorkspaceIds.filter((id) => id !== workspace.id))} className="size-5 accent-accent" /><span>{workspace.name}</span></label>)}</div></fieldset>
      {status ? <p role="status" className="mt-4 text-sm text-ink-dim">Automation: {status.state} · continuous scheduled work</p> : null}
      <div className="mt-5 flex justify-end"><button type="button" disabled={saving} onClick={() => void save()} className="min-h-11 rounded bg-accent px-4 text-sm font-medium text-white disabled:opacity-60">{saving ? "Saving…" : "Save autonomy settings"}</button></div>
    </section>

    <section className="mt-5" aria-labelledby="autonomy-health-title">
      <div className="flex items-center gap-2"><CheckCircle2 className="size-4 text-accent" /><h2 id="autonomy-health-title" className="font-medium">Recent signal health</h2></div>
      <div className="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-6">
        {[["Pending", health.pending], ["Processing", health.processing], ["Applied", health.applied], ["Ignored", health.ignored], ["Retried", health.retried], ["Dead letters", health.deadLetters]].map(([label, value]) => <div key={label} className="rounded border border-line bg-panel p-4"><p className="text-xs uppercase tracking-wide text-ink-dim">{label}</p><p className="mt-1 text-2xl font-semibold tabular-nums">{value}</p></div>)}
      </div>
    </section>

    <div className="mt-5 grid gap-5 xl:grid-cols-2">
      <section className="rounded border border-line bg-panel p-5" aria-labelledby="signal-history-title"><div className="flex items-center gap-2"><Clock3 className="size-4 text-accent" /><h2 id="signal-history-title" className="font-medium">Recent signals</h2></div><div className="mt-4"><AutonomySignalList signals={signals} /></div></section>
      <section className="rounded border border-line bg-panel p-5" aria-labelledby="revision-history-title"><div className="flex items-center gap-2"><RotateCcw className="size-4 text-accent" /><h2 id="revision-history-title" className="font-medium">Learned revisions</h2></div><div className="mt-4"><AutonomyRevisionList revisions={revisions} busyId={reverting} onRevert={setConfirmRevision} /></div></section>
    </div>

    {confirmRevision ? <div className="fixed inset-0 z-[60] grid place-items-center bg-black/60 p-4" role="presentation"><div ref={confirmRef} role="alertdialog" aria-modal="true" aria-labelledby="revert-revision-title" aria-describedby="revert-revision-description" className="w-full max-w-md rounded border border-line bg-panel p-5 shadow-2xl"><h2 id="revert-revision-title" className="font-semibold">Revert learned change?</h2><p id="revert-revision-description" className="mt-2 text-sm leading-relaxed text-ink-dim">Restore the previous {REVISION_NOUN_BY_TARGET[confirmRevision.targetType]} for <span className="font-mono text-ink">{confirmRevision.targetKey}</span>. Base agent instructions and capabilities are not changed.</p><div className="mt-5 flex justify-end gap-3"><button ref={cancelRef} type="button" disabled={Boolean(reverting)} onClick={() => setConfirmRevision(undefined)} className="min-h-11 rounded border border-line px-4 text-sm disabled:opacity-50">Cancel</button><button type="button" disabled={Boolean(reverting)} onClick={() => void revert()} className="min-h-11 rounded bg-amber-600 px-4 text-sm font-medium text-white disabled:opacity-50">{reverting ? "Reverting…" : "Revert change"}</button></div></div></div> : null}
  </div>;
}