From 2fd5725c04cf766849527adcedac1910207bbfe4 Mon Sep 17 00:00:00 2001 From: ishaan-berri <155045088+ishaan-berri@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:07:56 -0700 Subject: [PATCH] feat(lens): add copy link button to trace header (#44749) * feat(lens): add traceShareUrl helper for shareable trace links Co-Authored-By: Claude Opus 5.5 * feat(lens): add copy link button to trace header Co-Authored-By: Claude Opus 5.5 * test(lens): cover copy link on trace header Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .../lens/traces/detail/run/RunHeader.tsx | 28 +++++++++++++++---- .../lens/traces/detail/run/RunView.test.tsx | 19 ++++++++++++- .../src/components/lens/traces/routing.ts | 11 ++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) 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;