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 <noreply@anthropic.com>

* feat(lens): add copy link button to trace header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(lens): cover copy link on trace header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ishaan-berri 2026-10-05 20:07:56 -07:00 • committed by GitHub
parent 402fa63366
commit 2fd5725c04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 7 deletions

View file

@ -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 ? <Check className="size-3" /> : <Copy className="size-3" />}
{copied ? "Copied" : "Copy for agent"}
{copied ? <Check className="size-3" /> : <Icon className="size-3" />}
{copied ? "Copied" : label}
</Button>
);
}
@ -103,7 +111,15 @@ export function RunHeader({ trace, handoff, onBack, embedded }: RunHeaderProps)
Conversation
</TabsTrigger>
</TabsList>
<CopyForAgent handoff={handoff} />
<div className="flex items-center gap-1.5">
<CopyButton
label="Copy link"
icon={Link}
text={() => traceShareUrl(traceRefOf(summary), window.location)}
toast="Trace link copied"
/>
<CopyButton label="Copy for agent" icon={Copy} text={() => handoff.text} toast={handoff.copied} />
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">

View file

@ -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", () => {

View file

@ -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<Location, "origin" | "pathname" | "search">): 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;