diff --git a/apps/web/src/components/tool-set-selector.tsx b/apps/web/src/components/tool-set-selector.tsx index 7c3601326e..0077e0bde3 100644 --- a/apps/web/src/components/tool-set-selector.tsx +++ b/apps/web/src/components/tool-set-selector.tsx @@ -315,14 +315,22 @@ export function ToolSetSelector({ const connectionTools = selectedConnectionData?.tools ?? []; - // Check if specific tool is enabled + // Check if specific tool is enabled. A ["*"] grant means "all tools". const isToolSelected = (connectionId: string, toolName: string): boolean => { - return toolSet[connectionId]?.includes(toolName) ?? false; + const set = toolSet[connectionId]; + if (!set) return false; + return set.includes("*") || set.includes(toolName); }; // Toggle a single tool const toggleTool = (connectionId: string, toolName: string) => { - const currentTools = toolSet[connectionId] ?? []; + const rawTools = toolSet[connectionId] ?? []; + // Expand an "all tools" sentinel to concrete names first, so toggling one + // tool off leaves the rest selected. connectionTools is loaded for the open + // (selected) connection, which is the only one whose tools can be toggled. + const currentTools = rawTools.includes("*") + ? connectionTools.map((tt) => tt.name) + : rawTools; const isSelected = currentTools.includes(toolName); const newToolSet = { ...toolSet }; @@ -351,9 +359,11 @@ export function ToolSetSelector({ const currentTools = toolSet[connectionId] ?? []; const allToolNames = tools.map((t) => t.name); + // A ["*"] grant counts as everything selected. const allSelected = - currentTools.length > 0 && - allToolNames.every((name) => currentTools.includes(name)); + currentTools.includes("*") || + (currentTools.length > 0 && + allToolNames.every((name) => currentTools.includes(name))); const newToolSet = { ...toolSet }; @@ -453,7 +463,12 @@ export function ToolSetSelector({ {filteredConnections.map((connection) => { const isSelected = selectedConnectionId === connection.id; const totalTools = isSelected ? connectionTools.length : 0; - const activeTools = toolSet[connection.id]?.length ?? 0; + const rawSet = toolSet[connection.id]; + // "*" means all tools — reflect the full count once known. + const activeTools = + rawSet?.includes("*") && isSelected + ? connectionTools.length + : (rawSet?.length ?? 0); return ( = {}; for (const [key, tools] of Object.entries(permission)) { if (key === "self" || key === "models") continue; + // Keep the RAW grant, including a ["*"] "all tools" sentinel. The + // connection LIST endpoint omits `tools`, so expanding "*" here yields [] + // — which renders the connection as unchecked AND destroys the grant on the + // next save. ToolSetSelector understands "*" directly, so preserve it. if (key === "*") { + // Grant applies to every connection. for (const conn of connections) { - toolSet[conn.id] = tools.includes("*") - ? (conn.tools?.map((t) => t.name) ?? []) - : tools; + toolSet[conn.id] = tools; } } else { - const conn = connections.find((c) => c.id === key); - if (conn) { - toolSet[key] = tools.includes("*") - ? (conn.tools?.map((t) => t.name) ?? []) - : tools; - } + // Preserve unconditionally — even if the connection isn't in the current + // (possibly paginated) list, so a re-save round-trips instead of dropping. + toolSet[key] = tools; } }