diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx
new file mode 100644
index 00000000000..ef158626fc8
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/TraceDrawer.tsx
@@ -0,0 +1,349 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { Copy } from "lucide-react";
+import { useCallback, useMemo, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { ButtonGroup } from "@/components/ui/button-group";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
+import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
+import { copyToClipboard } from "@/utils/dataUtils";
+
+import { agentTraceCall } from "../../networking";
+import { AgentGraph } from "./AgentGraph";
+import { SpanDetail } from "./SpanDetail";
+import { SpanTree } from "./SpanTree";
+import { AgentTracePill, SpanStatusBadge } from "./TracePills";
+import { TraceSteps } from "./TraceSteps";
+import type { Trace } from "./traceTypes";
+import {
+ buildVisibleTree,
+ firstErrorSpan,
+ flattenTree,
+ fmtCost,
+ fmtMs,
+ fmtTok,
+ revealSpan,
+ sortStepsByCost,
+ spanRowIds,
+ stepsFromSpans,
+ subtreeStats,
+ totalCacheRead,
+ traceHasErrors,
+ type TreeUiState,
+} from "./traceUtils";
+import { stepSelection, useTraceNavigation } from "./useTraceNavigation";
+
+export type TraceViewMode = "steps" | "tree" | "graph";
+
+interface TraceDrawerProps {
+ open: boolean;
+ traceId: string | null;
+ /** Span to select on open (e.g. a child LLM row was clicked in the table). */
+ initialSpanId?: string | null;
+ accessToken: string;
+ onClose: () => void;
+ onOpenRequestLog?: (requestId: string) => void;
+}
+
+const DRAWER_WIDTH = "min(1180px, 80vw)";
+const MODES: { id: TraceViewMode; label: string }[] = [
+ { id: "steps", label: "Steps" },
+ { id: "tree", label: "Tree" },
+ { id: "graph", label: "Graph" },
+];
+const EMPTY_UI: TreeUiState = { collapsed: new Set(), groupShown: {} };
+
+interface InitialView {
+ mode: TraceViewMode;
+ selectedId: string | null;
+}
+
+/** Errors open on the first failed span in the Tree; otherwise the Steps narrative. */
+export function initialTraceView(trace: Trace, initialSpanId?: string | null): InitialView {
+ if (initialSpanId && trace.spans.some((s) => s.span_id === initialSpanId)) {
+ return { mode: "steps", selectedId: initialSpanId };
+ }
+ if (traceHasErrors(trace)) {
+ const failed = firstErrorSpan(trace.spans);
+ if (failed) return { mode: "tree", selectedId: failed.span_id };
+ }
+ const firstStep = stepsFromSpans(trace.spans)[0]?.span ?? trace.spans[0];
+ return { mode: "steps", selectedId: firstStep?.span_id ?? null };
+}
+
+/** Tree state with the initially selected span (e.g. the first failure) scrolled into reach. */
+function initialTreeUi(trace: Trace, selectedId: string | null): TreeUiState {
+ if (!selectedId) return EMPTY_UI;
+ const { children } = buildVisibleTree(trace.spans, false);
+ return revealSpan(children, subtreeStats(trace.spans), EMPTY_UI, selectedId);
+}
+
+function Stat({ label, value, sub }: { label: string; value: string; sub?: string }) {
+ return (
+
+
{label}
+
+ {value} {sub && {sub}}
+
+
+ );
+}
+
+export function TraceStats({ trace }: { trace: Trace }) {
+ const { summary } = trace;
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function AgentCostSplit({ trace }: { trace: Trace }) {
+ if (trace.agents.length < 2) return null;
+ const sorted = [...trace.agents].sort((a, b) => b.spend - a.spend);
+ return (
+
+ Cost by agent:
+ {sorted.map((agent) => (
+
+ {agent.name} {fmtCost(agent.spend)}
+
+ ))}
+
+ );
+}
+
+interface TraceBodyProps {
+ trace: Trace;
+ accessToken: string;
+ initialSpanId?: string | null;
+ onOpenRequestLog?: (requestId: string) => void;
+}
+
+function TraceBody({ trace, accessToken, initialSpanId, onOpenRequestLog }: TraceBodyProps) {
+ const [initial] = useState(() => initialTraceView(trace, initialSpanId));
+ const [mode, setMode] = useState(initial.mode);
+ const [selectedId, setSelectedId] = useState(initial.selectedId);
+ const [showFramework, setShowFramework] = useState(false);
+ const [sortByCost, setSortByCost] = useState(false);
+ const [ui, setUi] = useState(() => initialTreeUi(trace, initial.selectedId));
+
+ const { children, visibleCount } = useMemo(
+ () => buildVisibleTree(trace.spans, showFramework),
+ [trace.spans, showFramework],
+ );
+ const stats = useMemo(() => subtreeStats(trace.spans), [trace.spans]);
+ const rows = useMemo(() => flattenTree(children, stats, ui), [children, stats, ui]);
+ const timeSteps = useMemo(() => stepsFromSpans(trace.spans), [trace.spans]);
+ const steps = useMemo(() => (sortByCost ? sortStepsByCost(timeSteps) : timeSteps), [timeSteps, sortByCost]);
+ const selectedSpan = useMemo(
+ () => trace.spans.find((s) => s.span_id === selectedId) ?? null,
+ [trace.spans, selectedId],
+ );
+
+ const order = useMemo(
+ () => (mode === "steps" ? steps.map((s) => s.span.span_id) : spanRowIds(rows)),
+ [mode, steps, rows],
+ );
+ const handleStep = useCallback(
+ (delta: number) => setSelectedId((current) => stepSelection(order, current, delta)),
+ [order],
+ );
+ useTraceNavigation(mode !== "graph", handleStep);
+
+ const toggleCollapse = useCallback((spanId: string) => {
+ setUi((prev) => {
+ const collapsed = new Set(prev.collapsed);
+ if (collapsed.has(spanId)) collapsed.delete(spanId);
+ else collapsed.add(spanId);
+ return { ...prev, collapsed };
+ });
+ }, []);
+ const showGroup = useCallback((key: string, shown: number) => {
+ setUi((prev) => ({ ...prev, groupShown: { ...prev.groupShown, [key]: shown } }));
+ }, []);
+ const openInvocation = useCallback(
+ (spanId: string) => {
+ setUi((prev) => revealSpan(children, stats, prev, spanId));
+ setSelectedId(spanId);
+ setMode("tree");
+ },
+ [children, stats],
+ );
+ const changeMode = useCallback(
+ (next: TraceViewMode) => {
+ if (next === "tree" && selectedId) setUi((prev) => revealSpan(children, stats, prev, selectedId));
+ setMode(next);
+ },
+ [children, stats, selectedId],
+ );
+
+ return (
+
+
+
+
+ {MODES.map((m) => (
+
+ ))}
+
+ {mode === "tree" && (
+
+ )}
+ {mode === "steps" && (
+
+ )}
+
+ {mode === "tree" ? `${visibleCount} of ${trace.spans.length} spans` : `${steps.length} steps`}
+
+
+ {mode === "steps" &&
}
+ {mode === "tree" && (
+
+ )}
+ {mode === "graph" &&
}
+
+ {selectedSpan ? (
+
+ ) : (
+
Select a span to see its details.
+ )}
+
+ );
+}
+
+function TraceHeader({ trace, onClose }: { trace: Trace; onClose: () => void }) {
+ const { summary } = trace;
+ return (
+
+
+
+
{summary.name}
+
+
+ J /{" "}
+ K to move
+
+
+
+
+
+
+ service {summary.service}
+
+ ·
+ trace {summary.trace_id}
+
+ ·
+ {new Date(summary.start_time).toLocaleString()}
+
+
+
+ );
+}
+
+/** Right-side drawer for one agent trace: header, stats strip, Steps / Tree / Graph views and span details. */
+export function TraceDrawer({
+ open,
+ traceId,
+ initialSpanId,
+ accessToken,
+ onClose,
+ onOpenRequestLog,
+}: TraceDrawerProps) {
+ const traceQuery = useQuery({
+ queryKey: ["agentTrace", traceId, accessToken],
+ queryFn: () => agentTraceCall(accessToken, traceId as string),
+ enabled: open && traceId !== null,
+ staleTime: 30_000,
+ });
+ const trace = traceQuery.data;
+
+ return (
+ !nextOpen && onClose()}>
+
+ {traceQuery.isLoading && (
+
+ Loading trace
+
+
+ )}
+ {traceQuery.isError && (
+
+ Could not load trace
+ {traceQuery.error.message}
+
+ )}
+ {trace && (
+
+
+
+
+
+ )}
+
+
+ );
+}