(props: {
tools: readonly ToolSummary[];
selectedToolId: string | null;
onSelect: (toolId: string) => void;
/** When provided, each row gets a hover-revealed action menu that
* applies (or clears) a user policy for that exact node. Leaf rows
* emit the tool's full dotted id; group rows emit `prefix.*`. */
onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void;
onClearPolicy?: (pattern: string) => void;
/** Maps the displayed row path into the persisted policy pattern. */
patternForDisplay?: (displayPattern: string) => string;
/** Sorted user-authored policies (most-precedent first). Used to
* decide whether a node has its own exact-pattern user rule today
* (so the menu can show a "Clear" option). Optional — when absent,
* the menu hides "Clear". */
policies?: ReadonlyArray<{ readonly pattern: string; readonly action: ToolPolicyAction }>;
/** When true, render top-level sections per (owner, connection) account, each
* badged with its owner (Personal / Workspace). The dotted-name tree renders
* beneath each section. The same tool name can appear under two accounts (NOT
* deduped). When false/unset, render one flat tree (unchanged). */
groupByConnection?: boolean;
})
| 289 | // --------------------------------------------------------------------------- |
| 290 | |
| 291 | export function ToolTree(props: { |
| 292 | tools: readonly ToolSummary[]; |
| 293 | selectedToolId: string | null; |
| 294 | onSelect: (toolId: string) => void; |
| 295 | /** When provided, each row gets a hover-revealed action menu that |
| 296 | * applies (or clears) a user policy for that exact node. Leaf rows |
| 297 | * emit the tool's full dotted id; group rows emit `prefix.*`. */ |
| 298 | onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void; |
| 299 | onClearPolicy?: (pattern: string) => void; |
| 300 | /** Maps the displayed row path into the persisted policy pattern. */ |
| 301 | patternForDisplay?: (displayPattern: string) => string; |
| 302 | /** Sorted user-authored policies (most-precedent first). Used to |
| 303 | * decide whether a node has its own exact-pattern user rule today |
| 304 | * (so the menu can show a "Clear" option). Optional — when absent, |
| 305 | * the menu hides "Clear". */ |
| 306 | policies?: ReadonlyArray<{ readonly pattern: string; readonly action: ToolPolicyAction }>; |
| 307 | /** When true, render top-level sections per (owner, connection) account, each |
| 308 | * badged with its owner (Personal / Workspace). The dotted-name tree renders |
| 309 | * beneath each section. The same tool name can appear under two accounts (NOT |
| 310 | * deduped). When false/unset, render one flat tree (unchanged). */ |
| 311 | groupByConnection?: boolean; |
| 312 | }) { |
| 313 | const { |
| 314 | tools, |
| 315 | selectedToolId, |
| 316 | onSelect, |
| 317 | onSetPolicy, |
| 318 | onClearPolicy, |
| 319 | patternForDisplay = toPolicyPattern, |
| 320 | policies, |
| 321 | groupByConnection, |
| 322 | } = props; |
| 323 | const exactPatterns = useMemo(() => { |
| 324 | if (!policies) return new Map<string, ToolPolicyAction>(); |
| 325 | const m = new Map<string, ToolPolicyAction>(); |
| 326 | for (const p of policies) m.set(p.pattern, p.action); |
| 327 | return m; |
| 328 | }, [policies]); |
| 329 | const [search, setSearch] = useState(""); |
| 330 | const searchRef = useRef<HTMLInputElement>(null); |
| 331 | const selectedRowRef = useRef<HTMLButtonElement>(null); |
| 332 | const ownerDisplay = useOwnerDisplay(); |
| 333 | |
| 334 | const terms = search.trim().toLowerCase().split(/\s+/).filter(Boolean); |
| 335 | |
| 336 | const filteredTools = useMemo(() => { |
| 337 | if (terms.length === 0) return tools; |
| 338 | return tools.filter((t) => { |
| 339 | const corpus = [t.name, t.description ?? ""].join(" ").toLowerCase(); |
| 340 | return terms.every((term) => corpus.includes(term)); |
| 341 | }); |
| 342 | }, [tools, terms]); |
| 343 | |
| 344 | // Account sections (only when grouping). Each carries its already-filtered |
| 345 | // tools so empty sections drop out under an active search. |
| 346 | const accountGroups = useMemo( |
| 347 | () => (groupByConnection ? buildAccountGroups(filteredTools) : []), |
| 348 | [groupByConnection, filteredTools], |
nothing calls this directly
no test coverage detected