(props: {
tools: readonly ToolSummary[];
selectedToolId: string | null;
onSelect: (toolId: string) => void;
onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void;
onClearPolicy?: (pattern: string) => void;
patternForDisplay: (displayPattern: string) => string;
exactPatterns: ReadonlyMap<string, ToolPolicyAction>;
search: string;
terms: readonly string[];
selectedRowRef: React.Ref<HTMLButtonElement>;
})
| 452 | // --------------------------------------------------------------------------- |
| 453 | |
| 454 | function ToolTreeBody(props: { |
| 455 | tools: readonly ToolSummary[]; |
| 456 | selectedToolId: string | null; |
| 457 | onSelect: (toolId: string) => void; |
| 458 | onSetPolicy?: (pattern: string, action: ToolPolicyAction) => void; |
| 459 | onClearPolicy?: (pattern: string) => void; |
| 460 | patternForDisplay: (displayPattern: string) => string; |
| 461 | exactPatterns: ReadonlyMap<string, ToolPolicyAction>; |
| 462 | search: string; |
| 463 | terms: readonly string[]; |
| 464 | selectedRowRef: React.Ref<HTMLButtonElement>; |
| 465 | }) { |
| 466 | const { |
| 467 | tools, |
| 468 | selectedToolId, |
| 469 | onSelect, |
| 470 | onSetPolicy, |
| 471 | onClearPolicy, |
| 472 | patternForDisplay, |
| 473 | exactPatterns, |
| 474 | search, |
| 475 | terms, |
| 476 | selectedRowRef, |
| 477 | } = props; |
| 478 | const [manualOpen, setManualOpen] = useState<Set<string>>(() => new Set()); |
| 479 | |
| 480 | const tree = useMemo(() => buildTree(tools), [tools]); |
| 481 | |
| 482 | // When searching, expand everything so matches are visible. |
| 483 | // Also auto-expand groups that contain the selected tool. |
| 484 | const openSet = useMemo(() => { |
| 485 | if (terms.length > 0) { |
| 486 | const all = new Set<string>(); |
| 487 | collectGroupPaths(tree, all); |
| 488 | return all; |
| 489 | } |
| 490 | const set = new Set(manualOpen); |
| 491 | if (selectedToolId) { |
| 492 | const parts = selectedToolId.split("."); |
| 493 | // Progressively add ancestor paths (best-effort, based on dotted name). |
| 494 | let acc = ""; |
| 495 | for (let i = 0; i < parts.length - 1; i++) { |
| 496 | acc = acc ? `${acc}.${parts[i]}` : parts[i]!; |
| 497 | set.add(acc); |
| 498 | } |
| 499 | } |
| 500 | return set; |
| 501 | }, [tree, manualOpen, selectedToolId, terms.length]); |
| 502 | |
| 503 | const rows = useMemo(() => { |
| 504 | const acc: Row[] = []; |
| 505 | flattenTree(tree, 0, openSet, acc); |
| 506 | return acc; |
| 507 | }, [tree, openSet]); |
| 508 | |
| 509 | const toggleGroup = (path: string) => { |
| 510 | setManualOpen((prev) => { |
| 511 | const next = new Set(prev); |
nothing calls this directly
no test coverage detected