From ea33d1980f416f8b8c7f5081ea57b97590578a0a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 30 Jan 2026 22:52:07 +0700 Subject: [PATCH] feat: add keyboard navigation to combobox and select fields on web --- .../agent-form/agent-form-dropdowns.tsx | 66 +++++++++ .../components/ui/combobox-keyboard.test.ts | 33 +++++ .../src/components/ui/combobox-keyboard.ts | 19 +++ packages/app/src/components/ui/combobox.tsx | 128 ++++++++++++++++-- .../src/server/agent/agent-self-id-mcp.ts | 17 +++ .../agent-self-identification.e2e.test.ts | 66 +++++++++ packages/website/public/favicon.ico | Bin 15086 -> 15086 bytes packages/website/public/favicon.svg | 13 +- 8 files changed, 320 insertions(+), 22 deletions(-) create mode 100644 packages/app/src/components/ui/combobox-keyboard.test.ts create mode 100644 packages/app/src/components/ui/combobox-keyboard.ts diff --git a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx index 70a80e696..d9bbc7346 100644 --- a/packages/app/src/components/agent-form/agent-form-dropdowns.tsx +++ b/packages/app/src/components/agent-form/agent-form-dropdowns.tsx @@ -134,11 +134,44 @@ export function SelectField({ valueEllipsizeMode, testID, }: SelectFieldProps): ReactElement { + const getWebKey = useCallback((event: unknown): string | null => { + if (!event || typeof event !== "object") return null; + const eventWithNative = event as { nativeEvent?: unknown; key?: unknown }; + if (typeof eventWithNative.key === "string") return eventWithNative.key; + const nativeEvent = eventWithNative.nativeEvent as { key?: unknown } | undefined; + return typeof nativeEvent?.key === "string" ? nativeEvent.key : null; + }, []); + + const preventWebDefault = useCallback((event: unknown) => { + if (!event || typeof event !== "object") return; + const candidate = event as { preventDefault?: unknown }; + if (typeof candidate.preventDefault === "function") { + candidate.preventDefault(); + } + }, []); + + const handleKeyDown = useCallback( + (event: unknown) => { + if (Platform.OS !== "web") return; + const key = getWebKey(event); + if (key === "Enter" || key === " ") { + preventWebDefault(event); + onPress(); + } + }, + [getWebKey, onPress, preventWebDefault] + ); + return ( { + if (!event || typeof event !== "object") return null; + const eventWithNative = event as { nativeEvent?: unknown; key?: unknown }; + if (typeof eventWithNative.key === "string") return eventWithNative.key; + const nativeEvent = eventWithNative.nativeEvent as { key?: unknown } | undefined; + return typeof nativeEvent?.key === "string" ? nativeEvent.key : null; + }, []); + + const preventWebDefault = useCallback((event: unknown) => { + if (!event || typeof event !== "object") return; + const candidate = event as { preventDefault?: unknown }; + if (typeof candidate.preventDefault === "function") { + candidate.preventDefault(); + } + }, []); + + const handleKeyDown = useCallback( + (event: unknown) => { + if (Platform.OS !== "web") return; + const key = getWebKey(event); + if (key === "Enter" || key === " ") { + preventWebDefault(event); + onPress(); + } + }, + [getWebKey, onPress, preventWebDefault] + ); + return ( diff --git a/packages/app/src/components/ui/combobox-keyboard.test.ts b/packages/app/src/components/ui/combobox-keyboard.test.ts new file mode 100644 index 000000000..62acc5180 --- /dev/null +++ b/packages/app/src/components/ui/combobox-keyboard.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; + +import { getNextActiveIndex } from "./combobox-keyboard"; + +describe("getNextActiveIndex", () => { + it("returns -1 when itemCount is 0", () => { + expect( + getNextActiveIndex({ currentIndex: 0, itemCount: 0, key: "ArrowDown" }) + ).toBe(-1); + }); + + it("starts at 0 on ArrowDown when no active item", () => { + expect( + getNextActiveIndex({ currentIndex: -1, itemCount: 3, key: "ArrowDown" }) + ).toBe(0); + }); + + it("starts at last on ArrowUp when no active item", () => { + expect( + getNextActiveIndex({ currentIndex: -1, itemCount: 3, key: "ArrowUp" }) + ).toBe(2); + }); + + it("wraps around on ArrowDown and ArrowUp", () => { + expect( + getNextActiveIndex({ currentIndex: 2, itemCount: 3, key: "ArrowDown" }) + ).toBe(0); + expect( + getNextActiveIndex({ currentIndex: 0, itemCount: 3, key: "ArrowUp" }) + ).toBe(2); + }); +}); + diff --git a/packages/app/src/components/ui/combobox-keyboard.ts b/packages/app/src/components/ui/combobox-keyboard.ts new file mode 100644 index 000000000..6debe2c33 --- /dev/null +++ b/packages/app/src/components/ui/combobox-keyboard.ts @@ -0,0 +1,19 @@ +export function getNextActiveIndex(args: { + currentIndex: number; + itemCount: number; + key: "ArrowDown" | "ArrowUp"; +}): number { + const { currentIndex, itemCount, key } = args; + + if (itemCount <= 0) return -1; + + if (currentIndex < 0) { + return key === "ArrowDown" ? 0 : itemCount - 1; + } + + const normalizedCurrent = currentIndex % itemCount; + return key === "ArrowDown" + ? (normalizedCurrent + 1) % itemCount + : (normalizedCurrent - 1 + itemCount) % itemCount; +} + diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index 777964caf..6d529a762 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -27,6 +27,7 @@ import { size as floatingSize, useFloating, } from "@floating-ui/react-native"; +import { getNextActiveIndex } from "./combobox-keyboard"; const IS_WEB = Platform.OS === "web"; @@ -113,6 +114,7 @@ export interface ComboboxItemProps { label: string; description?: string; selected?: boolean; + active?: boolean; onPress: () => void; testID?: string; } @@ -121,6 +123,7 @@ export function ComboboxItem({ label, description, selected, + active, onPress, testID, }: ComboboxItemProps): ReactElement { @@ -132,6 +135,7 @@ export function ComboboxItem({ style={({ pressed }) => [ styles.comboboxItem, pressed && styles.comboboxItemPressed, + active && styles.comboboxItemActive, ]} > @@ -174,6 +178,7 @@ export function Combobox({ const [availableSize, setAvailableSize] = useState<{ width?: number; height?: number } | null>(null); const [referenceWidth, setReferenceWidth] = useState(null); const [searchQuery, setSearchQuery] = useState(""); + const [activeIndex, setActiveIndex] = useState(-1); const isControlled = typeof open === "boolean"; const [internalOpen, setInternalOpen] = useState(false); @@ -294,7 +299,6 @@ export function Combobox({ ); }, [options, normalizedSearch]); - const hasMatches = filteredOptions.length > 0; const sanitizedSearchValue = searchQuery.trim(); const showCustomOption = allowCustomValue && @@ -305,6 +309,56 @@ export function Combobox({ opt.label.toLowerCase() === sanitizedSearchValue.toLowerCase() ); + const visibleOptions = useMemo(() => { + const next: Array<{ + id: string; + label: string; + description?: string; + }> = []; + + if (showCustomOption) { + next.push({ + id: sanitizedSearchValue, + label: `${customValuePrefix} "${sanitizedSearchValue}"`, + description: customValueDescription, + }); + } + + for (const opt of filteredOptions) { + next.push({ + id: opt.id, + label: opt.label, + description: opt.description, + }); + } + + return next; + }, [ + customValueDescription, + customValuePrefix, + filteredOptions, + sanitizedSearchValue, + showCustomOption, + ]); + + useEffect(() => { + if (!isOpen) return; + if (!IS_WEB && isMobile) return; + + if (visibleOptions.length === 0) { + setActiveIndex(-1); + return; + } + + if (normalizedSearch) { + setActiveIndex(0); + return; + } + + const selectedIndex = visibleOptions.findIndex((opt) => opt.id === value); + setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0); + }, [isMobile, isOpen, normalizedSearch, value, visibleOptions]); + const handleSelect = useCallback( (id: string) => { onSelect(id); @@ -319,6 +373,58 @@ export function Combobox({ } }, [handleSelect, sanitizedSearchValue, showCustomOption]); + const handleDesktopKey = useCallback( + (key: "ArrowDown" | "ArrowUp" | "Enter" | "Escape", event?: KeyboardEvent) => { + if (!isOpen) return; + if (!IS_WEB && isMobile) return; + + if (key === "ArrowDown" || key === "ArrowUp") { + event?.preventDefault(); + setActiveIndex((currentIndex) => + getNextActiveIndex({ + currentIndex, + itemCount: visibleOptions.length, + key, + }) + ); + return; + } + + if (key === "Enter") { + if (visibleOptions.length === 0) return; + event?.preventDefault(); + const index = + activeIndex >= 0 && activeIndex < visibleOptions.length ? activeIndex : 0; + handleSelect(visibleOptions[index]!.id); + return; + } + + if (key === "Escape") { + event?.preventDefault(); + handleClose(); + } + }, + [activeIndex, handleClose, handleSelect, isMobile, isOpen, visibleOptions] + ); + + useEffect(() => { + if (!IS_WEB || !isOpen) return; + + const handler = (event: KeyboardEvent) => { + const key = event.key; + if (key !== "ArrowDown" && key !== "ArrowUp" && key !== "Enter" && key !== "Escape") { + return; + } + handleDesktopKey(key, event); + }; + + // react-native-web's TextInput can stop propagation on key events, so listen in capture phase. + window.addEventListener("keydown", handler, true); + return () => { + window.removeEventListener("keydown", handler, true); + }; + }, [handleDesktopKey, isOpen]); + const searchInput = ( - {showCustomOption ? ( - handleSelect(sanitizedSearchValue)} - /> - ) : null} - {hasMatches ? ( - filteredOptions.map((opt) => ( + {visibleOptions.length > 0 ? ( + visibleOptions.map((opt, index) => ( handleSelect(opt.id)} /> )) - ) : !showCustomOption ? ( + ) : ( {emptyText} - ) : null} + )} ); @@ -412,6 +512,7 @@ export function Combobox({ width: referenceWidth ?? undefined, }, floatingStyles, + referenceWidth === null ? { opacity: 0 } : null, typeof availableSize?.height === "number" ? { maxHeight: Math.min(availableSize.height, 400) } : null, typeof availableSize?.width === "number" ? { maxWidth: availableSize.width } : null, ]} @@ -475,6 +576,9 @@ const styles = StyleSheet.create((theme) => ({ comboboxItemPressed: { backgroundColor: theme.colors.surface2, }, + comboboxItemActive: { + backgroundColor: theme.colors.surface2, + }, comboboxItemCheckSlot: { width: 16, alignItems: "center", diff --git a/packages/server/src/server/agent/agent-self-id-mcp.ts b/packages/server/src/server/agent/agent-self-id-mcp.ts index 5572b574a..deb39e75a 100644 --- a/packages/server/src/server/agent/agent-self-id-mcp.ts +++ b/packages/server/src/server/agent/agent-self-id-mcp.ts @@ -13,6 +13,7 @@ */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { basename } from "path"; import { z } from "zod"; import { ensureValidJson } from "../json-utils.js"; import type { Logger } from "pino"; @@ -25,6 +26,7 @@ import { import { NotGitRepoError, renameCurrentBranch, + getCheckoutStatus, } from "../../utils/checkout-git.js"; export interface AgentSelfIdMcpOptions { @@ -165,6 +167,21 @@ export async function createAgentSelfIdMcpServer( ); } + const status = await getCheckoutStatus(agent.cwd, { + paseoHome: options.paseoHome, + }); + if (!status.isGit || !status.currentBranch) { + throw new AgentSelfIdToolError("NOT_GIT_REPO", "Unable to determine current branch"); + } + + const worktreeDirName = basename(status.repoRoot); + if (status.currentBranch !== worktreeDirName) { + throw new AgentSelfIdToolError( + "NOT_ALLOWED", + "Branch has already been renamed. Use git commands for subsequent renames." + ); + } + childLogger.debug({ branch: name }, "Renaming branch"); const result = await renameCurrentBranch(agent.cwd, name); if (result.currentBranch !== name) { diff --git a/packages/server/src/server/agent/agent-self-identification.e2e.test.ts b/packages/server/src/server/agent/agent-self-identification.e2e.test.ts index a34f08f0d..87f8bdbc2 100644 --- a/packages/server/src/server/agent/agent-self-identification.e2e.test.ts +++ b/packages/server/src/server/agent/agent-self-identification.e2e.test.ts @@ -224,6 +224,72 @@ describe("self-identification MCP tools", () => { } }); + test("set_branch rejects subsequent renames after initial rename", async () => { + const repoDir = mkdtempSync(path.join(tmpdir(), "paseo-self-ident-")); + const paseoHome = path.join(repoDir, "paseo-home"); + + try { + initGitRepo(repoDir); + const worktree = await createWorktree({ + branchName: "initial-branch", + cwd: repoDir, + baseBranch: "main", + worktreeSlug: "initial-branch", + paseoHome, + }); + + const storagePath = path.join(repoDir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const manager = new AgentManager({ + clients: { codex: new TestAgentClient() }, + registry: storage, + logger, + idFactory: () => "agent-subsequent-rename", + }); + + const agent = await manager.createAgent({ + provider: "codex", + cwd: worktree.worktreePath, + }); + + const server = await createAgentSelfIdMcpServer({ + agentManager: manager, + paseoHome, + callerAgentId: agent.id, + logger, + }); + const tool = (server as any)._registeredTools["set_branch"]; + + // First rename should succeed + await tool.callback({ name: "first-rename" }); + + const branchAfterFirst = execSync("git rev-parse --abbrev-ref HEAD", { + cwd: worktree.worktreePath, + stdio: "pipe", + }) + .toString() + .trim(); + expect(branchAfterFirst).toBe("first-rename"); + + // Second rename should fail + await expect(tool.callback({ name: "second-rename" })).rejects.toMatchObject({ + code: "NOT_ALLOWED", + message: expect.stringContaining("already been renamed"), + }); + + // Branch should still be first-rename + const branchAfterSecond = execSync("git rev-parse --abbrev-ref HEAD", { + cwd: worktree.worktreePath, + stdio: "pipe", + }) + .toString() + .trim(); + expect(branchAfterSecond).toBe("first-rename"); + } finally { + rmSync(repoDir, { recursive: true, force: true }); + } + }); + test("set_branch rejects non-Paseo checkouts", async () => { const repoDir = mkdtempSync(path.join(tmpdir(), "paseo-self-ident-")); diff --git a/packages/website/public/favicon.ico b/packages/website/public/favicon.ico index 170789c6b52e86cecf5cb05d64fb6bee012bdec3..936d6640ae6f330c5599e68d94cb6275d44bf86d 100644 GIT binary patch literal 15086 zcmeI3b&M2E7QhF0cVFBk1b2521PBt~fP+Bb4_pJs;oL!R4<6iuySuv_?(R-q|LUmz zs(YtrdS-W#)#EAR@pwW)e*8}8@%+bl zm(Zwb?-14F8D@0IVe~PEcv=~S_Y$)Fuk!IXz0o$tDCdpx-YDWXDewYM@RrUf-k_cF z@`F)+7oMH?U@m{NO`!E3z_)!qtT&LxD8JKQvGeKlo4vw*|F=<``uy(iIS&_uG*uQ_j`1b9au*1`*PsNZSLqx1t zv3|xp-UYJu1qu`p=gyt8v+VWv)vH%U&YU@a#vEMHs9(Rn`1tXoS6<)0e=oj&{~oH1 zZ&9K|30L?7S;s*8mMd3IeERgs!r!f1w?x;jT}4Pph~$rbRiQ!!apJ@YOF!mTy?XUu zHirZW5{Nr@?pS!5I(4du88fC=o?^$2E!M4DXX*dp!-r6Fh#Wbxm@;LGxOVND7&>&Q zh#EDjSA2o4wY~qaVZ$uEOq({%FCBQ9HEWh-9CN5rrHVPWeEIU0_N`mDii8Ohn(cNz zg82;}j~_oa>9A+w#*OQWMx;oQ#DobG%&|PYe*Ib$ELc#E&yXR5c>DIPrTy&Lvm#Zh zRIc#5nNPK9)hzhiw{IU1Zsx%E(&7I7`{tS9zIgE9f!-{xU%xIgWy%x~9jCdB968d& z{rU4}kvMVUfbp`==g*&S;^E=Kg$pJwz5~B?xOwxYNS{7^z_jf1SiE?#iT28sD~|Y> zSLe>11?PIwq)DPdg9ZY=6~5@vql;a;cA2<%*t2JkDFc!wO)8EYIbvytURd|i&Ye3= zoO}1~HTh9|QP-(c$8tv00LJQPg*jfjbV)VKdeNdqCeC>A;)z3t4q4g{95^7DyG}2V z=anm0n)oGzDM3ezn{UsZ*yGoN*oM z)vIU0;ghZ>ZYz z+I}xEhV!cQZtmh|&z_m6HgDeS6_1Y7+jG{eSuOk3MEK*OQ>RW6FZ4Kzx{k{I>U3u` zY0|`wO8#EEcC9H_XV0E(_ThVZIRUm2vVX~v zC1w|NaW}j=_36{6k5?VvBw%|$_d$Pf!e{5-Za?Vf7u4vqPPp8@U$bV7jmywK*~8pv z{&2Qw)5bEmdGqGt(W6I}cj^Z+wrtt5esTJu$y(|%FusPp9#@a^=$57k1lLjmwxZqd-2{;qKkL0vxK}uc&wL-fgmlzKBkm zB}*3RCvVoQnLxio*RsR*?c2lb3ki?;;xF?Sv#|54eq*O&KkB}q@ZcjXIPB*@@v7I% z5#JZSwXo1~IX2+fBmRstXU^y>OWOuNApUC3fJ@q8?MM75==fc_bm>HyGGzolo$&U3 zyPQAqyRk0&hIOqXV*I}h-b0M?@eiB>@#_pm0ooWPBGxA&asFL${@0%l#^ayx9a#7F zf3aRv}w~yy8zu;`3VmmJSg*<6fNQcuHD z3b@E0z%P6I_HDT~hYue%jLEvSi7CdB9(b9|OQfc8c|4XX9@q1_lsE#(rj< z?%lg5-(9?TQN{q>*yL)Bz@dB+_(s)xRg<3*Cr%vG&S!tBxZ>*7tEFvFyLN3E_ks!X zo#080_g53+10OLSJwJza;rI0E)8&5X)~%cLkukT99XpCwuU^Uhg8!R6fL~turSQ>_ zSK^O0?daX)K5))dT$5Neer-Jtz!@OVhP?`J^g@^dX;K76SVKVoD96Mws!2VVJebwY7AoslivY0(!&m)9B`w(Egl%I;T z!G57|R(#Rb+s2I>Cvh=%WH4)s9gTcUk|aqeJe&oU8^hkg9w+X`cYOZ*xr|M@^-mHr zM}`7p$Bvcj41VUQ^Qb>(%a%>RD{>q8KpXP2Ql(0wtRWA$$Myccp4s=xw~UPQ)mOE# z&+(CP-Wadm2UEi{YsMKz=kSLE|D)oU+!SPzzcIerIcwzRamV4`^3|7E1+pI?7pPF7 zLO%QZ!ll}3)TkkK57yGxJxYvtmc!I6jFPtCNz$tEwAB;QMz2Dyaz~Qu!QFz6No)*V0lgW0&B}}rjO?T|rA=i`dg6`)nwn%)B@5kM(`%%y{uvz)O*ctF- zr_FEfd;9}>8}|=7DtD7V-G{tR{r?rv_rb^d+jVN4hI%IN58INxgKj_$!M=U_{KbIH zJ;+{Nuwa4wpCM>+cRS7X2TgU3%sI2xoE`S8w|%B~{ZZ>@(B#9`$Hc9@S9CeQVS@hx DG;4!I literal 15086 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 diff --git a/packages/website/public/favicon.svg b/packages/website/public/favicon.svg index 5d1dedaef..0ef308b97 100644 --- a/packages/website/public/favicon.svg +++ b/packages/website/public/favicon.svg @@ -1,11 +1,4 @@ - - - - - - - - - - + + +