fix(discord): keep command previews emoji-safe (#99539)

This commit is contained in:
xingzhou
2026-07-06 13:40:39 +08:00
committed by GitHub
parent 5dbad1102f
commit a13acacd3c
2 changed files with 78 additions and 3 deletions

View File

@@ -2,7 +2,54 @@
import { describe, expect, it } from "vitest";
import { discordApprovalNativeRuntime } from "./approval-handler.runtime.js";
async function buildExecApprovalPayloadText(commandText: string): Promise<string> {
const pending = await discordApprovalNativeRuntime.presentation.buildPendingPayload({
cfg: {} as never,
accountId: "main",
context: {
token: "discord-token",
config: {} as never,
},
request: {
id: "approval-1",
request: {
command: commandText,
},
createdAtMs: 0,
expiresAtMs: 1_000,
},
approvalKind: "exec",
nowMs: 0,
view: {
approvalKind: "exec",
phase: "pending",
approvalId: "approval-1",
title: "Exec Approval Required",
commandText,
commandPreview: null,
expiresAtMs: 1_000,
metadata: [],
actions: [
{
label: "Allow",
decision: "allow-once",
style: "success",
command: "/approve approval-1 allow-once",
},
],
},
});
return JSON.stringify(pending);
}
describe("discordApprovalNativeRuntime", () => {
it("does not split emoji graphemes when truncating exec command previews", async () => {
const prefix = "a".repeat(999);
await expect(buildExecApprovalPayloadText(`${prefix}😀x`)).resolves.toContain(`${prefix}...`);
await expect(buildExecApprovalPayloadText(`${prefix}🇺🇸x`)).resolves.toContain(`${prefix}...`);
});
it("routes origin approval updates to the Discord thread channel when threadId is present", async () => {
const prepared = await discordApprovalNativeRuntime.transport.prepareTarget({
cfg: {} as never,

View File

@@ -159,10 +159,38 @@ function buildExecApprovalPayload(container: DiscordUiContainer): MessagePayload
return { components };
}
const commandPreviewSegmenter =
typeof Intl !== "undefined" && "Segmenter" in Intl
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null;
function* iterateCommandPreviewSegments(commandText: string): Iterable<string> {
if (!commandPreviewSegmenter) {
yield* Array.from(commandText);
return;
}
try {
for (const segment of commandPreviewSegmenter.segment(commandText)) {
yield segment.segment;
}
} catch {
yield* Array.from(commandText);
}
}
function truncateCommandPreview(commandText: string, maxChars: number): string {
let commandRaw = "";
for (const segment of iterateCommandPreviewSegments(commandText)) {
if (commandRaw.length + segment.length > maxChars) {
return `${commandRaw}...`;
}
commandRaw += segment;
}
return commandText;
}
function formatCommandPreview(commandText: string, maxChars: number): string {
const commandRaw =
commandText.length > maxChars ? `${commandText.slice(0, maxChars)}...` : commandText;
return commandRaw.replace(/`/g, "\u200b`");
return truncateCommandPreview(commandText, maxChars).replace(/`/g, "\u200b`");
}
function formatOptionalCommandPreview(