feat(lens): collapse the live run to an ambient line with show work

This commit is contained in:
Ishaan Jaff 2026-10-03 16:49:53 -07:00
parent d2e96c41f2
commit 1fb70ff6be
No known key found for this signature in database
3 changed files with 136 additions and 67 deletions

View file

@ -1,11 +1,12 @@
"use client";
import { ChevronDown, Loader2 } from "lucide-react";
import { useMemo, useRef } from "react";
import { ProviderLogo } from "@/components/molecules/models/ProviderLogo";
import { StatusBadge, type StatusTone } from "@/components/shared/table_cells";
import { Button } from "@/components/ui/button";
import { useNow } from "@/hooks/useNow";
import { cn } from "@/lib/cva.config";
import { durationText, money } from "../../model/format";
import { modelsUsed } from "../../model/inbox";
@ -18,7 +19,7 @@ import {
providerOf,
reviewKey,
shownCount,
tokenLabel,
tickerLine,
verdictLine,
} from "../../model/live";
import type { Job, Review } from "../../model/types";
@ -26,78 +27,106 @@ import { ConclusionsPanel, type Arrival } from "./ConclusionsPanel";
import { ReadingPanel } from "./ReadingPanel";
import { ReviewQueue } from "./ReviewQueue";
import { useReviewPlayback } from "./useReviewPlayback";
import { useShowWork } from "./useShowWork";
const STATUS: Record<Job["status"], { label: string; tone: StatusTone }> = {
queued: { label: "Queued", tone: "neutral" },
running: { label: "Running", tone: "neutral" },
completed: { label: "Done", tone: "neutral" },
failed: { label: "Failed", tone: "error" },
cancelled: { label: "Cancelled", tone: "neutral" },
};
const COLUMN_HEAD =
"sticky top-0 z-sticky flex h-8 shrink-0 items-center justify-between gap-2 border-b border-border bg-muted/40 px-3 text-[10px] tracking-[0.08em] text-muted-foreground uppercase backdrop-blur";
const COLUMN = "flex min-h-0 flex-col overflow-y-auto";
const COLUMN = "flex max-h-[320px] min-h-0 flex-col overflow-y-auto";
function useArrival(review: Review | null, verdict: boolean, compact: boolean): Arrival | null {
const issue = review && verdict && !compact ? review.verdicts.find((v) => v.kind === "issue") : undefined;
function useArrival(review: Review | null, verdict: boolean, enabled: boolean): Arrival | null {
const issue = review && verdict && enabled ? review.verdicts.find((v) => v.kind === "issue") : undefined;
const key = review && issue ? reviewKey(review) : "";
const checkId = issue?.check_id ?? "";
const text = review ? verdictLine(review) : "";
return useMemo(() => (key ? { key, checkId, text } : null), [key, checkId, text]);
}
function ModelLabel({ model }: { model: string }) {
function Model({ model }: { model: string }) {
if (!model) return null;
const provider = providerOf(model);
return (
<span data-testid="live-run-model" className="inline-flex min-w-0 items-center gap-1.5 text-muted-foreground">
{provider && <ProviderLogo provider={provider} className="size-3.5 shrink-0" />}
<span className="truncate font-mono text-[11px]">{model}</span>
<span data-testid="live-run-model" className="inline-flex shrink-0 items-center gap-1.5 text-foreground">
{provider && <ProviderLogo provider={provider} className="size-4 shrink-0" />}
<span className="font-mono text-[11px]">{model}</span>
</span>
);
}
function Ticker({ review }: { review: Review | null }) {
return (
<span className="relative h-4 min-w-0 flex-1 overflow-hidden text-muted-foreground">
{review && (
<span
key={reviewKey(review)}
data-testid="live-run-ticker"
className="absolute inset-0 truncate motion-safe:animate-in motion-safe:fade-in motion-safe:duration-300"
>
{tickerLine(review)}
</span>
)}
</span>
);
}
export function LiveRun({ job, onCancel }: { job: Job; onCancel?: () => void }) {
const live = job.status === "queued" || job.status === "running";
const [open, toggle] = useShowWork();
const playback = useReviewPlayback(job.reviews, live);
const { current, played, phase, duration } = playback;
const { current, played, phase, duration, replay } = playback;
const now = useNow(1000);
const verdictRef = useRef<HTMLDivElement>(null);
const model = analysisModel([...job.reviews.map((r) => r.model), ...modelsUsed(job.steps), job.settings.model]);
const decided = phase.verdict && current ? [...played, current] : played;
const groups = conclusions(decided, job.settings.checks);
const arrival = useArrival(current, phase.verdict, isCompact(duration));
const arrival = useArrival(current, phase.verdict, open && !isCompact(duration));
const stats = liveStats(job, now);
const reviewed = shownCount(job.reviewed, playback);
const reviewed = live ? shownCount(job.reviewed, playback) : job.reviewed;
const selected = job.coverage.selected;
const percent = live ? Math.min(100, Math.round((reviewed / Math.max(1, selected)) * 100)) : 100;
const issues = decided.filter((r) => outcome(r) === "issue").length;
const status = STATUS[job.status];
const issues = (live ? decided : job.reviews).filter((r) => outcome(r) === "issue").length;
const showWork = () => {
if (!open && !live) replay();
toggle();
};
const issueText = `${issues} ${issues === 1 ? "issue" : "issues"}`;
return (
<section aria-label="Analysis progress" className="overflow-hidden rounded-md border bg-card">
<header className="flex min-h-10 flex-wrap items-center justify-between gap-x-4 gap-y-1 border-b px-3 py-1.5 text-[12px]">
<div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
<StatusBadge tone={status.tone} label={status.label} dataTestId="live-run-status" />
<span role="status" className="truncate text-foreground">
{live ? job.stage : "Complete"}
<div className="flex h-9 items-center gap-3 px-3 text-[12px]">
{live ? (
<>
<Loader2 aria-hidden="true" className="size-3.5 shrink-0 text-muted-foreground motion-safe:animate-spin" />
<Model model={model} />
<Ticker review={current} />
<span role="status" className="shrink-0 text-muted-foreground tabular-nums">
{reviewed} of {selected} traces · {issueText} · {money(stats.cost)} · {durationText(stats.elapsedSeconds)}
</span>
</>
) : (
<span role="status" className="flex min-w-0 flex-1 items-center gap-1.5 text-muted-foreground">
<span className="shrink-0">Reviewed {reviewed} traces with</span>
<Model model={model} />
<span className="truncate tabular-nums">
in {durationText(stats.elapsedSeconds)} · {issueText} · {money(stats.cost)}
</span>
</span>
<span className="text-muted-foreground tabular-nums">
{reviewed} of {selected} traces
</span>
<ModelLabel model={model} />
</div>
<div className="flex items-center gap-4 font-mono text-[11px] text-muted-foreground tabular-nums">
<span title="Elapsed">{durationText(stats.elapsedSeconds)}</span>
<span title="Tokens">{tokenLabel(stats.tokens)}</span>
<span title="Cost">{money(stats.cost)}</span>
{live && onCancel && (
<Button variant="ghost" size="xs" onClick={onCancel}>
Cancel
</Button>
)}
</div>
</header>
)}
{live && onCancel && (
<Button variant="ghost" size="xs" className="shrink-0" onClick={onCancel}>
Cancel
</Button>
)}
<Button
variant="ghost"
size="xs"
aria-expanded={open}
className="shrink-0 text-muted-foreground"
onClick={showWork}
>
{open ? "Hide work" : "Show work"}
<ChevronDown className={cn("transition-transform duration-150", open && "rotate-180")} />
</Button>
</div>
<div
role="progressbar"
aria-label="Traces reviewed"
@ -105,37 +134,37 @@ export function LiveRun({ job, onCancel }: { job: Job; onCancel?: () => void })
aria-valuemax={100}
aria-valuenow={percent}
aria-valuetext={`${reviewed} of ${selected} traces reviewed`}
className="h-0.5 bg-foreground/10"
className="h-px bg-foreground/10"
>
<div
className="h-full bg-foreground transition-[width] duration-300 motion-reduce:transition-none"
className="h-full bg-foreground/60 transition-[width] duration-300 motion-reduce:transition-none"
style={{ width: `${percent}%` }}
/>
</div>
<div className="grid grid-cols-1 md:h-[420px] md:grid-cols-[minmax(0,15rem)_minmax(0,1fr)] xl:grid-cols-[16rem_minmax(0,1fr)_20rem]">
<div className={`${COLUMN} max-h-[420px] border-b md:border-r md:border-b-0`}>
<div className={COLUMN_HEAD}>
<span>Traces</span>
<span className="font-mono tracking-normal normal-case">{reviewed}</span>
{open && (
<div className="grid grid-cols-1 md:grid-cols-[minmax(0,15rem)_minmax(0,1fr)] xl:grid-cols-[16rem_minmax(0,1fr)_20rem]">
<div className={`${COLUMN} border-b md:border-r md:border-b-0`}>
<div className={COLUMN_HEAD}>
<span>Traces</span>
<span className="font-mono tracking-normal normal-case">{reviewed}</span>
</div>
<ReviewQueue playback={playback} live={live} />
</div>
<ReviewQueue playback={playback} live={live} />
</div>
<div className={`${COLUMN} max-h-[420px] border-b xl:border-r xl:border-b-0`}>
<div className={COLUMN_HEAD}>
<span>{live ? "Reading trace" : "Last trace"}</span>
<div className={`${COLUMN} border-b xl:border-r xl:border-b-0`}>
<div className={COLUMN_HEAD}>
<span>{live ? "Reading trace" : "Last trace"}</span>
</div>
<ReadingPanel review={current} phase={phase} verdictRef={verdictRef} />
</div>
<ReadingPanel review={current} phase={phase} verdictRef={verdictRef} />
</div>
<div className={`${COLUMN} max-h-[420px] md:col-span-2 xl:col-span-1`}>
<div className={COLUMN_HEAD}>
<span>Conclusions</span>
<span className="font-mono tracking-normal normal-case">
{issues} {issues === 1 ? "issue" : "issues"}
</span>
<div className={`${COLUMN} md:col-span-2 xl:col-span-1`}>
<div className={COLUMN_HEAD}>
<span>Conclusions</span>
<span className="font-mono tracking-normal normal-case">{issueText}</span>
</div>
<ConclusionsPanel groups={groups} reviewed={decided.length} arrival={arrival} verdictRef={verdictRef} />
</div>
<ConclusionsPanel groups={groups} reviewed={decided.length} arrival={arrival} verdictRef={verdictRef} />
</div>
</div>
)}
</section>
);
}

View file

@ -1,6 +1,6 @@
"use client";
import { useEffect, useReducer, useState } from "react";
import { useCallback, useEffect, useReducer, useState } from "react";
import { playbackPhase, playbackReducer, startPlayback, type Phase, type Playback } from "../../model/live";
import type { Review } from "../../model/types";
@ -11,7 +11,10 @@ function prefersReducedMotion(): boolean {
return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
export function useReviewPlayback(reviews: readonly Review[], live: boolean): Playback & { phase: Phase } {
export function useReviewPlayback(
reviews: readonly Review[],
live: boolean,
): Playback & { phase: Phase; replay: () => void } {
const [still] = useState(prefersReducedMotion);
const [state, dispatch] = useReducer(playbackReducer, undefined, () =>
playbackReducer(startPlayback(reviews, live), still ? { type: "settle" } : { type: "tick", now: Date.now() }),
@ -34,10 +37,15 @@ export function useReviewPlayback(reviews: readonly Review[], live: boolean): Pl
return () => window.clearInterval(timer);
}, [busy, still]);
const replay = useCallback(() => {
dispatch({ type: "replay" });
if (still) dispatch({ type: "settle" });
}, [still]);
const current = state.current;
const phase =
still || !current
? { span: -1, typed: current?.reasoning.length ?? 0, verdict: true }
: playbackPhase(now - state.startedAt, state.duration, current.spans.length, current.reasoning.length);
return { ...state, phase };
return { ...state, phase, replay };
}

View file

@ -0,0 +1,32 @@
"use client";
import { useCallback, useState } from "react";
const KEY = "litellm.lens.showWork";
function readStored(): boolean {
try {
return window.localStorage.getItem(KEY) === "1";
} catch {
return false;
}
}
function store(open: boolean): void {
try {
window.localStorage.setItem(KEY, open ? "1" : "0");
} catch {
return;
}
}
export function useShowWork(): [boolean, () => void] {
const [open, setOpen] = useState(readStored);
const toggle = useCallback(() => {
setOpen((current) => {
store(!current);
return !current;
});
}, []);
return [open, toggle];
}