feat(lens): slide one model rectangle over the traces being read

A single rounded rectangle carrying the provider logo and model wraps the real in-flight rows from job.reading. It translates and resizes over 250ms as traces finish in place. Before job.reading arrives it sits on a top slot showing the honest progress line, and when the run completes it fades out over 400ms. Rows have a fixed height and stable execution_id keys, so polls don't cause jumps or flicker.
This commit is contained in:
Ishaan Jaff 2026-10-03 17:58:18 -07:00
parent 5724190869
commit 28f70ea41d
No known key found for this signature in database
4 changed files with 134 additions and 52 deletions

View file

@ -10,6 +10,7 @@ import type { Job, Review } from "../../model/types";
import { ConclusionsPanel } from "./ConclusionsPanel";
import { ModelName } from "./LiveStrip";
import { TraceList } from "./TraceList";
import { useLensMode } from "./useLensMode";
const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
queued: { label: "Queued", tone: "neutral" },
@ -29,6 +30,7 @@ export function LiveDrawer({
reviewed,
reviews,
now,
done,
reading,
groups,
scope,
@ -42,6 +44,7 @@ export function LiveDrawer({
reviewed: number;
reviews: readonly Review[];
now: string | null;
done: string | null;
reading: readonly InFlight[];
groups: readonly Conclusion[];
scope: string;
@ -49,6 +52,8 @@ export function LiveDrawer({
}) {
const [group, setGroup] = useState<string | null>(null);
const badge = STATUS[status];
const live = status === "queued" || status === "running";
const mode = useLensMode(live);
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)]">
@ -63,13 +68,14 @@ export function LiveDrawer({
<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="text-[11px] tabular-nums text-muted-foreground">
{now ?? `${reviewed} reviewed`}
<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 ? (
<TraceList reading={reading} reviews={reviews} model={model} group={group} />
{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>

View file

@ -5,6 +5,7 @@ import { useState } from "react";
import { modelsUsed } from "../../model/inbox";
import {
analysisModel,
doneLine,
conclusions,
inFlight,
issueCount,
@ -80,6 +81,7 @@ export function LiveRun({
reviews={reviews}
now={job.status === "running" ? nowLine(job, reading.length) : null}
reading={reading}
done={job.status === "completed" ? doneLine(job) : null}
groups={conclusions(reviews, job.settings.checks)}
scope={job.reviewed > reviews.length ? `From the latest ${reviews.length} of ${job.reviewed} reviewed traces` : ""}
waiting={waiting}

View file

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useLayoutEffect, useRef, useState } from "react";
import { agoLabel } from "@/components/view_logs/TraceView/lensField";
import { useNow } from "@/hooks/useNow";
@ -12,6 +12,9 @@ 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
@ -38,17 +41,12 @@ function Expanded({ review }: { review: Review }) {
);
}
function ReadingRow({ item, model, now }: { item: InFlight; model: string; now: number }) {
function ReadingRow({ item, now }: { item: InFlight; now: number }) {
return (
<div className="grid w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 rounded-lg bg-muted px-2 py-2 text-[12px] ring-1 ring-border transition-colors duration-300">
<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="flex min-w-0 items-center gap-2 text-muted-foreground">
<span className="inline-flex shrink-0 items-center rounded-md bg-background px-1.5 py-0.5 ring-1 ring-border">
<ModelName model={model} />
</span>
<span className="truncate font-mono text-[11px]">{item.trace_id.slice(0, 8)}</span>
</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>
@ -56,61 +54,114 @@ function ReadingRow({ item, model, now }: { item: InFlight; model: string; now:
);
}
function DoneRow({ review, now, open, onToggle }: { review: Review; now: number; open: boolean; onToggle: () => void }) {
const result = outcome(review);
return (
<>
<button type="button" aria-expanded={open} onClick={onToggle} className={cn(ROW, "rounded-lg hover:bg-muted/50")}>
<span
aria-label={result}
className={cn("size-1.5 rounded-full", result === "issue" ? "bg-[#e5484d]" : "bg-muted-foreground/40")}
/>
<span className="truncate text-foreground">{review.agent || review.name}</span>
<span
className={cn(
"truncate motion-safe:animate-in motion-safe:fade-in motion-safe:duration-300",
result === "issue" ? RED : "text-muted-foreground",
)}
>
{shortVerdict(review)}
</span>
<span className="text-[11px] tabular-nums text-muted-foreground">
{durationLabel(review.duration_ms)} · {agoLabel(Date.parse(review.at), now)}
</span>
</button>
{open && <Expanded review={review} />}
</>
);
}
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);
const [expanded, setExpanded] = useState<string | null>(null);
const rows = liveRows(reading, reviews, LIMIT).filter((row) => row.kind === "reading" || inGroup(row.review, group));
if (!rows.length) return <p className="px-2 py-2 text-[12px] text-muted-foreground">No traces in this group yet.</p>;
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;
return (
<ol aria-label="Reviewed traces" className="flex flex-col">
{rows.map((row) => {
if (row.kind === "reading") {
return (
<li key={row.key} className="motion-safe:animate-in motion-safe:fade-in motion-safe:duration-150">
<ReadingRow item={row.item} model={model} now={now} />
</li>
);
}
const { review, key } = row;
const result = outcome(review);
const open = expanded === key;
return (
<li
key={key}
className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-1 motion-safe:duration-150"
>
<button
type="button"
aria-expanded={open}
onClick={() => setExpanded(open ? null : key)}
className="grid w-full grid-cols-[0.75rem_minmax(0,8rem)_minmax(0,1fr)_auto] items-center gap-2 rounded-lg px-2 py-2 text-left text-[12px] transition-colors duration-300 hover:bg-muted/50"
>
<span
aria-label={result}
className={cn("size-1.5 rounded-full", result === "issue" ? "bg-[#e5484d]" : "bg-muted-foreground/40")}
<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)}
/>
<span className="truncate text-foreground">{review.agent || review.name}</span>
<span className={cn("truncate", result === "issue" ? RED : "text-muted-foreground")}>
{shortVerdict(review)}
</span>
<span className="text-[11px] tabular-nums text-muted-foreground">
{durationLabel(review.duration_ms)} · {agoLabel(Date.parse(review.at), now)}
</span>
</button>
{open && <Expanded review={review} />}
)}
</li>
);
})}
</ol>
))}
</ol>
{!rows.length && !showSlot && (
<p className="px-2 py-2 text-[12px] text-muted-foreground">No traces in this group yet.</p>
)}
</div>
);
}

View file

@ -0,0 +1,23 @@
"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";
}