| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>Manus AI Terminal</title> |
| | <style> |
| | * { |
| | margin: 0; |
| | padding: 0; |
| | box-sizing: border-box; |
| | } |
| | |
| | body { |
| | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; |
| | background: #0d1117; |
| | color: #c9d1d9; |
| | height: 100vh; |
| | overflow: hidden; |
| | } |
| | |
| | .terminal-container { |
| | display: flex; |
| | flex-direction: column; |
| | height: 100vh; |
| | background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); |
| | border: 1px solid #30363d; |
| | } |
| | |
| | .terminal-header { |
| | display: flex; |
| | align-items: center; |
| | justify-content: space-between; |
| | padding: 12px 16px; |
| | background: #161b22; |
| | border-bottom: 1px solid #30363d; |
| | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); |
| | } |
| | |
| | .terminal-title { |
| | display: flex; |
| | align-items: center; |
| | gap: 8px; |
| | font-size: 14px; |
| | font-weight: 600; |
| | color: #f0f6fc; |
| | } |
| | |
| | .terminal-icon { |
| | width: 16px; |
| | height: 16px; |
| | background: #238636; |
| | border-radius: 50%; |
| | position: relative; |
| | } |
| | |
| | .terminal-icon::after { |
| | content: '>'; |
| | position: absolute; |
| | top: 50%; |
| | left: 50%; |
| | transform: translate(-50%, -50%); |
| | font-size: 10px; |
| | color: white; |
| | font-weight: bold; |
| | } |
| | |
| | .terminal-controls { |
| | display: flex; |
| | gap: 8px; |
| | } |
| | |
| | .control-btn { |
| | width: 12px; |
| | height: 12px; |
| | border-radius: 50%; |
| | border: none; |
| | cursor: pointer; |
| | transition: opacity 0.2s; |
| | } |
| | |
| | .control-btn:hover { |
| | opacity: 0.8; |
| | } |
| | |
| | .close { background: #ff5f56; } |
| | .minimize { background: #ffbd2e; } |
| | .maximize { background: #27ca3f; } |
| | |
| | .terminal-body { |
| | flex: 1; |
| | display: flex; |
| | flex-direction: column; |
| | overflow: hidden; |
| | } |
| | |
| | .terminal-output { |
| | flex: 1; |
| | padding: 16px; |
| | overflow-y: auto; |
| | font-size: 13px; |
| | line-height: 1.4; |
| | background: #0d1117; |
| | scrollbar-width: thin; |
| | scrollbar-color: #30363d #0d1117; |
| | } |
| | |
| | .terminal-output::-webkit-scrollbar { |
| | width: 8px; |
| | } |
| | |
| | .terminal-output::-webkit-scrollbar-track { |
| | background: #0d1117; |
| | } |
| | |
| | .terminal-output::-webkit-scrollbar-thumb { |
| | background: #30363d; |
| | border-radius: 4px; |
| | } |
| | |
| | .terminal-output::-webkit-scrollbar-thumb:hover { |
| | background: #484f58; |
| | } |
| | |
| | .terminal-line { |
| | margin-bottom: 2px; |
| | white-space: pre-wrap; |
| | word-wrap: break-word; |
| | } |
| | |
| | .command-line { |
| | color: #58a6ff; |
| | font-weight: 600; |
| | } |
| | |
| | .output-line { |
| | color: #c9d1d9; |
| | } |
| | |
| | .error-line { |
| | color: #f85149; |
| | } |
| | |
| | .success-line { |
| | color: #56d364; |
| | } |
| | |
| | .system-line { |
| | color: #ffa657; |
| | font-style: italic; |
| | } |
| | |
| | .timestamp { |
| | color: #7d8590; |
| | font-size: 11px; |
| | margin-right: 8px; |
| | } |
| | |
| | .terminal-input { |
| | display: flex; |
| | align-items: center; |
| | padding: 12px 16px; |
| | background: #161b22; |
| | border-top: 1px solid #30363d; |
| | } |
| | |
| | .prompt { |
| | color: #58a6ff; |
| | margin-right: 8px; |
| | font-weight: 600; |
| | } |
| | |
| | .input-field { |
| | flex: 1; |
| | background: transparent; |
| | border: none; |
| | color: #c9d1d9; |
| | font-family: inherit; |
| | font-size: 13px; |
| | outline: none; |
| | } |
| | |
| | .input-field::placeholder { |
| | color: #7d8590; |
| | } |
| | |
| | .status-indicator { |
| | display: flex; |
| | align-items: center; |
| | gap: 8px; |
| | margin-left: 12px; |
| | } |
| | |
| | .status-dot { |
| | width: 8px; |
| | height: 8px; |
| | border-radius: 50%; |
| | background: #7d8590; |
| | transition: background-color 0.3s; |
| | } |
| | |
| | .status-dot.connected { |
| | background: #56d364; |
| | box-shadow: 0 0 8px rgba(86, 211, 100, 0.5); |
| | } |
| | |
| | .status-dot.running { |
| | background: #ffa657; |
| | animation: pulse 1.5s infinite; |
| | } |
| | |
| | .status-dot.error { |
| | background: #f85149; |
| | } |
| | |
| | @keyframes pulse { |
| | 0%, 100% { opacity: 1; } |
| | 50% { opacity: 0.5; } |
| | } |
| | |
| | .typing-indicator { |
| | display: none; |
| | color: #7d8590; |
| | font-style: italic; |
| | animation: blink 1s infinite; |
| | } |
| | |
| | @keyframes blink { |
| | 0%, 50% { opacity: 1; } |
| | 51%, 100% { opacity: 0; } |
| | } |
| | |
| | .command-history { |
| | position: absolute; |
| | bottom: 60px; |
| | left: 16px; |
| | right: 16px; |
| | background: #21262d; |
| | border: 1px solid #30363d; |
| | border-radius: 6px; |
| | max-height: 200px; |
| | overflow-y: auto; |
| | display: none; |
| | z-index: 1000; |
| | } |
| | |
| | .history-item { |
| | padding: 8px 12px; |
| | cursor: pointer; |
| | border-bottom: 1px solid #30363d; |
| | transition: background-color 0.2s; |
| | } |
| | |
| | .history-item:hover { |
| | background: #30363d; |
| | } |
| | |
| | .history-item:last-child { |
| | border-bottom: none; |
| | } |
| | |
| | |
| | @media (max-width: 768px) { |
| | .terminal-header { |
| | padding: 8px 12px; |
| | } |
| | |
| | .terminal-output { |
| | padding: 12px; |
| | font-size: 12px; |
| | } |
| | |
| | .terminal-input { |
| | padding: 8px 12px; |
| | } |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <div class="terminal-container"> |
| | <div class="terminal-header"> |
| | <div class="terminal-title"> |
| | <div class="terminal-icon"></div> |
| | <span>Manus AI Terminal</span> |
| | </div> |
| | <div class="terminal-controls"> |
| | <button class="control-btn close" onclick="closeTerminal()"></button> |
| | <button class="control-btn minimize" onclick="minimizeTerminal()"></button> |
| | <button class="control-btn maximize" onclick="maximizeTerminal()"></button> |
| | </div> |
| | </div> |
| | |
| | <div class="terminal-body"> |
| | <div class="terminal-output" id="output"></div> |
| | <div class="command-history" id="history"></div> |
| | |
| | <div class="terminal-input"> |
| | <span class="prompt">$</span> |
| | <input type="text" class="input-field" id="commandInput" |
| | placeholder="Type a command and press Enter..." |
| | autocomplete="off" spellcheck="false"> |
| | <div class="status-indicator"> |
| | <div class="status-dot" id="statusDot"></div> |
| | <span id="statusText">Disconnected</span> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | class ManusTerminal { |
| | constructor() { |
| | this.ws = null; |
| | this.output = document.getElementById('output'); |
| | this.input = document.getElementById('commandInput'); |
| | this.statusDot = document.getElementById('statusDot'); |
| | this.statusText = document.getElementById('statusText'); |
| | this.history = document.getElementById('history'); |
| | |
| | this.commandHistory = []; |
| | this.historyIndex = -1; |
| | this.isConnected = false; |
| | this.isRunning = false; |
| | |
| | this.init(); |
| | } |
| | |
| | init() { |
| | this.setupEventListeners(); |
| | this.connect(); |
| | this.addWelcomeMessage(); |
| | } |
| | |
| | setupEventListeners() { |
| | this.input.addEventListener('keydown', (e) => this.handleKeyDown(e)); |
| | this.input.addEventListener('keyup', (e) => this.handleKeyUp(e)); |
| | |
| | |
| | window.addEventListener('focus', () => { |
| | if (!this.isConnected) { |
| | this.connect(); |
| | } |
| | }); |
| | } |
| | |
| | connect() { |
| | try { |
| | this.ws = new WebSocket('ws://localhost:8765'); |
| | |
| | this.ws.onopen = () => { |
| | this.isConnected = true; |
| | this.updateStatus('connected', 'Connected'); |
| | this.addSystemMessage('π Connected to terminal server'); |
| | }; |
| | |
| | this.ws.onmessage = (event) => { |
| | const data = JSON.parse(event.data); |
| | this.handleMessage(data); |
| | }; |
| | |
| | this.ws.onclose = () => { |
| | this.isConnected = false; |
| | this.isRunning = false; |
| | this.updateStatus('error', 'Disconnected'); |
| | this.addSystemMessage('β Connection lost. Attempting to reconnect...'); |
| | |
| | |
| | setTimeout(() => this.connect(), 3000); |
| | }; |
| | |
| | this.ws.onerror = (error) => { |
| | this.addSystemMessage('β οΈ Connection error. Check if the server is running.'); |
| | }; |
| | |
| | } catch (error) { |
| | this.addSystemMessage('β Failed to connect to terminal server'); |
| | } |
| | } |
| | |
| | handleMessage(data) { |
| | const timestamp = new Date(data.timestamp).toLocaleTimeString(); |
| | |
| | switch (data.type) { |
| | case 'connected': |
| | this.addSystemMessage(data.message); |
| | break; |
| | |
| | case 'command_start': |
| | this.isRunning = true; |
| | this.updateStatus('running', 'Running'); |
| | this.addCommandLine(data.message); |
| | break; |
| | |
| | case 'output': |
| | this.addOutputLine(data.data, data.stream); |
| | break; |
| | |
| | case 'command_complete': |
| | this.isRunning = false; |
| | this.updateStatus('connected', 'Connected'); |
| | this.addSystemMessage(`Process completed with exit code ${data.exit_code}`); |
| | break; |
| | |
| | case 'error': |
| | this.addErrorLine(data.data); |
| | break; |
| | |
| | case 'interrupted': |
| | this.isRunning = false; |
| | this.updateStatus('connected', 'Connected'); |
| | this.addSystemMessage(data.message); |
| | break; |
| | } |
| | } |
| | |
| | handleKeyDown(e) { |
| | switch (e.key) { |
| | case 'Enter': |
| | e.preventDefault(); |
| | this.executeCommand(); |
| | break; |
| | |
| | case 'ArrowUp': |
| | e.preventDefault(); |
| | this.navigateHistory(-1); |
| | break; |
| | |
| | case 'ArrowDown': |
| | e.preventDefault(); |
| | this.navigateHistory(1); |
| | break; |
| | |
| | case 'Tab': |
| | e.preventDefault(); |
| | |
| | break; |
| | |
| | case 'c': |
| | if (e.ctrlKey) { |
| | e.preventDefault(); |
| | this.interruptCommand(); |
| | } |
| | break; |
| | } |
| | } |
| | |
| | handleKeyUp(e) { |
| | |
| | if (e.target.value.length > 0) { |
| | |
| | } |
| | } |
| | |
| | executeCommand() { |
| | const command = this.input.value.trim(); |
| | if (!command || !this.isConnected) return; |
| | |
| | |
| | if (this.commandHistory[this.commandHistory.length - 1] !== command) { |
| | this.commandHistory.push(command); |
| | } |
| | this.historyIndex = this.commandHistory.length; |
| | |
| | |
| | this.ws.send(JSON.stringify({ |
| | type: 'command', |
| | command: command |
| | })); |
| | |
| | |
| | this.input.value = ''; |
| | } |
| | |
| | interruptCommand() { |
| | if (this.isRunning && this.isConnected) { |
| | this.ws.send(JSON.stringify({ |
| | type: 'interrupt' |
| | })); |
| | } |
| | } |
| | |
| | navigateHistory(direction) { |
| | if (this.commandHistory.length === 0) return; |
| | |
| | this.historyIndex += direction; |
| | |
| | if (this.historyIndex < 0) { |
| | this.historyIndex = 0; |
| | } else if (this.historyIndex >= this.commandHistory.length) { |
| | this.historyIndex = this.commandHistory.length; |
| | this.input.value = ''; |
| | return; |
| | } |
| | |
| | this.input.value = this.commandHistory[this.historyIndex] || ''; |
| | } |
| | |
| | updateStatus(status, text) { |
| | this.statusDot.className = `status-dot ${status}`; |
| | this.statusText.textContent = text; |
| | } |
| | |
| | addWelcomeMessage() { |
| | this.addSystemMessage('π― Manus AI Terminal - Ready for commands'); |
| | this.addSystemMessage('π‘ Use Ctrl+C to interrupt running commands'); |
| | this.addSystemMessage('π Use β/β arrows to navigate command history'); |
| | } |
| | |
| | addCommandLine(text) { |
| | this.addLine(text, 'command-line'); |
| | } |
| | |
| | addOutputLine(text, stream = 'stdout') { |
| | const className = stream === 'stderr' ? 'error-line' : 'output-line'; |
| | this.addLine(text, className); |
| | } |
| | |
| | addErrorLine(text) { |
| | this.addLine(text, 'error-line'); |
| | } |
| | |
| | addSystemMessage(text) { |
| | this.addLine(text, 'system-line'); |
| | } |
| | |
| | addLine(text, className = 'output-line') { |
| | const line = document.createElement('div'); |
| | line.className = `terminal-line ${className}`; |
| | |
| | const timestamp = document.createElement('span'); |
| | timestamp.className = 'timestamp'; |
| | timestamp.textContent = new Date().toLocaleTimeString(); |
| | |
| | const content = document.createElement('span'); |
| | content.textContent = text; |
| | |
| | line.appendChild(timestamp); |
| | line.appendChild(content); |
| | |
| | this.output.appendChild(line); |
| | this.scrollToBottom(); |
| | } |
| | |
| | scrollToBottom() { |
| | this.output.scrollTop = this.output.scrollHeight; |
| | } |
| | |
| | clear() { |
| | this.output.innerHTML = ''; |
| | this.addWelcomeMessage(); |
| | } |
| | } |
| | |
| | |
| | function closeTerminal() { |
| | if (confirm('Are you sure you want to close the terminal?')) { |
| | window.close(); |
| | } |
| | } |
| | |
| | function minimizeTerminal() { |
| | |
| | console.log('Minimize terminal'); |
| | } |
| | |
| | function maximizeTerminal() { |
| | if (document.fullscreenElement) { |
| | document.exitFullscreen(); |
| | } else { |
| | document.documentElement.requestFullscreen(); |
| | } |
| | } |
| | |
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | window.terminal = new ManusTerminal(); |
| | }); |
| | |
| | |
| | window.addEventListener('keydown', (e) => { |
| | if (e.ctrlKey && e.key === 'l') { |
| | e.preventDefault(); |
| | window.terminal.clear(); |
| | } |
| | }); |
| | </script> |
| | </body> |
| | </html> |