From de40b3720eeb10d7733446a67f865d8924126b3c Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 18 Apr 2026 17:49:27 -0400 Subject: [PATCH] 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) --- .../PromptArgumentsForm.stories.tsx | 91 ++++++++++--------- .../PromptArgumentsForm.tsx | 29 +++--- .../PromptControls/PromptControls.stories.tsx | 14 +-- .../groups/PromptControls/PromptControls.tsx | 23 +++-- .../PromptListItem/PromptListItem.stories.tsx | 27 +++++- .../groups/PromptListItem/PromptListItem.tsx | 10 +- .../PromptMessagesDisplay.stories.tsx | 16 +--- .../PromptMessagesDisplay.tsx | 15 ++- .../PromptsScreen/PromptsScreen.stories.tsx | 91 ++++++++----------- .../screens/PromptsScreen/PromptsScreen.tsx | 72 +++++++++------ .../ConnectedView/ConnectedView.stories.tsx | 68 ++++++-------- 11 files changed, 230 insertions(+), 226 deletions(-) diff --git a/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.stories.tsx b/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.stories.tsx index 74c85e00..3cc01bac 100644 --- a/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.stories.tsx +++ b/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.stories.tsx @@ -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 = { export default meta; type Story = StoryObj; +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: {}, }, }; diff --git a/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx b/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx index a75ba8d4..f09258f8 100644 --- a/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx +++ b/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx @@ -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; 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 ( - Prompt Arguments + {title ?? name} {description && {description}} - {promptArguments.length > 0 && ( + Arguments + {promptArguments && promptArguments.length > 0 && ( {promptArguments.map((arg) => ( = { export default meta; type Story = StoryObj; -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", }, }; diff --git a/clients/web/src/components/groups/PromptControls/PromptControls.tsx b/clients/web/src/components/groups/PromptControls/PromptControls.tsx index 68ea074c..a945db46 100644 --- a/clients/web/src/components/groups/PromptControls/PromptControls.tsx +++ b/clients/web/src/components/groups/PromptControls/PromptControls.tsx @@ -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)} /> - + {filteredPrompts.map((prompt) => ( onSelectPrompt(prompt.name)} + prompt={prompt} + selected={prompt.name === selectedName} + onClick={() => { + if (prompt.name !== selectedName) onSelectPrompt(prompt.name); + }} /> ))} diff --git a/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx b/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx index baadb52d..231cbb5a 100644 --- a/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx +++ b/clients/web/src/components/groups/PromptListItem/PromptListItem.stories.tsx @@ -15,23 +15,40 @@ type Story = StoryObj; 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, }, }; diff --git a/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx b/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx index c4debda5..85babf38 100644 --- a/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx +++ b/clients/web/src/components/groups/PromptListItem/PromptListItem.tsx @@ -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 ( - {name} + {title ?? name} {description && {description}} diff --git a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx index 78579649..674eed59 100644 --- a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx +++ b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.stories.tsx @@ -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", + }, }, ], }, diff --git a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx index 9fb2be94..0c882edd 100644 --- a/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx +++ b/clients/web/src/components/groups/PromptMessagesDisplay/PromptMessagesDisplay.tsx @@ -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, diff --git a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx index f3838aa1..4474582f 100644 --- a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx +++ b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.stories.tsx @@ -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 = { title: "Screens/PromptsScreen", @@ -10,8 +11,8 @@ const meta: Meta = { args: { onRefreshList: fn(), onSelectPrompt: fn(), - onArgumentChange: fn(), onGetPrompt: fn(), + onCopyMessages: fn(), listChanged: false, }, }; @@ -19,42 +20,54 @@ const meta: Meta = { export default meta; type Story = StoryObj; -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, }, }; diff --git a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx index 903c1416..634faefe 100644 --- a/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx +++ b/clients/web/src/components/screens/PromptsScreen/PromptsScreen.tsx @@ -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; +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) => 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>( + {}, + ); + const selectedPrompt = selectedPromptName + ? prompts.find((p) => p.name === selectedPromptName) + : undefined; + return ( { + setArgumentValues({}); + onSelectPrompt(name); + }} /> @@ -90,17 +97,22 @@ export function PromptsScreen({ <> + setArgumentValues((prev) => ({ ...prev, [name]: value })) + } + onGetPrompt={() => + onGetPrompt(selectedPrompt.name, argumentValues) + } /> - {messages && ( + {getPromptState?.result && ( - + )} diff --git a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx index fd8baa01..ba8f85b9 100644 --- a/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx +++ b/clients/web/src/components/views/ConnectedView/ConnectedView.stories.tsx @@ -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()} /> ), },