feat(lens): add an ambient live strip that opens the drawer

This commit is contained in:
Ishaan Jaff 2026-10-03 17:04:54 -07:00
parent 8e473ade78
commit 68cc5484d1
No known key found for this signature in database
4 changed files with 218 additions and 178 deletions

View file

@ -1,170 +1,78 @@
"use client";
import { ChevronDown, Loader2 } from "lucide-react";
import { useMemo, useRef } from "react";
import { useState } from "react";
import { ProviderLogo } from "@/components/molecules/models/ProviderLogo";
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";
import {
analysisModel,
conclusions,
isCompact,
liveStats,
outcome,
providerOf,
focusedReview,
issueCount,
reviewKey,
shownCount,
tickerLine,
verdictLine,
stripState,
} from "../../model/live";
import type { Job, Review } from "../../model/types";
import { ConclusionsPanel, type Arrival } from "./ConclusionsPanel";
import { ReadingPanel } from "./ReadingPanel";
import { ReviewQueue } from "./ReviewQueue";
import { LiveDrawer } from "./LiveDrawer";
import { LiveStrip } from "./LiveStrip";
import { useDrawerOpen, useStripOpen } from "./useLivePanels";
import { useReviewPlayback } from "./useReviewPlayback";
import { useShowWork } from "./useShowWork";
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 max-h-[320px] min-h-0 flex-col overflow-y-auto";
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 Model({ model }: { model: string }) {
if (!model) return null;
const provider = providerOf(model);
return (
<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 }) {
export function LiveRun({ job, name }: { job: Job; name: string }) {
const live = job.status === "queued" || job.status === "running";
const [open, toggle] = useShowWork();
const [stripOpen, setStripOpen] = useStripOpen();
const [drawerOpen, setDrawerOpen] = useDrawerOpen(job.id, live);
const [pinned, setPinned] = useState<string | null>(null);
const playback = useReviewPlayback(job.reviews, live);
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, open && !isCompact(duration));
const stats = liveStats(job, now);
const { review: focused, following } = focusedReview(job.reviews, pinned, playback.current);
const issues = issueCount(job);
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 = (live ? decided : job.reviews).filter((r) => outcome(r) === "issue").length;
const showWork = () => {
if (!open && !live) replay();
toggle();
const open = (review?: Review) => {
setPinned(review && review !== playback.current ? reviewKey(review) : null);
setDrawerOpen(true);
};
const issueText = `${issues} ${issues === 1 ? "issue" : "issues"}`;
return (
<section aria-label="Analysis progress" className="overflow-hidden rounded-md border bg-card">
<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>
)}
{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"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={percent}
aria-valuetext={`${reviewed} of ${selected} traces reviewed`}
className="h-px bg-foreground/10"
>
<div
className="h-full bg-foreground/60 transition-[width] duration-300 motion-reduce:transition-none"
style={{ width: `${percent}%` }}
<>
{stripOpen && (
<LiveStrip
model={model}
state={stripState(job, model)}
playback={playback}
reviewed={reviewed}
selected={job.coverage.selected}
issues={issues}
cost={job.cost}
onOpen={open}
onClose={() => setStripOpen(false)}
/>
</div>
{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>
<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>
<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>
</div>
)}
</section>
{!stripOpen && (
<button
type="button"
onClick={() => setStripOpen(true)}
className="self-start text-[11px] text-muted-foreground hover:text-foreground"
>
Show live trace results
</button>
)}
<LiveDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
name={name}
model={model}
job={job}
playback={playback}
live={live}
focused={focused}
following={following}
phase={playback.phase}
groups={conclusions(job.reviews, job.settings.checks)}
scope={issues.scope === "findings" ? `from the last ${job.reviews.length} reviews` : issues.scope}
onPick={(review) => setPinned(review === playback.current ? null : reviewKey(review))}
onFollow={() => setPinned(null)}
/>
</>
);
}

View file

@ -0,0 +1,121 @@
"use client";
import { PanelRight, X } from "lucide-react";
import { ProviderLogo } from "@/components/molecules/models/ProviderLogo";
import { agoLabel } from "@/components/view_logs/TraceView/lensField";
import { Button } from "@/components/ui/button";
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 type { Review } from "../../model/types";
const RECENT = 5;
const MARK = { issue: "●", clear: "✓", unknown: "–" } as const;
export function ModelName({ model }: { model: string }) {
if (!model) return null;
const provider = providerOf(model);
return (
<span data-testid="live-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 RecentLine({ review, now, onOpen }: { review: Review; now: number; onOpen: (review: Review) => void }) {
const result = outcome(review);
const issue = result === "issue";
return (
<li className="motion-safe:animate-in motion-safe:fade-in motion-safe:duration-300">
<button
type="button"
onClick={() => onOpen(review)}
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]" : ""}>
{MARK[result]}
</span>
<span className="truncate">{review.agent || review.name}</span>
<span className={cn("truncate", issue && "text-[#e5484d]")}>{shortVerdict(review)}</span>
<span className="tabular-nums">{agoLabel(Date.parse(review.at), now)}</span>
</button>
</li>
);
}
function issueLabel({ count, scope }: IssueCount): string {
const noun = count === 1 ? "issue" : "issues";
if (scope === "findings") return `${count} ${noun} found`;
return scope ? `${count} ${noun} ${scope}` : `${count} ${noun}`;
}
export function LiveStrip({
model,
state,
playback,
reviewed,
selected,
issues,
cost,
onOpen,
onClose,
}: {
model: string;
state: StripState;
playback: Pick<Playback, "played" | "current">;
reviewed: number;
selected: number;
issues: IssueCount;
cost: number;
onOpen: (review?: Review) => void;
onClose: () => void;
}) {
const now = useNow(5000);
const recent = queueRows(playback, RECENT);
return (
<section
aria-label="Live trace results"
className="flex flex-wrap gap-x-6 gap-y-2 rounded-md bg-muted/30 px-3 py-2 text-[12px] text-muted-foreground"
>
<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>
<div className="min-w-0 flex-1">
{state.kind === "failed" && (
<p role="alert" className="py-0.5 text-[#e5484d]">
{state.message}
</p>
)}
{state.kind === "waiting" && (
<p role="status" className="py-0.5">
{state.message}
</p>
)}
{recent.length > 0 && state.kind !== "waiting" && (
<ol aria-label="Recently reviewed traces" className="flex flex-col">
{recent.map((review) => (
<RecentLine key={reviewKey(review)} review={review} now={now} onOpen={onOpen} />
))}
</ol>
)}
</div>
<div className="flex shrink-0 items-start gap-1">
<Button variant="ghost" size="xs" onClick={() => onOpen()} disabled={!recent.length}>
<PanelRight />
View live
</Button>
<Button variant="ghost" size="icon-xs" aria-label="Hide live trace results" onClick={onClose}>
<X />
</Button>
</div>
</section>
);
}

View file

@ -0,0 +1,43 @@
"use client";
import { useCallback, useState } from "react";
const STRIP_KEY = "litellm.lens.liveStrip";
const DRAWER_KEY = "litellm.lens.liveDrawerClosed";
function read(key: string): string | null {
try {
return window.localStorage.getItem(key);
} catch {
return null;
}
}
function write(key: string, value: string): void {
try {
window.localStorage.setItem(key, value);
} catch {
return;
}
}
export function useStripOpen(): [boolean, (open: boolean) => void] {
const [open, setOpen] = useState(() => read(STRIP_KEY) !== "closed");
const update = useCallback((next: boolean) => {
setOpen(next);
write(STRIP_KEY, next ? "open" : "closed");
}, []);
return [open, update];
}
export function useDrawerOpen(jobId: string, live: boolean): [boolean, (open: boolean) => void] {
const [open, setOpen] = useState(() => live && read(DRAWER_KEY) !== jobId);
const update = useCallback(
(next: boolean) => {
setOpen(next);
if (!next) write(DRAWER_KEY, jobId);
},
[jobId],
);
return [open, update];
}

View file

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