feat: improved UX for tool calls via execute_code (#6205)
This commit is contained in:
@@ -271,6 +271,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) {
|
|||||||
Ok(call) => match call.name.to_string().as_str() {
|
Ok(call) => match call.name.to_string().as_str() {
|
||||||
"developer__text_editor" => render_text_editor_request(call, debug),
|
"developer__text_editor" => render_text_editor_request(call, debug),
|
||||||
"developer__shell" => render_shell_request(call, debug),
|
"developer__shell" => render_shell_request(call, debug),
|
||||||
|
"code_execution__execute_code" => render_execute_code_request(call, debug),
|
||||||
"subagent" => render_subagent_request(call, debug),
|
"subagent" => render_subagent_request(call, debug),
|
||||||
"todo__write" => render_todo_request(call, debug),
|
"todo__write" => render_todo_request(call, debug),
|
||||||
_ => render_default_request(call, debug),
|
_ => render_default_request(call, debug),
|
||||||
@@ -445,6 +446,61 @@ fn render_shell_request(call: &CallToolRequestParam, debug: bool) {
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_execute_code_request(call: &CallToolRequestParam, debug: bool) {
|
||||||
|
let tool_graph = call
|
||||||
|
.arguments
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|args| args.get("tool_graph"))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.filter(|arr| !arr.is_empty());
|
||||||
|
|
||||||
|
let Some(tool_graph) = tool_graph else {
|
||||||
|
return render_default_request(call, debug);
|
||||||
|
};
|
||||||
|
|
||||||
|
let count = tool_graph.len();
|
||||||
|
let plural = if count == 1 { "" } else { "s" };
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"─── {} tool call{} | {} ──────────────────────────",
|
||||||
|
style(count).cyan(),
|
||||||
|
plural,
|
||||||
|
style("execute_code").magenta().dim()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, node) in tool_graph.iter().filter_map(Value::as_object).enumerate() {
|
||||||
|
let tool = node
|
||||||
|
.get("tool")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let desc = node
|
||||||
|
.get("description")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("");
|
||||||
|
let deps: Vec<_> = node
|
||||||
|
.get("depends_on")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(Value::as_u64)
|
||||||
|
.map(|d| (d + 1).to_string())
|
||||||
|
.collect();
|
||||||
|
let deps_str = if deps.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" (uses {})", deps.join(", "))
|
||||||
|
};
|
||||||
|
println!(
|
||||||
|
" {}. {}: {}{}",
|
||||||
|
style(i + 1).dim(),
|
||||||
|
style(tool).cyan(),
|
||||||
|
style(desc).green(),
|
||||||
|
style(deps_str).dim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
fn render_subagent_request(call: &CallToolRequestParam, debug: bool) {
|
fn render_subagent_request(call: &CallToolRequestParam, debug: bool) {
|
||||||
print_tool_header(call);
|
print_tool_header(call);
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,25 @@ type ToolCallRequest = (
|
|||||||
tokio::sync::oneshot::Sender<Result<String, String>>,
|
tokio::sync::oneshot::Sender<Result<String, String>>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
|
struct ToolGraphNode {
|
||||||
|
/// Tool name in format "server/tool" (e.g., "developer/shell")
|
||||||
|
tool: String,
|
||||||
|
/// Brief description of what this call does (e.g., "list files in /src")
|
||||||
|
description: String,
|
||||||
|
/// Indices of nodes this depends on (empty if no dependencies)
|
||||||
|
#[serde(default)]
|
||||||
|
depends_on: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
struct ExecuteCodeParams {
|
struct ExecuteCodeParams {
|
||||||
/// JavaScript code with ES6 imports for MCP tools.
|
/// JavaScript code with ES6 imports for MCP tools.
|
||||||
code: String,
|
code: String,
|
||||||
|
/// DAG of tool calls showing execution flow. Each node represents a tool call.
|
||||||
|
/// Use depends_on to show data flow (e.g., node 1 uses output from node 0).
|
||||||
|
#[serde(default)]
|
||||||
|
tool_graph: Vec<ToolGraphNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
@@ -701,6 +716,7 @@ impl McpClientTrait for CodeExecutionClient {
|
|||||||
Err(Error::TransportClosed)
|
Err(Error::TransportClosed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn list_tools(
|
async fn list_tools(
|
||||||
&self,
|
&self,
|
||||||
_next_cursor: Option<String>,
|
_next_cursor: Option<String>,
|
||||||
@@ -746,6 +762,15 @@ impl McpClientTrait for CodeExecutionClient {
|
|||||||
- Last expression is the result
|
- Last expression is the result
|
||||||
- No comments in code
|
- No comments in code
|
||||||
|
|
||||||
|
TOOL_GRAPH: Always provide tool_graph to describe the execution flow for the UI.
|
||||||
|
Each node has: tool (server/name), description (what it does), depends_on (indices of dependencies).
|
||||||
|
Example for chained operations:
|
||||||
|
[
|
||||||
|
{"tool": "developer/shell", "description": "list files", "depends_on": []},
|
||||||
|
{"tool": "developer/text_editor", "description": "read README.md", "depends_on": []},
|
||||||
|
{"tool": "developer/text_editor", "description": "write output.txt", "depends_on": [0, 1]}
|
||||||
|
]
|
||||||
|
|
||||||
BEFORE CALLING: Use the read_module tool to check required parameters.
|
BEFORE CALLING: Use the read_module tool to check required parameters.
|
||||||
"#}
|
"#}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ impl MappingReport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn build_canonical_models() -> Result<()> {
|
async fn build_canonical_models() -> Result<()> {
|
||||||
println!("Fetching models from OpenRouter API...");
|
println!("Fetching models from OpenRouter API...");
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,10 @@ fi
|
|||||||
if [ "$CODE_EXEC_MODE" = true ]; then
|
if [ "$CODE_EXEC_MODE" = true ]; then
|
||||||
echo "Mode: code_execution (JS batching)"
|
echo "Mode: code_execution (JS batching)"
|
||||||
BUILTINS="developer,code_execution"
|
BUILTINS="developer,code_execution"
|
||||||
# Match "execute_code | code_execution" or "read_module | code_execution" in output
|
# Match code_execution tool usage:
|
||||||
SUCCESS_PATTERN="(execute_code \| code_execution)|(read_module \| code_execution)"
|
# - "execute_code | code_execution" or "read_module | code_execution" (fallback format)
|
||||||
|
# - "tool call | execute_code" or "tool calls | execute_code" (new format with tool_graph)
|
||||||
|
SUCCESS_PATTERN="(execute_code \| code_execution)|(read_module \| code_execution)|(tool calls? \| execute_code)"
|
||||||
SUCCESS_MSG="code_execution tool called"
|
SUCCESS_MSG="code_execution tool called"
|
||||||
FAILURE_MSG="no code_execution tools called"
|
FAILURE_MSG="no code_execution tools called"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import MCPUIResourceRenderer from './MCPUIResourceRenderer';
|
|||||||
import { isUIResource } from '@mcp-ui/client';
|
import { isUIResource } from '@mcp-ui/client';
|
||||||
import { CallToolResponse, Content, EmbeddedResource } from '../api';
|
import { CallToolResponse, Content, EmbeddedResource } from '../api';
|
||||||
|
|
||||||
|
interface ToolGraphNode {
|
||||||
|
tool: string;
|
||||||
|
description: string;
|
||||||
|
depends_on: number[];
|
||||||
|
}
|
||||||
|
|
||||||
interface ToolCallWithResponseProps {
|
interface ToolCallWithResponseProps {
|
||||||
isCancelledMessage: boolean;
|
isCancelledMessage: boolean;
|
||||||
toolRequest: ToolRequestMessageContent;
|
toolRequest: ToolRequestMessageContent;
|
||||||
@@ -410,6 +416,20 @@ function ToolCallView({
|
|||||||
case 'computer_control':
|
case 'computer_control':
|
||||||
return `poking around...`;
|
return `poking around...`;
|
||||||
|
|
||||||
|
case 'execute_code': {
|
||||||
|
const toolGraph = args.tool_graph as unknown as ToolGraphNode[] | undefined;
|
||||||
|
if (toolGraph && Array.isArray(toolGraph) && toolGraph.length > 0) {
|
||||||
|
if (toolGraph.length === 1) {
|
||||||
|
return `${toolGraph[0].description}`;
|
||||||
|
}
|
||||||
|
if (toolGraph.length === 2) {
|
||||||
|
return `${toolGraph[0].tool}, ${toolGraph[1].tool}`;
|
||||||
|
}
|
||||||
|
return `${toolGraph.length} tools used`;
|
||||||
|
}
|
||||||
|
return 'executing code';
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// Generic fallback for unknown tools: ToolName + CompactArguments
|
// Generic fallback for unknown tools: ToolName + CompactArguments
|
||||||
// This ensures any MCP tool works without explicit handling
|
// This ensures any MCP tool works without explicit handling
|
||||||
@@ -489,12 +509,34 @@ function ToolCallView({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Tool Details */}
|
{(() => {
|
||||||
{isToolDetails && (
|
const toolName = toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2);
|
||||||
<div className="border-t border-borderSubtle">
|
const toolGraph = toolCall.arguments?.tool_graph as unknown as ToolGraphNode[] | undefined;
|
||||||
<ToolDetailsView toolCall={toolCall} isStartExpanded={isExpandToolDetails} />
|
const code = toolCall.arguments?.code as unknown as string | undefined;
|
||||||
</div>
|
const hasToolGraph =
|
||||||
)}
|
toolName === 'execute_code' &&
|
||||||
|
toolGraph &&
|
||||||
|
Array.isArray(toolGraph) &&
|
||||||
|
toolGraph.length > 0;
|
||||||
|
|
||||||
|
if (hasToolGraph) {
|
||||||
|
return (
|
||||||
|
<div className="border-t border-borderSubtle">
|
||||||
|
<ToolGraphView toolGraph={toolGraph} code={code} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isToolDetails) {
|
||||||
|
return (
|
||||||
|
<div className="border-t border-borderSubtle">
|
||||||
|
<ToolDetailsView toolCall={toolCall} isStartExpanded={isExpandToolDetails} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
|
||||||
{logs && logs.length > 0 && (
|
{logs && logs.length > 0 && (
|
||||||
<div className="border-t border-borderSubtle">
|
<div className="border-t border-borderSubtle">
|
||||||
@@ -553,6 +595,45 @@ function ToolDetailsView({ toolCall, isStartExpanded }: ToolDetailsViewProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ToolGraphViewProps {
|
||||||
|
toolGraph: ToolGraphNode[];
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolGraphView({ toolGraph, code }: ToolGraphViewProps) {
|
||||||
|
const renderGraph = () => {
|
||||||
|
if (toolGraph.length === 0) return null;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
toolGraph.forEach((node, index) => {
|
||||||
|
const deps =
|
||||||
|
node.depends_on.length > 0 ? ` (uses ${node.depends_on.map((d) => d + 1).join(', ')})` : '';
|
||||||
|
lines.push(`${index + 1}. ${node.tool}: ${node.description}${deps}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<pre className="font-mono text-xs text-textSubtle whitespace-pre-wrap">{renderGraph()}</pre>
|
||||||
|
{code && (
|
||||||
|
<div className="border-t border-borderSubtle -mx-4 mt-2">
|
||||||
|
<ToolCallExpandable
|
||||||
|
label={<span className="pl-4 font-sans text-sm">Code</span>}
|
||||||
|
isStartExpanded={false}
|
||||||
|
>
|
||||||
|
<pre className="font-mono text-xs text-textSubtle whitespace-pre-wrap overflow-x-auto px-4 py-2">
|
||||||
|
{code}
|
||||||
|
</pre>
|
||||||
|
</ToolCallExpandable>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface ToolResultViewProps {
|
interface ToolResultViewProps {
|
||||||
result: Content;
|
result: Content;
|
||||||
isStartExpanded: boolean;
|
isStartExpanded: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user