Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 67 additions & 57 deletions slack-mcp/server/slack/handlers/eventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,20 @@ async function processAttachedFiles(
};
}

/**
* Add 👀 then remove it without blocking the response path.
*
* The reaction is purely visual feedback while the bot is thinking — the
* "Pensando..." message provides the real signal — so we never await
* either Slack RTT. Errors are swallowed: a failed reaction is harmless.
*/
function fireReactionCycle(skip: boolean, channel: string, ts: string): void {
if (skip) return;
addReaction(channel, ts, "eyes")
.then(() => removeReaction(channel, ts, "eyes"))
.catch(() => {});
}

/**
* Resolve a Slack user's display name (display_name → real_name → name → userId).
* Used as the stable decopilot thread_id so the agent has memory per person.
Expand Down Expand Up @@ -453,21 +467,21 @@ async function handleAppMention(
const showOnlyFinal =
teamConfig.responseConfig?.showOnlyFinalResponse ?? false;

if (!showOnlyFinal) {
await addReaction(channel, ts, "eyes");
}

const replyTo = thread_ts ?? ts;
const showThinking = showOnlyFinal
? false
: (teamConfig.responseConfig?.showThinkingMessage ?? true);
const thinkingMsg = showThinking
? await sendThinkingMessage(channel, replyTo)
: null;

if (!showOnlyFinal) {
await removeReaction(channel, ts, "eyes");
}
// Kick off the thinking message immediately — its Slack RTT is the
// user-visible latency, so we don't gate it behind anything. The 👀
// reaction is pure UI feedback, fire-and-forget. resolveUserName and
// file processing happen in parallel; we only await everything just
// before the LLM call (when we need their values).
fireReactionCycle(showOnlyFinal, channel, ts);
const thinkingPromise = showThinking
? sendThinkingMessage(channel, replyTo)
: Promise.resolve(null);
const userNamePromise = resolveUserName(user);

const { media, textFiles, transcriptions, audioWithoutWhisper } =
await processAttachedFiles(files);
Expand Down Expand Up @@ -498,19 +512,20 @@ async function handleAppMention(
const mediaForLLM =
transcriptions.length > 0 ? media.filter((m) => m.type === "image") : media;

const messages = await buildLLMMessages(
channel,
fullText,
ts,
thread_ts,
mediaForLLM,
true,
);
const [messages, isAvailable, thinkingMsg, userName] = await Promise.all([
buildLLMMessages(channel, fullText, ts, thread_ts, mediaForLLM, true),
isLLMAvailable(connectionId),
thinkingPromise,
userNamePromise,
]);

if (!(await isLLMAvailable(connectionId))) {
if (!isAvailable) {
const warningMsg =
"Bot ainda inicializando. Por favor, tente novamente em alguns segundos.";
await replyInThread(channel, replyTo, warningMsg);
if (thinkingMsg?.ts) {
await deleteMessage(channel, thinkingMsg.ts);
}
return;
}

Expand All @@ -524,7 +539,7 @@ async function handleAppMention(
replyTo,
thinkingMessageTs: thinkingMsg?.ts,
streamingEnabled: enableStreaming,
userName: await resolveUserName(user),
userName,
slackEvent: { text: fullText, user, ts, thread_ts },
});
} catch (error) {
Expand Down Expand Up @@ -657,43 +672,35 @@ async function handleDirectMessage(
const showOnlyFinal =
teamConfig.responseConfig?.showOnlyFinalResponse ?? false;

if (!showOnlyFinal) {
await addReaction(channel, ts, "eyes");
}

// Every top-level DM kicks off a brand-new thread under the user's
// message — bot's thinking message and final reply both live inside that
// thread. Subsequent replies from the user in the thread continue there
// (handled by handleThreadReply), so each subject stays isolated.
const replyTo = ts;

const showThinking = showOnlyFinal
? false
: (teamConfig.responseConfig?.showThinkingMessage ?? true);
const thinkingMsg = showThinking
? await sendThinkingMessage(channel, replyTo)
: null;

if (!showOnlyFinal) {
await removeReaction(channel, ts, "eyes");
}

// Resolve sender name (used as a prefix for the LLM context)
// Fire thinking + reactions + name lookup in parallel. The thinking msg
// RTT is the user-visible latency for "Pensando..." appearing in the
// thread, so we don't gate it behind anything.
fireReactionCycle(showOnlyFinal, channel, ts);
const thinkingPromise = showThinking
? sendThinkingMessage(channel, replyTo)
: Promise.resolve(null);
const senderName = await resolveUserName(user);
const senderText =
senderName && senderName !== user
? `[Mensagem de ${senderName}]\n${text}`
: text;

const messages = await buildLLMMessages(
channel,
senderText,
ts,
undefined,
media,
);
const [messages, isAvailable, thinkingMsg] = await Promise.all([
buildLLMMessages(channel, senderText, ts, undefined, media),
isLLMAvailable(connectionId),
thinkingPromise,
]);

if (!(await isLLMAvailable(connectionId))) {
if (!isAvailable) {
const warningMsg =
"Bot ainda inicializando. Por favor, tente novamente em alguns segundos.";
await replyInThread(channel, replyTo, warningMsg);
Expand Down Expand Up @@ -748,25 +755,28 @@ async function handleThreadReply(
): Promise<void> {
const showOnlyFinal =
teamConfig.responseConfig?.showOnlyFinalResponse ?? false;

if (!showOnlyFinal) {
await addReaction(channel, ts, "eyes");
}

const showThinking = showOnlyFinal
? false
: (teamConfig.responseConfig?.showThinkingMessage ?? true);
const thinkingMsg = showThinking
? await sendThinkingMessage(channel, threadTs)
: null;

if (!showOnlyFinal) {
await removeReaction(channel, ts, "eyes");
}

const messages = await buildLLMMessages(channel, text, ts, threadTs, media);

if (!(await isLLMAvailable(connectionId))) {
// Same parallelization as handleAppMention / handleDirectMessage:
// thinking RTT defines user-visible latency, so don't gate it on
// anything; reactions are cosmetic and fire-and-forget; name lookup
// and message building run alongside the Slack RTT.
fireReactionCycle(showOnlyFinal, channel, ts);
const thinkingPromise = showThinking
? sendThinkingMessage(channel, threadTs)
: Promise.resolve(null);
const userNamePromise = resolveUserName(user);

const [messages, isAvailable, thinkingMsg, userName] = await Promise.all([
buildLLMMessages(channel, text, ts, threadTs, media),
isLLMAvailable(connectionId),
thinkingPromise,
userNamePromise,
]);

if (!isAvailable) {
const warningMsg =
"Bot ainda inicializando. Por favor, tente novamente em alguns segundos.";
await replyInThread(channel, threadTs, warningMsg);
Expand All @@ -786,7 +796,7 @@ async function handleThreadReply(
replyTo: threadTs,
thinkingMessageTs: thinkingMsg?.ts,
streamingEnabled: enableStreaming,
userName: await resolveUserName(user),
userName,
slackEvent: { text, user, ts, thread_ts: threadTs },
});
} catch (error) {
Expand Down
Loading