Skip to content

Commit ca72ead

Browse files
committed
fix(slack): render approval controls for every pending tool call
The HITL renderer only posted approve/deny buttons for the first pending tool call, so when a turn paused on multiple tool approvals the rest never got controls and the turn could not finish. It now renders a section plus an approve/deny pair for each pending call.
1 parent d915b8f commit ca72ead

2 files changed

Lines changed: 38 additions & 15 deletions

File tree

packages/slack/src/index.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ describe("slack channel", () => {
6969
expect(text.length).toBeLessThan(3000);
7070
});
7171

72+
it("renderInteraction posts controls for every pending tool call, not just the first", () => {
73+
const c = slack({ id: "s-hitl-multi", token: "t" });
74+
const msg = c.renderInteraction?.(
75+
[
76+
{ toolCallId: "call-a", toolName: "refund", input: { amount: 1 } },
77+
{ toolCallId: "call-b", toolName: "sendEmail", input: { to: "x" } },
78+
],
79+
{ event: messageEvent(), deliveryId: "d1" }
80+
);
81+
expect(msg).not.toBeNull();
82+
const values = (msg!.blocks as any[]).flatMap((b) => b.elements ?? []).map((e: any) => e.value);
83+
expect(values).toEqual(
84+
expect.arrayContaining(["call-a::approve", "call-a::deny", "call-b::approve", "call-b::deny"])
85+
);
86+
});
87+
7288
it("onInteraction resolves a block_actions click to a tool output; ignores messages", () => {
7389
const c = slack({ id: "s-hitl2", token: "t" });
7490
const approve = c.onInteraction?.({

packages/slack/src/index.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -203,20 +203,19 @@ function defaultSlackRenderInteraction(
203203
pending: ChannelPendingToolCall[],
204204
_ctx: ChannelInteractionCtx<unknown>
205205
): ChannelMessage | null {
206-
const call = pending[0];
207-
if (!call) return null;
208-
let detail = "";
209-
if (call.input !== undefined) {
210-
const serialized = safeStringify(call.input);
211-
const shown =
212-
serialized.length > MAX_INTERACTION_INPUT_CHARS
213-
? serialized.slice(0, MAX_INTERACTION_INPUT_CHARS) + "\n... (truncated)"
214-
: serialized;
215-
detail = "\n```" + shown + "```";
216-
}
217-
return {
218-
text: `Approval needed: ${call.toolName}`,
219-
blocks: [
206+
if (pending.length === 0) return null;
207+
208+
const blocks = pending.flatMap((call) => {
209+
let detail = "";
210+
if (call.input !== undefined) {
211+
const serialized = safeStringify(call.input);
212+
const shown =
213+
serialized.length > MAX_INTERACTION_INPUT_CHARS
214+
? serialized.slice(0, MAX_INTERACTION_INPUT_CHARS) + "\n... (truncated)"
215+
: serialized;
216+
detail = "\n```" + shown + "```";
217+
}
218+
return [
220219
{
221220
type: "section",
222221
text: { type: "mrkdwn", text: `*Approval needed* for \`${call.toolName}\`${detail}` },
@@ -240,7 +239,15 @@ function defaultSlackRenderInteraction(
240239
},
241240
],
242241
},
243-
],
242+
];
243+
});
244+
245+
return {
246+
text:
247+
pending.length === 1
248+
? `Approval needed: ${pending[0]!.toolName}`
249+
: `Approval needed: ${pending.length} tool calls`,
250+
blocks,
244251
};
245252
}
246253

0 commit comments

Comments
 (0)