From f861b3fca1be3b9acc54a794a3a85928da41ebb1 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Mon, 9 Feb 2026 16:57:49 +0800 Subject: [PATCH] feat: slash command popover redesign, import dialog scroll fix, and new logo - Slash command popover: add search box, group commands/skills with section headers, assign unique icons per built-in command, remove Built-in badge, show source labels for skills, deduplicate API skills with built-in names - Import CLI Session dialog: fix list overflow and enable scrolling by replacing Radix ScrollArea with native overflow-y-auto - New chat empty state: replace BotIcon with custom CodePilotLogo SVG component supporting light/dark theme Co-Authored-By: Claude Opus 4.6 --- src/components/chat/CodePilotLogo.tsx | 75 ++++++ src/components/chat/MessageInput.tsx | 227 ++++++++++++------ src/components/chat/MessageList.tsx | 9 +- src/components/layout/ImportSessionDialog.tsx | 8 +- 4 files changed, 235 insertions(+), 84 deletions(-) create mode 100644 src/components/chat/CodePilotLogo.tsx diff --git a/src/components/chat/CodePilotLogo.tsx b/src/components/chat/CodePilotLogo.tsx new file mode 100644 index 0000000..6ad9ddd --- /dev/null +++ b/src/components/chat/CodePilotLogo.tsx @@ -0,0 +1,75 @@ +import { cn } from '@/lib/utils'; + +interface CodePilotLogoProps { + className?: string; +} + +export function CodePilotLogo({ className }: CodePilotLogoProps) { + return ( + + {/* Background: gray in light, dark in dark mode */} + + + + + {/* Top Face */} + + {/* Left Face */} + + {/* Right Face */} + + + + + {/* Layer Z = -1 (Bottom) */} + + + + + + + {/* Layer Z = 0 (Middle) */} + + + + + + + + + + + {/* Layer Z = 1 (Top) */} + + + + + + + ); +} diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index e9460bc..78913b2 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -4,7 +4,6 @@ import { useRef, useState, useCallback, useEffect, type KeyboardEvent, type Form import { HugeiconsIcon } from "@hugeicons/react"; import { AtIcon, - DivideSignIcon, FolderOpenIcon, Wrench01Icon, ClipboardIcon, @@ -13,6 +12,14 @@ import { CommandLineIcon, Attachment01Icon, Cancel01Icon, + Delete02Icon, + Coins01Icon, + FileZipIcon, + Stethoscope02Icon, + FileEditIcon, + SearchList01Icon, + BrainIcon, + GlobalIcon, } from "@hugeicons/core-free-icons"; import { cn } from '@/lib/utils'; import { FolderPicker } from './FolderPicker'; @@ -64,6 +71,7 @@ interface PopoverItem { builtIn?: boolean; immediate?: boolean; installedSource?: "agents" | "claude"; + icon?: typeof CommandLineIcon; } interface CommandBadge { @@ -85,15 +93,15 @@ const COMMAND_PROMPTS: Record = { }; const BUILT_IN_COMMANDS: PopoverItem[] = [ - { label: 'help', value: '/help', description: 'Show available commands and tips', builtIn: true, immediate: true }, - { label: 'clear', value: '/clear', description: 'Clear conversation history', builtIn: true, immediate: true }, - { label: 'cost', value: '/cost', description: 'Show token usage statistics', builtIn: true, immediate: true }, - { label: 'compact', value: '/compact', description: 'Compress conversation context', builtIn: true }, - { label: 'doctor', value: '/doctor', description: 'Diagnose project health', builtIn: true }, - { label: 'init', value: '/init', description: 'Initialize CLAUDE.md for project', builtIn: true }, - { label: 'review', value: '/review', description: 'Review code quality', builtIn: true }, - { label: 'terminal-setup', value: '/terminal-setup', description: 'Configure terminal settings', builtIn: true }, - { label: 'memory', value: '/memory', description: 'Edit project memory file', builtIn: true }, + { label: 'help', value: '/help', description: 'Show available commands and tips', builtIn: true, immediate: true, icon: HelpCircleIcon }, + { label: 'clear', value: '/clear', description: 'Clear conversation history', builtIn: true, immediate: true, icon: Delete02Icon }, + { label: 'cost', value: '/cost', description: 'Show token usage statistics', builtIn: true, immediate: true, icon: Coins01Icon }, + { label: 'compact', value: '/compact', description: 'Compress conversation context', builtIn: true, icon: FileZipIcon }, + { label: 'doctor', value: '/doctor', description: 'Diagnose project health', builtIn: true, icon: Stethoscope02Icon }, + { label: 'init', value: '/init', description: 'Initialize CLAUDE.md for project', builtIn: true, icon: FileEditIcon }, + { label: 'review', value: '/review', description: 'Review code quality', builtIn: true, icon: SearchList01Icon }, + { label: 'terminal-setup', value: '/terminal-setup', description: 'Configure terminal settings', builtIn: true, icon: CommandLineIcon }, + { label: 'memory', value: '/memory', description: 'Edit project memory file', builtIn: true, icon: BrainIcon }, ]; interface ModeOption { @@ -291,6 +299,7 @@ export function MessageInput({ }: MessageInputProps) { const textareaRef = useRef(null); const popoverRef = useRef(null); + const searchInputRef = useRef(null); const modeMenuRef = useRef(null); const modelMenuRef = useRef(null); @@ -358,11 +367,8 @@ export function MessageInput({ }, [sessionId]); // Fetch skills for / command (built-in + API) - const fetchSkills = useCallback(async (filter: string) => { - const builtIn = BUILT_IN_COMMANDS.filter((cmd) => - cmd.label.toLowerCase().includes(filter.toLowerCase()) - ); - + // Returns all items unfiltered — filtering is done by filteredItems + const fetchSkills = useCallback(async () => { let apiSkills: PopoverItem[] = []; try { const res = await fetch('/api/skills'); @@ -370,26 +376,23 @@ export function MessageInput({ const data = await res.json(); const skills = data.skills || []; apiSkills = skills - .filter((s: { name: string }) => s.name.toLowerCase().includes(filter.toLowerCase())) - .map((s: { name: string; description: string; source?: string; installedSource?: "agents" | "claude" }) => { - const sourceHint = - s.source === "installed" && s.installedSource - ? ` (${s.installedSource})` - : ""; - return { - label: s.name, - value: `/${s.name}`, - description: `${s.description || ""}${sourceHint}`, - builtIn: false, - installedSource: s.installedSource, - }; - }); + .map((s: { name: string; description: string; source?: string; installedSource?: "agents" | "claude" }) => ({ + label: s.name, + value: `/${s.name}`, + description: s.description || "", + builtIn: false, + installedSource: s.installedSource, + })); } } catch { // API not available - just use built-in commands } - return [...builtIn, ...apiSkills].slice(0, 20); + // Deduplicate: remove API skills that share a name with built-in commands + const builtInNames = new Set(BUILT_IN_COMMANDS.map(c => c.label)); + const uniqueSkills = apiSkills.filter(s => !builtInNames.has(s.label)); + + return [...BUILT_IN_COMMANDS, ...uniqueSkills]; }, []); // Close popover @@ -479,7 +482,7 @@ export function MessageInput({ setPopoverFilter(filter); setTriggerPos(cursorPos - slashMatch[2].length - 1); setSelectedIndex(0); - const items = await fetchSkills(filter); + const items = await fetchSkills(); setPopoverItems(items); return; } @@ -701,49 +704,127 @@ export function MessageInput({
{/* Popover */} - {popoverMode && filteredItems.length > 0 && ( -
-
- {popoverMode === 'file' ? 'Files' : 'Commands'} + {popoverMode && filteredItems.length > 0 && (() => { + const builtInItems = filteredItems.filter(item => item.builtIn); + const skillItems = filteredItems.filter(item => !item.builtIn); + let globalIdx = 0; + + const renderItem = (item: PopoverItem, idx: number) => ( + + ); + + return ( +
+ {popoverMode === 'skill' ? ( +
+ { + const val = e.target.value; + setPopoverFilter(val); + setSelectedIndex(0); + // Sync textarea: replace the filter portion after / + if (triggerPos !== null) { + const before = inputValue.slice(0, triggerPos + 1); + setInputValue(before + val); + } + }} + onKeyDown={(e) => { + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((prev) => (prev + 1) % filteredItems.length); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((prev) => (prev - 1 + filteredItems.length) % filteredItems.length); + } else if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault(); + if (filteredItems[selectedIndex]) { + insertItem(filteredItems[selectedIndex]); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + closePopover(); + textareaRef.current?.focus(); + } + }} + className="w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground outline-none" + autoFocus + /> +
+ ) : ( +
+ Files +
+ )} +
+ {popoverMode === 'file' ? ( + filteredItems.map((item, i) => renderItem(item, i)) + ) : ( + <> + {builtInItems.length > 0 && ( + <> +
+ Commands +
+ {builtInItems.map((item) => { + const idx = globalIdx++; + return renderItem(item, idx); + })} + + )} + {skillItems.length > 0 && ( + <> +
+ Skills +
+ {skillItems.map((item) => { + const idx = globalIdx++; + return renderItem(item, idx); + })} + + )} + + )} +
-
- {filteredItems.map((item, i) => ( - - ))} -
-
- )} + ); + })()} {/* PromptInput replaces the old input area */} - -
- } + icon={} />
); diff --git a/src/components/layout/ImportSessionDialog.tsx b/src/components/layout/ImportSessionDialog.tsx index f91f73b..897745a 100644 --- a/src/components/layout/ImportSessionDialog.tsx +++ b/src/components/layout/ImportSessionDialog.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; + import { Badge } from "@/components/ui/badge"; import { HugeiconsIcon } from "@hugeicons/react"; import { @@ -147,7 +147,7 @@ export function ImportSessionDialog({ return ( - + +
{loading ? (
@@ -316,7 +316,7 @@ export function ImportSessionDialog({ }) )}
- +
);