From a75dab2f410bf023924fc519dc79a686c1e7dcec Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 3 Oct 2026 17:36:52 -0700 Subject: [PATCH] feat(lens): show the queue reason and what the worker is doing in the live strip The progress header and the strip replace "Queued for your worker" with the concrete reason. While waiting, the strip lists the busy worker's investigations; click one to open it. --- .../investigations/InvestigationProgress.tsx | 20 ++++++++-- .../investigations/InvestigationsView.tsx | 6 +++ .../lens/investigations/QueueReasonText.tsx | 37 +++++++++++++++++++ .../detail/InvestigationDetail.tsx | 6 ++- .../lens/investigations/live/LiveRun.tsx | 26 ++++++++++++- .../lens/investigations/live/LiveStrip.tsx | 9 +++-- .../lens/investigations/useQueueReason.ts | 19 ++++++++++ 7 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx create mode 100644 ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx index 5d14a10f238..c169cfa0a3a 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationProgress.tsx @@ -18,18 +18,30 @@ import { durationText } from "../model/format"; import { nextCheckStatus } from "../model/status"; import { type Lens, type Job } from "../model/types"; import { cn } from "@/lib/cva.config"; +import { QueueReasonText } from "./QueueReasonText"; +import { useQueueReason, type QueueContext } from "./useQueueReason"; const steps = ["review runs", "find patterns", "check evidence"]; const markers = { done: "✓", active: "▸", todo: "·" }; const blocks = 32; +const QUEUED_TITLE = { busy: "Waiting in line", no_worker: "Waiting for a worker", starting: "Picking up" } as const; function stageState(index: number, current: number): keyof typeof markers { if (index < current) return "done"; return index === current ? "active" : "todo"; } -export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?: () => void }) { +export function InvestigationProgress({ + job, + queue, + onCancel, +}: { + job: Job; + queue?: QueueContext; + onCancel?: () => void; +}) { const now = useNow(1000); + const reason = useQueueReason(job, queue); const progress = analysisProgress(job); const fraction = analysisFraction(progress); const [samples, setSamples] = useState([]); @@ -53,7 +65,7 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
- {progress.title} + {reason ? QUEUED_TITLE[reason.kind] : progress.title} {!queued && · {progress.detail}} {onCancel && ( @@ -117,7 +129,9 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
{queued ? ( - {progress.detail} + + {reason ? : progress.detail} + ) : ( stats.map(([key, value]) => ( diff --git a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx index 3a5894962ec..187e96df238 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/InvestigationsView.tsx @@ -281,6 +281,12 @@ export function InvestigationsView({ connected={connected} results={results} agents={Array.isArray(agents.data) ? agents.data : []} + queue={{ + lenses, + workers: query.data?.workers ?? [], + onConnect: () => setWorkerSetup(true), + onOpenLens: selectLens, + }} /> )} {editing && ( diff --git a/ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx b/ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx new file mode 100644 index 00000000000..cec98b30fbc --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/QueueReasonText.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { queueReasonText, workerTaskText, type QueueReason } from "../model/status"; +import type { QueueContext } from "./useQueueReason"; + +export function QueueReasonText({ reason, onConnect }: { reason: QueueReason; onConnect?: () => void }) { + if (reason.kind !== "no_worker" || !onConnect) return <>{queueReasonText(reason)}; + return ( + <> + No worker connected. Start one from{" "} + + . + + ); +} + +export function WorkerTasks({ reason, queue }: { reason: QueueReason; queue?: QueueContext }) { + if (reason.kind === "no_worker" || !reason.tasks.length) return null; + return ( +
    + {reason.tasks.map((task) => ( +
  1. + +
  2. + ))} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx b/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx index 1a9d2297593..431bc9eefd3 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/InvestigationDetail.tsx @@ -7,6 +7,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { RunsTab } from "./RunsTab"; import { InvestigationProgress } from "../InvestigationProgress"; import { LiveRun } from "../live/LiveRun"; +import { type QueueContext } from "../useQueueReason"; import { liveJob } from "../../model/live"; import { RunNowDialog } from "./RunNowDialog"; import { useState } from "react"; @@ -32,6 +33,7 @@ export function InvestigationDetail({ connected, results, agents = [], + queue, }: { lens: Lens; readOnly: boolean; @@ -43,6 +45,7 @@ export function InvestigationDetail({ connected: boolean; results: ReturnType; agents?: readonly string[]; + queue?: QueueContext; }) { const [runNow, setRunNow] = useState(false); const { @@ -102,10 +105,11 @@ export function InvestigationDetail({ void update((api) => api.cancelRun(lens.id))} /> )} - {live && } + {live && } {runNow && ( (null); @@ -42,7 +56,15 @@ export function LiveRun({ job, reviews, name }: { job: Job; reviews: readonly Re {stripOpen ? ( + + + + ) + } playback={playback} reviewed={reviewed} selected={job.coverage.selected} diff --git a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx index bdd04b36621..3a7db401f5a 100644 --- a/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx +++ b/ui/litellm-dashboard/src/components/lens/investigations/live/LiveStrip.tsx @@ -1,6 +1,7 @@ "use client"; import { PanelRight, X } from "lucide-react"; +import type { ReactNode } from "react"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { agoLabel } from "@/components/view_logs/TraceView/lensField"; @@ -63,11 +64,13 @@ export function LiveStrip({ selected, issues, cost, + waiting, onOpen, onClose, }: { model: string; state: StripState; + waiting?: ReactNode; playback: Pick; reviewed: number; selected: number; @@ -96,9 +99,9 @@ export function LiveStrip({

)} {state.kind === "waiting" && ( -

- {state.message} -

+
+ {waiting ?? state.message} +
)} {recent.length > 0 && state.kind !== "waiting" && (
    diff --git a/ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts b/ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts new file mode 100644 index 00000000000..7337892d671 --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/investigations/useQueueReason.ts @@ -0,0 +1,19 @@ +"use client"; + +import { useNow } from "@/hooks/useNow"; + +import { queueReason, type QueueReason } from "../model/status"; +import type { Job, Lens, LensList } from "../model/types"; + +export interface QueueContext { + lenses: readonly Lens[]; + workers: readonly LensList["workers"][number][]; + onConnect: () => void; + onOpenLens: (id: string) => void; +} + +export function useQueueReason(job: Job, queue: QueueContext | undefined): QueueReason | null { + const now = useNow(1000); + if (job.status !== "queued" || !queue) return null; + return queueReason(job, queue.lenses, queue.workers, now); +}