Files
claude-web-ui/frontend/src/App.jsx
Nikolas Syring 9eb0ecfb57 feat: Add SSH remote execution for multi-host Claude sessions
- Backend: SSH execution via spawn() with -T flag for JSON streaming
- Backend: PATH setup for non-login shells on remote hosts
- Backend: History loading via SSH (tail -n 2000 for large files)
- Frontend: Host selector UI with colored buttons in Sidebar
- Frontend: Auto-select first project when host changes
- Frontend: Pass host parameter to history and session APIs
- Docker: Install openssh-client, mount SSH keys

Enables running Claude sessions on remote hosts via SSH while
viewing them through the web UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 22:59:34 +01:00

227 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useEffect, useCallback } from 'react';
import { useClaudeSession } from './hooks/useClaudeSession';
import { MessageList } from './components/MessageList';
import { ChatInput } from './components/ChatInput';
import { Sidebar } from './components/Sidebar';
import { Header } from './components/Header';
// Slash command definitions
const SLASH_COMMANDS = {
clear: {
description: 'Clear chat history (UI only)',
execute: ({ clearMessages, addSystemMessage }) => {
clearMessages();
addSystemMessage('Chat cleared');
}
},
help: {
description: 'Show available commands',
execute: ({ addSystemMessage }) => {
const helpText = Object.entries(SLASH_COMMANDS)
.map(([cmd, { description }]) => `/${cmd} - ${description}`)
.join('\n');
addSystemMessage(`Available commands:\n${helpText}`);
}
},
export: {
description: 'Export chat as Markdown',
execute: ({ messages, addSystemMessage }) => {
const markdown = messages.map(m => {
if (m.type === 'user') return `**You:** ${m.content}`;
if (m.type === 'assistant') return `**Claude:** ${m.content}`;
if (m.type === 'tool_use') return `> Tool: ${m.tool}`;
if (m.type === 'system') return `_${m.content}_`;
return '';
}).filter(Boolean).join('\n\n');
const blob = new Blob([markdown], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-chat-${new Date().toISOString().slice(0,10)}.md`;
a.click();
URL.revokeObjectURL(url);
addSystemMessage('Chat exported as Markdown');
}
},
scroll: {
description: 'Scroll to top or bottom (/scroll top|bottom)',
execute: ({ args, addSystemMessage }) => {
const direction = args[0] || 'bottom';
const container = document.querySelector('.overflow-y-auto');
if (container) {
if (direction === 'top') {
container.scrollTo({ top: 0, behavior: 'smooth' });
} else {
container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
}
}
addSystemMessage(`Scrolled to ${direction}`);
}
},
new: {
description: 'Start a new session (clears history)',
execute: ({ clearMessages, stopSession, startSession, selectedProject, addSystemMessage }) => {
stopSession();
clearMessages();
setTimeout(() => {
startSession(selectedProject, false); // false = don't resume
}, 500);
addSystemMessage('Starting new session...');
}
},
info: {
description: 'Show session info',
execute: ({ connected, sessionActive, currentProject, messages, addSystemMessage }) => {
const info = [
`Connected: ${connected ? 'Yes' : 'No'}`,
`Session: ${sessionActive ? 'Active' : 'Inactive'}`,
`Project: ${currentProject || 'None'}`,
`Messages: ${messages.length}`
].join('\n');
addSystemMessage(info);
}
}
};
function App() {
const {
connected,
sessionActive,
messages,
currentProject,
isProcessing,
error,
startSession,
sendMessage,
stopSession,
clearMessages,
setError,
setMessages
} = useClaudeSession();
const [selectedProject, setSelectedProject] = useState('/projects/claude-web-ui');
const [selectedHost, setSelectedHost] = useState('local');
const [sidebarOpen, setSidebarOpen] = useState(true);
const [resumeSession, setResumeSession] = useState(true);
// Add system message helper
const addSystemMessage = useCallback((content) => {
setMessages(prev => [...prev, {
type: 'system',
content,
timestamp: Date.now()
}]);
}, [setMessages]);
const handleStartSession = () => {
startSession(selectedProject, resumeSession, selectedHost);
};
// Handle slash commands
const handleCommand = useCallback((command, args) => {
const cmd = SLASH_COMMANDS[command.toLowerCase()];
if (cmd) {
cmd.execute({
clearMessages,
addSystemMessage,
messages,
stopSession,
startSession,
selectedProject,
connected,
sessionActive,
currentProject,
args
});
return true;
}
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 parts = message.slice(1).split(' ');
const command = parts[0];
const args = parts.slice(1);
if (handleCommand(command, args)) {
return; // Command handled
} else {
addSystemMessage(`Unknown command: /${command}. Type /help for available commands.`);
return;
}
}
// Regular message
sendMessage(message);
};
return (
<div className="flex h-screen bg-dark-950">
{/* Sidebar */}
<Sidebar
open={sidebarOpen}
onToggle={() => setSidebarOpen(!sidebarOpen)}
selectedProject={selectedProject}
onSelectProject={setSelectedProject}
selectedHost={selectedHost}
onSelectHost={setSelectedHost}
sessionActive={sessionActive}
onStartSession={handleStartSession}
onStopSession={stopSession}
onClearMessages={clearMessages}
resumeSession={resumeSession}
onToggleResume={() => setResumeSession(!resumeSession)}
/>
{/* Main Content */}
<div className="flex-1 flex flex-col min-w-0">
<Header
connected={connected}
sessionActive={sessionActive}
currentProject={currentProject}
isProcessing={isProcessing}
onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
/>
{/* Error Banner */}
{error && (
<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)}
className="text-red-400 hover:text-red-300"
>
×
</button>
</div>
)}
{/* Messages */}
<MessageList messages={messages} isProcessing={isProcessing} />
{/* Input */}
<ChatInput
onSend={handleSendMessage}
disabled={!sessionActive || isProcessing}
placeholder={
!connected
? 'Connecting...'
: !sessionActive
? 'Start a session to begin'
: isProcessing
? 'Claude is thinking...'
: 'Type your message...'
}
/>
</div>
</div>
);
}
export default App;