diff --git a/src/lib/bot/agent-runner.ts b/src/lib/bot/agent-runner.ts index 1626d6734..b8b00a2a6 100644 --- a/src/lib/bot/agent-runner.ts +++ b/src/lib/bot/agent-runner.ts @@ -9,6 +9,7 @@ import { getConversationContext, formatConversationContextForPrompt, } from '@/lib/bot/conversation-context'; +import { buildPrSignature, getRequesterInfo } from '@/lib/bot/pr-signature'; import { updateBotRequest, linkBotRequestToSession } from '@/lib/bot/request-logging'; import spawnCloudAgentSession, { spawnCloudAgentInputSchema, @@ -35,7 +36,7 @@ import { ToolLoopAgent, generateText, stepCountIs, tool } from 'ai'; import type { StepResult, ToolSet } from 'ai'; import { Actions, Card, CardText, LinkButton, Section } from 'chat'; import { ThreadImpl } from 'chat'; -import type { Author, Thread } from 'chat'; +import type { Author, Message, Thread } from 'chat'; export type BotAgentContinuation = { finalText: string; @@ -47,6 +48,8 @@ export type BotAgentContinuation = { type RunBotAgentParams = { thread: Thread; message: BotAgentMessageLike; + /** Full chat Message for PR signature (has `raw` for platform-specific fields). */ + rawMessage?: Message; platformIntegration: PlatformIntegration; user: User; botRequestId: string | undefined; @@ -204,6 +207,24 @@ export async function runBotAgent(params: RunBotAgentParams): Promise = { + slack: { label: 'Kilo for Slack', url: 'https://kilo.ai/features/slack-integration' }, + discord: { label: 'Kilo for Discord', url: 'https://kilo.ai' }, +}; + +const DEFAULT_PLATFORM_LINK = { label: 'Kilo', url: 'https://kilo.ai' }; + +/** + * Build the PR signature instruction to append to the Cloud Agent prompt. + * Instructs the agent to include a "Built for …" line at the end of any + * PR/MR description it creates. + */ +export function buildPrSignature(requesterInfo: RequesterInfo): string { + const requesterPart = requesterInfo.messageLink + ? `[${requesterInfo.displayName}](${requesterInfo.messageLink})` + : requesterInfo.displayName; + + const { label, url } = PLATFORM_LINKS[requesterInfo.platform] ?? DEFAULT_PLATFORM_LINK; + + return ` + +--- +**PR Signature to include in the PR description:** +When you create a pull request or merge request, include the following signature at the end of the PR/MR description: + +Built for ${requesterPart} by [${label}](${url})`; +} + +/** + * Gather requester info (display name + message link) for the PR signature. + * Platform-specific: uses the Slack API for permalinks, constructs Discord + * links from IDs, and degrades gracefully for unknown platforms. + */ +export async function getRequesterInfo( + thread: Thread, + message: Message, + platformIntegration: PlatformIntegration +): Promise { + const platform = thread.id.split(':')[0]; + const displayName = message.author.fullName || message.author.userName || message.author.userId; + + switch (platform) { + case 'slack': + return getSlackRequesterInfo(message, platformIntegration, displayName); + case 'discord': + return getDiscordRequesterInfo(message, displayName); + default: + return { displayName, platform }; + } +} + +async function getSlackRequesterInfo( + message: Message, + platformIntegration: PlatformIntegration, + displayName: string +): Promise { + const accessToken = getAccessTokenFromInstallation(platformIntegration); + if (!accessToken) { + return { displayName, platform: 'slack' }; + } + + const raw = (message as Message).raw; + const channelId = + typeof raw === 'object' && raw !== null && 'channel' in raw + ? (raw as { channel?: string }).channel + : undefined; + const messageTs = message.id; // chat SDK uses Slack ts as the message ID + + if (!channelId || !messageTs) { + return { displayName, platform: 'slack' }; + } + + const slackClient = new WebClient(accessToken); + const permalink = await getSlackMessagePermalink(slackClient, channelId, messageTs); + + return { displayName, messageLink: permalink, platform: 'slack' }; +} + +function getDiscordRequesterInfo(message: Message, displayName: string): RequesterInfo { + const raw = message.raw as { guild_id?: string; channel_id?: string } | null; + const guildId = raw?.guild_id; + const channelId = raw?.channel_id; + const messageId = message.id; + + const messageLink = + guildId && channelId && messageId + ? `https://discord.com/channels/${guildId}/${channelId}/${messageId}` + : undefined; + + return { displayName, messageLink, platform: 'discord' }; +} diff --git a/src/lib/bot/run.ts b/src/lib/bot/run.ts index c1164e04f..2c3873764 100644 --- a/src/lib/bot/run.ts +++ b/src/lib/bot/run.ts @@ -23,6 +23,7 @@ export async function processMessage({ const result = await runBotAgent({ thread, message, + rawMessage: message, platformIntegration, user, botRequestId, diff --git a/src/lib/bot/tools/spawn-cloud-agent-session.ts b/src/lib/bot/tools/spawn-cloud-agent-session.ts index 5de03bf34..6cf667db4 100644 --- a/src/lib/bot/tools/spawn-cloud-agent-session.ts +++ b/src/lib/bot/tools/spawn-cloud-agent-session.ts @@ -82,15 +82,17 @@ export default async function spawnCloudAgentSession( authToken: string, ticketUserId: string, botRequestId: string | undefined, - onSessionReady?: RunSessionInput['onSessionReady'] + onSessionReady?: RunSessionInput['onSessionReady'], + options?: { prSignature?: string; chatPlatform?: string } ): Promise { - console.log('[SlackBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2)); + console.log('[KiloBot] spawnCloudAgentSession called with args:', JSON.stringify(args, null, 2)); // Build platform-specific prepareInput and initiateInput const kilocodeOrganizationId = platformIntegration.owned_by_organization_id || undefined; let prepareInput: PrepareSessionInput; let initiateInput: { githubToken?: string; kilocodeOrganizationId?: string }; const mode: AgentMode = args.mode; + const chatPlatform = options?.chatPlatform ?? 'slack'; const callbackTarget = botRequestId && INTERNAL_API_SECRET ? { @@ -104,7 +106,7 @@ export default async function spawnCloudAgentSession( } const isGitLab = !!args.gitlabProject; - const prompt = + let prompt = mode === 'code' ? args.prompt + (isGitLab @@ -112,6 +114,11 @@ export default async function spawnCloudAgentSession( : '\n\nOpen a pull request with your changes and return the PR URL.') : args.prompt; + // Append PR/MR signature to the prompt if available + if (options?.prSignature) { + prompt += options.prSignature; + } + if (args.gitlabProject) { // GitLab path: get token + instance URL, build clone URL, use gitUrl/gitToken const gitlabToken = @@ -135,7 +142,7 @@ export default async function spawnCloudAgentSession( const isSelfHosted = !/^https?:\/\/(www\.)?gitlab\.com(\/|$)/i.test(instanceUrl); console.log( - '[SlackBot] GitLab session - project:', + '[KiloBot] GitLab session - project:', args.gitlabProject, 'instance:', isSelfHosted ? 'self-hosted' : 'gitlab.com' @@ -149,7 +156,7 @@ export default async function spawnCloudAgentSession( gitToken: gitlabToken, platform: 'gitlab', kilocodeOrganizationId, - createdOnPlatform: 'slack', + createdOnPlatform: chatPlatform, callbackTarget, }; initiateInput = { kilocodeOrganizationId }; @@ -174,7 +181,7 @@ export default async function spawnCloudAgentSession( model, githubToken, kilocodeOrganizationId, - createdOnPlatform: 'slack', + createdOnPlatform: chatPlatform, callbackTarget, }; initiateInput = { githubToken, kilocodeOrganizationId };