feat(lens): simplify View run to traces and conclusions

The left pane is the trace list. A soft highlighter carrying the provider and model slides to the trace being reviewed, and clicking a row shows just Lens's reasoning and verdicts. The right pane keeps one conclusion card per check.
This commit is contained in:
Ishaan Jaff 2026-10-03 17:47:02 -07:00
parent 55d9425a82
commit b00cb8d733
No known key found for this signature in database
5 changed files with 181 additions and 112 deletions

View file

@ -39,47 +39,7 @@ export function ConclusionsPanel({
if (!groups.length) return <p className="text-[12px] text-muted-foreground">Nothing concluded yet.</p>;
return (
<div className="flex flex-col gap-1.5">
{SECTIONS.map(({ issue, title }) => (
<GroupList
key={title}
title={title}
groups={groups.filter((group) => group.issue === issue)}
total={total}
selected={selected}
flashing={flashing}
onSelect={onSelect}
/>
))}
{scope && <p className="text-[11px] text-muted-foreground">{scope}</p>}
</div>
);
}
const SECTIONS = [
{ issue: true, title: "Issues" },
{ issue: false, title: "Patterns" },
] as const;
function GroupList({
title,
groups,
total,
selected,
flashing,
onSelect,
}: {
title: string;
groups: readonly Conclusion[];
total: number;
selected: string | null;
flashing: ReadonlySet<string>;
onSelect: (key: string | null) => void;
}) {
if (!groups.length) return null;
return (
<>
<h3 className="pt-1 text-[11px] text-muted-foreground">{title}</h3>
<ol aria-label={title} className="flex flex-col gap-1.5">
<ol aria-label="Conclusions" className="flex flex-col gap-1.5">
{groups.map((group) => {
const active = selected === group.key;
return (
@ -104,10 +64,25 @@ function GroupList({
group.issue ? "bg-[#e5484d]" : "bg-muted-foreground/40",
)}
/>
<span className="line-clamp-2 text-[13px] leading-snug text-foreground">{group.label}</span>
<span className="flex min-w-0 flex-col gap-0.5">
<span className="line-clamp-2 text-[13px] leading-snug font-medium text-foreground">
{group.label}
</span>
<span className="line-clamp-1 text-[11px] text-muted-foreground">{group.latest}</span>
</span>
</span>
<span className="text-[20px] leading-none font-semibold tabular-nums text-foreground">
{group.count}
<span className="flex shrink-0 flex-col items-end gap-0.5">
<span
className={cn(
"text-[20px] leading-none font-semibold tabular-nums",
group.count ? "text-foreground" : "text-muted-foreground",
)}
>
{group.count}
</span>
{group.noted > 0 && (
<span className="text-[10px] tabular-nums text-muted-foreground">{group.noted} noted</span>
)}
</span>
</span>
<span aria-hidden="true" className="h-1 w-full overflow-hidden rounded-full bg-muted">
@ -124,6 +99,7 @@ function GroupList({
);
})}
</ol>
</>
{scope && <p className="text-[11px] text-muted-foreground">{scope}</p>}
</div>
);
}

View file

@ -3,15 +3,13 @@
import { useState, type ReactNode } from "react";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import type { Conclusion, Phase, Playback } from "../../model/live";
import type { Job, Review } from "../../model/types";
import type { Conclusion, Playback } from "../../model/live";
import type { Job } from "../../model/types";
import { ConclusionsPanel } from "./ConclusionsPanel";
import { ModelName } from "./LiveStrip";
import { ReadingPanel } from "./ReadingPanel";
import { ReviewQueue } from "./ReviewQueue";
import { TraceList } from "./TraceList";
const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
queued: { label: "Queued", tone: "neutral" },
@ -21,7 +19,6 @@ const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
cancelled: { label: "Cancelled", tone: "neutral" },
};
const PANE_TITLE = "text-[12px] font-semibold text-foreground";
const SETTLED: Phase = { span: -1, typed: Number.MAX_SAFE_INTEGER, verdict: true };
export function LiveDrawer({
open,
@ -31,38 +28,27 @@ export function LiveDrawer({
status,
reviewed,
playback,
live,
focused,
following,
phase,
groups,
decided,
scope,
waiting,
onPick,
onFollow,
}: {
waiting?: ReactNode;
open: boolean;
onClose: () => void;
name: string;
model: string;
status: Job["status"];
reviewed: number;
playback: Pick<Playback, "played" | "current">;
live: boolean;
focused: Review | null;
following: boolean;
phase: Phase;
playback: Pick<Playback, "played" | "current" | "pending">;
groups: readonly Conclusion[];
decided: number;
scope: string;
onPick: (review: Review) => void;
onFollow: () => void;
waiting?: ReactNode;
}) {
const [group, setGroup] = useState<string | null>(null);
const badge = STATUS[status];
const shown = playback.played.length + (playback.current ? 1 : 0);
const empty = !shown && !playback.pending.length;
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)]">
@ -71,43 +57,33 @@ export function LiveDrawer({
<SheetDescription render={<div />} className="flex flex-wrap items-center gap-2.5 pr-10">
<StatusBadge tone={badge.tone} label={badge.label} />
<ModelName model={model} size="md" />
{!following && live && (
<Button variant="outline" size="xs" className="ml-auto rounded-full" onClick={onFollow}>
Back to live
</Button>
)}
</SheetDescription>
</SheetHeader>
<div className="grid min-h-0 flex-1 grid-cols-1 md:grid-cols-[minmax(0,58fr)_minmax(0,42fr)]">
<section aria-label="Now reviewing" className="min-h-0 overflow-y-auto border-b px-5 py-4 md:border-r md:border-b-0">
<h2 className={`${PANE_TITLE} mb-3`}>{following && live ? "Now reviewing" : "Trace"}</h2>
{focused ? (
<ReadingPanel
key={following ? "live" : focused.execution_id}
review={focused}
phase={following ? phase : SETTLED}
/>
) : (
<div className="flex flex-col gap-1 py-6 text-[12px] text-muted-foreground">
<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="text-[11px] tabular-nums text-muted-foreground">
{reviewed} reviewed{reviewed > shown && shown ? ` · showing latest ${shown}` : ""}
</span>
</div>
{empty ? (
<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>
) : (
<TraceList
playback={playback}
model={model}
live={status === "queued" || status === "running"}
group={group}
/>
)}
</section>
<section aria-label="Conclusions so far" className="flex min-h-0 flex-col gap-5 overflow-y-auto px-5 py-4">
<div className="flex flex-col gap-3">
<h2 className={PANE_TITLE}>Conclusions so far</h2>
<ConclusionsPanel groups={groups} total={decided} scope={scope} selected={group} onSelect={setGroup} />
</div>
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between gap-3">
<h2 className={PANE_TITLE}>Traces</h2>
<span className="text-[11px] tabular-nums text-muted-foreground">
{reviewed} reviewed{reviewed > shown ? ` · showing latest ${shown}` : ""}
</span>
</div>
<ReviewQueue playback={playback} live={live} focused={focused} group={group} onPick={onPick} />
</div>
<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={decided} scope={scope} selected={group} onSelect={setGroup} />
</section>
</div>
</SheetContent>

View file

@ -7,9 +7,7 @@ import {
analysisModel,
conclusions,
decidedReviews,
focusedReview,
issueCount,
reviewKey,
shownCount,
stripState,
} from "../../model/live";
@ -37,19 +35,13 @@ export function LiveRun({
const reason = useQueueReason(job, queue);
const [stripOpen, setStripOpen] = useStripOpen();
const [drawerOpen, setDrawerOpen] = useState(false);
const [pinned, setPinned] = useState<string | null>(null);
const playback = useReviewPlayback(reviews, live);
const withReviews = { ...job, reviews: [...reviews] };
const model = analysisModel([...reviews.map((r) => r.model), ...modelsUsed(job.steps), job.settings.model]);
const { review: focused, following } = focusedReview(reviews, pinned, playback.current);
const issues = issueCount(withReviews);
const reviewed = live ? shownCount(job.reviewed, playback) : job.reviewed;
const decided = decidedReviews(playback, playback.phase.verdict);
const pin = (review: Review) => setPinned(review === playback.current ? null : reviewKey(review));
const open = (review?: Review) => {
if (review) pin(review);
setDrawerOpen(true);
};
const open = () => setDrawerOpen(true);
const waiting = reason && (
<>
<QueueReasonText reason={reason} onConnect={queue?.onConnect} />
@ -89,16 +81,10 @@ export function LiveRun({
status={job.status}
reviewed={reviewed}
playback={playback}
live={live}
focused={focused}
following={following}
phase={playback.phase}
groups={conclusions(decided, job.settings.checks)}
decided={decided.length}
scope={job.reviewed > reviews.length ? `From the latest ${reviews.length} of ${job.reviewed} reviewed traces` : ""}
waiting={waiting}
onPick={pin}
onFollow={() => setPinned(null)}
/>
</>
);

View file

@ -0,0 +1,132 @@
"use client";
import { useLayoutEffect, useRef, useState } from "react";
import { agoLabel } from "@/components/view_logs/TraceView/lensField";
import { useNow } from "@/hooks/useNow";
import { cn } from "@/lib/cva.config";
import { briefReasoning, inGroup, outcome, reviewKey, shortVerdict, traceRows, type Playback } from "../../model/live";
import type { Review } from "../../model/types";
import { ModelName } from "./LiveStrip";
const LIMIT = 80;
const RED = "text-[#e5484d]";
function Expanded({ review }: { review: Review }) {
const verdicts = review.verdicts.length
? review.verdicts
: [{ check_id: "", kind: "pattern" as const, summary: review.cannot_assess ? "Not enough evidence to judge" : "No issue observed" }];
return (
<div className="flex flex-col gap-1.5 px-7 pt-0.5 pb-2.5 text-[12px] leading-relaxed">
{review.reasoning && <p className="text-muted-foreground">{briefReasoning(review.reasoning)}</p>}
<ul className="flex flex-col gap-1">
{verdicts.map((verdict, index) => (
<li key={`${verdict.check_id}-${index}`} className={cn("flex gap-2", verdict.kind === "issue" ? RED : "text-foreground")}>
<span
aria-hidden="true"
className={cn(
"mt-[0.45rem] size-1.5 shrink-0 rounded-full",
verdict.kind === "issue" ? "bg-[#e5484d]" : "bg-muted-foreground/40",
)}
/>
<span className="min-w-0">{verdict.summary}</span>
</li>
))}
</ul>
</div>
);
}
function useHighlighter(active: string | null, layout: string) {
const list = useRef<HTMLOListElement>(null);
const [box, setBox] = useState<{ top: number; height: number } | null>(null);
useLayoutEffect(() => {
const row = active ? list.current?.querySelector<HTMLElement>(`[data-row="${CSS.escape(active)}"]`) : null;
setBox(row ? { top: row.offsetTop, height: row.offsetHeight } : null);
}, [active, layout]);
return { list, box };
}
export function TraceList({
playback,
model,
live,
group,
}: {
playback: Pick<Playback, "played" | "current" | "pending">;
model: string;
live: boolean;
group: string | null;
}) {
const now = useNow(5000);
const [expanded, setExpanded] = useState<string | null>(null);
const rows = traceRows(playback, LIMIT)
.map((row) => (live || row.state !== "reviewing" ? row : { ...row, state: "done" as const }))
.filter(({ review, state }) => state !== "done" || inGroup(review, group));
const active = live && playback.current ? reviewKey(playback.current) : null;
const { list, box } = useHighlighter(active, `${expanded}|${rows.map((row) => reviewKey(row.review)).join(",")}`);
if (!rows.length) return <p className="py-2 text-[12px] text-muted-foreground">No traces yet.</p>;
return (
<ol aria-label="Reviewed traces" ref={list} className="relative flex flex-col">
{box && (
<li
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 rounded-lg bg-muted ring-1 ring-border transition-[transform,height] duration-200 ease-out motion-reduce:transition-none"
style={{ transform: `translateY(${box.top}px)`, height: box.height }}
/>
)}
{rows.map(({ review, state }) => {
const key = reviewKey(review);
const result = outcome(review);
const open = expanded === key;
const reviewing = state === "reviewing";
const queued = state === "queued";
return (
<li key={key} data-row={key} className="relative">
<button
type="button"
aria-expanded={open}
disabled={queued}
onClick={() => setExpanded(open ? null : key)}
className={cn(
"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]",
!reviewing && !queued && "hover:bg-muted/50",
queued && "opacity-40",
)}
>
<span
aria-label={queued ? "queued" : reviewing ? "reviewing" : result}
className={cn(
"size-1.5 rounded-full",
reviewing
? "bg-foreground motion-safe:animate-pulse"
: result === "issue" && !queued
? "bg-[#e5484d]"
: "bg-muted-foreground/40",
)}
/>
<span className="truncate text-foreground">{review.agent || review.name}</span>
{reviewing ? (
<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">reading…</span>
</span>
) : (
<span className={cn("truncate", result === "issue" && !queued ? RED : "text-muted-foreground")}>
{queued ? "up next" : shortVerdict(review)}
</span>
)}
<span className="text-[11px] tabular-nums text-muted-foreground">
{agoLabel(Date.parse(review.at), now)}
</span>
</button>
{open && <Expanded review={review} />}
</li>
);
})}
</ol>
);
}

View file

@ -3,7 +3,6 @@
import { useCallback, useEffect, useReducer, useState } from "react";
import { playbackPhase, playbackReducer, startPlayback, type Phase, type Playback } from "../../model/live";
import { timeline } from "../../model/spanPreview";
import type { Review } from "../../model/types";
const FRAME_MS = 40;
@ -47,6 +46,6 @@ export function useReviewPlayback(
const phase =
still || !current
? { span: -1, typed: current?.reasoning.length ?? 0, verdict: true }
: playbackPhase(now - state.startedAt, state.duration, timeline(current.spans).length, current.reasoning.length);
: playbackPhase(now - state.startedAt, state.duration, current.spans.length, current.reasoning.length);
return { ...state, phase, replay };
}