Slack: add rich text previews for modal inputs

This commit is contained in:
Colin
2026-02-16 13:41:20 -05:00
committed by Peter Steinberger
parent 05ab147081
commit 9fcb93dd13
2 changed files with 104 additions and 2 deletions

View File

@@ -707,7 +707,15 @@ describe("registerSlackInteractionEvents", () => {
type: "rich_text_input",
rich_text_value: {
type: "rich_text",
elements: [{ type: "rich_text_section", elements: [] }],
elements: [
{
type: "rich_text_section",
elements: [
{ type: "text", text: "Ship this now" },
{ type: "text", text: "with canary metrics" },
],
},
],
},
},
},
@@ -736,6 +744,7 @@ describe("registerSlackInteractionEvents", () => {
inputEmail?: string;
inputUrl?: string;
richTextValue?: unknown;
richTextPreview?: string;
}>;
};
expect(payload.inputs).toEqual(
@@ -791,15 +800,72 @@ describe("registerSlackInteractionEvents", () => {
expect.objectContaining({
actionId: "richtext_input",
inputKind: "rich_text",
richTextPreview: "Ship this now with canary metrics",
richTextValue: {
type: "rich_text",
elements: [{ type: "rich_text_section", elements: [] }],
elements: [
{
type: "rich_text_section",
elements: [
{ type: "text", text: "Ship this now" },
{ type: "text", text: "with canary metrics" },
],
},
],
},
}),
]),
);
});
it("truncates rich text preview to keep payload summaries compact", async () => {
enqueueSystemEventMock.mockReset();
const { ctx, getViewHandler } = createContext();
registerSlackInteractionEvents({ ctx: ctx as never });
const viewHandler = getViewHandler();
expect(viewHandler).toBeTruthy();
const longText = "deploy ".repeat(40).trim();
const ack = vi.fn().mockResolvedValue(undefined);
await viewHandler!({
ack,
body: {
user: { id: "U555" },
view: {
id: "V555",
callback_id: "openclaw:long_richtext",
state: {
values: {
richtext_block: {
richtext_input: {
type: "rich_text_input",
rich_text_value: {
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [{ type: "text", text: longText }],
},
],
},
},
},
},
},
},
},
});
expect(ack).toHaveBeenCalled();
const [eventText] = enqueueSystemEventMock.mock.calls[0] as [string];
const payload = JSON.parse(eventText.replace("Slack interaction: ", "")) as {
inputs: Array<{ actionId: string; richTextPreview?: string }>;
};
const richInput = payload.inputs.find((input) => input.actionId === "richtext_input");
expect(richInput?.richTextPreview).toBeTruthy();
expect((richInput?.richTextPreview ?? "").length).toBeLessThanOrEqual(120);
});
it("captures modal close events and enqueues view closed event", async () => {
enqueueSystemEventMock.mockReset();
const { ctx, getViewClosedHandler, resolveSessionKey } = createContext();

View File

@@ -38,6 +38,7 @@ type InteractionSummary = {
inputEmail?: string;
inputUrl?: string;
richTextValue?: unknown;
richTextPreview?: string;
userId?: string;
teamId?: string;
triggerId?: string;
@@ -66,6 +67,7 @@ type ModalInputSummary = {
inputEmail?: string;
inputUrl?: string;
richTextValue?: unknown;
richTextPreview?: string;
};
function readOptionValues(options: unknown): string[] | undefined {
@@ -107,6 +109,35 @@ function uniqueNonEmptyStrings(values: string[]): string[] {
return unique;
}
function collectRichTextFragments(value: unknown, out: string[]): void {
if (!value || typeof value !== "object") {
return;
}
const typed = value as { text?: unknown; elements?: unknown };
if (typeof typed.text === "string" && typed.text.trim().length > 0) {
out.push(typed.text.trim());
}
if (Array.isArray(typed.elements)) {
for (const child of typed.elements) {
collectRichTextFragments(child, out);
}
}
}
function summarizeRichTextPreview(value: unknown): string | undefined {
const fragments: string[] = [];
collectRichTextFragments(value, fragments);
if (fragments.length === 0) {
return undefined;
}
const joined = fragments.join(" ").replace(/\s+/g, " ").trim();
if (!joined) {
return undefined;
}
const max = 120;
return joined.length <= max ? joined : `${joined.slice(0, max - 1)}`;
}
function summarizeAction(
action: Record<string, unknown>,
): Omit<InteractionSummary, "actionId" | "blockId"> {
@@ -166,6 +197,7 @@ function summarizeAction(
}
}
const richTextValue = actionType === "rich_text_input" ? typed.rich_text_value : undefined;
const richTextPreview = summarizeRichTextPreview(richTextValue);
const inputKind =
actionType === "number_input"
? "number"
@@ -197,6 +229,7 @@ function summarizeAction(
inputEmail,
inputUrl,
richTextValue,
richTextPreview,
};
}
@@ -242,6 +275,9 @@ function formatInteractionSelectionLabel(params: {
if (typeof params.summary.selectedDateTime === "number") {
return new Date(params.summary.selectedDateTime * 1000).toISOString();
}
if (params.summary.richTextPreview) {
return params.summary.richTextPreview;
}
if (params.summary.value?.trim()) {
return params.summary.value.trim();
}