Skip to content
Open
Show file tree
Hide file tree
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
55 changes: 46 additions & 9 deletions src/app/slack/webhook/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Hard-split path drops one character

When no newline exists within the limit, splitAt is set to charLimit, but this line still advances by + 1. That skips the character at the split boundary, so long single-line responses lose one character per block and can still break PR/MR URLs or code blocks.

Suggested change
remaining = remaining.slice(splitAt + 1);
remaining = remaining.slice(remaining[splitAt] === '\n' ? splitAt + 1 : splitAt);

}

return blocks;
}

/**
* Post an ephemeral message to the user with a button to view the cloud agent session
*/
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions src/lib/bot/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
21 changes: 18 additions & 3 deletions src/lib/slack-bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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: {
Expand Down Expand Up @@ -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;
Expand Down
Loading