From 2b07ba905cac053fec111b8c92754f79a2e0198e Mon Sep 17 00:00:00 2001 From: Ben Drucker Date: Sun, 15 Mar 2026 12:05:52 -0700 Subject: [PATCH 1/3] feat: add customizable icon prop to ToolHeader --- .../content/components/(chatbot)/tool.mdx | 18 +++++ packages/elements/src/tool.tsx | 37 ++++++--- packages/examples/src/tool-custom-icons.tsx | 76 +++++++++++++++++++ 3 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 packages/examples/src/tool-custom-icons.tsx diff --git a/apps/docs/content/components/(chatbot)/tool.mdx b/apps/docs/content/components/(chatbot)/tool.mdx index a56f31c4..ed33c886 100644 --- a/apps/docs/content/components/(chatbot)/tool.mdx +++ b/apps/docs/content/components/(chatbot)/tool.mdx @@ -176,6 +176,7 @@ export async function POST(req: Request) { ## Features - Collapsible interface for showing/hiding tool details +- Customizable tool icons, with support for mapping icons by tool name - Visual status indicators with icons and badges - Support for multiple tool execution states (pending, running, completed, error) - Formatted parameter display with JSON syntax highlighting @@ -211,6 +212,19 @@ Shows a tool that encountered an error during execution. Opens by default to dis +### Custom Icons + +Pass a function to the `icon` prop to render a custom icon. The function receives an object with: + +- `type`: the tool part type +- `state`: the current execution state +- `toolName`: the derived tool name +- `className`: default sizing and color styles + +Use `toolName` to map different icons by tool type. When the function returns `null`, the default wrench icon is used. + + + ## Props ### `` @@ -229,6 +243,10 @@ Shows a tool that encountered an error during execution. Opens by default to dis ReactNode)", + }, title: { description: "Custom title to display instead of the derived tool name.", type: "string", diff --git a/packages/elements/src/tool.tsx b/packages/elements/src/tool.tsx index 9a22010e..9aaea5b2 100644 --- a/packages/elements/src/tool.tsx +++ b/packages/elements/src/tool.tsx @@ -1,5 +1,8 @@ "use client"; +import type { DynamicToolUIPart, ToolUIPart } from "ai"; +import type { ComponentProps, ReactNode } from "react"; + import { Badge } from "@repo/shadcn-ui/components/ui/badge"; import { Collapsible, @@ -7,7 +10,6 @@ import { CollapsibleTrigger, } from "@repo/shadcn-ui/components/ui/collapsible"; import { cn } from "@repo/shadcn-ui/lib/utils"; -import type { DynamicToolUIPart, ToolUIPart } from "ai"; import { CheckCircleIcon, ChevronDownIcon, @@ -16,7 +18,6 @@ import { WrenchIcon, XCircleIcon, } from "lucide-react"; -import type { ComponentProps, ReactNode } from "react"; import { isValidElement } from "react"; import { CodeBlock } from "./code-block"; @@ -32,17 +33,22 @@ export const Tool = ({ className, ...props }: ToolProps) => ( export type ToolPart = ToolUIPart | DynamicToolUIPart; -export type ToolHeaderProps = { +type ToolPartProps = + | (Pick & { toolName?: never }) + | Pick; + +export type ToolIconProps = { + type: ToolPart["type"]; + state: ToolPart["state"]; + toolName: string; + className: string; +}; + +export type ToolHeaderProps = ToolPartProps & { + icon?: ReactNode | ((props: ToolIconProps) => ReactNode); title?: string; className?: string; -} & ( - | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never } - | { - type: DynamicToolUIPart["type"]; - state: DynamicToolUIPart["state"]; - toolName: string; - } -); +}; const statusLabels: Record = { "approval-requested": "Awaiting Approval", @@ -73,6 +79,7 @@ export const getStatusBadge = (status: ToolPart["state"]) => ( export const ToolHeader = ({ className, + icon, title, type, state, @@ -82,6 +89,12 @@ export const ToolHeader = ({ const derivedName = type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-"); + const iconClassName = "size-4 shrink-0 text-muted-foreground"; + const resolvedIcon = + typeof icon === "function" + ? icon({ type, state, toolName: derivedName, className: iconClassName }) + : icon; + return (
- + {resolvedIcon ?? } {title ?? derivedName} {getStatusBadge(state)}
diff --git a/packages/examples/src/tool-custom-icons.tsx b/packages/examples/src/tool-custom-icons.tsx new file mode 100644 index 00000000..96a2f964 --- /dev/null +++ b/packages/examples/src/tool-custom-icons.tsx @@ -0,0 +1,76 @@ +"use client"; + +import type { ComponentProps, ReactNode } from "react"; +import { Tool, ToolContent, ToolHeader, ToolInput } from "@repo/elements/tool"; + +type SvgProps = ComponentProps<"svg">; + +const DatabaseIcon = (props: SvgProps) => ( + + + + + +); + +const SearchIcon = (props: SvgProps) => ( + + + + +); + +const toolIcons: Record ReactNode> = { + database_query: DatabaseIcon, + web_search: SearchIcon, +}; + +const renderIcon = ({ toolName, className }: { toolName: string; className: string }) => { + const Icon = toolIcons[toolName]; + return Icon ? : null; +}; + +const Example = () => ( +
+ + } + state="output-available" + title="database_query" + type="tool-database_query" + /> + + = ?", + params: ["2024-01-01"], + }} + /> + + + + + + + + + + + + + + +
+); + +export default Example; From 09991265d495fd405d12d762da4214a7b359e80c Mon Sep 17 00:00:00 2001 From: Ben Drucker Date: Sun, 15 Mar 2026 12:12:55 -0700 Subject: [PATCH 2/3] feat: revert ToolPartProps, use lucide icons in examples --- packages/elements/src/tool.tsx | 15 +++++---- packages/examples/src/tool-custom-icons.tsx | 37 ++++++++++----------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/elements/src/tool.tsx b/packages/elements/src/tool.tsx index 9aaea5b2..32959f40 100644 --- a/packages/elements/src/tool.tsx +++ b/packages/elements/src/tool.tsx @@ -33,10 +33,6 @@ export const Tool = ({ className, ...props }: ToolProps) => ( export type ToolPart = ToolUIPart | DynamicToolUIPart; -type ToolPartProps = - | (Pick & { toolName?: never }) - | Pick; - export type ToolIconProps = { type: ToolPart["type"]; state: ToolPart["state"]; @@ -44,11 +40,18 @@ export type ToolIconProps = { className: string; }; -export type ToolHeaderProps = ToolPartProps & { +export type ToolHeaderProps = { icon?: ReactNode | ((props: ToolIconProps) => ReactNode); title?: string; className?: string; -}; +} & ( + | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never } + | { + type: DynamicToolUIPart["type"]; + state: DynamicToolUIPart["state"]; + toolName: string; + } +); const statusLabels: Record = { "approval-requested": "Awaiting Approval", diff --git a/packages/examples/src/tool-custom-icons.tsx b/packages/examples/src/tool-custom-icons.tsx index 96a2f964..78b4c722 100644 --- a/packages/examples/src/tool-custom-icons.tsx +++ b/packages/examples/src/tool-custom-icons.tsx @@ -1,28 +1,14 @@ "use client"; -import type { ComponentProps, ReactNode } from "react"; +import type { ReactNode } from "react"; import { Tool, ToolContent, ToolHeader, ToolInput } from "@repo/elements/tool"; +import { DatabaseIcon, GlobeIcon, SearchIcon } from "lucide-react"; +import type { LucideProps } from "lucide-react"; -type SvgProps = ComponentProps<"svg">; - -const DatabaseIcon = (props: SvgProps) => ( - - - - - -); - -const SearchIcon = (props: SvgProps) => ( - - - - -); - -const toolIcons: Record ReactNode> = { +const toolIcons: Record ReactNode> = { database_query: DatabaseIcon, web_search: SearchIcon, + browse_url: GlobeIcon, }; const renderIcon = ({ toolName, className }: { toolName: string; className: string }) => { @@ -59,6 +45,19 @@ const Example = () => (
+ + ( + + )} + state="output-available" + title="custom_search" + type="tool-custom_search" + /> + + + + Date: Sun, 15 Mar 2026 12:26:11 -0700 Subject: [PATCH 3/3] fix: clean up icon examples and revert wrapper approach --- packages/examples/src/tool-custom-icons.tsx | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/examples/src/tool-custom-icons.tsx b/packages/examples/src/tool-custom-icons.tsx index 78b4c722..f2029899 100644 --- a/packages/examples/src/tool-custom-icons.tsx +++ b/packages/examples/src/tool-custom-icons.tsx @@ -2,13 +2,13 @@ import type { ReactNode } from "react"; import { Tool, ToolContent, ToolHeader, ToolInput } from "@repo/elements/tool"; -import { DatabaseIcon, GlobeIcon, SearchIcon } from "lucide-react"; +import { cn } from "@repo/shadcn-ui/lib/utils"; +import { DatabaseIcon, SearchIcon, WrenchIcon } from "lucide-react"; import type { LucideProps } from "lucide-react"; const toolIcons: Record ReactNode> = { database_query: DatabaseIcon, web_search: SearchIcon, - browse_url: GlobeIcon, }; const renderIcon = ({ toolName, className }: { toolName: string; className: string }) => { @@ -20,7 +20,7 @@ const Example = () => (
} + icon={renderIcon} state="output-available" title="database_query" type="tool-database_query" @@ -47,15 +47,17 @@ const Example = () => ( ( - - )} - state="output-available" - title="custom_search" - type="tool-custom_search" + icon={} + state="input-available" + title="custom_style" + type="tool-custom_style" /> - +