diff --git a/web/static/css/c2.css b/web/static/css/c2.css index c620de07..b6881b6c 100644 --- a/web/static/css/c2.css +++ b/web/static/css/c2.css @@ -1218,32 +1218,172 @@ Task Detail Modal ============================================================================ */ -.c2-task-detail { line-height: 2; } -.c2-task-detail > div { margin-bottom: 6px; font-size: 13px; } +.c2-modal.c2-modal--wide { + max-width: 720px; +} + +.c2-task-modal-header { + align-items: flex-start; +} + +.c2-task-modal-heading { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.c2-task-modal-heading h3 { + margin: 0; +} + +.c2-task-detail { + display: flex; + flex-direction: column; + gap: 20px; +} + +.c2-task-detail-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.c2-task-kv { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 14px; + background: var(--c2-surface-alt); + border: 1px solid var(--c2-border); + border-radius: var(--c2-radius-sm); +} + +.c2-task-kv__label { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--c2-text-muted); +} + +.c2-task-kv__value { + font-size: 13px; + font-weight: 500; + color: var(--c2-text); + word-break: break-all; + line-height: 1.45; +} + +.c2-task-kv__value--mono { + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text-dim); +} + +.c2-task-kv__value--accent { + font-family: var(--c2-mono); + font-weight: 600; + color: var(--c2-accent); +} + +.c2-task-timeline { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + padding: 14px 16px; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.06), rgba(59, 130, 246, 0.02)); + border: 1px solid rgba(59, 130, 246, 0.14); + border-radius: var(--c2-radius-sm); +} + +.c2-task-time-card { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.c2-task-time-card:not(:last-child) { + padding-right: 10px; + border-right: 1px solid rgba(59, 130, 246, 0.12); +} + +.c2-task-code-section, +.c2-task-error-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.c2-task-code-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.c2-task-code-title { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--c2-text-dim); +} .c2-task-error { color: var(--c2-red); - padding: 14px; + padding: 14px 16px; background: var(--c2-red-dim); border: 1px solid rgba(239, 68, 68, 0.15); border-radius: var(--c2-radius-sm); - margin-top: 12px; font-size: 13px; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-word; } -.c2-task-result pre { +.c2-task-result-pre, +.c2-task-command-pre { background: #0f172a; color: #e2e8f0; - padding: 16px; + padding: 14px 16px; border-radius: var(--c2-radius-sm); overflow-x: auto; font-family: var(--c2-mono); font-size: 12px; - margin-top: 8px; - max-height: 400px; + margin: 0; + max-height: 360px; overflow-y: auto; border: 1px solid #1e293b; line-height: 1.6; + white-space: pre-wrap; + word-break: break-all; +} + +.c2-task-command-pre { + max-height: 140px; +} + +.c2-task-command-cell { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--c2-mono); + font-size: 12px; + color: var(--c2-text-muted, #64748b); +} + +.c2-task-item-compact .c2-task-command { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--c2-mono); + font-size: 11px; + color: var(--c2-text-muted, #64748b); } /* ============================================================================ @@ -1277,6 +1417,11 @@ Modal ============================================================================ */ +/* Toast 须高于模态遮罩 (10050),避免被 backdrop-filter 模糊 */ +#c2-toast-container { + z-index: 10100 !important; +} + .c2-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; @@ -1388,4 +1533,13 @@ .c2-stats { flex-direction: column; gap: 12px; } .c2-payload-grid { grid-template-columns: 1fr; } .c2-listener-grid { grid-template-columns: 1fr; padding: 16px; } + .c2-task-detail-grid { grid-template-columns: 1fr; } + .c2-task-timeline { grid-template-columns: 1fr; } + .c2-task-time-card:not(:last-child) { + padding-right: 0; + padding-bottom: 10px; + border-right: none; + border-bottom: 1px solid rgba(59, 130, 246, 0.12); + } + .c2-modal.c2-modal--wide { max-width: 100%; } } diff --git a/web/static/css/style.css b/web/static/css/style.css index 5900c44c..2434d052 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -21184,6 +21184,11 @@ button.chat-files-dropdown-item:hover:not(:disabled) { gap: 12px; } +/* 全局 Toast 须高于模态遮罩 (10050) */ +#toast-notification-container { + z-index: 10100 !important; +} + .chat-files-toast { position: fixed; z-index: 1100; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index d1a40773..d5194a5c 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2671,7 +2671,7 @@ "confirmDeleteSession": "Remove this session and related tasks/files from the server? (Does not send exit to the implant; use Kill Session to exit the agent.)", "toastExitSent": "Exit command sent", "toastSessionDeleted": "Session record deleted", - "terminalWelcome": "CyberStrikeAI C2 Terminal — AI-Native Command & Control", + "terminalWelcome": "CyberStrikeAI C2 Terminal — Enter to run; ↑↓ history; Ctrl+L clear; Ctrl+C cancel input", "termStatusReady": "Ready", "termStatusExec": "Executing…", "termStatusErr": "Error", @@ -2680,6 +2680,9 @@ "termWaitTimeout": "[Timed out waiting for result]", "termCleared": "Terminal cleared", "termNoSelection": "No text selected", + "termWaitFinish": "Please wait for the current command to finish", + "termCtrlC": "Remote interrupt is not supported in this version", + "termQueued": "[Command queued — will run after the current task completes]", "clearTerminal": "Clear" }, "tasks": { @@ -2706,6 +2709,7 @@ "colTask": "Task", "colSession": "Session", "colType": "Type", + "colCommand": "Command", "colStatus": "Status", "colDuration": "Duration", "colCreated": "Created", @@ -2716,6 +2720,8 @@ "labelId": "ID", "labelSession": "Session", "labelType": "Type", + "labelCommand": "Command", + "labelPayload": "Payload", "labelStatus": "Status", "labelCreated": "Created", "labelSent": "Sent", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 616472a9..c2dcd649 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2660,7 +2660,7 @@ "confirmDeleteSession": "从服务器删除此会话及其关联任务与文件记录?(不会向植入体发送退出;若需退出目标进程请使用「终止会话」。)", "toastExitSent": "退出指令已发送", "toastSessionDeleted": "会话记录已删除", - "terminalWelcome": "CyberStrikeAI C2 终端 — AI-Native 命令与控制", + "terminalWelcome": "CyberStrikeAI C2 终端 — 回车执行;↑↓ 历史;Ctrl+L 清屏;Ctrl+C 取消输入", "termStatusReady": "就绪", "termStatusExec": "执行中…", "termStatusErr": "错误", @@ -2669,6 +2669,9 @@ "termWaitTimeout": "[等待结果超时]", "termCleared": "终端已清屏", "termNoSelection": "未选中文本", + "termWaitFinish": "请等待当前命令执行完成", + "termCtrlC": "当前版本暂不支持中断远程命令", + "termQueued": "[命令已加入队列,将在当前任务完成后执行]", "clearTerminal": "清屏" }, "tasks": { @@ -2695,6 +2698,7 @@ "colTask": "任务", "colSession": "会话", "colType": "类型", + "colCommand": "命令", "colStatus": "状态", "colDuration": "耗时", "colCreated": "创建时间", @@ -2705,6 +2709,8 @@ "labelId": "ID", "labelSession": "会话", "labelType": "类型", + "labelCommand": "命令", + "labelPayload": "参数", "labelStatus": "状态", "labelCreated": "创建时间", "labelSent": "发送时间", diff --git a/web/static/js/c2.js b/web/static/js/c2.js index ad4d086d..eec7da7d 100644 --- a/web/static/js/c2.js +++ b/web/static/js/c2.js @@ -27,7 +27,11 @@ terminalFitAddon: null, terminalResizeObserver: null, terminalContainer: null, - terminalSessionId: 'main', + terminalSessionId: null, + terminalHistory: {}, + terminalLogs: {}, + terminalBusy: false, + terminalQueue: [], // 文件管理 currentPath: '/', fileList: [], @@ -90,6 +94,56 @@ return status; } + function formatTaskCommand(task) { + if (!task) return ''; + const type = String(task.taskType || '').toLowerCase(); + const p = task.payload; + if (!p || typeof p !== 'object' || Object.keys(p).length === 0) { + if (type === 'pwd' || type === 'ps' || type === 'screenshot') return type; + return ''; + } + switch (type) { + case 'shell': + case 'exec': + return p.command != null ? String(p.command) : ''; + case 'ls': + case 'cd': + return p.path != null ? String(p.path) : ''; + case 'download': + return p.remote_path != null ? String(p.remote_path) : ''; + case 'upload': + if (p.remote_path) return String(p.remote_path); + if (p.file_id) return 'file:' + String(p.file_id); + return ''; + case 'kill_proc': + return p.pid != null ? 'pid:' + String(p.pid) : ''; + case 'sleep': + let sleepStr = p.seconds != null ? 'sleep ' + p.seconds + 's' : ''; + if (p.jitter != null) sleepStr += (sleepStr ? ', ' : '') + 'jitter ' + p.jitter + '%'; + return sleepStr; + case 'port_fwd': + return [p.action, p.remote_host, p.remote_port, p.local_port].filter(v => v != null && v !== '').join(':'); + case 'socks_start': + case 'socks_stop': + return p.port != null ? 'port:' + String(p.port) : type; + case 'load_assembly': + if (p.args) return String(p.args); + if (p.file_id) return 'file:' + String(p.file_id); + return ''; + case 'persist': + return p.method != null ? String(p.method) : ''; + default: + try { return JSON.stringify(p); } catch (e) { return ''; } + } + } + + function truncateCommand(cmd, maxLen) { + if (!cmd) return ''; + const s = String(cmd); + if (!maxLen || s.length <= maxLen) return s; + return s.substring(0, maxLen - 1) + '\u2026'; + } + // ============================================================================ // 工具函数 // ============================================================================ @@ -116,10 +170,11 @@ const container = document.getElementById('c2-toast-container') || (() => { const div = document.createElement('div'); div.id = 'c2-toast-container'; - div.style.cssText = 'position:fixed;top:20px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:8px;'; + div.style.cssText = 'position:fixed;top:20px;right:20px;z-index:10100;display:flex;flex-direction:column;gap:8px;'; document.body.appendChild(div); return div; })(); + container.style.zIndex = '10100'; const toast = document.createElement('div'); const colors = { error: '#e53e3e', success: '#38a169', info: '#3182ce', warn: '#d69e2e' }; toast.style.cssText = `background:${colors[type] || colors.info};color:#fff;padding:10px 18px;border-radius:6px;font-size:0.875rem;box-shadow:0 4px 12px rgba(0,0,0,0.2);opacity:0;transition:opacity .3s;max-width:400px;word-break:break-word;`; @@ -725,7 +780,6 @@ C2.selectedSessionId = id; C2.renderSessions(); C2.renderSessionDetail(id); - C2.initTerminal(); }; C2.renderSessionDetail = function(id) { @@ -829,7 +883,10 @@ if (panel) panel.style.display = 'block'; if (tab === 'terminal') { - setTimeout(() => C2.fitTerminal(), 50); + setTimeout(function () { + C2.fitTerminal(); + if (C2.terminalInstance) C2.terminalInstance.focus(); + }, 50); } }; @@ -875,97 +932,57 @@ // xterm 终端 // ============================================================================ - C2.initTerminal = function() { - const container = document.getElementById('c2-terminal-container'); - if (!container || typeof Terminal === 'undefined') return; - - if (C2.terminalInstance) { - C2.terminalInstance.dispose(); + C2.serializeTerminalBuffer = function(term) { + if (!term || !term.buffer || !term.buffer.active) return ''; + const buf = term.buffer.active; + const lines = []; + for (let i = 0; i < buf.length; i++) { + const line = buf.getLine(i); + if (line) lines.push(line.translateToString(true)); } - - const term = new Terminal({ - cursorBlink: true, - cursorStyle: 'block', - fontSize: 14, - fontFamily: 'Menlo, Monaco, "Courier New", monospace', - lineHeight: 1.3, - scrollback: 5000, - theme: { - background: '#0d1117', - foreground: '#e6edf3', - cursor: '#58a6ff', - selection: 'rgba(88, 166, 255, 0.3)' - } - }); - - if (typeof FitAddon !== 'undefined') { - const FitCtor = FitAddon.FitAddon || FitAddon; - C2.terminalFitAddon = new FitCtor(); - term.loadAddon(C2.terminalFitAddon); - } - - term.open(container); - - try { - if (C2.terminalFitAddon) C2.terminalFitAddon.fit(); - } catch (e) {} - - let lineBuffer = ''; - const prompt = '$ '; - - term.writeln('\x1b[36m' + c2t('c2.sessions.terminalWelcome') + '\x1b[0m'); - term.writeln(''); - term.write(prompt); - - term.onData(e => { - const code = e.charCodeAt(0); - if (code === 13) { // Enter - term.writeln(''); - const cmd = lineBuffer.trim(); - lineBuffer = ''; - if (cmd) { - C2.executeInTerminal(cmd, term); - } else { - term.write(prompt); - } - } else if (code === 127) { // Backspace - if (lineBuffer.length > 0) { - lineBuffer = lineBuffer.slice(0, -1); - term.write('\b \b'); - } - } else if (code >= 32) { // Printable - lineBuffer += e; - term.write(e); - } - }); - - C2.terminalInstance = term; - - // Resize observer - if (C2.terminalResizeObserver) { - C2.terminalResizeObserver.disconnect(); - } - C2.terminalResizeObserver = new ResizeObserver(() => { - C2.fitTerminal(); - }); - C2.terminalResizeObserver.observe(container); + return lines.join('\n'); }; - C2.fitTerminal = function() { - if (C2.terminalFitAddon && C2.terminalInstance) { - try { - C2.terminalFitAddon.fit(); - } catch (e) {} + C2.pushTerminalHistory = function(cmd) { + const sid = C2.selectedSessionId; + if (!sid || !cmd) return; + if (!C2.terminalHistory[sid]) C2.terminalHistory[sid] = []; + const hist = C2.terminalHistory[sid]; + if (hist.length === 0 || hist[hist.length - 1] !== cmd) { + hist.push(cmd); + if (hist.length > 200) hist.shift(); } }; - C2.executeInTerminal = function(cmd, term) { + C2.finishTerminalCommand = function(term, status) { + C2.terminalBusy = false; + const statusEl = document.getElementById('c2-terminal-status'); + if (status === 'err' && statusEl) { + statusEl.textContent = c2t('c2.sessions.termStatusErr'); + } else if (status === 'timeout' && statusEl) { + statusEl.textContent = c2t('c2.sessions.termStatusTimeout'); + } else if (statusEl && C2.terminalQueue.length === 0) { + statusEl.textContent = c2t('c2.sessions.termStatusReady'); + } + if (C2.terminalQueue.length > 0) { + const next = C2.terminalQueue.shift(); + C2.runTerminalCommand(next, term); + return; + } + term.write('$ '); + if (statusEl && status !== 'err' && status !== 'timeout') { + statusEl.textContent = c2t('c2.sessions.termStatusReady'); + } + }; + + C2.runTerminalCommand = function(cmd, term) { if (!C2.selectedSessionId) { term.writeln('\x1b[31m' + c2t('c2.sessions.termNoSession') + '\x1b[0m'); term.write('$ '); return; } - + C2.terminalBusy = true; + C2.pushTerminalHistory(cmd); const statusEl = document.getElementById('c2-terminal-status'); if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusExec'); @@ -976,14 +993,29 @@ }).then(data => { if (data.error) { term.writeln(`\x1b[31mError: ${data.error}\x1b[0m`); - term.write('$ '); - if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusErr'); + C2.finishTerminalCommand(term, 'err'); } else { C2.waitForTaskResult(data.task?.id || data.task_id, term); } + }).catch(function () { + term.writeln('\x1b[31mError: request failed\x1b[0m'); + C2.finishTerminalCommand(term, 'err'); }); }; + C2.executeInTerminal = function(cmd, term) { + if (!cmd) { + term.write('$ '); + return; + } + if (C2.terminalBusy) { + C2.terminalQueue.push(cmd); + term.writeln('\x1b[33m' + c2t('c2.sessions.termQueued') + '\x1b[0m'); + return; + } + C2.runTerminalCommand(cmd, term); + }; + C2.waitForTaskResult = function(taskId, term) { let attempts = 0; const maxAttempts = 60; @@ -992,9 +1024,7 @@ const check = () => { if (++attempts > maxAttempts) { term.writeln('\x1b[33m' + c2t('c2.sessions.termWaitTimeout') + '\x1b[0m'); - term.write('$ '); - const statusEl = document.getElementById('c2-terminal-status'); - if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusTimeout'); + C2.finishTerminalCommand(term, 'timeout'); return; } apiRequest('GET', `${API_BASE}/tasks/${taskId}`).then(data => { @@ -1007,24 +1037,376 @@ if (task.error) { term.writeln(`\x1b[31m${task.error}\x1b[0m`); } - term.write('$ '); - const statusEl = document.getElementById('c2-terminal-status'); - if (statusEl) statusEl.textContent = c2t('c2.sessions.termStatusReady'); + C2.finishTerminalCommand(term, task.status === 'failed' ? 'err' : 'ready'); } else { delay = Math.min(delay * 1.5, maxDelay); setTimeout(check, delay); } + }).catch(function () { + C2.finishTerminalCommand(term, 'err'); }); }; check(); }; + C2.initTerminal = function() { + const container = document.getElementById('c2-terminal-container'); + if (!container || typeof Terminal === 'undefined') return; + + if (C2.terminalInstance && C2.terminalSessionId) { + C2.terminalLogs[C2.terminalSessionId] = C2.serializeTerminalBuffer(C2.terminalInstance); + } + if (C2.terminalInstance) { + C2.terminalInstance.dispose(); + } + + const sessionId = C2.selectedSessionId || '_none'; + C2.terminalSessionId = sessionId; + C2.terminalQueue = []; + C2.terminalBusy = false; + + const term = new Terminal({ + cursorBlink: true, + cursorStyle: 'block', + fontSize: 14, + fontFamily: 'Menlo, Monaco, "Courier New", monospace', + lineHeight: 1.3, + scrollback: 5000, + theme: { + background: '#0d1117', + foreground: '#e6edf3', + cursor: '#58a6ff', + cursorAccent: '#0d1117', + selection: 'rgba(88, 166, 255, 0.3)' + } + }); + + if (typeof FitAddon !== 'undefined') { + const FitCtor = FitAddon.FitAddon || FitAddon; + C2.terminalFitAddon = new FitCtor(); + term.loadAddon(C2.terminalFitAddon); + } + + term.open(container); + try { + if (C2.terminalFitAddon) C2.terminalFitAddon.fit(); + } catch (e) {} + + let lineBuffer = ''; + let cursorIndex = 0; + let historyIndex = -1; + let lastPasteAt = 0; + let lastPasteText = ''; + const prompt = '$ '; + + function redrawInputLine() { + term.write('\x1b[2K\r' + prompt + lineBuffer); + const tail = lineBuffer.length - cursorIndex; + if (tail > 0) term.write('\x1b[' + tail + 'D'); + } + + function resetInputLine() { + lineBuffer = ''; + cursorIndex = 0; + historyIndex = -1; + term.write('\x1b[2K\r' + prompt); + } + + function insertPlainText(text) { + const safe = String(text).replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ''); + if (!safe) return; + lineBuffer = lineBuffer.slice(0, cursorIndex) + safe + lineBuffer.slice(cursorIndex); + cursorIndex += safe.length; + redrawInputLine(); + } + + function deleteWordBeforeCursor() { + if (cursorIndex === 0) return; + let start = cursorIndex; + while (start > 0 && /\s/.test(lineBuffer[start - 1])) start--; + while (start > 0 && !/\s/.test(lineBuffer[start - 1])) start--; + lineBuffer = lineBuffer.slice(0, start) + lineBuffer.slice(cursorIndex); + cursorIndex = start; + redrawInputLine(); + } + + function moveWordLeft() { + if (cursorIndex === 0) return; + let pos = cursorIndex; + while (pos > 0 && /\s/.test(lineBuffer[pos - 1])) pos--; + while (pos > 0 && !/\s/.test(lineBuffer[pos - 1])) pos--; + const delta = cursorIndex - pos; + if (delta > 0) { + term.write('\x1b[' + delta + 'D'); + cursorIndex = pos; + } + } + + function moveWordRight() { + if (cursorIndex >= lineBuffer.length) return; + let pos = cursorIndex; + while (pos < lineBuffer.length && /\s/.test(lineBuffer[pos])) pos++; + while (pos < lineBuffer.length && !/\s/.test(lineBuffer[pos])) pos++; + const delta = pos - cursorIndex; + if (delta > 0) { + term.write('\x1b[' + delta + 'C'); + cursorIndex = pos; + } + } + + function showHistoryEntry(entry) { + lineBuffer = entry || ''; + cursorIndex = lineBuffer.length; + term.write('\x1b[2K\r' + prompt + lineBuffer); + } + + function submitCurrentLine() { + if (C2.terminalBusy) { + term.writeln(''); + term.writeln('\x1b[33m' + c2t('c2.sessions.termWaitFinish') + '\x1b[0m'); + term.write(prompt + lineBuffer); + const tail = lineBuffer.length - cursorIndex; + if (tail > 0) term.write('\x1b[' + tail + 'D'); + return; + } + term.writeln(''); + const cmd = lineBuffer.trim(); + lineBuffer = ''; + cursorIndex = 0; + historyIndex = -1; + if (cmd) { + C2.executeInTerminal(cmd, term); + } else { + term.write(prompt); + } + } + + function handlePasteText(text) { + const now = Date.now(); + if (text === lastPasteText && now - lastPasteAt < 80) return; + lastPasteAt = now; + lastPasteText = text; + + const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + if (normalized.indexOf('\n') === -1) { + insertPlainText(normalized); + return; + } + const endsWithNewline = normalized.endsWith('\n'); + const parts = normalized.split('\n'); + const tail = parts.pop() || ''; + parts.forEach(function (part) { + insertPlainText(part); + submitCurrentLine(); + }); + if (tail) insertPlainText(tail); + else if (endsWithNewline && parts.length === 0) submitCurrentLine(); + } + + const savedLog = C2.terminalLogs[sessionId]; + if (savedLog) { + term.write(String(savedLog).replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/\n/g, '\r\n')); + if (!savedLog.endsWith('\n')) term.write('\r\n'); + } else { + term.writeln('\x1b[36m' + c2t('c2.sessions.terminalWelcome') + '\x1b[0m'); + term.writeln(''); + } + term.write(prompt); + + term.onData(function (e) { + if (e === '\x0c') { + term.clear(); + resetInputLine(); + C2.terminalLogs[sessionId] = ''; + return; + } + if (e === '\x03') { + if (C2.terminalBusy) { + term.writeln(''); + term.writeln('\x1b[33m^C (' + c2t('c2.sessions.termCtrlC') + ')\x1b[0m'); + } + resetInputLine(); + return; + } + if (e === '\x16') { + if (navigator.clipboard && navigator.clipboard.readText) { + navigator.clipboard.readText().then(handlePasteText).catch(function () {}); + } + return; + } + if (e.length > 1 && e.indexOf('\x1b') !== 0) { + handlePasteText(e); + return; + } + if (e === '\x1b[D' || e === '\x1bOD') { + if (cursorIndex > 0) { + cursorIndex--; + term.write('\x1b[D'); + } + return; + } + if (e === '\x1b[C' || e === '\x1bOC') { + if (cursorIndex < lineBuffer.length) { + cursorIndex++; + term.write('\x1b[C'); + } + return; + } + if (e === '\x1b[1;3D' || e === '\x1bb') { + moveWordLeft(); + return; + } + if (e === '\x1b[1;3C' || e === '\x1bf') { + moveWordRight(); + return; + } + if (e === '\x1b[A' || e === '\x1bOA') { + const hist = C2.terminalHistory[sessionId] || []; + if (hist.length === 0) return; + historyIndex = historyIndex < 0 ? hist.length - 1 : Math.max(0, historyIndex - 1); + showHistoryEntry(hist[historyIndex]); + return; + } + if (e === '\x1b[B' || e === '\x1bOB') { + const hist = C2.terminalHistory[sessionId] || []; + if (hist.length === 0) return; + historyIndex = historyIndex < 0 ? -1 : Math.min(hist.length - 1, historyIndex + 1); + if (historyIndex < 0) showHistoryEntry(''); + else showHistoryEntry(hist[historyIndex]); + return; + } + if (e === '\x1b[H' || e === '\x1bOH' || e === '\x01') { + if (cursorIndex > 0) { + term.write('\x1b[' + cursorIndex + 'D'); + cursorIndex = 0; + } + return; + } + if (e === '\x1b[F' || e === '\x1bOF' || e === '\x05') { + const move = lineBuffer.length - cursorIndex; + if (move > 0) { + term.write('\x1b[' + move + 'C'); + cursorIndex = lineBuffer.length; + } + return; + } + if (e === '\x1b[3~') { + if (cursorIndex < lineBuffer.length) { + lineBuffer = lineBuffer.slice(0, cursorIndex) + lineBuffer.slice(cursorIndex + 1); + redrawInputLine(); + } + return; + } + if (e === '\x15') { + resetInputLine(); + return; + } + if (e === '\x0b') { + lineBuffer = lineBuffer.slice(0, cursorIndex); + redrawInputLine(); + return; + } + if (e === '\x17') { + deleteWordBeforeCursor(); + return; + } + if (e === '\x1b\x7f') { + deleteWordBeforeCursor(); + return; + } + + const code = e.charCodeAt(0); + if (code === 13 || code === 10) { + submitCurrentLine(); + } else if (code === 127 || code === 8) { + if (cursorIndex > 0) { + lineBuffer = lineBuffer.slice(0, cursorIndex - 1) + lineBuffer.slice(cursorIndex); + cursorIndex--; + redrawInputLine(); + } + } else if (e.length === 1 && code >= 32) { + historyIndex = -1; + lineBuffer = lineBuffer.slice(0, cursorIndex) + e + lineBuffer.slice(cursorIndex); + cursorIndex++; + if (cursorIndex === lineBuffer.length) { + term.write(e); + } else { + redrawInputLine(); + } + } + }); + + const onTerminalPaste = function (ev) { + const text = ev.clipboardData && ev.clipboardData.getData('text'); + if (!text) return; + ev.preventDefault(); + handlePasteText(text); + }; + if (term.element) { + term.element.addEventListener('paste', onTerminalPaste); + } + + term.attachCustomKeyEventHandler(function (ev) { + if (ev.type !== 'keydown') return true; + if ((ev.ctrlKey || ev.metaKey) && !ev.shiftKey && (ev.key === 'c' || ev.key === 'C')) { + if (term.getSelection()) return true; + } + const isPaste = (ev.ctrlKey || ev.metaKey) && !ev.shiftKey && !ev.altKey + && (ev.key === 'v' || ev.key === 'V'); + if (isPaste && navigator.clipboard && navigator.clipboard.readText) { + ev.preventDefault(); + navigator.clipboard.readText().then(handlePasteText).catch(function () {}); + return false; + } + if (ev.shiftKey && ev.key === 'Insert' && navigator.clipboard && navigator.clipboard.readText) { + ev.preventDefault(); + navigator.clipboard.readText().then(handlePasteText).catch(function () {}); + return false; + } + return true; + }); + + container.addEventListener('click', function () { + term.focus(); + }); + container.setAttribute('tabindex', '0'); + + C2.terminalInstance = term; + + if (C2.terminalResizeObserver) { + C2.terminalResizeObserver.disconnect(); + } + C2.terminalResizeObserver = new ResizeObserver(function () { + C2.fitTerminal(); + }); + C2.terminalResizeObserver.observe(container); + + setTimeout(function () { + try { + if (C2.terminalFitAddon) C2.terminalFitAddon.fit(); + term.focus(); + } catch (e) {} + }, 100); + }; + + C2.fitTerminal = function() { + if (C2.terminalFitAddon && C2.terminalInstance) { + try { + C2.terminalFitAddon.fit(); + } catch (e) {} + } + }; + C2.clearTerminal = function() { if (C2.terminalInstance) { C2.terminalInstance.clear(); C2.terminalInstance.writeln('\x1b[36m' + c2t('c2.sessions.termCleared') + '\x1b[0m'); C2.terminalInstance.write('$ '); + if (C2.terminalSessionId) { + C2.terminalLogs[C2.terminalSessionId] = C2.serializeTerminalBuffer(C2.terminalInstance); + } } + C2.terminalQueue = []; }; C2.copyTerminal = function() { @@ -1314,10 +1696,13 @@ container.innerHTML = tasks.map(t => { const rawId = t.id || ''; + const cmd = formatTaskCommand(t); + const cmdShort = truncateCommand(cmd, 40); return `
${escapeHtml(cmd)}
+ ${escapeHtml(JSON.stringify(t.payload, null, 2))}
+ ${escapeHtml(t.resultText)}
+ ${escapeHtml(t.resultText)}