Skip to content

Commit 19c2987

Browse files
feat(bot): add PR signature to Cloud Agent prompts in bot SDK (#1914)
* feat(bot): add PR signature to Cloud Agent prompts in bot SDK Port the PR signature feature from the old slack-bot/discord-bot implementations into the new bot SDK. When the Cloud Agent creates a PR/MR, the description now includes a 'Built for [User](link) by [Kilo for Platform]' attribution line. * style(bot): fix formatting in pr-signature.ts --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
1 parent 7577f7f commit 19c2987

File tree

4 files changed

+142
-8
lines changed

4 files changed

+142
-8
lines changed

src/lib/bot/agent-runner.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getConversationContext,
1010
formatConversationContextForPrompt,
1111
} from '@/lib/bot/conversation-context';
12+
import { buildPrSignature, getRequesterInfo } from '@/lib/bot/pr-signature';
1213
import { updateBotRequest, linkBotRequestToSession } from '@/lib/bot/request-logging';
1314
import spawnCloudAgentSession, {
1415
spawnCloudAgentInputSchema,
@@ -35,7 +36,7 @@ import { ToolLoopAgent, generateText, stepCountIs, tool } from 'ai';
3536
import type { StepResult, ToolSet } from 'ai';
3637
import { Actions, Card, CardText, LinkButton, Section } from 'chat';
3738
import { ThreadImpl } from 'chat';
38-
import type { Author, Thread } from 'chat';
39+
import type { Author, Message, Thread } from 'chat';
3940

4041
export type BotAgentContinuation = {
4142
finalText: string;
@@ -47,6 +48,8 @@ export type BotAgentContinuation = {
4748
type RunBotAgentParams = {
4849
thread: Thread;
4950
message: BotAgentMessageLike;
51+
/** Full chat Message for PR signature (has `raw` for platform-specific fields). */
52+
rawMessage?: Message;
5053
platformIntegration: PlatformIntegration;
5154
user: User;
5255
botRequestId: string | undefined;
@@ -204,6 +207,24 @@ export async function runBotAgent(params: RunBotAgentParams): Promise<BotAgentCo
204207
(params.platformIntegration.metadata as { model_slug?: string }).model_slug ??
205208
DEFAULT_BOT_MODEL;
206209
const owner = ownerFromIntegration(params.platformIntegration);
210+
const chatPlatform = params.thread.id.split(':')[0];
211+
212+
// Build PR signature from requester info (display name + message permalink)
213+
let prSignature: string | undefined;
214+
if (params.rawMessage) {
215+
try {
216+
const requesterInfo = await getRequesterInfo(
217+
params.thread,
218+
params.rawMessage,
219+
params.platformIntegration
220+
);
221+
if (requesterInfo) {
222+
prSignature = buildPrSignature(requesterInfo);
223+
}
224+
} catch (error) {
225+
console.warn('[KiloBot] Failed to build PR signature, continuing without it:', error);
226+
}
227+
}
207228

208229
const startedAt = Date.now();
209230
const collectedSteps: BotRequestStep[] = [];
@@ -250,7 +271,8 @@ This tool returns an acknowledgement immediately. The final Cloud Agent result w
250271
provider,
251272
modelSlug,
252273
});
253-
}
274+
},
275+
{ prSignature, chatPlatform }
254276
);
255277

256278
// Persist the session link synchronously so callbacks can

src/lib/bot/pr-signature.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { getAccessTokenFromInstallation } from '@/lib/integrations/slack-service';
2+
import { getSlackMessagePermalink } from '@/lib/slack-bot/slack-utils';
3+
import { WebClient } from '@slack/web-api';
4+
import type { SlackEvent } from '@chat-adapter/slack';
5+
import type { PlatformIntegration } from '@kilocode/db';
6+
import type { Thread, Message } from 'chat';
7+
8+
type RequesterInfo = {
9+
displayName: string;
10+
messageLink?: string;
11+
platform: string;
12+
};
13+
14+
const PLATFORM_LINKS: Record<string, { label: string; url: string }> = {
15+
slack: { label: 'Kilo for Slack', url: 'https://kilo.ai/features/slack-integration' },
16+
discord: { label: 'Kilo for Discord', url: 'https://kilo.ai' },
17+
};
18+
19+
const DEFAULT_PLATFORM_LINK = { label: 'Kilo', url: 'https://kilo.ai' };
20+
21+
/**
22+
* Build the PR signature instruction to append to the Cloud Agent prompt.
23+
* Instructs the agent to include a "Built for …" line at the end of any
24+
* PR/MR description it creates.
25+
*/
26+
export function buildPrSignature(requesterInfo: RequesterInfo): string {
27+
const requesterPart = requesterInfo.messageLink
28+
? `[${requesterInfo.displayName}](${requesterInfo.messageLink})`
29+
: requesterInfo.displayName;
30+
31+
const { label, url } = PLATFORM_LINKS[requesterInfo.platform] ?? DEFAULT_PLATFORM_LINK;
32+
33+
return `
34+
35+
---
36+
**PR Signature to include in the PR description:**
37+
When you create a pull request or merge request, include the following signature at the end of the PR/MR description:
38+
39+
Built for ${requesterPart} by [${label}](${url})`;
40+
}
41+
42+
/**
43+
* Gather requester info (display name + message link) for the PR signature.
44+
* Platform-specific: uses the Slack API for permalinks, constructs Discord
45+
* links from IDs, and degrades gracefully for unknown platforms.
46+
*/
47+
export async function getRequesterInfo(
48+
thread: Thread,
49+
message: Message,
50+
platformIntegration: PlatformIntegration
51+
): Promise<RequesterInfo | undefined> {
52+
const platform = thread.id.split(':')[0];
53+
const displayName = message.author.fullName || message.author.userName || message.author.userId;
54+
55+
switch (platform) {
56+
case 'slack':
57+
return getSlackRequesterInfo(message, platformIntegration, displayName);
58+
case 'discord':
59+
return getDiscordRequesterInfo(message, displayName);
60+
default:
61+
return { displayName, platform };
62+
}
63+
}
64+
65+
async function getSlackRequesterInfo(
66+
message: Message,
67+
platformIntegration: PlatformIntegration,
68+
displayName: string
69+
): Promise<RequesterInfo> {
70+
const accessToken = getAccessTokenFromInstallation(platformIntegration);
71+
if (!accessToken) {
72+
return { displayName, platform: 'slack' };
73+
}
74+
75+
const raw = (message as Message<SlackEvent>).raw;
76+
const channelId =
77+
typeof raw === 'object' && raw !== null && 'channel' in raw
78+
? (raw as { channel?: string }).channel
79+
: undefined;
80+
const messageTs = message.id; // chat SDK uses Slack ts as the message ID
81+
82+
if (!channelId || !messageTs) {
83+
return { displayName, platform: 'slack' };
84+
}
85+
86+
const slackClient = new WebClient(accessToken);
87+
const permalink = await getSlackMessagePermalink(slackClient, channelId, messageTs);
88+
89+
return { displayName, messageLink: permalink, platform: 'slack' };
90+
}
91+
92+
function getDiscordRequesterInfo(message: Message, displayName: string): RequesterInfo {
93+
const raw = message.raw as { guild_id?: string; channel_id?: string } | null;
94+
const guildId = raw?.guild_id;
95+
const channelId = raw?.channel_id;
96+
const messageId = message.id;
97+
98+
const messageLink =
99+
guildId && channelId && messageId
100+
? `https://discord.com/channels/${guildId}/${channelId}/${messageId}`
101+
: undefined;
102+
103+
return { displayName, messageLink, platform: 'discord' };
104+
}

