Skip to content
Closed
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
3 changes: 3 additions & 0 deletions packages/junior/src/chat/providers/slack/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,12 @@ async function postSlackMessageBestEffort(
text: string,
conversationId?: string,
replyAttribution?: ReplyAttribution,
bodyFormat?: import("@/chat/slack/footer").SlackReplyBodyFormat,
): Promise<void> {
try {
if (conversationId) {
await sendSlackReply({
bodyFormat,
channelId,
conversationId,
replyAttribution,
Expand Down Expand Up @@ -904,6 +906,7 @@ async function resumeSlackTurnInContext(
),
runArgs.conversationId,
runArgs.run?.dispatch?.replyAttribution,
"mrkdwn",
);
}
return true;
Expand Down
1 change: 1 addition & 0 deletions packages/junior/src/chat/providers/slack/turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ export function createSlackTurn(deps: SlackTurnDeps) {
await beforeFirstResponsePost();
if (channelId && threadTs) {
await sendSlackReply({
bodyFormat: "mrkdwn",
channelId,
conversationId,
replyAttribution: options.execution?.dispatch?.replyAttribution,
Expand Down
38 changes: 31 additions & 7 deletions packages/junior/src/chat/slack/footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ export interface SlackReplyFooter {
items: SlackReplyFooterItem[];
}

/**
* Controls which Slack block type is used for the reply body.
*
* - `"commonmark"` (default): wraps the text in a `{ type: "markdown" }` block,
* which Slack renders from standard Markdown (bold, links, tables, headers).
* Use this for normal agent replies where `normalizeSlackReplyMarkdown` has
* already formatted the output for CommonMark delivery.
*
* - `"mrkdwn"`: wraps the text in a `{ type: "section", text: { type: "mrkdwn" } }`
* block. Use this when the text is already pre-formatted as Slack mrkdwn
* (e.g. `<@user>` mentions, `<url>` angle-bracket links, single-`*` emphasis)
* and must not be passed through the markdown-to-rich_text converter, which
* can mis-tag mrkdwn tokens and trigger `invalid_blocks`.
*/
export type SlackReplyBodyFormat = "commonmark" | "mrkdwn";

/** Render compact reply attribution for the Slack footer. */
export function formatReplyAttribution(attribution: ReplyAttribution): string {
return attribution.detail
Expand Down Expand Up @@ -90,21 +106,29 @@ export function buildSlackReplyFooter(args: {
: undefined;
}

/** Build Slack blocks for a reply chunk using the Slack-flavored markdown block for the body. */
/**
* Build Slack blocks for a reply chunk.
*
* The body block format is controlled by `bodyFormat`:
* - `"commonmark"` (default): uses `{ type: "markdown" }` for CommonMark rendering.
* - `"mrkdwn"`: uses `{ type: "section", text: { type: "mrkdwn" } }` for
* pre-formatted mrkdwn text (e.g. auth-pause notices with `<@user>` mentions).
*/
export function buildSlackReplyBlocks(
text: string,
footer: SlackReplyFooter | undefined,
bodyFormat: SlackReplyBodyFormat = "commonmark",
): SlackMessageBlock[] | undefined {
if (!text.trim()) {
return undefined;
}

const blocks: SlackMessageBlock[] = [
{
type: "markdown",
text,
},
];
const bodyBlock: SlackMessageBlock =
bodyFormat === "mrkdwn"
? { type: "section", text: { type: "mrkdwn", text } }
: { type: "markdown", text };

const blocks: SlackMessageBlock[] = [bodyBlock];

if (footer && (footer.attribution || footer.items.length > 0)) {
const attributionElements: SlackPlainTextObject[] = footer.attribution
Expand Down
3 changes: 3 additions & 0 deletions packages/junior/src/chat/slack/reply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
buildSlackReplyBlocks,
buildSlackReplyFooter,
formatReplyAttribution,
type SlackReplyBodyFormat,
} from "@/chat/slack/footer";
import { escapeSlackMrkdwnText } from "@/chat/slack/mrkdwn";
import { postSlackMessage } from "@/chat/slack/outbound";
Expand All @@ -20,6 +21,7 @@ import { splitSlackReplyText } from "@/chat/slack/output";
* context, and posts through the shared Slack outbound boundary.
*/
export async function sendSlackReply(args: {
bodyFormat?: SlackReplyBodyFormat;
channelId: string;
conversationId: string;
replyAttribution?: ReplyAttribution;
Expand All @@ -41,6 +43,7 @@ export async function sendSlackReply(args: {
const blocks = buildSlackReplyBlocks(
text,
isFinalChunk ? footer : undefined,
args.bodyFormat,
);
const fallbackText =
isFinalChunk && args.replyAttribution
Expand Down
11 changes: 11 additions & 0 deletions packages/junior/tests/unit/slack/footer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@ describe("buildSlackReplyBlocks", () => {
]);
});

it("renders a mrkdwn section block for pre-formatted mrkdwn text (auth-pause path)", () => {
const text =
"<@U123> I need access to GitHub to continue.\n\n*Why:* check out <https://github.com/foo/bar> and proceed\n\nI sent you a link.";
expect(buildSlackReplyBlocks(text, undefined, "mrkdwn")).toEqual([
{
type: "section",
text: { type: "mrkdwn", text },
},
]);
});

it("does not emit blocks when the reply has no visible text", () => {
const footer = buildSlackReplyFooter({
conversationId: "slack:C123:1700000000.000100",
Expand Down
Loading