From 9a81f0e22ba667c6c6939ddcccf273258944c039 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Sat, 11 Apr 2026 13:23:14 +0000 Subject: [PATCH] feat(nexus): extend command palette search + fix shortcut destructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CommandPalette now consumes CommandPaletteContext (open/setOpen) and the internal Cmd+K useEffect is gone — the provider owns the global listener. The search index gains live groups for conversations (via chatApi.listConversations) and studio workshops (mapped from the phase 10 WorkshopSlugs), plus stubbed entries for 8 settings section anchors that will resolve once phase 13 splits InstanceGeneralSettings into cards. Recipes are stubbed as a disabled placeholder pending v1.8's recipe API. useKeyboardShortcuts: adds `onSearch` to the destructure (the phase 6/11 review noted it was referenced at line 25 but never destructured at lines 12-17) and adds it to the useEffect dependency list. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/CommandPalette.tsx | 146 ++++++++++++++++++++++++--- ui/src/hooks/useKeyboardShortcuts.ts | 3 +- 2 files changed, 133 insertions(+), 16 deletions(-) diff --git a/ui/src/components/CommandPalette.tsx b/ui/src/components/CommandPalette.tsx index ed1c66cd..648e0b91 100644 --- a/ui/src/components/CommandPalette.tsx +++ b/ui/src/components/CommandPalette.tsx @@ -1,12 +1,14 @@ -import { useState, useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "@/lib/router"; import { useQuery } from "@tanstack/react-query"; import { useCompany } from "../context/CompanyContext"; import { useDialog } from "../context/DialogContext"; import { useSidebar } from "../context/SidebarContext"; +import { useCommandPalette } from "../context/CommandPaletteContext"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; import { projectsApi } from "../api/projects"; +import { chatApi } from "../api/chat"; import { queryKeys } from "../lib/queryKeys"; import { CommandDialog, @@ -29,12 +31,37 @@ import { SquarePen, Plus, Search, + Settings, + Sparkles, + MessageSquare, + ChefHat, } from "lucide-react"; import { Identity } from "./Identity"; import { agentUrl, projectUrl } from "../lib/utils"; +import { WORKSHOPS } from "./studio/workshops"; + +/** + * Phase 14 — settings section anchors. + * + * Phase 13 is scheduled to split InstanceGeneralSettings into 8 cards with + * anchor slugs. Until that ships, the palette exposes stub entries that + * route to `/instance/settings/general#` — once Phase 13 lands the + * hash will resolve to the correct card via scrollIntoView, and if it + * hasn't the user lands on the general settings page unchanged. + */ +const SETTINGS_SECTIONS: ReadonlyArray<{ slug: string; label: string }> = [ + { slug: "profile", label: "Profile" }, + { slug: "appearance", label: "Appearance" }, + { slug: "notifications", label: "Notifications" }, + { slug: "keyboard", label: "Keyboard Shortcuts" }, + { slug: "providers", label: "AI Providers" }, + { slug: "voice", label: "Voice" }, + { slug: "integrations", label: "Integrations" }, + { slug: "advanced", label: "Advanced" }, +]; export function CommandPalette() { - const [open, setOpen] = useState(false); + const { open, setOpen } = useCommandPalette(); const [query, setQuery] = useState(""); const navigate = useNavigate(); const { selectedCompanyId } = useCompany(); @@ -42,17 +69,11 @@ export function CommandPalette() { const { isMobile, setSidebarOpen } = useSidebar(); const searchQuery = query.trim(); + // Close-on-mobile-open side effect lives alongside the palette itself + // because the CommandPaletteProvider doesn't know about the sidebar. useEffect(() => { - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setOpen(true); - if (isMobile) setSidebarOpen(false); - } - } - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [isMobile, setSidebarOpen]); + if (open && isMobile) setSidebarOpen(false); + }, [open, isMobile, setSidebarOpen]); useEffect(() => { if (!open) setQuery(""); @@ -86,6 +107,17 @@ export function CommandPalette() { [allProjects], ); + // Conversations index — Phase 14 extension. Uses a non-company query key + // to avoid colliding with Assistant page's own list cache. + const { data: conversationsData } = useQuery({ + queryKey: ["command-palette", "conversations", selectedCompanyId], + queryFn: () => + chatApi.listConversations(selectedCompanyId!, { limit: 25 }), + enabled: !!selectedCompanyId && open, + staleTime: 30_000, + }); + const conversations = conversationsData?.items ?? []; + function go(path: string) { setOpen(false); navigate(path); @@ -102,12 +134,15 @@ export function CommandPalette() { ); return ( - { + { setOpen(v); if (v && isMobile) setSidebarOpen(false); - }}> + }} + > @@ -148,6 +183,10 @@ export function CommandPalette() { Create new project + go("/assistant")}> + + New conversation + @@ -177,6 +216,14 @@ export function CommandPalette() { Agents + go("/assistant")}> + + Assistant + + go("/content-studio")}> + + Content Studio + go("/costs")}> Costs @@ -244,6 +291,75 @@ export function CommandPalette() { )} + + {conversations.length > 0 && ( + <> + + + {conversations.slice(0, 10).map((conv) => ( + go(`/assistant/${conv.id}`)} + > + + + {conv.title || "Untitled conversation"} + + + ))} + + + )} + + + + {WORKSHOPS.map((w) => ( + go(`/content-studio/${w.slug}`)} + > + + {w.title} + + {w.subtitle} + + + ))} + + + + + {SETTINGS_SECTIONS.map((section) => ( + + go(`/instance/settings/general#${section.slug}`) + } + > + + {section.label} + + ))} + + + {/* Recipes — stubbed. The recipe API is scheduled for v1.8; until it + lands we expose an entry that navigates to the (non-existent) + /recipes route, which currently falls through to NotFound. Once + the API exists this group will switch to useQuery. */} + + + { + /* no-op — v1.8 */ + }} + > + + + Recipes (coming in v1.8) + + + ); diff --git a/ui/src/hooks/useKeyboardShortcuts.ts b/ui/src/hooks/useKeyboardShortcuts.ts index 417a39f8..4f8134ae 100644 --- a/ui/src/hooks/useKeyboardShortcuts.ts +++ b/ui/src/hooks/useKeyboardShortcuts.ts @@ -14,6 +14,7 @@ export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePanel, + onSearch, }: ShortcutHandlers) { useEffect(() => { if (!enabled) return; @@ -52,5 +53,5 @@ export function useKeyboardShortcuts({ document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [enabled, onNewIssue, onToggleSidebar, onTogglePanel]); + }, [enabled, onNewIssue, onToggleSidebar, onTogglePanel, onSearch]); }