feat(lens): show what the worker is reading and make View run obvious

Each trace in flight gets a highlighted row with the model and a live timer, and becomes its completed row in place. Completed rows show the real review time. View run is an outline button next to the progress line, and clicking anywhere on the strip opens it too.
This commit is contained in:
Ishaan Jaff 2026-10-03 17:55:12 -07:00
parent 0376a8d7c6
commit b174232308
No known key found for this signature in database
4 changed files with 123 additions and 118 deletions

View file

@ -5,8 +5,8 @@ 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, Playback } from "../../model/live";
import type { Job } from "../../model/types";
import type { Conclusion, InFlight } from "../../model/live";
import type { Job, Review } from "../../model/types";
import { ConclusionsPanel } from "./ConclusionsPanel";
import { ModelName } from "./LiveStrip";
import { TraceList } from "./TraceList";
@ -27,9 +27,10 @@ export function LiveDrawer({
model,
status,
reviewed,
playback,
reviews,
now,
reading,
groups,
decided,
scope,
waiting,
}: {
@ -39,16 +40,15 @@ export function LiveDrawer({
model: string;
status: Job["status"];
reviewed: number;
playback: Pick<Playback, "played" | "current" | "pending">;
reviews: readonly Review[];
now: string | null;
reading: readonly InFlight[];
groups: readonly Conclusion[];
decided: number;
scope: string;
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)]">
@ -64,26 +64,28 @@ export function LiveDrawer({
<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}` : ""}
{now ?? `${reviewed} reviewed`}
{reviewed > reviews.length && reviews.length ? ` · showing latest ${reviews.length}` : ""}
</span>
</div>
{empty ? (
{reviews.length || reading.length ? (
<TraceList reading={reading} reviews={reviews} model={model} group={group} />
) : (
<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-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} />
<ConclusionsPanel
groups={groups}
total={reviews.length}
scope={scope}
selected={group}
onSelect={setGroup}
/>
</section>
</div>
</SheetContent>

View file

@ -6,9 +6,9 @@ import { modelsUsed } from "../../model/inbox";
import {
analysisModel,
conclusions,
decidedReviews,
inFlight,
issueCount,
shownCount,
nowLine,
stripState,
} from "../../model/live";
import { queueReasonText } from "../../model/status";
@ -18,7 +18,6 @@ import { useQueueReason, type QueueContext } from "../useQueueReason";
import { LiveDrawer } from "./LiveDrawer";
import { LiveStrip } from "./LiveStrip";
import { useStripOpen } from "./useLivePanels";
import { useReviewPlayback } from "./useReviewPlayback";
export function LiveRun({
job,
@ -35,12 +34,10 @@ export function LiveRun({
const reason = useQueueReason(job, queue);
const [stripOpen, setStripOpen] = useStripOpen();
const [drawerOpen, setDrawerOpen] = useState(false);
const playback = useReviewPlayback(reviews, live);
const reading = inFlight(job);
const withReviews = { ...job, reviews: [...reviews] };
const model = analysisModel([...reviews.map((r) => r.model), ...modelsUsed(job.steps), job.settings.model]);
const issues = issueCount(withReviews);
const reviewed = live ? shownCount(job.reviewed, playback) : job.reviewed;
const decided = decidedReviews(playback, playback.phase.verdict);
const open = () => setDrawerOpen(true);
const waiting = reason && (
<>
@ -56,8 +53,8 @@ export function LiveRun({
model={model}
state={stripState(withReviews, model, reason ? queueReasonText(reason) : undefined)}
waiting={waiting}
playback={playback}
reviewed={reviewed}
reviews={reviews}
reviewed={job.reviewed}
selected={job.coverage.selected}
issues={issues}
cost={job.cost}
@ -79,10 +76,11 @@ export function LiveRun({
name={name}
model={model}
status={job.status}
reviewed={reviewed}
playback={playback}
groups={conclusions(decided, job.settings.checks)}
decided={decided.length}
reviewed={job.reviewed}
reviews={reviews}
now={job.status === "running" ? nowLine(job, reading.length) : null}
reading={reading}
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 { PanelRight, X } from "lucide-react";
import { ArrowRight, PanelRight, X } from "lucide-react";
import type { ReactNode } from "react";
import { ProviderLogo } from "@/components/molecules/models/ProviderLogo";
@ -10,8 +10,7 @@ import { useNow } from "@/hooks/useNow";
import { cn } from "@/lib/cva.config";
import { money } from "../../model/format";
import { outcome, providerOf, queueRows, reviewKey, shortVerdict, type IssueCount, type StripState } from "../../model/live";
import type { Playback } from "../../model/live";
import { newestFirst, outcome, providerOf, reviewKey, shortVerdict, type IssueCount, type StripState } from "../../model/live";
import type { Review } from "../../model/types";
const RECENT = 3;
@ -29,14 +28,17 @@ export function ModelName({ model, size = "sm" }: { model: string; size?: keyof
);
}
function RecentLine({ review, now, onOpen }: { review: Review; now: number; onOpen: (review: Review) => void }) {
function RecentLine({ review, now, onOpen }: { review: Review; now: number; onOpen: () => void }) {
const result = outcome(review);
const issue = result === "issue";
return (
<li className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-top-1 motion-safe:duration-300">
<button
type="button"
onClick={() => onOpen(review)}
onClick={(event) => {
event.stopPropagation();
onOpen();
}}
className="grid w-full grid-cols-[0.75rem_minmax(0,9rem)_minmax(0,1fr)_auto] items-center gap-2 rounded px-1.5 py-0.5 text-left hover:bg-muted"
>
<span aria-label={result} className={issue ? "text-[#e5484d]" : ""}>
@ -59,7 +61,7 @@ function issueLabel({ count, scope }: IssueCount): string {
export function LiveStrip({
model,
state,
playback,
reviews,
reviewed,
selected,
issues,
@ -71,26 +73,42 @@ export function LiveStrip({
model: string;
state: StripState;
waiting?: ReactNode;
playback: Pick<Playback, "played" | "current">;
reviews: readonly Review[];
reviewed: number;
selected: number;
issues: IssueCount;
cost: number;
onOpen: (review?: Review) => void;
onOpen: () => void;
onClose: () => void;
}) {
const now = useNow(5000);
const recent = queueRows(playback, RECENT);
const recent = newestFirst(reviews, RECENT);
return (
<section
aria-label="Live trace results"
className="flex flex-wrap items-start gap-x-6 gap-y-2 rounded-md bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground"
onClick={onOpen}
className="flex cursor-pointer flex-wrap items-start gap-x-6 gap-y-2 rounded-md bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground transition-colors hover:bg-muted/50"
>
<div className="flex min-w-[16rem] flex-col gap-1">
<ModelName model={model} />
<span className="tabular-nums">
{reviewed} of {selected} traces · {issueLabel(issues)} · {money(cost)}
</span>
<div className="flex min-w-[16rem] items-start gap-3">
<div className="flex flex-col gap-1">
<ModelName model={model} />
<span className="tabular-nums">
{reviewed} of {selected} traces · {issueLabel(issues)} · {money(cost)}
</span>
</div>
<Button
variant="outline"
size="xs"
className="mt-0.5 shrink-0"
onClick={(event) => {
event.stopPropagation();
onOpen();
}}
>
<PanelRight />
View run
<ArrowRight />
</Button>
</div>
<div className="min-w-0 flex-1">
{state.kind === "failed" && (
@ -99,7 +117,7 @@ export function LiveStrip({
</p>
)}
{state.kind === "waiting" && (
<div role="status" className="flex flex-col gap-0.5 py-0.5">
<div role="status" className="flex flex-col gap-0.5 py-0.5" onClick={(event) => event.stopPropagation()}>
{waiting ?? state.message}
</div>
)}
@ -111,15 +129,18 @@ export function LiveStrip({
</ol>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
<Button variant="outline" size="xs" onClick={() => onOpen()} disabled={state.kind === "done" && !recent.length}>
<PanelRight />
View run
</Button>
<Button variant="ghost" size="icon-xs" aria-label="Hide live trace results" onClick={onClose}>
<X />
</Button>
</div>
<Button
variant="ghost"
size="icon-xs"
aria-label="Hide live trace results"
className="shrink-0"
onClick={(event) => {
event.stopPropagation();
onClose();
}}
>
<X />
</Button>
</section>
);
}

View file

@ -1,16 +1,16 @@
"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, inGroup, outcome, reviewKey, shortVerdict, traceRows, type Playback } from "../../model/live";
import { briefReasoning, durationLabel, inGroup, liveRows, outcome, shortVerdict, type InFlight } from "../../model/live";
import type { Review } from "../../model/types";
import { ModelName } from "./LiveStrip";
const LIMIT = 80;
const LIMIT = 200;
const RED = "text-[#e5484d]";
function Expanded({ review }: { review: Review }) {
@ -38,89 +38,73 @@ function Expanded({ review }: { review: Review }) {
);
}
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 };
function ReadingRow({ item, model, now }: { item: InFlight; model: string; 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">
<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="text-[11px] tabular-nums text-muted-foreground">
reading · {durationLabel(Math.max(0, now - Date.parse(item.started_at)))}
</span>
</div>
);
}
export function TraceList({
playback,
reading,
reviews,
model,
live,
group,
}: {
playback: Pick<Playback, "played" | "current" | "pending">;
reading: readonly InFlight[];
reviews: readonly Review[];
model: string;
live: boolean;
group: string | null;
}) {
const now = useNow(5000);
const now = useNow(reading.length ? 200 : 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>;
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>;
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);
<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;
const reviewing = state === "reviewing";
const queued = state === "queued";
return (
<li key={key} data-row={key} className="relative">
<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}
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",
)}
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={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",
)}
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>
{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={cn("truncate", result === "issue" ? RED : "text-muted-foreground")}>
{shortVerdict(review)}
</span>
<span className="text-[11px] tabular-nums text-muted-foreground">
{agoLabel(Date.parse(review.at), now)}
{durationLabel(review.duration_ms)} · {agoLabel(Date.parse(review.at), now)}
</span>
</button>
{open && <Expanded review={review} />}