feat(nexus): extend command palette search + fix shortcut destructure
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) <noreply@anthropic.com>
This commit is contained in:
parent
45d2a9ff24
commit
9a81f0e22b
2 changed files with 133 additions and 16 deletions
|
|
@ -1,12 +1,14 @@
|
||||||
import { useState, useEffect, useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useNavigate } from "@/lib/router";
|
import { useNavigate } from "@/lib/router";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { useDialog } from "../context/DialogContext";
|
import { useDialog } from "../context/DialogContext";
|
||||||
import { useSidebar } from "../context/SidebarContext";
|
import { useSidebar } from "../context/SidebarContext";
|
||||||
|
import { useCommandPalette } from "../context/CommandPaletteContext";
|
||||||
import { issuesApi } from "../api/issues";
|
import { issuesApi } from "../api/issues";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
import { projectsApi } from "../api/projects";
|
import { projectsApi } from "../api/projects";
|
||||||
|
import { chatApi } from "../api/chat";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
|
|
@ -29,12 +31,37 @@ import {
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
|
Settings,
|
||||||
|
Sparkles,
|
||||||
|
MessageSquare,
|
||||||
|
ChefHat,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Identity } from "./Identity";
|
import { Identity } from "./Identity";
|
||||||
import { agentUrl, projectUrl } from "../lib/utils";
|
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#<slug>` — 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() {
|
export function CommandPalette() {
|
||||||
const [open, setOpen] = useState(false);
|
const { open, setOpen } = useCommandPalette();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { selectedCompanyId } = useCompany();
|
const { selectedCompanyId } = useCompany();
|
||||||
|
|
@ -42,17 +69,11 @@ export function CommandPalette() {
|
||||||
const { isMobile, setSidebarOpen } = useSidebar();
|
const { isMobile, setSidebarOpen } = useSidebar();
|
||||||
const searchQuery = query.trim();
|
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(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
if (open && isMobile) setSidebarOpen(false);
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
}, [open, isMobile, setSidebarOpen]);
|
||||||
e.preventDefault();
|
|
||||||
setOpen(true);
|
|
||||||
if (isMobile) setSidebarOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [isMobile, setSidebarOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) setQuery("");
|
if (!open) setQuery("");
|
||||||
|
|
@ -86,6 +107,17 @@ export function CommandPalette() {
|
||||||
[allProjects],
|
[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) {
|
function go(path: string) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
navigate(path);
|
navigate(path);
|
||||||
|
|
@ -102,12 +134,15 @@ export function CommandPalette() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={(v) => {
|
<CommandDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(v) => {
|
||||||
setOpen(v);
|
setOpen(v);
|
||||||
if (v && isMobile) setSidebarOpen(false);
|
if (v && isMobile) setSidebarOpen(false);
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search issues, agents, projects..."
|
placeholder="Search issues, agents, projects, conversations, settings..."
|
||||||
value={query}
|
value={query}
|
||||||
onValueChange={setQuery}
|
onValueChange={setQuery}
|
||||||
/>
|
/>
|
||||||
|
|
@ -148,6 +183,10 @@ export function CommandPalette() {
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Create new project
|
Create new project
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/assistant")}>
|
||||||
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
|
New conversation
|
||||||
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
|
|
@ -177,6 +216,14 @@ export function CommandPalette() {
|
||||||
<Bot className="mr-2 h-4 w-4" />
|
<Bot className="mr-2 h-4 w-4" />
|
||||||
Agents
|
Agents
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/assistant")}>
|
||||||
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
|
Assistant
|
||||||
|
</CommandItem>
|
||||||
|
<CommandItem onSelect={() => go("/content-studio")}>
|
||||||
|
<Sparkles className="mr-2 h-4 w-4" />
|
||||||
|
Content Studio
|
||||||
|
</CommandItem>
|
||||||
<CommandItem onSelect={() => go("/costs")}>
|
<CommandItem onSelect={() => go("/costs")}>
|
||||||
<DollarSign className="mr-2 h-4 w-4" />
|
<DollarSign className="mr-2 h-4 w-4" />
|
||||||
Costs
|
Costs
|
||||||
|
|
@ -244,6 +291,75 @@ export function CommandPalette() {
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{conversations.length > 0 && (
|
||||||
|
<>
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading="Conversations">
|
||||||
|
{conversations.slice(0, 10).map((conv) => (
|
||||||
|
<CommandItem
|
||||||
|
key={conv.id}
|
||||||
|
onSelect={() => go(`/assistant/${conv.id}`)}
|
||||||
|
>
|
||||||
|
<MessageSquare className="mr-2 h-4 w-4" />
|
||||||
|
<span className="flex-1 truncate">
|
||||||
|
{conv.title || "Untitled conversation"}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading="Workshops">
|
||||||
|
{WORKSHOPS.map((w) => (
|
||||||
|
<CommandItem
|
||||||
|
key={w.slug}
|
||||||
|
onSelect={() => go(`/content-studio/${w.slug}`)}
|
||||||
|
>
|
||||||
|
<w.icon className="mr-2 h-4 w-4" />
|
||||||
|
<span className="flex-1 truncate">{w.title}</span>
|
||||||
|
<span className="text-xs text-muted-foreground ml-2">
|
||||||
|
{w.subtitle}
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading="Settings">
|
||||||
|
{SETTINGS_SECTIONS.map((section) => (
|
||||||
|
<CommandItem
|
||||||
|
key={section.slug}
|
||||||
|
onSelect={() =>
|
||||||
|
go(`/instance/settings/general#${section.slug}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
{section.label}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
|
||||||
|
{/* 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. */}
|
||||||
|
<CommandSeparator />
|
||||||
|
<CommandGroup heading="Recipes">
|
||||||
|
<CommandItem
|
||||||
|
disabled
|
||||||
|
onSelect={() => {
|
||||||
|
/* no-op — v1.8 */
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChefHat className="mr-2 h-4 w-4" />
|
||||||
|
<span className="flex-1 text-muted-foreground">
|
||||||
|
Recipes (coming in v1.8)
|
||||||
|
</span>
|
||||||
|
</CommandItem>
|
||||||
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export function useKeyboardShortcuts({
|
||||||
onNewIssue,
|
onNewIssue,
|
||||||
onToggleSidebar,
|
onToggleSidebar,
|
||||||
onTogglePanel,
|
onTogglePanel,
|
||||||
|
onSearch,
|
||||||
}: ShortcutHandlers) {
|
}: ShortcutHandlers) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
|
|
@ -52,5 +53,5 @@ export function useKeyboardShortcuts({
|
||||||
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [enabled, onNewIssue, onToggleSidebar, onTogglePanel]);
|
}, [enabled, onNewIssue, onToggleSidebar, onTogglePanel, onSearch]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue