diff --git a/client/src/App.tsx b/client/src/App.tsx
index 26eb44c5..3ceafcae 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -52,7 +52,7 @@ import { z } from "zod";
import "./App.css";
import AuthDebugger from "./components/AuthDebugger";
import ConsoleTab from "./components/ConsoleTab";
-import HistoryAndNotifications from "./components/History";
+import HistoryAndNotifications from "./components/HistoryAndNotifications";
import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import ResourcesTab from "./components/ResourcesTab";
diff --git a/client/src/components/History.tsx b/client/src/components/HistoryAndNotifications.tsx
similarity index 89%
rename from client/src/components/History.tsx
rename to client/src/components/HistoryAndNotifications.tsx
index 78394dea..34c035f5 100644
--- a/client/src/components/History.tsx
+++ b/client/src/components/HistoryAndNotifications.tsx
@@ -110,15 +110,27 @@ const HistoryAndNotifications = ({
>
toggleNotificationExpansion(index)}
+ onClick={() =>
+ toggleNotificationExpansion(
+ serverNotifications.length - 1 - index,
+ )
+ }
>
{serverNotifications.length - index}.{" "}
{notification.method}
- {expandedNotifications[index] ? "▼" : "▶"}
+
+ {expandedNotifications[
+ serverNotifications.length - 1 - index
+ ]
+ ? "▼"
+ : "▶"}
+
- {expandedNotifications[index] && (
+ {expandedNotifications[
+ serverNotifications.length - 1 - index
+ ] && (
diff --git a/client/src/components/__tests__/HistoryAndNotifications.test.tsx b/client/src/components/__tests__/HistoryAndNotifications.test.tsx
new file mode 100644
index 00000000..42c58519
--- /dev/null
+++ b/client/src/components/__tests__/HistoryAndNotifications.test.tsx
@@ -0,0 +1,226 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { describe, it, expect, jest } from "@jest/globals";
+import HistoryAndNotifications from "../HistoryAndNotifications";
+import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
+
+// Mock JsonView component
+jest.mock("../JsonView", () => {
+ return function JsonView({ data }: { data: string }) {
+ return {data}
;
+ };
+});
+
+describe("HistoryAndNotifications", () => {
+ const mockRequestHistory = [
+ {
+ request: JSON.stringify({ method: "test/method1", params: {} }),
+ response: JSON.stringify({ result: "success" }),
+ },
+ {
+ request: JSON.stringify({ method: "test/method2", params: {} }),
+ response: JSON.stringify({ result: "success" }),
+ },
+ ];
+
+ const mockNotifications: ServerNotification[] = [
+ {
+ method: "notifications/message",
+ params: {
+ level: "info" as const,
+ message: "First notification",
+ },
+ },
+ {
+ method: "notifications/progress",
+ params: {
+ progressToken: "test-token",
+ progress: 50,
+ message: "Second notification",
+ },
+ },
+ ];
+
+ it("renders history and notifications sections", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("History")).toBeTruthy();
+ expect(screen.getByText("Server Notifications")).toBeTruthy();
+ });
+
+ it("displays request history items with correct numbering", () => {
+ render(
+ ,
+ );
+
+ // Items should be numbered in reverse order (newest first)
+ expect(screen.getByText("2. test/method2")).toBeTruthy();
+ expect(screen.getByText("1. test/method1")).toBeTruthy();
+ });
+
+ it("displays server notifications with correct numbering", () => {
+ render(
+ ,
+ );
+
+ // Items should be numbered in reverse order (newest first)
+ expect(screen.getByText("2. notifications/progress")).toBeTruthy();
+ expect(screen.getByText("1. notifications/message")).toBeTruthy();
+ });
+
+ it("expands and collapses request items when clicked", () => {
+ render(
+ ,
+ );
+
+ const firstRequestHeader = screen.getByText("2. test/method2");
+
+ // Initially collapsed - should show ▶ arrows (there are multiple)
+ expect(screen.getAllByText("▶")).toHaveLength(2);
+ expect(screen.queryByText("Request:")).toBeNull();
+
+ // Click to expand
+ fireEvent.click(firstRequestHeader);
+
+ // Should now be expanded - one ▼ and one ▶
+ expect(screen.getByText("▼")).toBeTruthy();
+ expect(screen.getAllByText("▶")).toHaveLength(1);
+ expect(screen.getByText("Request:")).toBeTruthy();
+ expect(screen.getByText("Response:")).toBeTruthy();
+ });
+
+ it("expands and collapses notification items when clicked", () => {
+ render(
+ ,
+ );
+
+ const firstNotificationHeader = screen.getByText(
+ "2. notifications/progress",
+ );
+
+ // Initially collapsed
+ expect(screen.getAllByText("▶")).toHaveLength(2);
+ expect(screen.queryByText("Details:")).toBeNull();
+
+ // Click to expand
+ fireEvent.click(firstNotificationHeader);
+
+ // Should now be expanded
+ expect(screen.getByText("▼")).toBeTruthy();
+ expect(screen.getAllByText("▶")).toHaveLength(1);
+ expect(screen.getByText("Details:")).toBeTruthy();
+ });
+
+ it("maintains expanded state when new notifications are added", () => {
+ const { rerender } = render(
+ ,
+ );
+
+ // Find and expand the older notification (should be "1. notifications/message")
+ const olderNotificationHeader = screen.getByText(
+ "1. notifications/message",
+ );
+ fireEvent.click(olderNotificationHeader);
+
+ // Verify it's expanded
+ expect(screen.getByText("Details:")).toBeTruthy();
+
+ // Add a new notification at the beginning (simulating real behavior)
+ const newNotifications: ServerNotification[] = [
+ {
+ method: "notifications/resources/updated",
+ params: { uri: "file://test.txt" },
+ },
+ ...mockNotifications,
+ ];
+
+ // Re-render with new notifications
+ rerender(
+ ,
+ );
+
+ // The original notification should still be expanded
+ // It's now numbered as "2. notifications/message" due to the new item
+ expect(screen.getByText("3. notifications/progress")).toBeTruthy();
+ expect(screen.getByText("2. notifications/message")).toBeTruthy();
+ expect(screen.getByText("1. notifications/resources/updated")).toBeTruthy();
+
+ // The originally expanded notification should still show its details
+ expect(screen.getByText("Details:")).toBeTruthy();
+ });
+
+ it("maintains expanded state when new requests are added", () => {
+ const { rerender } = render(
+ ,
+ );
+
+ // Find and expand the older request (should be "1. test/method1")
+ const olderRequestHeader = screen.getByText("1. test/method1");
+ fireEvent.click(olderRequestHeader);
+
+ // Verify it's expanded
+ expect(screen.getByText("Request:")).toBeTruthy();
+ expect(screen.getByText("Response:")).toBeTruthy();
+
+ // Add a new request at the beginning
+ const newRequestHistory = [
+ {
+ request: JSON.stringify({ method: "test/new_method", params: {} }),
+ response: JSON.stringify({ result: "new success" }),
+ },
+ ...mockRequestHistory,
+ ];
+
+ // Re-render with new request history
+ rerender(
+ ,
+ );
+
+ // The original request should still be expanded
+ // It's now numbered as "2. test/method1" due to the new item
+ expect(screen.getByText("3. test/method2")).toBeTruthy();
+ expect(screen.getByText("2. test/method1")).toBeTruthy();
+ expect(screen.getByText("1. test/new_method")).toBeTruthy();
+
+ // The originally expanded request should still show its details
+ expect(screen.getByText("Request:")).toBeTruthy();
+ expect(screen.getByText("Response:")).toBeTruthy();
+ });
+
+ it("displays empty state messages when no data is available", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("No history yet")).toBeTruthy();
+ expect(screen.getByText("No notifications yet")).toBeTruthy();
+ });
+});