返回 DeepSeek-Reasonix
ToolGroup.tsx
根目录 / desktop / frontend / src / components / ToolGroup.tsx
1 import { memo, useRef, useState } from "react";
2 import { ChevronRight } from "lucide-react";
3 import { useT } from "../lib/i18n";
4 import { useGSAPCollapse } from "../lib/useGSAPCollapse";
5 import type { Item } from "../lib/useController";
6 import { ToolCard } from "./ToolCard";
7
8 type ToolItem = Extract<Item, { kind: "tool" }>;
9
10 export type ToolGroupKind = "explore" | "modify" | "delegate" | "shell";
11
12 const SHELL_TOOLS = new Set(["bash", "bash_output", "wait", "waitJob", "kill_shell"]);
13 const EXPLORE_TOOLS = new Set(["read_file", "ls", "grep", "glob", "web_fetch", "code_index", "read_skill", "connect_tool_source"]);
14 const MODIFY_TOOLS = new Set(["write_file", "edit_file", "multi_edit", "move_file", "delete_range", "delete_symbol", "notebook_edit"]);
15 const DELEGATE_TOOLS = new Set(["task", "run_skill", "explore", "research", "review", "security_review"]);
16
17 export function toolGroupKind(item: ToolItem): ToolGroupKind | null {
18 if (item.parentId || item.name === "todo_write" || item.name === "exit_plan_mode") return null;
19 if (SHELL_TOOLS.has(item.name)) return "shell";
20 if (EXPLORE_TOOLS.has(item.name)) return "explore";
21 if (MODIFY_TOOLS.has(item.name)) return "modify";
22 if (DELEGATE_TOOLS.has(item.name)) return "delegate";
23 return item.readOnly ? "explore" : "modify";
24 }
25
26 export function isCreationGroupableTool(item: ToolItem): boolean {
27 return item.status !== "running" && toolGroupKind(item) !== null;
28 }
29
30 function count(items: ToolItem[], names: readonly string[]): number {
31 return items.filter((item) => names.includes(item.name)).length;
32 }
33
34 function titleFor(kind: ToolGroupKind, t: ReturnType<typeof useT>): string {
35 switch (kind) {
36 case "explore": return t("creation.toolGroup.explore");
37 case "modify": return t("creation.toolGroup.modify");
38 case "delegate": return t("creation.toolGroup.delegate");
39 case "shell": return t("creation.toolGroup.shell");
40 }
41 }
42
43 function groupSummary(kind: ToolGroupKind, items: ToolItem[], t: ReturnType<typeof useT>): string {
44 const parts: string[] = [];
45 if (kind === "explore") {
46 const readCount = count(items, ["read_file", "ls", "web_fetch", "read_skill"]);
47 const searchCount = count(items, ["grep", "glob", "code_index"]);
48 const otherCount = items.length - readCount - searchCount;
49 if (readCount > 0) parts.push(t("creation.toolStat.read", { n: readCount }));
50 if (searchCount > 0) parts.push(t("creation.toolStat.search", { n: searchCount }));
51 if (otherCount > 0) parts.push(t("creation.toolStat.other", { n: otherCount }));
52 } else if (kind === "modify") {
53 const writeCount = count(items, ["write_file"]);
54 const editCount = count(items, ["edit_file", "multi_edit", "notebook_edit"]);
55 const moveCount = count(items, ["move_file"]);
56 const deleteCount = count(items, ["delete_range", "delete_symbol"]);
57 const otherCount = items.length - writeCount - editCount - moveCount - deleteCount;
58 if (writeCount > 0) parts.push(t("creation.toolStat.write", { n: writeCount }));
59 if (editCount > 0) parts.push(t("creation.toolStat.edit", { n: editCount }));
60 if (moveCount > 0) parts.push(t("creation.toolStat.move", { n: moveCount }));
61 if (deleteCount > 0) parts.push(t("creation.toolStat.delete", { n: deleteCount }));
62 if (otherCount > 0) parts.push(t("creation.toolStat.other", { n: otherCount }));
63 } else if (kind === "delegate") {
64 const taskCount = count(items, ["task", "run_skill", "explore", "research", "review", "security_review"]);
65 const otherCount = items.length - taskCount;
66 if (taskCount > 0) parts.push(t("creation.toolStat.task", { n: taskCount }));
67 if (otherCount > 0) parts.push(t("creation.toolStat.other", { n: otherCount }));
68 } else {
69 const commandCount = count(items, ["bash"]);
70 const checkCount = count(items, ["bash_output", "wait", "waitJob"]);
71 const stopCount = count(items, ["kill_shell"]);
72 const otherCount = items.length - commandCount - checkCount - stopCount;
73 if (commandCount > 0) parts.push(t("creation.toolStat.command", { n: commandCount }));
74 if (checkCount > 0) parts.push(t("creation.toolStat.check", { n: checkCount }));
75 if (stopCount > 0) parts.push(t("creation.toolStat.stop", { n: stopCount }));
76 if (otherCount > 0) parts.push(t("creation.toolStat.other", { n: otherCount }));
77 }
78 return parts.join(", ");
79 }
80
81 function titleCaseName(name: string): string {
82 return name
83 .replace(/[-_]+/g, " ")
84 .split(" ")
85 .filter(Boolean)
86 .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
87 .join(" ");
88 }
89
90 function toolDisplayName(name: string): string {
91 switch (name) {
92 case "read_file": return "Read";
93 case "ls": return "List";
94 case "web_fetch": return "Web Fetch";
95 case "code_index": return "Code Index";
96 case "write_file": return "Write";
97 case "edit_file": return "Edit";
98 case "multi_edit": return "Multi Edit";
99 case "move_file": return "Move";
100 case "bash": return "Shell";
101 case "bash_output": return "Shell Output";
102 case "kill_shell": return "Kill Shell";
103 case "wait":
104 case "waitJob": return "Wait";
105 case "use_capability": return "MCP";
106 default: return titleCaseName(name);
107 }
108 }
109
110 export const ToolGroup = memo(function ToolGroup({
111 kind,
112 items,
113 subcalls,
114 tabId,
115 }: {
116 kind: ToolGroupKind;
117 items: ToolItem[];
118 subcalls: ReadonlyMap<string, ToolItem[]>;
119 tabId?: string;
120 }) {
121 const t = useT();
122 const [open, setOpen] = useState(false);
123 const bodyRef = useRef<HTMLDivElement>(null);
124 useGSAPCollapse(bodyRef, open);
125
126 if (items.length === 0) return null;
127
128 return (
129 <div className={`tool-group tool-group--${kind}${open ? " tool-group--open" : ""}`} data-kind={kind} data-entrance={items[0]?.id}>
130 <button type="button" className="tool-group__head" onClick={() => setOpen((value) => !value)} aria-expanded={open}>
131 <span className="tool-group__title">{titleFor(kind, t)}</span>
132 <span className="tool-group__summary">{groupSummary(kind, items, t)}</span>
133 <ChevronRight className={`tool-group__chevron${open ? " tool-group__chevron--open" : ""}`} size={12} />
134 </button>
135 <div ref={bodyRef} className="tool-group__body">
136 {items.map((item) => (
137 <ToolCard key={item.id} item={item} subcalls={subcalls.get(item.id)} tabId={tabId} displayName={toolDisplayName(item.name)} />
138 ))}
139 </div>
140 </div>
141 );
142 });
143
143 lines Plain Text