mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-10-02 19:57:59 +00:00
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
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:
parent
5f961fe0e3
commit
6c57e35e35
5 changed files with 92 additions and 142 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue