diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/RunNowDialog.test.ts b/ui/litellm-dashboard/src/components/lens/investigations/detail/RunNowDialog.test.ts
new file mode 100644
index 00000000000..617685d80e2
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/RunNowDialog.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+
+import { runRequest } from "./RunNowDialog";
+
+const base = { agent: "support", saved: "support", start: "", end: "" };
+
+describe("runRequest", () => {
+ it("sends nothing extra for the default since-last-run choice", () => {
+ expect(runRequest({ ...base, preset: null })).toEqual({});
+ });
+
+ it("only overrides the agent when it differs from the saved one", () => {
+ expect(runRequest({ ...base, preset: null, agent: "billing" })).toEqual({ agent_name: "billing" });
+ expect(runRequest({ ...base, preset: null, agent: " support " })).toEqual({});
+ });
+
+ it("turns a preset into a lookback window", () => {
+ expect(runRequest({ ...base, preset: 168 })).toEqual({ lookback_hours: 168 });
+ });
+
+ it("sends an exact window as ISO timestamps for a custom range", () => {
+ const choice = { ...base, preset: -1 as const, start: "2026-10-02T10:00", end: "2026-10-02T12:00" };
+ const request = runRequest(choice);
+ expect(request).toEqual({
+ start: new Date("2026-10-02T10:00").toISOString(),
+ end: new Date("2026-10-02T12:00").toISOString(),
+ });
+ });
+
+ it("rejects a missing or backwards custom range with a message instead of a request", () => {
+ const missing = { ...base, preset: -1 as const, start: "", end: "2026-10-02T12:00" };
+ const backwards = { ...base, preset: -1 as const, start: "2026-10-02T12:00", end: "2026-10-02T10:00" };
+ expect(runRequest(missing)).toBe("Choose a start and end time");
+ expect(runRequest(backwards)).toBe("Start time must be before end time");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/lens/investigations/detail/RunNowDialog.tsx b/ui/litellm-dashboard/src/components/lens/investigations/detail/RunNowDialog.tsx
new file mode 100644
index 00000000000..45a8ae06040
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/investigations/detail/RunNowDialog.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+
+import type { Lens } from "../../model/types";
+
+export interface RunWindow {
+ agent_name?: string;
+ start?: string;
+ end?: string;
+ lookback_hours?: number;
+}
+
+const PRESETS = [
+ { label: "Since last run", hours: null },
+ { label: "Last hour", hours: 1 },
+ { label: "Last 24h", hours: 24 },
+ { label: "Last 7d", hours: 168 },
+ { label: "Custom", hours: -1 },
+] as const;
+
+const localInput = (date: Date) =>
+ new Date(date.getTime() - date.getTimezoneOffset() * 60_000).toISOString().slice(0, 16);
+
+export interface RunChoice {
+ preset: (typeof PRESETS)[number]["hours"];
+ agent: string;
+ saved: string;
+ start: string;
+ end: string;
+}
+
+export function runRequest({ preset, agent, saved, start, end }: RunChoice): RunWindow | string {
+ const agentPart = agent.trim() && agent.trim() !== saved ? { agent_name: agent.trim() } : {};
+ if (preset === null) return agentPart;
+ if (preset > 0) return { ...agentPart, lookback_hours: preset };
+ const startMs = Date.parse(start);
+ const endMs = Date.parse(end);
+ if (Number.isNaN(startMs) || Number.isNaN(endMs)) return "Choose a start and end time";
+ if (startMs >= endMs) return "Start time must be before end time";
+ return { ...agentPart, start: new Date(startMs).toISOString(), end: new Date(endMs).toISOString() };
+}
+
+export function RunNowDialog({
+ lens,
+ agents,
+ busy,
+ onClose,
+ onRun,
+}: {
+ lens: Lens;
+ agents: readonly string[];
+ busy: boolean;
+ onClose: () => void;
+ onRun: (request: RunWindow) => Promise
;
+}) {
+ const now = new Date();
+ const [preset, setPreset] = useState<(typeof PRESETS)[number]["hours"]>(null);
+ const [agent, setAgent] = useState(lens.settings.agent_name ?? "");
+ const [start, setStart] = useState(localInput(new Date(now.getTime() - 3_600_000)));
+ const [end, setEnd] = useState(localInput(now));
+ const [error, setError] = useState("");
+ const submit = async () => {
+ const choice: RunChoice = { preset, agent, saved: lens.settings.agent_name ?? "", start, end };
+ const request = runRequest(choice);
+ if (typeof request === "string") {
+ setError(request);
+ return;
+ }
+ setError("");
+ await onRun(request);
+ };
+ return (
+ !open && !busy && onClose()}>
+
+
+ Run now
+
+ Runs once with these choices. The saved schedule and settings stay the same.
+
+
+
+
+
+ Cancel
+
+ void submit()}>
+ Run now
+
+
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/lens/model/inbox.test.ts b/ui/litellm-dashboard/src/components/lens/model/inbox.test.ts
new file mode 100644
index 00000000000..2114047ab3a
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/model/inbox.test.ts
@@ -0,0 +1,181 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ ALL_AGENTS,
+ UNKNOWN_AGENT,
+ filterInbox,
+ findingAgents,
+ inboxAgents,
+ inboxRows,
+ modelsUsed,
+ scheduleLabel,
+ stepLine,
+ type Step,
+} from "./inbox";
+import type { Finding, Lens } from "./types";
+
+const finding = (overrides: Partial): Finding =>
+ ({
+ id: "f",
+ check_id: "c",
+ kind: "issue",
+ status: "open",
+ title: "Tool errors swallowed",
+ description: "",
+ suggestion: "Retry on 5xx",
+ priority: "medium",
+ occurrences: ["run-1"],
+ evidence: [],
+ first_seen: "2026-10-01T00:00:00Z",
+ last_seen: "2026-10-01T00:00:00Z",
+ revision: 1,
+ ...overrides,
+ }) as Finding;
+
+const lens = (
+ id: string,
+ agent: string,
+ findings: Finding[],
+ { settings = {}, runs = [] }: { settings?: Partial; runs?: { id: string; service: string }[] } = {},
+): Lens =>
+ ({
+ id,
+ findings,
+ jobs: [{ sample: { executions: runs } }],
+ next_run_at: "2026-10-03T12:10:00Z",
+ settings: { name: id, agent_name: agent, service: "", enabled: true, interval_minutes: 15, ...settings },
+ }) as unknown as Lens;
+
+describe("findingAgents", () => {
+ it("names the agents the finding's runs were actually recorded under", () => {
+ const seen = lens("a", "", [], {
+ runs: [
+ { id: "run-1", service: "support-bot" },
+ { id: "run-2", service: "billing-bot" },
+ ],
+ });
+ expect(findingAgents(seen, finding({ occurrences: ["run-2", "run-1"] }))).toEqual(["billing-bot", "support-bot"]);
+ });
+
+ it("falls back to the configured agent, then to an explicit unknown, when no run says", () => {
+ expect(findingAgents(lens("a", "support", []), finding({}))).toEqual(["support"]);
+ expect(findingAgents(lens("a", "", []), finding({}))).toEqual([UNKNOWN_AGENT]);
+ });
+});
+
+const ranked = (id: string, title: string, priority: Finding["priority"], lastSeen: string): Partial => ({
+ id,
+ title,
+ priority,
+ last_seen: lastSeen,
+});
+
+describe("inboxRows", () => {
+ it("merges the same problem for the same agent across investigations into one row", () => {
+ const rows = inboxRows([
+ lens("a", "support", [finding({ id: "1", occurrences: ["run-1", "run-2"] })]),
+ lens("b", "support", [finding({ id: "2", title: "tool errors swallowed ", occurrences: ["run-2", "run-3"] })]),
+ ]);
+ expect(rows).toHaveLength(1);
+ expect(rows[0].sources).toHaveLength(2);
+ expect(rows[0].runs).toBe(3);
+ });
+
+ it("keeps the same title for different agents as separate problems", () => {
+ expect(inboxRows([lens("a", "support", [finding({})]), lens("b", "billing", [finding({})])])).toHaveLength(2);
+ });
+
+ it("takes the most urgent priority and latest sighting across merged findings", () => {
+ const [row] = inboxRows([
+ lens("a", "support", [finding({ priority: "low", last_seen: "2026-10-03T00:00:00Z" })]),
+ lens("b", "support", [finding({ priority: "high", last_seen: "2026-10-01T00:00:00Z" })]),
+ ]);
+ expect(row.priority).toBe("high");
+ expect(row.lastSeen).toBe("2026-10-03T00:00:00Z");
+ });
+
+ it("leaves out resolved, dismissed and pattern findings", () => {
+ const rows = inboxRows([
+ lens("a", "support", [
+ finding({ id: "1", status: "resolved", title: "a" }),
+ finding({ id: "2", status: "dismissed", title: "b" }),
+ finding({ id: "3", kind: "pattern", title: "c" }),
+ ]),
+ ]);
+ expect(rows).toEqual([]);
+ });
+
+ it("orders by priority, then by most recent", () => {
+ const rows = inboxRows([
+ lens("a", "x", [
+ finding(ranked("1", "old high", "high", "2026-10-01T00:00:00Z")),
+ finding(ranked("2", "new low", "low", "2026-10-03T00:00:00Z")),
+ finding(ranked("3", "new high", "high", "2026-10-02T00:00:00Z")),
+ ]),
+ ]);
+ expect(rows.map((r) => r.title)).toEqual(["new high", "old high", "new low"]);
+ });
+});
+
+const step = (overrides: Partial): Step =>
+ ({
+ at: "2026-10-03T12:00:00Z",
+ kind: "model",
+ label: "Reviewed a run",
+ model: "gpt-5.6",
+ purpose: "extract",
+ prompt_tokens: 0,
+ completion_tokens: 0,
+ cost: 0,
+ ...overrides,
+ }) as Step;
+
+describe("filterInbox", () => {
+ const rows = inboxRows([
+ lens("a", "support", [finding({ id: "1", title: "one", priority: "high" })]),
+ lens("b", "billing", [finding({ id: "2", title: "two", priority: "low" })]),
+ ]);
+
+ it("narrows to one agent and one priority, and lists every agent once", () => {
+ expect(filterInbox(rows, { agent: "billing", priority: "all" }).map((r) => r.title)).toEqual(["two"]);
+ expect(filterInbox(rows, { agent: ALL_AGENTS, priority: "high" }).map((r) => r.title)).toEqual(["one"]);
+ expect(filterInbox(rows, { agent: "billing", priority: "high" })).toEqual([]);
+ expect(inboxAgents(rows)).toEqual(["billing", "support"]);
+ });
+});
+
+describe("step feed helpers", () => {
+ it("summarizes a model call with its token count and cost", () => {
+ expect(stepLine(step({ prompt_tokens: 1000, completion_tokens: 200, cost: 0.0031 }))).toBe(
+ "Reviewed a run · 1.2k tok · $0.0031",
+ );
+ expect(stepLine(step({ kind: "stage", label: "Grouping observations" }))).toBe("Grouping observations");
+ });
+
+ it("lists models used, most used first, ignoring stage steps", () => {
+ expect(
+ modelsUsed([
+ step({ model: "claude-sonnet-5-5" }),
+ step({ model: "gpt-5.6" }),
+ step({ model: "gpt-5.6" }),
+ step({ kind: "stage", model: "" }),
+ ]),
+ ).toEqual(["gpt-5.6", "claude-sonnet-5-5"]);
+ });
+});
+
+describe("scheduleLabel", () => {
+ const now = Date.parse("2026-10-03T12:00:00Z");
+
+ it("says how often it runs and when the next run is", () => {
+ expect(scheduleLabel(lens("a", "x", []), now)).toBe("every 15m · next in 10m");
+ expect(scheduleLabel(lens("a", "x", [], { settings: { interval_minutes: 120 } }), now)).toBe(
+ "every 2h · next in 10m",
+ );
+ });
+
+ it("calls out paused and overdue investigations", () => {
+ expect(scheduleLabel(lens("a", "x", [], { settings: { enabled: false } }), now)).toBe("paused");
+ expect(scheduleLabel(lens("a", "x", []), Date.parse("2026-10-03T12:30:00Z"))).toBe("every 15m · due now");
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/lens/model/inbox.ts b/ui/litellm-dashboard/src/components/lens/model/inbox.ts
new file mode 100644
index 00000000000..1e2fd91175f
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/lens/model/inbox.ts
@@ -0,0 +1,123 @@
+import type { Finding, Job, Lens } from "./types";
+
+export type Step = Job["steps"][number];
+export type Priority = NonNullable;
+
+export interface InboxRow {
+ key: string;
+ title: string;
+ agents: readonly string[];
+ priority: Priority;
+ suggestion: string;
+ runs: number;
+ lastSeen: string;
+ sources: readonly { lens: Lens; finding: Finding }[];
+}
+
+export interface InboxFilter {
+ agent: string;
+ priority: Priority | "all";
+}
+
+export const ALL_AGENTS = "all";
+export const UNKNOWN_AGENT = "unknown agent";
+
+const priorityRank = { high: 0, medium: 1, low: 2 } as const;
+
+export function sampledExecutions(lens: Lens) {
+ return lens.jobs.flatMap((job) => job.sample?.executions ?? []);
+}
+
+export function findingAgents(lens: Lens, finding: Finding): readonly string[] {
+ const services = new Map(sampledExecutions(lens).map((run) => [run.id, run.service] as const));
+ const seen = new Set(finding.occurrences.map((id) => services.get(id)).filter((s): s is string => !!s));
+ if (seen.size) return [...seen].sort();
+ const configured = lens.settings.agent_name || lens.settings.service;
+ return [configured || UNKNOWN_AGENT];
+}
+
+function groupBy(items: readonly T[], key: (item: T) => string): Map {
+ return items.reduce((groups, item) => {
+ const k = key(item);
+ return groups.set(k, [...(groups.get(k) ?? []), item]);
+ }, new Map());
+}
+
+export function inboxRows(lenses: readonly Lens[]): InboxRow[] {
+ const open = lenses.flatMap((lens) =>
+ lens.findings
+ .filter((f) => f.status === "open" && f.kind === "issue")
+ .map((finding) => ({ lens, finding, agents: findingAgents(lens, finding) })),
+ );
+ const grouped = groupBy(open, ({ agents, finding }) => `${agents.join(",")}::${finding.title.trim().toLowerCase()}`);
+ return [...grouped.entries()]
+ .map(([key, sources]): InboxRow => {
+ const best = sources.reduce((a, b) =>
+ priorityRank[a.finding.priority ?? "medium"] <= priorityRank[b.finding.priority ?? "medium"] ? a : b,
+ );
+ return {
+ key,
+ title: best.finding.title,
+ agents: best.agents,
+ priority: best.finding.priority ?? "medium",
+ suggestion: best.finding.suggestion,
+ runs: new Set(sources.flatMap((s) => s.finding.occurrences)).size,
+ lastSeen: sources.map((s) => s.finding.last_seen).reduce((a, b) => (Date.parse(a) > Date.parse(b) ? a : b)),
+ sources: sources.map(({ lens, finding }) => ({ lens, finding })),
+ };
+ })
+ .sort(
+ (a, b) => priorityRank[a.priority] - priorityRank[b.priority] || Date.parse(b.lastSeen) - Date.parse(a.lastSeen),
+ );
+}
+
+export function filterInbox(rows: readonly InboxRow[], { agent, priority }: InboxFilter): InboxRow[] {
+ const agentMatches = (row: InboxRow) => agent === ALL_AGENTS || row.agents.includes(agent);
+ const priorityMatches = (row: InboxRow) => priority === "all" || row.priority === priority;
+ return rows.filter((row) => agentMatches(row) && priorityMatches(row));
+}
+
+export function inboxAgents(rows: readonly InboxRow[]): string[] {
+ return [...new Set(rows.flatMap((row) => row.agents))].sort();
+}
+
+export function stepLine(step: Step): string {
+ if (step.kind !== "model") return step.label;
+ const tokens = step.prompt_tokens + step.completion_tokens;
+ const size = tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k tok` : `${tokens} tok`;
+ return `${step.label} · ${size} · $${step.cost.toFixed(4)}`;
+}
+
+export function modelsUsed(steps: readonly Step[]): string[] {
+ const counts = groupBy(
+ steps.filter((s) => s.kind === "model" && s.model),
+ (s) => s.model,
+ );
+ return [...counts.entries()].sort((a, b) => b[1].length - a[1].length).map(([model]) => model);
+}
+
+function nextRunLabel(msUntil: number): string {
+ if (msUntil <= 0) return "due now";
+ if (msUntil < 60_000) return "next in <1m";
+ return `next in ${Math.ceil(msUntil / 60_000)}m`;
+}
+
+export function scheduleLabel(lens: Lens, now: number): string {
+ if (!lens.settings.enabled) return "paused";
+ const every = lens.settings.interval_minutes;
+ const cadence = every % 60 === 0 ? `every ${every / 60}h` : `every ${every}m`;
+ const next = Date.parse(lens.next_run_at) - now;
+ return `${cadence} · ${nextRunLabel(next)}`;
+}
+
+const WINDOW_FORMAT: Intl.DateTimeFormatOptions = {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+};
+
+export function windowLabel(job: Job): string {
+ const fmt = (iso: string) => new Date(iso).toLocaleString(undefined, WINDOW_FORMAT);
+ return `${fmt(job.start)} → ${fmt(job.end)}`;
+}
diff --git a/ui/litellm-dashboard/src/components/lens/model/progress.test.ts b/ui/litellm-dashboard/src/components/lens/model/progress.test.ts
index 06d396d3458..26ef40431b7 100644
--- a/ui/litellm-dashboard/src/components/lens/model/progress.test.ts
+++ b/ui/litellm-dashboard/src/components/lens/model/progress.test.ts
@@ -24,6 +24,8 @@ const coverage: Job["coverage"] = {
const job: Job = {
assessments: [],
+ steps: [],
+ trigger: "schedule",
coverage,
attempts: 0,
error: "",
diff --git a/ui/litellm-dashboard/src/components/lens/model/status.test.ts b/ui/litellm-dashboard/src/components/lens/model/status.test.ts
index a43061a16f4..b77490e981e 100644
--- a/ui/litellm-dashboard/src/components/lens/model/status.test.ts
+++ b/ui/litellm-dashboard/src/components/lens/model/status.test.ts
@@ -18,6 +18,8 @@ const coverage: Job["coverage"] = {
const job: Job = {
assessments: [],
+ steps: [],
+ trigger: "schedule",
coverage,
attempts: 0,
error: "",
diff --git a/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetupDialog.integration.test.tsx b/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetupDialog.integration.test.tsx
index 20406f0be34..3ea43016929 100644
--- a/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetupDialog.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/lens/setup/InvestigationSetupDialog.integration.test.tsx
@@ -133,18 +133,18 @@ describe("Lens setup", () => {
await user.click(screen.getByRole("button", { name: /Add your own/ }));
await user.click(screen.getByRole("button", { name: "Remove check 3" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
- await waitFor(() => expect(screen.getByRole("button", { name: "Run investigation" })).toBeEnabled());
+ await waitFor(() => expect(screen.getByRole("button", { name: "Run and monitor" })).toBeEnabled());
expect(screen.getByText("1 matching run")).toBeInTheDocument();
expect(screen.getByText("Research report")).toBeInTheDocument();
expect(screen.getByText(/2026-09-30/)).toBeInTheDocument();
expect(screen.getByRole("spinbutton", { name: "Monthly limit (USD)" })).not.toBeVisible();
- await user.click(screen.getByRole("button", { name: "Run investigation" }));
+ await user.click(screen.getByRole("button", { name: "Run and monitor" }));
const expected = {
name: "Research follow-up",
service: "",
agent_name: "",
filters: [{ key: "swarm", value: "research" }],
- enabled: false,
+ enabled: true,
sample_size: null,
model: "analysis",
monthly_budget: 100,
@@ -323,11 +323,11 @@ it.each(["new", "duplicate"] as const)("blocks a %s investigation until its mode
await user.click(screen.getByRole("button", { name: "Continue" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
expect(await screen.findByText("1 matching run")).toBeInTheDocument();
- expect(screen.getByRole("button", { name: "Run investigation" })).toBeDisabled();
+ expect(screen.getByRole("button", { name: mode === "new" ? "Run and monitor" : "Run investigation" })).toBeDisabled();
expect(save).not.toHaveBeenCalled();
});
-it("shows optional budget and repeat controls only under advanced options and saves their values", async () => {
+it("keeps a duplicated investigation's schedule off and saves the interval once switched on", async () => {
const user = userEvent.setup();
const save = vi.fn().mockResolvedValue(undefined);
renderWithProviders(
@@ -343,17 +343,39 @@ it("shows optional budget and repeat controls only under advanced options and sa
await user.click(screen.getByRole("button", { name: "Continue" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() => expect(screen.getByRole("button", { name: "Run investigation" })).toBeEnabled());
+ expect(screen.getByRole("checkbox", { name: "Keep watching for new traces" })).not.toBeChecked();
await user.click(screen.getByText("Advanced options"));
- expect(screen.getByRole("checkbox", { name: "Repeat this investigation" })).not.toBeChecked();
fireEvent.change(screen.getByRole("spinbutton", { name: "Monthly limit (USD)" }), { target: { value: "8" } });
- await user.click(screen.getByRole("checkbox", { name: "Repeat this investigation" }));
- fireEvent.change(screen.getByRole("spinbutton", { name: "Repeat every" }), { target: { value: "120" } });
+ await user.click(screen.getByRole("checkbox", { name: "Keep watching for new traces" }));
+ fireEvent.change(screen.getByRole("spinbutton", { name: "Check every" }), { target: { value: "120" } });
await user.click(screen.getByRole("button", { name: "Run and monitor" }));
expect(save).toHaveBeenCalledWith(
expect.objectContaining({ enabled: true, interval_minutes: 120, monthly_budget: 8 }),
);
});
+it("watches new investigations every 15 minutes by default, outside advanced options", async () => {
+ const user = userEvent.setup();
+ const save = vi.fn().mockResolvedValue(undefined);
+ renderWithProviders(
+ ,
+ );
+ await user.click(screen.getByRole("button", { name: "Continue" }));
+ await user.click(screen.getByRole("button", { name: "Continue" }));
+ const watching = await screen.findByRole("checkbox", { name: "Keep watching for new traces" });
+ expect(watching).toBeChecked();
+ expect(watching).toBeVisible();
+ await waitFor(() => expect(screen.getByRole("button", { name: "Run and monitor" })).toBeEnabled());
+ await user.click(screen.getByRole("button", { name: "Run and monitor" }));
+ expect(save).toHaveBeenCalledWith(expect.objectContaining({ enabled: true, interval_minutes: 15 }));
+});
+
it("does not silently analyze everything after individual selection is enabled", async () => {
const user = userEvent.setup();
vi.mocked(apiClient.post).mockResolvedValue({
@@ -510,8 +532,8 @@ describe("Watch for", () => {
expect(tile("unhappy")).toHaveAttribute("aria-pressed", "false");
expect(tile("looping")).toHaveAttribute("aria-pressed", "true");
await user.click(screen.getByRole("button", { name: "Continue" }));
- await waitFor(() => expect(screen.getByRole("button", { name: "Run investigation" })).toBeEnabled());
- await user.click(screen.getByRole("button", { name: "Run investigation" }));
+ await waitFor(() => expect(screen.getByRole("button", { name: "Run and monitor" })).toBeEnabled());
+ await user.click(screen.getByRole("button", { name: "Run and monitor" }));
const saved = (save.mock.calls[0][0] as Settings).checks.map((check) => check.id);
expect(saved).toEqual(["watch_unsolved", "watch_blocked", "watch_looping"]);
});
@@ -558,7 +580,7 @@ describe("Watch for", () => {
);
await user.click(screen.getByRole("button", { name: "Continue" }));
await user.click(screen.getByRole("button", { name: "Continue" }));
- expect(await screen.findByRole("button", { name: "Run investigation" })).toBeInTheDocument();
+ expect(await screen.findByRole("button", { name: "Run and monitor" })).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Back" }));
for (const name of ["unsolved", "blocked", "unhappy"]) await user.click(tile(name));
await user.click(screen.getByRole("button", { name: "Continue" }));
diff --git a/ui/litellm-dashboard/src/components/lens/setup/investigationSchema.ts b/ui/litellm-dashboard/src/components/lens/setup/investigationSchema.ts
index 2d3efc9abd2..78db823bab8 100644
--- a/ui/litellm-dashboard/src/components/lens/setup/investigationSchema.ts
+++ b/ui/litellm-dashboard/src/components/lens/setup/investigationSchema.ts
@@ -193,8 +193,8 @@ export function investigationDefaults(
questions: (initial?.checks ?? []).filter((check) => !isWatch(check)),
selectedModel: initial?.model ?? null,
budget: initial?.monthly_budget ?? 100,
- repeat: mode === "edit" && !!initial?.enabled,
- interval: initial?.interval_minutes ?? 30,
+ repeat: mode === "new" || (mode === "edit" && !!initial?.enabled),
+ interval: initial?.interval_minutes ?? 15,
manualSelection: !!initial?.execution_ids?.length,
};
}
diff --git a/ui/litellm-dashboard/src/components/lens/setup/steps/RunStep.tsx b/ui/litellm-dashboard/src/components/lens/setup/steps/RunStep.tsx
index 35bdd4b3822..7f177b8262d 100644
--- a/ui/litellm-dashboard/src/components/lens/setup/steps/RunStep.tsx
+++ b/ui/litellm-dashboard/src/components/lens/setup/steps/RunStep.tsx
@@ -34,6 +34,26 @@ export function RunStep({
const repeat = useWatch({ control, name: "repeat" });
return (
<>
+
+
+
+ Keep watching for new traces
+
+ {repeat && (
+
(
+
+ )}
+ />
+ )}
+ {errors.interval?.message && (
+
+ {errors.interval.message}
+
+ )}
+
Advanced options
@@ -87,30 +107,6 @@ export function RunStep({
)}
-
-
-
- Repeat this investigation
-
- {repeat && (
-
(
-
- )}
- />
- )}
- {errors.interval?.message && (
-
- {errors.interval.message}
-
- )}
-
diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts
index 3ed4103a227..54e43f4aac3 100644
--- a/ui/litellm-dashboard/src/lib/http/schema.d.ts
+++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts
@@ -8968,6 +8968,23 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/lens/watch-all": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ /** Watch All */
+ post: operations["watch_all_lens_watch_all_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
"/lens/worker/claim": {
parameters: {
query?: never;
@@ -33344,6 +33361,17 @@ export interface components {
* @enum {string}
*/
status: "queued" | "running" | "completed" | "failed" | "cancelled";
+ /**
+ * Steps
+ * @default []
+ */
+ steps: components["schemas"]["Step"][];
+ /**
+ * Trigger
+ * @default schedule
+ * @enum {string}
+ */
+ trigger: "schedule" | "manual";
/** Worker Id */
worker_id?: string | null;
};
@@ -44240,9 +44268,15 @@ export interface components {
};
/** RunRequest */
RunRequest: {
+ /** Agent Name */
+ agent_name?: string | null;
+ /** End */
+ end?: string | null;
/** Lookback Hours */
lookback_hours?: number | null;
settings?: components["schemas"]["LensSettings"] | null;
+ /** Start */
+ start?: string | null;
};
/** SCIMEnterpriseUser */
SCIMEnterpriseUser: {
@@ -45573,6 +45607,46 @@ export interface components {
*/
user_ids: string[];
};
+ /** Step */
+ Step: {
+ /**
+ * At
+ * Format: date-time
+ */
+ at: string;
+ /**
+ * Completion Tokens
+ * @default 0
+ */
+ completion_tokens: number;
+ /**
+ * Cost
+ * @default 0
+ */
+ cost: number;
+ /**
+ * Kind
+ * @enum {string}
+ */
+ kind: "stage" | "model" | "error";
+ /** Label */
+ label: string;
+ /**
+ * Model
+ * @default
+ */
+ model: string;
+ /**
+ * Prompt Tokens
+ * @default 0
+ */
+ prompt_tokens: number;
+ /**
+ * Purpose
+ * @default
+ */
+ purpose: string;
+ };
/**
* SuccessfulKeyUpdate
* @description Successfully updated key with its updated information
@@ -49334,6 +49408,25 @@ export interface components {
} & {
[key: string]: unknown;
};
+ /** WatchAllResult */
+ WatchAllResult: {
+ /**
+ * Skipped
+ * @default []
+ */
+ skipped: components["schemas"]["WatchSkipped"][];
+ /** Watching */
+ watching: string[];
+ };
+ /** WatchSkipped */
+ WatchSkipped: {
+ /** Id */
+ id: string;
+ /** Name */
+ name: string;
+ /** Reason */
+ reason: string;
+ };
/**
* WebSearchInterceptionSettings
* @description Configuration for server-side web search interception
@@ -62274,6 +62367,26 @@ export interface operations {
};
};
};
+ watch_all_lens_watch_all_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["WatchAllResult"];
+ };
+ };
+ };
+ };
claim_lens_worker_claim_post: {
parameters: {
query?: {