feat(command-palette): add global Cmd+K palette with v1 actions

This commit is contained in:
simosmik
2026-04-30 06:36:06 +00:00
parent 33d3be6e73
commit 2811c00eb8
2 changed files with 101 additions and 0 deletions

View File

@@ -1,8 +1,10 @@
import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Sidebar from '../sidebar/view/Sidebar';
import MainContent from '../main-content/view/MainContent';
import CommandPalette from '../command-palette/CommandPalette';
import { useWebSocket } from '../../contexts/WebSocketContext';
import { useDeviceSettings } from '../../hooks/useDeviceSettings';
import { useSessionProtection } from '../../hooks/useSessionProtection';
@@ -40,6 +42,7 @@ export default function AppContent() {
openSettings,
refreshProjectsSilently,
sidebarSharedProps,
handleNewSession,
} = useProjectsState({
sessionId,
navigate,
@@ -202,6 +205,11 @@ export default function AppContent() {
/>
</div>
<CommandPalette
selectedProject={selectedProject}
onStartNewChat={handleNewSession}
onOpenSettings={() => openSettings()}
/>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import * as React from 'react';
import { MessageSquarePlus, Settings, SunMoon } from 'lucide-react';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Dialog,
DialogContent,
DialogTitle,
} from '../../shared/view/ui';
import { useTheme } from '../../contexts/ThemeContext';
import type { Project } from '../../types/app';
type CommandPaletteProps = {
selectedProject: Project | null;
onStartNewChat: (project: Project) => void;
onOpenSettings: () => void;
};
export default function CommandPalette({
selectedProject,
onStartNewChat,
onOpenSettings,
}: CommandPaletteProps) {
const [open, setOpen] = React.useState(false);
const { toggleDarkMode } = useTheme();
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const isCmdK = (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k';
if (!isCmdK) return;
e.preventDefault();
setOpen((prev) => !prev);
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
const run = React.useCallback((fn: () => void) => {
setOpen(false);
fn();
}, []);
const startNewChatDisabled = !selectedProject;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-xl overflow-hidden p-0">
<DialogTitle>Command palette</DialogTitle>
<Command label="Command palette">
<CommandInput placeholder="Type a command or search…" />
<CommandList>
<CommandEmpty>No results.</CommandEmpty>
<CommandGroup heading="Actions">
<CommandItem
value="start new chat"
disabled={startNewChatDisabled}
onSelect={() => {
if (!selectedProject) return;
run(() => onStartNewChat(selectedProject));
}}
>
<MessageSquarePlus className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1">Start new chat</span>
{startNewChatDisabled && (
<span className="text-xs text-muted-foreground">Select a project first</span>
)}
</CommandItem>
<CommandItem
value="open settings"
onSelect={() => run(onOpenSettings)}
>
<Settings className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1">Open settings</span>
</CommandItem>
<CommandItem
value="toggle theme dark light mode"
onSelect={() => run(toggleDarkMode)}
>
<SunMoon className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
<span className="flex-1">Toggle theme</span>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
);
}