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.
This commit is contained in:
Ishaan Jaff 2026-10-03 17:36:52 -07:00
parent 12738b0d79
commit a75dab2f41
No known key found for this signature in database
7 changed files with 114 additions and 9 deletions

View file

@ -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<ProgressSample[]>([]);
@ -53,7 +65,7 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
<section aria-label="Analysis progress" className="space-y-3 rounded-md border bg-muted/40 px-4 py-3 text-sm">
<div className="flex items-center justify-between gap-2">
<span className="truncate" role="status">
<span className="font-medium">{progress.title}</span>
<span className="font-medium">{reason ? QUEUED_TITLE[reason.kind] : progress.title}</span>
{!queued && <span className="text-muted-foreground"> · {progress.detail}</span>}
</span>
{onCancel && (
@ -117,7 +129,9 @@ export function InvestigationProgress({ job, onCancel }: { job: Job; onCancel?:
</ol>
<div className="flex flex-wrap gap-x-5 gap-y-1 pt-1 tabular-nums">
{queued ? (
<span className="text-muted-foreground">{progress.detail}</span>
<span className="text-muted-foreground">
{reason ? <QueueReasonText reason={reason} onConnect={queue?.onConnect} /> : progress.detail}
</span>
) : (
stats.map(([key, value]) => (
<span key={key}>

View file

@ -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 && (

View file

@ -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{" "}
<button type="button" onClick={onConnect} className="font-medium text-foreground underline underline-offset-2">
Connect worker
</button>
.
</>
);
}
export function WorkerTasks({ reason, queue }: { reason: QueueReason; queue?: QueueContext }) {
if (reason.kind === "no_worker" || !reason.tasks.length) return null;
return (
<ol aria-label="What the worker is doing" className="flex flex-col">
{reason.tasks.map((task) => (
<li key={task.lensId}>
<button
type="button"
disabled={!queue}
onClick={() => queue?.onOpenLens(task.lensId)}
className="w-full truncate rounded px-1.5 py-0.5 text-left tabular-nums hover:bg-muted hover:text-foreground"
>
{workerTaskText(task)}
</button>
</li>
))}
</ol>
);
}

View file

@ -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<typeof useInvestigationResults>;
agents?: readonly string[];
queue?: QueueContext;
}) {
const [runNow, setRunNow] = useState(false);
const {
@ -102,10 +105,11 @@ export function InvestigationDetail({
<InvestigationProgress
key={active.id}
job={active}
queue={queue}
onCancel={readOnly ? undefined : () => void update((api) => api.cancelRun(lens.id))}
/>
)}
{live && <LiveRun key={live.id} job={live} reviews={live.reviews} name={lens.settings.name} />}
{live && <LiveRun key={live.id} job={live} reviews={live.reviews} name={lens.settings.name} queue={queue} />}
{runNow && (
<RunNowDialog
lens={lens}

View file

@ -13,14 +13,28 @@ import {
shownCount,
stripState,
} from "../../model/live";
import { queueReasonText } from "../../model/status";
import type { Job, Review } from "../../model/types";
import { QueueReasonText, WorkerTasks } from "../QueueReasonText";
import { useQueueReason, type QueueContext } from "../useQueueReason";
import { LiveDrawer } from "./LiveDrawer";
import { LiveStrip } from "./LiveStrip";
import { useStripOpen } from "./useLivePanels";
import { useReviewPlayback } from "./useReviewPlayback";
export function LiveRun({ job, reviews, name }: { job: Job; reviews: readonly Review[]; name: string }) {
export function LiveRun({
job,
reviews,
name,
queue,
}: {
job: Job;
reviews: readonly Review[];
name: string;
queue?: QueueContext;
}) {
const live = job.status === "queued" || job.status === "running";
const reason = useQueueReason(job, queue);
const [stripOpen, setStripOpen] = useStripOpen();
const [drawerOpen, setDrawerOpen] = useState(false);
const [pinned, setPinned] = useState<string | null>(null);
@ -42,7 +56,15 @@ export function LiveRun({ job, reviews, name }: { job: Job; reviews: readonly Re
{stripOpen ? (
<LiveStrip
model={model}
state={stripState(withReviews, model)}
state={stripState(withReviews, model, reason ? queueReasonText(reason) : undefined)}
waiting={
reason && (
<>
<QueueReasonText reason={reason} onConnect={queue?.onConnect} />
<WorkerTasks reason={reason} queue={queue} />
</>
)
}
playback={playback}
reviewed={reviewed}
selected={job.coverage.selected}

View file

@ -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<Playback, "played" | "current">;
reviewed: number;
selected: number;
@ -96,9 +99,9 @@ export function LiveStrip({
</p>
)}
{state.kind === "waiting" && (
<p role="status" className="py-0.5">
{state.message}
</p>
<div role="status" className="flex flex-col gap-0.5 py-0.5">
{waiting ?? state.message}
</div>
)}
{recent.length > 0 && state.kind !== "waiting" && (
<ol aria-label="Recently reviewed traces" className="flex flex-col">

View file

@ -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);
}