Slack: send assistant thread status while typing
This commit is contained in:
committed by
Peter Steinberger
parent
792ae99ffc
commit
8ebc789d25
@@ -87,6 +87,7 @@
|
|||||||
- Skills: emit MEDIA token after Nano Banana Pro image generation. Thanks @Iamadig for PR #271.
|
- Skills: emit MEDIA token after Nano Banana Pro image generation. Thanks @Iamadig for PR #271.
|
||||||
- WhatsApp: set sender E.164 for direct chats so owner commands work in DMs.
|
- WhatsApp: set sender E.164 for direct chats so owner commands work in DMs.
|
||||||
- Slack: keep auto-replies in the original thread when responding to thread messages. Thanks @scald for PR #251.
|
- Slack: keep auto-replies in the original thread when responding to thread messages. Thanks @scald for PR #251.
|
||||||
|
- Slack: send typing status updates via assistant threads. Thanks @thewilloftheshadow for PR #320.
|
||||||
- Slack: fix Slack provider startup under Bun by using a named import for Bolt `App`. Thanks @snopoke for PR #299.
|
- Slack: fix Slack provider startup under Bun by using a named import for Bolt `App`. Thanks @snopoke for PR #299.
|
||||||
- Discord: surface missing-permission hints (muted/role overrides) when replies fail.
|
- Discord: surface missing-permission hints (muted/role overrides) when replies fail.
|
||||||
- Discord: use channel IDs for DMs instead of user IDs. Thanks @VACInc for PR #261.
|
- Discord: use channel IDs for DMs instead of user IDs. Thanks @VACInc for PR #261.
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ vi.mock("@slack/bolt", () => {
|
|||||||
user: { profile: { display_name: "Ada" } },
|
user: { profile: { display_name: "Ada" } },
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
assistant: {
|
||||||
|
threads: {
|
||||||
|
setStatus: vi.fn().mockResolvedValue({ ok: true }),
|
||||||
|
},
|
||||||
|
},
|
||||||
reactions: {
|
reactions: {
|
||||||
add: (...args: unknown[]) => reactMock(...args),
|
add: (...args: unknown[]) => reactMock(...args),
|
||||||
},
|
},
|
||||||
@@ -149,6 +154,49 @@ describe("monitorSlackProvider tool results", () => {
|
|||||||
expect(sendMock.mock.calls[1][1]).toBe("PFX final reply");
|
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 () => {
|
it("accepts channel messages when mentionPatterns match", async () => {
|
||||||
config = {
|
config = {
|
||||||
messages: { responsePrefix: "PFX" },
|
messages: { responsePrefix: "PFX" },
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
matchesMentionPatterns,
|
matchesMentionPatterns,
|
||||||
} from "../auto-reply/reply/mentions.js";
|
} from "../auto-reply/reply/mentions.js";
|
||||||
import { createReplyDispatcher } from "../auto-reply/reply/reply-dispatcher.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 { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.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: {
|
const isChannelAllowed = (params: {
|
||||||
channelId?: string;
|
channelId?: string;
|
||||||
channelName?: string;
|
channelName?: string;
|
||||||
@@ -823,6 +859,15 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
|
|
||||||
// Only thread replies if the incoming message was in a thread.
|
// Only thread replies if the incoming message was in a thread.
|
||||||
const incomingThreadTs = message.thread_ts;
|
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({
|
const dispatcher = createReplyDispatcher({
|
||||||
responsePrefix: cfg.messages?.responsePrefix,
|
responsePrefix: cfg.messages?.responsePrefix,
|
||||||
deliver: async (payload) => {
|
deliver: async (payload) => {
|
||||||
@@ -835,10 +880,18 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
|||||||
threadTs: incomingThreadTs,
|
threadTs: incomingThreadTs,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
onIdle: () => {
|
||||||
|
typingController?.markDispatchIdle();
|
||||||
|
},
|
||||||
onError: (err, info) => {
|
onError: (err, info) => {
|
||||||
runtime.error?.(
|
runtime.error?.(
|
||||||
danger(`slack ${info.kind} reply failed: ${String(err)}`),
|
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,
|
ctx: ctxPayload,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
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()) {
|
if (shouldLogVerbose()) {
|
||||||
const finalCount = counts.final;
|
const finalCount = counts.final;
|
||||||
logVerbose(
|
logVerbose(
|
||||||
|
|||||||
Reference in New Issue
Block a user