import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useAtomValue } from "@effect/atom-react"; import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"; import { toolSchemaAtom } from "../api/atoms"; import { type ToolAddress, type EffectivePolicy, type ToolPolicyAction, type Connection, type ConnectionName, type IntegrationSlug, } from "@executor-js/sdk/shared"; import { Badge } from "./badge"; import { Button } from "./button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu"; import { Markdown } from "./markdown"; import { SchemaExplorer } from "./schema-explorer"; import { ExpandableCodeBlock } from "./expandable-code-block"; import { ToolRunPanel } from "./tool-run-panel"; import { CardStack, CardStackHeader, CardStackContent } from "./card-stack"; import { CopyButton } from "./copy-button"; import { ChevronRight, ChevronDownIcon } from "lucide-react"; import { cn } from "../lib/utils"; import { toPolicyPattern } from "../lib/policy-pattern"; import { trackEvent } from "../api/analytics"; import { POLICY_ACTION_LABEL, POLICY_ACTIONS_IN_ORDER, POLICY_BADGE_VARIANT, POLICY_STATE_LABEL, } from "../lib/policy-display"; // Render the effective policy as a badge. User policies show the // matched pattern; plugin defaults read "Default: ". Silent for // the always-run plugin default — that's the safe state and the // header would just be noise. const policyBadgeFor = (policy: EffectivePolicy) => { if (policy.source === "plugin-default" && policy.action === "approve") { return null; } if (policy.source === "user") { return { variant: POLICY_BADGE_VARIANT[policy.action], title: `Matched policy: ${policy.pattern}`, text: `${POLICY_STATE_LABEL[policy.action]} · ${policy.pattern}`, className: "font-mono text-[10px]", }; } return { variant: "outline" as const, title: "No matching policy — plugin default applies", text: `Default: ${POLICY_STATE_LABEL[policy.action]}`, className: "text-[10px] text-muted-foreground", }; }; function EmptySection(props: { title: string; message: string }) { return ( {props.title}

