From 1d9f09944db5bcdf557ef98e8b0821222136f36d Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Thu, 28 May 2026 17:52:28 +0530 Subject: [PATCH] feat: Allow executing multiple tools in the Prompt Playground --- .../messagesView/rootMessageView.tsx | 6 + .../components/messagesView/toolCallView.tsx | 368 +++++++++++++----- ui/components/prompts/context.tsx | 125 ++++++ 3 files changed, 402 insertions(+), 97 deletions(-) diff --git a/ui/components/prompts/components/messagesView/rootMessageView.tsx b/ui/components/prompts/components/messagesView/rootMessageView.tsx index 9b08ebe6e1e..5325cdd2023 100644 --- a/ui/components/prompts/components/messagesView/rootMessageView.tsx +++ b/ui/components/prompts/components/messagesView/rootMessageView.tsx @@ -22,6 +22,9 @@ export function MessagesView() { supportsVision, handleSubmitToolResult, handleExecuteToolCall, + handleSubmitAllToolResults, + handleExecuteAllToolCalls, + fetchToolResult, } = usePromptContext(); const messagesEndRef = useRef(null); const prevLengthRef = useRef(messages.length); @@ -116,6 +119,9 @@ export function MessagesView() { onRemove={canRemove ? () => handleRemoveMessage(index) : undefined} onSubmitToolResult={(toolCallId, content) => handleSubmitToolResult(index, toolCallId, content)} onExecuteToolCall={(toolCall) => handleExecuteToolCall(index, toolCall)} + onSubmitAllToolResults={(results) => handleSubmitAllToolResults(index, results)} + onExecuteAllToolCalls={(toolCalls) => handleExecuteAllToolCalls(index, toolCalls)} + fetchToolResult={fetchToolResult} respondedToolCallIds={respondedIds} /> ); diff --git a/ui/components/prompts/components/messagesView/toolCallView.tsx b/ui/components/prompts/components/messagesView/toolCallView.tsx index 876df913e43..24bd404cf72 100644 --- a/ui/components/prompts/components/messagesView/toolCallView.tsx +++ b/ui/components/prompts/components/messagesView/toolCallView.tsx @@ -1,27 +1,13 @@ import { Button } from "@/components/ui/button"; import { CodeEditor } from "@/components/ui/codeEditor"; import { Textarea } from "@/components/ui/textarea"; -import { cn } from "@/lib/utils"; import { Message, MessageRole, SerializedMessage, type ToolCall } from "@/lib/message"; +import { cn } from "@/lib/utils"; import { isJson } from "@/lib/utils/validation"; -import { Loader2, PencilLine, Play, Send, Wrench, XIcon } from "lucide-react"; +import { Check, Loader2, PencilLine, Play, Send, Wrench, XIcon } from "lucide-react"; import { useRef, useState } from "react"; import MessageRoleSwitcher from "./messageRoleSwitcher"; -/** - * Renders a UI for viewing and editing tool-call entries on a message, including optional argument editing and submitting tool responses. - * - * The component displays each tool call's name, id, and arguments (JSON arguments open in an editable code editor). JSON edits are buffered locally and only committed to `onChange` when the editor loses focus or when the message role changes. The component also exposes controls for switching the message role, deleting the message, and entering/submitting a response for individual tool calls. - * - * @param message - Message instance containing zero or more toolCalls to render; edits are serialized via `onChange`. - * @param disabled - When true, disables interactive controls and makes editors read-only. - * @param onChange - Called with the message's serialized form after committed edits (e.g., buffered JSON arguments flushed or role changed). - * @param onRemove - If provided, called when the delete button is clicked. - * @param onSubmitToolResult - If provided, called with (toolCallId, content) when a user submits a response for a tool call. - * @param respondedToolCallIds - Optional set of toolCall ids that have already received responses; tool calls in this set hide the response UI. - * - * @returns The rendered React element for the tool-call message view. - */ export default function ToolCallMessageView({ message, disabled, @@ -29,6 +15,9 @@ export default function ToolCallMessageView({ onRemove, onSubmitToolResult, onExecuteToolCall, + onSubmitAllToolResults, + onExecuteAllToolCalls, + fetchToolResult, respondedToolCallIds, }: { message: Message; @@ -37,16 +26,31 @@ export default function ToolCallMessageView({ onRemove?: () => void; onSubmitToolResult?: (toolCallId: string, content: string) => void; onExecuteToolCall?: (toolCall: ToolCall) => Promise; + onSubmitAllToolResults?: (results: { toolCallId: string; content: string }[]) => Promise; + onExecuteAllToolCalls?: (toolCalls: ToolCall[]) => Promise; + fetchToolResult?: (toolCall: ToolCall) => Promise; respondedToolCallIds?: Set; }) { const toolCalls = message.toolCalls ?? []; const [responses, setResponses] = useState>({}); const [executingIds, setExecutingIds] = useState>(new Set()); + const [resolvedIds, setResolvedIds] = useState>(new Set()); const [manualEntryIds, setManualEntryIds] = useState>(new Set()); + const [isExecutingAll, setIsExecutingAll] = useState(false); + const [isSubmittingAll, setIsSubmittingAll] = useState(false); const messageRef = useRef(message); messageRef.current = message; const jsonBufferRef = useRef>({}); + const pendingToolCalls = toolCalls.filter((tc) => !respondedToolCallIds?.has(tc.id)); + const isMultiple = pendingToolCalls.length > 1; + const isBusy = isExecutingAll || isSubmittingAll || executingIds.size > 0; + + const resolvedCount = pendingToolCalls.filter( + (tc) => resolvedIds.has(tc.id) || (manualEntryIds.has(tc.id) && responses[tc.id]?.trim()), + ).length; + const allResolved = pendingToolCalls.length > 0 && resolvedCount === pendingToolCalls.length; + const applyPendingJsonBuffers = (msg: Message): Message => { const keys = Object.keys(jsonBufferRef.current); if (keys.length === 0) return msg; @@ -73,6 +77,22 @@ export default function ToolCallMessageView({ } }; + const flushAllJsonBuffers = () => { + const keys = Object.keys(jsonBufferRef.current); + if (keys.length === 0) return; + const clone = messageRef.current.clone(); + let changed = false; + for (const toolCallId of keys) { + const tc = clone.toolCalls?.find((t) => t.id === toolCallId); + if (tc) { + tc.function.arguments = jsonBufferRef.current[toolCallId]; + changed = true; + } + } + jsonBufferRef.current = {}; + if (changed) onChange(clone.serialized); + }; + const handleRoleChange = (role: string) => { const latest = applyPendingJsonBuffers(messageRef.current); const clone = latest.clone(); @@ -84,6 +104,7 @@ export default function ToolCallMessageView({ setResponses((prev) => ({ ...prev, [toolCallId]: value })); }; + // Single mode: submit one result and continue conversation immediately const handleSubmitResponse = (toolCallId: string) => { const content = responses[toolCallId]?.trim(); if (!content || !onSubmitToolResult) return; @@ -100,7 +121,8 @@ export default function ToolCallMessageView({ }); }; - const handleExecute = async (tc: ToolCall) => { + // Single mode: execute and immediately continue conversation + const handleExecuteSingle = async (tc: ToolCall) => { if (!onExecuteToolCall) return; flushJsonBuffer(tc.id); const latestTc = messageRef.current.toolCalls?.find((t) => t.id === tc.id) ?? tc; @@ -116,8 +138,67 @@ export default function ToolCallMessageView({ } }; + // Multi mode: execute one tool, store result locally (don't submit yet) + const handleExecuteOne = async (tc: ToolCall) => { + if (!fetchToolResult) return; + flushJsonBuffer(tc.id); + const latestTc = messageRef.current.toolCalls?.find((t) => t.id === tc.id) ?? tc; + setExecutingIds((prev) => new Set(prev).add(tc.id)); + try { + const content = await fetchToolResult(latestTc); + setResponses((prev) => ({ ...prev, [tc.id]: content })); + setResolvedIds((prev) => new Set(prev).add(tc.id)); + } finally { + setExecutingIds((prev) => { + const next = new Set(prev); + next.delete(tc.id); + return next; + }); + } + }; + + // Multi mode: execute all pending tools in parallel, store results locally + const handleExecuteAll = async () => { + if (!onExecuteAllToolCalls) return; + flushAllJsonBuffers(); + const latestCalls = pendingToolCalls.map( + (tc) => messageRef.current.toolCalls?.find((t) => t.id === tc.id) ?? tc, + ); + setIsExecutingAll(true); + try { + await onExecuteAllToolCalls(latestCalls); + } finally { + setIsExecutingAll(false); + } + }; + + // Multi mode: submit all collected results at once + const handleSubmitAll = async () => { + if (!onSubmitAllToolResults) return; + const results: { toolCallId: string; content: string }[] = []; + for (const tc of pendingToolCalls) { + const content = responses[tc.id]?.trim(); + if (!content) return; + results.push({ toolCallId: tc.id, content }); + } + setIsSubmittingAll(true); + try { + await onSubmitAllToolResults(results); + setResponses({}); + setManualEntryIds(new Set()); + setResolvedIds(new Set()); + } finally { + setIsSubmittingAll(false); + } + }; + const showManualEntry = (toolCallId: string) => { setManualEntryIds((prev) => new Set(prev).add(toolCallId)); + setResolvedIds((prev) => { + const next = new Set(prev); + next.delete(toolCallId); + return next; + }); }; const hideManualEntry = (toolCallId: string) => { @@ -126,8 +207,20 @@ export default function ToolCallMessageView({ next.delete(toolCallId); return next; }); + setResponses((prev) => { + const next = { ...prev }; + delete next[toolCallId]; + return next; + }); + setResolvedIds((prev) => { + const next = new Set(prev); + next.delete(toolCallId); + return next; + }); }; + const tcHasResult = (tcId: string) => resolvedIds.has(tcId) || (manualEntryIds.has(tcId) && !!responses[tcId]?.trim()); + return (
@@ -167,9 +260,11 @@ export default function ToolCallMessageView({ } } - const isExecuting = executingIds.has(tc.id); + const isExecuting = executingIds.has(tc.id) || isExecutingAll; const isResponded = respondedToolCallIds?.has(tc.id); const isManualEntryOpen = manualEntryIds.has(tc.id); + const hasResult = tcHasResult(tc.id); + const isResolved = resolvedIds.has(tc.id); return (
+ {/* Header */}
@@ -203,6 +299,12 @@ export default function ToolCallMessageView({ Responded )} + {!isResponded && isMultiple && hasResult && ( + + + Ready + + )}
@@ -211,6 +313,7 @@ export default function ToolCallMessageView({
+ {/* Arguments */} {formattedArgs && (
@@ -244,103 +347,174 @@ export default function ToolCallMessageView({
)} - {!disabled && onSubmitToolResult && !isResponded && ( -
- {isManualEntryOpen ? ( -
-
-
-
Tool result
-
- Paste the result returned by this tool call. -
-
- - -
- -