feat(desktop): add search to agent emoji picker (#2630)

## Why
The agent avatar picker disables Emoji Mart search, making emojis
difficult to find when creating or editing an agent.

## What
- Enable sticky search in the shared agent avatar picker
- Focus the search field when the Emoji tab opens
- Add end-to-end coverage for search, focus, and selection

## Risk Assessment
Low — this is limited to the desktop agent avatar picker and does not
change avatar persistence or agent configuration.

## References
- `pnpm --dir desktop test` — 3,448 passed
- `pnpm --dir desktop typecheck` — passed
- `pnpm --dir desktop exec playwright test tests/e2e/agents.spec.ts` —
20 passed

---
**Update Jul 24, 16:28 EDT:** Completed `CONTRIBUTING.md` validation.

### Manual test
1. Create an agent and open Add avatar → Emoji.
2. Confirm the search field is focused and filters results.
3. Select an emoji and confirm it becomes the avatar.
4. Repeat while editing an existing agent.

### Validation
- `just ci` — passed
- `just test` — passed

### Follow-up work
None.

Generated with Codex
This commit is contained in:
Luke Tornquist
2026-07-27 09:47:31 -07:00
committed by GitHub
parent be275cfc6c
commit 313f793c87
2 changed files with 51 additions and 1 deletions
@@ -133,6 +133,32 @@ export function AgentCreationPreview({
isAvatarMenuOpen && activeTab === "emoji",
);
// Emoji Mart mounts its search input inside a shadow root. Wait for it
// before focusing so the surrounding Radix popover cannot win the race.
React.useEffect(() => {
if (!isAvatarMenuOpen || activeTab !== "emoji") {
return;
}
let animationFrame = 0;
const focusSearchInput = () => {
const searchInput =
emojiPickerContainerRef.current
?.querySelector("em-emoji-picker")
?.shadowRoot?.querySelector<HTMLInputElement>(
'input[type="search"]',
) ?? null;
if (!searchInput) {
animationFrame = window.requestAnimationFrame(focusSearchInput);
return;
}
searchInput.focus();
};
animationFrame = window.requestAnimationFrame(focusSearchInput);
return () => window.cancelAnimationFrame(animationFrame);
}, [activeTab, isAvatarMenuOpen]);
const customColorDraft = React.useMemo(
() => hsvToHex(customHue, customSaturation, customValue),
[customHue, customSaturation, customValue],
@@ -550,6 +576,7 @@ export function AgentCreationPreview({
style={emojiMartThemeVars}
>
<Picker
autoFocus
categories={EMOJI_MART_CATEGORIES}
data={emojiData}
dynamicWidth
@@ -577,7 +604,7 @@ export function AgentCreationPreview({
applyEmojiAvatar(emoji.native, nextColor);
}}
previewPosition="none"
searchPosition="none"
searchPosition="sticky"
set="native"
skinTonePosition="none"
theme="auto"
+23
View File
@@ -263,6 +263,29 @@ test("built-in persona edits persist", async ({ page }) => {
});
});
test("searches agent avatar emoji with focus on open", async ({ page }) => {
await gotoApp(page);
await page.getByTestId("open-agents-view").click();
await page.getByTestId("new-agent-card").click();
await page.getByRole("menuitem", { name: "Create from scratch" }).click();
await expect(page.getByTestId("persona-dialog")).toBeVisible();
await page.getByLabel("Add avatar").click();
await page.getByRole("tab", { name: "Emoji" }).click();
const picker = page.locator("em-emoji-picker");
const searchInput = picker.locator("input[type='search']");
await expect(searchInput).toBeVisible();
await expect(searchInput).toBeFocused();
await searchInput.fill("rocket");
await picker.locator("button[aria-label='🚀']").first().click();
await expect(page.getByRole("img", { name: "Agent name avatar" })).toHaveText(
"🚀",
);
});
test("agent avatar emoji picker scrolls inside its popover", async ({
page,
}) => {