From 9551d5ec9e5daedb3c267e632c88e696155b139e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:25:48 +0200 Subject: [PATCH 1/2] fix(frontend): resolve per-repo git tools in the playbook editor's call picker The tool catalog lists each git tool once under the logical server "triagent-git", but every linked repo runs its own server named triagent-git-, and that aliased form is what a playbook's suggested_calls carry. The picker matched server/name exactly against the logical catalog, so every git call in a real playbook showed the "unknown" badge, the dropdown could not offer the aliased tools, and the input hints never appeared for them. Expand the catalog in the editor with one copy of each git tool per linked repo alias, fed by the existing /api/repos endpoint. The logical entry stays so system playbooks written against the template form still resolve. Co-Authored-By: Claude Fable 5.1 --- .../components/playbooks/PlaybookEditor.tsx | 14 +++++- frontend/components/playbooks/ToolPicker.tsx | 2 +- frontend/lib/mcps.test.ts | 47 ++++++++++++++++++- frontend/lib/mcps.ts | 26 +++++++++- 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/frontend/components/playbooks/PlaybookEditor.tsx b/frontend/components/playbooks/PlaybookEditor.tsx index 7748dd60..bb1296c8 100644 --- a/frontend/components/playbooks/PlaybookEditor.tsx +++ b/frontend/components/playbooks/PlaybookEditor.tsx @@ -9,7 +9,8 @@ import { useState, } from "react"; import { useSearchParams } from "next/navigation"; -import { api, ApiError, type Capabilities, type SyncState, type ToolEntry } from "@/lib/api"; +import { api, ApiError, type Capabilities, type LinkedRepo, type SyncState, type ToolEntry } from "@/lib/api"; +import { expandRepoAliases } from "@/lib/mcps"; import { useDialog } from "@/lib/dialog"; import { EditorChatDrawer } from "@/components/playbooks/EditorChatDrawer"; import { ProposalCard, type ProposalDraftPayload } from "@/components/playbooks/ProposalCard"; @@ -83,6 +84,11 @@ export function PlaybookEditor({ id, onBack, onMutated, onOpenPlaybook }: Props) // "latest" instead of the freshly-created commit. const [refetchKey, setRefetchKey] = useState(0); const [tools, setTools] = useState([]); + const [linkedRepos, setLinkedRepos] = useState([]); + // The picker resolves suggested_calls against per-repo server names + // (triagent-git-), so the logical catalog is expanded with + // one entry per linked repo before it reaches the node editor. + const catalog = useMemo(() => expandRepoAliases(tools, linkedRepos), [tools, linkedRepos]); const [capabilities, setCapabilities] = useState(null); const [selectedNode, setSelectedNode] = useState(null); // Right-aside view: "node" is the per-node editor; "tags" surfaces @@ -555,6 +561,10 @@ export function PlaybookEditor({ id, onBack, onMutated, onOpenPlaybook }: Props) // on capabilities.gh.authenticated && capabilities.repoPath.valid. useEffect(() => { api.listTools().then(setTools).catch(() => setTools([])); + api + .listRepos() + .then((r) => setLinkedRepos([...r.defaults, ...r.user])) + .catch(() => setLinkedRepos([])); api .listPlaybookTypes() .then((types) => @@ -1390,7 +1400,7 @@ export function PlaybookEditor({ id, onBack, onMutated, onOpenPlaybook }: Props) node={node} allNodeIds={allNodeIds} allPlaybookIds={allPlaybookIds.filter((p) => p !== draft.id)} - catalog={tools} + catalog={catalog} readOnly={readOnly} onChange={(next) => { if (!selectedNode) return; diff --git a/frontend/components/playbooks/ToolPicker.tsx b/frontend/components/playbooks/ToolPicker.tsx index 7f486f22..2b292f73 100644 --- a/frontend/components/playbooks/ToolPicker.tsx +++ b/frontend/components/playbooks/ToolPicker.tsx @@ -74,7 +74,7 @@ export function ToolPicker({ value, onChange, catalog, disabled }: Props) { {value && !known && ( unknown diff --git a/frontend/lib/mcps.test.ts b/frontend/lib/mcps.test.ts index 7a4e496c..310f1c14 100644 --- a/frontend/lib/mcps.test.ts +++ b/frontend/lib/mcps.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import type { Investigation } from "@/lib/api"; -import { activeMCPs, chipClasses } from "@/lib/mcps"; +import type { Investigation, LinkedRepo, ToolEntry } from "@/lib/api"; +import { activeMCPs, chipClasses, expandRepoAliases } from "@/lib/mcps"; function makeInv(overrides: Partial = {}): Investigation { return { @@ -47,3 +47,46 @@ describe("activeMCPs cloud sources", () => { expect(chipClasses("cloud")).not.toBe(chipClasses("docs")); }); }); + +describe("expandRepoAliases", () => { + const gitTool: ToolEntry = { + server: "triagent-git", + name: "latest_tags", + description: "List tags", + inputs: [{ name: "limit", type: "integer" }], + }; + const k8sTool: ToolEntry = { server: "triagent-k8s", name: "get_logs", description: "Logs" }; + + it("adds one copy of every git tool per linked repo alias, keeping the logical entry", () => { + const out = expandRepoAliases( + [k8sTool, gitTool], + [ + { owner: "camunda", name: "camunda-operator" }, + { owner: "camunda", name: "saas-argocd-apps", alias: "argocd" }, + ], + ); + expect(out.map((t) => `${t.server}/${t.name}`)).toEqual([ + "triagent-k8s/get_logs", + "triagent-git/latest_tags", + "triagent-git-camunda-operator/latest_tags", + "triagent-git-argocd/latest_tags", + ]); + // The aliased copy carries the same inputs so the editor can offer args. + expect(out[2].inputs).toEqual(gitTool.inputs); + }); + + it("dedupes repos that resolve to the same alias", () => { + const out = expandRepoAliases( + [gitTool], + [ + { owner: "camunda", name: "zeebe" }, + { owner: "fork", name: "zeebe" }, + ], + ); + expect(out.map((t) => t.server)).toEqual(["triagent-git", "triagent-git-zeebe"]); + }); + + it("returns the catalog untouched when no repos are linked", () => { + expect(expandRepoAliases([k8sTool, gitTool], [])).toEqual([k8sTool, gitTool]); + }); +}); diff --git a/frontend/lib/mcps.ts b/frontend/lib/mcps.ts index d3956a5c..773f1eae 100644 --- a/frontend/lib/mcps.ts +++ b/frontend/lib/mcps.ts @@ -2,7 +2,7 @@ // already on the Investigation DTO (promEnabled / docsPrefix / linkedRepos); // this module just turns it into a renderable list and sorts the entries. -import type { Investigation, MCPCallStats, MCPProbeResult, ToolEntry } from "./api"; +import type { Investigation, LinkedRepo, MCPCallStats, MCPProbeResult, ToolEntry } from "./api"; export type MCPCategory = "core" | "metrics" | "docs" | "git" | "wiki" | "slack" | "incidentio" | "cloud"; @@ -137,6 +137,30 @@ export function logicalServer(wireAlias: string): string { return wireAlias; } +// expandRepoAliases turns the launcher's logical git tool set into the +// per-repo entries a playbook actually addresses. The catalog lists each +// git tool once under "triagent-git", but every linked repo runs its own +// server named triagent-git-, and that aliased form is what +// suggested_calls carry. The logical entry stays so system playbooks +// written against the template form still resolve; one copy per alias +// follows in repo order, deduped when two repos resolve to one alias. +export function expandRepoAliases(tools: ToolEntry[], repos: LinkedRepo[]): ToolEntry[] { + const aliases: string[] = []; + for (const r of repos) { + const alias = r.alias || r.name; + if (!aliases.includes(alias)) aliases.push(alias); + } + if (aliases.length === 0) return tools; + const gitTools = tools.filter((t) => t.server === "triagent-git"); + const out = [...tools]; + for (const alias of aliases) { + for (const t of gitTools) { + out.push({ ...t, server: `triagent-git-${alias}` }); + } + } + return out; +} + // groupTools buckets the flat tool list by server alias. Returns a Map // so callers can iterate insertion-order-preserving. export function groupTools(tools: ToolEntry[]): Map { From 03dea4b8c95a2d67036cefc82f46e6a2654162df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=86gir=20M=C3=A1ni=20Hauksson?= <54936225+sourcehawk@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:31:21 +0200 Subject: [PATCH 2/2] refactor(frontend): dedupe repo aliases with a Set Co-Authored-By: Claude Fable 5.1 --- frontend/lib/mcps.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/lib/mcps.ts b/frontend/lib/mcps.ts index 773f1eae..2c3f80c2 100644 --- a/frontend/lib/mcps.ts +++ b/frontend/lib/mcps.ts @@ -145,12 +145,8 @@ export function logicalServer(wireAlias: string): string { // written against the template form still resolve; one copy per alias // follows in repo order, deduped when two repos resolve to one alias. export function expandRepoAliases(tools: ToolEntry[], repos: LinkedRepo[]): ToolEntry[] { - const aliases: string[] = []; - for (const r of repos) { - const alias = r.alias || r.name; - if (!aliases.includes(alias)) aliases.push(alias); - } - if (aliases.length === 0) return tools; + const aliases = new Set(repos.map((r) => r.alias || r.name)); + if (aliases.size === 0) return tools; const gitTools = tools.filter((t) => t.server === "triagent-git"); const out = [...tools]; for (const alias of aliases) {