BashTool.tsx

  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;