1import React, { useState } from "react";
2import { LLMContent } from "../types";
3
4// Display data from the bash tool backend
5interface BashDisplayData {
6 workingDir: string;
7}
8
9interface BashToolProps {
10 // For tool_use (pending state)
11 toolInput?: unknown;
12 isRunning?: boolean;
13
14 // For tool_result (completed state)
15 toolResult?: LLMContent[];
16 hasError?: boolean;
17 executionTime?: string;
18 display?: unknown;
19}
20
21function BashTool({
22 toolInput,
23 isRunning,
24 toolResult,
25 hasError,
26 executionTime,
27 display,
28}: BashToolProps) {
29 const [isExpanded, setIsExpanded] = useState(false);
30
31 // Extract working directory from display data
32 const displayData: BashDisplayData | null =
33 display &&
34 typeof display === "object" &&
35 "workingDir" in display &&
36 typeof display.workingDir === "string"
37 ? (display as BashDisplayData)
38 : null;
39
40 // Extract command from toolInput
41 const command =
42 typeof toolInput === "object" &&
43 toolInput !== null &&
44 "command" in toolInput &&
45 typeof toolInput.command === "string"
46 ? toolInput.command
47 : typeof toolInput === "string"
48 ? toolInput
49 : "";
50
51 // Extract output from toolResult
52 const output =
53 toolResult && toolResult.length > 0 && toolResult[0].Text ? toolResult[0].Text : "";
54
55 // Check if this was a cancelled operation
56 const isCancelled = hasError && output.toLowerCase().includes("cancel");
57
58 // Truncate command for display
59 const truncateCommand = (cmd: string, maxLen: number = 300) => {
60 if (cmd.length <= maxLen) return cmd;
61 return cmd.substring(0, maxLen) + "...";
62 };
63
64 const displayCommand = truncateCommand(command);
65 const isComplete = !isRunning && toolResult !== undefined;
66
67 return (
68 <div
69 className="bash-tool"
70 data-testid={isComplete ? "tool-call-completed" : "tool-call-running"}
71 >
72 <div className="bash-tool-header" onClick={() => setIsExpanded(!isExpanded)}>
73 <div className="bash-tool-summary">
74 <span className={`bash-tool-emoji ${isRunning ? "running" : ""}`}>🛠️</span>
75 <span className="bash-tool-command">{displayCommand}</span>
76 {displayData?.workingDir && (
77 <span className="bash-tool-cwd" title={displayData.workingDir}>
78 in {displayData.workingDir}
79 </span>
80 )}
81 {isComplete && isCancelled && <span className="bash-tool-cancelled">✗ cancelled</span>}
82 {isComplete && hasError && !isCancelled && <span className="bash-tool-error">✗</span>}
83 {isComplete && !hasError && <span className="bash-tool-success">✓</span>}
84 </div>
85 <button
86 className="bash-tool-toggle"
87 aria-label={isExpanded ? "Collapse" : "Expand"}
88 aria-expanded={isExpanded}
89 >
90 <svg
91 width="12"
92 height="12"
93 viewBox="0 0 12 12"
94 fill="none"
95 xmlns="http://www.w3.org/2000/svg"
96 style={{
97 transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
98 transition: "transform 0.2s",
99 }}
100 >
101 <path
102 d="M4.5 3L7.5 6L4.5 9"
103 stroke="currentColor"
104 strokeWidth="1.5"
105 strokeLinecap="round"
106 strokeLinejoin="round"
107 />
108 </svg>
109 </button>
110 </div>
111
112 {isExpanded && (
113 <div className="bash-tool-details">
114 {displayData?.workingDir && (
115 <div className="bash-tool-section">
116 <div className="bash-tool-label">Working Directory:</div>
117 <pre className="bash-tool-code bash-tool-code-cwd">{displayData.workingDir}</pre>
118 </div>
119 )}
120 <div className="bash-tool-section">
121 <div className="bash-tool-label">Command:</div>
122 <pre className="bash-tool-code">{command}</pre>
123 </div>
124
125 {isComplete && (
126 <div className="bash-tool-section">
127 <div className="bash-tool-label">
128 Output{hasError ? " (Error)" : ""}:
129 {executionTime && <span className="bash-tool-time">{executionTime}</span>}
130 </div>
131 <pre className={`bash-tool-code ${hasError ? "error" : ""}`}>
132 {output || "(no output)"}
133 </pre>
134 </div>
135 )}
136 </div>
137 )}
138 </div>
139 );
140}
141
142export default BashTool;