Skip to content

Commit c1aeb7b

Browse files
authored
feat(bot): Improve Slack-native AI Assistant behavior (#2842)
* feat(bot): add Slack assistant prompts * Small changes * Cleanup * Cleaner * Add some more instructions * feat(bot): improve Slack App Home * refactor(bot): require Slack bot token
1 parent b8e650c commit c1aeb7b

5 files changed

Lines changed: 184 additions & 65 deletions

File tree

apps/web/src/app/api/internal/bot-session-callback/[botRequestId]/route.ts

Lines changed: 31 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ async function getPlatformIntegrationById(platformIntegrationId: string | null)
6767
return integration ?? null;
6868
}
6969

70-
async function getSlackBotToken(platformIntegrationId: string | null): Promise<string | null> {
70+
async function getSlackBotToken(platformIntegrationId: string | null): Promise<string> {
7171
if (!platformIntegrationId) {
72-
return null;
72+
throw new Error('No Slack bot token found for null platform integration');
7373
}
7474

7575
const [integration] = await db
@@ -82,14 +82,18 @@ async function getSlackBotToken(platformIntegrationId: string | null): Promise<s
8282

8383
const teamId = integration?.platformInstallationId;
8484
if (!teamId) {
85-
return null;
85+
throw new Error(`No Slack team found for platform integration ${platformIntegrationId}`);
8686
}
8787

8888
await bot.initialize();
8989
const slackAdapter = bot.getAdapter('slack');
9090
const installation = await slackAdapter.getInstallation(teamId);
9191

92-
return installation?.botToken ?? null;
92+
if (!installation?.botToken) {
93+
throw new Error(`No Slack bot token found for platform integration ${platformIntegrationId}`);
94+
}
95+
96+
return installation.botToken;
9397
}
9498

9599
function logCallback(message: string, extra?: Record<string, unknown>) {
@@ -108,35 +112,6 @@ function parseTerminalCallbackStatus(status: unknown): TerminalCallbackStatus |
108112
return undefined;
109113
}
110114

111-
/**
112-
* Swap the :eyes: reaction on the original user message to :check: (or just
113-
* remove :eyes: on failure). Best-effort — failures are logged but never block.
114-
*/
115-
async function swapReaction(
116-
requestRow: NonNullable<Awaited<ReturnType<typeof getBotRequest>>>,
117-
success: boolean
118-
): Promise<void> {
119-
const messageId = requestRow.platform_message_id;
120-
const threadId = requestRow.platform_thread_id;
121-
if (!messageId) return;
122-
123-
try {
124-
await bot.initialize();
125-
const slackAdapter = bot.getAdapter('slack');
126-
const botToken = await getSlackBotToken(requestRow.platform_integration_id);
127-
if (!botToken) return;
128-
129-
await slackAdapter.withBotToken(botToken, async () => {
130-
await slackAdapter.removeReaction(threadId, messageId, 'eyes').catch(() => {});
131-
if (success) {
132-
await slackAdapter.addReaction(threadId, messageId, 'white_check_mark').catch(() => {});
133-
}
134-
});
135-
} catch (error) {
136-
console.error('[BotSessionCallback] Failed to swap reaction:', error);
137-
}
138-
}
139-
140115
async function completeBotRequest(params: {
141116
botRequestId: string;
142117
expectedCloudAgentSessionId?: string;
@@ -211,7 +186,23 @@ async function failBotRequestForCallbackProcessingError(params: {
211186
markdown: params.errorMessage,
212187
platformIntegrationId: params.requestRow.platform_integration_id,
213188
});
214-
await swapReaction(params.requestRow, false);
189+
}
190+
191+
async function startTyping({
192+
threadId,
193+
platformIntegrationId,
194+
}: {
195+
threadId: string;
196+
platformIntegrationId: string | null;
197+
}): Promise<void> {
198+
const botToken = await getSlackBotToken(platformIntegrationId);
199+
200+
await bot.initialize();
201+
const slackAdapter = bot.getAdapter('slack');
202+
203+
await slackAdapter.withBotToken(botToken, async () => {
204+
await slackAdapter.startTyping(threadId, 'Processing Cloud Agent result...');
205+
});
215206
}
216207

217208
async function postSlackThreadMessage(params: {
@@ -226,11 +217,6 @@ async function postSlackThreadMessage(params: {
226217
});
227218

228219
const botToken = await getSlackBotToken(params.platformIntegrationId);
229-
if (!botToken) {
230-
throw new Error(
231-
`No Slack bot token found for platform integration ${params.platformIntegrationId ?? 'null'}`
232-
);
233-
}
234220

235221
await bot.initialize();
236222
const slackAdapter = bot.getAdapter('slack');
@@ -267,14 +253,9 @@ async function continueBotAgentAfterCallback(params: {
267253
}
268254

269255
await bot.initialize();
270-
await bot.registerSingleton();
256+
bot.registerSingleton();
271257
const slackAdapter = bot.getAdapter('slack');
272258
const botToken = await getSlackBotToken(params.requestRow.platform_integration_id);
273-
if (!botToken) {
274-
throw new Error(
275-
`No Slack bot token found for platform integration ${params.requestRow.platform_integration_id ?? 'null'}`
276-
);
277-
}
278259

279260
return await slackAdapter.withBotToken(botToken, async () => {
280261
const [threadInfo, originalMessage] = await Promise.all([
@@ -565,6 +546,11 @@ async function handleCompletedCallback(
565546
);
566547
}
567548

549+
await startTyping({
550+
threadId: requestRow.platform_thread_id,
551+
platformIntegrationId: requestRow.platform_integration_id,
552+
});
553+
568554
const failedSessions = readiness.sessions.filter(session => session.status !== 'completed');
569555
if (failedSessions.length > 0) {
570556
const errorMessage = formatTerminalGroupFailureMessage(readiness.sessions);
@@ -586,7 +572,6 @@ async function handleCompletedCallback(
586572
markdown: errorMessage,
587573
platformIntegrationId: requestRow.platform_integration_id,
588574
});
589-
await swapReaction(requestRow, false);
590575
}
591576
return;
592577
}
@@ -614,7 +599,6 @@ async function handleCompletedCallback(
614599
markdown: errorMessage,
615600
platformIntegrationId: requestRow.platform_integration_id,
616601
});
617-
await swapReaction(requestRow, false);
618602
}
619603
return;
620604
}
@@ -704,7 +688,6 @@ async function handleCompletedCallback(
704688
platformIntegrationId: requestRow.platform_integration_id,
705689
});
706690

707-
await swapReaction(requestRow, true);
708691
return;
709692
}
710693

@@ -764,8 +747,6 @@ ${cloudAgentResultsForPrompt}`;
764747
markdown: continuation.finalText,
765748
platformIntegrationId: requestRow.platform_integration_id,
766749
});
767-
768-
await swapReaction(requestRow, true);
769750
}
770751

771752
async function handleFailedCallback(
@@ -852,8 +833,6 @@ async function handleFailedCallback(
852833
markdown: errorMessage,
853834
platformIntegrationId: requestRow.platform_integration_id,
854835
});
855-
856-
await swapReaction(requestRow, false);
857836
}
858837

859838
export async function POST(

apps/web/src/lib/bot.ts

Lines changed: 145 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Chat, emoji, type ActionEvent, type Message, type Thread } from 'chat';
2-
import { createSlackAdapter } from '@chat-adapter/slack';
1+
import { Chat, type ActionEvent, type Message, type Thread } from 'chat';
2+
import { createSlackAdapter, SlackAdapter } from '@chat-adapter/slack';
33
import { captureException } from '@sentry/nextjs';
4+
import type { HomeView } from '@slack/types';
45
import { resolveKiloUserId, unlinkKiloUser } from '@/lib/bot-identity';
56
import { getPlatformIdentity, getPlatformIntegration } from '@/lib/bot/platform-helpers';
67
import { LINK_ACCOUNT_ACTION_PREFIX, promptLinkAccount } from '@/lib/bot/link-account';
@@ -10,10 +11,115 @@ import { processMessage } from '@/lib/bot/run';
1011
import { createChatState } from '@/lib/bot/state';
1112
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET } from '@/lib/config.server';
1213

14+
const SLACK_ASSISTANT_SUGGESTED_PROMPTS = [
15+
{
16+
title: 'Fix an issue in my codebase',
17+
message: 'Please ask me for the link to an issue that I want you to fix.',
18+
},
19+
{
20+
title: 'Fix a bug',
21+
message: 'Help me investigate and fix a bug in my codebase.',
22+
},
23+
{
24+
title: 'Review code',
25+
message: 'Please ask me for a PR that you should review',
26+
},
27+
{
28+
title: 'Explain Kilo Bot',
29+
message: 'What can Kilo Bot do from Slack, and how do I get started?',
30+
},
31+
] as const;
32+
33+
const ASSISTANT_PROMPTS_TITLE = 'Try asking Kilo Bot';
34+
35+
export function buildSlackAppHomeView() {
36+
return {
37+
type: 'home',
38+
blocks: [
39+
{
40+
type: 'header',
41+
text: { type: 'plain_text', text: 'Welcome to Kilo Bot', emoji: true },
42+
},
43+
{
44+
type: 'section',
45+
text: {
46+
type: 'mrkdwn',
47+
text: 'Turn Slack messages into focused coding work. Ask Kilo to investigate bugs, review pull requests, explain code, or start a Cloud Agent session in your connected repositories.',
48+
},
49+
},
50+
{
51+
type: 'actions',
52+
elements: [
53+
{
54+
type: 'button',
55+
text: { type: 'plain_text', text: 'Read the docs', emoji: true },
56+
url: 'https://kilo.ai/docs/advanced-usage/slackbot',
57+
action_id: 'kilo_bot_home_docs',
58+
},
59+
{
60+
type: 'button',
61+
text: { type: 'plain_text', text: 'Open Kilo', emoji: true },
62+
url: 'https://app.kilo.ai',
63+
action_id: 'kilo_bot_home_app',
64+
style: 'primary',
65+
},
66+
],
67+
},
68+
{ type: 'divider' },
69+
{
70+
type: 'section',
71+
text: { type: 'mrkdwn', text: '*What you can ask me to do*' },
72+
},
73+
{
74+
type: 'section',
75+
fields: [
76+
{
77+
type: 'mrkdwn',
78+
text: '*Fix issues*\nPaste an issue link or describe a bug and I can investigate the codebase.',
79+
},
80+
{
81+
type: 'mrkdwn',
82+
text: '*Review PRs*\nSend a pull request link and ask for risks, regressions, or missing tests.',
83+
},
84+
{
85+
type: 'mrkdwn',
86+
text: '*Make changes*\nAsk for implementation work and I can start a Cloud Agent session.',
87+
},
88+
{
89+
type: 'mrkdwn',
90+
text: '*Answer questions*\nAsk about repo structure, code behavior, or how to use Kilo from Slack.',
91+
},
92+
],
93+
},
94+
{ type: 'divider' },
95+
{
96+
type: 'section',
97+
text: { type: 'mrkdwn', text: '*Try these prompts*' },
98+
},
99+
{
100+
type: 'section',
101+
text: {
102+
type: 'mrkdwn',
103+
text: '• `Fix this issue: <issue link>`\n• `Review this PR for bugs: <PR link>`\n• `Implement <feature> in <repo>`\n• `Explain how <component> works`',
104+
},
105+
},
106+
{ type: 'divider' },
107+
{
108+
type: 'context',
109+
elements: [
110+
{
111+
type: 'mrkdwn',
112+
text: 'Tip: If your Slack account is not linked yet, mention Kilo or send a message and I will provide a secure link prompt.',
113+
},
114+
],
115+
},
116+
],
117+
} satisfies HomeView;
118+
}
119+
13120
function createKiloBot(slackAdapter: ReturnType<typeof createSlackAdapter>) {
14121
const chatBot = new Chat({
15-
// TODO(remon): Update names before going live
16-
userName: process.env.NODE_ENV === 'production' ? 'Pound' : 'Sjors Bot',
122+
userName: process.env.NODE_ENV === 'production' ? 'Kilo' : 'Henk',
17123
adapters: {
18124
slack: slackAdapter,
19125
},
@@ -62,10 +168,9 @@ function createKiloBot(slackAdapter: ReturnType<typeof createSlackAdapter>) {
62168
modelUsed: undefined,
63169
});
64170

65-
const received = thread.createSentMessageFromMessage(message);
66-
await received.addReaction(emoji.eyes);
171+
chatBot.registerSingleton();
67172

68-
await chatBot.registerSingleton();
173+
await thread.startTyping('Thinking...');
69174

70175
try {
71176
await processMessage({ thread, message, platformIntegration, user, botRequestId });
@@ -98,6 +203,39 @@ function createKiloBot(slackAdapter: ReturnType<typeof createSlackAdapter>) {
98203
}
99204
});
100205

206+
chatBot.onAssistantThreadStarted(async event => {
207+
if (!(event.adapter instanceof SlackAdapter)) return;
208+
209+
try {
210+
await event.adapter.setSuggestedPrompts(
211+
event.channelId,
212+
event.threadTs,
213+
[...SLACK_ASSISTANT_SUGGESTED_PROMPTS],
214+
ASSISTANT_PROMPTS_TITLE
215+
);
216+
} catch (error) {
217+
console.error('[Bot] Failed to set suggested prompts:', error);
218+
captureException(error, {
219+
tags: { component: 'kilo-bot', op: 'assistant-thread-started' },
220+
extra: { userId: event.userId, channelId: event.channelId },
221+
});
222+
}
223+
});
224+
225+
chatBot.onAppHomeOpened(async event => {
226+
if (!(event.adapter instanceof SlackAdapter)) return;
227+
228+
try {
229+
await event.adapter.publishHomeView(event.userId, buildSlackAppHomeView());
230+
} catch (error) {
231+
console.error('[Bot] Failed to publish Slack App Home:', error);
232+
captureException(error, {
233+
tags: { component: 'kilo-bot', op: 'app-home-opened' },
234+
extra: { userId: event.userId, channelId: event.channelId },
235+
});
236+
}
237+
});
238+
101239
return chatBot;
102240
}
103241

apps/web/src/lib/bot/agent-runner.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async function buildSystemPrompt(
113113
- If the user's request is ambiguous, ask 1-2 clarifying questions instead of guessing.
114114
115115
## Answering questions about Kilo Bot
116-
- When users ask what you can do, how you work, or for general help, include a link to the Bot documentation: https://kilo.ai/docs/advanced-usage/slackbot
116+
- When users ask what you can do, how you work, or for general help, include a link to the Bot documentation: https://kilo.ai/docs/code-with-ai/platforms/slack
117117
- Provide the docs link along with your answer so users can learn more.
118118
119119
## Context you may receive
@@ -127,6 +127,9 @@ ${formatGitLabRepositoriesForPrompt(gitlabContext)}
127127
128128
Treat this context as authoritative. Prefer selecting a repo from the provided repository list. If the user requests work on a repo that isn't in the list, ask them to confirm the exact owner/repo (or group/project for GitLab) and ensure it's accessible to the integration. Never invent repository names.
129129
130+
## Cloud Agent tool
131+
If the user asks you to analyze or act on an attached image, you must use the spawnCloudAgentSession tool to start a Cloud Agent session that will analyze the image.
132+
130133
## Accuracy & safety
131134
- Don't claim you ran tools, changed code, or created a PR/MR unless the tool results confirm it.
132135
- Don't fabricate links (including PR/MR URLs).
@@ -272,6 +275,9 @@ This tool returns an acknowledgement immediately. The final Cloud Agent result w
272275
execute: async args => {
273276
let resolvedCloudAgentSessionId: string | undefined;
274277
let resolvedKiloSessionId: string | undefined;
278+
279+
await params.thread.startTyping('Spawning Cloud Agent session...');
280+
275281
const currentStep = getNextBotCallbackStep({
276282
completedStepCount,
277283
completedStepsInCurrentRun: collectedSteps.length,

0 commit comments

Comments
 (0)