From a67b0c1da8f2f447c74d0ee4ec35b5b2a0828a1f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Fri, 31 Jul 2026 12:11:10 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AF=20feat:=20Per-Tool=20Intent=20Labe?= =?UTF-8?q?l=20Toggles=20in=20the=20Agent=20Builder=20(#14550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎯 feat: Per-Tool Intent Label Toggles in the Agent Builder Saved agents have had per-tool intent control on the backend since the capability landed (tool_options[name].describe_intent, consumed by applyIntentLabels), but the builder offered no way to set it - the capability was invisible to saved agents on MCP tools, which default off. This is the deferred UI slice. The MCP tools panel gains a fourth per-tool option toggle (Captions icon, teal) next to defer / programmatic / background, plus the matching section-header bulk toggle, gated on the tool_intents capability. The toggle writes describe_intent: true through the same withBooleanOption path the sibling flags use, so an opt-in composes with existing entries and clearing the last flag drops the tool's entry entirely. No backend changes: the agent CRUD schema already validates describe_intent and initialization already consumes it. * 🧯 fix: Keep the Intent Toggle Truthful for Programmatic-Only Tools A tool marked Programmatic in the builder gets allowed_callers: ['code_execution'], and the backend's canInjectIntentParam deliberately skips non-direct tools (no card renders for calls made from code), so an intent opt-in on such a tool is guaranteed inert. The UI could nevertheless show both settings active. The intent toggle now mirrors the runtime gate: isToolProgrammaticOnly (allowed_callers set and missing 'direct', the exact backend predicate) renders the per-row toggle inert with a tooltip explaining why, shows it unpressed regardless of any stored flag, and the bulk toggle and its all-state consider only tools the label can actually reach. The stored describe_intent value is preserved, so unmarking Programmatic restores the user's earlier choice instead of destroying it. OptionToggle gains a disabled state (dimmed, non-interactive, tooltip kept) shared by the row and bulk variants. --- .../SidePanel/Agents/MCPToolItem.tsx | 25 ++++++- .../SidePanel/Agents/OptionToggle.tsx | 24 +++++-- .../ItemDialog/__tests__/McpSection.spec.tsx | 44 ++++++++++-- .../Tools/ItemDialog/sections/McpSection.tsx | 44 ++++++++++-- .../Agents/__tests__/MCPToolItem.spec.tsx | 32 +++++++++ .../__tests__/useAgentCapabilities.spec.ts | 13 ++++ .../__tests__/useMCPToolOptions.spec.ts | 72 +++++++++++++++++++ .../src/hooks/Agents/useAgentCapabilities.ts | 7 ++ client/src/hooks/Agents/useMCPToolOptions.ts | 27 ++++++- client/src/locales/en/translation.json | 5 ++ 10 files changed, 278 insertions(+), 15 deletions(-) diff --git a/client/src/components/SidePanel/Agents/MCPToolItem.tsx b/client/src/components/SidePanel/Agents/MCPToolItem.tsx index b82fc11797..c2704956a8 100644 --- a/client/src/components/SidePanel/Agents/MCPToolItem.tsx +++ b/client/src/components/SidePanel/Agents/MCPToolItem.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Check, Clock, Code2, Info, Zap } from 'lucide-react'; +import { Check, Clock, Code2, Captions, Info, Zap } from 'lucide-react'; import type { AgentToolType } from 'librechat-data-provider'; import OptionToggle from './OptionToggle'; import { useLocalize } from '~/hooks'; @@ -11,13 +11,19 @@ interface MCPToolItemProps { isDeferred: boolean; isProgrammatic: boolean; isBackground: boolean; + isIntent: boolean; + /** Intent labels never reach a programmatic-only tool (no card renders for + * calls made from code), so the toggle is shown inert with an explanation. */ + intentDisabled: boolean; deferredToolsEnabled: boolean; programmaticToolsEnabled: boolean; backgroundToolsEnabled: boolean; + toolIntentsEnabled: boolean; onToggleSelect: () => void; onToggleDefer: () => void; onToggleProgrammatic: () => void; onToggleBackground: () => void; + onToggleIntent: () => void; } const iconButton = @@ -33,9 +39,13 @@ export default function MCPToolItem({ onToggleProgrammatic, isBackground, onToggleBackground, + isIntent, + intentDisabled, + onToggleIntent, deferredToolsEnabled, programmaticToolsEnabled, backgroundToolsEnabled, + toolIntentsEnabled, }: MCPToolItemProps) { const localize = useLocalize(); const [expanded, setExpanded] = useState(false); @@ -100,6 +110,19 @@ export default function MCPToolItem({ onToggle={onToggleBackground} /> )} + {toolIntentsEnabled && ( + + )}