import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import type { Loadable } from "../../core/types";import { usePrefetchToolDetail,} from "../../hooks/sources";import type { SourceInspection, SourceInspectionToolDetail,} from "@executor/platform-sdk/schema";import { MethodBadge } from "./ui/badge";import { CodeBlock } from "./code-block";import { DocumentPanel } from "./document-panel";import { EmptyState, LoadableBlock } from "./loadable";import { Markdown } from "./markdown";import { IconCheck, IconChevron, IconClose, IconCopy, IconFolder, IconSearch, IconTool,} from "./icons";import { ToolPermissionBadge, ToolPermissionDot, resolveToolPermission, type ToolPermissionLevel } from "./tool-permission-badge";import type { LocalScopePolicy } from "@executor/platform-sdk/schema";import { Button } from "./ui/button";import { Input } from "./ui/input";import { cn } from "../lib/cn"; type ToolTreeNode = { segment: string; tool?: SourceInspectionToolDetail["summary"] | SourceInspection["tools"][number]; children: Map<string, ToolTreeNode>;}; export type SourceToolDiscoveryResult = { query: string; results: ReadonlyArray<{ path: string; score: number; description?: string | null; }>;}; export type ToolPermissionChangeRequest = { toolPath: string; level: ToolPermissionLevel;}; export type SourceToolDetailPanelProps = { detail: SourceInspectionToolDetail; policies?: ReadonlyArray<LocalScopePolicy>; onSetPermission?: (request: ToolPermissionChangeRequest) => void | Promise<void>; permissionPending?: boolean; renderHeaderMeta?: (detail: SourceInspectionToolDetail) => ReactNode; renderSchemaExtras?: (detail: SourceInspectionToolDetail) => ReactNode;}; export type SourceToolModelWorkbenchProps = { bundle: SourceInspection; detail: Loadable<SourceInspectionToolDetail | null>; selectedToolPath: string | null; onSelectTool: (toolPath: string) => void; sourceId: string; policies?: ReadonlyArray<LocalScopePolicy>; onSetPermission?: (request: ToolPermissionChangeRequest) => void | Promise<void>; permissionPending?: boolean; renderDetail?: (detail: SourceInspectionToolDetail) => ReactNode;}; export type SourceToolDiscoveryPanelProps< TResult extends SourceToolDiscoveryResult = SourceToolDiscoveryResult,> = { discovery: Loadable<TResult>; initialQuery: string; onSubmitQuery: (query: string) => void; onOpenTool: (toolPath: string) => void;}; const buildToolTree = (tools: SourceInspection["tools"]): ToolTreeNode => { const root: ToolTreeNode = { segment: "", children: new Map(), }; for (const tool of tools) { const parts = tool.path.split("."); let node = root; for (const part of parts) { const existing = node.children.get(part); if (existing) { node = existing; continue; } const next: ToolTreeNode = { segment: part, children: new Map(), }; node.children.set(part, next); node = next; } node.tool = tool; } return root;}; const countToolLeaves = (node: ToolTreeNode): number => { let count = node.tool ? 1 : 0; for (const child of node.children.values()) { count += countToolLeaves(child); } return count;}; const highlightMatch = (text: string, search: string) => { if (!search.trim()) { return text; } const terms = search.trim().toLowerCase().split(/\s+/); const lowerText = text.toLowerCase(); const ranges: Array<[number, number]> = []; for (const term of terms) { let index = 0; while (index < lowerText.length) { const found = lowerText.indexOf(term, index); if (found === -1) { break; } ranges.push([found, found + term.length]); index = found + 1; } } if (ranges.length === 0) { return text; } ranges.sort((left, right) => left[0] - right[0]); const merged: Array<[number, number]> = [ranges[0]!]; for (let index = 1; index < ranges.length; index++) { const last = merged[merged.length - 1]!; const current = ranges[index]!; if (current[0] <= last[1]) { last[1] = Math.max(last[1], current[1]); } else { merged.push(current); } } const parts: Array<{ text: string; highlighted: boolean }> = []; let cursor = 0; for (const [start, end] of merged) { if (cursor < start) { parts.push({ text: text.slice(cursor, start), highlighted: false }); } parts.push({ text: text.slice(start, end), highlighted: true }); cursor = end; } if (cursor < text.length) { parts.push({ text: text.slice(cursor), highlighted: false }); } return ( <> {parts.map((part, index) => part.highlighted ? ( <mark key={index} className="rounded-sm bg-primary/20 px-px text-foreground"> {part.text} </mark> ) : ( <span key={index}>{part.text}</span> ), )} </> );}; const CopyButton = (props: { text: string; field: string; copiedField: string | null; onCopy: (text: string, field: string) => void | Promise<void>;}) => ( <Button type="button" variant="ghost" size="icon" onClick={() => { void props.onCopy(props.text, props.field); }} className="size-6 shrink-0 text-muted-foreground/30 hover:text-muted-foreground" title={`Copy ${props.field}`} > {props.copiedField === props.field ? <IconCheck /> : <IconCopy />} </Button>); const SourceToolTree = (props: { tools: SourceInspection["tools"]; selectedToolPath: string | null; onSelectTool: (path: string) => void; search: string; isFiltered: boolean; sourceId: string; policies?: ReadonlyArray<LocalScopePolicy>;}) => { const tree = useMemo(() => buildToolTree(props.tools), [props.tools]); const prefetch = usePrefetchToolDetail(); const entries = [...tree.children.values()].sort((left, right) => left.segment.localeCompare(right.segment) ); return ( <div className="flex flex-col gap-px"> {entries.map((node) => ( <SourceToolTreeNodeView key={node.segment} node={node} depth={0} selectedToolPath={props.selectedToolPath} onSelectTool={props.onSelectTool} search={props.search} defaultOpen={props.isFiltered} sourceId={props.sourceId} prefetch={prefetch} policies={props.policies} /> ))} </div> );}; const SourceToolTreeNodeView = (props: { node: ToolTreeNode; depth: number; selectedToolPath: string | null; onSelectTool: (path: string) => void; search: string; defaultOpen: boolean; sourceId: string; prefetch: ReturnType<typeof usePrefetchToolDetail>; policies?: ReadonlyArray<LocalScopePolicy>;}) => { const { node, depth, selectedToolPath, onSelectTool, search, defaultOpen, sourceId, prefetch, } = props; const hasChildren = node.children.size > 0; const isLeaf = !!node.tool && !hasChildren; const hasSelectedDescendant = useMemo(() => { if (!selectedToolPath) { return false; } const check = (candidate: ToolTreeNode): boolean => { if (candidate.tool?.path === selectedToolPath) { return true; } for (const child of candidate.children.values()) { if (check(child)) { return true; } } return false; }; return check(node); }, [node, selectedToolPath]); const [open, setOpen] = useState(defaultOpen || hasSelectedDescendant); useEffect(() => { if (defaultOpen || hasSelectedDescendant) { setOpen(true); } }, [defaultOpen, hasSelectedDescendant]); if (isLeaf) { return ( <SourceToolListItem tool={node.tool as SourceInspection["tools"][number]} active={node.tool?.path === selectedToolPath} onSelect={() => onSelectTool(node.tool!.path)} search={search} depth={depth} sourceId={sourceId} prefetch={prefetch} policies={props.policies} /> ); } const paddingLeft = 8 + depth * 16; const sortedChildren = [...node.children.values()].sort((left, right) => left.segment.localeCompare(right.segment) ); const leafCount = countToolLeaves(node); return ( <div> {node.tool ? ( <div className="flex items-center"> <Button type="button" variant="ghost" size="icon" onClick={() => setOpen((value) => !value)} aria-expanded={open} className="shrink-0 size-auto rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground" style={{ marginLeft: paddingLeft }} > <IconChevron className="shrink-0 transition-transform duration-150" style={{ width: 8, height: 8, transform: open ? "rotate(90deg)" : "rotate(0deg)", transformOrigin: "50% 50%", }} /> </Button> <SourceToolListItem tool={node.tool as SourceInspection["tools"][number]} active={node.tool?.path === selectedToolPath} onSelect={() => onSelectTool(node.tool!.path)} search={search} depth={-1} className="flex-1 pl-1" sourceId={sourceId} prefetch={prefetch} policies={props.policies} /> </div> ) : ( <Button type="button" variant="ghost" onClick={() => setOpen((value) => !value)} aria-expanded={open} className={cn( "group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40", open ? "text-foreground/80" : "text-muted-foreground/60", )} style={{ paddingLeft }} > <IconChevron className="shrink-0 text-muted-foreground/30 transition-transform duration-150" style={{ width: 8, height: 8, transform: open ? "rotate(90deg)" : "rotate(0deg)", transformOrigin: "50% 50%", }} /> <IconFolder className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")} style={{ width: 12, height: 12 }} /> <span className="flex-1 truncate text-left font-mono"> {highlightMatch(node.segment, search)} </span> <span className="shrink-0 tabular-nums text-[10px] text-muted-foreground/25"> {leafCount} </span> </Button> )} {open && hasChildren && ( <div className="relative flex flex-col gap-px"> <span className="absolute bottom-1 top-0 w-px bg-border/40" style={{ left: paddingLeft + 5 }} aria-hidden /> {sortedChildren.map((child) => ( <SourceToolTreeNodeView key={child.segment} node={child} depth={depth + 1} selectedToolPath={selectedToolPath} onSelectTool={onSelectTool} search={search} defaultOpen={defaultOpen} sourceId={sourceId} prefetch={prefetch} policies={props.policies} /> ))} </div> )} </div> );}; const SourceToolListItem = (props: { tool: SourceInspection["tools"][number]; active: boolean; onSelect: () => void; search: string; depth: number; className?: string; sourceId: string; prefetch: ReturnType<typeof usePrefetchToolDetail>; policies?: ReadonlyArray<LocalScopePolicy>;}) => { const ref = useRef<HTMLButtonElement>(null); const paddingLeft = props.depth >= 0 ? 8 + props.depth * 16 + 8 : undefined; useEffect(() => { if (props.active && ref.current) { ref.current.scrollIntoView({ block: "nearest" }); } }, [props.active]); const label = props.depth >= 0 ? props.tool.path.split(".").pop() ?? props.tool.path : props.tool.path; return ( <Button ref={ref} type="button" variant="ghost" onMouseEnter={() => { props.prefetch(props.sourceId, props.tool.path); }} onClick={props.onSelect} className={cn( "group flex h-auto w-full items-center gap-2 rounded-md py-1.5 pr-2.5 text-left", props.active ? "border-l-2 border-l-primary bg-primary/10 text-foreground" : "text-foreground/70 hover:bg-accent/50 hover:text-foreground", props.className, )} style={paddingLeft != null ? { paddingLeft } : undefined} > <IconTool className="size-3 shrink-0 text-muted-foreground/40" /> <span className="flex-1 truncate font-mono text-[12px]"> {highlightMatch(label, props.search)} </span> {props.tool.method && <MethodBadge method={props.tool.method} />} {props.policies && ( <ToolPermissionDot toolPath={props.tool.path} policies={props.policies} interaction={props.tool.interaction} /> )} </Button> );}; const permissionLevels: Array<{ level: ToolPermissionLevel; label: string; description: string; dotClass: string; activeClass: string;}> = [ { level: "auto-run", label: "Auto-run", description: "Execute without asking", dotClass: "bg-emerald-500", activeClass: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", }, { level: "requires-approval", label: "Approval", description: "Ask before running", dotClass: "bg-amber-500", activeClass: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", }, { level: "denied", label: "Denied", description: "Block completely", dotClass: "bg-red-500", activeClass: "border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300", },]; const ToolPermissionControl = (props: { toolPath: string;Lines 1–500 of 918. Load more
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import type { Loadable } from "../../core/types"; import { usePrefetchToolDetail, } from "../../hooks/sources"; import type { SourceInspection, SourceInspectionToolDetail, } from "@executor/platform-sdk/schema"; import { MethodBadge } from "./ui/badge"; import { CodeBlock } from "./code-block"; import { DocumentPanel } from "./document-panel"; import { EmptyState, LoadableBlock } from "./loadable"; import { Markdown } from "./markdown"; import { IconCheck, IconChevron, IconClose, IconCopy, IconFolder, IconSearch, IconTool, } from "./icons"; import { ToolPermissionBadge, ToolPermissionDot, resolveToolPermission, type ToolPermissionLevel } from "./tool-permission-badge"; import type { LocalScopePolicy } from "@executor/platform-sdk/schema"; import { Button } from "./ui/button"; import { Input } from "./ui/input"; import { cn } from "../lib/cn"; type ToolTreeNode = { segment: string; tool?: SourceInspectionToolDetail["summary"] | SourceInspection["tools"][number]; children: Map<string, ToolTreeNode>; }; export type SourceToolDiscoveryResult = { query: string; results: ReadonlyArray<{ path: string; score: number; description?: string | null; }>; }; export type ToolPermissionChangeRequest = { toolPath: string; level: ToolPermissionLevel; }; export type SourceToolDetailPanelProps = { detail: SourceInspectionToolDetail; policies?: ReadonlyArray<LocalScopePolicy>; onSetPermission?: (request: ToolPermissionChangeRequest) => void | Promise<void>; permissionPending?: boolean; renderHeaderMeta?: (detail: SourceInspectionToolDetail) => ReactNode; renderSchemaExtras?: (detail: SourceInspectionToolDetail) => ReactNode; }; export type SourceToolModelWorkbenchProps = { bundle: SourceInspection; detail: Loadable<SourceInspectionToolDetail | null>; selectedToolPath: string | null; onSelectTool: (toolPath: string) => void; sourceId: string; policies?: ReadonlyArray<LocalScopePolicy>; onSetPermission?: (request: ToolPermissionChangeRequest) => void | Promise<void>; permissionPending?: boolean; renderDetail?: (detail: SourceInspectionToolDetail) => ReactNode; }; export type SourceToolDiscoveryPanelProps< TResult extends SourceToolDiscoveryResult = SourceToolDiscoveryResult, > = { discovery: Loadable<TResult>; initialQuery: string; onSubmitQuery: (query: string) => void; onOpenTool: (toolPath: string) => void; }; const buildToolTree = (tools: SourceInspection["tools"]): ToolTreeNode => { const root: ToolTreeNode = { segment: "", children: new Map(), }; for (const tool of tools) { const parts = tool.path.split("."); let node = root; for (const part of parts) { const existing = node.children.get(part); if (existing) { node = existing; continue; } const next: ToolTreeNode = { segment: part, children: new Map(), }; node.children.set(part, next); node = next; } node.tool = tool; } return root; }; const countToolLeaves = (node: ToolTreeNode): number => { let count = node.tool ? 1 : 0; for (const child of node.children.values()) { count += countToolLeaves(child); } return count; }; const highlightMatch = (text: string, search: string) => { if (!search.trim()) { return text; } const terms = search.trim().toLowerCase().split(/\s+/); const lowerText = text.toLowerCase(); const ranges: Array<[number, number]> = []; for (const term of terms) { let index = 0; while (index < lowerText.length) { const found = lowerText.indexOf(term, index); if (found === -1) { break; } ranges.push([found, found + term.length]); index = found + 1; } } if (ranges.length === 0) { return text; } ranges.sort((left, right) => left[0] - right[0]); const merged: Array<[number, number]> = [ranges[0]!]; for (let index = 1; index < ranges.length; index++) { const last = merged[merged.length - 1]!; const current = ranges[index]!; if (current[0] <= last[1]) { last[1] = Math.max(last[1], current[1]); } else { merged.push(current); } } const parts: Array<{ text: string; highlighted: boolean }> = []; let cursor = 0; for (const [start, end] of merged) { if (cursor < start) { parts.push({ text: text.slice(cursor, start), highlighted: false }); } parts.push({ text: text.slice(start, end), highlighted: true }); cursor = end; } if (cursor < text.length) { parts.push({ text: text.slice(cursor), highlighted: false }); } return ( <> {parts.map((part, index) => part.highlighted ? ( <mark key={index} className="rounded-sm bg-primary/20 px-px text-foreground"> {part.text} </mark> ) : ( <span key={index}>{part.text}</span> ), )} </> ); }; const CopyButton = (props: { text: string; field: string; copiedField: string | null; onCopy: (text: string, field: string) => void | Promise<void>; }) => ( <Button type="button" variant="ghost" size="icon" onClick={() => { void props.onCopy(props.text, props.field); }} className="size-6 shrink-0 text-muted-foreground/30 hover:text-muted-foreground" title={`Copy ${props.field}`} > {props.copiedField === props.field ? <IconCheck /> : <IconCopy />} </Button> ); const SourceToolTree = (props: { tools: SourceInspection["tools"]; selectedToolPath: string | null; onSelectTool: (path: string) => void; search: string; isFiltered: boolean; sourceId: string; policies?: ReadonlyArray<LocalScopePolicy>; }) => { const tree = useMemo(() => buildToolTree(props.tools), [props.tools]); const prefetch = usePrefetchToolDetail(); const entries = [...tree.children.values()].sort((left, right) => left.segment.localeCompare(right.segment) ); return ( <div className="flex flex-col gap-px"> {entries.map((node) => ( <SourceToolTreeNodeView key={node.segment} node={node} depth={0} selectedToolPath={props.selectedToolPath} onSelectTool={props.onSelectTool} search={props.search} defaultOpen={props.isFiltered} sourceId={props.sourceId} prefetch={prefetch} policies={props.policies} /> ))} </div> ); }; const SourceToolTreeNodeView = (props: { node: ToolTreeNode; depth: number; selectedToolPath: string | null; onSelectTool: (path: string) => void; search: string; defaultOpen: boolean; sourceId: string; prefetch: ReturnType<typeof usePrefetchToolDetail>; policies?: ReadonlyArray<LocalScopePolicy>; }) => { const { node, depth, selectedToolPath, onSelectTool, search, defaultOpen, sourceId, prefetch, } = props; const hasChildren = node.children.size > 0; const isLeaf = !!node.tool && !hasChildren; const hasSelectedDescendant = useMemo(() => { if (!selectedToolPath) { return false; } const check = (candidate: ToolTreeNode): boolean => { if (candidate.tool?.path === selectedToolPath) { return true; } for (const child of candidate.children.values()) { if (check(child)) { return true; } } return false; }; return check(node); }, [node, selectedToolPath]); const [open, setOpen] = useState(defaultOpen || hasSelectedDescendant); useEffect(() => { if (defaultOpen || hasSelectedDescendant) { setOpen(true); } }, [defaultOpen, hasSelectedDescendant]); if (isLeaf) { return ( <SourceToolListItem tool={node.tool as SourceInspection["tools"][number]} active={node.tool?.path === selectedToolPath} onSelect={() => onSelectTool(node.tool!.path)} search={search} depth={depth} sourceId={sourceId} prefetch={prefetch} policies={props.policies} /> ); } const paddingLeft = 8 + depth * 16; const sortedChildren = [...node.children.values()].sort((left, right) => left.segment.localeCompare(right.segment) ); const leafCount = countToolLeaves(node); return ( <div> {node.tool ? ( <div className="flex items-center"> <Button type="button" variant="ghost" size="icon" onClick={() => setOpen((value) => !value)} aria-expanded={open} className="shrink-0 size-auto rounded p-0.5 text-muted-foreground/30 hover:text-muted-foreground" style={{ marginLeft: paddingLeft }} > <IconChevron className="shrink-0 transition-transform duration-150" style={{ width: 8, height: 8, transform: open ? "rotate(90deg)" : "rotate(0deg)", transformOrigin: "50% 50%", }} /> </Button> <SourceToolListItem tool={node.tool as SourceInspection["tools"][number]} active={node.tool?.path === selectedToolPath} onSelect={() => onSelectTool(node.tool!.path)} search={search} depth={-1} className="flex-1 pl-1" sourceId={sourceId} prefetch={prefetch} policies={props.policies} /> </div> ) : ( <Button type="button" variant="ghost" onClick={() => setOpen((value) => !value)} aria-expanded={open} className={cn( "group flex h-auto w-full items-center gap-1.5 rounded-md py-1 pr-2.5 text-[12px] hover:bg-accent/40", open ? "text-foreground/80" : "text-muted-foreground/60", )} style={{ paddingLeft }} > <IconChevron className="shrink-0 text-muted-foreground/30 transition-transform duration-150" style={{ width: 8, height: 8, transform: open ? "rotate(90deg)" : "rotate(0deg)", transformOrigin: "50% 50%", }} /> <IconFolder className={cn("shrink-0", open ? "text-primary/60" : "text-muted-foreground/30")} style={{ width: 12, height: 12 }} /> <span className="flex-1 truncate text-left font-mono"> {highlightMatch(node.segment, search)} </span> <span className="shrink-0 tabular-nums text-[10px] text-muted-foreground/25"> {leafCount} </span> </Button> )} {open && hasChildren && ( <div className="relative flex flex-col gap-px"> <span className="absolute bottom-1 top-0 w-px bg-border/40" style={{ left: paddingLeft + 5 }} aria-hidden /> {sortedChildren.map((child) => ( <SourceToolTreeNodeView key={child.segment} node={child} depth={depth + 1} selectedToolPath={selectedToolPath} onSelectTool={onSelectTool} search={search} defaultOpen={defaultOpen} sourceId={sourceId} prefetch={prefetch} policies={props.policies} /> ))} </div> )} </div> ); }; const SourceToolListItem = (props: { tool: SourceInspection["tools"][number]; active: boolean; onSelect: () => void; search: string; depth: number; className?: string; sourceId: string; prefetch: ReturnType<typeof usePrefetchToolDetail>; policies?: ReadonlyArray<LocalScopePolicy>; }) => { const ref = useRef<HTMLButtonElement>(null); const paddingLeft = props.depth >= 0 ? 8 + props.depth * 16 + 8 : undefined; useEffect(() => { if (props.active && ref.current) { ref.current.scrollIntoView({ block: "nearest" }); } }, [props.active]); const label = props.depth >= 0 ? props.tool.path.split(".").pop() ?? props.tool.path : props.tool.path; return ( <Button ref={ref} type="button" variant="ghost" onMouseEnter={() => { props.prefetch(props.sourceId, props.tool.path); }} onClick={props.onSelect} className={cn( "group flex h-auto w-full items-center gap-2 rounded-md py-1.5 pr-2.5 text-left", props.active ? "border-l-2 border-l-primary bg-primary/10 text-foreground" : "text-foreground/70 hover:bg-accent/50 hover:text-foreground", props.className, )} style={paddingLeft != null ? { paddingLeft } : undefined} > <IconTool className="size-3 shrink-0 text-muted-foreground/40" /> <span className="flex-1 truncate font-mono text-[12px]"> {highlightMatch(label, props.search)} </span> {props.tool.method && <MethodBadge method={props.tool.method} />} {props.policies && ( <ToolPermissionDot toolPath={props.tool.path} policies={props.policies} interaction={props.tool.interaction} /> )} </Button> ); }; const permissionLevels: Array<{ level: ToolPermissionLevel; label: string; description: string; dotClass: string; activeClass: string; }> = [ { level: "auto-run", label: "Auto-run", description: "Execute without asking", dotClass: "bg-emerald-500", activeClass: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", }, { level: "requires-approval", label: "Approval", description: "Ask before running", dotClass: "bg-amber-500", activeClass: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300", }, { level: "denied", label: "Denied", description: "Block completely", dotClass: "bg-red-500", activeClass: "border-red-500/30 bg-red-500/10 text-red-700 dark:text-red-300", }, ]; const ToolPermissionControl = (props: { toolPath: string;