feat: implement tool grouping in ComposerAction for improved UI organization

This commit is contained in:
Anish Sarkar 2026-03-17 15:18:58 +05:30
parent 591bd6bb46
commit e5180aa0a3

View file

@ -614,6 +614,32 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
); );
}, [filteredTools, disabledTools]); }, [filteredTools, disabledTools]);
const groupedTools = useMemo(() => {
if (!filteredTools) return [];
const toolsByName = new Map(filteredTools.map((t) => [t.name, t]));
const result: { label: string; tools: typeof filteredTools }[] = [];
const placed = new Set<string>();
for (const group of TOOL_GROUPS) {
const matched = group.tools.flatMap((name) => {
const tool = toolsByName.get(name);
if (!tool) return [];
placed.add(name);
return [tool];
});
if (matched.length > 0) {
result.push({ label: group.label, tools: matched });
}
}
const ungrouped = filteredTools.filter((t) => !placed.has(t.name));
if (ungrouped.length > 0) {
result.push({ label: "Other", tools: ungrouped });
}
return result;
}, [filteredTools]);
useEffect(() => { useEffect(() => {
hydrateDisabled(); hydrateDisabled();
}, [hydrateDisabled]); }, [hydrateDisabled]);
@ -668,33 +694,40 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
{filteredEnabledCount}/{filteredTools?.length ?? 0} enabled {filteredEnabledCount}/{filteredTools?.length ?? 0} enabled
</span> </span>
</div> </div>
<div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}> <div className="overflow-y-auto pb-6" onScroll={handleToolsScroll}>
{filteredTools?.map((tool) => { {groupedTools.map((group) => (
const isDisabled = disabledTools.includes(tool.name); <div key={group.label}>
const ToolIcon = getToolIcon(tool.name); <div className="px-4 pt-3 pb-1 text-xs text-muted-foreground/80 font-medium select-none">
return ( {group.label}
<div
key={tool.name}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0"
/>
</div>
);
})}
{!filteredTools?.length && (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
Loading tools...
</div> </div>
)} {group.tools.map((tool) => {
</div> const isDisabled = disabledTools.includes(tool.name);
const ToolIcon = getToolIcon(tool.name);
return (
<div
key={tool.name}
className="flex w-full items-center gap-3 px-4 py-2 hover:bg-muted-foreground/10 transition-colors"
>
<ToolIcon className="size-4 shrink-0 text-muted-foreground" />
<span className="flex-1 min-w-0 text-sm font-medium truncate">
{formatToolName(tool.name)}
</span>
<Switch
checked={!isDisabled}
onCheckedChange={() => toggleTool(tool.name)}
className="shrink-0"
/>
</div>
);
})}
</div>
))}
{!filteredTools?.length && (
<div className="px-4 py-6 text-center text-sm text-muted-foreground">
Loading tools...
</div>
)}
</div>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
<Button <Button
@ -744,36 +777,43 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`, WebkitMaskImage: `linear-gradient(to bottom, ${toolsScrollPos === "top" ? "black" : "transparent"}, black 16px, black calc(100% - 16px), ${toolsScrollPos === "bottom" ? "black" : "transparent"})`,
}} }}
> >
{filteredTools?.map((tool) => { {groupedTools.map((group) => (
const isDisabled = disabledTools.includes(tool.name); <div key={group.label}>
const ToolIcon = getToolIcon(tool.name); <div className="px-2.5 sm:px-3 pt-2 pb-0.5 text-[10px] sm:text-xs text-muted-foreground/80 font-normal select-none">
const row = ( {group.label}
<div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors"> </div>
<ToolIcon className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" /> {group.tools.map((tool) => {
<span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate"> const isDisabled = disabledTools.includes(tool.name);
{formatToolName(tool.name)} const ToolIcon = getToolIcon(tool.name);
</span> const row = (
<Switch <div className="flex w-full items-center gap-2 sm:gap-3 px-2.5 sm:px-3 py-1 sm:py-1.5 hover:bg-muted-foreground/10 transition-colors">
checked={!isDisabled} <ToolIcon className="size-3.5 sm:size-4 shrink-0 text-muted-foreground" />
onCheckedChange={() => toggleTool(tool.name)} <span className="flex-1 min-w-0 text-xs sm:text-sm font-medium truncate">
className="shrink-0 scale-[0.6] sm:scale-75" {formatToolName(tool.name)}
/> </span>
</div> <Switch
); checked={!isDisabled}
return ( onCheckedChange={() => toggleTool(tool.name)}
<Tooltip key={tool.name}> className="shrink-0 scale-[0.6] sm:scale-75"
<TooltipTrigger asChild>{row}</TooltipTrigger> />
<TooltipContent side="right" className="max-w-64 text-xs"> </div>
{tool.description} );
</TooltipContent> return (
</Tooltip> <Tooltip key={tool.name}>
); <TooltipTrigger asChild>{row}</TooltipTrigger>
})} <TooltipContent side="right" className="max-w-64 text-xs">
{!filteredTools?.length && ( {tool.description}
<div className="px-3 py-4 text-center text-xs text-muted-foreground"> </TooltipContent>
Loading tools... </Tooltip>
</div> );
)} })}
</div>
))}
{!filteredTools?.length && (
<div className="px-3 py-4 text-center text-xs text-muted-foreground">
Loading tools...
</div>
)}
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@ -783,7 +823,7 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
type="button" type="button"
onClick={() => toggleTool("web_search")} onClick={() => toggleTool("web_search")}
className={cn( className={cn(
"rounded-full transition-all flex items-center gap-1 px-2 py-1 border h-8", "rounded-full transition-all flex items-center gap-1 px-2 py-1 border h-8 select-none",
isWebSearchEnabled isWebSearchEnabled
? "bg-sky-500/15 border-sky-500/60 text-sky-500" ? "bg-sky-500/15 border-sky-500/60 text-sky-500"
: "bg-transparent border-transparent text-muted-foreground hover:text-foreground" : "bg-transparent border-transparent text-muted-foreground hover:text-foreground"
@ -891,6 +931,22 @@ function formatToolName(name: string): string {
.join(" "); .join(" ");
} }
const TOOL_GROUPS: { label: string; tools: string[] }[] = [
{
label: "Research",
tools: ["search_knowledge_base", "search_surfsense_docs", "scrape_webpage", "link_preview"],
},
{
label: "Generate",
tools: ["generate_podcast", "generate_report", "generate_image", "display_image"],
},
{
label: "Memory",
tools: ["save_memory", "recall_memory"],
},
];
const MessageError: FC = () => { const MessageError: FC = () => {
return ( return (
<MessagePrimitive.Error> <MessagePrimitive.Error>