src/lib/bot/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export async function processMessage({
2323
const result = await runBotAgent({
2424
thread,
2525
message,
26+
rawMessage: message,
2627
platformIntegration,
2728
user,
2829
botRequestId,

src/lib/bot/tools/spawn-cloud-agent-session.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,17 @@ export default async function spawnCloudAgentSession(
8282
authToken: string,
8383
ticketUserId: string,
8484
botRequestId: string | undefined,
85-
onSessionReady?: RunSessionInput['onSessionReady']
85+
onSessionReady?: RunSessionInput['onSessionReady'],
86+
options?: { prSignature?: string; chatPlatform?: string }
8687
): Promise<SpawnCloudAgentResult> {
87-
console.log('[SlackBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2));
88+
console.log('[KiloBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2));
8889

8990
// Build platform-specific prepareInput and initiateInput
9091
const kilocodeOrganizationId = platformIntegration.owned_by_organization_id || undefined;
9192
let prepareInput: PrepareSessionInput;
9293
let initiateInput: { githubToken?: string; kilocodeOrganizationId?: string };
9394
const mode: AgentMode = args.mode;
95+
const chatPlatform = options?.chatPlatform ?? 'slack';
9496
const callbackTarget =
9597
botRequestId && INTERNAL_API_SECRET
9698
? {
@@ -104,14 +106,19 @@ export default async function spawnCloudAgentSession(
104106
}
105107

106108
const isGitLab = !!args.gitlabProject;
107-
const prompt =
109+
let prompt =
108110
mode === 'code'
109111
? args.prompt +
110112
(isGitLab
111113
? '\n\nOpen a merge request with your changes and return the MR URL.'
112114
: '\n\nOpen a pull request with your changes and return the PR URL.')
113115
: args.prompt;
114116

117+
// Append PR/MR signature to the prompt if available
118+
if (options?.prSignature) {
119+
prompt += options.prSignature;
120+
}
121+
115122
if (args.gitlabProject) {
116123
// GitLab path: get token + instance URL, build clone URL, use gitUrl/gitToken
117124
const gitlabToken =
@@ -135,7 +142,7 @@ export default async function spawnCloudAgentSession(
135142

136143
const isSelfHosted = !/^https?:\/\/(www\.)?gitlab\.com(\/|$)/i.test(instanceUrl);
137144
console.log(
138-
'[SlackBot] GitLab session - project:',
145+
'[KiloBot] GitLab session - project:',
139146
args.gitlabProject,
140147
'instance:',
141148
isSelfHosted ? 'self-hosted' : 'gitlab.com'
@@ -149,7 +156,7 @@ export default async function spawnCloudAgentSession(
149156
gitToken: gitlabToken,
150157
platform: 'gitlab',
151158
kilocodeOrganizationId,
152-
createdOnPlatform: 'slack',
159+
createdOnPlatform: chatPlatform,
153160
callbackTarget,
154161
};
155162
initiateInput = { kilocodeOrganizationId };
@@ -174,7 +181,7 @@ export default async function spawnCloudAgentSession(
174181
model,
175182
githubToken,
176183
kilocodeOrganizationId,
177-
createdOnPlatform: 'slack',
184+
createdOnPlatform: chatPlatform,
178185
callbackTarget,
179186
};
180187
initiateInput = { githubToken, kilocodeOrganizationId };

0 commit comments

Comments
 (0)