feat: Major UI improvements and SSH-only mode
- Tool rendering: Unified tool_use/tool_result cards with collapsible results - Special rendering for WebSearch, WebFetch, Task, Write tools - File upload support with drag & drop - Permission dialog for tool approvals - Status bar with session stats and permission mode toggle - SSH-only mode: Removed local container execution - Host switching disabled during active session with visual indicator - Directory browser: Browse remote directories via SSH - Recent directories dropdown with localStorage persistence - Follow-up messages during generation - Improved scroll behavior with "back to bottom" button 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import { MessageList } from './components/MessageList';
|
||||
import { ChatInput } from './components/ChatInput';
|
||||
import { Sidebar } from './components/Sidebar';
|
||||
import { Header } from './components/Header';
|
||||
import { StatusBar } from './components/StatusBar';
|
||||
import { PermissionDialog } from './components/PermissionDialog';
|
||||
|
||||
// Slash command definitions
|
||||
const SLASH_COMMANDS = {
|
||||
@@ -81,6 +83,37 @@ const SLASH_COMMANDS = {
|
||||
].join('\n');
|
||||
addSystemMessage(info);
|
||||
}
|
||||
},
|
||||
history: {
|
||||
description: 'Search input history (/history <term>)',
|
||||
execute: ({ args, addSystemMessage }) => {
|
||||
const HISTORY_KEY = 'claude-webui-input-history';
|
||||
try {
|
||||
const stored = localStorage.getItem(HISTORY_KEY);
|
||||
const history = stored ? JSON.parse(stored) : [];
|
||||
const searchTerm = args.join(' ').toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
// Show recent history
|
||||
const recent = history.slice(0, 10);
|
||||
if (recent.length === 0) {
|
||||
addSystemMessage('No input history found.');
|
||||
} else {
|
||||
addSystemMessage(`Recent inputs:\n${recent.map((h, i) => `${i + 1}. ${h.length > 80 ? h.slice(0, 80) + '...' : h}`).join('\n')}`);
|
||||
}
|
||||
} else {
|
||||
// Search history
|
||||
const matches = history.filter(h => h.toLowerCase().includes(searchTerm)).slice(0, 10);
|
||||
if (matches.length === 0) {
|
||||
addSystemMessage(`No history entries matching "${searchTerm}"`);
|
||||
} else {
|
||||
addSystemMessage(`History matching "${searchTerm}":\n${matches.map((h, i) => `${i + 1}. ${h.length > 80 ? h.slice(0, 80) + '...' : h}`).join('\n')}`);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
addSystemMessage('Failed to read input history.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,20 +121,29 @@ function App() {
|
||||
const {
|
||||
connected,
|
||||
sessionActive,
|
||||
sessionId,
|
||||
messages,
|
||||
currentProject,
|
||||
currentHost,
|
||||
isProcessing,
|
||||
error,
|
||||
sessionStats,
|
||||
permissionMode,
|
||||
controlInitialized,
|
||||
pendingPermission,
|
||||
startSession,
|
||||
sendMessage,
|
||||
stopSession,
|
||||
stopGeneration,
|
||||
clearMessages,
|
||||
changePermissionMode,
|
||||
respondToPermission,
|
||||
setError,
|
||||
setMessages
|
||||
} = useClaudeSession();
|
||||
|
||||
const [selectedProject, setSelectedProject] = useState('/projects/claude-web-ui');
|
||||
const [selectedHost, setSelectedHost] = useState('local');
|
||||
const [selectedProject, setSelectedProject] = useState('/home/sumdex/projects');
|
||||
const [selectedHost, setSelectedHost] = useState('neko');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [resumeSession, setResumeSession] = useState(true);
|
||||
|
||||
@@ -114,9 +156,29 @@ function App() {
|
||||
}]);
|
||||
}, [setMessages]);
|
||||
|
||||
const handleStartSession = () => {
|
||||
const handleStartSession = useCallback(() => {
|
||||
startSession(selectedProject, resumeSession, selectedHost);
|
||||
};
|
||||
}, [startSession, selectedProject, resumeSession, selectedHost]);
|
||||
|
||||
const handleSelectProject = useCallback((path) => {
|
||||
setSelectedProject(path);
|
||||
}, []);
|
||||
|
||||
const handleSelectHost = useCallback((host) => {
|
||||
setSelectedHost(host);
|
||||
}, []);
|
||||
|
||||
const handleToggleSidebar = useCallback(() => {
|
||||
setSidebarOpen(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleToggleResume = useCallback(() => {
|
||||
setResumeSession(prev => !prev);
|
||||
}, []);
|
||||
|
||||
const handleClearError = useCallback(() => {
|
||||
setError(null);
|
||||
}, [setError]);
|
||||
|
||||
// Handle slash commands
|
||||
const handleCommand = useCallback((command, args) => {
|
||||
@@ -139,11 +201,9 @@ function App() {
|
||||
return false;
|
||||
}, [clearMessages, addSystemMessage, messages, stopSession, startSession, selectedProject, connected, sessionActive, currentProject]);
|
||||
|
||||
const handleSendMessage = (message) => {
|
||||
if (!message.trim()) return;
|
||||
|
||||
// Check for slash command
|
||||
if (message.startsWith('/')) {
|
||||
const handleSendMessage = useCallback((message, attachedFiles = []) => {
|
||||
// Check for slash command (only if no files attached)
|
||||
if (message.startsWith('/') && attachedFiles.length === 0) {
|
||||
const parts = message.slice(1).split(' ');
|
||||
const command = parts[0];
|
||||
const args = parts.slice(1);
|
||||
@@ -156,26 +216,29 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
// Regular message
|
||||
sendMessage(message);
|
||||
};
|
||||
// Regular message (with optional attachments)
|
||||
if (message.trim() || attachedFiles.length > 0) {
|
||||
sendMessage(message, attachedFiles);
|
||||
}
|
||||
}, [handleCommand, addSystemMessage, sendMessage]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-dark-950">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
onToggle={() => setSidebarOpen(!sidebarOpen)}
|
||||
onToggle={handleToggleSidebar}
|
||||
selectedProject={selectedProject}
|
||||
onSelectProject={setSelectedProject}
|
||||
onSelectProject={handleSelectProject}
|
||||
selectedHost={selectedHost}
|
||||
onSelectHost={setSelectedHost}
|
||||
onSelectHost={handleSelectHost}
|
||||
sessionActive={sessionActive}
|
||||
activeHost={currentHost}
|
||||
onStartSession={handleStartSession}
|
||||
onStopSession={stopSession}
|
||||
onClearMessages={clearMessages}
|
||||
resumeSession={resumeSession}
|
||||
onToggleResume={() => setResumeSession(!resumeSession)}
|
||||
onToggleResume={handleToggleResume}
|
||||
/>
|
||||
|
||||
{/* Main Content */}
|
||||
@@ -185,7 +248,7 @@ function App() {
|
||||
sessionActive={sessionActive}
|
||||
currentProject={currentProject}
|
||||
isProcessing={isProcessing}
|
||||
onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
|
||||
onToggleSidebar={handleToggleSidebar}
|
||||
/>
|
||||
|
||||
{/* Error Banner */}
|
||||
@@ -193,7 +256,7 @@ function App() {
|
||||
<div className="bg-red-900/50 border-b border-red-800 px-4 py-2 flex justify-between items-center">
|
||||
<span className="text-red-200 text-sm">{error}</span>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
onClick={handleClearError}
|
||||
className="text-red-400 hover:text-red-300"
|
||||
>
|
||||
×
|
||||
@@ -202,23 +265,44 @@ function App() {
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<MessageList messages={messages} isProcessing={isProcessing} />
|
||||
<MessageList
|
||||
messages={messages}
|
||||
isProcessing={isProcessing}
|
||||
/>
|
||||
|
||||
{/* Status Bar */}
|
||||
<StatusBar
|
||||
sessionStats={sessionStats}
|
||||
isProcessing={isProcessing}
|
||||
connected={connected}
|
||||
permissionMode={permissionMode}
|
||||
controlInitialized={controlInitialized}
|
||||
onChangeMode={changePermissionMode}
|
||||
/>
|
||||
|
||||
{/* Input */}
|
||||
<ChatInput
|
||||
onSend={handleSendMessage}
|
||||
disabled={!sessionActive || isProcessing}
|
||||
onStop={stopGeneration}
|
||||
disabled={!sessionActive}
|
||||
isProcessing={isProcessing}
|
||||
sessionId={sessionId}
|
||||
placeholder={
|
||||
!connected
|
||||
? 'Connecting...'
|
||||
: !sessionActive
|
||||
? 'Start a session to begin'
|
||||
: isProcessing
|
||||
? 'Claude is thinking...'
|
||||
: 'Type your message...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Permission Dialog */}
|
||||
<PermissionDialog
|
||||
permission={pendingPermission}
|
||||
onAllow={(requestId) => respondToPermission(requestId, true)}
|
||||
onDeny={(requestId) => respondToPermission(requestId, false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user