Download src/components/features/command-menu/command-menu.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 14.4 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/command-menu/command-menu.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/src/components/features/command-menu/command-menu.tsx
-
curl -L -o command-menu.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/src/components/features/command-menu/command-menu.tsx
14.4 kB
| import React from "react"; | |
| import { createPortal } from "react-dom"; | |
| import { Search, X } from "lucide-react"; | |
| import { useTranslation } from "react-i18next"; | |
| import { I18nKey } from "#/i18n/declaration"; | |
| import { useNavigation } from "#/context/navigation-context"; | |
| import { useCommandMenuStore } from "#/stores/command-menu-store"; | |
| import { useSidebarStore } from "#/stores/sidebar-store"; | |
| import { cn } from "#/utils/utils"; | |
| import { | |
| COMMAND_MENU_GROUP_LABELS, | |
| COMMAND_MENU_GROUP_ORDER, | |
| type CommandMenuItemDefinition, | |
| commandMenuItemCopy, | |
| createCommandMenuItems, | |
| } from "./command-menu-items"; | |
| const COMMAND_MENU_SEARCH_INPUT_ID = "command-menu-search"; | |
| const COMMAND_MENU_LISTBOX_ID = "command-menu-results"; | |
| const COMMAND_MENU_OPTION_ID_PREFIX = "command-menu-option"; | |
| const COMMAND_MENU_TEST_ID = "command-menu"; | |
| const COMMAND_MENU_SHORTCUT_KEY = "k"; | |
| const COMMAND_MENU_ARROW_DOWN_KEY = "ArrowDown"; | |
| const COMMAND_MENU_ARROW_UP_KEY = "ArrowUp"; | |
| const COMMAND_MENU_ENTER_KEY = "Enter"; | |
| const COMMAND_MENU_ESCAPE_KEY = "Escape"; | |
| const EMPTY_QUERY = ""; | |
| const EMPTY_RESULTS_ACTIVE_INDEX = -1; | |
| function getOptionId(item: CommandMenuItemDefinition) { | |
| return `${COMMAND_MENU_OPTION_ID_PREFIX}-${item.id}`; | |
| } | |
| function matchesQuery({ | |
| item, | |
| query, | |
| translate, | |
| }: { | |
| item: CommandMenuItemDefinition; | |
| query: string; | |
| translate: (key: I18nKey) => string; | |
| }) { | |
| const terms = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean); | |
| if (terms.length === 0) { | |
| return true; | |
| } | |
| const searchableText = [ | |
| commandMenuItemCopy(item.title, item.titleKey, translate), | |
| commandMenuItemCopy(item.description, item.descriptionKey, translate), | |
| commandMenuItemCopy(item.keywords, item.keywordsKey, translate), | |
| ] | |
| .join(" ") | |
| .toLocaleLowerCase(); | |
| return terms.every((term) => searchableText.includes(term)); | |
| } | |
| export function CommandMenu() { | |
| const { t } = useTranslation("openhands"); | |
| const { navigate } = useNavigation(); | |
| const isOpen = useCommandMenuStore((state) => state.isOpen); | |
| const open = useCommandMenuStore((state) => state.open); | |
| const close = useCommandMenuStore((state) => state.close); | |
| const [query, setQuery] = React.useState(EMPTY_QUERY); | |
| const [activeIndex, setActiveIndex] = React.useState(0); | |
| const inputRef = React.useRef<HTMLInputElement>(null); | |
| const optionRefs = React.useRef(new Map<string, HTMLElement>()); | |
| React.useEffect(() => { | |
| const onKeyDown = (event: KeyboardEvent) => { | |
| if ( | |
| (event.metaKey || event.ctrlKey) && | |
| event.key.toLocaleLowerCase() === COMMAND_MENU_SHORTCUT_KEY | |
| ) { | |
| event.preventDefault(); | |
| open(); | |
| } | |
| }; | |
| window.addEventListener("keydown", onKeyDown); | |
| return () => window.removeEventListener("keydown", onKeyDown); | |
| }, [open]); | |
| React.useEffect(() => { | |
| if (!isOpen) { | |
| setQuery(EMPTY_QUERY); | |
| setActiveIndex(0); | |
| optionRefs.current.clear(); | |
| return undefined; | |
| } | |
| const frame = requestAnimationFrame(() => inputRef.current?.focus()); | |
| return () => cancelAnimationFrame(frame); | |
| }, [isOpen]); | |
| const items = React.useMemo( | |
| () => | |
| createCommandMenuItems({ | |
| toggleSidebar: () => useSidebarStore.getState().toggleCollapsed(), | |
| }), | |
| [], | |
| ); | |
| const filteredItems = React.useMemo( | |
| () => | |
| items.filter((item) => | |
| matchesQuery({ item, query, translate: (key) => t(key) }), | |
| ), | |
| [items, query, t], | |
| ); | |
| React.useEffect(() => { | |
| setActiveIndex((currentIndex) => { | |
| if (filteredItems.length === 0) { | |
| return EMPTY_RESULTS_ACTIVE_INDEX; | |
| } | |
| return Math.min(Math.max(currentIndex, 0), filteredItems.length - 1); | |
| }); | |
| }, [filteredItems.length]); | |
| React.useEffect(() => { | |
| const activeItem = filteredItems[activeIndex]; | |
| if (!activeItem) { | |
| return; | |
| } | |
| const activeNode = optionRefs.current.get(activeItem.id); | |
| if (typeof activeNode?.scrollIntoView === "function") { | |
| activeNode.scrollIntoView({ | |
| block: "nearest", | |
| }); | |
| } | |
| }, [activeIndex, filteredItems]); | |
| const runItem = React.useCallback( | |
| (item: CommandMenuItemDefinition | undefined) => { | |
| if (!item) { | |
| return; | |
| } | |
| close(); | |
| if (item.to) { | |
| navigate(item.to); | |
| return; | |
| } | |
| item.perform?.(); | |
| }, | |
| [close, navigate], | |
| ); | |
| const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { | |
| if (event.key === COMMAND_MENU_ARROW_DOWN_KEY) { | |
| event.preventDefault(); | |
| setActiveIndex((index) => | |
| filteredItems.length === 0 ? index : (index + 1) % filteredItems.length, | |
| ); | |
| return; | |
| } | |
| if (event.key === COMMAND_MENU_ARROW_UP_KEY) { | |
| event.preventDefault(); | |
| setActiveIndex((index) => | |
| filteredItems.length === 0 | |
| ? index | |
| : (index - 1 + filteredItems.length) % filteredItems.length, | |
| ); | |
| return; | |
| } | |
| if (event.key === COMMAND_MENU_ENTER_KEY) { | |
| event.preventDefault(); | |
| runItem(filteredItems[activeIndex]); | |
| return; | |
| } | |
| if (event.key === COMMAND_MENU_ESCAPE_KEY) { | |
| event.preventDefault(); | |
| close(); | |
| } | |
| }; | |
| if (!isOpen || typeof document === "undefined") { | |
| return null; | |
| } | |
| const activeItem = filteredItems[activeIndex]; | |
| return createPortal( | |
| <div | |
| className="fixed inset-0 z-[70] flex items-start justify-center px-3 pt-[10vh] sm:px-6" | |
| data-testid={COMMAND_MENU_TEST_ID} | |
| role="dialog" | |
| aria-modal="true" | |
| aria-label={t(I18nKey.COMMAND_MENU$ARIA_LABEL)} | |
| > | |
| <button | |
| type="button" | |
| className="absolute inset-0 cursor-default bg-black/65 backdrop-blur-[2px]" | |
| aria-label={t(I18nKey.COMMAND_MENU$CLOSE_LABEL)} | |
| onClick={close} | |
| /> | |
| <div | |
| className={cn( | |
| "relative flex max-h-[min(720px,78vh)] w-full max-w-2xl flex-col overflow-hidden rounded-2xl", | |
| "border border-[var(--oh-border)] bg-[var(--oh-surface)]", | |
| "shadow-[0_24px_90px_rgba(0,0,0,0.52),0_0_0_1px_rgba(255,255,255,0.03)_inset]", | |
| )} | |
| > | |
| <div className="relative flex items-center gap-3 border-b border-[var(--oh-border)] px-4 py-3"> | |
| <Search className="size-5 shrink-0 text-[var(--oh-text-dim)]" /> | |
| <input | |
| ref={inputRef} | |
| id={COMMAND_MENU_SEARCH_INPUT_ID} | |
| className="h-11 min-w-0 flex-1 bg-transparent text-base text-white outline-none placeholder:text-[var(--oh-text-dim)]" | |
| placeholder={t(I18nKey.COMMAND_MENU$PLACEHOLDER)} | |
| aria-label={t(I18nKey.COMMAND_MENU$SEARCH_LABEL)} | |
| role="combobox" | |
| aria-expanded="true" | |
| aria-controls={COMMAND_MENU_LISTBOX_ID} | |
| aria-activedescendant={ | |
| activeItem ? getOptionId(activeItem) : undefined | |
| } | |
| value={query} | |
| onChange={(event) => setQuery(event.target.value)} | |
| onKeyDown={handleInputKeyDown} | |
| /> | |
| {query ? ( | |
| <button | |
| type="button" | |
| className="inline-flex size-8 items-center justify-center rounded-lg text-[var(--oh-muted)] hover:bg-[var(--oh-surface-raised)] hover:text-white" | |
| aria-label={t(I18nKey.COMMAND_MENU$CLEAR_SEARCH_LABEL)} | |
| onClick={() => { | |
| setQuery(EMPTY_QUERY); | |
| inputRef.current?.focus(); | |
| }} | |
| > | |
| <X className="size-4" /> | |
| </button> | |
| ) : null} | |
| <kbd className="hidden rounded-md border border-[var(--oh-border)] bg-black/25 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--oh-text-dim)] sm:inline-flex"> | |
| {t(I18nKey.COMMAND_MENU$SHORTCUT)} | |
| </kbd> | |
| </div> | |
| <div | |
| id={COMMAND_MENU_LISTBOX_ID} | |
| role="listbox" | |
| className="relative min-h-0 flex-1 overflow-y-auto px-2 py-2 custom-scrollbar" | |
| > | |
| {filteredItems.length === 0 ? ( | |
| <div className="flex flex-col items-center justify-center gap-2 px-6 py-16 text-center"> | |
| <div className="flex size-11 items-center justify-center rounded-2xl border border-dashed border-[var(--oh-border)] text-[var(--oh-text-dim)]"> | |
| <Search className="size-5" /> | |
| </div> | |
| <p className="text-sm font-medium text-white"> | |
| {t(I18nKey.COMMAND_MENU$NO_RESULTS_TITLE)} | |
| </p> | |
| <p className="max-w-sm text-xs leading-5 text-[var(--oh-muted)]"> | |
| {t(I18nKey.COMMAND_MENU$NO_RESULTS_DESCRIPTION)} | |
| </p> | |
| </div> | |
| ) : ( | |
| COMMAND_MENU_GROUP_ORDER.map((groupId) => { | |
| const groupItems = filteredItems.filter( | |
| (item) => item.group === groupId, | |
| ); | |
| if (groupItems.length === 0) { | |
| return null; | |
| } | |
| return ( | |
| <section key={groupId} className="py-1"> | |
| <div className="px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--oh-text-dim)]"> | |
| {t(COMMAND_MENU_GROUP_LABELS[groupId])} | |
| </div> | |
| <div className="space-y-1"> | |
| {groupItems.map((item) => { | |
| const itemIndex = filteredItems.indexOf(item); | |
| const isActive = itemIndex === activeIndex; | |
| const to = item.to; | |
| const assignRef = (node: HTMLElement | null) => { | |
| if (node) { | |
| optionRefs.current.set(item.id, node); | |
| } else { | |
| optionRefs.current.delete(item.id); | |
| } | |
| }; | |
| const optionClassName = cn( | |
| "group flex w-full items-center gap-3 rounded-xl px-3 py-2.5 text-left transition-colors duration-150", | |
| isActive | |
| ? "bg-white/[0.09] text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)_inset]" | |
| : "text-[var(--oh-muted)] hover:bg-white/[0.05] hover:text-white", | |
| ); | |
| const content = ( | |
| <> | |
| <span | |
| className={cn( | |
| "flex size-9 shrink-0 items-center justify-center rounded-lg border transition-colors duration-150", | |
| isActive | |
| ? "border-[var(--oh-accent)] bg-[var(--oh-accent)]/15 text-white" | |
| : "border-[var(--oh-border)] bg-black/15 text-[var(--oh-text-dim)] group-hover:text-white", | |
| )} | |
| aria-hidden="true" | |
| > | |
| {item.icon} | |
| </span> | |
| <span className="min-w-0 flex-1"> | |
| <span className="block truncate text-sm font-medium text-current"> | |
| {commandMenuItemCopy( | |
| item.title, | |
| item.titleKey, | |
| t, | |
| )} | |
| </span> | |
| <span className="mt-0.5 block truncate text-xs text-[var(--oh-text-dim)]"> | |
| {commandMenuItemCopy( | |
| item.description, | |
| item.descriptionKey, | |
| t, | |
| )} | |
| </span> | |
| </span> | |
| <span className="hidden shrink-0 rounded-md border border-[var(--oh-border)] px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-[var(--oh-text-dim)] sm:inline-flex"> | |
| {to | |
| ? t(I18nKey.COMMAND_MENU$GO_HINT) | |
| : t(I18nKey.COMMAND_MENU$RUN_HINT)} | |
| </span> | |
| </> | |
| ); | |
| if (to) { | |
| return ( | |
| <a | |
| key={item.id} | |
| ref={assignRef} | |
| id={getOptionId(item)} | |
| href={to} | |
| role="option" | |
| aria-selected={isActive} | |
| onMouseEnter={() => setActiveIndex(itemIndex)} | |
| onClick={(event) => { | |
| if ( | |
| event.metaKey || | |
| event.ctrlKey || | |
| event.shiftKey || | |
| event.altKey | |
| ) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| runItem(item); | |
| }} | |
| className={optionClassName} | |
| > | |
| {content} | |
| </a> | |
| ); | |
| } | |
| return ( | |
| <button | |
| key={item.id} | |
| ref={assignRef} | |
| id={getOptionId(item)} | |
| type="button" | |
| role="option" | |
| aria-selected={isActive} | |
| onMouseEnter={() => setActiveIndex(itemIndex)} | |
| onClick={() => runItem(item)} | |
| className={optionClassName} | |
| > | |
| {content} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </section> | |
| ); | |
| }) | |
| )} | |
| </div> | |
| <div className="border-t border-[var(--oh-border)] px-4 py-2.5 text-[11px] text-[var(--oh-text-dim)]"> | |
| {t(I18nKey.COMMAND_MENU$FOOTER_HINT)} | |
| </div> | |
| </div> | |
| </div>, | |
| document.body, | |
| ); | |
| } | |