ToolsScreen, PromptsScreen, and ResourcesScreen each tracked the user's current selection via `selectedXxxName` / `onSelectXxx` props, hoisting screen-local UI state into whatever rendered them. InspectorView never threaded those callbacks anywhere, so clicks in the controls panel did not display the matching detail panel — the detail panel only rendered if a parent fed selection back in. Move selection into `useState` inside each screen and drop the `selectedXxxName` / `onSelectXxx` props. MCP-derived data (`callState`, `getPromptState`, `readState`) keeps flowing in from `InspectorView` as props. ResourcesScreen wraps `onReadResource` so a template-expanded URI becomes the selected resource for preview rendering. Update each screen's stories to drop the deleted props. Stories that need a pre-selected item use a `play` function to click it — no component coupling to Storybook. Add Phase 4.3 to the interfaces plan describing the rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
4 KiB
TypeScript
151 lines
4 KiB
TypeScript
import { useState } from "react";
|
|
import {
|
|
Alert,
|
|
Card,
|
|
Flex,
|
|
Group,
|
|
Loader,
|
|
ScrollArea,
|
|
Stack,
|
|
Text,
|
|
} from "@mantine/core";
|
|
import type {
|
|
GetPromptResult,
|
|
Prompt,
|
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
import { PromptControls } from "../../groups/PromptControls/PromptControls";
|
|
import { PromptArgumentsForm } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
|
|
import { PromptMessagesDisplay } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
|
|
|
|
export interface GetPromptState {
|
|
status: "idle" | "pending" | "ok" | "error";
|
|
result?: GetPromptResult;
|
|
error?: string;
|
|
}
|
|
|
|
export interface PromptsScreenProps {
|
|
prompts: Prompt[];
|
|
getPromptState?: GetPromptState;
|
|
listChanged: boolean;
|
|
onRefreshList: () => void;
|
|
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
|
onCopyMessages?: () => void;
|
|
}
|
|
|
|
const ScreenLayout = Flex.withProps({
|
|
variant: "screen",
|
|
h: "calc(100vh - var(--app-shell-header-height, 0px))",
|
|
gap: "md",
|
|
p: "xl",
|
|
});
|
|
|
|
const Sidebar = Stack.withProps({
|
|
w: 340,
|
|
flex: "0 0 auto",
|
|
});
|
|
|
|
const SidebarCard = Card.withProps({
|
|
withBorder: true,
|
|
padding: "lg",
|
|
});
|
|
|
|
const DetailCard = Card.withProps({
|
|
flex: 1,
|
|
withBorder: true,
|
|
padding: "lg",
|
|
});
|
|
|
|
const EmptyState = Text.withProps({
|
|
c: "dimmed",
|
|
ta: "center",
|
|
py: "xl",
|
|
});
|
|
|
|
export function PromptsScreen({
|
|
prompts,
|
|
getPromptState,
|
|
listChanged,
|
|
onRefreshList,
|
|
onGetPrompt,
|
|
onCopyMessages,
|
|
}: PromptsScreenProps) {
|
|
const [selectedPromptName, setSelectedPromptName] = useState<
|
|
string | undefined
|
|
>(undefined);
|
|
const [argumentValues, setArgumentValues] = useState<Record<string, string>>(
|
|
{},
|
|
);
|
|
const selectedPrompt = selectedPromptName
|
|
? prompts.find((p) => p.name === selectedPromptName)
|
|
: undefined;
|
|
|
|
return (
|
|
<ScreenLayout>
|
|
<Sidebar>
|
|
<SidebarCard>
|
|
<PromptControls
|
|
prompts={prompts}
|
|
selectedName={selectedPromptName}
|
|
listChanged={listChanged}
|
|
onRefreshList={onRefreshList}
|
|
onSelectPrompt={(name) => {
|
|
setArgumentValues({});
|
|
setSelectedPromptName(name);
|
|
}}
|
|
/>
|
|
</SidebarCard>
|
|
</Sidebar>
|
|
|
|
<ScrollArea.Autosize
|
|
flex={1}
|
|
mah="calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)"
|
|
>
|
|
<Group flex={1} gap="md" align="flex-start" wrap="nowrap">
|
|
{selectedPrompt ? (
|
|
<>
|
|
<DetailCard>
|
|
<PromptArgumentsForm
|
|
prompt={selectedPrompt}
|
|
argumentValues={argumentValues}
|
|
onArgumentChange={(name, value) =>
|
|
setArgumentValues((prev) => ({ ...prev, [name]: value }))
|
|
}
|
|
onGetPrompt={() =>
|
|
onGetPrompt(selectedPrompt.name, argumentValues)
|
|
}
|
|
/>
|
|
</DetailCard>
|
|
{getPromptState?.status === "pending" && (
|
|
<DetailCard>
|
|
<Stack align="center" py="xl">
|
|
<Loader size="sm" />
|
|
<Text c="dimmed">Loading prompt...</Text>
|
|
</Stack>
|
|
</DetailCard>
|
|
)}
|
|
{getPromptState?.status === "error" && (
|
|
<DetailCard>
|
|
<Alert color="red" variant="light" title="Prompt Error">
|
|
{getPromptState.error ?? "Failed to get prompt"}
|
|
</Alert>
|
|
</DetailCard>
|
|
)}
|
|
{getPromptState?.result && (
|
|
<DetailCard>
|
|
<PromptMessagesDisplay
|
|
messages={getPromptState.result.messages}
|
|
onCopyAll={onCopyMessages}
|
|
/>
|
|
</DetailCard>
|
|
)}
|
|
</>
|
|
) : (
|
|
<DetailCard>
|
|
<EmptyState>Select a prompt to view details</EmptyState>
|
|
</DetailCard>
|
|
)}
|
|
</Group>
|
|
</ScrollArea.Autosize>
|
|
</ScreenLayout>
|
|
);
|
|
}
|