From edd614bbff9696525a95ed4100e6f10523ceb72d Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:30:12 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B0=20feat:=20Redesign=20Agent=20Build?= =?UTF-8?q?er=20with=20Unified=20Tools=20Marketplace,=20Skills=20&=20Orche?= =?UTF-8?q?stration=20(#13952)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: redesign the agent builder tools, skills, and advanced panels Replace the stacked capability/MCP/skill/tool/action form sections with a unified tools marketplace, per-item configuration dialogs, and a consolidated Advanced panel. - unified tools marketplace (catalog, sidebar, polymorphic cards/rows) covering built-in capabilities, plugins, MCP servers, and actions, each with a detail/config dialog - dedicated Skills picker and a Tools section with selected-item summaries and empty states - redesigned action editor and authentication dialog (method cards, segmented controls) - rebuilt Advanced panel: orchestration hub (subagents, handoffs, chain), max steps, skills kill-switch, copyable agent id - restyled version history (timeline, tool/capability counts, in-app restore confirmation) - shared component updates (Radio, Input/Textarea, dropdown z-index, dialog primitives) and keyboard-only focus rings via useInputModality - format-hint placeholders for tool credential fields - sanitize numeric parameter inputs to prevent comma truncation * feat: refine agent builder tools, actions, and MCP sections * feat: restore Memory capability toggle in agent builder tools catalog * feat: refine agent tools picker (skills, MCP connect/OAuth, web search) - Skills picker: per-card visibility (public) and shared-author badges, category filtering, and an in-place Create skill flow that auto-attaches the new skill without leaving the builder - MCP: inline Connect button in the first dialog plus a dedicated OAuth dialog (continue, copyable URL, QR code) shown only when OAuth is required - Web search: auth-aware affordance, settings cog when user-provided and an info icon when system-defined - Remove orphaned com_ui_unavailable/com_ui_initializing keys and the dead Tools/MCPToolItem component * refactor: streamline MCP OAuth dialog - Remove the Cancel button (the flow auto-closes on connect / times out) - Show the URL in a read-only single-line scrollable input (cursor moves through it, not fully visible) with the shared CopyButton's smooth Copy/Check icon swap, matching the OAuth callback-URL field - Put the primary Continue with OAuth action (icon trailing) and an icon-only QR toggle together in a row at the bottom, below the URL - The QR reveals between the description and the URL with a smooth height animation (grid-rows 0fr to 1fr, matching MCPToolItem's reveal) * feat: smoothly collapse MCP connect button once connected * feat: cross-fade MCP tools between loading, list, and empty states * feat: show MCP server icon in OAuth dialog title * fix: vertically center OAuth dialog title against the MCP icon * feat: smoothly animate auth field changes in the MCP server dialog * feat: match Code Interpreter file upload to the File Search dropzone Swap Code Interpreter's thin btn-neutral bar for the same dashed dropzone (DropzoneContent + dropzoneClassName) File Search already uses, so the two capabilities' upload UIs are consistent. * feat: show a saving spinner and allow cancelling credential edits Drive the tool credential Save button from the real mutation state so it shows a spinner while the request is in flight, and add a Cancel button when re-editing already-saved credentials so the edit can be dismissed. * feat: make the skills create button a compact icon button * fix: restore MCP attach semantics and confirmations in the tools marketplace Connecting an MCP server from the item dialog now enables all of its tools once the connection settles, deselect-all keeps the server attached via its placeholder token instead of detaching it, adding a server writes the token so a zero-tool attachment survives a save, and removing a server from the tools list asks for confirmation again. Consume-only servers are excluded from the catalog, matching the old select dialog. Also share the catalog/selection pipeline between ToolsSection and the marketplace through useAgentItems, hoist NEW_ACTION_ID next to ActionItem, drop unused status/view union members and stale TranslationKeys casts, document the phase-2 Favorites/Made-by-you views, fix the needs-setup dot semantics and card focus suppression, remove the redundant close button in CreateSkillDialog, move useInputModality into @librechat/client so external consumers can mount it, and delete dead files and orphaned translation keys. * fix: scope tooltip elevation to dialogs and restore dialog close button size Tooltips go back to z-150 globally; inside a dialog they now borrow the depth-aware popover z-index so they still clear nested dialogs (the Tool Library item dialog) without outranking freshly opened modals everywhere else. The default dialog close icon returns to its original size, and the lc-field pointer-focus suppression ships with the package next to Input and Textarea so external consumers get the whole mechanism from @librechat/client. * feat: add favorites for marketplace tools, MCP servers, and skills Reintroduce the favorite star from the old skill picker, generalized to every marketplace item kind except per-agent actions. Cards in the Tool Library and Skills dialogs get a hover-revealed star (always visible once favorited), and the existing Favorites views in both dialogs now filter to starred items. Favorites persist in a dedicated ToolFavorite collection, one document per (user, itemType, itemId) with a unique compound index, exposed through atomic per-item PUT/DELETE endpoints under /api/user/settings/favorites/ tools. Per-item writes are idempotent and race-free across tabs/devices (the unique index backstops concurrent toggles), reads are a single index-backed query capped at 100 favorites per user, and the client keeps React Query as the source of truth with optimistic updates. Handlers live in @librechat/api with a thin route wrapper; methods follow the data-schemas factory pattern with tenant isolation. The favorites filter now matches on compound kind:id keys instead of bare ids, closing a cross-kind collision where a tool and a skill sharing an id would both match. The skill-favorites data-service stubs and the reserved TUserFavorite.skillId field are replaced by the new tool-favorites service. * feat: anchor the favorite star at the card's right edge Swap the ToolCard action-bar order so the star sits rightmost with the configure/info icon to its left. Every card can be favorited but only some are configurable, so anchoring the star keeps it in a consistent position across the grid. * chore: remove translation keys orphaned by the tool library redesign * fix: gate marketplace creation entries and resolve off-page selected skills The Create New menu exposed MCP server creation to users without the MCP_SERVERS create permission and action creation on deployments with the actions capability disabled; both entries are now gated like their pre-redesign counterparts, and the button hides when neither applies. Selected skills missing from the first catalog page (limit 100) were dropped from the Skills section entirely, leaving them impossible to inspect or remove. useResolvedSkills restores the per-id lookup: off-page skills are fetched individually and confirmed misses (deleted or no longer shared) stay visible under an Unavailable skill placeholder so the stale allowlist entry remains removable. * fix: refetch favorites when toggled before the list loads, lint fixes An optimistic favorite written over an unpopulated cache seeded the list with only the toggled item, and cancelQueries killed the initial fetch that would have corrected it, hiding existing favorites until reload. The optimistic write now only applies over known data; otherwise onSettled invalidates so the authoritative list is refetched. Also unnest the version date-label ternary and drop an unused form watch flagged by CI. * fix: sync skills_enabled with selection edits and hydrate agent file entries skills_enabled is the master opt-in for the skill allowlist, and an empty allowlist with the flag on means the full accessible catalog. Selection edits now sync the flag on empty/non-empty transitions via a shared skillsEnabledTransition helper: picking the first skill enables it so the choice takes effect on save, and removing the last one disables it so the agent doesn't silently escalate to every skill. Mid-selection edits leave the flag alone, preserving the Advanced kill switch's disable-without-clearing behavior. Agents loaded from the API carry only tool_resources.*.file_ids; the client-only context/knowledge/code file entry arrays were read directly, so existing attachments rendered as empty and could not be removed. A new useAgentFileEntries hook restores the legacy derivation (agent files query merged into the file map via processAgentOption) and now feeds AgentConfig, the item dialog, and the selected-items pipeline. * fix: hide plugin tools from the marketplace when the tools capability is off buildCatalog gated built-ins, MCP, and skills on their capabilities and permissions but pushed regular plugin tools unconditionally, so deployments that removed the tools capability still offered attachable tool cards in the marketplace. The loop now requires AgentCapabilities.tools, matching the old Add Tools gate. * fix: strip legacy MCP tokens on removal, guard action creation, model button spacing MCP selection accepts every historical token format (server placeholder, raw server name, mcp_-prefixed, and per-tool ids in prefix/suffix shapes) but removal only filtered the new placeholder plus the server's current tool ids, so a legacy token left the server permanently selected and its tools still expanded after save. Selection and removal now share a matchesMcpServer predicate. Creating an action from the marketplace on an unsaved agent opened an editor whose save was guaranteed to fail; it now surfaces the existing save-the-agent-first error, matching the action-removal guard. The model picker button keeps its tight px-1 with a provider icon but gets px-3 in the empty Select-a-model state so the placeholder is not flush against the border. * fix: strip legacy prefix MCP tokens in useRemoveMCPTool The hook only filtered the raw server name and suffix-delimiter tokens, so confirming removal in the selected-tools section left persisted prefix-format tokens (mcp_, mcp__) in the form and the row reappeared as selected. It now shares the matchesMcpServer predicate with the selection logic so removal can never lag selection. * fix: exact MCP token matching and keep errored skill lookups removable The mcp__ prefix clause in matchesMcpServer was invented by the redesign, not a persisted format (mcp_prefix is only ever used as the exact mcp_ pluginKey), and it claimed longer server names sharing a prefix: with servers github and github_extra, removing github also stripped github_extra's tokens. The predicate now only matches exact or delimiter-bounded shapes. An off-page selected skill whose per-id lookup failed with a transient error (retry disabled) vanished from the selected list until remount. Any settled lookup failure now keeps the placeholder entry so the allowlist id stays visible and removable; only in-flight lookups are briefly hidden. * fix: route file-backed built-in removal to the file manager Code Interpreter and File Search stay selected while they hold code_files or knowledge_files, so removing them by flipping the capability flag left the row visible and unremovable. Their removal now opens the config dialog where the files are managed, mirroring the file-only context built-in; with no files attached the flag still toggles off for a clean removal. * fix: preserve negative values in numeric parameter inputs sanitizeIntegerInput stripped every non-digit, so typing -1 in a numeric parameter field became 1. That broke Google thinkingBudget, where -1 is the dynamic/auto-thinking sentinel (range min is -1): users could no longer select auto and risked sending a one-token budget. The sanitizer now takes an opt-in allowNegative flag that keeps a single leading minus, and DynamicInput passes it when the field's range permits negatives. Thousands-separator cleanup is unchanged for all other fields. * fix: keep in-progress negative numeric input and localize the actions heading Typing a leading minus in a negative-capable numeric parameter (Google thinkingBudget) sanitized to a lone '-', which was then coerced by Number('-') to NaN, so the sign could not be typed before the digits. The lone '-' is now stored as a string until a digit resolves it to a number, matching how the empty-string case is already handled. The agent builder actions panel heading hard-coded 'Add'/'Edit actions'; it now uses com_assistants_add_actions and a restored com_assistants_edit_actions key so non-English locales translate it. * chore: fix import order drift flagged by CI * fix: treat pending web-search auth verification as needs_setup While useVerifyAgentToolAuth is still loading, data is undefined so web_search was not marked needs_setup, and the marketplace card takes the direct-enable path only when status is not needs_setup. On a slow connection a click before the response arrived enabled web_search without collecting the required user-provided key. The auth map now flags web_search needs_setup while the query is loading, routing the click to the config dialog; once verification resolves, a system-defined deployment or a satisfied key clears the flag for a direct toggle. * test: update agent builder e2e selectors --------- Co-authored-by: Danny Avila --- api/server/routes/settings.js | 15 + client/src/App.jsx | 3 +- client/src/Providers/AgentPanelContext.tsx | 6 +- client/src/common/types.ts | 3 + client/src/components/MCP/McpOAuthDialog.tsx | 156 ++++ .../MCP/__tests__/McpOAuthDialog.spec.tsx | 100 +++ .../Plugins/Store/PluginAuthForm.tsx | 294 ++++--- .../Store/__tests__/PluginAuthForm.spec.tsx | 38 +- .../SidePanel/Agents/ActionsInput.tsx | 160 ++-- .../SidePanel/Agents/ActionsPanel.tsx | 167 ---- .../SidePanel/Agents/ActionsTable/Columns.tsx | 70 +- .../Agents/ActionsTable/Skeleton.tsx | 45 ++ .../SidePanel/Agents/ActionsTable/Table.tsx | 27 +- .../SidePanel/Agents/ActionsTable/index.ts | 1 + .../Agents/Advanced/AdvancedButton.tsx | 12 +- .../Agents/Advanced/AdvancedPanel.tsx | 134 ++-- .../SidePanel/Agents/Advanced/AgentChain.tsx | 204 ++--- .../Agents/Advanced/AgentHandoffs.tsx | 232 ++---- .../SidePanel/Agents/Advanced/AgentList.tsx | 198 +++++ .../Agents/Advanced/AgentSubagents.tsx | 240 ++---- .../Agents/Advanced/MaxAgentSteps.tsx | 74 +- .../Agents/Advanced/OrchestrationHub.tsx | 66 ++ .../Agents/Advanced/OrchestrationPattern.tsx | 63 ++ .../Agents/Advanced/SkillsToggle.tsx | 31 + .../SidePanel/Agents/Advanced/ui.tsx | 98 +++ .../SidePanel/Agents/AgentAvatar.tsx | 43 +- .../SidePanel/Agents/AgentConfig.tsx | 735 ++++-------------- .../SidePanel/Agents/AgentFooter.tsx | 30 +- .../SidePanel/Agents/AgentPanelSkeleton.tsx | 96 +-- .../SidePanel/Agents/AgentPanelSwitch.tsx | 4 - .../components/SidePanel/Agents/AgentTool.tsx | 13 +- .../components/SidePanel/Agents/Artifacts.tsx | 125 --- .../SidePanel/Agents/Code/Action.tsx | 68 -- .../SidePanel/Agents/Code/Files.tsx | 46 +- .../components/SidePanel/Agents/Code/Form.tsx | 28 - .../SidePanel/Agents/FileContext.tsx | 184 ++--- .../SidePanel/Agents/FileSearch.tsx | 73 +- .../SidePanel/Agents/FileSearchCheckbox.tsx | 71 -- .../components/SidePanel/Agents/Images.tsx | 38 +- .../SidePanel/Agents/Instructions.tsx | 126 ++- .../components/SidePanel/Agents/MCPTool.tsx | 364 --------- .../SidePanel/Agents/MCPToolItem.tsx | 225 +++--- .../components/SidePanel/Agents/MCPTools.tsx | 78 -- .../SidePanel/Agents/ModelPanel.tsx | 56 +- .../components/SidePanel/Agents/Retrieval.tsx | 93 --- .../SidePanel/Agents/Search/Action.tsx | 111 +-- .../SidePanel/Agents/Search/Form.tsx | 31 - .../SidePanel/Agents/SectionHeader.tsx | 13 + .../SidePanel/Agents/Tools/ActionEditor.tsx | 156 ++++ .../SidePanel/Agents/Tools/CategoryFilter.tsx | 79 ++ .../Tools/ItemDialog/AddMcpServerDialog.tsx | 10 + .../Agents/Tools/ItemDialog/ItemDialog.tsx | 42 + .../Tools/ItemDialog/ItemDialogBody.tsx | 40 + .../Tools/ItemDialog/ItemDialogHeader.tsx | 66 ++ .../ItemDialog/__tests__/ItemDialog.spec.tsx | 87 +++ .../__tests__/ItemDialogBody.spec.tsx | 96 +++ .../ItemDialog/__tests__/McpSection.spec.tsx | 215 +++++ .../ItemDialog/sections/ActionSection.tsx | 32 + .../ItemDialog/sections/BuiltinSection.tsx | 124 +++ .../Tools/ItemDialog/sections/McpSection.tsx | 380 +++++++++ .../ItemDialog/sections/SkillSection.tsx | 27 + .../Tools/ItemDialog/sections/ToolSection.tsx | 97 +++ .../Agents/Tools/MarketplaceCatalog.tsx | 105 +++ .../Agents/Tools/MarketplaceSidebar.tsx | 201 +++++ .../SidePanel/Agents/Tools/SkillsDialog.tsx | 238 ++++++ .../SidePanel/Agents/Tools/ToolCard.tsx | 243 ++++++ .../SidePanel/Agents/Tools/ToolRow.tsx | 113 +++ .../Agents/Tools/ToolsMarketplaceDialog.tsx | 234 ++++++ .../SidePanel/Agents/Tools/ToolsSection.tsx | 368 +++++++++ .../__tests__/MarketplaceCatalog.spec.tsx | 60 ++ .../__tests__/MarketplaceSidebar.spec.tsx | 151 ++++ .../Agents/Tools/__tests__/ToolCard.spec.tsx | 177 +++++ .../__tests__/ToolsMarketplaceDialog.spec.tsx | 221 ++++++ .../Tools/__tests__/ToolsSection.spec.tsx | 139 ++++ .../__tests__/useBuiltinAuthMap.spec.tsx | 78 ++ .../__tests__/useResolvedSkills.spec.tsx | 124 +++ .../SidePanel/Agents/Tools/hooks.ts | 341 ++++++++ .../Agents/Tools/items/__tests__/auth.spec.ts | 30 + .../Tools/items/__tests__/catalog.spec.ts | 237 ++++++ .../items/__tests__/configurable.spec.ts | 82 ++ .../Tools/items/__tests__/filtering.spec.ts | 164 ++++ .../Tools/items/__tests__/icons.spec.ts | 84 ++ .../Tools/items/__tests__/mutations.spec.ts | 159 ++++ .../Tools/items/__tests__/selectors.spec.ts | 226 ++++++ .../Tools/items/__tests__/types.spec.ts | 43 + .../SidePanel/Agents/Tools/items/auth.ts | 10 + .../SidePanel/Agents/Tools/items/catalog.ts | 182 +++++ .../Agents/Tools/items/configurable.ts | 29 + .../SidePanel/Agents/Tools/items/filtering.ts | 68 ++ .../SidePanel/Agents/Tools/items/icons.ts | 92 +++ .../SidePanel/Agents/Tools/items/mutations.ts | 71 ++ .../SidePanel/Agents/Tools/items/selectors.ts | 116 +++ .../SidePanel/Agents/Tools/items/types.ts | 88 +++ .../SidePanel/Agents/UnconfiguredMCPTool.tsx | 84 -- .../SidePanel/Agents/UninitializedMCPTool.tsx | 139 ---- .../SidePanel/Agents/UploadDropzone.tsx | 24 + .../Agents/Version/VersionButton.tsx | 12 +- .../Agents/Version/VersionContent.tsx | 78 +- .../SidePanel/Agents/Version/VersionItem.tsx | 262 ++++++- .../SidePanel/Agents/Version/VersionPanel.tsx | 48 +- .../Version/__tests__/VersionItem.spec.tsx | 114 ++- .../Agents/__tests__/AgentFooter.spec.tsx | 16 +- .../Agents/__tests__/FileContext.spec.tsx | 8 +- .../Agents/__tests__/FileSearch.spec.tsx | 7 +- .../Agents/__tests__/MCPToolItem.spec.tsx | 94 +++ .../SidePanel/Builder/ActionCallback.tsx | 80 +- .../SidePanel/Builder/ActionsAuth.tsx | 585 ++++++-------- .../SidePanel/Builder/ActionsPanel.tsx | 104 +-- .../MCPServerDialog/sections/AuthSection.tsx | 147 ++-- .../SidePanel/Parameters/DynamicInput.tsx | 18 +- .../Skills/dialogs/CreateSkillDialog.tsx | 14 +- .../Skills/dialogs/SkillSelectDialog.tsx | 367 --------- client/src/components/Skills/dialogs/index.ts | 1 - client/src/components/Tools/MCPToolItem.tsx | 115 --- .../components/Tools/MCPToolSelectDialog.tsx | 379 --------- .../src/components/Tools/ToolSelectDialog.tsx | 259 ------ client/src/components/Tools/index.ts | 2 - client/src/components/ui/Collapse.tsx | 41 + .../components/ui/__tests__/Collapse.spec.tsx | 28 + client/src/components/ui/index.ts | 1 + client/src/data-provider/Favorites.ts | 86 +- .../MCP/__tests__/useRemoveMCPTool.spec.tsx | 76 ++ client/src/hooks/MCP/useRemoveMCPTool.ts | 14 +- .../hooks/__tests__/useToolFavorites.spec.tsx | 136 ++++ client/src/hooks/index.ts | 2 +- client/src/hooks/useSkillFavorites.ts | 96 --- client/src/hooks/useToolFavorites.ts | 90 +++ client/src/locales/en/translation.json | 134 +++- client/src/style.css | 73 +- client/src/utils/favoritesError.ts | 10 +- client/src/utils/index.ts | 1 + client/src/utils/numbers.spec.ts | 54 ++ client/src/utils/numbers.ts | 21 + client/test/itemFactories.ts | 44 ++ e2e/specs/mock/agents.spec.ts | 4 +- e2e/specs/mock/mcp.spec.ts | 31 +- packages/api/src/favorites/handlers.spec.ts | 178 +++++ packages/api/src/favorites/handlers.ts | 126 +++ packages/api/src/favorites/index.ts | 1 + packages/api/src/index.ts | 1 + packages/client/src/components/Button.tsx | 2 + .../client/src/components/ControlCombobox.tsx | 11 +- packages/client/src/components/Dropdown.css | 6 +- .../client/src/components/DropdownMenu.tsx | 11 +- .../client/src/components/DropdownPopup.tsx | 6 +- packages/client/src/components/Field.css | 11 + packages/client/src/components/Input.tsx | 3 +- .../client/src/components/OriginalDialog.tsx | 16 + packages/client/src/components/Radio.tsx | 57 +- packages/client/src/components/Textarea.tsx | 3 +- packages/client/src/components/Tooltip.tsx | 7 + packages/client/src/hooks/index.ts | 1 + packages/client/src/hooks/useInputModality.ts | 54 ++ packages/data-provider/src/api-endpoints.ts | 5 + packages/data-provider/src/data-service.ts | 18 +- packages/data-provider/src/keys.ts | 4 +- packages/data-provider/src/types/queries.ts | 14 +- packages/data-schemas/src/index.ts | 2 + .../data-schemas/src/methods/favorite.spec.ts | 153 ++++ packages/data-schemas/src/methods/favorite.ts | 90 +++ packages/data-schemas/src/methods/index.ts | 10 + packages/data-schemas/src/models/favorite.ts | 12 + packages/data-schemas/src/models/index.ts | 3 + packages/data-schemas/src/schema/favorite.ts | 40 + packages/data-schemas/src/schema/index.ts | 1 + packages/data-schemas/src/types/favorite.ts | 35 + packages/data-schemas/src/types/index.ts | 1 + 167 files changed, 10657 insertions(+), 5137 deletions(-) create mode 100644 client/src/components/MCP/McpOAuthDialog.tsx create mode 100644 client/src/components/MCP/__tests__/McpOAuthDialog.spec.tsx delete mode 100644 client/src/components/SidePanel/Agents/ActionsPanel.tsx create mode 100644 client/src/components/SidePanel/Agents/ActionsTable/Skeleton.tsx create mode 100644 client/src/components/SidePanel/Agents/Advanced/AgentList.tsx create mode 100644 client/src/components/SidePanel/Agents/Advanced/OrchestrationHub.tsx create mode 100644 client/src/components/SidePanel/Agents/Advanced/OrchestrationPattern.tsx create mode 100644 client/src/components/SidePanel/Agents/Advanced/SkillsToggle.tsx create mode 100644 client/src/components/SidePanel/Agents/Advanced/ui.tsx delete mode 100644 client/src/components/SidePanel/Agents/Artifacts.tsx delete mode 100644 client/src/components/SidePanel/Agents/Code/Action.tsx delete mode 100644 client/src/components/SidePanel/Agents/Code/Form.tsx delete mode 100644 client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx delete mode 100644 client/src/components/SidePanel/Agents/MCPTool.tsx delete mode 100644 client/src/components/SidePanel/Agents/MCPTools.tsx delete mode 100644 client/src/components/SidePanel/Agents/Retrieval.tsx delete mode 100644 client/src/components/SidePanel/Agents/Search/Form.tsx create mode 100644 client/src/components/SidePanel/Agents/SectionHeader.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ActionEditor.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/CategoryFilter.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/AddMcpServerDialog.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/ItemDialog.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/ItemDialogBody.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/ItemDialogHeader.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/ItemDialog.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/ItemDialogBody.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/__tests__/McpSection.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ActionSection.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/BuiltinSection.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/McpSection.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/SkillSection.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ItemDialog/sections/ToolSection.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/MarketplaceCatalog.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/MarketplaceSidebar.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/SkillsDialog.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ToolCard.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ToolRow.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ToolsMarketplaceDialog.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/ToolsSection.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/MarketplaceCatalog.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/MarketplaceSidebar.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/ToolCard.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/ToolsMarketplaceDialog.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/ToolsSection.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/useBuiltinAuthMap.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/__tests__/useResolvedSkills.spec.tsx create mode 100644 client/src/components/SidePanel/Agents/Tools/hooks.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/auth.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/catalog.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/configurable.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/filtering.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/icons.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/mutations.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/selectors.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/__tests__/types.spec.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/auth.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/catalog.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/configurable.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/filtering.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/icons.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/mutations.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/selectors.ts create mode 100644 client/src/components/SidePanel/Agents/Tools/items/types.ts delete mode 100644 client/src/components/SidePanel/Agents/UnconfiguredMCPTool.tsx delete mode 100644 client/src/components/SidePanel/Agents/UninitializedMCPTool.tsx create mode 100644 client/src/components/SidePanel/Agents/UploadDropzone.tsx create mode 100644 client/src/components/SidePanel/Agents/__tests__/MCPToolItem.spec.tsx delete mode 100644 client/src/components/Skills/dialogs/SkillSelectDialog.tsx delete mode 100644 client/src/components/Tools/MCPToolItem.tsx delete mode 100644 client/src/components/Tools/MCPToolSelectDialog.tsx delete mode 100644 client/src/components/Tools/ToolSelectDialog.tsx create mode 100644 client/src/components/ui/Collapse.tsx create mode 100644 client/src/components/ui/__tests__/Collapse.spec.tsx create mode 100644 client/src/hooks/MCP/__tests__/useRemoveMCPTool.spec.tsx create mode 100644 client/src/hooks/__tests__/useToolFavorites.spec.tsx delete mode 100644 client/src/hooks/useSkillFavorites.ts create mode 100644 client/src/hooks/useToolFavorites.ts create mode 100644 client/src/utils/numbers.spec.ts create mode 100644 client/src/utils/numbers.ts create mode 100644 client/test/itemFactories.ts create mode 100644 packages/api/src/favorites/handlers.spec.ts create mode 100644 packages/api/src/favorites/handlers.ts create mode 100644 packages/api/src/favorites/index.ts create mode 100644 packages/client/src/components/Field.css create mode 100644 packages/client/src/hooks/useInputModality.ts create mode 100644 packages/data-schemas/src/methods/favorite.spec.ts create mode 100644 packages/data-schemas/src/methods/favorite.ts create mode 100644 packages/data-schemas/src/models/favorite.ts create mode 100644 packages/data-schemas/src/schema/favorite.ts create mode 100644 packages/data-schemas/src/types/favorite.ts diff --git a/api/server/routes/settings.js b/api/server/routes/settings.js index c6b7c84b2c..38b353d560 100644 --- a/api/server/routes/settings.js +++ b/api/server/routes/settings.js @@ -1,4 +1,5 @@ const express = require('express'); +const { createToolFavoritesHandlers } = require('@librechat/api'); const { updateFavoritesController, getFavoritesController, @@ -8,9 +9,23 @@ const { updateSkillStatesController, } = require('~/server/controllers/SkillStatesController'); const { requireJwtAuth } = require('~/server/middleware'); +const { getToolFavorites, addToolFavorite, removeToolFavorite } = require('~/models'); const router = express.Router(); +const toolFavorites = createToolFavoritesHandlers({ + getToolFavorites, + addToolFavorite, + removeToolFavorite, +}); + +router.get('/favorites/tools', requireJwtAuth, toolFavorites.listToolFavorites); +router.put('/favorites/tools/:itemType/:itemId', requireJwtAuth, toolFavorites.addToolFavorite); +router.delete( + '/favorites/tools/:itemType/:itemId', + requireJwtAuth, + toolFavorites.removeToolFavorite, +); router.get('/favorites', requireJwtAuth, getFavoritesController); router.post('/favorites', requireJwtAuth, updateFavoritesController); router.get('/skills/active', requireJwtAuth, getSkillStatesController); diff --git a/client/src/App.jsx b/client/src/App.jsx index 78ed8438b0..5d20a99200 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,8 +4,8 @@ import { DndProvider } from 'react-dnd'; import { RouterProvider } from 'react-router-dom'; import * as RadixToast from '@radix-ui/react-toast'; import { HTML5Backend } from 'react-dnd-html5-backend'; -import { Toast, ThemeProvider, ToastProvider } from '@librechat/client'; import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'; +import { Toast, ThemeProvider, ToastProvider, useInputModality } from '@librechat/client'; import { ScreenshotProvider, useApiErrorBoundary } from './hooks'; import WakeLockManager from '~/components/System/WakeLockManager'; import QueryDevtoolsGate from '~/components/QueryDevtoolsGate'; @@ -17,6 +17,7 @@ import { router } from './routes'; const App = () => { const { setError } = useApiErrorBoundary(); + useInputModality(); const queryClient = new QueryClient({ defaultOptions: { diff --git a/client/src/Providers/AgentPanelContext.tsx b/client/src/Providers/AgentPanelContext.tsx index b0d74374b4..f465165e28 100644 --- a/client/src/Providers/AgentPanelContext.tsx +++ b/client/src/Providers/AgentPanelContext.tsx @@ -42,13 +42,16 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) const { data: regularTools } = useAvailableToolsQuery(EModelEndpoint.agents); - const { data: mcpData } = useMCPToolsQuery({ + const { data: mcpData, isFetching: mcpToolsFetching } = useMCPToolsQuery({ enabled: !isEphemeralAgent(agent_id) && !isLoading && availableMCPServers != null && availableMCPServers.length > 0, }); + /** Tools are still arriving when the query is in flight and nothing is cached + * yet (e.g., right after a hard refresh). Lets the MCP dialog show a skeleton. */ + const mcpToolsLoading = mcpToolsFetching && mcpData == null; const { agentsConfig, endpointsConfig } = useGetAgentsConfig(); const mcpServerNames = useMemo( @@ -148,6 +151,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode }) agentsConfig, startupConfig, mcpServersMap, + mcpToolsLoading, setActivePanel, endpointsConfig, setCurrentAgentId, diff --git a/client/src/common/types.ts b/client/src/common/types.ts index a27743a8f0..66e145fa61 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -233,6 +233,9 @@ export type AgentPanelContextType = { endpointsConfig?: t.TEndpointsConfig | null; /** Pre-computed MCP server information indexed by server key */ mcpServersMap: Map; + /** True while the MCP tools list is being fetched and no data has arrived yet, + * so consumers can show a skeleton instead of an empty "no tools" state. */ + mcpToolsLoading: boolean; availableMCPServers: MCPServerDefinition[]; availableMCPServersMap: t.MCPServersListResponse | undefined; }; diff --git a/client/src/components/MCP/McpOAuthDialog.tsx b/client/src/components/MCP/McpOAuthDialog.tsx new file mode 100644 index 0000000000..873478b184 --- /dev/null +++ b/client/src/components/MCP/McpOAuthDialog.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react'; +import { QRCodeSVG } from 'qrcode.react'; +import { QrCode, ExternalLink } from 'lucide-react'; +import { + Input, + Button, + OGDialog, + OGDialogTitle, + OGDialogContent, + OGDialogDescription, +} from '@librechat/client'; +import CopyButton from '~/components/Messages/Content/CopyButton'; +import { useLocalize, useCopyToClipboard } from '~/hooks'; +import { cn } from '~/utils'; + +interface McpOAuthDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + serverName: string; + oauthUrl: string; + /** The MCP server's icon, shown beside the title when the server provides one. */ + iconUrl?: string; +} + +/** + * Dedicated second dialog, opened ONLY when connecting an MCP server requires + * OAuth. Offers three ways to finish: continue in this browser, copy the + * authorization URL to open elsewhere, or reveal a QR code to scan on a phone. + * Auto-closes once the server connects (the caller derives `open` from + * connection state). + */ +export default function McpOAuthDialog({ + open, + onOpenChange, + serverName, + oauthUrl, + iconUrl, +}: McpOAuthDialogProps) { + const localize = useLocalize(); + const [isCopying, setIsCopying] = useState(false); + const [showQR, setShowQR] = useState(false); + const [iconError, setIconError] = useState(false); + const copyUrl = useCopyToClipboard({ text: oauthUrl }); + + if (!oauthUrl) { + return null; + } + + return ( + + +
+ {iconUrl && !iconError && ( + + )} + + {localize('com_nav_mcp_connect_server', { 0: serverName })} + +
+ + {localize('com_ui_mcp_oauth_description')} + + +
+ {/* Auto-height reveal via grid-template-rows 0fr -> 1fr so the QR slides + * open smoothly without a hardcoded height, matching MCPToolItem. */} +
+
+
+
+ +
+ + {localize('com_ui_mcp_oauth_scan_qr')} + +
+
+
+ +
+ event.currentTarget.select()} + className="pr-10 text-text-secondary" + data-testid="mcp-oauth-url" + /> + { + if (!isCopying) { + copyUrl(setIsCopying); + } + }} + className="absolute right-1 top-1/2 -translate-y-1/2" + /> +
+ +
+ + +
+
+
+
+ ); +} diff --git a/client/src/components/MCP/__tests__/McpOAuthDialog.spec.tsx b/client/src/components/MCP/__tests__/McpOAuthDialog.spec.tsx new file mode 100644 index 0000000000..1a747ca32f --- /dev/null +++ b/client/src/components/MCP/__tests__/McpOAuthDialog.spec.tsx @@ -0,0 +1,100 @@ +import type { ReactNode } from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import { render, screen, fireEvent } from '@testing-library/react'; +import McpOAuthDialog from '../McpOAuthDialog'; + +jest.mock('~/hooks', () => ({ + useLocalize: () => (key: string) => key, + useCopyToClipboard: () => jest.fn(), +})); + +jest.mock('~/components/Messages/Content/CopyButton', () => ({ + __esModule: true, + default: ({ onClick }: { onClick: () => void }) => ( + - + ); + })} - +
+ {onCancel && ( + + )} + +
+ ); } diff --git a/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx b/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx index d80e99e04e..0b3c7402cc 100644 --- a/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx +++ b/client/src/components/Plugins/Store/__tests__/PluginAuthForm.spec.tsx @@ -1,5 +1,5 @@ -import { render, screen } from 'test/layout-test-utils'; import userEvent from '@testing-library/user-event'; +import { render, screen } from 'test/layout-test-utils'; import PluginAuthForm from '../PluginAuthForm'; describe('PluginAuthForm', () => { @@ -48,6 +48,17 @@ describe('PluginAuthForm', () => { expect(urlField.parentElement?.querySelector('button')).toBeNull(); }); + it('shows a format-hint placeholder for recognized credential fields', () => { + const openAiPlugin = { + pluginKey: 'dalle', + authConfig: [{ authField: 'DALLE3_API_KEY||DALLE_API_KEY', label: 'OpenAI API Key' }], + }; + //@ts-ignore - dont need all props of plugin + render(); + + expect(screen.getByLabelText('OpenAI API Key')).toHaveAttribute('placeholder', 'sk-...'); + }); + it('calls the onSubmit function with the form data when submitted', async () => { //@ts-ignore - dont need all props of plugin render(); @@ -65,4 +76,29 @@ describe('PluginAuthForm', () => { }, }); }); + + it('reflects an external saving state as a disabled, in-progress submit button', () => { + //@ts-ignore - dont need all props of plugin + render(); + + const button = screen.getByRole('button', { name: 'Saving...' }); + expect(button).toBeDisabled(); + expect(screen.queryByRole('button', { name: 'Save' })).not.toBeInTheDocument(); + }); + + it('renders a Cancel button when onCancel is provided and invokes it on click', async () => { + const onCancel = jest.fn(); + //@ts-ignore - dont need all props of plugin + render(); + + await userEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onSubmit).not.toHaveBeenCalled(); + }); + + it('does not render a Cancel button by default', () => { + //@ts-ignore - dont need all props of plugin + render(); + expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument(); + }); }); diff --git a/client/src/components/SidePanel/Agents/ActionsInput.tsx b/client/src/components/SidePanel/Agents/ActionsInput.tsx index 82b7c7f07b..4311311734 100644 --- a/client/src/components/SidePanel/Agents/ActionsInput.tsx +++ b/client/src/components/SidePanel/Agents/ActionsInput.tsx @@ -1,12 +1,23 @@ import { useState, useEffect } from 'react'; import debounce from 'lodash/debounce'; +import { Maximize2 } from 'lucide-react'; import { useFormContext } from 'react-hook-form'; -import { Spinner, useToastContext } from '@librechat/client'; import { validateAndParseOpenAPISpec, openapiToFunction, AuthTypeEnum, } from 'librechat-data-provider'; +import { + Button, + Spinner, + Textarea, + OGDialog, + OGDialogTitle, + OGDialogHeader, + OGDialogContent, + OGDialogDescription, + useToastContext, +} from '@librechat/client'; import type { Action, FunctionTool, @@ -15,8 +26,8 @@ import type { } from 'librechat-data-provider'; import type { ActionAuthForm } from '~/common'; import type { Spec } from './ActionsTable'; +import { ActionsTable, ActionsTableSkeleton, columns } from './ActionsTable'; import ActionCallback from '~/components/SidePanel/Builder/ActionCallback'; -import { ActionsTable, columns } from './ActionsTable'; import { useUpdateAgentAction } from '~/data-provider'; import { useLocalize } from '~/hooks'; import { logger } from '~/utils'; @@ -29,19 +40,25 @@ const debouncedValidation = debounce( 800, ); +/** Placeholder rows shaped like the "Available actions" table (Name / Method / Path). */ export default function ActionsInput({ action, agent_id, setAction, + onCreated, + footerStart, }: { action?: Action; agent_id?: string; setAction: React.Dispatch>; + onCreated?: () => void; + footerStart?: React.ReactNode; }) { const handleResult = (result: ValidationResult) => { if (!result.status) { setData(null); setFunctions(null); + setIsValidating(false); } setValidationResult(result); }; @@ -51,6 +68,8 @@ export default function ActionsInput({ const { handleSubmit, reset } = useFormContext(); const [validationResult, setValidationResult] = useState(null); const [inputValue, setInputValue] = useState(''); + const [isValidating, setIsValidating] = useState(false); + const [isSchemaDialogOpen, setIsSchemaDialogOpen] = useState(false); const [data, setData] = useState(null); const [functions, setFunctions] = useState(null); @@ -61,7 +80,8 @@ export default function ActionsInput({ return; } setInputValue(rawSpec); - debouncedValidation(rawSpec, handleResult); + setIsValidating(true); + handleResult(validateAndParseOpenAPISpec(rawSpec)); }, [action?.metadata.raw_spec]); useEffect(() => { @@ -82,16 +102,21 @@ export default function ActionsInput({ setData(specs); setValidationResult(null); setFunctions(functionSignatures.map((f) => f.toObjectTool())); + setIsValidating(false); }, [validationResult]); const updateAgentAction = useUpdateAgentAction({ onSuccess(data) { + const wasCreate = !action?.action_id; showToast({ message: localize('com_assistants_update_actions_success'), status: 'success', }); reset(); setAction(data[1]); + if (wasCreate) { + onCreated?.(); + } }, onError(error) { showToast({ @@ -183,8 +208,10 @@ export default function ActionsInput({ if (!newValue) { setData(null); setFunctions(null); + setIsValidating(false); return setValidationResult(null); } + setIsValidating(true); debouncedValidation(newValue, handleResult); }; @@ -200,72 +227,103 @@ export default function ActionsInput({ return localize('com_ui_create'); }; + const validationError = + validationResult && validationResult.message !== 'OpenAPI spec is valid.' + ? validationResult.message + : null; + const showSkeleton = isValidating && !data; + return ( <> -
-
+
+
+
-
-
-