diff --git a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx index 9dbee5daa..c53cd81c5 100644 --- a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx +++ b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.stories.tsx @@ -71,6 +71,26 @@ const longQueryTool: Tool = { }, }; +const ICON_DATA_URL = + "data:image/svg+xml;utf8," + + encodeURIComponent( + '', + ); + +const iconedTool: Tool = { + name: "send_message", + title: "Send Message", + description: "Sends a message to the recipient", + icons: [{ src: ICON_DATA_URL }], + inputSchema: { + type: "object", + properties: { + message: { type: "string", description: "The message to send" }, + }, + required: ["message"], + }, +}; + const batchProcessTool: Tool = { name: "batch_process", description: "Processes items in batch", @@ -94,6 +114,12 @@ export const MultipleParams: Story = { }, }; +export const WithIcon: Story = { + args: { + tool: iconedTool, + }, +}; + export const WithAnnotations: Story = { args: { tool: deleteRecordsTool, diff --git a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.test.tsx b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.test.tsx index 8c87f98bf..95a93ad3e 100644 --- a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.test.tsx +++ b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.test.tsx @@ -27,6 +27,18 @@ const titledTool: Tool = { }, }; +const ICON_SRC = "data:image/svg+xml,%3Csvg/%3E"; + +const iconedTool: Tool = { + name: "send_message", + title: "Send Message", + icons: [{ src: ICON_SRC }], + inputSchema: { + type: "object", + properties: {}, + }, +}; + const annotatedTool: Tool = { name: "delete_records", description: "Deletes records", @@ -66,6 +78,17 @@ describe("ToolDetailPanel", () => { ).toBeInTheDocument(); }); + it("renders the first icon when tool.icons is present", () => { + renderWithMantine(); + const img = screen.getByRole("presentation"); + expect(img).toHaveAttribute("src", ICON_SRC); + }); + + it("does not render an icon when tool.icons is missing", () => { + renderWithMantine(); + expect(screen.queryByRole("presentation")).not.toBeInTheDocument(); + }); + it("does not render the description when none is provided", () => { renderWithMantine(); expect( diff --git a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx index 18780ee36..a3f5be717 100644 --- a/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx +++ b/clients/web/src/components/groups/ToolDetailPanel/ToolDetailPanel.tsx @@ -1,4 +1,4 @@ -import { Button, Divider, Group, Stack, Text } from "@mantine/core"; +import { Button, Divider, Group, Image, Stack, Text } from "@mantine/core"; import type { ProgressNotification, Tool, @@ -24,6 +24,19 @@ export interface ToolDetailPanelProps { onCancel: () => void; } +const TitleRow = Group.withProps({ + gap: "sm", + wrap: "nowrap", + align: "center", + miw: 0, +}); + +const ToolIcon = Image.withProps({ + w: 24, + h: 24, + fit: "contain", +}); + const ToolTitle = Text.withProps({ fw: 700, size: "lg", @@ -59,11 +72,15 @@ export function ToolDetailPanel({ onExecute, onCancel, }: ToolDetailPanelProps) { - const { name, title, description, annotations, inputSchema } = tool; + const { name, title, description, icons, annotations, inputSchema } = tool; + const iconSrc = icons?.[0]?.src; return ( - {resolveDisplayLabel(name, title)} + + {iconSrc && } + {resolveDisplayLabel(name, title)} + {hasAnyAnnotation(annotations) && annotations && ( {annotations.readOnlyHint && ( diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx index 86936a46d..e9bfc3187 100644 --- a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx +++ b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx @@ -25,6 +25,19 @@ const weatherToolWithTitle: Tool = { inputSchema: { type: "object" }, }; +const ICON_DATA_URL = + "data:image/svg+xml;utf8," + + encodeURIComponent( + '', + ); + +const weatherToolWithIcon: Tool = { + name: "get_weather", + title: "Get Weather", + icons: [{ src: ICON_DATA_URL }], + inputSchema: { type: "object" }, +}; + export const Default: Story = { args: { tool: weatherTool, @@ -46,6 +59,13 @@ export const WithTitle: Story = { }, }; +export const WithIcon: Story = { + args: { + tool: weatherToolWithIcon, + selected: false, + }, +}; + export const LongName: Story = { args: { tool: { diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.test.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.test.tsx index 749edbc47..f7b5aadbe 100644 --- a/clients/web/src/components/groups/ToolListItem/ToolListItem.test.tsx +++ b/clients/web/src/components/groups/ToolListItem/ToolListItem.test.tsx @@ -9,6 +9,8 @@ const tool: Tool = { inputSchema: { type: "object" }, }; +const ICON_SRC = "data:image/svg+xml,%3Csvg/%3E"; + describe("ToolListItem", () => { it("renders the name when title is missing", () => { renderWithMantine( @@ -43,4 +45,23 @@ describe("ToolListItem", () => { renderWithMantine( {}} />); expect(screen.getByRole("button")).toBeInTheDocument(); }); + + it("renders the first icon when tool.icons is present", () => { + renderWithMantine( + {}} + />, + ); + const img = screen.getByRole("presentation"); + expect(img).toHaveAttribute("src", ICON_SRC); + }); + + it("does not render an icon when tool.icons is missing", () => { + renderWithMantine( + {}} />, + ); + expect(screen.queryByRole("presentation")).not.toBeInTheDocument(); + }); }); diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx index 03ce97e97..184036dd1 100644 --- a/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx +++ b/clients/web/src/components/groups/ToolListItem/ToolListItem.tsx @@ -1,4 +1,4 @@ -import { Stack, Text, UnstyledButton } from "@mantine/core"; +import { Group, Image, Stack, Text, UnstyledButton } from "@mantine/core"; import type { Tool } from "@modelcontextprotocol/sdk/types.js"; import { resolveDisplayLabel } from "../../../utils/toolUtils"; @@ -19,8 +19,28 @@ const ItemSubLabel = Text.withProps({ truncate: true, }); +const ItemBody = Stack.withProps({ + gap: 2, + flex: 1, + miw: 0, +}); + +const Row = Group.withProps({ + gap: "sm", + wrap: "nowrap", + align: "flex-start", +}); + +const ToolIcon = Image.withProps({ + w: 20, + h: 20, + fit: "contain", +}); + export function ToolListItem({ tool, selected, onClick }: ToolListItemProps) { - const { name, title } = tool; + const { name, title, icons } = tool; + const iconSrc = icons?.[0]?.src; + return ( - - {resolveDisplayLabel(name, title)} - {title && {name}} - + + {iconSrc && } + + {resolveDisplayLabel(name, title)} + {title && {name}} + + ); }