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 { 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#<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() {
|
||||
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 (
|
||||
<CommandDialog open={open} onOpenChange={(v) => {
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
setOpen(v);
|
||||
if (v && isMobile) setSidebarOpen(false);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<CommandInput
|
||||
placeholder="Search issues, agents, projects..."
|
||||
placeholder="Search issues, agents, projects, conversations, settings..."
|
||||
value={query}
|
||||
onValueChange={setQuery}
|
||||
/>
|
||||
|
|
@ -148,6 +183,10 @@ export function CommandPalette() {
|
|||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create new project
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => go("/assistant")}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
New conversation
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
|
|
@ -177,6 +216,14 @@ export function CommandPalette() {
|
|||
<Bot className="mr-2 h-4 w-4" />
|
||||
Agents
|
||||
</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")}>
|
||||
<DollarSign className="mr-2 h-4 w-4" />
|
||||
Costs
|
||||
|
|
@ -244,6 +291,75 @@ export function CommandPalette() {
|
|||
</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>
|
||||
</CommandDialog>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue