diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts b/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts deleted file mode 100644 index 2d6ab928f3c..00000000000 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/useReviewPlayback.ts +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useReducer, useState } from "react"; - -import { playbackPhase, playbackReducer, startPlayback, type Phase, type Playback } from "../../model/live"; -import type { Review } from "../../model/types"; - -const FRAME_MS = 40; - -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; replay: () => void } { - const [still] = useState(prefersReducedMotion); - const [state, dispatch] = useReducer(playbackReducer, undefined, () => - playbackReducer(startPlayback(reviews, live), still ? { type: "settle" } : { type: "tick", now: Date.now() }), - ); - const [now, setNow] = useState(Date.now); - - useEffect(() => { - dispatch({ type: "enqueue", reviews }); - if (still) dispatch({ type: "settle" }); - }, [reviews, still]); - - const busy = state.pending.length > 0 || now - state.startedAt < state.duration; - useEffect(() => { - if (!busy || still) return; - const timer = window.setInterval(() => { - const at = Date.now(); - setNow(at); - dispatch({ type: "tick", now: at }); - }, FRAME_MS); - 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, replay }; -} diff --git a/ui/litellm-dashboard/src/components/lens/model/live.test.ts b/ui/litellm-dashboard/src/components/lens/model/live.test.ts index 915b0794915..98030136f08 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.test.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.test.ts @@ -4,8 +4,11 @@ import { briefReasoning, checkLabel, conclusions, - traceRows, - decidedReviews, + durationLabel, + newestFirst, + nowLine, + inFlight, + liveRows, grownGroups, inGroup, share, @@ -17,16 +20,10 @@ import { liveJob, liveStats, outcome, - playbackPhase, - playbackReducer, providerOf, - queueRows, rateLabel, reviewKey, secondsToFinishReading, - shownCount, - startPlayback, - stepDuration, tokenLabel, } from "./live"; import type { Job, Review } from "./types"; @@ -145,141 +142,12 @@ describe("conclusions", () => { expect([...grownGroups(before, after)]).toEqual(["y"]); }); - it("lists upcoming traces above the one being read and finished ones below, newest first", () => { - const [a, b, c, d] = ["a", "b", "c", "d"].map((id) => review(id)); - const rows = traceRows({ played: [a], current: b, pending: [c, d] }, 10); - expect(rows.map((row) => [row.review.execution_id, row.state])).toEqual([ - ["d", "queued"], - ["c", "queued"], - ["b", "reviewing"], - ["a", "done"], - ]); - expect(traceRows({ played: [a], current: b, pending: [c, d] }, 2)).toHaveLength(2); - }); - it("gives a bar share bounded to the total", () => { expect(share(3, 12)).toBe(0.25); expect(share(5, 0)).toBe(0); expect(share(9, 4)).toBe(1); }); - it("only counts the trace being read once its verdict is on screen", () => { - const [a, b] = [review("a"), review("b")]; - expect(decidedReviews({ played: [a], current: b }, false)).toEqual([a]); - expect(decidedReviews({ played: [a], current: b }, true)).toEqual([a, b]); - }); -}); - -describe("playback pacing", () => { - it("slows to a full window for one review and speeds up as the backlog grows", () => { - expect(stepDuration(1)).toBe(2400); - expect(stepDuration(0)).toBe(2400); - expect(stepDuration(2)).toBeLessThan(stepDuration(1)); - }); - - it("keeps every trace on screen long enough to read, however large the backlog", () => { - expect(stepDuration(3)).toBeGreaterThanOrEqual(1200); - expect(stepDuration(10_000)).toBeGreaterThanOrEqual(1200); - }); - - it("highlights spans one at a time, then types reasoning, then leaves the verdict up", () => { - const duration = 2000; - expect(playbackPhase(0, duration, 4, 100)).toEqual({ span: 0, typed: 0, verdict: false }); - expect(playbackPhase(duration * 0.29, duration, 4, 100).span).toBe(3); - const typing = playbackPhase(duration * 0.475, duration, 4, 100); - expect(typing).toEqual({ span: -1, typed: 50, verdict: false }); - const done = playbackPhase(duration * 0.65, duration, 4, 100); - expect(done).toEqual({ span: -1, typed: 100, verdict: true }); - expect(playbackPhase(duration * 0.99, duration, 4, 100)).toEqual(done); - }); - - it("shows a settled review whole", () => { - expect(playbackPhase(0, 0, 4, 100)).toEqual({ span: -1, typed: 100, verdict: true }); - }); -}); - -describe("playback queue", () => { - const reviews = ["a", "b", "c", "d", "e"].map((id) => review(id)); - - it("opens a live job replaying the last few reviews and a finished job on its final review", () => { - const live = startPlayback(reviews, true); - expect(live.played.map((r) => r.execution_id)).toEqual(["a", "b"]); - expect(live.current).toBeNull(); - expect(live.pending.map((r) => r.execution_id)).toEqual(["c", "d", "e"]); - - const done = startPlayback(reviews, false); - expect(done.played.map((r) => r.execution_id)).toEqual(["a", "b", "c", "d"]); - expect(done.current?.execution_id).toBe("e"); - expect(done.pending).toEqual([]); - }); - - it("enqueues only reviews it has not seen, so repeated polls do not replay", () => { - const start = startPlayback(reviews.slice(0, 2), true); - const polled = playbackReducer(start, { type: "enqueue", reviews: reviews.slice(0, 4) }); - expect(polled.pending.map((r) => r.execution_id)).toEqual(["a", "b", "c", "d"]); - expect(playbackReducer(polled, { type: "enqueue", reviews: reviews.slice(0, 4) })).toBe(polled); - }); - - it("treats a re-reviewed execution at a new time as new", () => { - const start = startPlayback([reviews[0]], false); - const again = review("a", { at: "2026-10-03T17:00:00Z" }); - expect(reviewKey(again)).not.toBe(reviewKey(reviews[0])); - expect(playbackReducer(start, { type: "enqueue", reviews: [again] }).pending).toEqual([again]); - }); - - it("advances one review per step and holds until the step finishes", () => { - const start = startPlayback(reviews.slice(0, 2), true); - expect(start.pending).toHaveLength(2); - const first = playbackReducer(start, { type: "tick", now: 1000 }); - expect(first.current?.execution_id).toBe("a"); - expect(first.duration).toBe(stepDuration(2)); - expect(playbackReducer(first, { type: "tick", now: 1000 + first.duration - 1 })).toBe(first); - const second = playbackReducer(first, { type: "tick", now: 1000 + first.duration }); - expect(second.current?.execution_id).toBe("b"); - expect(second.played.map((r) => r.execution_id)).toEqual(["a"]); - expect(playbackReducer(second, { type: "tick", now: 1e9 })).toBe(second); - }); - - it("catches up on a big backlog by skipping to the newest few, never by shortening steps", () => { - const many = Array.from({ length: 40 }, (_, n) => review(`r${n}`)); - const step = playbackReducer(startPlayback([], true), { type: "enqueue", reviews: many }); - const first = playbackReducer(step, { type: "tick", now: 0 }); - expect(first.current?.execution_id).toBe("r37"); - expect(first.pending.map((r) => r.execution_id)).toEqual(["r38", "r39"]); - expect(first.played.map((r) => r.execution_id)).toEqual(many.slice(0, 37).map((r) => r.execution_id)); - expect(first.duration).toBeGreaterThanOrEqual(1200); - expect(playbackReducer(first, { type: "tick", now: 1199 })).toBe(first); - }); - - it("replays a finished run, jumping to the last few", () => { - const done = startPlayback(reviews, false); - const again = playbackReducer(done, { type: "replay" }); - expect(again.current).toBeNull(); - expect(again.played).toEqual([]); - expect(again.pending.map((r) => r.execution_id)).toEqual(["a", "b", "c", "d", "e"]); - expect(playbackReducer(again, { type: "tick", now: 0 }).current?.execution_id).toBe("c"); - }); - - it("settles everything at once for reduced motion", () => { - const start = startPlayback(reviews, true); - const settled = playbackReducer(start, { type: "settle" }); - expect(settled.current?.execution_id).toBe("e"); - expect(settled.played.map((r) => r.execution_id)).toEqual(["a", "b", "c", "d"]); - expect(settled.pending).toEqual([]); - }); - - it("lists the newest review first, starting with the one being read", () => { - const start = startPlayback(reviews, false); - expect(queueRows(start, 3).map((r) => r.execution_id)).toEqual(["e", "d", "c"]); - const live = playbackReducer(startPlayback(reviews, true), { type: "tick", now: 0 }); - expect(queueRows(live, 10).map((r) => r.execution_id)).toEqual(["c", "b", "a"]); - }); - - it("counts reviews beyond the capped list without counting the unplayed backlog", () => { - const start = startPlayback(reviews, true); - expect(shownCount(5, start)).toBe(2); - expect(shownCount(120, start)).toBe(117); - }); }); describe("which job the live run shows", () => { @@ -462,3 +330,49 @@ describe("live stats", () => { expect(tokenLabel(3_400_000)).toBe("3.4M tok"); }); }); + +describe("honest live list", () => { + it("lists completed reviews newest first in the order they finished, never re-sorted by time", () => { + const reviews = [review("a", { at: "2026-10-03T16:05:00Z" }), review("b", { at: "2026-10-03T16:01:00Z" }), review("c")]; + expect(newestFirst(reviews, 10).map((r) => r.execution_id)).toEqual(["c", "b", "a"]); + expect(newestFirst(reviews, 2).map((r) => r.execution_id)).toEqual(["c", "b"]); + expect(reviews.map((r) => r.execution_id)).toEqual(["a", "b", "c"]); + }); + + it("says how many traces are in flight and how many are done", () => { + const job = (reviewed: number, selected: number) => ({ reviewed, coverage: { selected } }) as unknown as Job; + expect(nowLine(job(18, 30), 4)).toBe("Reviewing 4 at a time · 18 of 30 done"); + expect(nowLine(job(30, 30), 0)).toBe("30 of 30 done"); + expect(nowLine(job(3, 0), 1)).toBe("Reviewing 1 at a time · 3 done"); + }); + + it("keeps a trace as the same row from in flight to finished", () => { + const reading = (id: string) => ({ execution_id: id, trace_id: `t-${id}`, agent: "bot", started_at: "2026-10-03T16:00:00Z" }); + const before = liveRows([reading("x"), reading("y")], [review("a")], 10); + expect(before.map((row) => [row.kind, row.key])).toEqual([ + ["reading", "x"], + ["reading", "y"], + ["done", "a"], + ]); + const after = liveRows([reading("x"), reading("y")], [review("a"), review("y")], 10); + expect(after.map((row) => [row.kind, row.key])).toEqual([ + ["reading", "x"], + ["done", "y"], + ["done", "a"], + ]); + }); + + it("only shows in-flight traces while the job runs", () => { + const item = { execution_id: "x", trace_id: "t", agent: "bot", started_at: "2026-10-03T16:00:00Z" }; + const job = (status: Job["status"]) => ({ status, reading: [item] }) as unknown as Job; + expect(inFlight(job("running"))).toEqual([item]); + expect(inFlight(job("completed"))).toEqual([]); + expect(inFlight({ status: "running" } as Job)).toEqual([]); + }); + + it("formats review time", () => { + expect(durationLabel(420)).toBe("420ms"); + expect(durationLabel(1420)).toBe("1.4s"); + expect(durationLabel(83_000)).toBe("1m 23s"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/lens/model/live.ts b/ui/litellm-dashboard/src/components/lens/model/live.ts index 5325b047595..ca6f9a5a27e 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.ts @@ -12,12 +12,6 @@ export interface Conclusion { issue: boolean; } -export interface Phase { - span: number; - typed: number; - verdict: boolean; -} - export interface LiveStats { perSecond: number | null; tokens: number; @@ -25,13 +19,6 @@ export interface LiveStats { elapsedSeconds: number; } -const PACE_WINDOW_MS = 2400; -const MIN_VISIBLE_MS = 1500; -const CATCH_UP_BACKLOG = 3; -const READ_SHARE = 0.3; -const TYPE_SHARE = 0.35; -const REPLAY_ON_OPEN = 3; - export function liveJob(jobs: readonly Job[]): Job | undefined { const active = jobs.find((job) => job.status === "queued" || job.status === "running"); if (active) return active; @@ -116,23 +103,10 @@ export function unseen(reviews: readonly Review[], seen: ReadonlySet): R return reviews.filter((review) => !seen.has(reviewKey(review))); } -export function stepDuration(backlog: number): number { - return Math.max(MIN_VISIBLE_MS, Math.min(PACE_WINDOW_MS, Math.round(PACE_WINDOW_MS / Math.max(1, backlog)))); -} - export function analysisModel(candidates: readonly string[]): string { return candidates.find((model) => providerOf(model)) ?? candidates.find(Boolean) ?? ""; } -export function playbackPhase(elapsed: number, duration: number, spans: number, chars: number): Phase { - if (duration <= 0) return { span: -1, typed: chars, verdict: true }; - const t = Math.max(0, elapsed) / duration; - const reading = t < READ_SHARE; - const span = reading && spans > 0 ? Math.min(spans - 1, Math.floor((t / READ_SHARE) * spans)) : -1; - const typing = Math.min(1, Math.max(0, (t - READ_SHARE) / TYPE_SHARE)); - return { span, typed: Math.round(typing * chars), verdict: t >= READ_SHARE + TYPE_SHARE }; -} - const SHORT_LABEL = 48; function humanize(checkId: string): string { @@ -192,23 +166,6 @@ export function grownGroups(before: readonly Conclusion[], after: readonly Concl ); } -export type TraceRowState = "queued" | "reviewing" | "done"; - -export function traceRows( - playback: Pick, - limit: number, -): { review: Review; state: TraceRowState }[] { - return [ - ...[...playback.pending].reverse().map((review) => ({ review, state: "queued" as const })), - ...(playback.current ? [{ review: playback.current, state: "reviewing" as const }] : []), - ...[...playback.played].reverse().map((review) => ({ review, state: "done" as const })), - ].slice(0, limit); -} - -export function decidedReviews(playback: Pick, verdictShown: boolean): Review[] { - return [...playback.played, ...(playback.current && verdictShown ? [playback.current] : [])]; -} - export function readingStart(job: Pick): string { return job.steps.find((step) => step.kind === "stage" && step.label === "Reading executions")?.at ?? job.created_at; } @@ -245,98 +202,40 @@ export function tokenLabel(tokens: number): string { return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k tok` : `${tokens} tok`; } -export interface Playback { - played: readonly Review[]; - current: Review | null; - pending: readonly Review[]; - seen: ReadonlySet; - startedAt: number; - duration: number; +export function newestFirst(reviews: readonly Review[], limit: number): Review[] { + return [...reviews].reverse().slice(0, limit); } -export type PlaybackAction = - | { type: "enqueue"; reviews: readonly Review[] } - | { type: "tick"; now: number } - | { type: "settle" } - | { type: "replay" }; - -const PLAYED_LIMIT = 200; - -export function startPlayback(reviews: readonly Review[], live: boolean): Playback { - const replay = live ? Math.min(REPLAY_ON_OPEN, reviews.length) : 0; - const shown = reviews.slice(0, reviews.length - replay); - return { - played: live ? shown : shown.slice(0, -1), - current: live ? null : shown.at(-1) ?? null, - pending: reviews.slice(shown.length), - seen: new Set(reviews.map(reviewKey)), - startedAt: Number.NEGATIVE_INFINITY, - duration: 0, - }; +export interface InFlight { + execution_id: string; + trace_id: string; + agent: string; + started_at: string; } -function enqueue(state: Playback, reviews: readonly Review[]): Playback { - const fresh = unseen(reviews, state.seen); - if (!fresh.length) return state; - return { - ...state, - pending: [...state.pending, ...fresh], - seen: new Set([...state.seen, ...fresh.map(reviewKey)]), - }; +export function inFlight(job: Job): readonly InFlight[] { + const reading = (job as Job & { reading?: readonly InFlight[] }).reading; + return job.status === "running" ? (reading ?? []) : []; } -function advance(state: Playback, now: number): Playback { - if (!state.pending.length) return state; - if (state.current && now - state.startedAt < state.duration) return state; - const skip = Math.max(0, state.pending.length - CATCH_UP_BACKLOG); - const [next, ...rest] = state.pending.slice(skip); - const finished = [...(state.current ? [state.current] : []), ...state.pending.slice(0, skip)]; - return { - ...state, - played: [...state.played, ...finished].slice(-PLAYED_LIMIT), - current: next, - pending: rest, - startedAt: now, - duration: stepDuration(rest.length + 1), - }; +export type LiveRow = { kind: "reading"; key: string; item: InFlight } | { kind: "done"; key: string; review: Review }; + +export function liveRows(reading: readonly InFlight[], reviews: readonly Review[], limit: number): LiveRow[] { + const finished = new Set(reviews.map((review) => review.execution_id)); + const open = reading.filter((item) => !finished.has(item.execution_id)); + return [ + ...open.map((item) => ({ kind: "reading" as const, key: item.execution_id, item })), + ...newestFirst(reviews, limit).map((review) => ({ kind: "done" as const, key: review.execution_id, review })), + ]; } -function settle(state: Playback): Playback { - const all = [...state.played, ...(state.current ? [state.current] : []), ...state.pending]; - return { - ...state, - played: all.slice(0, -1).slice(-PLAYED_LIMIT), - current: all.at(-1) ?? null, - pending: [], - startedAt: Number.NEGATIVE_INFINITY, - duration: 0, - }; +export function nowLine(job: Pick, reading: number): string { + const { selected } = job.coverage; + const done = selected ? `${Math.min(job.reviewed, selected)} of ${selected} done` : `${job.reviewed} done`; + return reading ? `Reviewing ${reading} at a time · ${done}` : done; } -function replay(state: Playback): Playback { - const all = [...state.played, ...(state.current ? [state.current] : []), ...state.pending]; - return { ...state, played: [], current: null, pending: all, startedAt: Number.NEGATIVE_INFINITY, duration: 0 }; -} - -export function playbackReducer(state: Playback, action: PlaybackAction): Playback { - switch (action.type) { - case "enqueue": - return enqueue(state, action.reviews); - case "tick": - return advance(state, action.now); - case "settle": - return settle(state); - case "replay": - return replay(state); - } -} - -export function queueRows(playback: Pick, limit: number): Review[] { - const newest = playback.current ? [playback.current] : []; - return [...newest, ...[...playback.played].reverse()].slice(0, limit); -} - -export function shownCount(reviewed: number, playback: Pick): number { - const local = playback.played.length + (playback.current ? 1 : 0); - return Math.max(local, reviewed - playback.pending.length); +export function durationLabel(ms: number): string { + if (ms < 1000) return `${Math.max(0, Math.round(ms))}ms`; + return ms < 60_000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`; }