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
25 changes: 25 additions & 0 deletions clients/web/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@
/* ── History ───────────────────────────────────────────── */
--inspector-history-success: var(--mantine-color-green-6);
--inspector-history-error: var(--mantine-color-red-6);

/* ── Protocol direction (Protocol Builder) ─────────────── */
--inspector-protocol-send-text: var(--mantine-color-green-7);
--inspector-protocol-send-bg: var(--mantine-color-green-0);
--inspector-protocol-send-border: var(--mantine-color-green-3);
--inspector-protocol-receive-text: var(--mantine-color-blue-7);
--inspector-protocol-receive-bg: var(--mantine-color-blue-0);
--inspector-protocol-receive-border: var(--mantine-color-blue-3);
--inspector-protocol-recursion-text: var(--mantine-color-yellow-8);
--inspector-protocol-recursion-bg: var(--mantine-color-yellow-0);
--inspector-protocol-recursion-border: var(--mantine-color-yellow-3);
--inspector-protocol-target-bg: var(--mantine-color-blue-0);
--inspector-protocol-target-border: var(--mantine-color-blue-4);
}

/* Dark mode overrides — tokens that need adjustment in dark scheme */
Expand All @@ -67,6 +80,18 @@
--inspector-surface-code: var(--mantine-color-dark-9);
--inspector-border-subtle: var(--mantine-color-gray-4);
--inspector-input-background: var(--mantine-color-dark-9);

--inspector-protocol-send-text: var(--mantine-color-green-3);
--inspector-protocol-send-bg: var(--mantine-color-green-9);
--inspector-protocol-send-border: var(--mantine-color-green-7);
--inspector-protocol-receive-text: var(--mantine-color-blue-3);
--inspector-protocol-receive-bg: var(--mantine-color-blue-9);
--inspector-protocol-receive-border: var(--mantine-color-blue-7);
--inspector-protocol-recursion-text: var(--mantine-color-yellow-3);
--inspector-protocol-recursion-bg: var(--mantine-color-yellow-9);
--inspector-protocol-recursion-border: var(--mantine-color-yellow-7);
--inspector-protocol-target-bg: var(--mantine-color-blue-9);
--inspector-protocol-target-border: var(--mantine-color-blue-5);
}

/* ── Animations ──────────────────────────────────────────── */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { Card, Stack } from "@mantine/core";
import { ProtocolOutputPanel } from "./ProtocolOutputPanel";

const meta: Meta<typeof ProtocolOutputPanel> = {
title: "Groups/ProtocolOutputPanel",
component: ProtocolOutputPanel,
parameters: { layout: "padded" },
args: {
pythonSnippet:
'from llmsessioncontract import Monitor\n\nprotocol = "!Search.?SearchResult.end"\nmonitor = Monitor(protocol)',
copied: null,
onCopyDsl: fn(),
onCopyPython: fn(),
onDownload: fn(),
},
render: (args) => (
<Stack maw={520}>
<Card withBorder padding="lg">
<ProtocolOutputPanel {...args} />
</Card>
</Stack>
),
};

export default meta;
type Story = StoryObj<typeof ProtocolOutputPanel>;

export const SimpleSequence: Story = {
args: { protocol: "!Search.?SearchResult.!Book.?BookConfirm.end" },
};

export const Choice: Story = {
args: { protocol: "!{Yes.!Confirm.end, No.end}" },
};

export const Recursion: Story = {
args: { protocol: "rec X.!Ping.?Pong.X" },
};

export const Empty: Story = {
args: { protocol: "end" },
};

