From cfec9a268ae9cc6ccdf85a27b7c0462fac3bd39d Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Thu, 5 Mar 2026 17:57:47 -0600 Subject: [PATCH] feat: integrate tools catalog functionality into agent management - Added support for loading and displaying a tools catalog in the agent management interface. - Enhanced the AppViewState to include loading, error, and result states for the tools catalog. - Implemented loadToolsCatalog function to fetch tools based on the selected agent. - Updated UI components to reflect tools catalog loading states and errors. - Refactored agent tools rendering logic to utilize the new tools catalog data structure. --- ui/src/ui/app-render.ts | 29 +++++++- ui/src/ui/app-view-state.ts | 4 + ui/src/ui/app.ts | 4 + ui/src/ui/controllers/agents.ts | 38 +++++++++- ui/src/ui/types.ts | 9 +++ ui/src/ui/views/agents-panels-tools-skills.ts | 73 +++++++++++++++++-- ui/src/ui/views/agents-utils.ts | 60 ++++++++++++++- ui/src/ui/views/agents.ts | 11 +++ 8 files changed, 216 insertions(+), 12 deletions(-) diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 741163bfed3..98699d3b567 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -16,7 +16,7 @@ import type { AppViewState } from "./app-view-state.ts"; import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts"; import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts"; import { loadAgentSkills } from "./controllers/agent-skills.ts"; -import { loadAgents } from "./controllers/agents.ts"; +import { loadAgents, loadToolsCatalog } from "./controllers/agents.ts"; import { loadChannels } from "./controllers/channels.ts"; import { loadChatHistory } from "./controllers/chat.ts"; import { @@ -860,12 +860,25 @@ export function renderApp(state: AppViewState) { agentId: state.agentSkillsAgentId, filter: state.skillsFilter, }, + toolsCatalog: { + loading: state.toolsCatalogLoading, + error: state.toolsCatalogError, + result: state.toolsCatalogResult, + }, onRefresh: async () => { await loadAgents(state); const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? []; if (agentIds.length > 0) { void loadAgentIdentities(state, agentIds); } + const refreshedAgentId = + state.agentsSelectedId ?? + state.agentsList?.defaultId ?? + state.agentsList?.agents?.[0]?.id ?? + null; + if (state.agentsPanel === "tools" && refreshedAgentId) { + void loadToolsCatalog(state, refreshedAgentId); + } }, onSelectAgent: (agentId) => { if (state.agentsSelectedId === agentId) { @@ -881,10 +894,16 @@ export function renderApp(state: AppViewState) { state.agentSkillsReport = null; state.agentSkillsError = null; state.agentSkillsAgentId = null; + state.toolsCatalogResult = null; + state.toolsCatalogError = null; + state.toolsCatalogLoading = false; void loadAgentIdentity(state, agentId); if (state.agentsPanel === "files") { void loadAgentFiles(state, agentId); } + if (state.agentsPanel === "tools") { + void loadToolsCatalog(state, agentId); + } if (state.agentsPanel === "skills") { void loadAgentSkills(state, agentId); } @@ -906,6 +925,14 @@ export function renderApp(state: AppViewState) { void loadAgentSkills(state, resolvedAgentId); } } + if (panel === "tools" && resolvedAgentId) { + if ( + state.toolsCatalogResult?.agentId !== resolvedAgentId || + state.toolsCatalogError + ) { + void loadToolsCatalog(state, resolvedAgentId); + } + } if (panel === "channels") { void loadChannels(state, false); } diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index d014f43ee61..8fa6ccac14f 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -30,6 +30,7 @@ import type { SessionsListResult, SkillStatusReport, StatusSummary, + ToolsCatalogResult, } from "./types.ts"; import type { ChatAttachment, ChatQueueItem } from "./ui-types.ts"; import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts"; @@ -152,6 +153,9 @@ export type AppViewState = { agentsList: AgentsListResult | null; agentsError: string | null; agentsSelectedId: string | null; + toolsCatalogLoading: boolean; + toolsCatalogError: string | null; + toolsCatalogResult: ToolsCatalogResult | null; agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron"; agentFilesLoading: boolean; agentFilesError: string | null; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 4b3a91512e4..8bf039f18e6 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -81,6 +81,7 @@ import type { SkillStatusReport, StatusSummary, NostrProfile, + ToolsCatalogResult, } from "./types.ts"; import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types.ts"; import type { NostrProfileFormState } from "./views/channels.nostr-profile-form.ts"; @@ -267,6 +268,9 @@ export class OpenClawApp extends LitElement { @state() agentsList: AgentsListResult | null = null; @state() agentsError: string | null = null; @state() agentsSelectedId: string | null = null; + @state() toolsCatalogLoading = false; + @state() toolsCatalogError: string | null = null; + @state() toolsCatalogResult: ToolsCatalogResult | null = null; @state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" = "overview"; @state() agentFilesLoading = false; diff --git a/ui/src/ui/controllers/agents.ts b/ui/src/ui/controllers/agents.ts index e63cd9b60b7..e9be0630db2 100644 --- a/ui/src/ui/controllers/agents.ts +++ b/ui/src/ui/controllers/agents.ts @@ -1,5 +1,5 @@ import type { GatewayBrowserClient } from "../gateway.ts"; -import type { AgentsListResult } from "../types.ts"; +import type { AgentsListResult, ToolsCatalogResult } from "../types.ts"; export type AgentsState = { client: GatewayBrowserClient | null; @@ -8,6 +8,9 @@ export type AgentsState = { agentsError: string | null; agentsList: AgentsListResult | null; agentsSelectedId: string | null; + toolsCatalogLoading: boolean; + toolsCatalogError: string | null; + toolsCatalogResult: ToolsCatalogResult | null; }; export async function loadAgents(state: AgentsState) { @@ -35,3 +38,36 @@ export async function loadAgents(state: AgentsState) { state.agentsLoading = false; } } + +export async function loadToolsCatalog(state: AgentsState, agentId: string) { + const resolvedAgentId = agentId.trim(); + if (!state.client || !state.connected || !resolvedAgentId) { + return; + } + if (state.toolsCatalogLoading) { + return; + } + state.toolsCatalogLoading = true; + state.toolsCatalogError = null; + state.toolsCatalogResult = null; + try { + const res = await state.client.request("tools.catalog", { + agentId: resolvedAgentId, + includePlugins: true, + }); + if (state.agentsSelectedId && state.agentsSelectedId !== resolvedAgentId) { + return; + } + state.toolsCatalogResult = res; + } catch (err) { + if (state.agentsSelectedId && state.agentsSelectedId !== resolvedAgentId) { + return; + } + state.toolsCatalogResult = null; + state.toolsCatalogError = String(err); + } finally { + if (!state.agentsSelectedId || state.agentsSelectedId === resolvedAgentId) { + state.toolsCatalogLoading = false; + } + } +} diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index cbbf6a20c71..ba72e647732 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -573,6 +573,15 @@ export type ModelCatalogEntry = { input?: Array<"text" | "image">; }; +export type ToolCatalogProfile = + import("../../../src/gateway/protocol/schema/types.js").ToolCatalogProfile; +export type ToolCatalogEntry = + import("../../../src/gateway/protocol/schema/types.js").ToolCatalogEntry; +export type ToolCatalogGroup = + import("../../../src/gateway/protocol/schema/types.js").ToolCatalogGroup; +export type ToolsCatalogResult = + import("../../../src/gateway/protocol/schema/types.js").ToolsCatalogResult; + export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; export type LogEntry = { diff --git a/ui/src/ui/views/agents-panels-tools-skills.ts b/ui/src/ui/views/agents-panels-tools-skills.ts index 49da26f34bc..7c95ed3dc38 100644 --- a/ui/src/ui/views/agents-panels-tools-skills.ts +++ b/ui/src/ui/views/agents-panels-tools-skills.ts @@ -1,13 +1,15 @@ import { html, nothing } from "lit"; import { normalizeToolName } from "../../../../src/agents/tool-policy-shared.js"; -import type { SkillStatusEntry, SkillStatusReport } from "../types.ts"; +import type { SkillStatusEntry, SkillStatusReport, ToolsCatalogResult } from "../types.ts"; import { + type AgentToolEntry, + type AgentToolSection, isAllowedByPolicy, matchesList, - PROFILE_OPTIONS, resolveAgentConfig, + resolveToolProfileOptions, resolveToolProfile, - TOOL_SECTIONS, + resolveToolSections, } from "./agents-utils.ts"; import type { SkillGroup } from "./skills-grouping.ts"; import { groupSkills } from "./skills-grouping.ts"; @@ -17,12 +19,37 @@ import { renderSkillStatusChips, } from "./skills-shared.ts"; +function renderToolBadges(section: AgentToolSection, tool: AgentToolEntry) { + const source = tool.source ?? section.source; + const pluginId = tool.pluginId ?? section.pluginId; + const badges: string[] = []; + if (source === "plugin" && pluginId) { + badges.push(`plugin:${pluginId}`); + } else if (source === "core") { + badges.push("core"); + } + if (tool.optional) { + badges.push("optional"); + } + if (badges.length === 0) { + return nothing; + } + return html` +
+ ${badges.map((badge) => html`${badge}`)} +
+ `; +} + export function renderAgentTools(params: { agentId: string; configForm: Record | null; configLoading: boolean; configSaving: boolean; configDirty: boolean; + toolsCatalogLoading: boolean; + toolsCatalogError: string | null; + toolsCatalogResult: ToolsCatalogResult | null; onProfileChange: (agentId: string, profile: string | null, clearAllow: boolean) => void; onOverridesChange: (agentId: string, alsoAllow: string[], deny: string[]) => void; onConfigReload: () => void; @@ -32,6 +59,8 @@ export function renderAgentTools(params: { const agentTools = config.entry?.tools ?? {}; const globalTools = config.globalTools ?? {}; const profile = agentTools.profile ?? globalTools.profile ?? "full"; + const profileOptions = resolveToolProfileOptions(params.toolsCatalogResult); + const toolSections = resolveToolSections(params.toolsCatalogResult); const profileSource = agentTools.profile ? "agent override" : globalTools.profile @@ -40,7 +69,11 @@ export function renderAgentTools(params: { const hasAgentAllow = Array.isArray(agentTools.allow) && agentTools.allow.length > 0; const hasGlobalAllow = Array.isArray(globalTools.allow) && globalTools.allow.length > 0; const editable = - Boolean(params.configForm) && !params.configLoading && !params.configSaving && !hasAgentAllow; + Boolean(params.configForm) && + !params.configLoading && + !params.configSaving && + !hasAgentAllow && + !(params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError); const alsoAllow = hasAgentAllow ? [] : Array.isArray(agentTools.alsoAllow) @@ -50,7 +83,7 @@ export function renderAgentTools(params: { const basePolicy = hasAgentAllow ? { allow: agentTools.allow ?? [], deny: agentTools.deny ?? [] } : (resolveToolProfile(profile) ?? undefined); - const toolIds = TOOL_SECTIONS.flatMap((section) => section.tools.map((tool) => tool.id)); + const toolIds = toolSections.flatMap((section) => section.tools.map((tool) => tool.id)); const resolveAllowed = (toolId: string) => { const baseAllowed = isAllowedByPolicy(toolId, basePolicy); @@ -166,6 +199,22 @@ export function renderAgentTools(params: { ` : nothing } + ${ + params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError + ? html` +
Loading runtime tool catalog…
+ ` + : nothing + } + ${ + params.toolsCatalogError + ? html` +
+ Could not load runtime tool catalog. Showing built-in fallback list instead. +
+ ` + : nothing + }
@@ -191,7 +240,7 @@ export function renderAgentTools(params: {
Quick Presets
- ${PROFILE_OPTIONS.map( + ${profileOptions.map( (option) => html`