SKY-17496: Keep run ID copy in the Studio top bar only (#8749)
Some checks are pending
Run tests and pre-commit / Run tests and pre-commit hooks (push) Waiting to run
Run tests and pre-commit / Frontend Lint and Build (push) Waiting to run
Run tests and pre-commit / pip Package Smoke Tests (3.11) (push) Waiting to run
Run tests and pre-commit / pip Package Smoke Tests (3.13) (push) Waiting to run
Publish Fern Docs / run (push) Waiting to run

This commit is contained in:
Aaron Perez 2026-09-30 19:27:40 -05:00 • committed by GitHub
parent 5f961fe0e3
commit 6c57e35e35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 142 deletions

View file

@ -170,8 +170,8 @@ export function StudioPaneToggles() {
>
{STUDIO_PANE_IDS.map((id) => {
const { icon: Icon } = STUDIO_PANE_META[id];
// The run pane's tab names the inspected run ("View Run: wr_…") so the
// run id reads from the top bar; railLabel falls back to "Past Runs".
// The top-bar run control names the inspected run; it falls back to
// "Past Runs" when there is no URL-addressed run.
const label = railLabel(id, runId);
const open = panes.includes(id);
const blockedByDeletion = paneBlockedByDeletion(id);

View file

@ -1,11 +1,12 @@
// @vitest-environment jsdom
import { fireEvent, render, screen } from "@testing-library/react";
import { fireEvent, render, screen, within } from "@testing-library/react";
import { TooltipProvider } from "@/components/ui/tooltip";
import { copyText } from "@/util/copyText";
import { type ReactNode } from "react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { type StudioWorkspaceProps } from "./EditorTab";
import { type StudioPaneId } from "./panes";
import { paneAccessibleName } from "./paneMeta";
import { paneExpansionKeyframes } from "./paneLayout";
@ -13,7 +14,43 @@ import {
advanceRecordingStopLifecycle,
panesAfterRecordingTransition,
} from "./recordingPaneLifecycle";
import { EmbeddedBrowserOverlays, StudioPane } from "./StudioShell";
import {
EmbeddedBrowserOverlays,
StudioPane,
StudioShell,
} from "./StudioShell";
vi.mock("posthog-js/react", () => ({
usePostHog: () => ({ capture: vi.fn(), captureException: vi.fn() }),
}));
vi.mock("@/hooks/useLogging", () => ({
useLogging: () => ({ error: vi.fn() }),
}));
vi.mock("./BrowserPaneHeader", () => ({
BrowserPaneActions: () => null,
BrowserPaneViewPills: () => null,
}));
vi.mock("./CopilotPaneHeader", () => ({
CopilotActiveDot: () => null,
CopilotPaneControls: () => null,
CopilotPaneStatus: () => null,
}));
vi.mock("./EditorPaneHeader", () => ({
EditorPaneBlockSearch: () => null,
EditorPaneModeToggle: () => null,
}));
vi.mock("./BrowserTab", () => ({ BrowserTab: () => null }));
vi.mock("./EditorTab", () => ({ EditorTab: () => null }));
vi.mock("./RunTab", () => ({ RunTab: () => null }));
vi.mock("./runview/RunPaneHeader", () => ({
RunPaneActions: () => null,
RunPaneViewToggles: () => null,
}));
vi.mock("./StudioBrowserStream", () => ({ StudioBrowserStream: () => null }));
vi.mock("./StudioCoachMark", () => ({ StudioCoachMark: () => null }));
vi.mock("./StudioStageLauncher", () => ({ StudioStageLauncher: () => null }));
vi.mock("./StudioTopBar", () => ({ StudioTopBar: () => null }));
vi.mock("./StudioWorkflowPanels", () => ({ StudioWorkflowPanels: () => null }));
vi.mock("./useRunVisuals", () => ({
useRunVisuals: () => ({
@ -40,6 +77,36 @@ describe("EmbeddedBrowserOverlays", () => {
});
});
describe("StudioShell inspected run header", () => {
test("keeps the run pane header plain when a run is inspected", () => {
const runId = "wr_synthetic_inspected_run";
const props = {
workflow: {
workflow_permanent_id: "wpid_synthetic",
organization_id: "org_synthetic",
deleted_at: null,
workflow_definition: { blocks: [] },
},
initialTitle: "Synthetic workflow",
} as unknown as StudioWorkspaceProps;
render(
<MemoryRouter
initialEntries={[`/workflows/wpid_synthetic/studio?wr=${runId}`]}
>
<StudioShell {...props} />
</MemoryRouter>,
);
const header = screen.getByRole("group", { name: "Run pane header" });
expect(within(header).getByText("Run", { exact: true })).toBeTruthy();
expect(header.textContent).toBe("Run");
expect(
within(header).queryByRole("button", { name: "Copy to clipboard" }),
).toBeNull();
});
});
describe("panesAfterRecordingTransition", () => {
test("replaces the Editor with Browser and Copilot when recording starts", () => {
expect(
@ -137,10 +204,6 @@ describe("paneExpansionKeyframes", () => {
});
});
vi.mock("@/util/copyText", () => ({ copyText: vi.fn() }));
const mockedCopyText = vi.mocked(copyText);
// Chromium aborts a native drag when the DOM mutates inside the dragstart
// task, so the reorder state (drop overlays, source dim) must engage on a
// later task. These tests pin that timing contract; only a real mouse drag
@ -150,7 +213,6 @@ describe("StudioPane header", () => {
const renderPane = ({
id = "copilot",
runId,
headerActions,
expanded = false,
expansionTransitioning = false,
@ -159,7 +221,6 @@ describe("StudioPane header", () => {
onTransitionEnd,
}: {
id?: StudioPaneId;
runId?: string;
headerActions?: ReactNode;
expanded?: boolean;
expansionTransitioning?: boolean;
@ -184,7 +245,6 @@ describe("StudioPane header", () => {
<TooltipProvider delayDuration={0}>
<StudioPane
id={id}
runId={runId}
open
order={0}
flex={undefined}
@ -388,36 +448,13 @@ describe("StudioPane header", () => {
expect(reorder.onStart).not.toHaveBeenCalled();
});
test("a drag starting on the run id copy control is prevented", () => {
const { reorder, header } = renderPane({
id: "overview",
runId: "wr_5574abcdef",
});
test("the run pane header stays plain and has no copy action", () => {
renderPane({ id: "overview" });
fireEvent.pointerDown(
screen.getByRole("button", { name: "Copy to clipboard" }),
);
const notPrevented = fireEvent.dragStart(header, {
dataTransfer: dataTransfer(),
});
expect(notPrevented).toBe(false);
vi.runAllTimers();
expect(reorder.onStart).not.toHaveBeenCalled();
});
test("shows the full run id on hover and copies it from the header control", () => {
const runId = "wr_5574abcdef";
renderPane({ id: "overview", runId });
expect(screen.getByText("Run: wr_5574…")).toBeTruthy();
const fullRunId = screen.getByText(`Run: ${runId}`);
expect(fullRunId.getAttribute("title")).toBe(`Run: ${runId}`);
expect(fullRunId.className).toContain("truncate");
fireEvent.click(screen.getByRole("button", { name: "Copy to clipboard" }));
expect(mockedCopyText).toHaveBeenCalledWith(runId);
expect(screen.getByText("Run")).toBeTruthy();
expect(
screen.queryByRole("button", { name: "Copy to clipboard" }),
).toBeNull();
});
test("groups pane utilities separately from close", () => {

View file

@ -16,7 +16,6 @@ import {
ExitFullScreenIcon,
} from "@radix-ui/react-icons";
import { CopyButton } from "@/components/CopyButton";
import { PaneErrorBoundary } from "@/components/PaneErrorBoundary";
import { useLogging } from "@/hooks/useLogging";
import { StreamStatusPanel } from "@/routes/streaming/StreamDiagnostics";
@ -110,50 +109,8 @@ type PaneReorder = {
onMove: (direction: -1 | 1) => void;
};
function RunPaneLabel({
label,
runId,
dragHint,
}: {
label: string;
runId: string;
dragHint: string;
}) {
return (
<span className="group/runlabel inline-flex min-w-0 items-center text-xs font-medium text-foreground">
{/* Must not wrap: the header is a fixed h-11 row, so a second line of
"Run: wr_…" overflows it and squeezes the control cluster. */}
<span
className="inline-block min-w-0 truncate group-focus-within/runlabel:hidden group-hover/runlabel:hidden"
title={dragHint}
>
{label}
</span>
<span
className="hidden min-w-0 truncate group-focus-within/runlabel:inline-block group-hover/runlabel:inline-block"
title={`Run: ${runId}`}
>
Run: {runId}
</span>
<span
className={cn(
"inline-flex w-0 overflow-hidden opacity-0 transition-all",
"group-hover/runlabel:ml-1 group-hover/runlabel:w-5 group-hover/runlabel:opacity-100",
"group-focus-within/runlabel:ml-1 group-focus-within/runlabel:w-5 group-focus-within/runlabel:opacity-100",
)}
>
<CopyButton
value={runId}
className="h-5 w-5 shrink-0 p-0.5 text-muted-foreground hover:text-foreground"
/>
</span>
</span>
);
}
export function StudioPane({
id,
runId,
open,
order,
flex,
@ -171,8 +128,6 @@ export function StudioPane({
children,
}: {
id: StudioPaneId;
// The inspected run id, so the run pane's label can read "Run: wr_…".
runId?: string | null;
open: boolean;
order: number | undefined;
flex: string | undefined;
@ -193,10 +148,7 @@ export function StudioPane({
children: ReactNode;
}) {
const { icon: Icon } = STUDIO_PANE_META[id];
const label = paneLabel(id, runId);
// The run id shows in the visible header label only; the region, header,
// drag hint, and close control take the stable accessible name so a run
// switch never renames them for screen readers.
const label = paneLabel(id);
const accessibleLabel = paneAccessibleName(id);
const paneRef = useRef<HTMLElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
@ -364,20 +316,12 @@ export function StudioPane({
<Icon className="size-3.5 text-muted-foreground" aria-hidden />
{iconBadge}
</span>
{id === "overview" && runId ? (
<RunPaneLabel
label={label}
runId={runId}
dragHint={`Drag to reorder the ${accessibleLabel} pane (or Ctrl/Cmd+Shift+←/→)`}
/>
) : (
<span
className="min-w-0 truncate text-xs font-medium text-foreground"
title={`Drag to reorder the ${accessibleLabel} pane (or Ctrl/Cmd+Shift+←/→)`}
>
{label}
</span>
)}
<span
className="min-w-0 truncate text-xs font-medium text-foreground"
title={`Drag to reorder the ${accessibleLabel} pane (or Ctrl/Cmd+Shift+←/→)`}
>
{label}
</span>
<StudioPaneCompactContext.Provider value={compact}>
{headerExtras}
<span className="min-w-0 flex-1" />
@ -1018,7 +962,6 @@ function StudioStage(props: StudioWorkspaceProps) {
const index = panes.indexOf(id);
return {
id,
runId,
chromeless: embedded,
open: index >= 0 && visiblePanes.includes(id),
// Panes take even slots and the dividers between them take odd slots.

View file

@ -14,18 +14,8 @@ describe("paneLabel", () => {
expect(paneLabel("browser")).toBe("Browser");
});
test("the run pane reads 'Run' when no run is inspected", () => {
test("the run pane label is always 'Run'", () => {
expect(paneLabel("overview")).toBe("Run");
expect(paneLabel("overview", null)).toBe("Run");
expect(paneLabel("overview", undefined)).toBe("Run");
});
test("the run pane head-truncates the inspected run id", () => {
expect(paneLabel("overview", "wr_5538abcdef")).toBe("Run: wr_5538…");
});
test("a run id short enough to fit is shown without an ellipsis", () => {
expect(paneLabel("overview", "wr_12")).toBe("Run: wr_12");
});
});
@ -36,10 +26,9 @@ describe("paneAccessibleName", () => {
});
test("the run pane's controls keep the stable name 'Run'", () => {
// The pane's own controls (region/close/drag) announce "Run", matching the
// "Run: wr_…" content; "Past Runs" is the idle run control's label.
// "Past Runs" is the idle top-bar selector label.
expect(paneAccessibleName("overview")).toBe("Run");
expect(paneLabel("overview", "wr_5538abcdef")).toBe("Run: wr_5538…");
expect(paneLabel("overview")).toBe("Run");
});
});

View file

@ -15,34 +15,15 @@ export const STUDIO_PANE_META: Record<
copilot: { label: "Copilot", icon: ChatBubbleIcon },
editor: { label: "Editor", icon: Share1Icon },
browser: { label: "Browser", icon: GlobeIcon },
// "Overview" is retired from display (paneLabel returns "Run" / "Run: wr_…");
// it stays here as the registry fallback name for the pane.
// The internal "overview" pane is displayed as "Run" in Studio.
overview: { label: "Overview", icon: ReaderIcon },
};
// Head-truncate a run id for the run pane's tab label, e.g. "wr_5538…".
function truncateRunId(runId: string): string {
return runId.length > 7 ? `${runId.slice(0, 7)}…` : runId;
export function paneLabel(id: StudioPaneId): string {
return id === "overview" ? "Run" : STUDIO_PANE_META[id].label;
}
// The run pane ("overview") shows the inspected run instead of a static name:
// "Run: wr_5538…" while a run is inspected, "Run" when none. Every other pane
// keeps its registry label. Callers pass the URL-addressed run id
// (useStudioRunId) — deliberately NOT the latest-run fallback — so the label
// identifies the run named in the shareable URL, not just whatever is on screen
// (RunsTab's highlight, by contrast, follows the fallback via useStudioInspectedRun).
export function paneLabel(id: StudioPaneId, runId?: string | null): string {
if (id === "overview") {
return runId ? `Run: ${truncateRunId(runId)}` : "Run";
}
return STUDIO_PANE_META[id].label;
}
// The stable accessible name for a pane's OWN controls (region, header, drag,
// close, resize) and announcements. The run pane keeps "Run" so a screen reader
// hears "Close Run pane", matching its "Run: wr_…" content. The run id never
// enters this name: a truncated id is ambiguous and would rename the control
// on every run switch.
// Stable names keep the run pane's region and controls as "Run" across run switches.
export function paneAccessibleName(id: StudioPaneId): string {
return paneLabel(id);
}