mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 13:54:47 +00:00
refactor: deprecate legacy interactive reply APIs
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
ec9fa02c3af9c210f7dbf6157d2f18e5c7171c29e6ae13b4f539aefcb25d178a plugin-sdk-api-baseline.json
|
||||
2ee394b924edb9843987710a65f9d45523efadd7e7940e01c88ea39dcdcdad7c plugin-sdk-api-baseline.jsonl
|
||||
df6c2799805dc3c57924dbb1632d11e7ed08ef4d7759f535998b170f1a10a638 plugin-sdk-api-baseline.json
|
||||
e3526669b79e5eaa3b92e03bece552402209d3cf5b35343c33b62299f71b2efc plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -1245,6 +1245,16 @@ back through the existing Slack interaction event path. Keep them for old
|
||||
prompts and Slack-specific escape hatches; use shared presentation for new
|
||||
portable controls.
|
||||
|
||||
The directive compiler APIs are also deprecated for new producer code:
|
||||
|
||||
- `compileSlackInteractiveReplies(...)`
|
||||
- `parseSlackOptionsLine(...)`
|
||||
- `isSlackInteractiveRepliesEnabled(...)`
|
||||
- `buildSlackInteractiveBlocks(...)`
|
||||
|
||||
Use `presentation` payloads and `buildSlackPresentationBlocks(...)` for new
|
||||
Slack-rendered controls.
|
||||
|
||||
Notes:
|
||||
|
||||
- This is Slack-specific legacy UI. Other channels do not translate Slack Block
|
||||
|
||||
@@ -391,12 +391,33 @@ New code should accept or produce `MessagePresentation` directly. Existing
|
||||
`interactive` payloads are a deprecated subset of `presentation`; runtime
|
||||
support remains for older producers.
|
||||
|
||||
`presentationToInteractiveReply(...)` preserves visible presentation text by
|
||||
mapping the title, text, context, buttons, and selects into the older
|
||||
`InteractiveReply` shape. Component renderers that already draw title, text,
|
||||
context, and divider blocks natively should use
|
||||
`presentationToInteractiveControlsReply(...)` instead, then append only the
|
||||
button and select controls.
|
||||
The legacy `InteractiveReply*` types and conversion helpers are marked
|
||||
`@deprecated` in the SDK:
|
||||
|
||||
- `InteractiveReply`, `InteractiveReplyBlock`, `InteractiveReplyButton`,
|
||||
`InteractiveReplyOption`, `InteractiveReplySelectBlock`, and
|
||||
`InteractiveReplyTextBlock`
|
||||
- `normalizeInteractiveReply(...)`
|
||||
- `hasInteractiveReplyBlocks(...)`
|
||||
- `interactiveReplyToPresentation(...)`
|
||||
- `presentationToInteractiveReply(...)`
|
||||
- `presentationToInteractiveControlsReply(...)`
|
||||
- `resolveInteractiveTextFallback(...)`
|
||||
- `reduceInteractiveReply(...)`
|
||||
|
||||
`presentationToInteractiveReply(...)` and
|
||||
`presentationToInteractiveControlsReply(...)` remain available as renderer
|
||||
bridges for legacy channel implementations. New producer code should not call
|
||||
them; send `presentation` and let core/channel adaptation handle rendering.
|
||||
|
||||
Approval helpers also have presentation-first replacements:
|
||||
|
||||
- use `buildApprovalPresentationFromActionDescriptors(...)` instead of
|
||||
`buildApprovalInteractiveReplyFromActionDescriptors(...)`
|
||||
- use `buildApprovalPresentation(...)` instead of
|
||||
`buildApprovalInteractiveReply(...)`
|
||||
- use `buildExecApprovalPresentation(...)` instead of
|
||||
`buildExecApprovalInteractiveReply(...)`
|
||||
|
||||
`renderMessagePresentationFallbackText(...)` returns an empty string for
|
||||
presentation blocks that have no text fallback, such as a divider-only
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
|
||||
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
import { buildApprovalInteractiveReplyFromActionDescriptors } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import { buildApprovalPresentationFromActionDescriptors } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import type { ExecApprovalRequest } from "openclaw/plugin-sdk/approval-runtime";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { logError } from "openclaw/plugin-sdk/logging-core";
|
||||
@@ -142,7 +142,7 @@ function buildSlackPendingApprovalBlocks(view: ExecApprovalPendingView): SlackBl
|
||||
const interactiveBlocks =
|
||||
resolveSlackReplyBlocks({
|
||||
text: "",
|
||||
interactive: buildApprovalInteractiveReplyFromActionDescriptors(view.actions),
|
||||
presentation: buildApprovalPresentationFromActionDescriptors(view.actions),
|
||||
}) ?? [];
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -85,6 +85,9 @@ export function resolveSlackInteractiveBlockOffsets(
|
||||
return { buttonIndexOffset, selectIndexOffset };
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use buildSlackPresentationBlocks with MessagePresentation.
|
||||
*/
|
||||
export function buildSlackInteractiveBlocks(
|
||||
interactive?: InteractiveReply,
|
||||
options: SlackInteractiveBlockRenderOptions = {},
|
||||
|
||||
@@ -162,6 +162,9 @@ function resolveInteractiveRepliesFromCapabilities(capabilities: unknown): boole
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Only needed for legacy Slack reply directives. New producers should emit presentation payloads.
|
||||
*/
|
||||
export function isSlackInteractiveRepliesEnabled(params: {
|
||||
cfg: OpenClawConfig;
|
||||
accountId?: string | null;
|
||||
@@ -173,6 +176,9 @@ export function isSlackInteractiveRepliesEnabled(params: {
|
||||
return resolveInteractiveRepliesFromCapabilities(account.config.capabilities);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Slack reply directives are legacy. New producers should emit presentation payloads.
|
||||
*/
|
||||
export function compileSlackInteractiveReplies(payload: ReplyPayload): ReplyPayload {
|
||||
const text = payload.text;
|
||||
if (!text) {
|
||||
@@ -230,6 +236,9 @@ export function compileSlackInteractiveReplies(payload: ReplyPayload): ReplyPayl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Legacy Slack directive fallback. New producers should emit presentation payloads.
|
||||
*/
|
||||
export function parseSlackOptionsLine(payload: ReplyPayload): ReplyPayload {
|
||||
const text = payload.text;
|
||||
if (!text || payload.interactive?.blocks?.length || hasSlackBlocks(payload)) {
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { parseSlackBlocksInput, SLACK_MAX_BLOCKS } from "./blocks-input.js";
|
||||
import { buildSlackInteractiveBlocks, type SlackBlock } from "./blocks-render.js";
|
||||
import {
|
||||
buildSlackInteractiveBlocks,
|
||||
buildSlackPresentationBlocks,
|
||||
resolveSlackInteractiveBlockOffsets,
|
||||
type SlackBlock,
|
||||
} from "./blocks-render.js";
|
||||
|
||||
export function resolveSlackReplyBlocks(payload: ReplyPayload): SlackBlock[] | undefined {
|
||||
const slackData = payload.channelData?.slack;
|
||||
const interactiveBlocks = buildSlackInteractiveBlocks(payload.interactive);
|
||||
let channelBlocks: SlackBlock[] = [];
|
||||
if (slackData && typeof slackData === "object" && !Array.isArray(slackData)) {
|
||||
channelBlocks =
|
||||
(parseSlackBlocksInput((slackData as { blocks?: unknown }).blocks) as SlackBlock[]) ?? [];
|
||||
}
|
||||
const blocks = [...channelBlocks, ...interactiveBlocks];
|
||||
const presentationBlocks = buildSlackPresentationBlocks(
|
||||
payload.presentation,
|
||||
resolveSlackInteractiveBlockOffsets(channelBlocks),
|
||||
);
|
||||
const interactiveBlocks = buildSlackInteractiveBlocks(
|
||||
payload.interactive,
|
||||
resolveSlackInteractiveBlockOffsets([...channelBlocks, ...presentationBlocks]),
|
||||
);
|
||||
const blocks = [...channelBlocks, ...presentationBlocks, ...interactiveBlocks];
|
||||
if (blocks.length > SLACK_MAX_BLOCKS) {
|
||||
throw new Error(
|
||||
`Slack blocks cannot exceed ${SLACK_MAX_BLOCKS} items after interactive render`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildSlackInteractiveBlocks } from "./blocks-render.js";
|
||||
import { buildSlackInteractiveBlocks, buildSlackPresentationBlocks } from "./blocks-render.js";
|
||||
import { resolveSlackReplyBlocks } from "./reply-blocks.js";
|
||||
|
||||
describe("buildSlackInteractiveBlocks", () => {
|
||||
it("renders shared interactive blocks in authored order", () => {
|
||||
@@ -306,3 +307,87 @@ describe("buildSlackInteractiveBlocks", () => {
|
||||
expect(buttonBlock.elements?.[3]).not.toHaveProperty("style");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildSlackPresentationBlocks", () => {
|
||||
it("renders presentation controls without requiring legacy interactive payloads", () => {
|
||||
const blocks = buildSlackPresentationBlocks({
|
||||
blocks: [
|
||||
{ type: "text", text: "Pick" },
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Approve", value: "approve", style: "success" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(blocks).toEqual([
|
||||
{
|
||||
type: "section",
|
||||
text: { type: "mrkdwn", text: "Pick" },
|
||||
},
|
||||
{
|
||||
type: "actions",
|
||||
block_id: "openclaw_reply_buttons_1",
|
||||
elements: [
|
||||
{
|
||||
type: "button",
|
||||
action_id: "openclaw:reply_button:1:1",
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: "Approve",
|
||||
emoji: true,
|
||||
},
|
||||
value: "approve",
|
||||
style: "primary",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSlackReplyBlocks", () => {
|
||||
it("offsets legacy interactive blocks after channel and presentation controls", () => {
|
||||
const blocks = resolveSlackReplyBlocks({
|
||||
channelData: {
|
||||
slack: {
|
||||
blocks: [
|
||||
{
|
||||
type: "actions",
|
||||
block_id: "openclaw_reply_buttons_1",
|
||||
elements: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
presentation: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Stage", value: "stage" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
interactive: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Approve", value: "approve" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const presentationButtonBlock = blocks?.[1] as
|
||||
| { elements?: Array<{ action_id?: string }> }
|
||||
| undefined;
|
||||
const legacyButtonBlock = blocks?.[2] as
|
||||
| { elements?: Array<{ action_id?: string }> }
|
||||
| undefined;
|
||||
expect(blocks?.[0]?.block_id).toBe("openclaw_reply_buttons_1");
|
||||
expect(blocks?.[1]?.block_id).toBe("openclaw_reply_buttons_2");
|
||||
expect(presentationButtonBlock?.elements?.[0]?.action_id).toBe("openclaw:reply_button:2:1");
|
||||
expect(blocks?.[2]?.block_id).toBe("openclaw_reply_buttons_3");
|
||||
expect(legacyButtonBlock?.elements?.[0]?.action_id).toBe("openclaw:reply_button:3:1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
renderMessagePresentationFallbackText,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import type { MessagePresentation } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
@@ -138,7 +137,8 @@ function resolveTelegramButtonsFromParams(
|
||||
presentation = normalizeMessagePresentation(params.presentation),
|
||||
) {
|
||||
return resolveTelegramInlineButtons({
|
||||
interactive: presentation ? presentationToInteractiveReply(presentation) : params.interactive,
|
||||
presentation,
|
||||
interactive: params.interactive,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/a
|
||||
import { buildChannelApprovalNativeTargetKey } from "openclaw/plugin-sdk/approval-native-runtime";
|
||||
import { buildPluginApprovalPendingReplyPayload } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import {
|
||||
buildApprovalInteractiveReplyFromActionDescriptors,
|
||||
buildApprovalPresentationFromActionDescriptors,
|
||||
buildExecApprovalPendingReplyPayload,
|
||||
} from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import type { ExecApprovalPendingReplyParams } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
@@ -92,7 +92,7 @@ function buildPendingPayload(params: {
|
||||
return {
|
||||
text: payload.text ?? "",
|
||||
buttons: resolveTelegramInlineButtons({
|
||||
interactive: buildApprovalInteractiveReplyFromActionDescriptors(params.view.actions),
|
||||
presentation: buildApprovalPresentationFromActionDescriptors(params.view.actions),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -36,10 +36,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/config-contracts";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { runInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { normalizeMessagePresentation } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import {
|
||||
createOutboundPayloadPlan,
|
||||
projectOutboundPayloadPlanForDelivery,
|
||||
@@ -145,12 +142,10 @@ function resolvePayloadTelegramInlineButtons(
|
||||
| { buttons?: TelegramInlineButtons }
|
||||
| undefined;
|
||||
const presentation = normalizeMessagePresentation(payload.presentation);
|
||||
const interactive =
|
||||
payload.interactive ??
|
||||
(presentation ? presentationToInteractiveReply(presentation) : undefined);
|
||||
return resolveTelegramInlineButtons({
|
||||
buttons: telegramData?.buttons,
|
||||
interactive,
|
||||
presentation,
|
||||
interactive: payload.interactive,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -543,6 +543,37 @@ describe("registerTelegramNativeCommands", () => {
|
||||
expect(replyAt(firstDeliverRepliesParams()).mediaUrl).toBe("/tmp/render.png");
|
||||
});
|
||||
|
||||
it("falls back to a normal reply when a progress result has presentation controls", async () => {
|
||||
const presentation = {
|
||||
blocks: [
|
||||
{
|
||||
kind: "actions",
|
||||
buttons: [{ label: "Approve", action: { type: "command", value: "/approve yes" } }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { handler, sendMessage, deleteMessage } = registerPlugCommand({
|
||||
args: "now",
|
||||
command: {
|
||||
nativeProgressMessages: { telegram: "Working on it..." },
|
||||
},
|
||||
result: {
|
||||
text: "Approval required",
|
||||
presentation,
|
||||
},
|
||||
});
|
||||
|
||||
await handler(createPrivateCommandContext({ match: "now" }));
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledWith(100, "Working on it...", undefined);
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
expect(deleteMessage).toHaveBeenCalledWith(100, 999);
|
||||
expect(replyAt(firstDeliverRepliesParams())).toMatchObject({
|
||||
text: "Approval required",
|
||||
presentation,
|
||||
});
|
||||
});
|
||||
|
||||
it("cleans up the progress placeholder before falling back after an edit failure", async () => {
|
||||
const { handler, sendMessage, deleteMessage } = registerPlugCommand({
|
||||
args: "now",
|
||||
|
||||
@@ -333,6 +333,7 @@ function isEditableTelegramProgressResult(result: TelegramNativeReplyPayload): b
|
||||
result.text.trim() &&
|
||||
!result.mediaUrl &&
|
||||
(!result.mediaUrls || result.mediaUrls.length === 0) &&
|
||||
!result.presentation &&
|
||||
!result.interactive &&
|
||||
!result.btw &&
|
||||
telegramData?.pin !== true,
|
||||
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
toPluginMessageSentEvent,
|
||||
} from "openclaw/plugin-sdk/hook-runtime";
|
||||
import type { ReplyPayloadDelivery } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { normalizeMessagePresentation } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import {
|
||||
buildOutboundMediaLoadOptions,
|
||||
isGifMedia,
|
||||
@@ -773,9 +770,7 @@ export async function deliverReplies(params: {
|
||||
: [];
|
||||
const hasMedia = mediaList.length > 0;
|
||||
const presentation = normalizeMessagePresentation(reply?.presentation);
|
||||
const interactive =
|
||||
reply?.interactive ??
|
||||
(presentation ? presentationToInteractiveReply(presentation) : undefined);
|
||||
const interactive = reply?.interactive;
|
||||
const resolvedReplyText =
|
||||
resolveTelegramInteractiveTextFallback({
|
||||
text: reply?.text,
|
||||
@@ -850,6 +845,7 @@ export async function deliverReplies(params: {
|
||||
const replyMarkup = buildInlineKeyboard(
|
||||
resolveTelegramInlineButtons({
|
||||
buttons: telegramData?.buttons,
|
||||
presentation,
|
||||
interactive,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -81,5 +81,28 @@ export function describeTelegramInteractiveButtonBehavior(): void {
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers legacy interactive buttons over generic presentation buttons", () => {
|
||||
expect(
|
||||
resolveTelegramInlineButtons({
|
||||
presentation: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Generic", value: "generic" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
interactive: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Legacy", value: "legacy" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual([[{ text: "Legacy", callback_data: "legacy", style: undefined }]]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildTelegramInteractiveButtons } from "./button-types.js";
|
||||
import {
|
||||
buildTelegramInteractiveButtons,
|
||||
buildTelegramPresentationButtons,
|
||||
} from "./button-types.js";
|
||||
import { describeTelegramInteractiveButtonBehavior } from "./button-types.test-helpers.js";
|
||||
|
||||
describeTelegramInteractiveButtonBehavior();
|
||||
@@ -21,3 +24,27 @@ describe("buildTelegramInteractiveButtons callback limits", () => {
|
||||
).toEqual([[{ text: "Keep", callback_data: "ok", style: undefined }]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildTelegramPresentationButtons", () => {
|
||||
it("builds inline buttons from presentation blocks", () => {
|
||||
expect(
|
||||
buildTelegramPresentationButtons({
|
||||
blocks: [
|
||||
{ type: "text", text: "Choose" },
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Approve", value: "/approve req-1 allow-once", style: "success" }],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
[
|
||||
{
|
||||
text: "Approve",
|
||||
callback_data: "/approve req-1 allow-once",
|
||||
style: "success",
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { reduceInteractiveReply } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import {
|
||||
isMessagePresentationInteractiveBlock,
|
||||
normalizeMessagePresentation,
|
||||
normalizeInteractiveReply,
|
||||
type InteractiveReply,
|
||||
type InteractiveReplyButton,
|
||||
type MessagePresentation,
|
||||
type MessagePresentationButton,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { sanitizeTelegramCallbackData } from "./approval-callback-data.js";
|
||||
|
||||
@@ -21,12 +24,14 @@ export type TelegramInlineButtons = ReadonlyArray<ReadonlyArray<TelegramInlineBu
|
||||
const TELEGRAM_INTERACTIVE_ROW_SIZE = 3;
|
||||
|
||||
function toTelegramButtonStyle(
|
||||
style?: InteractiveReplyButton["style"],
|
||||
style?: MessagePresentationButton["style"],
|
||||
): TelegramInlineButton["style"] {
|
||||
return style === "danger" || style === "success" || style === "primary" ? style : undefined;
|
||||
}
|
||||
|
||||
function toTelegramInlineButton(button: InteractiveReplyButton): TelegramInlineButton | undefined {
|
||||
function toTelegramInlineButton(
|
||||
button: MessagePresentationButton,
|
||||
): TelegramInlineButton | undefined {
|
||||
const style = toTelegramButtonStyle(button.style);
|
||||
if (button.url) {
|
||||
return {
|
||||
@@ -54,7 +59,7 @@ function toTelegramInlineButton(button: InteractiveReplyButton): TelegramInlineB
|
||||
}
|
||||
|
||||
function chunkInteractiveButtons(
|
||||
buttons: readonly InteractiveReplyButton[],
|
||||
buttons: readonly MessagePresentationButton[],
|
||||
rows: TelegramInlineButton[][],
|
||||
) {
|
||||
for (let i = 0; i < buttons.length; i += TELEGRAM_INTERACTIVE_ROW_SIZE) {
|
||||
@@ -94,11 +99,37 @@ export function buildTelegramInteractiveButtons(
|
||||
return rows.length > 0 ? rows : undefined;
|
||||
}
|
||||
|
||||
export function buildTelegramPresentationButtons(
|
||||
presentation?: MessagePresentation,
|
||||
): TelegramInlineButtons | undefined {
|
||||
const rows: TelegramInlineButton[][] = [];
|
||||
for (const block of presentation?.blocks ?? []) {
|
||||
if (!isMessagePresentationInteractiveBlock(block)) {
|
||||
continue;
|
||||
}
|
||||
if (block.type === "buttons") {
|
||||
chunkInteractiveButtons(block.buttons, rows);
|
||||
continue;
|
||||
}
|
||||
chunkInteractiveButtons(
|
||||
block.options.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value,
|
||||
})),
|
||||
rows,
|
||||
);
|
||||
}
|
||||
return rows.length > 0 ? rows : undefined;
|
||||
}
|
||||
|
||||
export function resolveTelegramInlineButtons(params: {
|
||||
buttons?: TelegramInlineButtons;
|
||||
presentation?: unknown;
|
||||
interactive?: unknown;
|
||||
}): TelegramInlineButtons | undefined {
|
||||
return (
|
||||
params.buttons ?? buildTelegramInteractiveButtons(normalizeInteractiveReply(params.interactive))
|
||||
params.buttons ??
|
||||
buildTelegramInteractiveButtons(normalizeInteractiveReply(params.interactive)) ??
|
||||
buildTelegramPresentationButtons(normalizeMessagePresentation(params.presentation))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,6 +216,32 @@ describe("telegramOutbound", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves explicit Telegram buttons when rendering presentation payloads", async () => {
|
||||
const rendered = await telegramOutbound.renderPresentation?.({
|
||||
payload: {
|
||||
text: "Use native buttons:",
|
||||
channelData: {
|
||||
telegram: {
|
||||
buttons: [[{ text: "Native", callback_data: "native" }]],
|
||||
},
|
||||
},
|
||||
},
|
||||
presentation: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Generic", value: "generic" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
ctx: {} as never,
|
||||
});
|
||||
|
||||
expect((rendered?.channelData?.telegram as { buttons?: unknown })?.buttons).toEqual([
|
||||
[{ text: "Native", callback_data: "native" }],
|
||||
]);
|
||||
});
|
||||
|
||||
it("lets allow-always approval callbacks reach Telegram's callback rewrite", async () => {
|
||||
sendMessageTelegramMock.mockResolvedValueOnce({
|
||||
messageId: "tg-approval",
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-send-result";
|
||||
import {
|
||||
normalizeMessagePresentation,
|
||||
presentationToInteractiveReply,
|
||||
renderMessagePresentationFallbackText,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import type { OutboundDeliveryFormattingOptions } from "openclaw/plugin-sdk/outbound-runtime";
|
||||
@@ -118,19 +117,17 @@ export async function sendTelegramPayloadMessages(params: {
|
||||
const quoteText =
|
||||
typeof telegramData?.quoteText === "string" ? telegramData.quoteText : undefined;
|
||||
const presentation = normalizeMessagePresentation(params.payload.presentation);
|
||||
const interactive =
|
||||
params.payload.interactive ??
|
||||
(presentation ? presentationToInteractiveReply(presentation) : undefined);
|
||||
const text =
|
||||
resolveTelegramInteractiveTextFallback({
|
||||
text: params.payload.text,
|
||||
interactive,
|
||||
interactive: params.payload.interactive,
|
||||
presentation,
|
||||
}) ?? "";
|
||||
const mediaUrls = resolvePayloadMediaUrls(params.payload);
|
||||
const buttons = resolveTelegramInlineButtons({
|
||||
buttons: telegramData?.buttons,
|
||||
interactive,
|
||||
presentation,
|
||||
interactive: params.payload.interactive,
|
||||
});
|
||||
const payloadOpts = {
|
||||
...params.baseOpts,
|
||||
@@ -213,11 +210,24 @@ export function createTelegramOutboundAdapter(
|
||||
batch: true,
|
||||
},
|
||||
},
|
||||
renderPresentation: ({ payload, presentation }) => ({
|
||||
...payload,
|
||||
text: renderMessagePresentationFallbackText({ text: payload.text, presentation }),
|
||||
interactive: presentationToInteractiveReply(presentation),
|
||||
}),
|
||||
renderPresentation: ({ payload, presentation }) => {
|
||||
const telegramData = payload.channelData?.telegram as Record<string, unknown> | undefined;
|
||||
const hasExplicitButtons = telegramData && "buttons" in telegramData;
|
||||
const buttons = hasExplicitButtons
|
||||
? undefined
|
||||
: resolveTelegramInlineButtons({ presentation });
|
||||
return {
|
||||
...payload,
|
||||
text: renderMessagePresentationFallbackText({ text: payload.text, presentation }),
|
||||
channelData: {
|
||||
...payload.channelData,
|
||||
telegram: {
|
||||
...telegramData,
|
||||
...(buttons ? { buttons } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
pinDeliveredMessage: async ({ cfg, target, messageId, pin }) => {
|
||||
const { pinMessageTelegram } = await loadSendModule();
|
||||
await pinMessageTelegram(target.to, messageId, {
|
||||
|
||||
@@ -82,18 +82,31 @@ export type AgentRuntimeProviderHandle = {
|
||||
|
||||
export type AgentRuntimeInteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
|
||||
|
||||
export type AgentRuntimeInteractiveReplyButton = {
|
||||
export type AgentRuntimeMessagePresentationButton = {
|
||||
label: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
style?: AgentRuntimeInteractiveButtonStyle;
|
||||
};
|
||||
|
||||
export type AgentRuntimeInteractiveReplyOption = {
|
||||
export type AgentRuntimeMessagePresentationOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use AgentRuntimeMessagePresentationButton.
|
||||
*/
|
||||
export type AgentRuntimeInteractiveReplyButton = AgentRuntimeMessagePresentationButton;
|
||||
|
||||
/**
|
||||
* @deprecated Use AgentRuntimeMessagePresentationOption.
|
||||
*/
|
||||
export type AgentRuntimeInteractiveReplyOption = AgentRuntimeMessagePresentationOption;
|
||||
|
||||
/**
|
||||
* @deprecated Use AgentRuntimeMessagePresentationBlock.
|
||||
*/
|
||||
export type AgentRuntimeInteractiveReplyBlock =
|
||||
| {
|
||||
type: "text";
|
||||
@@ -109,6 +122,9 @@ export type AgentRuntimeInteractiveReplyBlock =
|
||||
options: AgentRuntimeInteractiveReplyOption[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use AgentRuntimeMessagePresentation.
|
||||
*/
|
||||
export type AgentRuntimeInteractiveReply = {
|
||||
blocks: AgentRuntimeInteractiveReplyBlock[];
|
||||
};
|
||||
@@ -134,12 +150,12 @@ export type AgentRuntimeMessagePresentationBlock =
|
||||
}
|
||||
| {
|
||||
type: "buttons";
|
||||
buttons: AgentRuntimeInteractiveReplyButton[];
|
||||
buttons: AgentRuntimeMessagePresentationButton[];
|
||||
}
|
||||
| {
|
||||
type: "select";
|
||||
placeholder?: string;
|
||||
options: AgentRuntimeInteractiveReplyOption[];
|
||||
options: AgentRuntimeMessagePresentationOption[];
|
||||
};
|
||||
|
||||
export type AgentRuntimeMessagePresentation = {
|
||||
@@ -166,6 +182,9 @@ export type AgentRuntimeReplyPayload = {
|
||||
sensitiveMedia?: boolean;
|
||||
presentation?: AgentRuntimeMessagePresentation;
|
||||
delivery?: AgentRuntimeReplyPayloadDelivery;
|
||||
/**
|
||||
* @deprecated Use presentation.
|
||||
*/
|
||||
interactive?: AgentRuntimeInteractiveReply;
|
||||
btw?: {
|
||||
question: string;
|
||||
|
||||
@@ -5,6 +5,9 @@ export {
|
||||
presentationPageSize,
|
||||
} from "./presentation-limits.js";
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentation helpers for new rendering paths.
|
||||
*/
|
||||
export function reduceInteractiveReply<TState>(
|
||||
interactive: InteractiveReply | undefined,
|
||||
initialState: TState,
|
||||
|
||||
@@ -96,7 +96,7 @@ function buildTelegramExecApprovalPendingPayloadForTest(params: {
|
||||
}): ReplyPayload {
|
||||
return {
|
||||
text: `Telegram exec approval ${params.request.id}`,
|
||||
interactive: {
|
||||
presentation: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -512,7 +512,7 @@ describe("exec approval forwarder", () => {
|
||||
expect(deliver).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("attaches shared interactive approval buttons in forwarded fallback payloads", async () => {
|
||||
it("attaches shared presentation approval buttons in forwarded fallback payloads", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { deliver, forwarder } = createForwarder({
|
||||
cfg: makeTargetsCfg([{ channel: "telegram", to: "123" }]),
|
||||
@@ -535,7 +535,7 @@ describe("exec approval forwarder", () => {
|
||||
expect(delivery.to).toBe("123");
|
||||
const payload = requireFirstPayload(deliver);
|
||||
expect(payload.channelData?.execApproval).toEqual({ approvalId: "req-1" });
|
||||
expect(payload.interactive).toEqual({
|
||||
expect(payload.presentation).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -559,6 +559,7 @@ describe("exec approval forwarder", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(payload.interactive).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stores exec metadata on generic forwarded fallback payloads", async () => {
|
||||
|
||||
@@ -258,7 +258,7 @@ describe("exec approval reply helpers", () => {
|
||||
sessionKey: undefined,
|
||||
},
|
||||
});
|
||||
expect(payload.interactive).toEqual({
|
||||
expect(payload.presentation).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -282,6 +282,7 @@ describe("exec approval reply helpers", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(payload.interactive).toBeUndefined();
|
||||
expect(payload.text).toContain("Heads up.");
|
||||
expect(payload.text).toContain("```txt\n/approve slug-1 allow-once\n```");
|
||||
expect(payload.text).toContain("```sh\necho ok\n```");
|
||||
@@ -324,7 +325,7 @@ describe("exec approval reply helpers", () => {
|
||||
expect(payload.text).toContain(
|
||||
"The effective approval policy requires approval every time, so Allow Always is unavailable.",
|
||||
);
|
||||
expect(payload.interactive).toEqual({
|
||||
expect(payload.presentation).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -343,6 +344,7 @@ describe("exec approval reply helpers", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(payload.interactive).toBeUndefined();
|
||||
});
|
||||
|
||||
it("stores agent and session metadata for downstream suppression checks", () => {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { InteractiveReply, InteractiveReplyButton } from "../interactive/payload.js";
|
||||
import type {
|
||||
InteractiveReply,
|
||||
InteractiveReplyButton,
|
||||
MessagePresentation,
|
||||
MessagePresentationButton,
|
||||
} from "../interactive/payload.js";
|
||||
import { formatHumanList } from "../shared/human-list.js";
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
@@ -35,7 +40,7 @@ export type ExecApprovalReplyMetadata = {
|
||||
export type ExecApprovalActionDescriptor = {
|
||||
decision: ExecApprovalReplyDecision;
|
||||
label: string;
|
||||
style: NonNullable<InteractiveReplyButton["style"]>;
|
||||
style: NonNullable<MessagePresentationButton["style"]>;
|
||||
command: string;
|
||||
};
|
||||
|
||||
@@ -162,6 +167,52 @@ function buildApprovalInteractiveButtons(
|
||||
}));
|
||||
}
|
||||
|
||||
function buildApprovalPresentationButtons(
|
||||
descriptors: readonly ExecApprovalActionDescriptor[],
|
||||
): MessagePresentationButton[] {
|
||||
return descriptors.map((descriptor) => ({
|
||||
label: descriptor.label,
|
||||
value: descriptor.command,
|
||||
style: descriptor.style,
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildApprovalPresentationFromActionDescriptors(
|
||||
actions: readonly ExecApprovalActionDescriptor[],
|
||||
): MessagePresentation | undefined {
|
||||
const buttons = buildApprovalPresentationButtons(actions);
|
||||
return buttons.length > 0 ? { blocks: [{ type: "buttons", buttons }] } : undefined;
|
||||
}
|
||||
|
||||
export function buildApprovalPresentation(params: {
|
||||
approvalId: string;
|
||||
ask?: string | null;
|
||||
allowedDecisions?: readonly ExecApprovalReplyDecision[];
|
||||
}): MessagePresentation | undefined {
|
||||
return buildApprovalPresentationFromActionDescriptors(
|
||||
buildExecApprovalActionDescriptors({
|
||||
approvalCommandId: params.approvalId,
|
||||
ask: params.ask,
|
||||
allowedDecisions: params.allowedDecisions,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function buildExecApprovalPresentation(params: {
|
||||
approvalCommandId: string;
|
||||
ask?: string | null;
|
||||
allowedDecisions?: readonly ExecApprovalReplyDecision[];
|
||||
}): MessagePresentation | undefined {
|
||||
return buildApprovalPresentation({
|
||||
approvalId: params.approvalCommandId,
|
||||
ask: params.ask,
|
||||
allowedDecisions: params.allowedDecisions,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use buildApprovalPresentationFromActionDescriptors.
|
||||
*/
|
||||
export function buildApprovalInteractiveReplyFromActionDescriptors(
|
||||
actions: readonly ExecApprovalActionDescriptor[],
|
||||
): InteractiveReply | undefined {
|
||||
@@ -169,6 +220,9 @@ export function buildApprovalInteractiveReplyFromActionDescriptors(
|
||||
return buttons.length > 0 ? { blocks: [{ type: "buttons", buttons }] } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use buildApprovalPresentation.
|
||||
*/
|
||||
export function buildApprovalInteractiveReply(params: {
|
||||
approvalId: string;
|
||||
ask?: string | null;
|
||||
@@ -183,6 +237,9 @@ export function buildApprovalInteractiveReply(params: {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use buildExecApprovalPresentation.
|
||||
*/
|
||||
export function buildExecApprovalInteractiveReply(params: {
|
||||
approvalCommandId: string;
|
||||
ask?: string | null;
|
||||
@@ -335,7 +392,7 @@ export function buildExecApprovalPendingReplyPayload(
|
||||
|
||||
return {
|
||||
text: lines.join("\n\n"),
|
||||
interactive: buildApprovalInteractiveReply({
|
||||
presentation: buildApprovalPresentation({
|
||||
approvalId: params.approvalId,
|
||||
allowedDecisions,
|
||||
}),
|
||||
|
||||
@@ -69,7 +69,7 @@ async function flushPendingDelivery(): Promise<void> {
|
||||
}
|
||||
|
||||
type DeliveryArgs = {
|
||||
payloads?: Array<{ text?: string; interactive?: unknown }>;
|
||||
payloads?: Array<{ text?: string; presentation?: unknown; interactive?: unknown }>;
|
||||
};
|
||||
|
||||
function deliveryArgs(deliver: ReturnType<typeof vi.fn>): DeliveryArgs | undefined {
|
||||
@@ -137,7 +137,7 @@ describe("plugin approval forwarding", () => {
|
||||
expect(text).toContain("Sensitive tool call");
|
||||
expect(text).toContain("plugin-req-1");
|
||||
expect(text).toContain("/approve");
|
||||
expect(payload?.interactive).toEqual({
|
||||
expect(payload?.presentation).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -161,6 +161,7 @@ describe("plugin approval forwarding", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(payload?.interactive).toBeUndefined();
|
||||
});
|
||||
|
||||
it("renders only request-scoped plugin approval decisions", async () => {
|
||||
@@ -179,7 +180,7 @@ describe("plugin approval forwarding", () => {
|
||||
const payload = firstDeliveredPayload(deliver);
|
||||
expect(payload?.text).toContain("Reply with: /approve <id> allow-once|deny");
|
||||
expect(payload?.text).not.toContain("allow-always");
|
||||
expect(payload?.interactive).toEqual({
|
||||
expect(payload?.presentation).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -198,6 +199,7 @@ describe("plugin approval forwarding", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(payload?.interactive).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes severity icon for critical", async () => {
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
|
||||
export type InteractiveButtonStyle = "primary" | "secondary" | "success" | "danger";
|
||||
|
||||
export type InteractiveReplyButton = {
|
||||
export type MessagePresentationTone = "info" | "success" | "warning" | "danger" | "neutral";
|
||||
|
||||
export type MessagePresentationButtonStyle = InteractiveButtonStyle;
|
||||
|
||||
export type MessagePresentationButton = {
|
||||
label: string;
|
||||
value?: string;
|
||||
url?: string;
|
||||
@@ -23,44 +27,61 @@ export type InteractiveReplyButton = {
|
||||
style?: InteractiveButtonStyle;
|
||||
};
|
||||
|
||||
export type InteractiveReplyOption = {
|
||||
export type MessagePresentationOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentationButton.
|
||||
*/
|
||||
export type InteractiveReplyButton = MessagePresentationButton;
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentationOption.
|
||||
*/
|
||||
export type InteractiveReplyOption = MessagePresentationOption;
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentationTextBlock.
|
||||
*/
|
||||
export type InteractiveReplyTextBlock = {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentationButtonsBlock.
|
||||
*/
|
||||
type InteractiveReplyButtonsBlock = {
|
||||
type: "buttons";
|
||||
buttons: InteractiveReplyButton[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentationSelectBlock.
|
||||
*/
|
||||
export type InteractiveReplySelectBlock = {
|
||||
type: "select";
|
||||
placeholder?: string;
|
||||
options: InteractiveReplyOption[];
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentationBlock.
|
||||
*/
|
||||
export type InteractiveReplyBlock =
|
||||
| InteractiveReplyTextBlock
|
||||
| InteractiveReplyButtonsBlock
|
||||
| InteractiveReplySelectBlock;
|
||||
|
||||
/**
|
||||
* @deprecated Use MessagePresentation.
|
||||
*/
|
||||
export type InteractiveReply = {
|
||||
blocks: InteractiveReplyBlock[];
|
||||
};
|
||||
|
||||
export type MessagePresentationTone = "info" | "success" | "warning" | "danger" | "neutral";
|
||||
|
||||
export type MessagePresentationButtonStyle = InteractiveButtonStyle;
|
||||
|
||||
export type MessagePresentationButton = InteractiveReplyButton;
|
||||
|
||||
export type MessagePresentationOption = InteractiveReplyOption;
|
||||
|
||||
export type MessagePresentationTextBlock = {
|
||||
type: "text";
|
||||
text: string;
|
||||
@@ -215,6 +236,9 @@ function normalizeInteractiveBlock(raw: unknown): InteractiveReplyBlock | undefi
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use normalizeMessagePresentation.
|
||||
*/
|
||||
export function normalizeInteractiveReply(raw: unknown): InteractiveReply | undefined {
|
||||
const record = toRecord(raw);
|
||||
if (!record) {
|
||||
@@ -271,6 +295,9 @@ export function normalizeMessagePresentation(raw: unknown): MessagePresentation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use hasMessagePresentationBlocks.
|
||||
*/
|
||||
export function hasInteractiveReplyBlocks(value: unknown): value is InteractiveReply {
|
||||
return Boolean(normalizeInteractiveReply(value));
|
||||
}
|
||||
@@ -279,6 +306,9 @@ export function hasMessagePresentationBlocks(value: unknown): value is MessagePr
|
||||
return Boolean(normalizeMessagePresentation(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Avoid producing InteractiveReply payloads; send MessagePresentation directly.
|
||||
*/
|
||||
export function presentationToInteractiveReply(
|
||||
presentation: MessagePresentation,
|
||||
): InteractiveReply | undefined {
|
||||
@@ -339,6 +369,9 @@ export function isMessagePresentationInteractiveBlock(
|
||||
return block.type === "buttons" || block.type === "select";
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Avoid producing InteractiveReply payloads; send MessagePresentation directly.
|
||||
*/
|
||||
export function presentationToInteractiveControlsReply(
|
||||
presentation: MessagePresentation,
|
||||
): InteractiveReply | undefined {
|
||||
@@ -347,6 +380,9 @@ export function presentationToInteractiveControlsReply(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Legacy bridge for old InteractiveReply payloads. New producers should send MessagePresentation.
|
||||
*/
|
||||
export function interactiveReplyToPresentation(
|
||||
interactive: InteractiveReply,
|
||||
): MessagePresentation | undefined {
|
||||
@@ -466,6 +502,9 @@ export function hasReplyPayloadContent(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use renderMessagePresentationFallbackText with MessagePresentation.
|
||||
*/
|
||||
export function resolveInteractiveTextFallback(params: {
|
||||
text?: string;
|
||||
interactive?: InteractiveReply;
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
describe("plugin-sdk/approval-renderers", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "builds shared approval payloads with generic interactive commands",
|
||||
name: "builds shared approval payloads with generic presentation commands",
|
||||
payload: buildApprovalPendingReplyPayload({
|
||||
approvalId: "plugin:approval-123",
|
||||
approvalSlug: "plugin:a",
|
||||
text: "Approval required @everyone",
|
||||
}),
|
||||
textExpected: (text: string) => expect(text).toContain("@everyone"),
|
||||
interactiveExpected: {
|
||||
presentationExpected: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -63,7 +63,7 @@ describe("plugin-sdk/approval-renderers", () => {
|
||||
},
|
||||
}),
|
||||
textExpected: (text: string) => expect(text).toContain("Plugin approval required"),
|
||||
interactiveExpected: {
|
||||
presentationExpected: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -119,7 +119,7 @@ describe("plugin-sdk/approval-renderers", () => {
|
||||
}),
|
||||
textExpected: (text: string) =>
|
||||
expect(text).toContain("Reply with: /approve <id> allow-once|deny"),
|
||||
interactiveExpected: {
|
||||
presentationExpected: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
@@ -158,7 +158,7 @@ describe("plugin-sdk/approval-renderers", () => {
|
||||
text: "resolved @everyone",
|
||||
}),
|
||||
textExpected: (text: string) => expect(text).toBe("resolved @everyone"),
|
||||
interactiveExpected: undefined,
|
||||
presentationExpected: undefined,
|
||||
channelDataExpected: {
|
||||
execApproval: {
|
||||
approvalId: "req-123",
|
||||
@@ -183,7 +183,7 @@ describe("plugin-sdk/approval-renderers", () => {
|
||||
},
|
||||
}),
|
||||
textExpected: (text: string) => expect(text).toContain("Plugin approval allowed once"),
|
||||
interactiveExpected: undefined,
|
||||
presentationExpected: undefined,
|
||||
channelDataExpected: {
|
||||
execApproval: {
|
||||
approvalId: "plugin-approval-123",
|
||||
@@ -195,13 +195,14 @@ describe("plugin-sdk/approval-renderers", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
])("$name", ({ payload, textExpected, interactiveExpected, channelDataExpected }) => {
|
||||
])("$name", ({ payload, textExpected, presentationExpected, channelDataExpected }) => {
|
||||
if (payload.text === undefined) {
|
||||
throw new Error("expected rendered approval text");
|
||||
}
|
||||
textExpected(payload.text);
|
||||
if (interactiveExpected) {
|
||||
expect(payload.interactive).toEqual(interactiveExpected);
|
||||
if (presentationExpected) {
|
||||
expect(payload.presentation).toEqual(presentationExpected);
|
||||
expect(payload.interactive).toBeUndefined();
|
||||
}
|
||||
if (channelDataExpected) {
|
||||
expect(payload.channelData).toEqual(channelDataExpected);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
buildApprovalInteractiveReply,
|
||||
buildApprovalPresentation,
|
||||
type ExecApprovalReplyDecision,
|
||||
} from "../infra/exec-approval-reply.js";
|
||||
import {
|
||||
@@ -27,7 +27,7 @@ export function buildApprovalPendingReplyPayload(params: {
|
||||
const allowedDecisions = params.allowedDecisions ?? DEFAULT_ALLOWED_DECISIONS;
|
||||
return {
|
||||
text: params.text,
|
||||
interactive: buildApprovalInteractiveReply({
|
||||
presentation: buildApprovalPresentation({
|
||||
approvalId: params.approvalId,
|
||||
allowedDecisions,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export {
|
||||
buildApprovalInteractiveReplyFromActionDescriptors,
|
||||
buildApprovalPresentation,
|
||||
buildApprovalPresentationFromActionDescriptors,
|
||||
buildExecApprovalPresentation,
|
||||
buildExecApprovalActionDescriptors,
|
||||
buildExecApprovalPendingReplyPayload,
|
||||
getExecApprovalApproverDmNoticeText,
|
||||
|
||||
Reference in New Issue
Block a user