import { useId, useState } from "react"; import { PAGE_OUTCOMES, type PageOutcome } from "../../../ledger/outcomes"; import type { LedgerView, LedgerViewRow } from "../../../ledger/view"; import { Badge } from "../components/badge"; import { Button } from "../components/button"; import { Input, NativeSelect } from "../components/input"; import { Label } from "../components/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../components/table"; import { clock } from "../format"; import { Empty, Pending, Scroller } from "../frame"; import { type Loaded, useLoaded } from "../lib/api"; const MAX_ROWS = 200; const COLUMNS = [ "when", "machine", "actor", "action", "target", "change", "outcome", ] as const; export const LOG_KEYS = [ "project", "runtime", "machine", "authority", "outcome", "since", "until", ] as const; export type LogFilter = Record<(typeof LOG_KEYS)[number], string>; const AUTHORITIES = ["user", "agent"] as const; const RUNTIMES = ["claude", "cursor", "codex", "copilot", "opencode", "unknown"] as const; const EMPTY_FILTER: LogFilter = { project: "", runtime: "", machine: "", authority: "", outcome: "", since: "", until: "", }; /** The machines a row could name are whatever the window actually contains. */ function machinesIn(view: Loaded): string[] { if (view.state !== "ready") return []; return [...new Set(view.data.rows.map((r) => r.machine))].filter(Boolean).sort(); } function ledgerQuery(filter: LogFilter): string { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filter)) if (value) params.set(key, value); const query = params.toString(); return query ? `/api/ledger?${query}` : "/api/ledger"; } function outcomeVariant(outcome: string) { if (outcome === "applied") return "neutral" as const; if (outcome === "failed") return "outline" as const; return "alarm" as const; } function Field({ label, control, }: { label: string; control: (id: string) => React.ReactNode; }) { const id = useId(); return (
{control(id)}
); } function Stats({ view }: { view: LedgerView }) { const cells: { n: number; label: string }[] = [ { n: view.stats.total, label: "actions" }, { n: view.stats.refusals, label: "refusals" }, ...PAGE_OUTCOMES.map((o: PageOutcome) => ({ n: view.stats.outcomes[o].total, label: o, })), ]; return (
{cells.map((cell) => (
{cell.n}
{cell.label}
))}
); } /** A refusal carries the command it stopped; an edit carries its line counts. */ function Row({ r, open, onToggle, }: { r: LedgerViewRow; open: boolean; onToggle: () => void; }) { return ( <> {clock(r.ts)} {r.machine} {r.actor} {r.authority} ยท {r.runtime} {r.tool} {r.target} {r.change} {r.outcome} {r.reason && ( {r.reason} )} {open && (r.command || r.reason) && (
              {r.command ? `$ ${r.command}\n` : ""}
              {r.reason ?? ""}
            
)} ); } export function Log({ filter, onFilter, }: { filter: LogFilter; onFilter: (next: Partial) => void; }) { const projects = useLoaded<{ slug: string }[]>("/api/projects"); const view = useLoaded(ledgerQuery(filter)); const [expanded, setExpanded] = useState(null); const choices = (key: keyof LogFilter, values: readonly string[], all = "all") => ( ( onFilter({ [key]: e.target.value })} > {values.map((value) => ( ))} )} /> ); return (
{choices( "project", projects.state === "ready" ? projects.data.map((p) => p.slug) : [] )} {choices("runtime", RUNTIMES)} {choices("machine", machinesIn(view), "both")} {choices("authority", AUTHORITIES)} {choices("outcome", PAGE_OUTCOMES)} ( onFilter({ since: e.target.value })} /> )} /> ( onFilter({ until: e.target.value })} /> )} />
{view.state === "ready" && ( <>
{view.data.rows.length === 0 ? ( No actions in this window. ) : ( {COLUMNS.map((column) => ( {column} ))} {view.data.rows.slice(0, MAX_ROWS).map((r) => ( setExpanded(expanded === r.id ? null : r.id)} /> ))}
)}
{view.data.rows.length > MAX_ROWS && (

Newest {MAX_ROWS} of {view.data.rows.length}. Narrow the window for the rest.

)} )}
); }