diff --git a/packages/slack/src/message.test.ts b/packages/slack/src/message.test.ts index 04ad6f6..ab43dfd 100644 --- a/packages/slack/src/message.test.ts +++ b/packages/slack/src/message.test.ts @@ -4,101 +4,110 @@ import { formatMessage } from "./message"; describe("formatMessage", () => { describe("markdown to Slack formatting", () => { test("converts markdown links to Slack format", () => { - expect(formatMessage("[click here](https://example.com)")).toBe( + expect(formatMessage("[click here](https://example.com)").text).toBe( "" ); }); test("converts double-star bold to single-star bold", () => { - expect(formatMessage("**hello**")).toBe("*hello*"); + expect(formatMessage("**hello**").text).toBe("*hello*"); }); }); describe("user ID mention wrapping", () => { test("wraps @-prefixed Slack user IDs", () => { - expect(formatMessage("@U02UD2WE3HA")).toBe("<@U02UD2WE3HA>"); + expect(formatMessage("@U02UD2WE3HA").text).toBe("<@U02UD2WE3HA>"); }); test("wraps bare Slack user IDs", () => { - expect(formatMessage("user U02UD2WE3HA said")).toBe( + expect(formatMessage("user U02UD2WE3HA said").text).toBe( "user <@U02UD2WE3HA> said" ); }); test("wraps workspace IDs starting with W", () => { - expect(formatMessage("@W01AB2CD3EF")).toBe("<@W01AB2CD3EF>"); + expect(formatMessage("@W01AB2CD3EF").text).toBe("<@W01AB2CD3EF>"); }); test("does not double-wrap already bracketed IDs", () => { - expect(formatMessage("<@U02UD2WE3HA>")).toBe("<@U02UD2WE3HA>"); + expect(formatMessage("<@U02UD2WE3HA>").text).toBe("<@U02UD2WE3HA>"); }); test("removes brackets from non-ID @handles", () => { - expect(formatMessage("<@john.doe>")).toBe("@john.doe"); + expect(formatMessage("<@john.doe>").text).toBe("@john.doe"); }); }); describe("false-positive prevention", () => { test("does not match pure-alpha words like WORKSPACE", () => { const input = "CODER_WORKSPACE_IS_PREBUILD_CLAIM=true"; - expect(formatMessage(input)).toBe(input); + expect(formatMessage(input).text).toBe(input); }); test("does not match UPPERCASE without digits", () => { - expect(formatMessage("UNDEFINED")).toBe("UNDEFINED"); - expect(formatMessage("WORKSPACEID")).toBe("WORKSPACEID"); + expect(formatMessage("UNDEFINED").text).toBe("UNDEFINED"); + expect(formatMessage("WORKSPACEID").text).toBe("WORKSPACEID"); }); test("does not match short IDs", () => { - expect(formatMessage("U1234")).toBe("U1234"); + expect(formatMessage("U1234").text).toBe("U1234"); }); }); describe("code block preservation", () => { test("does not format IDs inside inline code", () => { const input = "`U02UD2WE3HA`"; - expect(formatMessage(input)).toBe(input); + expect(formatMessage(input).text).toBe(input); }); test("does not format IDs inside code blocks", () => { const input = "```\nU02UD2WE3HA\n```"; - expect(formatMessage(input)).toBe(input); + expect(formatMessage(input).text).toBe(input); }); test("formats IDs outside code but preserves code content", () => { const input = "user U02UD2WE3HA said `U02UD2WE3HA`"; - expect(formatMessage(input)).toBe( + expect(formatMessage(input).text).toBe( "user <@U02UD2WE3HA> said `U02UD2WE3HA`" ); }); test("preserves markdown links inside code blocks", () => { const input = "```\n[text](url)\n```"; - expect(formatMessage(input)).toBe(input); + expect(formatMessage(input).text).toBe(input); }); test("preserves bold syntax inside inline code", () => { const input = "`**not bold**`"; - expect(formatMessage(input)).toBe(input); + expect(formatMessage(input).text).toBe(input); }); test("handles multiple code blocks", () => { const input = "`U02UD2WE3HA` and U02UD2WE3HA and ```U02UD2WE3HA```"; - expect(formatMessage(input)).toBe( + expect(formatMessage(input).text).toBe( "`U02UD2WE3HA` and <@U02UD2WE3HA> and ```U02UD2WE3HA```" ); }); test("does not format IDs inside code blocks with more than 3 backticks", () => { const input = "````\nU02UD2WE3HA\n````"; - expect(formatMessage(input)).toBe(input); + expect(formatMessage(input).text).toBe(input); }); }); describe("truncation", () => { test("truncates text longer than 3000 characters", () => { const input = "a".repeat(4000); - expect(formatMessage(input).length).toBe(3000); + const result = formatMessage(input); + expect(result.text.length).toBe(3000); + expect(result.truncated).toBe(true); + expect(result.originalLength).toBe(4000); + }); + + test("does not mark short messages as truncated", () => { + const result = formatMessage("hello"); + expect(result.truncated).toBe(false); + expect(result.originalLength).toBe(5); }); }); }); diff --git a/packages/slack/src/message.ts b/packages/slack/src/message.ts index cce864f..ad4fa26 100644 --- a/packages/slack/src/message.ts +++ b/packages/slack/src/message.ts @@ -24,9 +24,15 @@ import type { KnownEventFromType } from "@slack/bolt"; * @param text - The text to format. * @returns The formatted text. */ -export function formatMessage(text: string): string { +export function formatMessage(text: string): { + text: string; + truncated: boolean; + originalLength: number; +} { const maxLength = 3000; - if (text.length > maxLength) { + const originalLength = text.length; + const truncated = text.length > maxLength; + if (truncated) { text = text.slice(0, maxLength); } @@ -71,7 +77,7 @@ export function formatMessage(text: string): string { text = text.replace(placeholder(i), preserved[i] ?? ""); } - return text; + return { text, truncated, originalLength }; } /** @@ -86,6 +92,7 @@ export const formattingRules = `FORMATTING RULES: - = links - tables must be in a code block - user mentions must be in the format <@user_id> (e.g. <@U01UBAM2C4D>) +- messages are limited to 3000 characters; if your response is longer, it will be truncated and you'll need to send follow-up messages NEVER USE: - Headings (#, ##, ###, etc.) diff --git a/packages/slack/src/tools.ts b/packages/slack/src/tools.ts index 28f6e43..e50b1a2 100644 --- a/packages/slack/src/tools.ts +++ b/packages/slack/src/tools.ts @@ -113,7 +113,7 @@ ${formattingRules}` throw new Error("ts is required! Messaging in channels is disabled."); } - const text = formatMessage(args.message); + const { text, truncated, originalLength } = formatMessage(args.message); const blocks: AnyBlock[] = [ { type: "section", @@ -158,6 +158,13 @@ ${formattingRules}` if (!res.ok) { throw new Error(`Failed to send message: ${res.error}`); } + if (truncated) { + return { + success: true, + truncated: true, + message: `Your message was truncated from ${originalLength} to 3000 characters due to Slack's message limit. The last 100 characters sent were: "${text.slice(-100)}". Send a follow-up message to continue where you left off.`, + }; + } return { success: true, };