({ invocation }: { invocation: BaseToolInvocation })
| 322 | |
| 323 | // Main component |
| 324 | export function ToolInvocation({ invocation }: { invocation: BaseToolInvocation }) { |
| 325 | const [isOpen, setIsOpen] = useState(true); |
| 326 | const [progressMessages, setProgressMessages] = useState<ProgressMessage[]>([]); |
| 327 | const progressEndRef = useRef<HTMLDivElement>(null); |
| 328 | const seenProgressMessagesRef = useRef<Set<string>>(new Set()); |
| 329 | |
| 330 | // Memoize input hash to prevent reconnecting SSE on every render |
| 331 | const inputHash = useMemo(() => { |
| 332 | return JSON.stringify(invocation.input); |
| 333 | }, [invocation.input]); |
| 334 | |
| 335 | const isComplete = invocation.state === "output-available"; |
| 336 | |
| 337 | // Extract tool name from type (e.g., "tool-projectRetrieval" -> "projectRetrieval") |
| 338 | // or use toolName if available |
| 339 | const toolName = invocation.toolName || |
| 340 | (typeof invocation.type === "string" && invocation.type.startsWith("tool-") |
| 341 | ? invocation.type.substring(5) |
| 342 | : "unknown"); |
| 343 | |
| 344 | // Format tool name for display |
| 345 | const displayName = toolName |
| 346 | .replace(/([A-Z])/g, " $1") |
| 347 | .replace(/^./, (str) => str.toUpperCase()) |
| 348 | .trim(); |
| 349 | |
| 350 | // Connect to SSE for progress updates (experimentCodeUpdate only) |
| 351 | useEffect(() => { |
| 352 | if (toolName !== "experimentCodeUpdate" || isComplete) { |
| 353 | return; |
| 354 | } |
| 355 | |
| 356 | let eventSource: EventSource | null = null; |
| 357 | |
| 358 | const setupSSE = async () => { |
| 359 | try { |
| 360 | // Calculate input hash (using memoized inputHash to avoid recalculating) |
| 361 | const encoder = new TextEncoder(); |
| 362 | const data = encoder.encode(inputHash); |
| 363 | const hashBuffer = await crypto.subtle.digest("SHA-256", data); |
| 364 | const hashArray = Array.from(new Uint8Array(hashBuffer)); |
| 365 | const sha256Hash = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); |
| 366 | |
| 367 | // Poll for execution ID |
| 368 | const pollForExecutionId = async (): Promise<string | null> => { |
| 369 | const maxAttempts = 10; |
| 370 | let attempts = 0; |
| 371 | |
| 372 | while (attempts < maxAttempts) { |
| 373 | try { |
| 374 | const response = await fetch("/api/experiment-progress", { |
| 375 | method: "POST", |
| 376 | headers: { "Content-Type": "application/json" }, |
| 377 | body: JSON.stringify({ inputHash: sha256Hash }), |
| 378 | }); |
| 379 | |
| 380 | if (response.ok) { |
| 381 | const data = await response.json(); |
nothing calls this directly
no test coverage detected