Skip to content

Commit 5fa2a2d

Browse files
fix(batch): restore per-tool UI feedback + UX improvements
1 parent 8f309b1 commit 5fa2a2d

File tree

2 files changed

+70
-16
lines changed

2 files changed

+70
-16
lines changed

.opencode/opencode.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
22
"$schema": "https://opencode.ai/config.json",
3-
"plugin": ["opencode-openai-codex-auth"]
3+
"plugin": ["opencode-openai-codex-auth"],
4+
"experimental": {
5+
"batch_tool": true
6+
}
47
}

packages/opencode/src/tool/batch.ts

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const BatchTool = Tool.define("batch", async () => {
3131
return `Invalid parameters for tool 'batch':\n${formattedErrors}\n\nExpected payload format:\n [{"tool": "tool_name", "parameters": {...}}, {...}]`
3232
},
3333
async execute(params, ctx) {
34+
const { Session } = await import("../session")
3435
const { Identifier } = await import("../id/id")
3536

3637
const toolCalls = params.tool_calls
@@ -39,26 +40,36 @@ export const BatchTool = Tool.define("batch", async () => {
3940
const availableTools = await ToolRegistry.tools("", "")
4041
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
4142

43+
const partIDs = new Map<(typeof toolCalls)[0], string>()
4244
for (const call of toolCalls) {
43-
if (DISALLOWED.has(call.tool)) {
44-
throw new Error(
45-
`tool '${call.tool}' is not allowed in batch. Disallowed tools: ${Array.from(DISALLOWED).join(", ")}`,
46-
)
47-
}
48-
if (!toolMap.has(call.tool)) {
49-
const allowed = Array.from(toolMap.keys()).filter((name) => !FILTERED_FROM_SUGGESTIONS.has(name))
50-
throw new Error(`tool '${call.tool}' is not available. Available tools: ${allowed.join(", ")}`)
51-
}
45+
const partID = Identifier.ascending("part")
46+
partIDs.set(call, partID)
47+
Session.updatePart({
48+
id: partID,
49+
messageID: ctx.messageID,
50+
sessionID: ctx.sessionID,
51+
type: "tool",
52+
tool: call.tool,
53+
callID: partID,
54+
state: {
55+
status: "pending",
56+
input: call.parameters,
57+
raw: JSON.stringify(call),
58+
},
59+
})
5260
}
5361

5462
const executeCall = async (call: (typeof toolCalls)[0]) => {
55-
if (ctx.abort.aborted) {
56-
return { success: false as const, tool: call.tool, error: new Error("Aborted") }
57-
}
58-
59-
const partID = Identifier.ascending("part")
63+
const callStartTime = Date.now()
64+
const partID = partIDs.get(call)!
6065

6166
try {
67+
if (DISALLOWED.has(call.tool)) {
68+
throw new Error(
69+
`Tool '${call.tool}' is not allowed in batch. Disallowed tools: ${Array.from(DISALLOWED).join(", ")}`,
70+
)
71+
}
72+
6273
const tool = toolMap.get(call.tool)
6374
if (!tool) {
6475
const availableToolsList = Array.from(toolMap.keys()).filter((name) => !FILTERED_FROM_SUGGESTIONS.has(name))
@@ -68,13 +79,53 @@ export const BatchTool = Tool.define("batch", async () => {
6879

6980
const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
7081

82+
await Session.updatePart({
83+
id: partID,
84+
messageID: ctx.messageID,
85+
sessionID: ctx.sessionID,
86+
type: "tool",
87+
tool: call.tool,
88+
callID: partID,
89+
state: {
90+
status: "completed",
91+
input: call.parameters,
92+
output: result.output,
93+
title: result.title,
94+
metadata: result.metadata,
95+
attachments: result.attachments,
96+
time: {
97+
start: callStartTime,
98+
end: Date.now(),
99+
},
100+
},
101+
})
102+
71103
return { success: true as const, tool: call.tool, result }
72104
} catch (error) {
105+
await Session.updatePart({
106+
id: partID,
107+
messageID: ctx.messageID,
108+
sessionID: ctx.sessionID,
109+
type: "tool",
110+
tool: call.tool,
111+
callID: partID,
112+
state: {
113+
status: "error",
114+
input: call.parameters,
115+
error: error instanceof Error ? error.message : String(error),
116+
time: {
117+
start: callStartTime,
118+
end: Date.now(),
119+
},
120+
},
121+
})
122+
73123
return { success: false as const, tool: call.tool, error }
74124
}
75125
}
76126

77-
const results = await Promise.all(toolCalls.flatMap((call) => executeCall(call)))
127+
const results = await Promise.all(toolCalls.map((call) => executeCall(call)))
128+
78129
const successfulCalls = results.filter((r) => r.success).length
79130
const failedCalls = toolCalls.length - successfulCalls
80131

0 commit comments

Comments
 (0)