918 lines · 28.7 KB · TypeScript
RawDownload
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.