SubagentTool.tsx

  1import React, { useState } from "react";
  2import { LLMContent } from "../types";
  3
  4interface SubagentToolProps {
  5  // For tool_use (pending state)
  6  toolInput?: unknown; // { slug: string, prompt: string, timeout_seconds?: number, wait?: boolean }
  7  isRunning?: boolean;
  8
  9  // For tool_result (completed state)
 10  toolResult?: LLMContent[];
 11  hasError?: boolean;
 12  executionTime?: string;
 13  displayData?: { slug?: string; conversation_id?: string };
 14}
 15
 16function SubagentTool({
 17  toolInput,
 18  isRunning,
 19  toolResult,
 20  hasError,
 21  executionTime,
 22  displayData,
 23}: SubagentToolProps) {
 24  const [isExpanded, setIsExpanded] = useState(false);
 25
 26  // Extract fields from toolInput
 27  const input =
 28    typeof toolInput === "object" && toolInput !== null
 29      ? (toolInput as { slug?: string; prompt?: string; timeout_seconds?: number; wait?: boolean })
 30      : {};
 31
 32  const slug = input.slug || displayData?.slug || "subagent";
 33  const prompt = input.prompt || "";
 34  const wait = input.wait !== false;
 35  const timeout = input.timeout_seconds || 60;
 36
 37  // Extract result text
 38  const resultText =
 39    toolResult
 40      ?.filter((r) => r.Type === 2) // ContentTypeText
 41      .map((r) => r.Text)
 42      .join("\n") || "";
 43
 44  // Truncate prompt for display
 45  const truncateText = (text: string, maxLen: number = 60) => {
 46    if (!text) return "";
 47    const firstLine = text.split("\n")[0];
 48    if (firstLine.length <= maxLen) return firstLine;
 49    return firstLine.substring(0, maxLen) + "...";
 50  };
 51
 52  const displayPrompt = truncateText(prompt);
 53  const isComplete = !isRunning && toolResult !== undefined;
 54
 55  return (
 56    <div className="tool" data-testid={isComplete ? "tool-call-completed" : "tool-call-running"}>
 57      <div className="tool-header" onClick={() => setIsExpanded(!isExpanded)}>
 58        <div className="tool-summary">
 59          <span className={`tool-emoji ${isRunning ? "running" : ""}`}></span>
 60          <span className="tool-name">subagent</span>
 61          {isComplete && hasError && <span className="tool-error"></span>}
 62          {isComplete && !hasError && <span className="tool-success"></span>}
 63          <span className="tool-command">
 64            Subagent '{slug}' {isRunning ? (wait ? "running..." : "started") : ""}
 65            {displayPrompt && !isRunning && ` ${displayPrompt}`}
 66          </span>
 67        </div>
 68        <button
 69          className="tool-toggle"
 70          aria-label={isExpanded ? "Collapse" : "Expand"}
 71          aria-expanded={isExpanded}
 72        >
 73          <svg
 74            width="12"
 75            height="12"
 76            viewBox="0 0 12 12"
 77            fill="none"
 78            xmlns="http://www.w3.org/2000/svg"
 79            style={{
 80              transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
 81              transition: "transform 0.2s",
 82            }}
 83          >
 84            <path
 85              d="M4.5 3L7.5 6L4.5 9"
 86              stroke="currentColor"
 87              strokeWidth="1.5"
 88              strokeLinecap="round"
 89              strokeLinejoin="round"
 90            />
 91          </svg>
 92        </button>
 93      </div>
 94
 95      {isExpanded && (
 96        <div className="tool-details">
 97          <div className="tool-section">
 98            <div className="tool-label">
 99              Prompt to '{slug}':
100              {!wait && <span className="tool-badge">fire-and-forget</span>}
101              {timeout !== 60 && <span className="tool-badge">timeout: {timeout}s</span>}
102            </div>
103            <div className="tool-code">{prompt || "(no prompt)"}</div>
104          </div>
105
106          {isComplete && (
107            <div className="tool-section">
108              <div className="tool-label">
109                Response:
110                {executionTime && <span className="tool-time">{executionTime}</span>}
111              </div>
112              <div className={`tool-code ${hasError ? "error" : ""}`}>
113                {resultText || "(no response)"}
114              </div>
115            </div>
116          )}
117
118          {displayData?.conversation_id && (
119            <div className="tool-section">
120              <div className="tool-label">Conversation:</div>
121              <div className="tool-code">
122                <a
123                  href={`/c/${slug}`}
124                  onClick={(e) => {
125                    e.preventDefault();
126                    // Navigate to the subagent conversation
127                    window.history.pushState({}, "", `/c/${slug}`);
128                    window.dispatchEvent(new PopStateEvent("popstate"));
129                  }}
130                  style={{ color: "var(--link-color)", textDecoration: "underline" }}
131                >
132                  View subagent conversation 
133                </a>
134              </div>
135            </div>
136          )}
137        </div>
138      )}
139    </div>
140  );
141}
142
143export default SubagentTool;