diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunHeader.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunHeader.tsx
index 566ef0ebc4a..16afa455be6 100644
--- a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunHeader.tsx
+++ b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunHeader.tsx
@@ -1,6 +1,6 @@
"use client";
-import { ArrowLeft, Check, Copy } from "lucide-react";
+import { ArrowLeft, Check, Copy, Link } from "lucide-react";
import { useState } from "react";
import { useTimeout } from "usehooks-ts";
@@ -10,6 +10,7 @@ import { cn } from "@/lib/cva.config";
import { copyToClipboard } from "@/utils/dataUtils";
import type { TraceHandoff } from "../../api";
+import { traceRefOf, traceShareUrl } from "../../routing";
import { formatCost } from "../../list/AgentTracesTable";
import { IdChip } from "../../ui/IdChip";
import { SpanIcon } from "../../ui/SpanIcon";
@@ -17,7 +18,14 @@ import { FrameworkLogo, traceFramework } from "../../ui/TraceFramework";
import type { Trace } from "../../types";
import { fmtMs, fmtTok, traceAgentNames, traceDisplayName } from "../../utils";
-function CopyForAgent({ handoff }: { handoff: TraceHandoff }) {
+interface CopyButtonProps {
+ label: string;
+ icon: typeof Copy;
+ text: () => string;
+ toast: string;
+}
+
+function CopyButton({ label, icon: Icon, text, toast }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
useTimeout(() => setCopied(false), copied ? 1600 : null);
return (
@@ -25,10 +33,10 @@ function CopyForAgent({ handoff }: { handoff: TraceHandoff }) {
variant="outline"
size="xs"
className="h-7 shrink-0 gap-1.5 text-xs shadow-none"
- onClick={async () => setCopied(await copyToClipboard(handoff.text, handoff.copied))}
+ onClick={async () => setCopied(await copyToClipboard(text(), toast))}
>
- {copied ? : }
- {copied ? "Copied" : "Copy for agent"}
+ {copied ? : }
+ {copied ? "Copied" : label}
);
}
@@ -103,7 +111,15 @@ export function RunHeader({ trace, handoff, onBack, embedded }: RunHeaderProps)
Conversation
-
+
+ traceShareUrl(traceRefOf(summary), window.location)}
+ toast="Trace link copied"
+ />
+ handoff.text} toast={handoff.copied} />
+
diff --git a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx
index 9136d5e1cf9..8f23031036d 100644
--- a/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx
+++ b/ui/litellm-dashboard/src/components/lens/traces/detail/run/RunView.test.tsx
@@ -13,7 +13,7 @@ import { ShortcutHints } from "@/components/shared/ShortcutHints";
import { RunView } from "./RunView";
import { initialRunSelection } from "./useRunTree";
import { tickLabel, timeTicks } from "../tree/timeline";
-import { useOpenTraceRouting } from "../../routing";
+import { traceShareUrl, useOpenTraceRouting } from "../../routing";
import { agentHandoffText } from "../../api";
import type { Span } from "../../types";
import type { Trace } from "../../types";
@@ -476,6 +476,23 @@ describe("RunView", () => {
expect(agentHandoffText("t1")).toContain('"http://proxy.test/v1/traces/t1?format=md"');
expect(agentHandoffText("t1", "s1")).toContain("&span_id=s1");
});
+
+ it("copies a link that reopens just this run", async () => {
+ const user = userEvent.setup();
+ renderRun(research);
+
+ await user.click(await screen.findByRole("button", { name: /copy link/i }));
+ const url = new URL(vi.mocked(copyToClipboard).mock.calls[0][0] as string);
+ expect(url.pathname).toBe(window.location.pathname);
+ expect(url.searchParams.get("trace")).toBe(research.summary.trace_id);
+ expect(copyToClipboard).toHaveBeenCalledWith(expect.any(String), "Trace link copied");
+ const loc = { origin: "https://gw.test", pathname: "/ui/lens", search: "?q=refund&span=s1" };
+ expect(traceShareUrl({ traceId: "t1", traceRef: "R1" }, loc)).toBe("https://gw.test/ui/lens?trace=t1&trace_ref=R1");
+ expect(traceShareUrl({ traceId: "t1" }, loc)).toBe("https://gw.test/ui/lens?trace=t1");
+ expect(traceShareUrl({ traceId: "t1" }, { ...loc, search: "?demo=true" })).toBe(
+ "https://gw.test/ui/lens?demo=true&trace=t1",
+ );
+ });
});
describe("initialRunSelection", () => {
diff --git a/ui/litellm-dashboard/src/components/lens/traces/routing.ts b/ui/litellm-dashboard/src/components/lens/traces/routing.ts
index 3fd88699af6..e63f9c59343 100644
--- a/ui/litellm-dashboard/src/components/lens/traces/routing.ts
+++ b/ui/litellm-dashboard/src/components/lens/traces/routing.ts
@@ -32,6 +32,17 @@ export interface TraceRef {
export const traceRefOf = (run: TraceSummary): TraceRef => ({ traceId: run.trace_id, traceRef: run.trace_ref });
export const traceKey = (ref: TraceRef): string => ref.traceRef || ref.traceId;
+/** A shareable link that opens just this run on the current page, dropping list filters and step selection. */
+export const traceShareUrl = (ref: TraceRef, location: Pick): string => {
+ const demo = new URLSearchParams(location.search).get("demo") === "true";
+ const params = new URLSearchParams({
+ ...(demo ? { demo: "true" } : {}),
+ trace: ref.traceId,
+ ...(ref.traceRef ? { trace_ref: ref.traceRef } : {}),
+ });
+ return `${location.origin}${location.pathname}?${params}`;
+};
+
/** Which step, view and detail section of an open run are showing. Owned by the URL in the drawer, locally in sheets. */
export interface RunSelection {
spanId: string | null;