Phase 2: Refactor History cluster to use MessageEntry

Replace ad hoc flat props with MessageEntry from core/mcp/types.ts
across the History group components and HistoryScreen.

- HistoryEntry: accept `entry: MessageEntry` instead of flat props
  (timestamp, method, target, status, durationMs, parameters, response);
  derive method/target/status from embedded JSON-RPC objects; fix
  Collapse animation by removing outer {isExpanded && ...} wrapper;
  drop childEntries (not part of MessageEntry)
- HistoryControls: type onMethodFilterChange as (string | undefined)
- HistoryListPanel: accept `MessageEntry[]` + `pinnedIds: Set<string>`
  instead of separate entries/pinnedEntries arrays with pre-bound
  callbacks; pass onReplay/onTogglePin by entry ID
- HistoryScreen: accept `MessageEntry[]` + `pinnedIds` + ID-based
  callbacks; collapse two-array model into single list with pinned
  overlay
- Delete local HistoryEntryProps data shape, HistoryChildEntry type
- Update all stories with real JSON-RPC MessageEntry fixtures
- Update ConnectedView HistoryActive story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-20 16:55:23 -04:00
commit f104e6e21a
2 changed files with 479 additions and 380 deletions

View file

@ -1,101 +1,126 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
import { fn } from "storybook/test";
import { HistoryScreen } from "./HistoryScreen";
import type { HistoryEntryProps } from "../../groups/HistoryEntry/HistoryEntry";
const meta: Meta<typeof HistoryScreen> = {
title: "Screens/HistoryScreen",
component: HistoryScreen,
parameters: { layout: "fullscreen" },
args: {
pinnedIds: new Set<string>(),
onClearAll: fn(),
onExport: fn(),
onReplay: fn(),
onTogglePin: fn(),
},
};
export default meta;
type Story = StoryObj<typeof HistoryScreen>;
const entry1: HistoryEntryProps = {
timestamp: "2026-03-17T10:00:00Z",
method: "tools/call",
target: "send_message",
status: "success",
durationMs: 120,
parameters: { message: "Hello, world!" },
response: { result: "Message sent successfully" },
isPinned: false,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
};
const entry2: HistoryEntryProps = {
timestamp: "2026-03-17T10:01:00Z",
method: "resources/read",
target: "config.json",
status: "success",
durationMs: 45,
parameters: { uri: "file:///config.json" },
response: { contents: [{ uri: "file:///config.json", text: "{}" }] },
isPinned: false,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
};
const entry3: HistoryEntryProps = {
timestamp: "2026-03-17T10:02:00Z",
method: "tools/call",
target: "delete_records",
status: "error",
durationMs: 350,
parameters: { ids: [1, 2, 3] },
response: { error: "Permission denied" },
isPinned: false,
isListExpanded: true,
onReplay: fn(),
onTogglePin: fn(),
};
const pinnedEntry1: HistoryEntryProps = {
timestamp: "2026-03-17T09:30:00Z",
method: "tools/list",
status: "success",
durationMs: 80,
response: { tools: ["send_message", "list_users"] },
isPinned: true,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
};
const pinnedEntry2: HistoryEntryProps = {
timestamp: "2026-03-17T09:35:00Z",
method: "prompts/get",
target: "greeting",
status: "success",
durationMs: 60,
parameters: { name: "greeting" },
response: {
messages: [{ role: "user", content: { type: "text", text: "Hello!" } }],
const sampleEntries: MessageEntry[] = [
{
id: "req-1",
timestamp: new Date("2026-03-17T10:00:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "send_message", arguments: { message: "Hello, world!" } },
},
response: {
jsonrpc: "2.0",
id: 1,
result: {
content: [{ type: "text", text: "Message sent successfully" }],
},
},
duration: 120,
},
isPinned: true,
isListExpanded: false,
onReplay: fn(),
onTogglePin: fn(),
};
{
id: "req-2",
timestamp: new Date("2026-03-17T10:01:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 2,
method: "resources/read",
params: { uri: "file:///config.json" },
},
response: {
jsonrpc: "2.0",
id: 2,
result: {
contents: [{ uri: "file:///config.json", text: "{}" }],
},
},
duration: 45,
},
{
id: "req-3",
timestamp: new Date("2026-03-17T10:02:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 3,
method: "tools/call",
params: { name: "delete_records", arguments: { ids: [1, 2, 3] } },
},
response: {
jsonrpc: "2.0",
id: 3,
error: { code: -32000, message: "Permission denied" },
},
duration: 350,
},
{
id: "req-4",
timestamp: new Date("2026-03-17T09:30:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 4,
method: "tools/list",
},
response: {
jsonrpc: "2.0",
id: 4,
result: { tools: [] },
},
duration: 80,
},
{
id: "req-5",
timestamp: new Date("2026-03-17T09:35:00Z"),
direction: "request",
message: {
jsonrpc: "2.0",
id: 5,
method: "prompts/get",
params: { name: "greeting" },
},
response: {
jsonrpc: "2.0",
id: 5,
result: {
messages: [{ role: "user", content: { type: "text", text: "Hello!" } }],
},
},
duration: 60,
},
];
export const WithEntries: Story = {
args: {
entries: [entry1, entry2, entry3],
pinnedEntries: [pinnedEntry1, pinnedEntry2],
entries: sampleEntries,
pinnedIds: new Set(["req-4", "req-5"]),
},
};
export const Empty: Story = {
args: {
entries: [],
pinnedEntries: [],
},
};

View file

@ -1,14 +1,16 @@
import { useState } from "react";
import { Card, Flex, Stack } from "@mantine/core";
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
import { HistoryControls } from "../../groups/HistoryControls/HistoryControls";
import { HistoryListPanel } from "../../groups/HistoryListPanel/HistoryListPanel.js";
import type { HistoryEntryProps } from "../../groups/HistoryEntry/HistoryEntry";
export interface HistoryScreenProps {
entries: HistoryEntryProps[];
pinnedEntries: HistoryEntryProps[];
entries: MessageEntry[];
pinnedIds: Set<string>;
onClearAll: () => void;
onExport: () => void;
onReplay: (id: string) => void;
onTogglePin: (id: string) => void;
}
const ScreenLayout = Flex.withProps({
@ -30,9 +32,11 @@ const SidebarCard = Card.withProps({
export function HistoryScreen({
entries,
pinnedEntries,
pinnedIds,
onClearAll,
onExport,
onReplay,
onTogglePin,
}: HistoryScreenProps) {
const [searchText, setSearchText] = useState("");
const [methodFilter, setMethodFilter] = useState<string | undefined>();
@ -45,19 +49,19 @@ export function HistoryScreen({
searchText={searchText}
methodFilter={methodFilter}
onSearchChange={setSearchText}
onMethodFilterChange={(value) =>
setMethodFilter(value || undefined)
}
onMethodFilterChange={setMethodFilter}
/>
</SidebarCard>
</Sidebar>
<HistoryListPanel
entries={entries}
pinnedEntries={pinnedEntries}
pinnedIds={pinnedIds}
searchText={searchText}
methodFilter={methodFilter}
onClearAll={onClearAll}
onExport={onExport}
onReplay={onReplay}
onTogglePin={onTogglePin}
/>
</ScreenLayout>
);