Phase 2: Refactor Prompts cluster to MCP SDK types

Replace ad hoc local types with MCP SDK schema types across the Prompts
group components and PromptsScreen, per the v2 UX interfaces plan.

- PromptListItem: accept `prompt: Prompt` instead of flat `name`/`description`;
  render `title ?? name` as primary label
- PromptControls: accept `Prompt[]` + `selectedName` instead of `PromptItem[]`;
  no-op on clicking already-selected prompt; search includes title field
- PromptArgumentsForm: accept `prompt: Prompt` instead of flat props;
  read `prompt.arguments` directly; display `title ?? name` as heading
- PromptMessagesDisplay: narrow type from `(PromptMessage | SamplingMessage)[]`
  to `PromptMessage[]` per spec
- PromptsScreen: accept `Prompt[]` + `selectedPromptName` + `GetPromptState`;
  derive selected prompt via `prompts.find()`; own `argumentValues` state
  locally; reset arguments on tool switch
- Delete local `PromptItem`, `SelectedPrompt`, `PromptArgument` types
- Update all stories with real SDK `Prompt` fixtures
- Update ConnectedView PromptsActive story

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-04-18 17:49:27 -04:00
commit de40b3720e
11 changed files with 230 additions and 226 deletions

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { PromptArgumentsForm } from "./PromptArgumentsForm";
@ -14,64 +15,73 @@ const meta: Meta<typeof PromptArgumentsForm> = {
export default meta;
type Story = StoryObj<typeof PromptArgumentsForm>;
const summarizePrompt: Prompt = {
name: "summarize",
description: "Summarize the given text into key points",
};
const translatePrompt: Prompt = {
name: "translate",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "The language to translate into",
},
],
};
const summarizeWithArgsPrompt: Prompt = {
name: "summarize",
description: "Summarize the given text into key points",
arguments: [
{ name: "text", required: true, description: "The text to summarize" },
{
name: "maxLength",
required: true,
description: "Maximum length of summary in words",
},
{
name: "format",
required: false,
description: "Output format (bullets or paragraph)",
},
],
};
const codeReviewPrompt: Prompt = {
name: "code-review",
arguments: [
{ name: "code", required: true, description: "The code to review" },
],
};
export const NoArguments: Story = {
args: {
name: "summarize",
description: "Summarize the given text into key points",
arguments: [],
prompt: summarizePrompt,
argumentValues: {},
},
};
export const WithArguments: Story = {
args: {
name: "translate",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "The language to translate into",
},
],
prompt: translatePrompt,
argumentValues: {},
},
};
export const WithRequiredAndOptional: Story = {
args: {
name: "summarize",
description: "Summarize the given text into key points",
arguments: [
{ name: "text", required: true, description: "The text to summarize" },
{
name: "maxLength",
required: true,
description: "Maximum length of summary in words",
},
{
name: "format",
required: false,
description: "Output format (bullets or paragraph)",
},
],
prompt: summarizeWithArgsPrompt,
argumentValues: {},
},
};
export const AllFilled: Story = {
args: {
name: "translate",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "The language to translate into",
},
],
prompt: translatePrompt,
argumentValues: {
text: "Hello, how are you?",
targetLanguage: "Spanish",
@ -81,10 +91,7 @@ export const AllFilled: Story = {
export const NoDescription: Story = {
args: {
name: "code-review",
arguments: [
{ name: "code", required: true, description: "The code to review" },
],
prompt: codeReviewPrompt,
argumentValues: {},
},
};

View file

@ -1,20 +1,19 @@
import { Button, Group, Stack, Text, TextInput, Title } from "@mantine/core";
export interface PromptArgument {
name: string;
required: boolean;
description?: string;
}
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
export interface PromptArgumentsFormProps {
name: string;
description?: string;
arguments: PromptArgument[];
prompt: Prompt;
argumentValues: Record<string, string>;
onArgumentChange: (name: string, value: string) => void;
onGetPrompt: () => void;
}
const PromptTitle = Text.withProps({
fw: 700,
size: "lg",
truncate: "end",
});
const DescriptionText = Text.withProps({
size: "sm",
c: "dimmed",
@ -25,23 +24,25 @@ function formatPlaceholder(name: string): string {
}
export function PromptArgumentsForm({
description,
arguments: promptArguments,
prompt,
argumentValues,
onArgumentChange,
onGetPrompt,
}: PromptArgumentsFormProps) {
const { name, title, description, arguments: promptArguments } = prompt;
return (
<Stack gap="md">
<Title order={4}>Prompt Arguments</Title>
<PromptTitle>{title ?? name}</PromptTitle>
{description && <DescriptionText>{description}</DescriptionText>}
{promptArguments.length > 0 && (
<Title order={4}>Arguments</Title>
{promptArguments && promptArguments.length > 0 && (
<Stack gap="sm">
{promptArguments.map((arg) => (
<TextInput
key={arg.name}
label={arg.name}
withAsterisk={arg.required}
withAsterisk={arg.required === true}
description={arg.description}
placeholder={formatPlaceholder(arg.name)}
value={argumentValues[arg.name] || ""}

View file

@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { PromptControls } from "./PromptControls";
@ -15,28 +16,24 @@ const meta: Meta<typeof PromptControls> = {
export default meta;
type Story = StoryObj<typeof PromptControls>;
const samplePrompts = [
const samplePrompts: Prompt[] = [
{
name: "summarize",
description: "Summarize the given text into key points",
selected: false,
},
{
name: "translate",
description: "Translate text from one language to another",
selected: false,
},
{
name: "analyze",
description: "Analyze sentiment and tone of the text",
selected: false,
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
{ name: "refactor", selected: false },
{ name: "refactor" },
];
export const Default: Story = {
@ -47,9 +44,8 @@ export const Default: Story = {
export const WithSelection: Story = {
args: {
prompts: samplePrompts.map((p) =>
p.name === "translate" ? { ...p, selected: true } : p,
),
prompts: samplePrompts,
selectedName: "translate",
},
};

View file

@ -1,22 +1,23 @@
import { useState } from "react";
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { PromptListItem } from "../PromptListItem/PromptListItem";
import type { PromptItem } from "../../screens/PromptsScreen/PromptsScreen";
export interface PromptControlsProps {
prompts: PromptItem[];
prompts: Prompt[];
selectedName?: string;
listChanged: boolean;
onRefreshList: () => void;
onSelectPrompt: (name: string) => void;
}
function listMaxHeight(): string {
return "calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
}
const LIST_MAX_HEIGHT =
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
export function PromptControls({
prompts,
selectedName,
listChanged,
onRefreshList,
onSelectPrompt,
@ -26,6 +27,7 @@ export function PromptControls({
const filteredPrompts = prompts.filter(
(p) =>
p.name.toLowerCase().includes(query) ||
(p.title?.toLowerCase().includes(query) ?? false) ||
(p.description?.toLowerCase().includes(query) ?? false),
);
@ -40,15 +42,16 @@ export function PromptControls({
value={searchText}
onChange={(e) => setSearchText(e.currentTarget.value)}
/>
<ScrollArea.Autosize mah={listMaxHeight()}>
<ScrollArea.Autosize mah={LIST_MAX_HEIGHT}>
<Stack gap="xs">
{filteredPrompts.map((prompt) => (
<PromptListItem
key={prompt.name}
name={prompt.name}
description={prompt.description}
selected={prompt.selected}
onClick={() => onSelectPrompt(prompt.name)}
prompt={prompt}
selected={prompt.name === selectedName}
onClick={() => {
if (prompt.name !== selectedName) onSelectPrompt(prompt.name);
}}
/>
))}
</Stack>

View file

@ -15,23 +15,40 @@ type Story = StoryObj<typeof PromptListItem>;
export const Default: Story = {
args: {
name: "summarize",
description: "Summarize the given text into key points",
prompt: {
name: "summarize",
description: "Summarize the given text into key points",
},
selected: false,
},
};
export const Selected: Story = {
args: {
name: "translate",
description: "Translate text from one language to another",
prompt: {
name: "translate",
description: "Translate text from one language to another",
},
selected: true,
},
};
export const WithTitle: Story = {
args: {
prompt: {
name: "code-review",
title: "Code Review",
description: "Review code for issues and best practices",
},
selected: false,
},
};
export const NoDescription: Story = {
args: {
name: "code-review",
prompt: {
name: "code-review",
},
selected: false,
},
};

View file

@ -1,8 +1,8 @@
import { Stack, Text, UnstyledButton } from "@mantine/core";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
export interface PromptListItemProps {
name: string;
description?: string;
prompt: Prompt;
selected: boolean;
onClick: () => void;
}
@ -18,11 +18,11 @@ const DescriptionText = Text.withProps({
});
export function PromptListItem({
name,
description,
prompt,
selected,
onClick,
}: PromptListItemProps) {
const { name, title, description } = prompt;
return (
<UnstyledButton
w="100%"
@ -32,7 +32,7 @@ export function PromptListItem({
onClick={onClick}
>
<Stack gap={2}>
<NameText>{name}</NameText>
<NameText>{title ?? name}</NameText>
{description && <DescriptionText>{description}</DescriptionText>}
</Stack>
</UnstyledButton>

View file

@ -52,17 +52,11 @@ export const WithImage: Story = {
messages: [
{
role: "user",
content: [
{
type: "text",
text: "Here is a photo for you to analyze.",
},
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
],
content: {
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
mimeType: "image/png",
},
},
],
},

View file

@ -1,20 +1,17 @@
import { Button, Group, Stack, Text, Title } from "@mantine/core";
import type {
PromptMessage,
SamplingMessage,
} from "@modelcontextprotocol/sdk/types.js";
import type { PromptMessage } from "@modelcontextprotocol/sdk/types.js";
import { MessageBubble } from "../../elements/MessageBubble/MessageBubble";
export interface PromptMessagesDisplayProps {
messages: PromptMessage[];
onCopyAll?: () => void;
}
const CopyAllButton = Button.withProps({
variant: "subtle",
size: "sm",
});
export interface PromptMessagesDisplayProps {
messages: (PromptMessage | SamplingMessage)[];
onCopyAll?: () => void;
}
export function PromptMessagesDisplay({
messages,
onCopyAll,

View file

@ -1,7 +1,8 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { fn } from "storybook/test";
import { PromptsScreen } from "./PromptsScreen";
import type { PromptItem, SelectedPrompt } from "./PromptsScreen";
import type { GetPromptState } from "./PromptsScreen";
const meta: Meta<typeof PromptsScreen> = {
title: "Screens/PromptsScreen",
@ -10,8 +11,8 @@ const meta: Meta<typeof PromptsScreen> = {
args: {
onRefreshList: fn(),
onSelectPrompt: fn(),
onArgumentChange: fn(),
onGetPrompt: fn(),
onCopyMessages: fn(),
listChanged: false,
},
};
@ -19,42 +20,54 @@ const meta: Meta<typeof PromptsScreen> = {
export default meta;
type Story = StoryObj<typeof PromptsScreen>;
const samplePrompts: PromptItem[] = [
const samplePrompts: Prompt[] = [
{
name: "summarize",
description: "Summarize the given text into key points",
selected: false,
},
{
name: "translate",
description: "Translate text from one language to another",
selected: false,
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
{
name: "analyze",
description: "Analyze sentiment and tone of the text",
selected: false,
},
{ name: "refactor", selected: false },
{ name: "refactor" },
];
const selectedTranslate: SelectedPrompt = {
name: "translate",
description: "Translate text from one language to another",
arguments: [
{ name: "text", required: true, description: "The text to translate" },
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
argumentValues: {},
const translateResult: GetPromptState = {
status: "ok",
result: {
messages: [
{
role: "user",
content: {
type: "text",
text: 'Translate the following text to Spanish: "Hello, how are you?"',
},
},
{
role: "assistant",
content: {
type: "text",
text: "Hola, como estas?",
},
},
],
},
};
export const NoSelection: Story = {
@ -65,44 +78,16 @@ export const NoSelection: Story = {
export const PromptSelected: Story = {
args: {
prompts: samplePrompts.map((p) =>
p.name === "translate" ? { ...p, selected: true } : p,
),
selectedPrompt: selectedTranslate,
prompts: samplePrompts,
selectedPromptName: "translate",
},
};
export const WithResult: Story = {
args: {
prompts: samplePrompts.map((p) =>
p.name === "translate" ? { ...p, selected: true } : p,
),
selectedPrompt: {
...selectedTranslate,
argumentValues: {
text: "Hello, how are you?",
targetLanguage: "es",
},
},
messages: {
onCopyAll: fn(),
messages: [
{
role: "user",
content: {
type: "text",
text: 'Translate the following text to Spanish: "Hello, how are you?"',
},
},
{
role: "assistant",
content: {
type: "text",
text: "Hola, como estas?",
},
},
],
},
prompts: samplePrompts,
selectedPromptName: "translate",
getPromptState: translateResult,
},
};

View file

@ -1,32 +1,28 @@
import { useState } from "react";
import { Card, Flex, Group, 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";
import type { PromptArgument } from "../../groups/PromptArgumentsForm/PromptArgumentsForm";
import type { PromptMessagesDisplayProps } from "../../groups/PromptMessagesDisplay/PromptMessagesDisplay";
export interface PromptItem {
name: string;
description?: string;
selected: boolean;
}
export interface SelectedPrompt {
name: string;
description?: string;
arguments: PromptArgument[];
argumentValues: Record<string, string>;
export interface GetPromptState {
status: "idle" | "pending" | "ok" | "error";
result?: GetPromptResult;
error?: string;
}
export interface PromptsScreenProps {
prompts: PromptItem[];
selectedPrompt?: SelectedPrompt;
messages?: PromptMessagesDisplayProps;
prompts: Prompt[];
selectedPromptName?: string;
getPromptState?: GetPromptState;
listChanged: boolean;
onRefreshList: () => void;
onSelectPrompt: (name: string) => void;
onArgumentChange: (name: string, value: string) => void;
onGetPrompt: () => void;
onGetPrompt: (name: string, args: Record<string, string>) => void;
onCopyMessages?: () => void;
}
const ScreenLayout = Flex.withProps({
@ -60,23 +56,34 @@ const EmptyState = Text.withProps({
export function PromptsScreen({
prompts,
selectedPrompt,
messages,
selectedPromptName,
getPromptState,
listChanged,
onRefreshList,
onSelectPrompt,
onArgumentChange,
onGetPrompt,
onCopyMessages,
}: PromptsScreenProps) {
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={onSelectPrompt}
onSelectPrompt={(name) => {
setArgumentValues({});
onSelectPrompt(name);
}}
/>
</SidebarCard>
</Sidebar>
@ -90,17 +97,22 @@ export function PromptsScreen({
<>
<DetailCard>
<PromptArgumentsForm
name={selectedPrompt.name}
description={selectedPrompt.description}
arguments={selectedPrompt.arguments}
argumentValues={selectedPrompt.argumentValues}
onArgumentChange={onArgumentChange}
onGetPrompt={onGetPrompt}
prompt={selectedPrompt}
argumentValues={argumentValues}
onArgumentChange={(name, value) =>
setArgumentValues((prev) => ({ ...prev, [name]: value }))
}
onGetPrompt={() =>
onGetPrompt(selectedPrompt.name, argumentValues)
}
/>
</DetailCard>
{messages && (
{getPromptState?.result && (
<DetailCard>
<PromptMessagesDisplay {...messages} />
<PromptMessagesDisplay
messages={getPromptState.result.messages}
onCopyAll={onCopyMessages}
/>
</DetailCard>
)}
</>

View file

@ -594,60 +594,52 @@ export const PromptsActive: Story = {
{
name: "summarize",
description: "Summarize a document",
selected: false,
},
{
name: "translate",
description: "Translate text to another language",
selected: true,
arguments: [
{
name: "text",
required: true,
description: "The text to translate",
},
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
},
{
name: "code-review",
description: "Review code for issues",
selected: false,
},
]}
selectedPrompt={{
name: "translate",
description: "Translate text to another language",
arguments: [
{
name: "text",
required: true,
description: "The text to translate",
},
{
name: "targetLanguage",
required: true,
description: "Target language code",
},
],
argumentValues: {
text: "Hello, how are you?",
targetLanguage: "es",
},
}}
messages={{
onCopyAll: fn(),
messages: [
{
role: "user",
content: {
type: "text",
text: 'Translate the following text to Spanish: "Hello, how are you?"',
selectedPromptName="translate"
getPromptState={{
status: "ok",
result: {
messages: [
{
role: "user",
content: {
type: "text",
text: 'Translate the following text to Spanish: "Hello, how are you?"',
},
},
},
{
role: "assistant",
content: { type: "text", text: "Hola, como estas?" },
},
],
{
role: "assistant",
content: { type: "text", text: "Hola, como estas?" },
},
],
},
}}
listChanged={true}
onRefreshList={fn()}
onSelectPrompt={fn()}
onArgumentChange={fn()}
onGetPrompt={fn()}
onCopyMessages={fn()}
/>
),
},