feat: Add multi-host config and dynamic project scanning
Backend: - Load hosts from config/hosts.json - New /api/hosts endpoint listing available hosts - Dynamic project scanning with configurable depth - Support for local and SSH hosts (SSH execution coming next) Frontend (by Web-UI Claude): - Slash commands: /clear, /help, /export, /scroll, /new, /info - Chat export as Markdown Config: - hosts.json defines hosts with connection info and base paths - hosts.example.json as template (real config is gitignored) - Each host has name, description, color, icon, basePaths Next steps: - SSH command execution for remote hosts - Frontend host selector UI - Multi-agent collaboration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Send, Loader2 } from 'lucide-react';
|
||||
import { Send, Loader2, Command } from 'lucide-react';
|
||||
|
||||
// Available slash commands for autocomplete
|
||||
const COMMANDS = [
|
||||
{ name: 'help', description: 'Show available commands' },
|
||||
{ name: 'clear', description: 'Clear chat history' },
|
||||
{ name: 'export', description: 'Export chat as Markdown' },
|
||||
{ name: 'scroll', description: 'Scroll to top or bottom' },
|
||||
{ name: 'new', description: 'Start a new session' },
|
||||
{ name: 'info', description: 'Show session info' },
|
||||
];
|
||||
|
||||
export function ChatInput({ onSend, disabled, placeholder }) {
|
||||
const [message, setMessage] = useState('');
|
||||
const [showCommands, setShowCommands] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [filteredCommands, setFilteredCommands] = useState(COMMANDS);
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
// Auto-resize textarea
|
||||
@@ -14,15 +27,60 @@ export function ChatInput({ onSend, disabled, placeholder }) {
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
// Handle command filtering
|
||||
useEffect(() => {
|
||||
if (message.startsWith('/')) {
|
||||
const query = message.slice(1).toLowerCase();
|
||||
const filtered = COMMANDS.filter(cmd =>
|
||||
cmd.name.toLowerCase().startsWith(query)
|
||||
);
|
||||
setFilteredCommands(filtered);
|
||||
setShowCommands(filtered.length > 0 && message.length > 0);
|
||||
setSelectedIndex(0);
|
||||
} else {
|
||||
setShowCommands(false);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (message.trim() && !disabled) {
|
||||
onSend(message);
|
||||
setMessage('');
|
||||
setShowCommands(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectCommand = (cmdName) => {
|
||||
setMessage(`/${cmdName} `);
|
||||
setShowCommands(false);
|
||||
textareaRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
// Handle command selection
|
||||
if (showCommands) {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(i => Math.min(i + 1, filteredCommands.length - 1));
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex(i => Math.max(i - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Tab' || (e.key === 'Enter' && filteredCommands.length > 0)) {
|
||||
e.preventDefault();
|
||||
selectCommand(filteredCommands[selectedIndex].name);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setShowCommands(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
@@ -33,6 +91,25 @@ export function ChatInput({ onSend, disabled, placeholder }) {
|
||||
<form onSubmit={handleSubmit} className="p-4 border-t border-dark-800 bg-dark-900">
|
||||
<div className="flex gap-3 items-end max-w-4xl mx-auto">
|
||||
<div className="flex-1 relative">
|
||||
{/* Command autocomplete dropdown */}
|
||||
{showCommands && (
|
||||
<div className="absolute bottom-full left-0 right-0 mb-2 bg-dark-800 border border-dark-700 rounded-lg shadow-xl overflow-hidden z-10">
|
||||
{filteredCommands.map((cmd, index) => (
|
||||
<button
|
||||
key={cmd.name}
|
||||
type="button"
|
||||
onClick={() => selectCommand(cmd.name)}
|
||||
className={`w-full px-4 py-2.5 flex items-center gap-3 text-left transition-colors
|
||||
${index === selectedIndex ? 'bg-orange-600/20 text-orange-400' : 'hover:bg-dark-700 text-dark-200'}`}
|
||||
>
|
||||
<Command className="w-4 h-4 text-dark-500" />
|
||||
<span className="font-medium">/{cmd.name}</span>
|
||||
<span className="text-dark-500 text-sm">{cmd.description}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={message}
|
||||
@@ -50,7 +127,7 @@ export function ChatInput({ onSend, disabled, placeholder }) {
|
||||
`}
|
||||
/>
|
||||
<div className="absolute right-2 bottom-2 text-xs text-dark-600">
|
||||
Shift+Enter for newline
|
||||
{message.startsWith('/') ? 'Tab to complete' : 'Shift+Enter for newline'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user