import { useMemo, useState } from "react"; import { Link } from "@tanstack/react-router"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { ToolAddress, effectivePolicyFromSorted } from "@executor-js/sdk/shared"; import { policiesOptimisticAtom, toolsAllAtom } from "../api/atoms"; import { usePolicyActions } from "../hooks/use-policy-actions"; import { ToolTree, type ToolSummary } from "../components/tool-tree"; import { ToolDetail, ToolDetailEmpty } from "../components/tool-detail"; import { Button } from "../components/button"; import { Skeleton } from "../components/skeleton"; // Dynamic tool policy patterns are derived from the connection-aware address. // Static tools (for example Executor's own tools) use their address directly. type ToolRow = { readonly address: ToolAddress; readonly integration: string; readonly name: string; readonly description: string; readonly requiresApproval?: boolean; readonly static?: boolean; }; const policyId = (tool: ToolRow): string => tool.static ? String(tool.address) : `${tool.integration}.${tool.name}`; export function ToolsPage() { // Merged across BOTH owners (omit-owner read). The page dedupes to one row per // `.` policy id, so owner is irrelevant here — the global // Tools page is a flat policy tree, not an account-grouped view. Policy writes // still target a specific owner (Workspace default; see usePolicyActions). const tools = useAtomValue(toolsAllAtom); const policies = useAtomValue(policiesOptimisticAtom); const policyActions = usePolicyActions("org"); const [selectedToolId, setSelectedToolId] = useState(null); const policyList = useMemo( () => (AsyncResult.isSuccess(policies) ? policies.value : []), [policies], ); const sortedPolicies = useMemo( () => [...policyList].sort((a, b) => { if (a.position < b.position) return -1; if (a.position > b.position) return 1; return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; }), [policyList], ); // Address + static flag per selection id, so the detail view can fetch the // right schema for the selected `.` id and its policy // badge can write the matching pattern form. const selectionById = useMemo(() => { const map = new Map(); if (!AsyncResult.isSuccess(tools)) return map; for (const t of tools.value as readonly ToolRow[]) { const id = policyId(t); if (!map.has(id)) map.set(id, { address: t.address, static: t.static === true }); } return map; }, [tools]); const summaries: ToolSummary[] = useMemo(() => { if (!AsyncResult.isSuccess(tools)) return []; const seen = new Set(); const rows: ToolSummary[] = []; for (const t of tools.value as readonly ToolRow[]) { const id = policyId(t); if (seen.has(id)) continue; seen.add(id); // `id` is the connection-agnostic display id; match policies against the // FULL address (`integration.owner.connection.tool`, the address minus its // `tools.` prefix) so connection-aware rules resolve. const matchId = String(t.address).replace(/^tools\./, ""); rows.push({ id, name: id, description: t.description, policy: effectivePolicyFromSorted(matchId, sortedPolicies, t.requiresApproval), }); } return rows; }, [tools, sortedPolicies]); const selectedTool = useMemo( () => summaries.find((t) => t.id === selectedToolId) ?? null, [summaries, selectedToolId], ); const selection = selectedToolId ? (selectionById.get(selectedToolId) ?? null) : null; const selectedAddress = selection?.address ?? null; return (
{/* Header bar */}

Tools

{AsyncResult.isSuccess(tools) && ( {summaries.length} {summaries.length === 1 ? "tool" : "tools"} )}
{AsyncResult.match(tools, { onInitial: () => , onFailure: () =>
Failed to load tools
, onSuccess: () => summaries.length === 0 ? (

No tools registered

Add an integration to start discovering tools.

) : (
void policyActions.set(pattern, action)} onClearPolicy={(pattern) => void policyActions.clear(pattern)} policies={sortedPolicies} />
{selectedTool && selectedAddress ? ( void policyActions.set(pattern, action)} onClearPolicy={(pattern) => void policyActions.clear(pattern)} /> ) : ( 0} /> )}
), })}
); } function ToolsPageSkeleton() { return (
{Array.from({ length: 8 }).map((_, i) => (
))}
); }