Skip to content

Commit 817a2f2

Browse files
authored
🤖 fix: Handle emojis with variation selectors in status_set (#468)
Fixes two issues with the `status_set` tool: ## 1. Emoji Validation Failures **Problem:** Emojis with variation selectors (like ✏️) were incorrectly rejected by validation. **Root Cause:** The validator used `[...str].length` to count characters, which counts code points. Emojis with variation selectors (U+FE0F) have 2 code points but represent a single grapheme cluster. **Solution:** Use `Intl.Segmenter` (built-in API) to count grapheme clusters - what users perceive as single characters. This properly handles: - Variation selectors (✏️, ☀️, ➡️) - Skin tone modifiers (👋🏻, 👋🏽, 👋🏿) - Complex emojis (flags, multi-person emojis) **Example:** ```javascript const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" }); [...segmenter.segment("✏️")].length; // 1 (correct!) [..."✏️"].length; // 2 (wrong - counts code points) ``` ## 2. Error Message Styling **Problem:** Error messages in `StatusSetToolCall` used `text-sm` class, making them smaller than surrounding text in the tool header. **Solution:** Removed `text-sm` class so errors use the same font size as other header content. --- _Generated with `cmux`_
1 parent 1202238 commit 817a2f2

File tree

6 files changed

+53
-15
lines changed

6 files changed

+53
-15
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module.exports = {
1919
{
2020
tsconfig: {
2121
target: "ES2020",
22-
lib: ["ES2020", "DOM"],
22+
lib: ["ES2020", "DOM", "ES2022.Intl"],
2323
esModuleInterop: true,
2424
allowSyntheticDefaultImports: true,
2525
},

src/components/tools/StatusSetToolCall.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
3131
<Tooltip>status_set</Tooltip>
3232
</TooltipWrapper>
3333
<span className="text-muted-foreground italic">{args.message}</span>
34-
{errorMessage && <span className="text-error-foreground text-sm">({errorMessage})</span>}
34+
{errorMessage && <span className="text-error-foreground">({errorMessage})</span>}
3535
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
3636
</ToolHeader>
3737
</ToolContainer>

src/services/tools/status_set.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@ describe("status_set tool validation", () => {
3131
}
3232
});
3333

34+
it("should accept emojis with variation selectors", async () => {
35+
const tool = createStatusSetTool(mockConfig);
36+
37+
// Emojis with variation selectors (U+FE0F)
38+
const emojis = ["✏️", "✅", "➡️", "☀️"];
39+
for (const emoji of emojis) {
40+
const result = (await tool.execute!({ emoji, message: "Test" }, mockToolCallOptions)) as {
41+
success: boolean;
42+
emoji: string;
43+
message: string;
44+
};
45+
expect(result).toEqual({ success: true, emoji, message: "Test" });
46+
}
47+
});
48+
49+
it("should accept emojis with skin tone modifiers", async () => {
50+
const tool = createStatusSetTool(mockConfig);
51+
52+
const emojis = ["👋🏻", "👋🏽", "👋🏿"];
53+
for (const emoji of emojis) {
54+
const result = (await tool.execute!({ emoji, message: "Test" }, mockToolCallOptions)) as {
55+
success: boolean;
56+
emoji: string;
57+
message: string;
58+
};
59+
expect(result).toEqual({ success: true, emoji, message: "Test" });
60+
}
61+
});
62+
3463
it("should reject multiple emojis", async () => {
3564
const tool = createStatusSetTool(mockConfig);
3665

src/services/tools/status_set.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,35 @@ export type StatusSetToolResult =
1818

1919
/**
2020
* Validates that a string is a single emoji character
21-
* Uses Unicode property escapes to match emoji characters
21+
* Uses Intl.Segmenter to count grapheme clusters (handles variation selectors, skin tones, etc.)
2222
*/
2323
function isValidEmoji(str: string): boolean {
24-
// Check if string contains exactly one character (handles multi-byte emojis)
25-
const chars = [...str];
26-
if (chars.length !== 1) {
24+
if (!str) return false;
25+
26+
// Use Intl.Segmenter to count grapheme clusters (what users perceive as single characters)
27+
// This properly handles emojis with variation selectors (like ✏️), skin tones, flags, etc.
28+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
29+
const segments = [...segmenter.segment(str)];
30+
31+
// Must be exactly one grapheme cluster
32+
if (segments.length !== 1) {
2733
return false;
2834
}
2935

3036
// Check if it's an emoji using Unicode properties
31-
const emojiRegex = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]$/u;
32-
return emojiRegex.test(str);
37+
const emojiRegex = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]/u;
38+
return emojiRegex.test(segments[0].segment);
3339
}
3440

3541
/**
3642
* Status set tool factory for AI assistant
3743
* Creates a tool that allows the AI to set status indicator showing current activity
44+
*
45+
* The status is displayed IMMEDIATELY when this tool is called, even before other
46+
* tool calls complete. This prevents agents from prematurely declaring success
47+
* (e.g., "PR checks passed") when operations are still pending. Agents should only
48+
* set success status after confirming the outcome of long-running operations.
49+
*
3850
* @param config Required configuration (not used for this tool, but required by interface)
3951
*/
4052
export const createStatusSetTool: ToolFactory = () => {

src/utils/tools/toolDefinitions.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,13 +185,10 @@ export const TOOL_DEFINITIONS = {
185185
description:
186186
"Set a status indicator to show what the agent is currently doing. " +
187187
"The emoji appears left of the streaming indicator, and the message shows on hover. " +
188+
"The status is set IMMEDIATELY when this tool is called, even before other tool calls complete. " +
188189
"IMPORTANT: Always set a status at the start of each response and update it as your work progresses. " +
189-
"Set a final status before finishing your response that reflects the outcome: " +
190-
"'✅ PR checks pass and ready to merge' (success), " +
191-
"'❌ CreateWorkspace Tests failed' (failure), " +
192-
"'⚠️ Encountered serious issue with design' (warning/blocker). " +
193-
"The status is cleared at the start of each new response, so you must set it again. " +
194-
"Use this to communicate ongoing activities (e.g., '🔍 Analyzing code', '📝 Writing tests', '🔧 Refactoring logic').",
190+
"The status is cleared when a new user message comes in, so you must set it again for each response. " +
191+
"Use this to communicate ongoing activities and set a final status before completing that reflects the outcome.",
195192
schema: z
196193
.object({
197194
emoji: z.string().describe("A single emoji character representing the current activity"),

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"compilerOptions": {
33
"target": "ES2020",
4-
"lib": ["ES2023", "DOM"],
4+
"lib": ["ES2023", "DOM", "ES2022.Intl"],
55
"module": "ESNext",
66
"moduleResolution": "node",
77
"jsx": "react-jsx",

0 commit comments

Comments
 (0)