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 7ae1560ffb9..e020b0f1ca0 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.test.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { analysisModel, conclusions, + tickerLine, liveJob, liveStats, outcome, @@ -71,6 +72,11 @@ describe("review outcome", () => { expect(outcome(review("a", { cannot_assess: true, verdicts: [issue("i")] }))).toBe("unknown"); }); + it("names the agent and short trace id in the ticker, falling back to the run name", () => { + expect(tickerLine(review("a", { trace_id: "a91f3c02deadbeef" }))).toBe("reading support-bot · a91f3c02"); + expect(tickerLine(review("a", { agent: "", name: "refund", trace_id: "7d21" }))).toBe("reading refund · 7d21"); + }); + it("leads the verdict line with the issue summary over patterns", () => { expect(verdictLine(review("a", { verdicts: [pattern("p", "fine"), issue("i", "made it up")] }))).toBe( "made it up", @@ -174,6 +180,15 @@ describe("playback queue", () => { expect(playbackReducer(second, { type: "tick", now: 1e9 })).toBe(second); }); + it("replays a finished run from its first review", () => { + 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("a"); + }); + it("settles everything at once for reduced motion", () => { const start = startPlayback(reviews, true); const settled = playbackReducer(start, { type: "settle" }); diff --git a/ui/litellm-dashboard/src/components/lens/model/live.ts b/ui/litellm-dashboard/src/components/lens/model/live.ts index 210cfd30627..f5651c7c922 100644 --- a/ui/litellm-dashboard/src/components/lens/model/live.ts +++ b/ui/litellm-dashboard/src/components/lens/model/live.ts @@ -51,6 +51,10 @@ export function outcome(review: Pick): Out return review.verdicts.some((v) => v.kind === "issue") ? "issue" : "clear"; } +export function tickerLine(review: Pick): string { + return `reading ${review.agent || review.name} · ${review.trace_id.slice(0, 8)}`; +} + export function verdictLine(review: Pick): string { const issue = review.verdicts.find((v) => v.kind === "issue"); if (issue) return issue.summary; @@ -140,7 +144,8 @@ export interface Playback { export type PlaybackAction = | { type: "enqueue"; reviews: readonly Review[] } | { type: "tick"; now: number } - | { type: "settle" }; + | { type: "settle" } + | { type: "replay" }; const PLAYED_LIMIT = 200; @@ -193,6 +198,11 @@ function settle(state: Playback): Playback { }; } +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": @@ -201,6 +211,8 @@ export function playbackReducer(state: Playback, action: PlaybackAction): Playba return advance(state, action.now); case "settle": return settle(state); + case "replay": + return replay(state); } }