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:
parent
0db955ea7e
commit
f104e6e21a
2 changed files with 479 additions and 380 deletions
|
|
@ -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: [],
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue