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
9 changed files with 479 additions and 380 deletions
|
|
@ -5,12 +5,6 @@ import { HistoryControls } from "./HistoryControls";
|
||||||
const meta: Meta<typeof HistoryControls> = {
|
const meta: Meta<typeof HistoryControls> = {
|
||||||
title: "Groups/HistoryControls",
|
title: "Groups/HistoryControls",
|
||||||
component: HistoryControls,
|
component: HistoryControls,
|
||||||
};
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof HistoryControls>;
|
|
||||||
|
|
||||||
export const Default: Story = {
|
|
||||||
args: {
|
args: {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
onSearchChange: fn(),
|
onSearchChange: fn(),
|
||||||
|
|
@ -18,11 +12,14 @@ export const Default: Story = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof HistoryControls>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
export const WithFilters: Story = {
|
export const WithFilters: Story = {
|
||||||
args: {
|
args: {
|
||||||
searchText: "tools",
|
searchText: "tools",
|
||||||
methodFilter: "tools/call",
|
methodFilter: "tools/call",
|
||||||
onSearchChange: fn(),
|
|
||||||
onMethodFilterChange: fn(),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export interface HistoryControlsProps {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
methodFilter?: string;
|
methodFilter?: string;
|
||||||
onSearchChange: (text: string) => void;
|
onSearchChange: (text: string) => void;
|
||||||
onMethodFilterChange: (method: string) => void;
|
onMethodFilterChange: (method: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryControls({
|
export function HistoryControls({
|
||||||
|
|
@ -38,7 +38,7 @@ export function HistoryControls({
|
||||||
placeholder="All methods"
|
placeholder="All methods"
|
||||||
data={METHOD_OPTIONS}
|
data={METHOD_OPTIONS}
|
||||||
value={methodFilter ?? null}
|
value={methodFilter ?? null}
|
||||||
onChange={(value) => onMethodFilterChange(value ?? "")}
|
onChange={(value) => onMethodFilterChange(value ?? undefined)}
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { HistoryEntry } from "./HistoryEntry";
|
import { HistoryEntry } from "./HistoryEntry";
|
||||||
|
|
||||||
|
|
@ -14,13 +15,79 @@ const meta: Meta<typeof HistoryEntry> = {
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof HistoryEntry>;
|
type Story = StoryObj<typeof HistoryEntry>;
|
||||||
|
|
||||||
|
const toolCallEntry: MessageEntry = {
|
||||||
|
id: "req-1",
|
||||||
|
timestamp: new Date("2026-03-17T10:30:00Z"),
|
||||||
|
direction: "request",
|
||||||
|
message: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
method: "tools/call",
|
||||||
|
params: { name: "get_weather", arguments: { city: "San Francisco" } },
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
|
result: {
|
||||||
|
content: [{ type: "text", text: "18°C, partly cloudy" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duration: 142,
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorEntry: MessageEntry = {
|
||||||
|
id: "req-2",
|
||||||
|
timestamp: new Date("2026-03-17T10:31:15Z"),
|
||||||
|
direction: "request",
|
||||||
|
message: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 2,
|
||||||
|
method: "tools/call",
|
||||||
|
params: { name: "query_database" },
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 2,
|
||||||
|
error: { code: -32000, message: "Connection timeout" },
|
||||||
|
},
|
||||||
|
duration: 3200,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resourceReadEntry: MessageEntry = {
|
||||||
|
id: "req-3",
|
||||||
|
timestamp: new Date("2026-03-17T10:33:00Z"),
|
||||||
|
direction: "request",
|
||||||
|
message: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 3,
|
||||||
|
method: "resources/read",
|
||||||
|
params: { uri: "file:///config.json" },
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 3,
|
||||||
|
result: {
|
||||||
|
contents: [{ uri: "file:///config.json", text: '{"debug": true}' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duration: 45,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pendingEntry: MessageEntry = {
|
||||||
|
id: "req-4",
|
||||||
|
timestamp: new Date("2026-03-17T10:34:00Z"),
|
||||||
|
direction: "request",
|
||||||
|
message: {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 4,
|
||||||
|
method: "tools/call",
|
||||||
|
params: { name: "long_operation" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const SuccessCollapsed: Story = {
|
export const SuccessCollapsed: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:30:00Z",
|
entry: toolCallEntry,
|
||||||
method: "tools/call",
|
|
||||||
target: "get_weather",
|
|
||||||
status: "success",
|
|
||||||
durationMs: 142,
|
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
isListExpanded: false,
|
isListExpanded: false,
|
||||||
},
|
},
|
||||||
|
|
@ -28,79 +95,32 @@ export const SuccessCollapsed: Story = {
|
||||||
|
|
||||||
export const SuccessExpanded: Story = {
|
export const SuccessExpanded: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:30:00Z",
|
entry: toolCallEntry,
|
||||||
method: "tools/call",
|
|
||||||
target: "get_weather",
|
|
||||||
status: "success",
|
|
||||||
durationMs: 142,
|
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
isListExpanded: true,
|
isListExpanded: true,
|
||||||
parameters: {
|
|
||||||
city: "San Francisco",
|
|
||||||
units: "celsius",
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
temperature: 18,
|
|
||||||
conditions: "Partly cloudy",
|
|
||||||
humidity: 65,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Error: Story = {
|
export const Error: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:31:15Z",
|
entry: errorEntry,
|
||||||
method: "tools/call",
|
|
||||||
target: "query_database",
|
|
||||||
status: "error",
|
|
||||||
durationMs: 3200,
|
|
||||||
isPinned: false,
|
|
||||||
isListExpanded: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithChildren: Story = {
|
|
||||||
args: {
|
|
||||||
timestamp: "2026-03-17T10:32:00Z",
|
|
||||||
method: "tools/call",
|
|
||||||
target: "complex_operation",
|
|
||||||
status: "success",
|
|
||||||
durationMs: 1250,
|
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
isListExpanded: true,
|
isListExpanded: true,
|
||||||
parameters: {
|
|
||||||
action: "process",
|
|
||||||
},
|
|
||||||
response: {
|
|
||||||
result: "completed",
|
|
||||||
},
|
|
||||||
childEntries: [
|
|
||||||
{
|
|
||||||
timestamp: "+120ms",
|
|
||||||
method: "sampling/createMessage",
|
|
||||||
target: "gpt-4",
|
|
||||||
status: "success",
|
|
||||||
durationMs: 800,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamp: "+950ms",
|
|
||||||
method: "elicitation/create",
|
|
||||||
target: "confirm_action",
|
|
||||||
status: "success",
|
|
||||||
durationMs: 200,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Pinned: Story = {
|
export const Pinned: Story = {
|
||||||
args: {
|
args: {
|
||||||
timestamp: "2026-03-17T10:33:00Z",
|
entry: resourceReadEntry,
|
||||||
method: "resources/read",
|
|
||||||
target: "config.json",
|
|
||||||
status: "success",
|
|
||||||
durationMs: 45,
|
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
isListExpanded: false,
|
isListExpanded: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Pending: Story = {
|
||||||
|
args: {
|
||||||
|
entry: pendingEntry,
|
||||||
|
isPinned: false,
|
||||||
|
isListExpanded: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,11 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||||
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
import { ContentViewer } from "../../elements/ContentViewer/ContentViewer";
|
||||||
|
|
||||||
export interface HistoryChildEntry {
|
|
||||||
timestamp: string;
|
|
||||||
method: string;
|
|
||||||
target?: string;
|
|
||||||
status: "success" | "error";
|
|
||||||
durationMs: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoryEntryProps {
|
export interface HistoryEntryProps {
|
||||||
timestamp: string;
|
entry: MessageEntry;
|
||||||
method: string;
|
|
||||||
target?: string;
|
|
||||||
status: "success" | "error";
|
|
||||||
durationMs: number;
|
|
||||||
parameters?: Record<string, unknown>;
|
|
||||||
response?: Record<string, unknown>;
|
|
||||||
childEntries?: HistoryChildEntry[];
|
|
||||||
isPinned: boolean;
|
isPinned: boolean;
|
||||||
isListExpanded: boolean;
|
isListExpanded: boolean;
|
||||||
onReplay: () => void;
|
onReplay: () => void;
|
||||||
|
|
@ -65,64 +51,86 @@ const SubtleButton = Button.withProps({
|
||||||
size: "xs",
|
size: "xs",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ChildMethodBadge = Badge.withProps({
|
|
||||||
color: "dark",
|
|
||||||
size: "sm",
|
|
||||||
});
|
|
||||||
|
|
||||||
function formatDuration(ms: number): string {
|
function formatDuration(ms: number): string {
|
||||||
return `${ms}ms`;
|
return `${ms}ms`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatStatusLabel(status: "success" | "error"): string {
|
function formatTimestamp(date: Date): string {
|
||||||
return status === "success" ? "OK" : "Error";
|
return date.toISOString();
|
||||||
}
|
|
||||||
|
|
||||||
function statusColor(status: "success" | "error"): string {
|
|
||||||
return status === "success" ? "green" : "red";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPinLabel(isPinned: boolean): string {
|
function formatPinLabel(isPinned: boolean): string {
|
||||||
return isPinned ? "Unpin" : "Pin";
|
return isPinned ? "Unpin" : "Pin";
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeJson(value: Record<string, unknown>): string {
|
function extractMethod(entry: MessageEntry): string {
|
||||||
|
if ("method" in entry.message) {
|
||||||
|
return entry.message.method;
|
||||||
|
}
|
||||||
|
return "response";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTarget(entry: MessageEntry): string | undefined {
|
||||||
|
const msg = entry.message;
|
||||||
|
if (!("params" in msg) || !msg.params) return undefined;
|
||||||
|
const params = msg.params as Record<string, unknown>;
|
||||||
|
if (typeof params.name === "string") return params.name;
|
||||||
|
if (typeof params.uri === "string") return params.uri;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractStatus(entry: MessageEntry): "success" | "error" | "pending" {
|
||||||
|
if (!entry.response) return "pending";
|
||||||
|
if ("error" in entry.response) return "error";
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(status: "success" | "error" | "pending"): string {
|
||||||
|
if (status === "success") return "green";
|
||||||
|
if (status === "error") return "red";
|
||||||
|
return "gray";
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status: "success" | "error" | "pending"): string {
|
||||||
|
if (status === "success") return "OK";
|
||||||
|
if (status === "error") return "Error";
|
||||||
|
return "Pending";
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeMessage(value: unknown): string {
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HistoryEntry({
|
export function HistoryEntry({
|
||||||
timestamp,
|
entry,
|
||||||
method,
|
|
||||||
target,
|
|
||||||
status,
|
|
||||||
durationMs,
|
|
||||||
parameters,
|
|
||||||
response,
|
|
||||||
childEntries,
|
|
||||||
isPinned,
|
isPinned,
|
||||||
isListExpanded,
|
isListExpanded,
|
||||||
onReplay,
|
onReplay,
|
||||||
onTogglePin,
|
onTogglePin,
|
||||||
}: HistoryEntryProps) {
|
}: HistoryEntryProps) {
|
||||||
const [isExpanded, setIsExpanded] = useState(isListExpanded);
|
const [isExpanded, setIsExpanded] = useState(isListExpanded);
|
||||||
|
const method = extractMethod(entry);
|
||||||
|
const target = extractTarget(entry);
|
||||||
|
const status = extractStatus(entry);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsExpanded(isListExpanded);
|
setIsExpanded(isListExpanded);
|
||||||
}, [isListExpanded]);
|
}, [isListExpanded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EntryContainer>
|
<EntryContainer>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<HeaderRow>
|
<HeaderRow>
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<TimestampText>{timestamp}</TimestampText>
|
<TimestampText>{formatTimestamp(entry.timestamp)}</TimestampText>
|
||||||
<Badge color="dark">{method}</Badge>
|
<Badge color="dark">{method}</Badge>
|
||||||
{target && <TargetText>{target}</TargetText>}
|
{target && <TargetText>{target}</TargetText>}
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
<DurationText>{formatDuration(durationMs)}</DurationText>
|
{entry.duration != null && (
|
||||||
<Badge color={statusColor(status)}>
|
<DurationText>{formatDuration(entry.duration)}</DurationText>
|
||||||
{formatStatusLabel(status)}
|
)}
|
||||||
</Badge>
|
<Badge color={statusColor(status)}>{statusLabel(status)}</Badge>
|
||||||
</Group>
|
</Group>
|
||||||
</HeaderRow>
|
</HeaderRow>
|
||||||
|
|
||||||
|
|
@ -136,57 +144,33 @@ export function HistoryEntry({
|
||||||
</SubtleButton>
|
</SubtleButton>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{isExpanded && (
|
<Collapse in={isExpanded}>
|
||||||
<Collapse in={isExpanded}>
|
<Stack gap="sm">
|
||||||
<Stack gap="sm">
|
<Divider />
|
||||||
{parameters && (
|
<Stack gap="xs">
|
||||||
<>
|
<Text size="sm">Request:</Text>
|
||||||
<Divider />
|
<ContentViewer
|
||||||
<Stack gap="xs">
|
block={{
|
||||||
<Text size="sm">Parameters:</Text>
|
type: "text",
|
||||||
<ContentViewer
|
text: serializeMessage(entry.message),
|
||||||
block={{
|
}}
|
||||||
type: "text",
|
copyable
|
||||||
text: serializeJson(parameters),
|
/>
|
||||||
}}
|
|
||||||
copyable
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{response && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text size="sm">Response:</Text>
|
|
||||||
<ContentViewer
|
|
||||||
block={{
|
|
||||||
type: "text",
|
|
||||||
text: serializeJson(response),
|
|
||||||
}}
|
|
||||||
copyable
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
{childEntries && childEntries.length > 0 && (
|
|
||||||
<Stack gap="xs">
|
|
||||||
{childEntries.map((child, index) => (
|
|
||||||
<Group key={index} pl="lg" gap="sm">
|
|
||||||
<TimestampText>+-- </TimestampText>
|
|
||||||
<TimestampText>{child.timestamp}</TimestampText>
|
|
||||||
<ChildMethodBadge>{child.method}</ChildMethodBadge>
|
|
||||||
{child.target && <Text size="sm">{child.target}</Text>}
|
|
||||||
<Badge color={statusColor(child.status)} size="sm">
|
|
||||||
{formatStatusLabel(child.status)}
|
|
||||||
</Badge>
|
|
||||||
<DurationText>
|
|
||||||
{formatDuration(child.durationMs)}
|
|
||||||
</DurationText>
|
|
||||||
</Group>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Collapse>
|
{entry.response && (
|
||||||
)}
|
<Stack gap="xs">
|
||||||
|
<Text size="sm">Response:</Text>
|
||||||
|
<ContentViewer
|
||||||
|
block={{
|
||||||
|
type: "text",
|
||||||
|
text: serializeMessage(entry.response),
|
||||||
|
}}
|
||||||
|
copyable
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
</Stack>
|
</Stack>
|
||||||
</EntryContainer>
|
</EntryContainer>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,105 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { HistoryListPanel } from "./HistoryListPanel.js";
|
import { HistoryListPanel } from "./HistoryListPanel.js";
|
||||||
import type { HistoryEntryProps } from "../HistoryEntry/HistoryEntry";
|
|
||||||
|
|
||||||
const meta: Meta<typeof HistoryListPanel> = {
|
const meta: Meta<typeof HistoryListPanel> = {
|
||||||
title: "Groups/HistoryListPanel",
|
title: "Groups/HistoryListPanel",
|
||||||
component: HistoryListPanel,
|
component: HistoryListPanel,
|
||||||
args: {
|
args: {
|
||||||
searchText: "",
|
searchText: "",
|
||||||
|
pinnedIds: new Set<string>(),
|
||||||
onClearAll: fn(),
|
onClearAll: fn(),
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
|
onReplay: fn(),
|
||||||
|
onTogglePin: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof HistoryListPanel>;
|
type Story = StoryObj<typeof HistoryListPanel>;
|
||||||
|
|
||||||
function makeEntry(overrides: Partial<HistoryEntryProps>): HistoryEntryProps {
|
const sampleEntries: MessageEntry[] = [
|
||||||
return {
|
{
|
||||||
timestamp: "2026-03-17T10:00:00Z",
|
id: "req-1",
|
||||||
method: "tools/call",
|
timestamp: new Date("2026-03-17T10:00:00Z"),
|
||||||
target: "send_message",
|
direction: "request",
|
||||||
status: "success",
|
message: {
|
||||||
durationMs: 120,
|
jsonrpc: "2.0",
|
||||||
isPinned: false,
|
id: 1,
|
||||||
isListExpanded: false,
|
method: "tools/call",
|
||||||
onReplay: fn(),
|
params: { name: "send_message", arguments: { message: "Hello!" } },
|
||||||
onTogglePin: fn(),
|
},
|
||||||
...overrides,
|
response: {
|
||||||
};
|
jsonrpc: "2.0",
|
||||||
}
|
id: 1,
|
||||||
|
result: { content: [{ type: "text", text: "Sent" }] },
|
||||||
|
},
|
||||||
|
duration: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const WithEntries: Story = {
|
export const WithEntries: Story = {
|
||||||
args: {
|
args: {
|
||||||
entries: [
|
entries: sampleEntries,
|
||||||
makeEntry({
|
pinnedIds: new Set(["req-4"]),
|
||||||
timestamp: "2026-03-17T10:00:00Z",
|
|
||||||
method: "tools/call",
|
|
||||||
target: "send_message",
|
|
||||||
}),
|
|
||||||
makeEntry({
|
|
||||||
timestamp: "2026-03-17T10:01:00Z",
|
|
||||||
method: "resources/read",
|
|
||||||
target: "config.json",
|
|
||||||
durationMs: 45,
|
|
||||||
}),
|
|
||||||
makeEntry({
|
|
||||||
timestamp: "2026-03-17T10:02:00Z",
|
|
||||||
method: "tools/call",
|
|
||||||
target: "delete_records",
|
|
||||||
status: "error",
|
|
||||||
durationMs: 350,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
pinnedEntries: [
|
|
||||||
makeEntry({
|
|
||||||
timestamp: "2026-03-17T09:30:00Z",
|
|
||||||
method: "tools/list",
|
|
||||||
isPinned: true,
|
|
||||||
durationMs: 80,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
entries: [],
|
entries: [],
|
||||||
pinnedEntries: [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@ import {
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||||
import { HistoryEntry } from "../HistoryEntry/HistoryEntry";
|
import { HistoryEntry } from "../HistoryEntry/HistoryEntry";
|
||||||
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
import { ListToggle } from "../../elements/ListToggle/ListToggle";
|
||||||
import type { HistoryEntryProps } from "../HistoryEntry/HistoryEntry";
|
|
||||||
|
|
||||||
export interface HistoryRequestsPanelProps {
|
export interface HistoryListPanelProps {
|
||||||
entries: HistoryEntryProps[];
|
entries: MessageEntry[];
|
||||||
pinnedEntries: HistoryEntryProps[];
|
pinnedIds: Set<string>;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
methodFilter?: string;
|
methodFilter?: string;
|
||||||
onClearAll: () => void;
|
onClearAll: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
|
onReplay: (id: string) => void;
|
||||||
|
onTogglePin: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolbarButton = Button.withProps({
|
const ToolbarButton = Button.withProps({
|
||||||
|
|
@ -39,10 +41,6 @@ const EmptyState = Text.withProps({
|
||||||
py: "xl",
|
py: "xl",
|
||||||
});
|
});
|
||||||
|
|
||||||
function entryKey(entry: HistoryEntryProps): string {
|
|
||||||
return `${entry.timestamp}-${entry.method}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatPinnedTitle(count: number): string {
|
function formatPinnedTitle(count: number): string {
|
||||||
return `Pinned Requests (${count})`;
|
return `Pinned Requests (${count})`;
|
||||||
}
|
}
|
||||||
|
|
@ -51,17 +49,24 @@ function formatHistoryTitle(count: number): string {
|
||||||
return `History (${count})`;
|
return `History (${count})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractMethod(entry: MessageEntry): string {
|
||||||
|
if ("method" in entry.message) {
|
||||||
|
return entry.message.method;
|
||||||
|
}
|
||||||
|
return "response";
|
||||||
|
}
|
||||||
|
|
||||||
function matchesFilters(
|
function matchesFilters(
|
||||||
entry: HistoryEntryProps,
|
entry: MessageEntry,
|
||||||
searchText: string,
|
searchText: string,
|
||||||
methodFilter?: string,
|
methodFilter?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (methodFilter && entry.method !== methodFilter) return false;
|
const method = extractMethod(entry);
|
||||||
|
if (methodFilter && method !== methodFilter) return false;
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const term = searchText.toLowerCase();
|
const term = searchText.toLowerCase();
|
||||||
const responseText = entry.response ? JSON.stringify(entry.response) : "";
|
|
||||||
const searchable =
|
const searchable =
|
||||||
`${entry.method} ${entry.target ?? ""} ${entry.timestamp} ${responseText}`.toLowerCase();
|
`${method} ${entry.id} ${JSON.stringify(entry.message)}`.toLowerCase();
|
||||||
if (!searchable.includes(term)) return false;
|
if (!searchable.includes(term)) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -69,12 +74,14 @@ function matchesFilters(
|
||||||
|
|
||||||
export function HistoryListPanel({
|
export function HistoryListPanel({
|
||||||
entries,
|
entries,
|
||||||
pinnedEntries,
|
pinnedIds,
|
||||||
searchText,
|
searchText,
|
||||||
methodFilter,
|
methodFilter,
|
||||||
onClearAll,
|
onClearAll,
|
||||||
onExport,
|
onExport,
|
||||||
}: HistoryRequestsPanelProps) {
|
onReplay,
|
||||||
|
onTogglePin,
|
||||||
|
}: HistoryListPanelProps) {
|
||||||
const [compact, setCompact] = useState(false);
|
const [compact, setCompact] = useState(false);
|
||||||
|
|
||||||
const filteredEntries = useMemo(
|
const filteredEntries = useMemo(
|
||||||
|
|
@ -82,13 +89,17 @@ export function HistoryListPanel({
|
||||||
[entries, searchText, methodFilter],
|
[entries, searchText, methodFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredPinned = useMemo(
|
const pinnedEntries = useMemo(
|
||||||
() =>
|
() => filteredEntries.filter((e) => pinnedIds.has(e.id)),
|
||||||
pinnedEntries.filter((e) => matchesFilters(e, searchText, methodFilter)),
|
[filteredEntries, pinnedIds],
|
||||||
[pinnedEntries, searchText, methodFilter],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasResults = filteredEntries.length > 0 || filteredPinned.length > 0;
|
const unpinnedEntries = useMemo(
|
||||||
|
() => filteredEntries.filter((e) => !pinnedIds.has(e.id)),
|
||||||
|
[filteredEntries, pinnedIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasResults = filteredEntries.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
|
|
@ -110,34 +121,40 @@ export function HistoryListPanel({
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea.Autosize mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)">
|
<ScrollArea.Autosize mah="calc(100vh - var(--app-shell-header-height, 0px) - 150px)">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{filteredPinned.length > 0 && (
|
{pinnedEntries.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Title order={5}>
|
<Title order={5}>
|
||||||
{formatPinnedTitle(filteredPinned.length)}
|
{formatPinnedTitle(pinnedEntries.length)}
|
||||||
</Title>
|
</Title>
|
||||||
{filteredPinned.map((entry) => (
|
{pinnedEntries.map((entry) => (
|
||||||
<HistoryEntry
|
<HistoryEntry
|
||||||
key={entryKey(entry)}
|
key={entry.id}
|
||||||
{...entry}
|
entry={entry}
|
||||||
|
isPinned={true}
|
||||||
isListExpanded={!compact}
|
isListExpanded={!compact}
|
||||||
|
onReplay={() => onReplay(entry.id)}
|
||||||
|
onTogglePin={() => onTogglePin(entry.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredEntries.length > 0 && (
|
{unpinnedEntries.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={5}>
|
<Title order={5}>
|
||||||
{formatHistoryTitle(filteredEntries.length)}
|
{formatHistoryTitle(unpinnedEntries.length)}
|
||||||
</Title>
|
</Title>
|
||||||
<ToolbarButton onClick={onClearAll}>Clear</ToolbarButton>
|
<ToolbarButton onClick={onClearAll}>Clear</ToolbarButton>
|
||||||
</Group>
|
</Group>
|
||||||
{filteredEntries.map((entry) => (
|
{unpinnedEntries.map((entry) => (
|
||||||
<HistoryEntry
|
<HistoryEntry
|
||||||
key={entryKey(entry)}
|
key={entry.id}
|
||||||
{...entry}
|
entry={entry}
|
||||||
|
isPinned={false}
|
||||||
isListExpanded={!compact}
|
isListExpanded={!compact}
|
||||||
|
onReplay={() => onReplay(entry.id)}
|
||||||
|
onTogglePin={() => onTogglePin(entry.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,126 @@
|
||||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||||
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||||
import { fn } from "storybook/test";
|
import { fn } from "storybook/test";
|
||||||
import { HistoryScreen } from "./HistoryScreen";
|
import { HistoryScreen } from "./HistoryScreen";
|
||||||
import type { HistoryEntryProps } from "../../groups/HistoryEntry/HistoryEntry";
|
|
||||||
|
|
||||||
const meta: Meta<typeof HistoryScreen> = {
|
const meta: Meta<typeof HistoryScreen> = {
|
||||||
title: "Screens/HistoryScreen",
|
title: "Screens/HistoryScreen",
|
||||||
component: HistoryScreen,
|
component: HistoryScreen,
|
||||||
parameters: { layout: "fullscreen" },
|
parameters: { layout: "fullscreen" },
|
||||||
args: {
|
args: {
|
||||||
|
pinnedIds: new Set<string>(),
|
||||||
onClearAll: fn(),
|
onClearAll: fn(),
|
||||||
onExport: fn(),
|
onExport: fn(),
|
||||||
|
onReplay: fn(),
|
||||||
|
onTogglePin: fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof HistoryScreen>;
|
type Story = StoryObj<typeof HistoryScreen>;
|
||||||
|
|
||||||
const entry1: HistoryEntryProps = {
|
const sampleEntries: MessageEntry[] = [
|
||||||
timestamp: "2026-03-17T10:00:00Z",
|
{
|
||||||
method: "tools/call",
|
id: "req-1",
|
||||||
target: "send_message",
|
timestamp: new Date("2026-03-17T10:00:00Z"),
|
||||||
status: "success",
|
direction: "request",
|
||||||
durationMs: 120,
|
message: {
|
||||||
parameters: { message: "Hello, world!" },
|
jsonrpc: "2.0",
|
||||||
response: { result: "Message sent successfully" },
|
id: 1,
|
||||||
isPinned: false,
|
method: "tools/call",
|
||||||
isListExpanded: false,
|
params: { name: "send_message", arguments: { message: "Hello, world!" } },
|
||||||
onReplay: fn(),
|
},
|
||||||
onTogglePin: fn(),
|
response: {
|
||||||
};
|
jsonrpc: "2.0",
|
||||||
|
id: 1,
|
||||||
const entry2: HistoryEntryProps = {
|
result: {
|
||||||
timestamp: "2026-03-17T10:01:00Z",
|
content: [{ type: "text", text: "Message sent successfully" }],
|
||||||
method: "resources/read",
|
},
|
||||||
target: "config.json",
|
},
|
||||||
status: "success",
|
duration: 120,
|
||||||
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!" } }],
|
|
||||||
},
|
},
|
||||||
isPinned: true,
|
{
|
||||||
isListExpanded: false,
|
id: "req-2",
|
||||||
onReplay: fn(),
|
timestamp: new Date("2026-03-17T10:01:00Z"),
|
||||||
onTogglePin: fn(),
|
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 = {
|
export const WithEntries: Story = {
|
||||||
args: {
|
args: {
|
||||||
entries: [entry1, entry2, entry3],
|
entries: sampleEntries,
|
||||||
pinnedEntries: [pinnedEntry1, pinnedEntry2],
|
pinnedIds: new Set(["req-4", "req-5"]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Empty: Story = {
|
export const Empty: Story = {
|
||||||
args: {
|
args: {
|
||||||
entries: [],
|
entries: [],
|
||||||
pinnedEntries: [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Card, Flex, Stack } from "@mantine/core";
|
import { Card, Flex, Stack } from "@mantine/core";
|
||||||
|
import type { MessageEntry } from "../../../../../../core/mcp/types.js";
|
||||||
import { HistoryControls } from "../../groups/HistoryControls/HistoryControls";
|
import { HistoryControls } from "../../groups/HistoryControls/HistoryControls";
|
||||||
import { HistoryListPanel } from "../../groups/HistoryListPanel/HistoryListPanel.js";
|
import { HistoryListPanel } from "../../groups/HistoryListPanel/HistoryListPanel.js";
|
||||||
import type { HistoryEntryProps } from "../../groups/HistoryEntry/HistoryEntry";
|
|
||||||
|
|
||||||
export interface HistoryScreenProps {
|
export interface HistoryScreenProps {
|
||||||
entries: HistoryEntryProps[];
|
entries: MessageEntry[];
|
||||||
pinnedEntries: HistoryEntryProps[];
|
pinnedIds: Set<string>;
|
||||||
onClearAll: () => void;
|
onClearAll: () => void;
|
||||||
onExport: () => void;
|
onExport: () => void;
|
||||||
|
onReplay: (id: string) => void;
|
||||||
|
onTogglePin: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ScreenLayout = Flex.withProps({
|
const ScreenLayout = Flex.withProps({
|
||||||
|
|
@ -30,9 +32,11 @@ const SidebarCard = Card.withProps({
|
||||||
|
|
||||||
export function HistoryScreen({
|
export function HistoryScreen({
|
||||||
entries,
|
entries,
|
||||||
pinnedEntries,
|
pinnedIds,
|
||||||
onClearAll,
|
onClearAll,
|
||||||
onExport,
|
onExport,
|
||||||
|
onReplay,
|
||||||
|
onTogglePin,
|
||||||
}: HistoryScreenProps) {
|
}: HistoryScreenProps) {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [methodFilter, setMethodFilter] = useState<string | undefined>();
|
const [methodFilter, setMethodFilter] = useState<string | undefined>();
|
||||||
|
|
@ -45,19 +49,19 @@ export function HistoryScreen({
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
methodFilter={methodFilter}
|
methodFilter={methodFilter}
|
||||||
onSearchChange={setSearchText}
|
onSearchChange={setSearchText}
|
||||||
onMethodFilterChange={(value) =>
|
onMethodFilterChange={setMethodFilter}
|
||||||
setMethodFilter(value || undefined)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</SidebarCard>
|
</SidebarCard>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<HistoryListPanel
|
<HistoryListPanel
|
||||||
entries={entries}
|
entries={entries}
|
||||||
pinnedEntries={pinnedEntries}
|
pinnedIds={pinnedIds}
|
||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
methodFilter={methodFilter}
|
methodFilter={methodFilter}
|
||||||
onClearAll={onClearAll}
|
onClearAll={onClearAll}
|
||||||
onExport={onExport}
|
onExport={onExport}
|
||||||
|
onReplay={onReplay}
|
||||||
|
onTogglePin={onTogglePin}
|
||||||
/>
|
/>
|
||||||
</ScreenLayout>
|
</ScreenLayout>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -599,79 +599,98 @@ export const HistoryActive: Story = {
|
||||||
<HistoryScreen
|
<HistoryScreen
|
||||||
entries={[
|
entries={[
|
||||||
{
|
{
|
||||||
timestamp: "2026-03-17T10:00:00Z",
|
id: "req-1",
|
||||||
method: "tools/call",
|
timestamp: new Date("2026-03-17T10:00:00Z"),
|
||||||
target: "send_message",
|
direction: "request" as const,
|
||||||
status: "success",
|
message: {
|
||||||
durationMs: 120,
|
jsonrpc: "2.0" as const,
|
||||||
parameters: { message: "Hello, world!" },
|
id: 1,
|
||||||
response: { result: "Message sent successfully" },
|
method: "tools/call",
|
||||||
isPinned: false,
|
params: {
|
||||||
isListExpanded: false,
|
name: "send_message",
|
||||||
onReplay: fn(),
|
arguments: { message: "Hello, world!" },
|
||||||
onTogglePin: fn(),
|
},
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
response: {
|
||||||
isListExpanded: false,
|
jsonrpc: "2.0" as const,
|
||||||
onReplay: fn(),
|
id: 1,
|
||||||
onTogglePin: fn(),
|
result: {
|
||||||
|
content: [{ type: "text", text: "Message sent successfully" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duration: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: "2026-03-17T10:02:00Z",
|
id: "req-2",
|
||||||
method: "tools/call",
|
timestamp: new Date("2026-03-17T10:01:00Z"),
|
||||||
target: "delete_records",
|
direction: "request" as const,
|
||||||
status: "error",
|
message: {
|
||||||
durationMs: 350,
|
jsonrpc: "2.0" as const,
|
||||||
parameters: { ids: [1, 2, 3] },
|
id: 2,
|
||||||
response: { error: "Permission denied" },
|
method: "resources/read",
|
||||||
isPinned: false,
|
params: { uri: "file:///config.json" },
|
||||||
isListExpanded: true,
|
},
|
||||||
onReplay: fn(),
|
response: {
|
||||||
onTogglePin: fn(),
|
jsonrpc: "2.0" as const,
|
||||||
},
|
id: 2,
|
||||||
]}
|
result: {
|
||||||
pinnedEntries={[
|
contents: [{ uri: "file:///config.json", text: "{}" }],
|
||||||
{
|
},
|
||||||
timestamp: "2026-03-17T09:30:00Z",
|
},
|
||||||
method: "tools/list",
|
duration: 45,
|
||||||
status: "success",
|
},
|
||||||
durationMs: 80,
|
{
|
||||||
response: { tools: ["send_message", "list_users"] },
|
id: "req-3",
|
||||||
isPinned: true,
|
timestamp: new Date("2026-03-17T10:02:00Z"),
|
||||||
isListExpanded: false,
|
direction: "request" as const,
|
||||||
onReplay: fn(),
|
message: {
|
||||||
onTogglePin: fn(),
|
jsonrpc: "2.0" as const,
|
||||||
},
|
id: 3,
|
||||||
{
|
method: "tools/call",
|
||||||
timestamp: "2026-03-17T09:35:00Z",
|
params: { name: "delete_records", arguments: { ids: [1, 2, 3] } },
|
||||||
method: "prompts/get",
|
},
|
||||||
target: "greeting",
|
response: {
|
||||||
status: "success",
|
jsonrpc: "2.0" as const,
|
||||||
durationMs: 60,
|
id: 3,
|
||||||
parameters: { name: "greeting" },
|
error: { code: -32000, message: "Permission denied" },
|
||||||
response: {
|
},
|
||||||
messages: [
|
duration: 350,
|
||||||
{ role: "user", content: { type: "text", text: "Hello!" } },
|
},
|
||||||
],
|
{
|
||||||
},
|
id: "req-4",
|
||||||
isPinned: true,
|
timestamp: new Date("2026-03-17T09:30:00Z"),
|
||||||
isListExpanded: false,
|
direction: "request" as const,
|
||||||
onReplay: fn(),
|
message: { jsonrpc: "2.0" as const, id: 4, method: "tools/list" },
|
||||||
onTogglePin: fn(),
|
response: { jsonrpc: "2.0" as const, id: 4, result: { tools: [] } },
|
||||||
|
duration: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "req-5",
|
||||||
|
timestamp: new Date("2026-03-17T09:35:00Z"),
|
||||||
|
direction: "request" as const,
|
||||||
|
message: {
|
||||||
|
jsonrpc: "2.0" as const,
|
||||||
|
id: 5,
|
||||||
|
method: "prompts/get",
|
||||||
|
params: { name: "greeting" },
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
jsonrpc: "2.0" as const,
|
||||||
|
id: 5,
|
||||||
|
result: {
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: { type: "text", text: "Hello!" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
duration: 60,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
pinnedIds={new Set(["req-4", "req-5"])}
|
||||||
onClearAll={fn()}
|
onClearAll={fn()}
|
||||||
onExport={fn()}
|
onExport={fn()}
|
||||||
|
onReplay={fn()}
|
||||||
|
onTogglePin={fn()}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue