Slack: send assistant thread status while typing

This commit is contained in:
Shadow
2026-01-06 11:58:28 -06:00
committed by Peter Steinberger
parent 792ae99ffc
commit 8ebc789d25
3 changed files with 117 additions and 1 deletions

View File

@@ -63,6 +63,11 @@ vi.mock("@slack/bolt", () => {
user: { profile: { display_name: "Ada" } },
}),
},
assistant: {
threads: {
setStatus: vi.fn().mockResolvedValue({ ok: true }),
},
},
reactions: {
add: (...args: unknown[]) => reactMock(...args),
},
@@ -149,6 +154,49 @@ describe("monitorSlackProvider tool results", () => {
expect(sendMock.mock.calls[1][1]).toBe("PFX final reply");
});
it("updates assistant thread status when replies start", async () => {
replyMock.mockImplementation(async (_ctx, opts) => {
await opts?.onReplyStart?.();
return { text: "final reply" };
});
const controller = new AbortController();
const run = monitorSlackProvider({
botToken: "bot-token",
appToken: "app-token",
abortSignal: controller.signal,
});
await waitForEvent("message");
const handler = getSlackHandlers()?.get("message");
if (!handler) throw new Error("Slack message handler not registered");
await handler({
event: {
type: "message",
user: "U1",
text: "hello",
ts: "123",
channel: "C1",
channel_type: "im",
},
});
await flush();
controller.abort();
await run;
const client = getSlackClient() as {
assistant?: { threads?: { setStatus?: ReturnType<typeof vi.fn> } };
};
expect(client.assistant?.threads?.setStatus).toHaveBeenCalledWith({
token: "bot-token",
channel_id: "C1",
thread_ts: "123",
status: "is typing...",
});
});
it("accepts channel messages when mentionPatterns match", async () => {
config = {
messages: { responsePrefix: "PFX" },

View File

@@ -20,6 +20,7 @@ import {
matchesMentionPatterns,
} from "../auto-reply/reply/mentions.js";
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.js";
import type { TypingController } from "../auto-reply/reply/typing.js";
import { getReplyFromConfig } from "../auto-reply/reply.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import type { ReplyPayload } from "../auto-reply/types.js";
@@ -499,6 +500,41 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
}
};
const setSlackThreadStatus = async (params: {
channelId: string;
threadTs?: string;
status: string;
}) => {
if (!params.threadTs) return;
const payload = {
token: botToken,
channel_id: params.channelId,
thread_ts: params.threadTs,
status: params.status,
};
const client = app.client as unknown as {
assistant?: {
threads?: {
setStatus?: (args: typeof payload) => Promise<unknown>;
};
};
apiCall?: (method: string, args: typeof payload) => Promise<unknown>;
};
try {
if (client.assistant?.threads?.setStatus) {
await client.assistant.threads.setStatus(payload);
return;
}
if (typeof client.apiCall === "function") {
await client.apiCall("assistant.threads.setStatus", payload);
}
} catch (err) {
logVerbose(
`slack status update failed for channel ${params.channelId}: ${String(err)}`,
);
}
};
const isChannelAllowed = (params: {
channelId?: string;
channelName?: string;
@@ -823,6 +859,15 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
// Only thread replies if the incoming message was in a thread.
const incomingThreadTs = message.thread_ts;
const statusThreadTs = message.thread_ts ?? message.ts;
const onReplyStart = async () => {
await setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "is typing...",
});
};
let typingController: TypingController | undefined;
const dispatcher = createReplyDispatcher({
responsePrefix: cfg.messages?.responsePrefix,
deliver: async (payload) => {
@@ -835,10 +880,18 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
threadTs: incomingThreadTs,
});
},
onIdle: () => {
typingController?.markDispatchIdle();
},
onError: (err, info) => {
runtime.error?.(
danger(`slack ${info.kind} reply failed: ${String(err)}`),
);
void setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "",
});
},
});
@@ -846,8 +899,22 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
ctx: ctxPayload,
cfg,
dispatcher,
replyOptions: {
onReplyStart,
onTypingController: (typing) => {
typingController = typing;
},
},
});
if (!queuedFinal) return;
typingController?.markDispatchIdle();
if (!queuedFinal) {
await setSlackThreadStatus({
channelId: message.channel,
threadTs: statusThreadTs,
status: "",
});
return;
}
if (shouldLogVerbose()) {
const finalCount = counts.final;
logVerbose(