diff --git a/app/ui_layer/adapters/browser_adapter.py b/app/ui_layer/adapters/browser_adapter.py index 84fec510a..7c3bca8e8 100644 --- a/app/ui_layer/adapters/browser_adapter.py +++ b/app/ui_layer/adapters/browser_adapter.py @@ -1686,6 +1686,10 @@ async def _handle_ws_message(self, data: Dict[str, Any], ws=None) -> None: env_value = data.get("value", "") await self._handle_mcp_update_env(name, env_key, env_value) + # Slash command list (for autocomplete) + elif msg_type == "command_list": + await self._handle_command_list() + # Skill settings operations elif msg_type == "skill_list": await self._handle_skill_list() @@ -5116,6 +5120,34 @@ async def _handle_mcp_update_env( # Skill Settings Handlers # ───────────────────────────────────────────────────────────────────── + async def _handle_command_list(self) -> None: + """Get list of registered non-skill slash commands for autocomplete.""" + try: + from app.ui_layer.commands.builtin.skill_invoke import SkillInvokeCommand + + cmds = self._controller.command_registry.list_commands(include_hidden=False) + commands = [ + {"name": c.name.lstrip("/"), "description": c.description} + for c in cmds + if not isinstance(c, SkillInvokeCommand) + ] + await self._broadcast({ + "type": "command_list", + "data": { + "success": True, + "commands": commands, + }, + }) + except Exception as e: + await self._broadcast({ + "type": "command_list", + "data": { + "success": False, + "error": str(e), + "commands": [], + }, + }) + async def _handle_skill_list(self) -> None: """Get list of all skills.""" try: diff --git a/app/ui_layer/browser/frontend/src/components/Chat/Chat.module.css b/app/ui_layer/browser/frontend/src/components/Chat/Chat.module.css index 09c049731..913f5ed69 100644 --- a/app/ui_layer/browser/frontend/src/components/Chat/Chat.module.css +++ b/app/ui_layer/browser/frontend/src/components/Chat/Chat.module.css @@ -185,6 +185,7 @@ min-width: 0; border-radius: var(--radius-md); transition: outline var(--transition-fast), background var(--transition-fast); + position: relative; } .inputWrapperDragOver { diff --git a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx index 14ea658d3..f0029971c 100644 --- a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx +++ b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx @@ -4,7 +4,8 @@ import { Send, Paperclip, X, Loader2, File, AlertCircle, Reply, Mic, MicOff, Che import { useVirtualizer } from '@tanstack/react-virtual' import { useWebSocket } from '../../contexts/WebSocketContext' import { useToast } from '../../contexts/ToastContext' -import { Button, IconButton, StatusIndicator } from '../ui' +import { Button, IconButton, SlashCommandAutocomplete, StatusIndicator } from '../ui' +import type { SlashCommandAutocompleteHandle } from '../ui' import { useDerivedAgentStatus } from '../../hooks' import { ChatMessageItem } from '../../pages/Chat/ChatMessage' import styles from './Chat.module.css' @@ -128,6 +129,7 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { const [isDragOver, setIsDragOver] = useState(false) const [previewAttachment, setPreviewAttachment] = useState(null) const inputRef = useRef(null) + const autocompleteRef = useRef(null) const fileInputRef = useRef(null) // Voice input state @@ -404,8 +406,29 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { } const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Tab' && !e.shiftKey) { + if (autocompleteRef.current?.handleTab()) { + e.preventDefault() + return + } + } + if (e.key === 'ArrowUp') { + if (autocompleteRef.current?.handleUpArrow()) { + e.preventDefault() + return + } + } + if (e.key === 'ArrowDown') { + if (autocompleteRef.current?.handleDownArrow()) { + e.preventDefault() + return + } + } if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() + if (autocompleteRef.current?.handleEnter()) { + return + } handleSend() } else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { const history = inputHistoryRef.current @@ -422,6 +445,10 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { setInput(history[historyIndexRef.current]) } else if (e.key === 'ArrowDown') { e.preventDefault() + if (autocompleteRef.current?.handleDownArrow()) { + e.preventDefault() + return + } if (historyIndexRef.current === -1) return if (historyIndexRef.current < history.length - 1) { historyIndexRef.current++ @@ -625,13 +652,13 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { )} - + {/* Status bar */}
{status.message}
- + {/* Input area */}
@@ -727,7 +754,15 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { ))}
)} - + + { + setInput(`/${name}`) + inputRef.current?.focus() + }} + />