feat(lens): put the now reading stage above the trace list in View run

This commit is contained in:
Ishaan Jaff 2026-10-03 18:09:50 -07:00
parent 3577f3bebf
commit 6fb7e8124f
No known key found for this signature in database
4 changed files with 118 additions and 163 deletions

View file

@ -5,12 +5,14 @@ import { useState, type ReactNode } from "react";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import type { Conclusion, InFlight } from "../../model/live";
import type { Job, Review } from "../../model/types";
import { conclusions, type InFlight } from "../../model/live";
import { releasedReviews } from "../../model/stage";
import type { Job, Review, Settings } from "../../model/types";
import { ConclusionsPanel } from "./ConclusionsPanel";
import { ModelName } from "./LiveStrip";
import { NowReading } from "./NowReading";
import { TraceList } from "./TraceList";
import { useLensMode } from "./useLensMode";
import { useStage } from "./useStage";
const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
queued: { label: "Queued", tone: "neutral" },
@ -21,6 +23,33 @@ const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
};
const PANE_TITLE = "text-[12px] font-semibold text-foreground";
function Stage({
reviews,
reading,
running,
slots,
model,
counter,
checks,
children,
}: {
reviews: readonly Review[];
reading: readonly InFlight[];
running: boolean;
slots: number;
model: string;
counter: string;
checks: Settings["checks"];
children: (listed: readonly Review[], groups: ReturnType<typeof conclusions>, nowReading: ReactNode) => ReactNode;
}) {
const { stage, now, charMs } = useStage(reviews, reading, running, slots);
const listed = releasedReviews(reviews, stage);
const nowReading = running && (
<NowReading model={model} counter={counter} lanes={stage.lanes} now={now} charMs={charMs} />
);
return <>{children(listed, conclusions(listed, checks), nowReading)}</>;
}
export function LiveDrawer({
open,
onClose,
@ -29,10 +58,11 @@ export function LiveDrawer({
status,
reviewed,
reviews,
now,
done,
reading,
groups,
counter,
done,
slots,
checks,
scope,
waiting,
}: {
@ -43,17 +73,17 @@ export function LiveDrawer({
status: Job["status"];
reviewed: number;
reviews: readonly Review[];
now: string | null;
done: string | null;
reading: readonly InFlight[];
groups: readonly Conclusion[];
counter: string;
done: string | null;
slots: number;
checks: Settings["checks"];
scope: string;
waiting?: ReactNode;
}) {
const [group, setGroup] = useState<string | null>(null);
const badge = STATUS[status];
const live = status === "queued" || status === "running";
const mode = useLensMode(live);
const running = status === "running";
return (
<Sheet open={open} onOpenChange={(value) => !value && onClose()}>
<SheetContent className="flex h-full w-full flex-col gap-0 data-[side=right]:sm:max-w-[min(1200px,94vw)]">
@ -64,36 +94,59 @@ export function LiveDrawer({
<ModelName model={model} size="md" />
</SheetDescription>
</SheetHeader>
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,55fr)_minmax(0,45fr)]">
<section aria-label="Traces" className="min-h-0 overflow-y-auto border-b px-4 py-4 md:border-r md:border-b-0">
<div className="mb-2 flex items-baseline justify-between gap-3 px-2">
<h2 className={PANE_TITLE}>Traces</h2>
<span className="inline-flex items-center gap-1.5 text-[11px] tabular-nums text-muted-foreground">
{now ?? done ?? `${reviewed} reviewed`}
{done && !now && <ModelName model={model} />}
{reviewed > reviews.length && reviews.length ? ` · showing latest ${reviews.length}` : ""}
</span>
</div>
{reviews.length || reading.length || status === "running" ? (
<TraceList reading={reading} reviews={reviews} model={model} group={group} mode={mode} nowLine={now} />
) : (
<div className="flex flex-col gap-1 px-2 py-6 text-[12px] text-muted-foreground">
<p className="text-[13px] text-foreground">Waiting for the first trace…</p>
{waiting}
{open && (
<Stage
reviews={reviews}
reading={reading}
running={running}
slots={slots}
model={model}
counter={counter}
checks={checks}
>
{(listed, groups, nowReading) => (
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,55fr)_minmax(0,45fr)]">
<section
aria-label="Traces"
className="min-h-0 overflow-y-auto border-b px-4 py-4 md:border-r md:border-b-0"
>
<div className="mb-2 flex items-baseline justify-between gap-3 px-2">
<h2 className={PANE_TITLE}>Traces</h2>
<span className="inline-flex items-center gap-1.5 text-[11px] tabular-nums text-muted-foreground">
{done ?? `${reviewed} reviewed`}
{done && <ModelName model={model} />}
{reviewed > reviews.length && reviews.length ? ` · showing latest ${reviews.length}` : ""}
</span>
</div>
{nowReading}
{listed.length || reviews.length ? (
<TraceList reviews={listed} group={group} />
) : (
!running && (
<div className="flex flex-col gap-1 px-2 py-6 text-[12px] text-muted-foreground">
<p className="text-[13px] text-foreground">Waiting for the first trace…</p>
{waiting}
</div>
)
)}
</section>
<section
aria-label="Conclusions so far"
className="flex min-h-0 flex-col gap-3 overflow-y-auto px-5 py-4"
>
<h2 className={PANE_TITLE}>Conclusions so far</h2>
<ConclusionsPanel
groups={groups}
total={listed.length}
scope={scope}
selected={group}
onSelect={setGroup}
/>
</section>
</div>
)}
</section>
<section aria-label="Conclusions so far" className="flex min-h-0 flex-col gap-3 overflow-y-auto px-5 py-4">
<h2 className={PANE_TITLE}>Conclusions so far</h2>
<ConclusionsPanel
groups={groups}
total={reviews.length}
scope={scope}
selected={group}
onSelect={setGroup}
/>
</section>
</div>
</Stage>
)}
</SheetContent>
</Sheet>
);

View file

@ -6,7 +6,6 @@ import { modelsUsed } from "../../model/inbox";
import {
analysisModel,
doneLine,
conclusions,
inFlight,
issueCount,
nowLine,
@ -20,6 +19,8 @@ import { LiveDrawer } from "./LiveDrawer";
import { LiveStrip } from "./LiveStrip";
import { useStripOpen } from "./useLivePanels";
const MAX_LANES = 4;
export function LiveRun({
job,
reviews,
@ -79,10 +80,11 @@ export function LiveRun({
status={job.status}
reviewed={job.reviewed}
reviews={reviews}
now={job.status === "running" ? nowLine(job, reading.length) : null}
reading={reading}
counter={nowLine(job, reading.length)}
done={job.status === "completed" ? doneLine(job) : null}
groups={conclusions(reviews, job.settings.checks)}
slots={Math.max(1, Math.min(job.settings.concurrency, MAX_LANES))}
checks={job.settings.checks}
scope={job.reviewed > reviews.length ? `From the latest ${reviews.length} of ${job.reviewed} reviewed traces` : ""}
waiting={waiting}
/>

View file

@ -1,21 +1,18 @@
"use client";
import { useLayoutEffect, useRef, useState } from "react";
import { useState } from "react";
import { agoLabel } from "@/components/view_logs/TraceView/lensField";
import { useNow } from "@/hooks/useNow";
import { cn } from "@/lib/cva.config";
import { briefReasoning, durationLabel, inGroup, liveRows, outcome, shortVerdict, type InFlight } from "../../model/live";
import { briefReasoning, durationLabel, inGroup, newestFirst, outcome, shortVerdict } from "../../model/live";
import type { Review } from "../../model/types";
import { ModelName } from "./LiveStrip";
const LIMIT = 200;
const RED = "text-[#e5484d]";
const ROW = "grid h-9 w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 px-2 text-left text-[12px]";
export type LensMode = "live" | "settling" | "off";
function Expanded({ review }: { review: Review }) {
const verdicts = review.verdicts.length
? review.verdicts
@ -41,19 +38,6 @@ function Expanded({ review }: { review: Review }) {
);
}
function ReadingRow({ item, now }: { item: InFlight; now: number }) {
return (
<div className={ROW}>
<span aria-label="reading" className="size-1.5 rounded-full bg-foreground motion-safe:animate-pulse" />
<span className="truncate text-foreground">{item.agent || "trace"}</span>
<span className="truncate font-mono text-[11px] text-muted-foreground">{item.trace_id.slice(0, 8)}</span>
<span className="text-[11px] tabular-nums text-muted-foreground">
reading · {durationLabel(Math.max(0, now - Date.parse(item.started_at)))}
</span>
</div>
);
}
function DoneRow({ review, now, open, onToggle }: { review: Review; now: number; open: boolean; onToggle: () => void }) {
const result = outcome(review);
return (
@ -81,87 +65,26 @@ function DoneRow({ review, now, open, onToggle }: { review: Review; now: number;
);
}
function useLensBox(target: string, layout: string) {
const list = useRef<HTMLDivElement>(null);
const [box, setBox] = useState<{ top: number; height: number } | null>(null);
useLayoutEffect(() => {
const nodes = [...(list.current?.querySelectorAll<HTMLElement>(`[data-lens="${target}"]`) ?? [])];
const first = nodes[0];
const last = nodes.at(-1);
setBox(first && last ? { top: first.offsetTop, height: last.offsetTop + last.offsetHeight - first.offsetTop } : null);
}, [target, layout]);
return { list, box };
}
function Lens({ box, model, mode }: { box: { top: number; height: number }; model: string; mode: LensMode }) {
return (
<div
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-x-0 top-0 rounded-xl bg-muted ring-1 ring-border",
"transition-[transform,height,opacity] duration-[250ms] ease-out motion-reduce:transition-none",
mode === "settling" && "opacity-0 duration-[400ms]",
)}
style={{ transform: `translateY(${box.top}px)`, height: box.height }}
>
<span className="absolute -top-2.5 right-3 inline-flex items-center rounded-full bg-background px-2 py-0.5 shadow-sm ring-1 ring-border">
<ModelName model={model} size="md" />
</span>
</div>
);
}
export function TraceList({
reading,
reviews,
model,
group,
mode,
nowLine,
}: {
reading: readonly InFlight[];
reviews: readonly Review[];
model: string;
group: string | null;
mode: LensMode;
nowLine: string | null;
}) {
const now = useNow(reading.length ? 200 : 5000);
export function TraceList({ reviews, group }: { reviews: readonly Review[]; group: string | null }) {
const now = useNow(5000);
const [expanded, setExpanded] = useState<string | null>(null);
const rows = liveRows(reading, reviews, LIMIT).filter((row) => row.kind === "reading" || inGroup(row.review, group));
const inFlight = rows.some((row) => row.kind === "reading");
const target = inFlight ? "reading" : "slot";
const layout = `${expanded}|${rows.map((row) => `${row.kind}:${row.key}`).join(",")}`;
const { list, box } = useLensBox(target, layout);
const showSlot = mode === "live" && !inFlight;
const rows = newestFirst(reviews, LIMIT).filter((review) => inGroup(review, group));
if (!rows.length) return <p className="px-2 py-2 text-[12px] text-muted-foreground">No traces here yet.</p>;
return (
<div ref={list} className="relative pt-3">
{box && mode !== "off" && <Lens box={box} model={model} mode={mode} />}
{showSlot && nowLine && (
<div data-lens="slot" role="status" className={cn(ROW, "relative text-muted-foreground")}>
<span aria-hidden="true" className="size-1.5 rounded-full bg-foreground motion-safe:animate-pulse" />
<span className="col-span-3 truncate tabular-nums">{nowLine}</span>
</div>
)}
<ol aria-label="Reviewed traces" className="relative flex flex-col">
{rows.map((row) => (
<li key={row.key} data-lens={row.kind === "reading" ? "reading" : undefined}>
{row.kind === "reading" ? (
<ReadingRow item={row.item} now={now} />
) : (
<DoneRow
review={row.review}
now={now}
open={expanded === row.key}
onToggle={() => setExpanded(expanded === row.key ? null : row.key)}
/>
)}
</li>
))}
</ol>
{!rows.length && !showSlot && (
<p className="px-2 py-2 text-[12px] text-muted-foreground">No traces in this group yet.</p>
)}
</div>
<ol aria-label="Reviewed traces" className="flex flex-col">
{rows.map((review) => (
<li
key={review.execution_id}
className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-3 motion-safe:duration-300"
>
<DoneRow
review={review}
now={now}
open={expanded === review.execution_id}
onToggle={() => setExpanded(expanded === review.execution_id ? null : review.execution_id)}
/>
</li>
))}
</ol>
);
}

View file

@ -1,23 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import type { LensMode } from "./TraceList";
const SETTLE_MS = 400;
export function useLensMode(live: boolean): LensMode {
const [wasLive, setWasLive] = useState(live);
const [settled, setSettled] = useState(!live);
if (live && !wasLive) {
setWasLive(true);
setSettled(false);
}
useEffect(() => {
if (live || settled) return;
const timer = window.setTimeout(() => setSettled(true), SETTLE_MS);
return () => window.clearTimeout(timer);
}, [live, settled]);
if (live) return "live";
return settled ? "off" : "settling";
}