feat(slack): add typingReaction config for DM typing indicator fallback (#19816)

* feat(slack): add typingReaction config for DM typing indicator fallback

Adds a reaction-based typing indicator for Slack DMs that works without
assistant mode. When `channels.slack.typingReaction` is set (e.g.
"hourglass_flowing_sand"), the emoji is added to the user's message when
processing starts and removed when the reply is sent.

Addresses #19809

* test(slack): add typingReaction to createSlackMonitorContext test callers

* test(slack): add typingReaction to test context callers

* test(slack): add typingReaction to context fixture

* docs(changelog): credit Slack typingReaction feature

* test(slack): align existing-thread history expectation

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Dale Yarborough
2026-03-03 23:07:17 -06:00
committed by GitHub
parent 230fea1ca6
commit a95a0be133
10 changed files with 115 additions and 32 deletions

View File

@@ -11,7 +11,7 @@ import { resolveStorePath, updateLastRoute } from "../../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../globals.js";
import { resolveAgentOutboundIdentity } from "../../../infra/outbound/identity.js";
import { resolvePinnedMainDmOwnerFromAllowlist } from "../../../security/dm-policy-shared.js";
import { removeSlackReaction } from "../../actions.js";
import { reactSlackMessage, removeSlackReaction } from "../../actions.js";
import { createSlackDraftStream } from "../../draft-stream.js";
import { normalizeSlackOutboundText } from "../../format.js";
import { recordSlackThreadParticipation } from "../../sent-thread-cache.js";
@@ -140,6 +140,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
});
const typingTarget = statusThreadTs ? `${message.channel}/${statusThreadTs}` : message.channel;
const typingReaction = ctx.typingReaction;
const typingCallbacks = createTypingCallbacks({
start: async () => {
didSetStatus = true;
@@ -148,6 +149,12 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
threadTs: statusThreadTs,
status: "is typing...",
});
if (typingReaction && message.ts) {
await reactSlackMessage(message.channel, message.ts, typingReaction, {
token: ctx.botToken,
client: ctx.app.client,
}).catch(() => {});
}
},
stop: async () => {
if (!didSetStatus) {
@@ -159,6 +166,12 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
threadTs: statusThreadTs,
status: "",
});
if (typingReaction && message.ts) {
await removeSlackReaction(message.channel, message.ts, typingReaction, {
token: ctx.botToken,
client: ctx.app.client,
}).catch(() => {});
}
},
onStartError: (err) => {
logTypingFailure({