diff --git a/apps/code/package.json b/apps/code/package.json index 188469e3e..de26dbe59 100644 --- a/apps/code/package.json +++ b/apps/code/package.json @@ -133,7 +133,7 @@ "@posthog/git": "workspace:*", "@posthog/hedgehog-mode": "^0.0.48", "@posthog/platform": "workspace:*", - "@posthog/quill": "0.1.0-alpha.7", + "@posthog/quill": "0.3.0-beta.1", "@posthog/shared": "workspace:*", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-icons": "^1.3.2", @@ -186,16 +186,17 @@ "react-markdown": "^10.1.0", "react-resizable-panels": "^3.0.6", "reflect-metadata": "^0.2.2", - "semver": "^7.6.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", + "semver": "^7.6.0", "shadcn": "^4.1.2", "smol-toml": "^1.6.0", "sonner": "^2.0.7", "striptags": "^3.2.0", "tailwind-merge": "^3.5.0", + "tailwindcss-scroll-mask": "^0.0.3", "tippy.js": "^6.3.7", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2", diff --git a/apps/code/src/renderer/features/command/components/CommandKeyHints.tsx b/apps/code/src/renderer/features/command/components/CommandKeyHints.tsx index 865cac623..c06f67b35 100644 --- a/apps/code/src/renderer/features/command/components/CommandKeyHints.tsx +++ b/apps/code/src/renderer/features/command/components/CommandKeyHints.tsx @@ -1,34 +1,27 @@ -import { KeyHint } from "@features/command/components/KeyHint"; -import { Code, Flex } from "@radix-ui/themes"; +import { Kbd, KbdGroup } from "@posthog/quill"; export function CommandKeyHints() { return ( - - - - - Navigate - - - - - - Select - - - - - - Close - - - +
+
+ + + + + navigate +
+
+ + + + select +
+
+ + Esc + + close +
+
); } diff --git a/apps/code/src/renderer/features/command/components/CommandMenu.tsx b/apps/code/src/renderer/features/command/components/CommandMenu.tsx index 48cbcde36..3fafe99f4 100644 --- a/apps/code/src/renderer/features/command/components/CommandMenu.tsx +++ b/apps/code/src/renderer/features/command/components/CommandMenu.tsx @@ -1,9 +1,20 @@ import { useReviewNavigationStore } from "@features/code-review/stores/reviewNavigationStore"; -import { Command } from "@features/command/components/Command"; import { CommandKeyHints } from "@features/command/components/CommandKeyHints"; import { useFolders } from "@features/folders/hooks/useFolders"; import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; +import { + Autocomplete, + AutocompleteCollection, + AutocompleteGroup, + AutocompleteInput, + AutocompleteItem, + AutocompleteLabel, + AutocompleteList, + AutocompleteStatus, + Dialog, + DialogContent, +} from "@posthog/quill"; import { DesktopIcon, FileTextIcon, @@ -13,27 +24,37 @@ import { SunIcon, ViewVerticalIcon, } from "@radix-ui/react-icons"; -import { Flex, Text } from "@radix-ui/themes"; import { ANALYTICS_EVENTS, type CommandMenuAction, } from "@shared/types/analytics"; import { useNavigationStore } from "@stores/navigationStore"; -import { THEME_CYCLE_LABELS, useThemeStore } from "@stores/themeStore"; +import { useThemeStore } from "@stores/themeStore"; import { track } from "@utils/analytics"; -import { useCallback, useEffect, useRef } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; +import { useCallback, useEffect, useMemo, useState } from "react"; interface CommandMenuProps { open: boolean; onOpenChange: (open: boolean) => void; } +type Command = { + id: string; + label: string; + keywords?: string; + icon: React.ReactNode; + action: CommandMenuAction; + onRun: () => void; +}; + +type CommandSection = { label: string; items: Command[] }; + export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const { navigateToTaskInput } = useNavigationStore(); const openSettingsDialog = useSettingsDialogStore((state) => state.open); + const closeSettingsDialog = useSettingsDialogStore((state) => state.close); const { folders } = useFolders(); - const { theme, cycleTheme } = useThemeStore(); + const { theme, setTheme } = useThemeStore(); const toggleLeftSidebar = useSidebarStore((state) => state.toggle); const view = useNavigationStore((state) => state.view); const setReviewMode = useReviewNavigationStore( @@ -42,6 +63,19 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const getReviewMode = useReviewNavigationStore( (state) => state.getReviewMode, ); + const [query, setQuery] = useState(""); + const [systemPrefersDark, setSystemPrefersDark] = useState( + () => window.matchMedia("(prefers-color-scheme: dark)").matches, + ); + + useEffect(() => { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const onChange = (e: MediaQueryListEvent) => + setSystemPrefersDark(e.matches); + mq.addEventListener("change", onChange); + return () => mq.removeEventListener("change", onChange); + }, []); + const openReviewPanel = useCallback(() => { const taskId = view.type === "task-detail" ? view.data?.id : undefined; if (!taskId) return; @@ -50,179 +84,209 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { setReviewMode(taskId, "split"); } }, [view, getReviewMode, setReviewMode]); - const commandRef = useRef(null); - - const close = useCallback(() => onOpenChange(false), [onOpenChange]); - - const trackAction = useCallback((action: CommandMenuAction) => { - track(ANALYTICS_EVENTS.COMMAND_MENU_ACTION, { action_type: action }); - }, []); - - const runAndClose = useCallback( - ( - fn: (...args: T) => void, - action?: CommandMenuAction, - ) => - (...args: T) => { - if (action) { - trackAction(action); - } - fn(...args); - close(); - }, - [close, trackAction], - ); useEffect(() => { if (open) { track(ANALYTICS_EVENTS.COMMAND_MENU_OPENED); + } else { + setQuery(""); } }, [open]); - useHotkeys("escape", close, { - enabled: open, - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }); - - useHotkeys("mod+k", close, { - enabled: open, - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }); - - useHotkeys("mod+p", close, { - enabled: open, - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }); + const themeOptions = useMemo(() => { + const options: Command[] = []; + if (theme !== "light") { + options.push({ + id: "switch-theme-light", + label: "Switch to light mode", + keywords: "theme appearance", + icon: , + action: "toggle-theme", + onRun: () => setTheme("light"), + }); + } + if (theme !== "dark") { + options.push({ + id: "switch-theme-dark", + label: "Switch to dark mode", + keywords: "theme appearance", + icon: , + action: "toggle-theme", + onRun: () => setTheme("dark"), + }); + } + const systemMatchesCurrent = + (theme === "dark" && systemPrefersDark) || + (theme === "light" && !systemPrefersDark); + if (theme !== "system" && !systemMatchesCurrent) { + options.push({ + id: "switch-theme-system", + label: "Switch to system theme", + keywords: "theme appearance auto", + icon: , + action: "toggle-theme", + onRun: () => setTheme("system"), + }); + } + return options; + }, [theme, setTheme, systemPrefersDark]); - useEffect(() => { - if (!open) return; + const sections = useMemo(() => { + const navigation: Command[] = [ + { + id: "home", + label: "Home", + icon: , + action: "home", + onRun: () => { + closeSettingsDialog(); + navigateToTaskInput(); + }, + }, + { + id: "settings", + label: "Settings", + icon: , + action: "settings", + onRun: () => openSettingsDialog(), + }, + ]; + + const actions: Command[] = [ + ...themeOptions, + { + id: "toggle-left-sidebar", + label: "Toggle left sidebar", + icon: , + action: "toggle-left-sidebar", + onRun: toggleLeftSidebar, + }, + { + id: "open-review-panel", + label: "Open review panel", + icon: , + action: "open-review-panel", + onRun: openReviewPanel, + }, + { + id: "new-task", + label: "New task", + keywords: "create", + icon: , + action: "new-task", + onRun: () => { + closeSettingsDialog(); + navigateToTaskInput(); + }, + }, + ]; - const handleClickOutside = (event: MouseEvent) => { - if ( - commandRef.current && - !commandRef.current.contains(event.target as Node) - ) { - close(); - } - }; + const out: CommandSection[] = [ + { label: "Navigation", items: navigation }, + { label: "Actions", items: actions }, + ]; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [open, close]); + if (folders.length > 0) { + out.push({ + label: "New task in folder", + items: folders.map((folder) => ({ + id: `new-task-folder-${folder.id}`, + label: `New task in ${folder.name}`, + keywords: folder.path, + icon: , + action: "new-task", + onRun: () => { + closeSettingsDialog(); + navigateToTaskInput(folder.id); + }, + })), + }); + } - if (!open) return null; + return out; + }, [ + folders, + themeOptions, + navigateToTaskInput, + openSettingsDialog, + closeSettingsDialog, + toggleLeftSidebar, + openReviewPanel, + ]); + + const allCommands = useMemo( + () => sections.flatMap((s) => s.items), + [sections], + ); + + const handleSelect = (id: string | null): void => { + if (id === null) return; + const cmd = allCommands.find((c) => c.id === id); + if (!cmd) return; + track(ANALYTICS_EVENTS.COMMAND_MENU_ACTION, { action_type: cmd.action }); + cmd.onRun(); + onOpenChange(false); + setQuery(""); + }; return ( - -
+ - -
- -
- - - No results found. - - - - - Home - - openSettingsDialog(), "settings")} - > - - Settings - - - - - - {theme === "dark" && ( - - )} - {theme === "light" && ( - - )} - {theme === "system" && ( - - )} - {THEME_CYCLE_LABELS[theme]} - - - - Toggle left sidebar - - - - Open review panel - - - - New task - - - - {folders.length > 0 && ( - - {folders.map((folder) => ( - navigateToTaskInput(folder.id), - "new-task", - )} - > - - - New task in{" "} - {folder.name} - - - ))} - + + inline + defaultOpen + items={sections} + value={query} + autoHighlight="always" + onValueChange={(val, eventDetails) => { + if (eventDetails.reason !== "input-change") return; + if (typeof val === "string") { + setQuery(val); + } + }} + filter={(cmd, q) => { + if (!q) return true; + const haystack = `${cmd.label} ${cmd.keywords ?? ""}`.toLowerCase(); + return haystack.includes(q.toLowerCase()); + }} + > + + + No commands match "{query}" + + } + /> + + {(section: CommandSection) => ( + + {section.label} + + {(cmd: Command) => ( + handleSelect(cmd.id)} + > + {cmd.icon} + {cmd.label} + + )} + + )} - -
- + + -
-
+ + ); } diff --git a/apps/code/src/renderer/features/command/components/KeyHint.tsx b/apps/code/src/renderer/features/command/components/KeyHint.tsx deleted file mode 100644 index d456e195c..000000000 --- a/apps/code/src/renderer/features/command/components/KeyHint.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Kbd } from "@radix-ui/themes"; - -interface KeyHintProps { - keys: string[]; - className?: string; -} - -export function KeyHint({ keys, className }: KeyHintProps) { - return ( -
- {keys.map((key) => ( - - {key} - - ))} -
- ); -} diff --git a/apps/code/src/renderer/features/editor/components/GithubRefChip.tsx b/apps/code/src/renderer/features/editor/components/GithubRefChip.tsx index cd2d96475..ccd97bb67 100644 --- a/apps/code/src/renderer/features/editor/components/GithubRefChip.tsx +++ b/apps/code/src/renderer/features/editor/components/GithubRefChip.tsx @@ -15,7 +15,6 @@ export function GithubRefChip({ return ( window.open(href, "_blank")} className="cli-file-mention mx-0.5 max-w-full cursor-pointer! whitespace-nowrap pl-1 align-middle active:translate-y-0" > diff --git a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx index a1673d0d5..3d87a65da 100644 --- a/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx +++ b/apps/code/src/renderer/features/message-editor/tiptap/MentionChipView.tsx @@ -87,7 +87,6 @@ function DefaultChip({ const chipContent = ( window.open(id, "_blank") : undefined} className={`${chipBase} max-w-full whitespace-nowrap ${isGithubRef ? "cursor-pointer!" : "cursor-default! active:translate-y-0!"} ${isCommand ? "cli-slash-command" : "cli-file-mention"} ${selected ? selectedRing : ""}`} @@ -148,7 +147,6 @@ function PastedTextChip({ =20'} peerDependencies: - react: ^19.0.0 - react-dom: ^19.0.0 + react: ^18.3.1 || ^19.0.0 + react-dom: ^18.3.1 || ^19.0.0 tailwindcss: ^4.0.0 '@posthog/rollup-plugin@1.4.0': @@ -10824,6 +10827,11 @@ packages: tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + tailwindcss-scroll-mask@0.0.3: + resolution: {integrity: sha512-vPuacFs5yHJRZ8MFP/qqKKRbnWlVDPAJ5VfRzOwrmtEJQ4pHm5K1xw/dXdF4v6Sx1QdbtCWJuGs9ucSTc+YanQ==} + peerDependencies: + tailwindcss: ^4 + tailwindcss@3.4.19: resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} @@ -15286,22 +15294,19 @@ snapshots: dependencies: cross-spawn: 7.0.6 - '@posthog/quill@0.1.0-alpha.7(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.2.2)': + '@posthog/quill@0.3.0-beta.1(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(tailwindcss@4.2.2)': dependencies: '@base-ui/react': 1.3.0(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) class-variance-authority: 0.7.1 clsx: 2.1.1 - cmdk: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) lucide-react: 0.577.0(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) react-resizable-panels: 4.10.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tailwind-merge: 2.6.1 tailwindcss: 4.2.2 - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.11))(@types/react@19.2.11)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - '@types/react' - - '@types/react-dom' '@posthog/rollup-plugin@1.4.0(rollup@4.57.1)': dependencies: @@ -23493,6 +23498,10 @@ snapshots: tailwind-merge@3.5.0: {} + tailwindcss-scroll-mask@0.0.3(tailwindcss@4.2.2): + dependencies: + tailwindcss: 4.2.2 + tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0