From 830c83dfca2afe4f06a00ee4ec6832275c3fda1e Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:15:59 -0700 Subject: [PATCH 01/23] Simple Command Tool --- fission/src/Synthesis.tsx | 2 + fission/src/ui/components/CommandPalette.tsx | 245 +++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 fission/src/ui/components/CommandPalette.tsx diff --git a/fission/src/Synthesis.tsx b/fission/src/Synthesis.tsx index 1fc7414429..b6032859ab 100644 --- a/fission/src/Synthesis.tsx +++ b/fission/src/Synthesis.tsx @@ -18,6 +18,7 @@ import MainMenuModal from "./ui/modals/MainMenuModal.tsx" import { StateProvider } from "./ui/StateProvider.tsx" import { ThemeProvider } from "./ui/ThemeProvider.tsx" import { UIProvider } from "./ui/UIProvider.tsx" +import CommandPalette from "@/ui/components/CommandPalette.tsx" function Synthesis() { const [consentPopupDisable, setConsentPopupDisable] = useState(true) @@ -77,6 +78,7 @@ function Synthesis() { + diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx new file mode 100644 index 0000000000..6ff1c4d040 --- /dev/null +++ b/fission/src/ui/components/CommandPalette.tsx @@ -0,0 +1,245 @@ +import { Box, List, ListItemButton, ListItemText, Paper, Stack, TextField } from "@mui/material" +import type React from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import DebugPanel from "@/ui/panels/DebugPanel" +import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel" +import SettingsModal from "@/ui/modals/configuring/SettingsModal" +import type { PanelImplProps } from "@/ui/components/Panel" +import type { ModalImplProps } from "@/ui/components/Modal" +import { useUIContext } from "@/ui/helpers/UIProviderHelpers" +import { useStateContext } from "@/ui/helpers/StateProviderHelpers" +import World from "@/systems/World" +import type { ConfigurationType } from "@/ui/panels/configuring/assembly-config/ConfigTypes" + +type CommandDefinition = { + id: string + label: string + description?: string + keywords?: string[] + perform: () => void +} + +function isTextInputTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false + const tagName = target.tagName.toLowerCase() + const editable = target.getAttribute("contenteditable") + return tagName === "input" || tagName === "textarea" || editable === "" || editable === "true" +} + +const CommandPalette: React.FC = () => { + const { addToast, openPanel, openModal } = useUIContext() + const { isMainMenuOpen } = useStateContext() + + const [isOpen, setIsOpen] = useState(false) + const [query, setQuery] = useState("") + const [activeIndex, setActiveIndex] = useState(0) + const inputRef = useRef(null) + + const openImportPanel = useCallback( + (configurationType: ConfigurationType) => { + openPanel( + ImportMirabufPanel as unknown as React.FunctionComponent< + PanelImplProps + >, + { configurationType } + ) + }, + [openPanel] + ) + + const commands = useMemo( + () => [ + { + id: "toast-info", + label: "Toast: Show info", + description: "Show an info toast in the bottom-right.", + keywords: ["notification", "snackbar", "message"], + perform: () => addToast("info", "Hello from Command Palette"), + }, + { + id: "toast-success", + label: "Toast: Show success", + description: "Show a success toast in the bottom-right.", + keywords: ["notification", "snackbar", "message"], + perform: () => addToast("success", "Success!"), + }, + { + id: "open-debug-panel", + label: "Open Debug Panel", + description: "Open the Debug tools panel.", + keywords: ["panel", "debug"], + perform: () => + openPanel(DebugPanel as unknown as React.FunctionComponent>, undefined), + }, + + { + id: "spawn-asset-robots", + label: "Spawn Asset (Robots)", + description: "Open asset spawn panel scoped to robots.", + keywords: ["spawn", "asset", "robot", "import", "mirabuf"], + perform: () => openImportPanel("ROBOTS"), + }, + { + id: "spawn-asset-fields", + label: "Spawn Asset (Fields)", + description: "Open asset spawn panel scoped to fields.", + keywords: ["spawn", "asset", "field", "import", "mirabuf"], + perform: () => openImportPanel("FIELDS"), + }, + { + id: "open-settings", + label: "Open Settings", + description: "Open the Settings modal.", + keywords: ["settings", "preferences", "config"], + perform: () => + openModal( + SettingsModal as unknown as React.FunctionComponent>, + undefined + ), + }, + ], + [addToast, openPanel, openModal, openImportPanel] + ) + + const filtered = useMemo(() => { + const q = query.trim().toLowerCase() + if (q.length === 0) return commands + + type Scored = { cmd: CommandDefinition; score: number; idx: number } + const scored: Scored[] = [] + for (let i = 0; i < commands.length; i++) { + const c = commands[i] + const label = c.label.toLowerCase() + const keywords = (c.keywords ?? []).map(k => k.toLowerCase()) + let score = 0 + if (label === q) score += 1000 + if (label.startsWith(q)) score += 500 + if (label.includes(q)) score += 200 + if (keywords.includes(q)) score += 300 + if (keywords.some(k => k.startsWith(q))) score += 150 + if (keywords.some(k => k.includes(q))) score += 50 + if (score > 0) { + scored.push({ cmd: c, score, idx: i }) + } + } + scored.sort((a, b) => (a.score === b.score ? a.idx - b.idx : a.score - b.score)) + return scored.map(s => s.cmd) + }, [commands, query]) + + const visible = useMemo(() => filtered.slice(0, 5), [filtered]) + + const execute = useCallback( + (index: number) => { + const cmd = visible[index] + cmd.perform() + setIsOpen(false) + setQuery("") + setActiveIndex(0) + }, + [visible] + ) + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "/") { + if (isTextInputTarget(e.target)) return + if (!World.isAlive) return + if (isMainMenuOpen) return + e.preventDefault() + setIsOpen(true) + setTimeout(() => inputRef.current?.focus(), 0) + } else if (e.key === "Escape") { + if (isOpen) { + e.preventDefault() + setIsOpen(false) + setQuery("") + setActiveIndex(0) + } + } + } + window.addEventListener("keydown", onKeyDown) + return () => window.removeEventListener("keydown", onKeyDown) + }, [isOpen, isMainMenuOpen]) + + useEffect(() => { + if (isMainMenuOpen && isOpen) { + setIsOpen(false) + setQuery("") + setActiveIndex(0) + } + }, [isMainMenuOpen, isOpen]) + + useEffect(() => { + if (!isOpen) return + setActiveIndex(visible.length > 0 ? visible.length - 1 : 0) + }, [isOpen, visible.length]) + + const onInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault() + setActiveIndex(i => Math.min(i + 1, Math.max(visible.length - 1, 0))) + } else if (e.key === "ArrowUp") { + e.preventDefault() + setActiveIndex(i => Math.max(i - 1, 0)) + } else if (e.key === "Enter") { + e.preventDefault() + execute(activeIndex) + } else if (e.key === "Escape") { + e.preventDefault() + setIsOpen(false) + setQuery("") + setActiveIndex(0) + } + }, + [activeIndex, execute, visible.length] + ) + + if (!isOpen) return null + + return ( + + + + {visible.length > 0 && ( + + {visible.map((c, i) => ( + setActiveIndex(i)} + onClick={() => execute(i)} + > + + + ))} + + )} + { + setQuery(e.target.value) + }} + onKeyDown={onInputKeyDown} + /> + + + + ) +} + +export default CommandPalette From 40ffa015fbc605eb5583f67e6a8c4110ab580037 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:19:34 -0700 Subject: [PATCH 02/23] Toggles Drag Mode & Cleaner Code --- fission/src/ui/components/CommandPalette.tsx | 51 +++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 6ff1c4d040..540193c5d5 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -35,6 +35,17 @@ const CommandPalette: React.FC = () => { const [activeIndex, setActiveIndex] = useState(0) const inputRef = useRef(null) + const closePalette = useCallback(() => { + setIsOpen(false) + setQuery("") + setActiveIndex(0) + }, []) + + const openPalette = useCallback(() => { + setIsOpen(true) + setTimeout(() => inputRef.current?.focus(), 0) + }, []) + const openImportPanel = useCallback( (configurationType: ConfigurationType) => { openPanel( @@ -71,6 +82,19 @@ const CommandPalette: React.FC = () => { perform: () => openPanel(DebugPanel as unknown as React.FunctionComponent>, undefined), }, + { + id: "toggle-drag-mode", + label: "Toggle Drag Mode", + description: "Enable or disable drag mode.", + keywords: ["drag", "mode", "toggle", "move"], + perform: () => { + const dragSystem = World.dragModeSystem + if (!dragSystem) return + dragSystem.enabled = !dragSystem.enabled + const status = dragSystem.enabled ? "enabled" : "disabled" + addToast("info", "Drag Mode", `Drag mode has been ${status}`) + }, + }, { id: "spawn-asset-robots", @@ -132,11 +156,9 @@ const CommandPalette: React.FC = () => { (index: number) => { const cmd = visible[index] cmd.perform() - setIsOpen(false) - setQuery("") - setActiveIndex(0) + closePalette() }, - [visible] + [visible, closePalette] ) useEffect(() => { @@ -146,28 +168,23 @@ const CommandPalette: React.FC = () => { if (!World.isAlive) return if (isMainMenuOpen) return e.preventDefault() - setIsOpen(true) - setTimeout(() => inputRef.current?.focus(), 0) + openPalette() } else if (e.key === "Escape") { if (isOpen) { e.preventDefault() - setIsOpen(false) - setQuery("") - setActiveIndex(0) + closePalette() } } } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) - }, [isOpen, isMainMenuOpen]) + }, [isOpen, isMainMenuOpen, openPalette, closePalette]) useEffect(() => { if (isMainMenuOpen && isOpen) { - setIsOpen(false) - setQuery("") - setActiveIndex(0) + closePalette() } - }, [isMainMenuOpen, isOpen]) + }, [isMainMenuOpen, isOpen, closePalette]) useEffect(() => { if (!isOpen) return @@ -187,12 +204,10 @@ const CommandPalette: React.FC = () => { execute(activeIndex) } else if (e.key === "Escape") { e.preventDefault() - setIsOpen(false) - setQuery("") - setActiveIndex(0) + closePalette() } }, - [activeIndex, execute, visible.length] + [activeIndex, execute, visible.length, closePalette] ) if (!isOpen) return null From e81374502c1e2917e65d9bb76d74995694c71570 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:29:37 -0700 Subject: [PATCH 03/23] Smarter Search --- fission/bun.lock | 91 +------------------- fission/package.json | 1 + fission/src/ui/components/CommandPalette.tsx | 40 ++++----- 3 files changed, 22 insertions(+), 110 deletions(-) diff --git a/fission/bun.lock b/fission/bun.lock index 2cfc629382..c4a8dfc549 100644 --- a/fission/bun.lock +++ b/fission/bun.lock @@ -19,6 +19,7 @@ "colord": "^2.9.3", "electron-squirrel-startup": "^1.0.1", "framer-motion": "^10.18.0", + "fuse.js": "^7.1.0", "lygia": "^1.3.3", "msw": "^2.10.4", "notistack": "^3.0.2", @@ -1108,6 +1109,8 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], + "galactus": ["galactus@1.0.0", "", { "dependencies": { "debug": "^4.3.4", "flora-colossus": "^2.0.0", "fs-extra": "^10.1.0" } }, "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ=="], "gar": ["gar@1.0.4", "", {}, "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w=="], @@ -2114,8 +2117,6 @@ "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], @@ -2132,10 +2133,6 @@ "@electron/windows-sign/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], - "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "@inquirer/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -2148,8 +2145,6 @@ "@mui/base/@mui/utils": ["@mui/utils@6.4.9", "", { "dependencies": { "@babel/runtime": "^7.26.0", "@mui/types": "~7.2.24", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg=="], - "@mui/private-theming/@mui/utils": ["@mui/utils@5.17.1", "", { "dependencies": { "@babel/runtime": "^7.23.9", "@mui/types": "~7.2.15", "@types/prop-types": "^15.7.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.0.0" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg=="], - "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": "bin.js" }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "@react-three/fiber/zustand": ["zustand@3.7.2", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA=="], @@ -2192,8 +2187,6 @@ "cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], @@ -2204,8 +2197,6 @@ "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], - "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "electron/@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], @@ -2242,8 +2233,6 @@ "eslint/doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], - "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -2320,8 +2309,6 @@ "ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], @@ -2396,36 +2383,20 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@electron/asar/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@humanwhocodes/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "@inquirer/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], - "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "@isaacs/cliui/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], "@mui/base/@mui/utils/@mui/types": ["@mui/types@7.2.24", "", { "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw=="], - "@mui/private-theming/@mui/utils/@mui/types": ["@mui/types@7.2.24", "", { "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw=="], - "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -2436,16 +2407,8 @@ "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "cliui/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cliui/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "electron-installer-common/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "electron-installer-debian/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "electron-installer-debian/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], @@ -2472,8 +2435,6 @@ "eslint-plugin-import/tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], - "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "execa/cross-spawn/path-key": ["path-key@2.0.1", "", {}, "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw=="], "execa/cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -2504,42 +2465,20 @@ "read-pkg-up/find-up/locate-path": ["locate-path@2.0.0", "", { "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" } }, "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA=="], - "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "stats-gl/@types/three/fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="], "sucrase/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "wrap-ansi/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "@electron/asar/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "@isaacs/cliui/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "cacache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "cliui/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "electron-installer-common/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "electron-installer-debian/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "electron-installer-debian/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "electron-installer-redhat/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "electron-installer-redhat/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "electron/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], @@ -2548,38 +2487,14 @@ "execa/cross-spawn/shebang-command/shebang-regex": ["shebang-regex@1.0.0", "", {}, "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ=="], - "flat-cache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - "ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "read-pkg-up/find-up/locate-path/p-locate": ["p-locate@2.0.0", "", { "dependencies": { "p-limit": "^1.1.0" } }, "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg=="], "read-pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "sucrase/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "wrap-ansi/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - - "@npmcli/move-file/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "cacache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "electron-installer-debian/yargs/cliui/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "electron-installer-debian/yargs/cliui/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "electron-installer-redhat/yargs/cliui/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "electron-installer-redhat/yargs/cliui/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "flat-cache/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "read-pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@1.3.0", "", { "dependencies": { "p-try": "^1.0.0" } }, "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q=="], - - "electron-installer-debian/yargs/cliui/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "electron-installer-redhat/yargs/cliui/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], } } diff --git a/fission/package.json b/fission/package.json index 92669d9a6d..fcbf34f83a 100644 --- a/fission/package.json +++ b/fission/package.json @@ -45,6 +45,7 @@ "colord": "^2.9.3", "electron-squirrel-startup": "^1.0.1", "framer-motion": "^10.18.0", + "fuse.js": "^7.1.0", "lygia": "^1.3.3", "msw": "^2.10.4", "notistack": "^3.0.2", diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 540193c5d5..737d5c07dd 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -1,3 +1,4 @@ +import Fuse from "fuse.js" import { Box, List, ListItemButton, ListItemText, Paper, Stack, TextField } from "@mui/material" import type React from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -125,30 +126,25 @@ const CommandPalette: React.FC = () => { [addToast, openPanel, openModal, openImportPanel] ) + const fuse = useMemo(() => { + return new Fuse(commands, { + keys: ["label", "description", "keywords"], + threshold: 0.3, + ignoreLocation: true, + includeMatches: true, + shouldSort: false, + includeScore: true, + }) + }, [commands]) + const filtered = useMemo(() => { const q = query.trim().toLowerCase() - if (q.length === 0) return commands - - type Scored = { cmd: CommandDefinition; score: number; idx: number } - const scored: Scored[] = [] - for (let i = 0; i < commands.length; i++) { - const c = commands[i] - const label = c.label.toLowerCase() - const keywords = (c.keywords ?? []).map(k => k.toLowerCase()) - let score = 0 - if (label === q) score += 1000 - if (label.startsWith(q)) score += 500 - if (label.includes(q)) score += 200 - if (keywords.includes(q)) score += 300 - if (keywords.some(k => k.startsWith(q))) score += 150 - if (keywords.some(k => k.includes(q))) score += 50 - if (score > 0) { - scored.push({ cmd: c, score, idx: i }) - } - } - scored.sort((a, b) => (a.score === b.score ? a.idx - b.idx : a.score - b.score)) - return scored.map(s => s.cmd) - }, [commands, query]) + if (!q) return commands + return fuse + .search(q) + .reverse() + .map(r => r.item) + }, [commands, fuse, query]) const visible = useMemo(() => filtered.slice(0, 5), [filtered]) From e3def286c90497491341218ef9a62d1c4d1cb535 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:46:35 -0700 Subject: [PATCH 04/23] General UX Improvements --- fission/src/ui/components/CommandPalette.tsx | 56 ++++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 737d5c07dd..8b900ab11e 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -11,6 +11,7 @@ import { useUIContext } from "@/ui/helpers/UIProviderHelpers" import { useStateContext } from "@/ui/helpers/StateProviderHelpers" import World from "@/systems/World" import type { ConfigurationType } from "@/ui/panels/configuring/assembly-config/ConfigTypes" +import ConfigurePanel from "@/ui/panels/configuring/assembly-config/ConfigurePanel" type CommandDefinition = { id: string @@ -28,13 +29,14 @@ function isTextInputTarget(target: EventTarget | null): boolean { } const CommandPalette: React.FC = () => { - const { addToast, openPanel, openModal } = useUIContext() + const { addToast, openPanel, openModal, modal } = useUIContext() const { isMainMenuOpen } = useStateContext() const [isOpen, setIsOpen] = useState(false) const [query, setQuery] = useState("") const [activeIndex, setActiveIndex] = useState(0) const inputRef = useRef(null) + const containerRef = useRef(null) const closePalette = useCallback(() => { setIsOpen(false) @@ -61,20 +63,6 @@ const CommandPalette: React.FC = () => { const commands = useMemo( () => [ - { - id: "toast-info", - label: "Toast: Show info", - description: "Show an info toast in the bottom-right.", - keywords: ["notification", "snackbar", "message"], - perform: () => addToast("info", "Hello from Command Palette"), - }, - { - id: "toast-success", - label: "Toast: Show success", - description: "Show a success toast in the bottom-right.", - keywords: ["notification", "snackbar", "message"], - perform: () => addToast("success", "Success!"), - }, { id: "open-debug-panel", label: "Open Debug Panel", @@ -96,7 +84,6 @@ const CommandPalette: React.FC = () => { addToast("info", "Drag Mode", `Drag mode has been ${status}`) }, }, - { id: "spawn-asset-robots", label: "Spawn Asset (Robots)", @@ -111,6 +98,13 @@ const CommandPalette: React.FC = () => { keywords: ["spawn", "asset", "field", "import", "mirabuf"], perform: () => openImportPanel("FIELDS"), }, + { + id: "configure-assets", + label: "Configure Assets", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config"], + perform: () => openPanel(ConfigurePanel, {}), + }, { id: "open-settings", label: "Open Settings", @@ -151,10 +145,15 @@ const CommandPalette: React.FC = () => { const execute = useCallback( (index: number) => { const cmd = visible[index] - cmd.perform() + if (cmd) { + cmd.perform() + } else { + addToast("error", "Command Not Found", "The command you entered was not found.") + } + closePalette() }, - [visible, closePalette] + [visible, closePalette, addToast] ) useEffect(() => { @@ -163,6 +162,7 @@ const CommandPalette: React.FC = () => { if (isTextInputTarget(e.target)) return if (!World.isAlive) return if (isMainMenuOpen) return + if (modal) return e.preventDefault() openPalette() } else if (e.key === "Escape") { @@ -174,19 +174,31 @@ const CommandPalette: React.FC = () => { } window.addEventListener("keydown", onKeyDown) return () => window.removeEventListener("keydown", onKeyDown) - }, [isOpen, isMainMenuOpen, openPalette, closePalette]) + }, [isOpen, isMainMenuOpen, modal, openPalette, closePalette]) useEffect(() => { - if (isMainMenuOpen && isOpen) { + if ((isMainMenuOpen || modal) && isOpen) { closePalette() } - }, [isMainMenuOpen, isOpen, closePalette]) + }, [isMainMenuOpen, modal, isOpen, closePalette]) useEffect(() => { if (!isOpen) return setActiveIndex(visible.length > 0 ? visible.length - 1 : 0) }, [isOpen, visible.length]) + useEffect(() => { + if (!isOpen) return + const onPointerDown = (e: PointerEvent) => { + const target = e.target as Node | null + if (containerRef.current && target && !containerRef.current.contains(target)) { + closePalette() + } + } + document.addEventListener("pointerdown", onPointerDown) + return () => document.removeEventListener("pointerdown", onPointerDown) + }, [isOpen, closePalette]) + const onInputKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { @@ -221,7 +233,7 @@ const CommandPalette: React.FC = () => { }} > - + {visible.length > 0 && ( {visible.map((c, i) => ( From 8806a751c24347de7f0f9edc10d57843db1ba408 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:11:19 -0700 Subject: [PATCH 05/23] Arrow Keys Cycle --- fission/src/ui/components/CommandPalette.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 8b900ab11e..91c3d601d4 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -203,10 +203,18 @@ const CommandPalette: React.FC = () => { (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault() - setActiveIndex(i => Math.min(i + 1, Math.max(visible.length - 1, 0))) + setActiveIndex(i => { + const count = visible.length + if (count <= 0) return 0 + return (i + 1 + count) % count + }) } else if (e.key === "ArrowUp") { e.preventDefault() - setActiveIndex(i => Math.max(i - 1, 0)) + setActiveIndex(i => { + const count = visible.length + if (count <= 0) return 0 + return (i - 1 + count) % count + }) } else if (e.key === "Enter") { e.preventDefault() execute(activeIndex) From 83947cc92fbb61834708853d346ef20b98cb05be Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:27:03 -0700 Subject: [PATCH 06/23] Commands for Configuring Robots/Fields --- fission/src/ui/components/CommandPalette.tsx | 86 ++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 91c3d601d4..50a4d9a629 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -61,8 +61,8 @@ const CommandPalette: React.FC = () => { [openPanel] ) - const commands = useMemo( - () => [ + const commands = useMemo(() => { + const list: CommandDefinition[] = [ { id: "open-debug-panel", label: "Open Debug Panel", @@ -105,6 +105,40 @@ const CommandPalette: React.FC = () => { keywords: ["configure", "asset", "config"], perform: () => openPanel(ConfigurePanel, {}), }, + { + id: "configure-robots", + label: "Configure Robots", + description: "Open the configuration panel scoped to spawned robots.", + keywords: ["configure", "robot", "robots", "config"], + perform: () => { + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() + if (!robots || robots.length === 0) { + addToast("warning", "No Robots", "No robots are currently spawned.") + return + } + openPanel(ConfigurePanel, { + configurationType: "ROBOTS", + selectedAssembly: robots.length === 1 ? robots[0] : undefined, + }) + }, + }, + { + id: "configure-field", + label: "Configure Field", + description: "Open the configuration panel scoped to the spawned field.", + keywords: ["configure", "field", "config"], + perform: () => { + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (!field) { + addToast("warning", "No Field", "No field is currently spawned.") + return + } + openPanel(ConfigurePanel, { + configurationType: "FIELDS", + selectedAssembly: field, + }) + }, + }, { id: "open-settings", label: "Open Settings", @@ -116,9 +150,51 @@ const CommandPalette: React.FC = () => { undefined ), }, - ], - [addToast, openPanel, openModal, openImportPanel] - ) + ] + + // Dynamic per-assembly configuration commands (robots and field) + if (isOpen && World.isAlive && World.sceneRenderer) { + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() || [] + for (const r of robots) { + const name = r.assemblyName || "Robot" + const nameTokens = String(name) + .split(/\s+|[-_]/g) + .filter(Boolean) + list.push({ + id: `configure-robot-${r.id}`, + label: `Configure ${name}`, + description: `Open configuration for robot ${name}.`, + keywords: ["configure", "robot", ...nameTokens.map(t => t.toLowerCase())], + perform: () => + openPanel(ConfigurePanel, { + configurationType: "ROBOTS", + selectedAssembly: r, + }), + }) + } + + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (field) { + const name = field.assemblyName || "Field" + const nameTokens = String(name) + .split(/\s+|[-_]/g) + .filter(Boolean) + list.push({ + id: `configure-field-${field.id}`, + label: `Configure ${name}`, + description: `Open configuration for field ${name}.`, + keywords: ["configure", "field", ...nameTokens.map(t => t.toLowerCase())], + perform: () => + openPanel(ConfigurePanel, { + configurationType: "FIELDS", + selectedAssembly: field, + }), + }) + } + } + + return list + }, [addToast, openPanel, openModal, openImportPanel, isOpen]) const fuse = useMemo(() => { return new Fuse(commands, { From e065102458b7f773d29679786146ee25cfdf5b1e Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:20:38 -0700 Subject: [PATCH 07/23] More Commands --- fission/src/ui/components/CommandPalette.tsx | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 50a4d9a629..44f251c5ff 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -12,6 +12,8 @@ import { useStateContext } from "@/ui/helpers/StateProviderHelpers" import World from "@/systems/World" import type { ConfigurationType } from "@/ui/panels/configuring/assembly-config/ConfigTypes" import ConfigurePanel from "@/ui/panels/configuring/assembly-config/ConfigurePanel" +import MatchMode from "@/systems/match_mode/MatchMode" +import MatchModeConfigPanel from "../panels/configuring/MatchModeConfigPanel" type CommandDefinition = { id: string @@ -150,6 +152,20 @@ const CommandPalette: React.FC = () => { undefined ), }, + { + id: "toggle-match-mode", + label: "Toggle Match Mode", + description: "Toggle match mode, allowing you to simulate and run a full match.", + keywords: ["match", "mode", "start", "play", "game", "simulate", "toggle"], + perform: () => { + if (MatchMode.getInstance().isMatchEnabled()) { + MatchMode.getInstance().sandboxModeStart() + addToast("info", "Match Mode Cancelled") + } else { + openPanel(MatchModeConfigPanel, undefined) + } + }, + }, ] // Dynamic per-assembly configuration commands (robots and field) @@ -171,6 +187,15 @@ const CommandPalette: React.FC = () => { selectedAssembly: r, }), }) + list.push({ + id: `remove-robot-${r.id}`, + label: `Remove ${name}`, + description: `Remove the robot ${name}.`, + keywords: ["remove", "delete", "robot", ...nameTokens.map(t => t.toLowerCase())], + perform: () => { + World.sceneRenderer.removeSceneObject(r.id) + }, + }) } const field = World.sceneRenderer.mirabufSceneObjects.getField() @@ -190,6 +215,15 @@ const CommandPalette: React.FC = () => { selectedAssembly: field, }), }) + list.push({ + id: `remove-field-${field.id}`, + label: `Remove ${name}`, + description: `Remove the field ${name}.`, + keywords: ["remove", "delete", "field", ...nameTokens.map(t => t.toLowerCase())], + perform: () => { + World.sceneRenderer.removeSceneObject(field.id) + }, + }) } } From da046e429e15091d0e15c40e904961266d831137 Mon Sep 17 00:00:00 2001 From: Zach Rutman Date: Thu, 14 Aug 2025 16:01:28 -0700 Subject: [PATCH 08/23] fix: remove casting --- fission/src/ui/components/CommandPalette.tsx | 32 ++++++-------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 44f251c5ff..cc6b5b7815 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -1,18 +1,16 @@ -import Fuse from "fuse.js" import { Box, List, ListItemButton, ListItemText, Paper, Stack, TextField } from "@mui/material" +import Fuse from "fuse.js" import type React from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import DebugPanel from "@/ui/panels/DebugPanel" -import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel" -import SettingsModal from "@/ui/modals/configuring/SettingsModal" -import type { PanelImplProps } from "@/ui/components/Panel" -import type { ModalImplProps } from "@/ui/components/Modal" -import { useUIContext } from "@/ui/helpers/UIProviderHelpers" -import { useStateContext } from "@/ui/helpers/StateProviderHelpers" +import MatchMode from "@/systems/match_mode/MatchMode" import World from "@/systems/World" +import { useStateContext } from "@/ui/helpers/StateProviderHelpers" +import { useUIContext } from "@/ui/helpers/UIProviderHelpers" +import SettingsModal from "@/ui/modals/configuring/SettingsModal" import type { ConfigurationType } from "@/ui/panels/configuring/assembly-config/ConfigTypes" import ConfigurePanel from "@/ui/panels/configuring/assembly-config/ConfigurePanel" -import MatchMode from "@/systems/match_mode/MatchMode" +import DebugPanel from "@/ui/panels/DebugPanel" +import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel" import MatchModeConfigPanel from "../panels/configuring/MatchModeConfigPanel" type CommandDefinition = { @@ -53,12 +51,7 @@ const CommandPalette: React.FC = () => { const openImportPanel = useCallback( (configurationType: ConfigurationType) => { - openPanel( - ImportMirabufPanel as unknown as React.FunctionComponent< - PanelImplProps - >, - { configurationType } - ) + openPanel(ImportMirabufPanel, { configurationType }) }, [openPanel] ) @@ -70,8 +63,7 @@ const CommandPalette: React.FC = () => { label: "Open Debug Panel", description: "Open the Debug tools panel.", keywords: ["panel", "debug"], - perform: () => - openPanel(DebugPanel as unknown as React.FunctionComponent>, undefined), + perform: () => openPanel(DebugPanel, undefined), }, { id: "toggle-drag-mode", @@ -146,11 +138,7 @@ const CommandPalette: React.FC = () => { label: "Open Settings", description: "Open the Settings modal.", keywords: ["settings", "preferences", "config"], - perform: () => - openModal( - SettingsModal as unknown as React.FunctionComponent>, - undefined - ), + perform: () => openModal(SettingsModal, undefined), }, { id: "toggle-match-mode", From cbb241972257e87d80ccde435baea8bfdefd2362 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 15 Aug 2025 08:31:48 -0700 Subject: [PATCH 09/23] Update fission/src/ui/components/CommandPalette.tsx Co-authored-by: Zach Rutman --- fission/src/ui/components/CommandPalette.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index cc6b5b7815..62e8eebe6a 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -51,7 +51,7 @@ const CommandPalette: React.FC = () => { const openImportPanel = useCallback( (configurationType: ConfigurationType) => { - openPanel(ImportMirabufPanel, { configurationType }) + openPanel(ImportMirabufPanel, { configurationType }) }, [openPanel] ) From 1df5b83b6a08ac629e9700190de952e51b4cb1a6 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 15 Aug 2025 08:39:23 -0700 Subject: [PATCH 10/23] Include Robot Control Scheme Name --- fission/src/ui/components/CommandPalette.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 62e8eebe6a..ff1ef682d2 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -166,8 +166,8 @@ const CommandPalette: React.FC = () => { .filter(Boolean) list.push({ id: `configure-robot-${r.id}`, - label: `Configure ${name}`, - description: `Open configuration for robot ${name}.`, + label: `Configure ${r.nameTag?.text()} (${name})`, + description: `Open configuration for robot ${r.nameTag?.text()} (${name}).`, keywords: ["configure", "robot", ...nameTokens.map(t => t.toLowerCase())], perform: () => openPanel(ConfigurePanel, { @@ -177,8 +177,8 @@ const CommandPalette: React.FC = () => { }) list.push({ id: `remove-robot-${r.id}`, - label: `Remove ${name}`, - description: `Remove the robot ${name}.`, + label: `Remove ${r.nameTag?.text()} (${name})`, + description: `Remove the robot ${r.nameTag?.text()} (${name}).`, keywords: ["remove", "delete", "robot", ...nameTokens.map(t => t.toLowerCase())], perform: () => { World.sceneRenderer.removeSceneObject(r.id) From 7e8820c67b833c1fc35cae163bb09aa887373223 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 15 Aug 2025 08:46:35 -0700 Subject: [PATCH 11/23] Disables Input if Command Palette is Open --- fission/src/systems/input/InputSystem.ts | 15 +++++++++++++++ fission/src/ui/components/CommandPalette.tsx | 3 +++ 2 files changed, 18 insertions(+) diff --git a/fission/src/systems/input/InputSystem.ts b/fission/src/systems/input/InputSystem.ts index d959904ca9..9066d3560d 100644 --- a/fission/src/systems/input/InputSystem.ts +++ b/fission/src/systems/input/InputSystem.ts @@ -18,6 +18,9 @@ class InputSystem extends WorldSystem { /** The keys currently being pressed. */ private static _keysPressed: Partial> = {} + /** Whether the command palette is currently open, which blocks robot input */ + private static _isCommandPaletteOpen: boolean = false + private static _gpIndex: number | null public static gamepad: Gamepad | null @@ -35,6 +38,13 @@ class InputSystem extends WorldSystem { }) } + /** + * Sets whether the command palette is open, which blocks all robot inputs + */ + public static setCommandPaletteOpen(isOpen: boolean) { + InputSystem._isCommandPaletteOpen = isOpen + } + constructor() { super() @@ -159,6 +169,11 @@ class InputSystem extends WorldSystem { * @returns {number} A number between -1 and 1 based on the current state of the input. */ public static getInput(inputName: InputName, brainIndex: number): number { + // Block all robot inputs when command palette is open + if (InputSystem._isCommandPaletteOpen) { + return 0 + } + const targetScheme = InputSystem.brainIndexSchemeMap.get(brainIndex) const targetInput = targetScheme?.inputs.find(input => input.inputName == inputName) as Input diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index ff1ef682d2..0b918d0786 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -4,6 +4,7 @@ import type React from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import MatchMode from "@/systems/match_mode/MatchMode" import World from "@/systems/World" +import InputSystem from "@/systems/input/InputSystem" import { useStateContext } from "@/ui/helpers/StateProviderHelpers" import { useUIContext } from "@/ui/helpers/UIProviderHelpers" import SettingsModal from "@/ui/modals/configuring/SettingsModal" @@ -42,10 +43,12 @@ const CommandPalette: React.FC = () => { setIsOpen(false) setQuery("") setActiveIndex(0) + InputSystem.setCommandPaletteOpen(false) }, []) const openPalette = useCallback(() => { setIsOpen(true) + InputSystem.setCommandPaletteOpen(true) setTimeout(() => inputRef.current?.focus(), 0) }, []) From a50b72bbc8b9191f8a165be922ca59572408ccab Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 15 Aug 2025 09:12:31 -0700 Subject: [PATCH 12/23] Switch Luna Default Inputs --- fission/src/systems/input/DefaultInputs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fission/src/systems/input/DefaultInputs.ts b/fission/src/systems/input/DefaultInputs.ts index 3c4cf2b623..3b409fe545 100644 --- a/fission/src/systems/input/DefaultInputs.ts +++ b/fission/src/systems/input/DefaultInputs.ts @@ -91,7 +91,7 @@ class DefaultInputs { ButtonInput.onKeyboard("eject", "KeyL"), ButtonInput.onKeyboard("unstick", "KeyK"), - AxisInput.onKeyboardSingleKey("joint 1", "Slash", negativeModifierKeys), + AxisInput.onKeyboardSingleKey("joint 1", "Quote", negativeModifierKeys), AxisInput.onKeyboardSingleKey("joint 2", "Period", negativeModifierKeys), AxisInput.onKeyboardSingleKey("joint 3", "Comma", negativeModifierKeys), AxisInput.onKeyboardSingleKey("joint 4", "KeyM", negativeModifierKeys), From 4bf9491c747ed07efb9624917b4f95f400083d05 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:09:07 -0700 Subject: [PATCH 13/23] Separate CommandRegistry --- fission/src/ui/components/CommandPalette.tsx | 57 ++++++--- fission/src/ui/components/CommandRegistry.ts | 125 +++++++++++++++++++ 2 files changed, 162 insertions(+), 20 deletions(-) create mode 100644 fission/src/ui/components/CommandRegistry.ts diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 0b918d0786..dd2b699811 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -13,14 +13,7 @@ import ConfigurePanel from "@/ui/panels/configuring/assembly-config/ConfigurePan import DebugPanel from "@/ui/panels/DebugPanel" import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel" import MatchModeConfigPanel from "../panels/configuring/MatchModeConfigPanel" - -type CommandDefinition = { - id: string - label: string - description?: string - keywords?: string[] - perform: () => void -} +import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@/ui/components/CommandRegistry" function isTextInputTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false @@ -59,8 +52,9 @@ const CommandPalette: React.FC = () => { [openPanel] ) - const commands = useMemo(() => { - const list: CommandDefinition[] = [ + // Register initial static commands with the registry + useEffect(() => { + const staticCommands: CommandDefinition[] = [ { id: "open-debug-panel", label: "Open Debug Panel", @@ -159,8 +153,17 @@ const CommandPalette: React.FC = () => { }, ] - // Dynamic per-assembly configuration commands (robots and field) - if (isOpen && World.isAlive && World.sceneRenderer) { + const registry = CommandRegistry.get() + const dispose = registry.registerCommands(staticCommands) + return () => dispose() + }, [addToast, openPanel, openModal, openImportPanel]) + + // Register dynamic per-assembly commands via a provider + useEffect(() => { + const provider: CommandProvider = () => { + if (!World.isAlive || !World.sceneRenderer) return [] + const list: CommandDefinition[] = [] + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() || [] for (const r of robots) { const name = r.assemblyName || "Robot" @@ -216,10 +219,25 @@ const CommandPalette: React.FC = () => { }, }) } + + return list } - return list - }, [addToast, openPanel, openModal, openImportPanel, isOpen]) + const registry = CommandRegistry.get() + const dispose = registry.registerProvider(provider) + return () => dispose() + }, [openPanel]) + + // Subscribe to registry updates to refresh palette command list + const [registryTick, setRegistryTick] = useState(0) + useEffect(() => { + const registry = CommandRegistry.get() + return registry.subscribe(() => setRegistryTick(t => t + 1)) + }, []) + + const commands = useMemo(() => { + return CommandRegistry.get().getCommands() + }, [registryTick]) const fuse = useMemo(() => { return new Fuse(commands, { @@ -227,7 +245,7 @@ const CommandPalette: React.FC = () => { threshold: 0.3, ignoreLocation: true, includeMatches: true, - shouldSort: false, + shouldSort: true, includeScore: true, }) }, [commands]) @@ -235,13 +253,12 @@ const CommandPalette: React.FC = () => { const filtered = useMemo(() => { const q = query.trim().toLowerCase() if (!q) return commands - return fuse - .search(q) - .reverse() - .map(r => r.item) + return fuse.search(q).map(r => r.item) }, [commands, fuse, query]) - const visible = useMemo(() => filtered.slice(0, 5), [filtered]) + const visible = useMemo(() => { + return filtered.slice(0, 5).reverse() + }, [filtered]) const execute = useCallback( (index: number) => { diff --git a/fission/src/ui/components/CommandRegistry.ts b/fission/src/ui/components/CommandRegistry.ts new file mode 100644 index 0000000000..5bffa3c474 --- /dev/null +++ b/fission/src/ui/components/CommandRegistry.ts @@ -0,0 +1,125 @@ +export type CommandDefinition = { + id: string + label: string + description?: string + keywords?: string[] + perform: () => void +} + +export type CommandProvider = () => CommandDefinition[] + +/** + * Central registry for commands used by the Command Palette. + * + * Features can register either static commands or dynamic providers. Dynamic providers are functions + * that return a set of commands at the time of retrieval, which is useful for context-sensitive + * commands that depend on runtime state. + * + * Adoption pattern: + * - Each feature owns its registrations (e.g., in its initializer or module load). + * - On enable/mount, call registry.registerCommand(s)/registerProvider and keep the disposer. + * - On disable/unmount, call the disposer to unregister. + */ +class CommandRegistry { + private static instance: CommandRegistry | null = null + + private staticCommands: Map = new Map() + private providers: Set = new Set() + private listeners: Set<() => void> = new Set() + private notifyScheduled: boolean = false + private notifyPending: boolean = false + + static get(): CommandRegistry { + if (!CommandRegistry.instance) { + CommandRegistry.instance = new CommandRegistry() + } + return CommandRegistry.instance + } + + /** Register a single static command. Returns an unregister function. */ + registerCommand(command: CommandDefinition): () => void { + this.staticCommands.set(command.id, command) + this.notify() + return () => { + if (this.staticCommands.get(command.id) === command) { + this.staticCommands.delete(command.id) + this.notify() + } + } + } + + /** Register multiple static commands. Returns an unregister function. */ + registerCommands(commands: CommandDefinition[]): () => void { + const disposers = commands.map(c => this.registerCommand(c)) + return () => { + for (const dispose of disposers) { + try { + dispose() + } catch { + console.error("Error in command dispose", dispose) + } + } + } + } + + /** Register a dynamic provider. Returns an unregister function. */ + registerProvider(provider: CommandProvider): () => void { + this.providers.add(provider) + this.notify() + return () => { + if (this.providers.delete(provider)) { + this.notify() + } + } + } + + /** + * Returns all commands: static ones plus the union of all provider results. + * If duplicate ids exist, the last one encountered wins (provider results are applied after statics). + */ + getCommands(): CommandDefinition[] { + const merged = new Map() + for (const [id, cmd] of this.staticCommands) { + merged.set(id, cmd) + } + for (const provider of this.providers) { + try { + const provided = provider() || [] + for (const cmd of provided) { + merged.set(cmd.id, cmd) + } + } catch (err) { + console.error("Error in command provider", provider) + } + } + return Array.from(merged.values()) + } + + subscribe(listener: () => void): () => void { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } + + private notify() { + // Coalesce multiple rapid updates into a single microtask flush + this.notifyPending = true + if (this.notifyScheduled) return + this.notifyScheduled = true + queueMicrotask(() => { + this.notifyScheduled = false + if (!this.notifyPending) return + this.notifyPending = false + for (const l of this.listeners) { + try { + l() + } catch { + console.error("Error in command notify", l) + } + } + }) + } +} + +export default CommandRegistry From 279c989c5b57bed96926d20656401fc43cc99018 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:57:27 -0700 Subject: [PATCH 14/23] Commands Registered by Proper Objects --- fission/src/systems/match_mode/MatchMode.ts | 20 ++ fission/src/ui/components/CommandPalette.tsx | 187 ++---------------- .../ui/modals/configuring/SettingsModal.tsx | 12 +- fission/src/ui/panels/DebugPanel.tsx | 12 +- .../assembly-config/ConfigurePanel.tsx | 95 +++++++++ .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 21 ++ 6 files changed, 174 insertions(+), 173 deletions(-) diff --git a/fission/src/systems/match_mode/MatchMode.ts b/fission/src/systems/match_mode/MatchMode.ts index c5ef4fb8ec..595ddc255b 100644 --- a/fission/src/systems/match_mode/MatchMode.ts +++ b/fission/src/systems/match_mode/MatchMode.ts @@ -10,6 +10,26 @@ import SimulationSystem from "../simulation/SimulationSystem" import { SoundPlayer } from "../sound/SoundPlayer" import { MatchModeType } from "./MatchModeTypes" import RobotDimensionTracker from "./RobotDimensionTracker" +import CommandRegistry from "@/ui/components/CommandRegistry" +import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControls" + +// Register command: Toggle Match Mode +CommandRegistry.get().registerCommand({ + id: "toggle-match-mode", + label: "Toggle Match Mode", + description: "Toggle match mode, allowing you to simulate and run a full match.", + keywords: ["match", "mode", "start", "play", "game", "simulate", "toggle"], + perform: () => { + if (MatchMode.getInstance().isMatchEnabled()) { + MatchMode.getInstance().sandboxModeStart() + globalAddToast("info", "Match Mode Cancelled") + } else { + import("@/ui/panels/configuring/MatchModeConfigPanel").then(m => { + globalOpenPanel(m.default, undefined) + }) + } + }, +}) class MatchMode { private static _instance: MatchMode diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index dd2b699811..4dd79bac95 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -2,18 +2,16 @@ import { Box, List, ListItemButton, ListItemText, Paper, Stack, TextField } from import Fuse from "fuse.js" import type React from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import MatchMode from "@/systems/match_mode/MatchMode" import World from "@/systems/World" import InputSystem from "@/systems/input/InputSystem" import { useStateContext } from "@/ui/helpers/StateProviderHelpers" import { useUIContext } from "@/ui/helpers/UIProviderHelpers" -import SettingsModal from "@/ui/modals/configuring/SettingsModal" -import type { ConfigurationType } from "@/ui/panels/configuring/assembly-config/ConfigTypes" -import ConfigurePanel from "@/ui/panels/configuring/assembly-config/ConfigurePanel" -import DebugPanel from "@/ui/panels/DebugPanel" -import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel" -import MatchModeConfigPanel from "../panels/configuring/MatchModeConfigPanel" -import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@/ui/components/CommandRegistry" +import CommandRegistry, { type CommandDefinition } from "@/ui/components/CommandRegistry" +import "@/ui/panels/DebugPanel" +import "@/ui/modals/configuring/SettingsModal" +import "@/ui/panels/mirabuf/ImportMirabufPanel" +import "@/ui/panels/configuring/assembly-config/ConfigurePanel" +import "@/ui/panels/configuring/MatchModeConfigPanel" function isTextInputTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false @@ -23,7 +21,7 @@ function isTextInputTarget(target: EventTarget | null): boolean { } const CommandPalette: React.FC = () => { - const { addToast, openPanel, openModal, modal } = useUIContext() + const { addToast, modal } = useUIContext() const { isMainMenuOpen } = useStateContext() const [isOpen, setIsOpen] = useState(false) @@ -45,23 +43,9 @@ const CommandPalette: React.FC = () => { setTimeout(() => inputRef.current?.focus(), 0) }, []) - const openImportPanel = useCallback( - (configurationType: ConfigurationType) => { - openPanel(ImportMirabufPanel, { configurationType }) - }, - [openPanel] - ) - - // Register initial static commands with the registry + // Register command(s) not owned elsewhere useEffect(() => { const staticCommands: CommandDefinition[] = [ - { - id: "open-debug-panel", - label: "Open Debug Panel", - description: "Open the Debug tools panel.", - keywords: ["panel", "debug"], - perform: () => openPanel(DebugPanel, undefined), - }, { id: "toggle-drag-mode", label: "Toggle Drag Mode", @@ -75,158 +59,12 @@ const CommandPalette: React.FC = () => { addToast("info", "Drag Mode", `Drag mode has been ${status}`) }, }, - { - id: "spawn-asset-robots", - label: "Spawn Asset (Robots)", - description: "Open asset spawn panel scoped to robots.", - keywords: ["spawn", "asset", "robot", "import", "mirabuf"], - perform: () => openImportPanel("ROBOTS"), - }, - { - id: "spawn-asset-fields", - label: "Spawn Asset (Fields)", - description: "Open asset spawn panel scoped to fields.", - keywords: ["spawn", "asset", "field", "import", "mirabuf"], - perform: () => openImportPanel("FIELDS"), - }, - { - id: "configure-assets", - label: "Configure Assets", - description: "Open the asset configuration panel.", - keywords: ["configure", "asset", "config"], - perform: () => openPanel(ConfigurePanel, {}), - }, - { - id: "configure-robots", - label: "Configure Robots", - description: "Open the configuration panel scoped to spawned robots.", - keywords: ["configure", "robot", "robots", "config"], - perform: () => { - const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() - if (!robots || robots.length === 0) { - addToast("warning", "No Robots", "No robots are currently spawned.") - return - } - openPanel(ConfigurePanel, { - configurationType: "ROBOTS", - selectedAssembly: robots.length === 1 ? robots[0] : undefined, - }) - }, - }, - { - id: "configure-field", - label: "Configure Field", - description: "Open the configuration panel scoped to the spawned field.", - keywords: ["configure", "field", "config"], - perform: () => { - const field = World.sceneRenderer.mirabufSceneObjects.getField() - if (!field) { - addToast("warning", "No Field", "No field is currently spawned.") - return - } - openPanel(ConfigurePanel, { - configurationType: "FIELDS", - selectedAssembly: field, - }) - }, - }, - { - id: "open-settings", - label: "Open Settings", - description: "Open the Settings modal.", - keywords: ["settings", "preferences", "config"], - perform: () => openModal(SettingsModal, undefined), - }, - { - id: "toggle-match-mode", - label: "Toggle Match Mode", - description: "Toggle match mode, allowing you to simulate and run a full match.", - keywords: ["match", "mode", "start", "play", "game", "simulate", "toggle"], - perform: () => { - if (MatchMode.getInstance().isMatchEnabled()) { - MatchMode.getInstance().sandboxModeStart() - addToast("info", "Match Mode Cancelled") - } else { - openPanel(MatchModeConfigPanel, undefined) - } - }, - }, ] const registry = CommandRegistry.get() const dispose = registry.registerCommands(staticCommands) return () => dispose() - }, [addToast, openPanel, openModal, openImportPanel]) - - // Register dynamic per-assembly commands via a provider - useEffect(() => { - const provider: CommandProvider = () => { - if (!World.isAlive || !World.sceneRenderer) return [] - const list: CommandDefinition[] = [] - - const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() || [] - for (const r of robots) { - const name = r.assemblyName || "Robot" - const nameTokens = String(name) - .split(/\s+|[-_]/g) - .filter(Boolean) - list.push({ - id: `configure-robot-${r.id}`, - label: `Configure ${r.nameTag?.text()} (${name})`, - description: `Open configuration for robot ${r.nameTag?.text()} (${name}).`, - keywords: ["configure", "robot", ...nameTokens.map(t => t.toLowerCase())], - perform: () => - openPanel(ConfigurePanel, { - configurationType: "ROBOTS", - selectedAssembly: r, - }), - }) - list.push({ - id: `remove-robot-${r.id}`, - label: `Remove ${r.nameTag?.text()} (${name})`, - description: `Remove the robot ${r.nameTag?.text()} (${name}).`, - keywords: ["remove", "delete", "robot", ...nameTokens.map(t => t.toLowerCase())], - perform: () => { - World.sceneRenderer.removeSceneObject(r.id) - }, - }) - } - - const field = World.sceneRenderer.mirabufSceneObjects.getField() - if (field) { - const name = field.assemblyName || "Field" - const nameTokens = String(name) - .split(/\s+|[-_]/g) - .filter(Boolean) - list.push({ - id: `configure-field-${field.id}`, - label: `Configure ${name}`, - description: `Open configuration for field ${name}.`, - keywords: ["configure", "field", ...nameTokens.map(t => t.toLowerCase())], - perform: () => - openPanel(ConfigurePanel, { - configurationType: "FIELDS", - selectedAssembly: field, - }), - }) - list.push({ - id: `remove-field-${field.id}`, - label: `Remove ${name}`, - description: `Remove the field ${name}.`, - keywords: ["remove", "delete", "field", ...nameTokens.map(t => t.toLowerCase())], - perform: () => { - World.sceneRenderer.removeSceneObject(field.id) - }, - }) - } - - return list - } - - const registry = CommandRegistry.get() - const dispose = registry.registerProvider(provider) - return () => dispose() - }, [openPanel]) + }, [addToast]) // Subscribe to registry updates to refresh palette command list const [registryTick, setRegistryTick] = useState(0) @@ -235,6 +73,13 @@ const CommandPalette: React.FC = () => { return registry.subscribe(() => setRegistryTick(t => t + 1)) }, []) + // Force a refresh when opening, so dynamic providers reflect current assemblies + useEffect(() => { + if (isOpen) { + setRegistryTick(t => t + 1) + } + }, [isOpen]) + const commands = useMemo(() => { return CommandRegistry.get().getCommands() }, [registryTick]) diff --git a/fission/src/ui/modals/configuring/SettingsModal.tsx b/fission/src/ui/modals/configuring/SettingsModal.tsx index ee1fdbc0b8..f981dd2423 100644 --- a/fission/src/ui/modals/configuring/SettingsModal.tsx +++ b/fission/src/ui/modals/configuring/SettingsModal.tsx @@ -3,7 +3,7 @@ import { Button } from "@/ui/components/StyledComponents" import type React from "react" import { useCallback, useEffect, useReducer, useState } from "react" import { GiPerspectiveDiceSixFacesOne } from "react-icons/gi" -import { globalAddToast } from "@/components/GlobalUIControls.ts" +import { globalAddToast, globalOpenModal } from "@/components/GlobalUIControls.ts" import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import type { GlobalPreference, GlobalPreferences } from "@/systems/preferences/PreferenceTypes" import { SoundPlayer } from "@/systems/sound/SoundPlayer" @@ -16,6 +16,16 @@ import { Spacer } from "@/ui/components/StyledComponents" import { useThemeContext } from "@/ui/helpers/ThemeProviderHelpers" import { useUIContext } from "@/ui/helpers/UIProviderHelpers" import { randomColor } from "@/util/Random" +import CommandRegistry from "@/ui/components/CommandRegistry" + +// Register command: Open Settings (module-scope side effect) +CommandRegistry.get().registerCommand({ + id: "open-settings", + label: "Open Settings", + description: "Open the Settings modal.", + keywords: ["settings", "preferences", "config"], + perform: () => import("./SettingsModal").then(m => globalOpenModal(m.default, undefined)), +}) // Graphics settings constants const MIN_LIGHT_INTENSITY = 1 diff --git a/fission/src/ui/panels/DebugPanel.tsx b/fission/src/ui/panels/DebugPanel.tsx index 71783ccf84..e916293a86 100644 --- a/fission/src/ui/panels/DebugPanel.tsx +++ b/fission/src/ui/panels/DebugPanel.tsx @@ -11,13 +11,23 @@ import MirabufCachingService, { import PreferencesSystem from "@/systems/preferences/PreferencesSystem" import World from "@/systems/World" import { random } from "@/util/Random" -import { globalAddToast } from "../components/GlobalUIControls" +import { globalAddToast, globalOpenPanel } from "../components/GlobalUIControls" import Label from "../components/Label" import type { PanelImplProps } from "../components/Panel" import { useUIContext } from "../helpers/UIProviderHelpers" import PokerPanel from "./PokerPanel" import WsViewPanel from "./WsViewPanel" import ConfirmModal from "@/ui/modals/common/ConfirmModal" +import CommandRegistry from "@/ui/components/CommandRegistry" + +// Register command: Open Debug Panel (module-scope side effect) +CommandRegistry.get().registerCommand({ + id: "open-debug-panel", + label: "Open Debug Panel", + description: "Open the Debug tools panel.", + keywords: ["panel", "debug"], + perform: () => import("./DebugPanel").then(m => globalOpenPanel(m.default, undefined)), +}) function toggleDragMode() { const dragSystem = World.dragModeSystem diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index 7b378e1c62..a3d05cbb1a 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -31,6 +31,101 @@ import SequentialBehaviorsInterface from "./interfaces/SequentialBehaviorsInterf import SimulationInterface from "./interfaces/SimulationInterface" import ConfigureProtectedZonesInterface from "./interfaces/scoring/ConfigureProtectedZonesInterface" import ConfigureScoringZonesInterface from "./interfaces/scoring/ConfigureScoringZonesInterface" +import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@/ui/components/CommandRegistry" +import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControls" + +// Register command: Configure Assets (module-scope side effect) +CommandRegistry.get().registerCommand({ + id: "configure-assets", + label: "Configure Assets", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config"], + perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, {})), +}) + +// Register command: Configure Robots (module-scope) +CommandRegistry.get().registerCommand({ + id: "configure-robots", + label: "Configure Robots", + description: "Open the configuration panel scoped to spawned robots.", + keywords: ["configure", "robot", "robots", "config"], + perform: () => { + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() + if (!robots || robots.length === 0) { + globalAddToast("warning", "No Robots", "No robots are currently spawned.") + return + } + import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { + configurationType: "ROBOTS", + selectedAssembly: robots.length === 1 ? robots[0] : undefined, + })) + }, +}) + +// Register dynamic provider: per-assembly configure/remove commands (module-scope) +const provider: CommandProvider = () => { + if (!World.isAlive || !World.sceneRenderer) return [] + const list: CommandDefinition[] = [] + + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() || [] + for (const r of robots) { + const name = r.assemblyName || "Robot" + const nameTokens = String(name) + .split(/\s+|[-_]/g) + .filter(Boolean) + list.push({ + id: `configure-robot-${r.id}`, + label: `Configure ${r.nameTag?.text()} (${name})`, + description: `Open configuration for robot ${r.nameTag?.text()} (${name}).`, + keywords: ["configure", "robot", ...nameTokens.map(t => t.toLowerCase())], + perform: () => + import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { + configurationType: "ROBOTS", + selectedAssembly: r, + })), + }) + list.push({ + id: `remove-robot-${r.id}`, + label: `Remove ${r.nameTag?.text()} (${name})`, + description: `Remove the robot ${r.nameTag?.text()} (${name}).`, + keywords: ["remove", "delete", "robot", ...nameTokens.map(t => t.toLowerCase())], + perform: () => { + World.sceneRenderer.removeSceneObject(r.id) + }, + }) + } + + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (field) { + const name = field.assemblyName || "Field" + const nameTokens = String(name) + .split(/\s+|[-_]/g) + .filter(Boolean) + list.push({ + id: `configure-field-${field.id}`, + label: `Configure ${name}`, + description: `Open configuration for field ${name}.`, + keywords: ["configure", "field", ...nameTokens.map(t => t.toLowerCase())], + perform: () => + import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { + configurationType: "FIELDS", + selectedAssembly: field, + })), + }) + list.push({ + id: `remove-field-${field.id}`, + label: `Remove ${name}`, + description: `Remove the field ${name}.`, + keywords: ["remove", "delete", "field", ...nameTokens.map(t => t.toLowerCase())], + perform: () => { + World.sceneRenderer.removeSceneObject(field.id) + }, + }) + } + + return list +} +CommandRegistry.get().registerProvider(provider) interface ConfigInterfaceProps { panel: UIScreen diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index b270550563..f486fc2599 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -40,6 +40,27 @@ import ImportLocalMirabufModal from "@/ui/modals/mirabuf/ImportLocalMirabufModal import type TaskStatus from "@/util/TaskStatus" import type { ConfigurationType } from "../configuring/assembly-config/ConfigTypes" import InitialConfigPanel from "../configuring/initial-config/InitialConfigPanel" +import CommandRegistry from "@/ui/components/CommandRegistry" + +// Register commands: Open import panel scoped to robots/fields (module-scope side effect) +CommandRegistry.get().registerCommand({ + id: "spawn-asset-robots", + label: "Spawn Asset (Robots)", + description: "Open asset spawn panel scoped to robots.", + keywords: ["spawn", "asset", "robot", "import", "mirabuf"], + perform: () => { + globalOpenPanel(ImportMirabufPanel, { configurationType: "ROBOTS" }) + }, +}) +CommandRegistry.get().registerCommand({ + id: "spawn-asset-fields", + label: "Spawn Asset (Fields)", + description: "Open asset spawn panel scoped to fields.", + keywords: ["spawn", "asset", "field", "import", "mirabuf"], + perform: () => { + globalOpenPanel(ImportMirabufPanel, { configurationType: "FIELDS" }) + }, +}) interface ItemCardProps { id: string From 67261837776693d3ad6de899babe69cb37ecfe7c Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:58:24 -0700 Subject: [PATCH 15/23] Formatting --- fission/src/systems/match_mode/MatchMode.ts | 28 +-- .../ui/modals/configuring/SettingsModal.tsx | 10 +- fission/src/ui/panels/DebugPanel.tsx | 2 +- .../assembly-config/ConfigurePanel.tsx | 166 +++++++++--------- 4 files changed, 106 insertions(+), 100 deletions(-) diff --git a/fission/src/systems/match_mode/MatchMode.ts b/fission/src/systems/match_mode/MatchMode.ts index 595ddc255b..f887f34384 100644 --- a/fission/src/systems/match_mode/MatchMode.ts +++ b/fission/src/systems/match_mode/MatchMode.ts @@ -15,20 +15,20 @@ import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControl // Register command: Toggle Match Mode CommandRegistry.get().registerCommand({ - id: "toggle-match-mode", - label: "Toggle Match Mode", - description: "Toggle match mode, allowing you to simulate and run a full match.", - keywords: ["match", "mode", "start", "play", "game", "simulate", "toggle"], - perform: () => { - if (MatchMode.getInstance().isMatchEnabled()) { - MatchMode.getInstance().sandboxModeStart() - globalAddToast("info", "Match Mode Cancelled") - } else { - import("@/ui/panels/configuring/MatchModeConfigPanel").then(m => { - globalOpenPanel(m.default, undefined) - }) - } - }, + id: "toggle-match-mode", + label: "Toggle Match Mode", + description: "Toggle match mode, allowing you to simulate and run a full match.", + keywords: ["match", "mode", "start", "play", "game", "simulate", "toggle"], + perform: () => { + if (MatchMode.getInstance().isMatchEnabled()) { + MatchMode.getInstance().sandboxModeStart() + globalAddToast("info", "Match Mode Cancelled") + } else { + import("@/ui/panels/configuring/MatchModeConfigPanel").then(m => { + globalOpenPanel(m.default, undefined) + }) + } + }, }) class MatchMode { diff --git a/fission/src/ui/modals/configuring/SettingsModal.tsx b/fission/src/ui/modals/configuring/SettingsModal.tsx index f981dd2423..1764c27dca 100644 --- a/fission/src/ui/modals/configuring/SettingsModal.tsx +++ b/fission/src/ui/modals/configuring/SettingsModal.tsx @@ -20,11 +20,11 @@ import CommandRegistry from "@/ui/components/CommandRegistry" // Register command: Open Settings (module-scope side effect) CommandRegistry.get().registerCommand({ - id: "open-settings", - label: "Open Settings", - description: "Open the Settings modal.", - keywords: ["settings", "preferences", "config"], - perform: () => import("./SettingsModal").then(m => globalOpenModal(m.default, undefined)), + id: "open-settings", + label: "Open Settings", + description: "Open the Settings modal.", + keywords: ["settings", "preferences", "config"], + perform: () => import("./SettingsModal").then(m => globalOpenModal(m.default, undefined)), }) // Graphics settings constants diff --git a/fission/src/ui/panels/DebugPanel.tsx b/fission/src/ui/panels/DebugPanel.tsx index e916293a86..de2fcb68b2 100644 --- a/fission/src/ui/panels/DebugPanel.tsx +++ b/fission/src/ui/panels/DebugPanel.tsx @@ -26,7 +26,7 @@ CommandRegistry.get().registerCommand({ label: "Open Debug Panel", description: "Open the Debug tools panel.", keywords: ["panel", "debug"], - perform: () => import("./DebugPanel").then(m => globalOpenPanel(m.default, undefined)), + perform: () => import("./DebugPanel").then(m => globalOpenPanel(m.default, undefined)), }) function toggleDragMode() { diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index a3d05cbb1a..a9b71459ab 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -36,94 +36,100 @@ import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControl // Register command: Configure Assets (module-scope side effect) CommandRegistry.get().registerCommand({ - id: "configure-assets", - label: "Configure Assets", - description: "Open the asset configuration panel.", - keywords: ["configure", "asset", "config"], - perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, {})), + id: "configure-assets", + label: "Configure Assets", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config"], + perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, {})), }) // Register command: Configure Robots (module-scope) CommandRegistry.get().registerCommand({ - id: "configure-robots", - label: "Configure Robots", - description: "Open the configuration panel scoped to spawned robots.", - keywords: ["configure", "robot", "robots", "config"], - perform: () => { - const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() - if (!robots || robots.length === 0) { - globalAddToast("warning", "No Robots", "No robots are currently spawned.") - return - } - import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { - configurationType: "ROBOTS", - selectedAssembly: robots.length === 1 ? robots[0] : undefined, - })) - }, + id: "configure-robots", + label: "Configure Robots", + description: "Open the configuration panel scoped to spawned robots.", + keywords: ["configure", "robot", "robots", "config"], + perform: () => { + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() + if (!robots || robots.length === 0) { + globalAddToast("warning", "No Robots", "No robots are currently spawned.") + return + } + import("./ConfigurePanel").then(m => + globalOpenPanel(m.default, { + configurationType: "ROBOTS", + selectedAssembly: robots.length === 1 ? robots[0] : undefined, + }) + ) + }, }) // Register dynamic provider: per-assembly configure/remove commands (module-scope) const provider: CommandProvider = () => { - if (!World.isAlive || !World.sceneRenderer) return [] - const list: CommandDefinition[] = [] - - const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() || [] - for (const r of robots) { - const name = r.assemblyName || "Robot" - const nameTokens = String(name) - .split(/\s+|[-_]/g) - .filter(Boolean) - list.push({ - id: `configure-robot-${r.id}`, - label: `Configure ${r.nameTag?.text()} (${name})`, - description: `Open configuration for robot ${r.nameTag?.text()} (${name}).`, - keywords: ["configure", "robot", ...nameTokens.map(t => t.toLowerCase())], - perform: () => - import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { - configurationType: "ROBOTS", - selectedAssembly: r, - })), - }) - list.push({ - id: `remove-robot-${r.id}`, - label: `Remove ${r.nameTag?.text()} (${name})`, - description: `Remove the robot ${r.nameTag?.text()} (${name}).`, - keywords: ["remove", "delete", "robot", ...nameTokens.map(t => t.toLowerCase())], - perform: () => { - World.sceneRenderer.removeSceneObject(r.id) - }, - }) - } - - const field = World.sceneRenderer.mirabufSceneObjects.getField() - if (field) { - const name = field.assemblyName || "Field" - const nameTokens = String(name) - .split(/\s+|[-_]/g) - .filter(Boolean) - list.push({ - id: `configure-field-${field.id}`, - label: `Configure ${name}`, - description: `Open configuration for field ${name}.`, - keywords: ["configure", "field", ...nameTokens.map(t => t.toLowerCase())], - perform: () => - import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { - configurationType: "FIELDS", - selectedAssembly: field, - })), - }) - list.push({ - id: `remove-field-${field.id}`, - label: `Remove ${name}`, - description: `Remove the field ${name}.`, - keywords: ["remove", "delete", "field", ...nameTokens.map(t => t.toLowerCase())], - perform: () => { - World.sceneRenderer.removeSceneObject(field.id) - }, - }) - } - - return list + if (!World.isAlive || !World.sceneRenderer) return [] + const list: CommandDefinition[] = [] + + const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() || [] + for (const r of robots) { + const name = r.assemblyName || "Robot" + const nameTokens = String(name) + .split(/\s+|[-_]/g) + .filter(Boolean) + list.push({ + id: `configure-robot-${r.id}`, + label: `Configure ${r.nameTag?.text()} (${name})`, + description: `Open configuration for robot ${r.nameTag?.text()} (${name}).`, + keywords: ["configure", "robot", ...nameTokens.map(t => t.toLowerCase())], + perform: () => + import("./ConfigurePanel").then(m => + globalOpenPanel(m.default, { + configurationType: "ROBOTS", + selectedAssembly: r, + }) + ), + }) + list.push({ + id: `remove-robot-${r.id}`, + label: `Remove ${r.nameTag?.text()} (${name})`, + description: `Remove the robot ${r.nameTag?.text()} (${name}).`, + keywords: ["remove", "delete", "robot", ...nameTokens.map(t => t.toLowerCase())], + perform: () => { + World.sceneRenderer.removeSceneObject(r.id) + }, + }) + } + + const field = World.sceneRenderer.mirabufSceneObjects.getField() + if (field) { + const name = field.assemblyName || "Field" + const nameTokens = String(name) + .split(/\s+|[-_]/g) + .filter(Boolean) + list.push({ + id: `configure-field-${field.id}`, + label: `Configure ${name}`, + description: `Open configuration for field ${name}.`, + keywords: ["configure", "field", ...nameTokens.map(t => t.toLowerCase())], + perform: () => + import("./ConfigurePanel").then(m => + globalOpenPanel(m.default, { + configurationType: "FIELDS", + selectedAssembly: field, + }) + ), + }) + list.push({ + id: `remove-field-${field.id}`, + label: `Remove ${name}`, + description: `Remove the field ${name}.`, + keywords: ["remove", "delete", "field", ...nameTokens.map(t => t.toLowerCase())], + perform: () => { + World.sceneRenderer.removeSceneObject(field.id) + }, + }) + } + + return list } CommandRegistry.get().registerProvider(provider) From 96354803b6eba1a753cbe60ceadd67087d037686 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:02:48 -0700 Subject: [PATCH 16/23] Lint Error Fix --- fission/src/ui/components/CommandRegistry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fission/src/ui/components/CommandRegistry.ts b/fission/src/ui/components/CommandRegistry.ts index 5bffa3c474..603710e6b1 100644 --- a/fission/src/ui/components/CommandRegistry.ts +++ b/fission/src/ui/components/CommandRegistry.ts @@ -88,7 +88,7 @@ class CommandRegistry { for (const cmd of provided) { merged.set(cmd.id, cmd) } - } catch (err) { + } catch { console.error("Error in command provider", provider) } } From d43f192d3c719cea50dcc9d2c681f1ed75675cb9 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:37:28 -0700 Subject: [PATCH 17/23] Command Pallete Analytics --- fission/src/systems/analytics/AnalyticsSystem.ts | 5 +++++ fission/src/ui/components/CommandPalette.tsx | 1 + 2 files changed, 6 insertions(+) diff --git a/fission/src/systems/analytics/AnalyticsSystem.ts b/fission/src/systems/analytics/AnalyticsSystem.ts index c5af51c3b6..b9908a6021 100644 --- a/fission/src/systems/analytics/AnalyticsSystem.ts +++ b/fission/src/systems/analytics/AnalyticsSystem.ts @@ -53,6 +53,10 @@ export interface AnalyticsEvents { isCustomized: boolean schemeName: string } + + "Command Executed": { + command: string + } } class AnalyticsSystem extends WorldSystem { @@ -77,6 +81,7 @@ class AnalyticsSystem extends WorldSystem { } public event(name: K, params?: AnalyticsEvents[K]) { + console.log("AnalyticsEvent", name, params) event({ name: name, params: params ?? {} }) } diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 4dd79bac95..0681e232e6 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -108,6 +108,7 @@ const CommandPalette: React.FC = () => { const execute = useCallback( (index: number) => { const cmd = visible[index] + World.analyticsSystem?.event("Command Executed", { command: cmd?.label ?? "Unknown" }) if (cmd) { cmd.perform() } else { From d5b17493837278b10b600ff49e922fbf575b0369 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:41:45 -0700 Subject: [PATCH 18/23] Configure Assets Multiple Tabs --- .../assembly-config/ConfigurePanel.tsx | 24 ++++++++++++++----- .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 7 +++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index 231261212d..6097367a1c 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -35,13 +35,25 @@ import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@ import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControls" // Register command: Configure Assets (module-scope side effect) -CommandRegistry.get().registerCommand({ - id: "configure-assets", - label: "Configure Assets", +CommandRegistry.get().registerCommands([{ + id: "configure-assets-robots", + label: "Configure Assets (Robots)", description: "Open the asset configuration panel.", - keywords: ["configure", "asset", "config"], - perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, {})), -}) + keywords: ["configure", "asset", "config", "robot", "robots"], + perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "ROBOTS" })), +}, { + id: "configure-assets-fields", + label: "Configure Assets (Fields)", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config", "field", "fields"], + perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "FIELDS" })), +}, { + id: "configure-assets-inputs", + label: "Configure Assets (Inputs)", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config", "input", "inputs"], + perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "INPUTS" })), +}]) // Register command: Configure Robots (module-scope) CommandRegistry.get().registerCommand({ diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index 7e83a0fb50..48e14b6ced 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -42,7 +42,7 @@ import InitialConfigPanel from "../configuring/initial-config/InitialConfigPanel import CommandRegistry from "@/ui/components/CommandRegistry" // Register commands: Open import panel scoped to robots/fields (module-scope side effect) -CommandRegistry.get().registerCommand({ +CommandRegistry.get().registerCommands([{ id: "spawn-asset-robots", label: "Spawn Asset (Robots)", description: "Open asset spawn panel scoped to robots.", @@ -50,8 +50,7 @@ CommandRegistry.get().registerCommand({ perform: () => { globalOpenPanel(ImportMirabufPanel, { configurationType: "ROBOTS" }) }, -}) -CommandRegistry.get().registerCommand({ +}, { id: "spawn-asset-fields", label: "Spawn Asset (Fields)", description: "Open asset spawn panel scoped to fields.", @@ -59,7 +58,7 @@ CommandRegistry.get().registerCommand({ perform: () => { globalOpenPanel(ImportMirabufPanel, { configurationType: "FIELDS" }) }, -}) +}]) interface ItemCardProps { id: string From bcf34f7480e08e4fe355a522ce568aec6c7a01fa Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 22 Aug 2025 08:45:53 -0700 Subject: [PATCH 19/23] Register Commands Formatting --- .../assembly-config/ConfigurePanel.tsx | 45 +++++++++++-------- .../ui/panels/mirabuf/ImportMirabufPanel.tsx | 33 +++++++------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index 6097367a1c..2554a1c741 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -35,25 +35,32 @@ import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@ import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControls" // Register command: Configure Assets (module-scope side effect) -CommandRegistry.get().registerCommands([{ - id: "configure-assets-robots", - label: "Configure Assets (Robots)", - description: "Open the asset configuration panel.", - keywords: ["configure", "asset", "config", "robot", "robots"], - perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "ROBOTS" })), -}, { - id: "configure-assets-fields", - label: "Configure Assets (Fields)", - description: "Open the asset configuration panel.", - keywords: ["configure", "asset", "config", "field", "fields"], - perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "FIELDS" })), -}, { - id: "configure-assets-inputs", - label: "Configure Assets (Inputs)", - description: "Open the asset configuration panel.", - keywords: ["configure", "asset", "config", "input", "inputs"], - perform: () => import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "INPUTS" })), -}]) +CommandRegistry.get().registerCommands([ + { + id: "configure-assets-robots", + label: "Configure Assets (Robots)", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config", "robot", "robots"], + perform: () => + import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "ROBOTS" })), + }, + { + id: "configure-assets-fields", + label: "Configure Assets (Fields)", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config", "field", "fields"], + perform: () => + import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "FIELDS" })), + }, + { + id: "configure-assets-inputs", + label: "Configure Assets (Inputs)", + description: "Open the asset configuration panel.", + keywords: ["configure", "asset", "config", "input", "inputs"], + perform: () => + import("./ConfigurePanel").then(m => globalOpenPanel(m.default, { configurationType: "INPUTS" })), + }, +]) // Register command: Configure Robots (module-scope) CommandRegistry.get().registerCommand({ diff --git a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx index 48e14b6ced..aad3b35913 100644 --- a/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx +++ b/fission/src/ui/panels/mirabuf/ImportMirabufPanel.tsx @@ -42,23 +42,26 @@ import InitialConfigPanel from "../configuring/initial-config/InitialConfigPanel import CommandRegistry from "@/ui/components/CommandRegistry" // Register commands: Open import panel scoped to robots/fields (module-scope side effect) -CommandRegistry.get().registerCommands([{ - id: "spawn-asset-robots", - label: "Spawn Asset (Robots)", - description: "Open asset spawn panel scoped to robots.", - keywords: ["spawn", "asset", "robot", "import", "mirabuf"], - perform: () => { - globalOpenPanel(ImportMirabufPanel, { configurationType: "ROBOTS" }) +CommandRegistry.get().registerCommands([ + { + id: "spawn-asset-robots", + label: "Spawn Asset (Robots)", + description: "Open asset spawn panel scoped to robots.", + keywords: ["spawn", "asset", "robot", "import", "mirabuf"], + perform: () => { + globalOpenPanel(ImportMirabufPanel, { configurationType: "ROBOTS" }) + }, }, -}, { - id: "spawn-asset-fields", - label: "Spawn Asset (Fields)", - description: "Open asset spawn panel scoped to fields.", - keywords: ["spawn", "asset", "field", "import", "mirabuf"], - perform: () => { - globalOpenPanel(ImportMirabufPanel, { configurationType: "FIELDS" }) + { + id: "spawn-asset-fields", + label: "Spawn Asset (Fields)", + description: "Open asset spawn panel scoped to fields.", + keywords: ["spawn", "asset", "field", "import", "mirabuf"], + perform: () => { + globalOpenPanel(ImportMirabufPanel, { configurationType: "FIELDS" }) + }, }, -}]) +]) interface ItemCardProps { id: string From 6baf55400044bb489d3e22d89349356ac8063b34 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 22 Aug 2025 09:16:20 -0700 Subject: [PATCH 20/23] Removed Useless Command & Black Border --- fission/src/ui/components/CommandPalette.tsx | 8 +++++++ .../assembly-config/ConfigurePanel.tsx | 23 +------------------ 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 0681e232e6..1baf3597e4 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -230,6 +230,14 @@ const CommandPalette: React.FC = () => { setQuery(e.target.value) }} onKeyDown={onInputKeyDown} + sx={{ + "& .MuiOutlinedInput-root": { + "&:hover fieldset, &.Mui-focused fieldset": { + borderColor: "rgba(0, 0, 0, 0.9)", + borderWidth: "1px", + }, + }, + }} /> diff --git a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx index 2554a1c741..cacbfca5a8 100644 --- a/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx +++ b/fission/src/ui/panels/configuring/assembly-config/ConfigurePanel.tsx @@ -32,7 +32,7 @@ import SimulationInterface from "./interfaces/SimulationInterface" import ConfigureProtectedZonesInterface from "./interfaces/scoring/ConfigureProtectedZonesInterface" import ConfigureScoringZonesInterface from "./interfaces/scoring/ConfigureScoringZonesInterface" import CommandRegistry, { type CommandDefinition, type CommandProvider } from "@/ui/components/CommandRegistry" -import { globalAddToast, globalOpenPanel } from "@/ui/components/GlobalUIControls" +import { globalOpenPanel } from "@/ui/components/GlobalUIControls" // Register command: Configure Assets (module-scope side effect) CommandRegistry.get().registerCommands([ @@ -62,27 +62,6 @@ CommandRegistry.get().registerCommands([ }, ]) -// Register command: Configure Robots (module-scope) -CommandRegistry.get().registerCommand({ - id: "configure-robots", - label: "Configure Robots", - description: "Open the configuration panel scoped to spawned robots.", - keywords: ["configure", "robot", "robots", "config"], - perform: () => { - const robots = World.sceneRenderer.mirabufSceneObjects.getRobots() - if (!robots || robots.length === 0) { - globalAddToast("warning", "No Robots", "No robots are currently spawned.") - return - } - import("./ConfigurePanel").then(m => - globalOpenPanel(m.default, { - configurationType: "ROBOTS", - selectedAssembly: robots.length === 1 ? robots[0] : undefined, - }) - ) - }, -}) - // Register dynamic provider: per-assembly configure/remove commands (module-scope) const provider: CommandProvider = () => { if (!World.isAlive || !World.sceneRenderer) return [] From 20a4e34bd3d26aec0bcbf28f81ce795a2ec52709 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:09:19 -0700 Subject: [PATCH 21/23] Disable Autocomplete Autocomplete could sometimes get in the way, and be annoying. --- fission/src/ui/components/CommandPalette.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 1baf3597e4..f9c0f37bf7 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -229,6 +229,7 @@ const CommandPalette: React.FC = () => { onChange={e => { setQuery(e.target.value) }} + autoComplete="off" onKeyDown={onInputKeyDown} sx={{ "& .MuiOutlinedInput-root": { From ba04eef564fed1d12e00640676f64e48ab3593c6 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:18:09 -0700 Subject: [PATCH 22/23] No radius in text field top edges --- fission/src/ui/components/CommandPalette.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index f9c0f37bf7..76767e1564 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -233,6 +233,8 @@ const CommandPalette: React.FC = () => { onKeyDown={onInputKeyDown} sx={{ "& .MuiOutlinedInput-root": { + borderTopLeftRadius: 0, + borderTopRightRadius: 0, "&:hover fieldset, &.Mui-focused fieldset": { borderColor: "rgba(0, 0, 0, 0.9)", borderWidth: "1px", From 839224b18919adb2d82867524a9b8a593d898ee8 Mon Sep 17 00:00:00 2001 From: Alexey Dmitriev <157652245+AlexD717@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:07:25 -0700 Subject: [PATCH 23/23] Scrollable Command Palette --- fission/src/ui/components/CommandPalette.tsx | 27 ++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/fission/src/ui/components/CommandPalette.tsx b/fission/src/ui/components/CommandPalette.tsx index 76767e1564..a285f41710 100644 --- a/fission/src/ui/components/CommandPalette.tsx +++ b/fission/src/ui/components/CommandPalette.tsx @@ -29,6 +29,7 @@ const CommandPalette: React.FC = () => { const [activeIndex, setActiveIndex] = useState(0) const inputRef = useRef(null) const containerRef = useRef(null) + const listItemRefs = useRef<(HTMLDivElement | null)[]>([]) const closePalette = useCallback(() => { setIsOpen(false) @@ -102,7 +103,7 @@ const CommandPalette: React.FC = () => { }, [commands, fuse, query]) const visible = useMemo(() => { - return filtered.slice(0, 5).reverse() + return [...filtered].reverse() }, [filtered]) const execute = useCallback( @@ -151,6 +152,10 @@ const CommandPalette: React.FC = () => { setActiveIndex(visible.length > 0 ? visible.length - 1 : 0) }, [isOpen, visible.length]) + useEffect(() => { + listItemRefs.current = listItemRefs.current.slice(0, visible.length) + }, [visible.length]) + useEffect(() => { if (!isOpen) return const onPointerDown = (e: PointerEvent) => { @@ -163,6 +168,14 @@ const CommandPalette: React.FC = () => { return () => document.removeEventListener("pointerdown", onPointerDown) }, [isOpen, closePalette]) + useEffect(() => { + if (!isOpen) return + const activeItem = listItemRefs.current[activeIndex] + if (activeItem) { + activeItem.scrollIntoView({ block: "nearest" }) + } + }, [isOpen, activeIndex]) + const onInputKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { @@ -207,13 +220,23 @@ const CommandPalette: React.FC = () => { {visible.length > 0 && ( - + {visible.map((c, i) => ( setActiveIndex(i)} onClick={() => execute(i)} + ref={_element => { + listItemRefs.current[i] = _element + }} >