export const Copied: Story = {
args: {
protocol: "!Search.?SearchResult.end",
copied: "dsl",
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithMantine, screen } from "../../../test/renderWithMantine";
import { ProtocolOutputPanel } from "./ProtocolOutputPanel";

function makeProps(
overrides: Partial<React.ComponentProps<typeof ProtocolOutputPanel>> = {},
) {
return {
protocol: "!Search.?SearchResult.end",
pythonSnippet: "from llmsessioncontract import Monitor",
copied: null as "dsl" | "python" | null,
onCopyDsl: vi.fn(),
onCopyPython: vi.fn(),
onDownload: vi.fn(),
...overrides,
};
}

describe("ProtocolOutputPanel", () => {
it("renders the DSL, FSM preview, and Python snippet", () => {
renderWithMantine(<ProtocolOutputPanel {...makeProps()} />);
expect(screen.getByText("Session Type DSL")).toBeInTheDocument();
expect(screen.getByText("State Machine Preview")).toBeInTheDocument();
expect(screen.getByText("Python Integration")).toBeInTheDocument();
expect(
screen.getByText("from llmsessioncontract import Monitor"),
).toBeInTheDocument();
});

it("invokes the copy callbacks when Copy buttons are clicked", async () => {
const onCopyDsl = vi.fn();
const onCopyPython = vi.fn();
renderWithMantine(
<ProtocolOutputPanel {...makeProps({ onCopyDsl, onCopyPython })} />,
);
const user = userEvent.setup();
const copyButtons = screen.getAllByRole("button", { name: /^Copy$/ });
await user.click(copyButtons[0]);
await user.click(copyButtons[1]);
expect(onCopyDsl).toHaveBeenCalled();
expect(onCopyPython).toHaveBeenCalled();
});

it("shows 'Copied!' on the DSL button when copied is 'dsl'", () => {
renderWithMantine(
<ProtocolOutputPanel {...makeProps({ copied: "dsl" })} />,
);
const buttons = screen.getAllByRole("button");
const labels = buttons.map((b) => b.textContent ?? "");
expect(labels).toContain("Copied!");
});

it("shows 'Copied!' on the Python button when copied is 'python'", () => {
renderWithMantine(
<ProtocolOutputPanel {...makeProps({ copied: "python" })} />,
);
const buttons = screen.getAllByRole("button");
const labels = buttons.map((b) => b.textContent ?? "");
expect(labels).toContain("Copied!");
});

it("invokes onDownload when the download button is clicked", async () => {
const onDownload = vi.fn();
renderWithMantine(<ProtocolOutputPanel {...makeProps({ onDownload })} />);
const user = userEvent.setup();
await user.click(
screen.getByRole("button", { name: /Download Python File/ }),
);
expect(onDownload).toHaveBeenCalled();
});

it("renders the empty FSM hint for an empty protocol", () => {
renderWithMantine(
<ProtocolOutputPanel {...makeProps({ protocol: "end" })} />,
);
expect(
screen.getByText("Add steps to see the state machine"),
).toBeInTheDocument();
});

it("renders FSM transitions for a non-trivial protocol", () => {
renderWithMantine(
<ProtocolOutputPanel
{...makeProps({ protocol: "!{Yes.!Done.end, No.end}" })}
/>,
);
// The choice fans out into two outgoing edges, both rendered as pills
expect(screen.getAllByText(/!Yes|!No/).length).toBeGreaterThan(0);
});

it("renders a loop transition for recursion", () => {
renderWithMantine(
<ProtocolOutputPanel
{...makeProps({ protocol: "rec X.!Ping.?Pong.X" })}
/>,
);
expect(screen.getByText(/↻X/)).toBeInTheDocument();
});

it("highlights protocol tokens by category", () => {
renderWithMantine(
<ProtocolOutputPanel {...makeProps({ protocol: "rec X.!A.?B.X.end" })} />,
);
expect(screen.getAllByText("rec").length).toBeGreaterThan(0);
expect(screen.getByText("end")).toBeInTheDocument();
expect(screen.getAllByText("!").length).toBeGreaterThan(0);
expect(screen.getAllByText("?").length).toBeGreaterThan(0);
});

it("highlights spaces and other characters without crashing", () => {
renderWithMantine(
<ProtocolOutputPanel {...makeProps({ protocol: "!{Yes, No}.end" })} />,
);
expect(screen.getByText("end")).toBeInTheDocument();
});
});
Loading