返回 DeepSeek-Reasonix
chatToolPresentation.ts
根目录 / desktop / frontend / src / lib / chatToolPresentation.ts
1 import type { Item } from "./useController";
2 import { isShellToolName, isPowerShellToolName } from "./shellToolIdentity";
3
4 export type ToolItem = Extract<Item, { kind: "tool" }>;
5 export type ToolPresentationKind = "search" | "web" | "shell" | "agent" | "file" | "present" | "tool";
6
7 /** Shell execution metadata is authoritative for shell calls, not output readers. */
8 export function toolPresentation(item: ToolItem): {
9 state: ToolItem["status"]; dot: "ongoing" | "done" | "error" | "warning" | "idle";
10 label: "chat.running" | "chat.done" | "chat.failed" | "chat.stopped" | "chat.timedOut" | "chat.notRun" | "chat.background" | "chat.unknown";
11 exitCode?: number;
12 } {
13 const execution = classifyTool(item) === "shell" ? item.execution : undefined;
14 switch (execution?.state) {
15 case "timed_out": return { state: "error", dot: "error", label: "chat.timedOut" };
16 case "cancelled": return { state: "stopped", dot: "warning", label: "chat.stopped" };
17 case "not_run": return { state: "stopped", dot: "warning", label: "chat.notRun" };
18 // This is the settled launch receipt, not a live observation of the job.
19 // Job progress is reported by its own output/status tools across turns.
20 case "background_started": return { state: "done", dot: "done", label: "chat.background" };
21 case "failed": return { state: "error", dot: "error", label: "chat.failed", exitCode: execution.exitCode };
22 case "completed":
23 if (execution.exitCode == null) return { state: "unknown", dot: "idle", label: "chat.unknown" };
24 return execution.exitCode === 0 ? { state: "done", dot: "done", label: "chat.done", exitCode: 0 }
25 : { state: "error", dot: "error", label: "chat.failed", exitCode: execution.exitCode };
26 }
27 if (item.status === "running") return { state: "running", dot: "ongoing", label: "chat.running" };
28 if (item.resultMissing || item.status === "unknown") return { state: "unknown", dot: "idle", label: "chat.unknown" };
29 if (item.status === "error" || item.error || (execution?.exitCode != null && execution.exitCode !== 0)) {
30 return { state: "error", dot: "error", label: "chat.failed", exitCode: execution?.exitCode };
31 }
32 if (item.status === "stopped") return { state: "stopped", dot: "warning", label: "chat.stopped" };
33 if (classifyTool(item) === "shell" && (execution?.exitCode == null || (execution.state && execution.state !== "running"))) {
34 return { state: "unknown", dot: "idle", label: "chat.unknown" };
35 }
36 return { state: "done", dot: "done", label: "chat.done", exitCode: execution?.exitCode };
37 }
38
39 const AGENT_TOOLS = new Set(["task", "read_only_task", "parallel_tasks", "fleet", "subagent"]);
40 const FILE_TOOLS = new Set([
41 "read_file", "glob", "grep", "ls", "code_index",
42 "write_file", "edit_file", "multi_edit", "move_file", "notebook_edit", "delete_range", "delete_symbol",
43 ]);
44
45 /**
46 * Resolve a renderer from trusted built-in identities. A plugin whose display
47 * name happens to contain "read" or "write" must remain a generic tool.
48 */
49 export function classifyTool(item: ToolItem): ToolPresentationKind {
50 if (item.name === "present") return "present";
51 if (item.name === "web_search") return "search";
52 if (item.name === "web_fetch") return "web";
53 if (isShellToolName(item.name) || item.isShell) return "shell";
54 if (AGENT_TOOLS.has(item.name)) return "agent";
55 if (FILE_TOOLS.has(item.name)) return "file";
56 return "tool";
57 }
58
59 export function shellDisplayName(item: ToolItem): string {
60 const shell = item.execution?.shell?.trim().toLowerCase() || (isPowerShellToolName(item.name) ? "pwsh" : "");
61 if (shell === "powershell" || shell === "pwsh") return "PowerShell";
62 if (shell === "git-bash") return "Git Bash";
63 if (shell === "bash") return "Bash";
64 if (shell === "zsh") return "Zsh";
65 if (shell === "sh") return "Shell";
66 return "Terminal";
67 }
68
68 lines TYPESCRIPT