From 3afcab8f32f82c36dbeb1758a2123c956438249d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 8 May 2026 12:01:40 -0400 Subject: [PATCH 1/3] feat(apps): render tool.icons on ToolListItem (#1264) Add icon rendering to ToolListItem so the Tools sidebar matches the Apps sidebar treatment. Falls back gracefully when tool.icons is absent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ToolListItem/ToolListItem.stories.tsx | 20 +++++++++++ .../groups/ToolListItem/ToolListItem.test.tsx | 21 +++++++++++ .../groups/ToolListItem/ToolListItem.tsx | 35 +++++++++++++++---- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx index 86936a46d..2a4a922d8 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}} + + ); } From 624b1aa285867eb6c7e462ba325f246b130015d6 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 8 May 2026 16:09:19 -0400 Subject: [PATCH 2/3] fix(stories): use #228be6 in ToolListItem WithIcon data URL The source SVG had %23228be6 pre-encoded, but encodeURIComponent then re-escaped the % to %25, leaving fill="%23228be6" in the decoded SVG and making the icon render black instead of Mantine blue. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/groups/ToolListItem/ToolListItem.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx index 2a4a922d8..e9bfc3187 100644 --- a/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx +++ b/clients/web/src/components/groups/ToolListItem/ToolListItem.stories.tsx @@ -28,7 +28,7 @@ const weatherToolWithTitle: Tool = { const ICON_DATA_URL = "data:image/svg+xml;utf8," + encodeURIComponent( - '', + '', ); const weatherToolWithIcon: Tool = { From 65e389e3fd8cec3c1eae06fe6e38f3cd0efebf05 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 8 May 2026 19:52:28 -0400 Subject: [PATCH 3/3] feat(apps): render tool.icons on ToolDetailPanel (#1264) Show the tool icon next to the title in the detail panel, matching the header icon size used in AppsScreen (24x24, fit: contain). Adds a WithIcon story and tests for icon-present/icon-absent states. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ToolDetailPanel.stories.tsx | 26 +++++++++++++++++++ .../ToolDetailPanel/ToolDetailPanel.test.tsx | 23 ++++++++++++++++ .../ToolDetailPanel/ToolDetailPanel.tsx | 23 +++++++++++++--- 3 files changed, 69 insertions(+), 3 deletions(-) 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 && (