{props.message}

); } function ToolDescription(props: { description: string }) { const { description } = props; const contentRef = useRef(null); const [expanded, setExpanded] = useState(false); const [canExpand, setCanExpand] = useState( description.length > 160 || description.includes("\n"), ); useEffect(() => { setExpanded(false); setCanExpand(description.length > 160 || description.includes("\n")); }, [description]); useEffect(() => { if (expanded) return; const element = contentRef.current; if (!element) return; const measure = () => { setCanExpand( element.scrollHeight > element.clientHeight + 1 || description.length > 160 || description.includes("\n"), ); }; measure(); const win = element.ownerDocument.defaultView; win?.addEventListener("resize", measure); return () => win?.removeEventListener("resize", measure); }, [description, expanded]); return (
{description}
{canExpand && ( )}
); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- const friendlyName = (name: string): string => { const leaf = name.split(".").pop() ?? name; return leaf .replace(/([a-z])([A-Z])/g, "$1 $2") .replace(/[_.-]/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); }; const breadcrumbParts = (name: string): string[] => name.split(".").map((p) => p .replace(/([a-z])([A-Z])/g, "$1 $2") .replace(/[_-]/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()), ); // --------------------------------------------------------------------------- // ToolDetail // --------------------------------------------------------------------------- export function ToolDetail(props: { /** Full per-connection tool address `tools....`. */ address: ToolAddress; /** Policy id `.` — the tree path and display value. */ toolName: string; /** True for plugin-contributed static tools (policy-matched on their * address verbatim, not the connection-wildcarded pattern). */ staticTool?: boolean; /** Resolved effective policy — user-authored or plugin-default, * unified into one shape. Surfaces in the header. */ policy?: EffectivePolicy; /** When provided, the policy badge becomes a dropdown trigger that * applies a user rule to this tool's exact id. */ onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void; onClearPolicy?: (pattern: string, policyId?: string) => void; patternForDisplay?: (displayPattern: string) => string; /** Run-tab wiring. When `integration` + `runToolName` are provided, a third * "Run" tab hosts the per-connection tool tester. */ integration?: IntegrationSlug; /** Bare `` address segment (may contain dots) for the Run tab. */ runToolName?: string; /** This integration's connections across both owners. */ connections?: readonly Connection[]; /** Pre-select this connection in the Run tab (the selected tool's account). */ initialConnectionName?: ConnectionName | string | null; }) { const toolContract = useAtomValue(toolSchemaAtom(props.address)); const [tab, setTab] = useState<"schema" | "typescript" | "run">("schema"); const canRun = props.integration != null && props.runToolName != null; // Don't strand the user on the Run tab when this ToolDetail has no run wiring. const activeTab = tab === "run" && !canRun ? "schema" : tab; const isBlocked = props.policy?.action === "block"; const data = useMemo(() => { if (!AsyncResult.isSuccess(toolContract)) return null; const v = toolContract.value; const definitions = Object.entries(v.typeScriptDefinitions ?? {}).map(([name, body]) => ({ name, code: String(body), })); return { description: v.description, inputSchema: v.inputSchema, outputSchema: v.outputSchema, schemaDefinitions: v.schemaDefinitions, inputTypeScript: v.inputTypeScript ? `type Input = ${v.inputTypeScript}` : null, outputTypeScript: v.outputTypeScript ? `type Output = ${v.outputTypeScript}` : null, definitions, }; }, [toolContract]); const crumbs = breadcrumbParts(props.toolName); const displayName = friendlyName(props.toolName); return (
{/* Header + tabs */}
{crumbs.length > 1 && (
{crumbs.slice(0, -1).map((part, i) => ( {i > 0 && } {part} ))}
)}

{displayName}

{ const nameParts = props.toolName.split("."); trackEvent("tool_id_copied", { integration_slug: nameParts[0] ?? props.toolName, tool_name: nameParts.slice(1).join(".") || props.toolName, }); }} />
{data?.description && } {/* Tabs */}
{canRun && ( )}
{/* Content */}
{isBlocked ? (
) : ( AsyncResult.match(toolContract, { onInitial: () =>
Loading…
, onFailure: () => (
Something went wrong
), onSuccess: () => activeTab === "run" && props.integration && props.runToolName ? (
) : activeTab === "schema" ? (
{data?.inputSchema ? ( ) : ( )} {data?.outputSchema ? ( ) : ( )}
) : ( ), }) )}
); } function ToolTypeScriptPanel(props: { inputTypeScript: string | null; outputTypeScript: string | null; definitions: ReadonlyArray<{ name: string; code: string }>; }) { return (
{props.inputTypeScript ? ( Input ) : ( )} {props.outputTypeScript ? ( Output ) : ( )}
); } // --------------------------------------------------------------------------- // PolicyBadgeMenu — clickable header badge that opens the same // Always run / Require approval / Block / Clear menu the tree row uses. // Falls back to a plain Badge when no actions are provided. // --------------------------------------------------------------------------- function PolicyBadgeMenu(props: { toolName: string; /** Static (plugin-contributed) tools are policy-matched on their full * address verbatim; dynamic tools on the connection-wildcarded form. */ staticTool?: boolean; policy?: EffectivePolicy; onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void; onClearPolicy?: (pattern: string, policyId?: string) => void; patternForDisplay?: (displayPattern: string) => string; }) { const interactive = !!props.onSetPolicy; // The same pattern bridge the tree rows apply — the pattern WRITTEN and the // pattern LOOKED UP must be the same string, or the menu authors rules that // never match and can't see its own rule afterward. const pattern = props.staticTool ? props.toolName : (props.patternForDisplay ?? toPolicyPattern)(props.toolName); // The "Clear" affordance only makes sense when there's a user rule // pinned to this exact tool id — clearing a wildcard rule from a // single tool's detail header would silently affect siblings. const hasExactUserRule = props.policy?.source === "user" && props.policy.pattern === pattern; const currentAction = hasExactUserRule ? props.policy?.action : undefined; if (!interactive) { if (!props.policy) return null; const badge = policyBadgeFor(props.policy); if (!badge) return null; return ( {badge.text} ); } // Interactive trigger always renders, even when the effective policy // would otherwise be "silent" (auto-approve plugin-default), so the // user can click it to override. const badge = props.policy ? policyBadgeFor(props.policy) : null; const triggerLabel = badge?.text ?? "Set policy"; const triggerVariant = badge?.variant ?? "outline"; const triggerTitle = badge?.title ?? "Set policy"; const triggerClassName = badge?.className ?? "text-[10px] text-muted-foreground"; return ( {pattern} {POLICY_ACTIONS_IN_ORDER.map((action) => ( props.onSetPolicy?.(pattern, action)}> {POLICY_ACTION_LABEL[action]} {currentAction === action && ( ✓ )} ))} {hasExactUserRule && props.onClearPolicy && ( <> props.onClearPolicy?.(pattern, props.policy?.policyId)} className="text-muted-foreground" > Clear )} ); } // --------------------------------------------------------------------------- // Empty state // --------------------------------------------------------------------------- export function ToolDetailEmpty(props: { hasTools: boolean; title?: string; description?: string; action?: ReactNode; }) { const title = props.title ?? (props.hasTools ? "Select a tool" : "No tools available"); return (

{title}

{props.description || props.hasTools ? (

{props.description ?? "Choose from the list to see what it does."}

) : null} {props.action ?
{props.action}
: null}
); }