diff --git a/src/app/slack/webhook/route.ts b/src/app/slack/webhook/route.ts index 0bb05087d..74f223986 100644 --- a/src/app/slack/webhook/route.ts +++ b/src/app/slack/webhook/route.ts @@ -36,6 +36,44 @@ export const maxDuration = 800; const PROCESSING_REACTION = 'hourglass_flowing_sand'; const COMPLETE_REACTION = 'white_check_mark'; +/** + * Split a mrkdwn string into Slack section blocks, each within the character limit. + * Splits on newline boundaries to avoid breaking mid-line/mid-URL. + */ +function toSectionBlocks( + text: string, + charLimit: number +): Array<{ type: 'section'; text: { type: 'mrkdwn'; text: string } }> { + if (text.length <= charLimit) { + return [{ type: 'section', text: { type: 'mrkdwn', text } }]; + } + + const blocks: Array<{ type: 'section'; text: { type: 'mrkdwn'; text: string } }> = []; + let remaining = text; + + while (remaining.length > 0) { + if (remaining.length <= charLimit) { + blocks.push({ type: 'section', text: { type: 'mrkdwn', text: remaining } }); + break; + } + + // Find the last newline within the limit to avoid splitting mid-line + let splitAt = remaining.lastIndexOf('\n', charLimit); + if (splitAt <= 0) { + // No newline found; hard-split at the limit as a fallback + splitAt = charLimit; + } + + blocks.push({ + type: 'section', + text: { type: 'mrkdwn', text: remaining.slice(0, splitAt) }, + }); + remaining = remaining.slice(splitAt + 1); + } + + return blocks; +} + /** * Post an ephemeral message to the user with a button to view the cloud agent session */ @@ -215,20 +253,19 @@ async function processSlackMessage(event: AppMentionEvent | GenericMessageEvent, const responseWithDevInfo = result.response + getDevUserSuffix(); const slackFormattedMessage = markdownToSlackMrkdwn(responseWithDevInfo); + // Slack section blocks have a 3000-character limit for the text field. + // When the response exceeds this (common for cloud agent results that include + // PR/MR URLs near the end), the block gets silently truncated, cutting off the URL. + // For long messages, split into multiple section blocks to preserve the full content. + const SLACK_SECTION_CHAR_LIMIT = 3000; + const blocks = toSectionBlocks(slackFormattedMessage, SLACK_SECTION_CHAR_LIMIT); + // Post the response in the thread const slackResponse = await postSlackMessageByAccessToken(accessToken, { channel, text: slackFormattedMessage, thread_ts: replyThreadTs, - blocks: [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: slackFormattedMessage, - }, - }, - ], + blocks, }); console.log( diff --git a/src/lib/bot/run.ts b/src/lib/bot/run.ts index d45854af5..b7d200c13 100644 --- a/src/lib/bot/run.ts +++ b/src/lib/bot/run.ts @@ -90,6 +90,9 @@ ${formatGitLabRepositoriesForPrompt(gitlabContext)} 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. +## Sharing results +- After calling a tool in "code" mode, check the result for a PR/MR URL and share it with the user — this is the most important output. Always include the full PR/MR URL in your response so the user can click it directly. + ## Accuracy & safety - Don't claim you ran tools, changed code, or created a PR/MR unless the tool results confirm it. - Don't fabricate links (including PR/MR URLs). diff --git a/src/lib/slack-bot.ts b/src/lib/slack-bot.ts index 841adfea6..f8ad9f0aa 100644 --- a/src/lib/slack-bot.ts +++ b/src/lib/slack-bot.ts @@ -113,6 +113,9 @@ Your prompt to the agent should usually include: - any constraints (keep changes minimal, follow existing patterns, etc.) - a request to open a PR (GitHub) or MR (GitLab) and return the URL +### After the tool returns +When the tool returns, check the result for a PR/MR URL and share it with the user — this is the most important output. Always include the full PR/MR URL in your response so the user can click it directly. + ## Accuracy & safety - Don't claim you ran tools, changed code, or created a PR/MR unless the tool results confirm it. - Don't fabricate links (including PR/MR URLs). @@ -126,7 +129,7 @@ const SPAWN_CLOUD_AGENT_TOOL: OpenAI.Chat.Completions.ChatCompletionTool = { function: { name: 'spawn_cloud_agent', description: - 'Spawn a Cloud Agent session to perform coding tasks on a GitHub repository or GitLab project. Provide exactly one of githubRepo or gitlabProject.', + 'Spawn a Cloud Agent session to perform coding tasks on a GitHub repository or GitLab project. Provide exactly one of githubRepo or gitlabProject. After the tool returns, if mode was "code", check the result for a PR/MR URL and share it with the user — this is the most important output.', parameters: { type: 'object', properties: { @@ -281,10 +284,22 @@ async function spawnCloudAgentSession( kilocodeOrganizationId = owner.id; } + const mode = args.mode || 'code'; + const isGitLab = !!args.gitlabProject; + + // For "code" mode, explicitly instruct the agent to open a PR/MR and return the URL + const promptWithPrInstruction = + mode === 'code' + ? args.prompt + + (isGitLab + ? '\n\nOpen a merge request with your changes and return the MR URL.' + : '\n\nOpen a pull request with your changes and return the PR URL.') + : args.prompt; + // Append PR/MR signature to the prompt if we have requester info const promptWithSignature = requesterInfo - ? args.prompt + buildPrSignature(requesterInfo) - : args.prompt; + ? promptWithPrInstruction + buildPrSignature(requesterInfo) + : promptWithPrInstruction; // Build platform-specific prepareInput and initiateInput let prepareInput: PrepareSessionInput;