Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"prepare": "husky || true"
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.8",
"@fontsource-variable/inter": "^5.2.8",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.2",
Expand Down
6 changes: 4 additions & 2 deletions src/components/gateways/VirtualServerDetailsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,12 @@ describe("VirtualServerDetailsPanel components list", () => {

await screen.findByText("Titled Tool");

await user.click(screen.getByRole("button", { name: "Copy Titled Tool" }));
await user.click(screen.getByRole("button", { name: "Copy tool name for Titled Tool" }));
expect(copyToClipboard).toHaveBeenCalledWith("titled-tool-id");

await user.click(screen.getByRole("button", { name: "Copy Plain Tool" }));
// The untitled row has no separate name to reference, so its copy label
// uses the component-type noun rather than the raw identifier value.
await user.click(screen.getByRole("button", { name: "Copy tool" }));
expect(copyToClipboard).toHaveBeenCalledWith("Plain Tool");
});

Expand Down
16 changes: 11 additions & 5 deletions src/components/gateways/VirtualServerDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,8 @@ export function VirtualServerDetailsPanel({
<CopyButton
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: title },
{ id: `gateways.details.component.copyName.${component.type}` },
{ name: title },
)}
className="size-5 text-muted-foreground"
/>
Expand All @@ -603,7 +603,7 @@ export function VirtualServerDetailsPanel({
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: identifier },
{ label: getComponentLabel(component.type) },
)}
className="size-5 text-muted-foreground"
/>
Expand Down Expand Up @@ -674,10 +674,16 @@ export function VirtualServerDetailsPanel({
{server.version ?? intl.formatMessage({ id: "gateways.details.notAvailable" })}
</DetailRow>
<DetailRow label={intl.formatMessage({ id: "gateways.details.serverId" })}>
<CopyValue label="server ID" value={server.id} />
<CopyValue
label={intl.formatMessage({ id: "gateways.details.serverIdCopyLabel" })}
value={server.id}
/>
</DetailRow>
<DetailRow label={intl.formatMessage({ id: "gateways.details.url" })}>
<CopyValue label="URL" value={endpoint} />
<CopyValue
label={intl.formatMessage({ id: "gateways.details.urlCopyLabel" })}
value={endpoint}
/>
</DetailRow>
{(() => {
const tagLabels = tags.map((tag) => tag.label);
Expand Down
8 changes: 5 additions & 3 deletions src/components/prompts/PromptCodeTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,15 @@ describe("PromptCodeTab", () => {
await waitFor(() =>
expect(screen.getByRole("button", { name: /re-run/i })).toBeInTheDocument(),
);
// Status row is announced live once the run completes.
expect(screen.getByRole("status")).toBeInTheDocument();
// Status row is announced live once the run completes. (Copy buttons on
// the page also mount a plain, unlabelled `role="status"` region, so
// disambiguate on `aria-live`, which only the preview status row sets.)
expect(document.querySelector('[role="status"][aria-live="polite"]')).toBeInTheDocument();

await user.click(screen.getByRole("tab", { name: "Python" }));

expect(screen.getByRole("button", { name: /^preview$/i })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /re-run/i })).not.toBeInTheDocument();
expect(screen.queryByRole("status")).not.toBeInTheDocument();
expect(document.querySelector('[role="status"][aria-live="polite"]')).not.toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/components/prompts/PromptSnippetTabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe("PromptSnippetTabs", () => {
expect(writeText).toHaveBeenCalledTimes(1);
expect(writeText.mock.calls[0][0]).toContain("curl -X POST");
expect(writeText.mock.calls[0][0]).toContain('"user":"Alice"');
expect(screen.getByRole("tooltip")).toHaveTextContent("Copied!");
expect(screen.getByRole("status")).toHaveTextContent("Copied!");
});

it("rebuilds the snippet when args change", () => {
Expand Down
8 changes: 7 additions & 1 deletion src/components/resources/ResourcePreviewResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,13 @@ function ResourceContentPreview({
<CodeBlock
code={content.text ?? ""}
language={codeLanguageFor(mimeType)}
copyLabel={mimeType || "content"}
copyLabel={intl.formatMessage(
{ id: "common.copyValue" },
{
label:
mimeType || intl.formatMessage({ id: "resources.details.preview.contentFallback" }),
},
)}
/>
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/resources/ResourcesTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe("ResourcesTable", () => {
const resources = [createMockResource(1, { uri: "resource://my-uri" })];
render(<ResourcesTable resources={resources} onSelectResource={mockOnSelectResource} />);

const copyButton = screen.getByLabelText("Copy resource://my-uri");
const copyButton = screen.getByLabelText("Copy URI for Resource 1 Title");
await user.click(copyButton);

expect(clipboardUtils.copyToClipboard).toHaveBeenCalledWith("resource://my-uri");
Expand All @@ -180,7 +180,7 @@ describe("ResourcesTable", () => {
const resources = [createMockResource(1)];
render(<ResourcesTable resources={resources} onSelectResource={mockOnSelectResource} />);

const copyButton = screen.getByLabelText(/^Copy resource:\/\//);
const copyButton = screen.getByLabelText(/^Copy URI for/);
await user.click(copyButton);

expect(mockOnSelectResource).not.toHaveBeenCalled();
Expand All @@ -191,7 +191,7 @@ describe("ResourcesTable", () => {
const resources = [createMockResource(1, { id: "resource-abc-123" })];
render(<ResourcesTable resources={resources} onSelectResource={mockOnSelectResource} />);

const copyButton = screen.getByLabelText("Copy resource ID");
const copyButton = screen.getByLabelText("Copy resource ID for Resource 1 Title");
await user.click(copyButton);

expect(clipboardUtils.copyToClipboard).toHaveBeenCalledWith("resource-abc-123");
Expand Down
7 changes: 5 additions & 2 deletions src/components/resources/ResourcesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function ResourcesTable({
value={resource.uriTemplate || resource.uri}
label={intl.formatMessage(
{ id: "resources.table.copyUri" },
{ uri: resource.uriTemplate || resource.uri },
{ name: resource.title || resource.name },
)}
iconClassName="size-3"
className="ml-4 size-4 shrink-0 text-muted-foreground hover:text-foreground"
Expand All @@ -97,7 +97,10 @@ export function ResourcesTable({
</span>
<CopyButton
value={resource.id}
label={intl.formatMessage({ id: "resources.table.copyResourceId" })}
label={intl.formatMessage(
{ id: "resources.table.copyResourceId" },
{ name: resource.title || resource.name },
)}
iconClassName="size-3"
className="ml-4 size-4 shrink-0 text-muted-foreground hover:text-foreground"
/>
Expand Down
11 changes: 6 additions & 5 deletions src/components/servers/MCPServerDetailsPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -817,11 +817,12 @@ describe("MCPServerDetailsPanel", () => {
/>,
);

// A titled tool copies via its title label; an untitled one via its identifier.
await user.click(await screen.findByRole("button", { name: "Copy Tool One" }));
// A titled tool copies via a "tool name for {title}" label; an untitled
// one via the component-type noun (its identifier is a raw value, not a name).
await user.click(await screen.findByRole("button", { name: "Copy tool name for Tool One" }));
expect(copyToClipboard).toHaveBeenCalledWith("original_tool_1");

await user.click(screen.getByRole("button", { name: "Copy original_tool_2" }));
await user.click(screen.getByRole("button", { name: "Copy tool" }));
expect(copyToClipboard).toHaveBeenCalledWith("original_tool_2");
});

Expand All @@ -836,7 +837,7 @@ describe("MCPServerDetailsPanel", () => {
initialTab="components"
/>,
);
await screen.findByRole("button", { name: "Copy Tool One" });
await screen.findByRole("button", { name: "Copy tool name for Tool One" });

const allTab = screen.getByRole("tab", { name: "All" });
allTab.focus();
Expand Down Expand Up @@ -926,7 +927,7 @@ describe("MCPServerDetailsPanel", () => {
initialTab="components"
/>,
);
await screen.findByRole("button", { name: "Copy Tool One" });
await screen.findByRole("button", { name: "Copy tool name for Tool One" });

const searchBox = screen.getByRole("searchbox");
await user.click(searchBox);
Expand Down
30 changes: 22 additions & 8 deletions src/components/servers/MCPServerDetailsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,12 @@ export function MCPServerDetailsPanel({

const intl = useIntl();

const getComponentLabel = useCallback(
(type: Exclude<ComponentTab, "all">) =>
intl.formatMessage({ id: `mcpServer.details.component.${type}` }),
[intl],
);

const getVisibilityLabel = useCallback(
(value?: string) => {
if (value === "team") return intl.formatMessage({ id: "common.visibility.team" });
Expand Down Expand Up @@ -479,7 +485,7 @@ export function MCPServerDetailsPanel({
<span className="mr-1.5 inline-flex">
{getComponentIcon(component.type)}
</span>
{component.type.slice(0, -1)}
{getComponentLabel(component.type)}
</Badge>
{title ? (
<>
Expand All @@ -491,8 +497,10 @@ export function MCPServerDetailsPanel({
<CopyButton
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: title },
{
id: `mcpServer.details.component.copyName.${component.type}`,
},
{ name: title },
)}
className="size-5 text-muted-foreground"
/>
Expand All @@ -506,7 +514,7 @@ export function MCPServerDetailsPanel({
value={identifier}
label={intl.formatMessage(
{ id: "common.copyValue" },
{ label: identifier },
{ label: getComponentLabel(component.type) },
)}
className="size-5 text-muted-foreground"
/>
Expand Down Expand Up @@ -568,13 +576,19 @@ export function MCPServerDetailsPanel({
{getTransportLabel(server.transport)}
</span>
</DetailRow>
<DetailRow label="UUID">
<CopyValue label="UUID" value={server.id} />
<DetailRow label={intl.formatMessage({ id: "mcpServer.details.uuid" })}>
<CopyValue
label={intl.formatMessage({ id: "mcpServer.details.uuid" })}
value={server.id}
/>
</DetailRow>
<DetailRow label="URL">
<DetailRow label={intl.formatMessage({ id: "mcpServer.details.url" })}>
<span className="flex items-center gap-2">
<Globe className="size-3.5 text-muted-foreground" />
<CopyValue label="URL" value={server.url} />
<CopyValue
label={intl.formatMessage({ id: "mcpServer.details.url" })}
value={server.url}
/>
</span>
</DetailRow>
{server.team && (
Expand Down
10 changes: 6 additions & 4 deletions src/components/servers/ServersTable.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ServersTable } from "./ServersTable";
Expand Down Expand Up @@ -38,6 +38,10 @@ describe("ServersTable", () => {
vi.clearAllMocks();
});

afterEach(() => {
vi.useRealTimers();
});

// ── Loading state ────────────────────────────────────────────────────────────

it("renders a loading indicator when isLoading is true", () => {
Expand Down Expand Up @@ -200,9 +204,7 @@ describe("ServersTable", () => {
act(() => {
vi.advanceTimersByTime(1500);
});
expect(screen.queryByRole("status")).not.toBeInTheDocument();

vi.useRealTimers();
expect(screen.getByRole("status")).toHaveTextContent("");
});

it("shows a failed indicator instead of throwing when copying to the clipboard fails", async () => {
Expand Down
6 changes: 6 additions & 0 deletions src/components/tokens/TokenCreatedDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ describe("TokenCreatedDialog", () => {
expect(onClose).toHaveBeenCalledTimes(1);
});

it("auto-focuses the copy button on open (no redirect needed without a hover tooltip)", async () => {
renderWithProviders(<TokenCreatedDialog token="raw-secret-123" onClose={vi.fn()} />);

await waitFor(() => expect(screen.getByRole("button", { name: /copy/i })).toHaveFocus());
});

it("calls copyToClipboard with the token when the copy button is clicked", () => {
renderWithProviders(<TokenCreatedDialog token="raw-secret-123" onClose={vi.fn()} />);
fireEvent.click(screen.getByRole("button", { name: /copy/i }));
Expand Down
7 changes: 0 additions & 7 deletions src/components/tokens/TokenCreatedDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@ export function TokenCreatedDialog({ token, onClose, returnFocusRef }: TokenCrea
>
<DialogContent
className="gap-6 sm:max-w-xl"
onOpenAutoFocus={(event) => {
// Radix would otherwise auto-focus the first focusable descendant,
// the copy button — whose tooltip opens on focus and would eat the
// first Escape press instead of the dialog. Focus Close instead.
event.preventDefault();
closeButtonRef.current?.focus();
}}
onCloseAutoFocus={(event) => {
const target = returnFocusRef?.current;
if (!target) return; // No target: leave Radix's default restore alone.
Expand Down
Loading
Loading