Skip to content

Commit 23f64b3

Browse files
fix(discord): keep command previews emoji-safe
1 parent 0cc9927 commit 23f64b3

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

extensions/discord/src/approval-handler.runtime.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,54 @@
22
import { describe, expect, it } from "vitest";
33
import { discordApprovalNativeRuntime } from "./approval-handler.runtime.js";
44

5+
async function buildExecApprovalPayloadText(commandText: string): Promise<string> {
6+
const pending = await discordApprovalNativeRuntime.presentation.buildPendingPayload({
7+
cfg: {} as never,
8+
accountId: "main",
9+
context: {
10+
token: "discord-token",
11+
config: {} as never,
12+
},
13+
request: {
14+
id: "approval-1",
15+
request: {
16+
command: commandText,
17+
},
18+
createdAtMs: 0,
19+
expiresAtMs: 1_000,
20+
},
21+
approvalKind: "exec",
22+
nowMs: 0,
23+
view: {
24+
approvalKind: "exec",
25+
phase: "pending",
26+
approvalId: "approval-1",
27+
title: "Exec Approval Required",
28+
commandText,
29+
commandPreview: null,
30+
expiresAtMs: 1_000,
31+
metadata: [],
32+
actions: [
33+
{
34+
label: "Allow",
35+
decision: "allow-once",
36+
style: "success",
37+
command: "/approve approval-1 allow-once",
38+
},
39+
],
40+
},
41+
});
42+
return JSON.stringify(pending);
43+
}
44+
545
describe("discordApprovalNativeRuntime", () => {
46+
it("does not split emoji graphemes when truncating exec command previews", async () => {
47+
const prefix = "a".repeat(999);
48+
49+
await expect(buildExecApprovalPayloadText(`${prefix}😀x`)).resolves.toContain(`${prefix}...`);
50+
await expect(buildExecApprovalPayloadText(`${prefix}🇺🇸x`)).resolves.toContain(`${prefix}...`);
51+
});
52+
653
it("routes origin approval updates to the Discord thread channel when threadId is present", async () => {
754
const prepared = await discordApprovalNativeRuntime.transport.prepareTarget({
855
cfg: {} as never,

extensions/discord/src/approval-handler.runtime.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,38 @@ function buildExecApprovalPayload(container: DiscordUiContainer): MessagePayload
159159
return { components };
160160
}
161161

162+
const commandPreviewSegmenter =
163+
typeof Intl !== "undefined" && "Segmenter" in Intl
164+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
165+
: null;
166+
167+
function* iterateCommandPreviewSegments(commandText: string): Iterable<string> {
168+
if (!commandPreviewSegmenter) {
169+
yield* Array.from(commandText);
170+
return;
171+
}
172+
try {
173+
for (const segment of commandPreviewSegmenter.segment(commandText)) {
174+
yield segment.segment;
175+
}
176+
} catch {
177+
yield* Array.from(commandText);
178+
}
179+
}
180+
181+
function truncateCommandPreview(commandText: string, maxChars: number): string {
182+
let commandRaw = "";
183+
for (const segment of iterateCommandPreviewSegments(commandText)) {
184+
if (commandRaw.length + segment.length > maxChars) {
185+
return `${commandRaw}...`;
186+
}
187+
commandRaw += segment;
188+
}
189+
return commandText;
190+
}
191+
162192
function formatCommandPreview(commandText: string, maxChars: number): string {
163-
const commandRaw =
164-
commandText.length > maxChars ? `${commandText.slice(0, maxChars)}...` : commandText;
165-
return commandRaw.replace(/`/g, "\u200b`");
193+
return truncateCommandPreview(commandText, maxChars).replace(/`/g, "\u200b`");
166194
}
167195

168196
function formatOptionalCommandPreview(

0 commit comments

Comments
 (0)