Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
{
tsconfig: {
target: "ES2020",
lib: ["ES2020", "DOM"],
lib: ["ES2020", "DOM", "ES2022.Intl"],
esModuleInterop: true,
allowSyntheticDefaultImports: true,
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/tools/StatusSetToolCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const StatusSetToolCall: React.FC<StatusSetToolCallProps> = ({
<Tooltip>status_set</Tooltip>
</TooltipWrapper>
<span className="text-muted-foreground italic">{args.message}</span>
{errorMessage && <span className="text-error-foreground text-sm">({errorMessage})</span>}
{errorMessage && <span className="text-error-foreground">({errorMessage})</span>}
<StatusIndicator status={status}>{statusDisplay}</StatusIndicator>
</ToolHeader>
</ToolContainer>
Expand Down
29 changes: 29 additions & 0 deletions src/services/tools/status_set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,35 @@ describe("status_set tool validation", () => {
}
});

it("should accept emojis with variation selectors", async () => {
const tool = createStatusSetTool(mockConfig);

// Emojis with variation selectors (U+FE0F)
const emojis = ["✏️", "βœ…", "➑️", "β˜€οΈ"];
for (const emoji of emojis) {
const result = (await tool.execute!({ emoji, message: "Test" }, mockToolCallOptions)) as {
success: boolean;
emoji: string;
message: string;
};
expect(result).toEqual({ success: true, emoji, message: "Test" });
}
});

it("should accept emojis with skin tone modifiers", async () => {
const tool = createStatusSetTool(mockConfig);

const emojis = ["πŸ‘‹πŸ»", "πŸ‘‹πŸ½", "πŸ‘‹πŸΏ"];
for (const emoji of emojis) {
const result = (await tool.execute!({ emoji, message: "Test" }, mockToolCallOptions)) as {
success: boolean;
emoji: string;
message: string;
};
expect(result).toEqual({ success: true, emoji, message: "Test" });
}
});

it("should reject multiple emojis", async () => {
const tool = createStatusSetTool(mockConfig);

Expand Down
24 changes: 18 additions & 6 deletions src/services/tools/status_set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,35 @@ export type StatusSetToolResult =

/**
* Validates that a string is a single emoji character
* Uses Unicode property escapes to match emoji characters
* Uses Intl.Segmenter to count grapheme clusters (handles variation selectors, skin tones, etc.)
*/
function isValidEmoji(str: string): boolean {
// Check if string contains exactly one character (handles multi-byte emojis)
const chars = [...str];
if (chars.length !== 1) {
if (!str) return false;

// Use Intl.Segmenter to count grapheme clusters (what users perceive as single characters)
// This properly handles emojis with variation selectors (like ✏️), skin tones, flags, etc.
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const segments = [...segmenter.segment(str)];

// Must be exactly one grapheme cluster
if (segments.length !== 1) {
return false;
}

// Check if it's an emoji using Unicode properties
const emojiRegex = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]$/u;
return emojiRegex.test(str);
const emojiRegex = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]/u;
return emojiRegex.test(segments[0].segment);
}

/**
* Status set tool factory for AI assistant
* Creates a tool that allows the AI to set status indicator showing current activity
*
* The status is displayed IMMEDIATELY when this tool is called, even before other
* tool calls complete. This prevents agents from prematurely declaring success
* (e.g., "PR checks passed") when operations are still pending. Agents should only
* set success status after confirming the outcome of long-running operations.
*
* @param config Required configuration (not used for this tool, but required by interface)
*/
export const createStatusSetTool: ToolFactory = () => {
Expand Down
9 changes: 3 additions & 6 deletions src/utils/tools/toolDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,10 @@ export const TOOL_DEFINITIONS = {
description:
"Set a status indicator to show what the agent is currently doing. " +
"The emoji appears left of the streaming indicator, and the message shows on hover. " +
"The status is set IMMEDIATELY when this tool is called, even before other tool calls complete. " +
"IMPORTANT: Always set a status at the start of each response and update it as your work progresses. " +
"Set a final status before finishing your response that reflects the outcome: " +
"'βœ… PR checks pass and ready to merge' (success), " +
"'❌ CreateWorkspace Tests failed' (failure), " +
"'⚠️ Encountered serious issue with design' (warning/blocker). " +
"The status is cleared at the start of each new response, so you must set it again. " +
"Use this to communicate ongoing activities (e.g., 'πŸ” Analyzing code', 'πŸ“ Writing tests', 'πŸ”§ Refactoring logic').",
"The status is cleared when a new user message comes in, so you must set it again for each response. " +
"Use this to communicate ongoing activities and set a final status before completing that reflects the outcome.",
schema: z
.object({
emoji: z.string().describe("A single emoji character representing the current activity"),
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2023", "DOM"],
"lib": ["ES2023", "DOM", "ES2022.Intl"],
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
Expand Down
Loading