revert: restore terminal monitor layout
This commit is contained in:
parent
d71c935d3c
commit
21f206559e
@ -416,16 +416,22 @@ class TerminalManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
terminal = self.terminals[target_session]
|
terminal = self.terminals[target_session]
|
||||||
output = terminal.get_output(last_n_lines)
|
snapshot = terminal.get_snapshot(
|
||||||
|
last_n_lines or self.default_snapshot_lines,
|
||||||
|
self.max_snapshot_chars
|
||||||
|
)
|
||||||
|
output = snapshot.get("output") if snapshot.get("success") else terminal.get_output(last_n_lines)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"session": target_session,
|
"session": target_session,
|
||||||
"output": output,
|
"output": output,
|
||||||
"is_interactive": terminal.is_interactive,
|
"is_interactive": snapshot.get("is_interactive", terminal.is_interactive),
|
||||||
"last_command": terminal.last_command,
|
"last_command": snapshot.get("last_command", terminal.last_command),
|
||||||
"seconds_since_last_output": terminal._seconds_since_last_output(),
|
"seconds_since_last_output": snapshot.get("seconds_since_last_output", terminal._seconds_since_last_output()),
|
||||||
"echo_loop_detected": terminal.echo_loop_detected
|
"echo_loop_detected": snapshot.get("echo_loop_detected", terminal.echo_loop_detected),
|
||||||
|
"lines_returned": snapshot.get("lines_returned"),
|
||||||
|
"truncated": snapshot.get("truncated", False)
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_terminal_snapshot(
|
def get_terminal_snapshot(
|
||||||
|
|||||||
@ -616,6 +616,7 @@
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
<!-- 右侧拖拽手柄 -->
|
<!-- 右侧拖拽手柄 -->
|
||||||
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
|
||||||
|
|
||||||
|
|||||||
@ -14,31 +14,36 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
:root {
|
||||||
|
--claude-bg: #eeece2;
|
||||||
|
--claude-panel: rgba(255, 255, 255, 0.92);
|
||||||
|
--claude-border: rgba(118, 103, 84, 0.25);
|
||||||
|
--claude-text: #3d3929;
|
||||||
|
--claude-text-secondary: #7f7766;
|
||||||
|
--claude-accent: #da7756;
|
||||||
|
--claude-muted: rgba(121, 109, 94, 0.45);
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: var(--claude-bg);
|
||||||
color: #fff;
|
color: var(--claude-text);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 头部区域 */
|
|
||||||
.header {
|
.header {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: var(--claude-bg);
|
||||||
padding: 15px 20px;
|
padding: 15px 24px;
|
||||||
backdrop-filter: blur(10px);
|
border-bottom: 1px solid var(--claude-border);
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
box-shadow: 0 10px 20px rgba(61, 57, 41, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@ -46,109 +51,87 @@
|
|||||||
background: #4ade80;
|
background: #4ade80;
|
||||||
animation: pulse 2s infinite;
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); }
|
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); }
|
||||||
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
||||||
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.disconnected {
|
.status-indicator.disconnected {
|
||||||
background: #ef4444;
|
background: #ef4444;
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 会话标签栏 */
|
|
||||||
.session-tabs {
|
.session-tabs {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: var(--claude-bg);
|
||||||
padding: 10px 20px;
|
padding: 12px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid var(--claude-border);
|
||||||
|
box-shadow: inset 0 -1px rgba(61, 57, 41, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.78);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(118, 103, 84, 0.25);
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.25s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
color: var(--claude-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
box-shadow: 0 6px 16px rgba(61, 57, 41, 0.12);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: #007acc;
|
background: var(--claude-accent);
|
||||||
border-color: #007acc;
|
border-color: var(--claude-accent);
|
||||||
box-shadow: 0 4px 12px rgba(0, 122, 204, 0.3);
|
color: #fffaf0;
|
||||||
|
box-shadow: 0 10px 24px rgba(218, 119, 86, 0.26);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-close {
|
|
||||||
margin-left: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.6;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-close:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主要内容区域 */
|
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 20px;
|
padding: 22px;
|
||||||
gap: 20px;
|
gap: 22px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 终端容器 */
|
|
||||||
.terminal-wrapper {
|
.terminal-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #1e1e1e;
|
background: #ffffff;
|
||||||
border-radius: 12px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 24px 48px rgba(61, 57, 41, 0.16);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--claude-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-header {
|
.terminal-header {
|
||||||
background: #2d2d2d;
|
background: rgba(255, 255, 255, 0.86);
|
||||||
padding: 10px 15px;
|
padding: 12px 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #444;
|
border-bottom: 1px solid var(--claude-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-title {
|
.terminal-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #888;
|
color: var(--claude-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-controls {
|
.terminal-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn {
|
.control-btn {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@ -156,129 +139,129 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn:hover {
|
.control-btn:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-btn.close { background: #ff5f56; }
|
.control-btn.close { background: #ff5f56; }
|
||||||
.control-btn.minimize { background: #ffbd2e; }
|
.control-btn.minimize { background: #ffbd2e; }
|
||||||
.control-btn.maximize { background: #27c93f; }
|
.control-btn.maximize { background: #27c93f; }
|
||||||
|
.terminal-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 28px 0 32px;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
#terminal {
|
#terminal {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 10px;
|
height: 100%;
|
||||||
padding-bottom: 30px; /* 增加底部内边距 */
|
margin: 0 28px 0 36px;
|
||||||
overflow-y: auto; /* 确保可以滚动 */
|
background: #ffffff;
|
||||||
|
color: var(--claude-text);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#terminal .xterm-viewport {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
#terminal .xterm-scroll-area {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
#terminal .xterm-rows {
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边栏信息 */
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: var(--claude-panel);
|
||||||
border-radius: 12px;
|
border-radius: 18px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
backdrop-filter: blur(10px);
|
border: 1px solid var(--claude-border);
|
||||||
|
box-shadow: 0 18px 40px rgba(61, 57, 41, 0.1);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-section {
|
.info-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-section h3 {
|
.info-section h3 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: var(--claude-text-secondary);
|
||||||
text-transform: uppercase;
|
letter-spacing: 0.05em;
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(118, 103, 84, 0.15);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: var(--claude-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
.info-value {
|
||||||
color: #fff;
|
color: var(--claude-text);
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 命令历史 */
|
|
||||||
.command-history {
|
.command-history {
|
||||||
max-height: 200px;
|
max-height: 210px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(255, 255, 255, 0.78);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(118, 103, 84, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.command-item {
|
.command-item {
|
||||||
padding: 5px;
|
padding: 6px;
|
||||||
margin: 2px 0;
|
margin: 3px 0;
|
||||||
font-family: monospace;
|
font-family: inherit;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #4ade80;
|
color: var(--claude-accent);
|
||||||
border-left: 2px solid #4ade80;
|
border-left: 3px solid var(--claude-accent);
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
background: rgba(218, 119, 86, 0.08);
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部状态栏 */
|
|
||||||
.status-bar {
|
.status-bar {
|
||||||
background: rgba(0, 0, 0, 0.3);
|
background: var(--claude-panel);
|
||||||
padding: 10px 20px;
|
padding: 12px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
border-top: 1px solid var(--claude-border);
|
||||||
|
box-shadow: 0 -8px 18px rgba(61, 57, 41, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-left {
|
.status-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item {
|
.status-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 6px;
|
||||||
|
color: var(--claude-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.main-content {
|
.main-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载动画 */
|
|
||||||
.loader {
|
.loader {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
border: 3px solid rgba(61, 57, 41, 0.15);
|
||||||
border-top-color: #fff;
|
border-top-color: var(--claude-accent);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 提示信息 */
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
@ -325,7 +308,9 @@
|
|||||||
<div class="control-btn maximize" title="最大化"></div>
|
<div class="control-btn maximize" title="最大化"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="terminal"></div>
|
<div class="terminal-body">
|
||||||
|
<div id="terminal"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
@ -420,31 +405,106 @@
|
|||||||
let currentSession = null;
|
let currentSession = null;
|
||||||
let sessions = {};
|
let sessions = {};
|
||||||
let commandHistory = [];
|
let commandHistory = [];
|
||||||
|
const sessionLogs = {};
|
||||||
|
const sessionHydrated = {};
|
||||||
|
const pendingHistory = {};
|
||||||
|
const MAX_SESSION_LOG_LENGTH = 400000;
|
||||||
let stats = {
|
let stats = {
|
||||||
commandCount: 0,
|
commandCount: 0,
|
||||||
outputLines: 0,
|
outputLines: 0,
|
||||||
dataReceived: 0,
|
dataReceived: 0,
|
||||||
startTime: Date.now()
|
startTime: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeHistoryOutput(payload) {
|
||||||
|
if (!payload) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload.join('');
|
||||||
|
}
|
||||||
|
if (typeof payload === 'string') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
return String(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimSessionLog(session) {
|
||||||
|
if (!session || !sessionLogs[session]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sessionLogs[session].length > MAX_SESSION_LOG_LENGTH) {
|
||||||
|
sessionLogs[session] = sessionLogs[session].slice(-MAX_SESSION_LOG_LENGTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSessionLog(session, chunk) {
|
||||||
|
if (!session || !chunk) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sessionLogs[session]) {
|
||||||
|
sessionLogs[session] = '';
|
||||||
|
}
|
||||||
|
sessionLogs[session] += chunk;
|
||||||
|
trimSessionLog(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCurrentSessionLog() {
|
||||||
|
if (!term) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
term.clear();
|
||||||
|
if (!currentSession) {
|
||||||
|
term.writeln('\x1b[33m暂无活动终端\x1b[0m');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
term.writeln(`\x1b[36m[终端: ${currentSession}]\x1b[0m`);
|
||||||
|
term.writeln('');
|
||||||
|
const log = sessionLogs[currentSession];
|
||||||
|
if (log && log.length) {
|
||||||
|
term.write(log);
|
||||||
|
} else if (pendingHistory[currentSession]) {
|
||||||
|
term.writeln('\x1b[33m正在加载历史输出...\x1b[0m');
|
||||||
|
} else {
|
||||||
|
term.writeln('\x1b[90m暂无历史输出,等待新内容...\x1b[0m');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHistoryPayload(data) {
|
||||||
|
if (!data || !data.session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const session = data.session;
|
||||||
|
const historyText = normalizeHistoryOutput(data.output);
|
||||||
|
const existing = sessionLogs[session] || '';
|
||||||
|
const shouldCombine = !sessionHydrated[session];
|
||||||
|
sessionLogs[session] = historyText + (shouldCombine ? existing : '');
|
||||||
|
trimSessionLog(session);
|
||||||
|
sessionHydrated[session] = true;
|
||||||
|
pendingHistory[session] = false;
|
||||||
|
if (session === currentSession) {
|
||||||
|
renderCurrentSessionLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化终端
|
// 初始化终端
|
||||||
function initTerminal() {
|
function initTerminal() {
|
||||||
term = new Terminal({
|
term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Consolas, Monaco, "Courier New", monospace',
|
fontFamily: '"JetBrains Mono", "SFMono-Regular", "Consolas", monospace',
|
||||||
theme: {
|
theme: {
|
||||||
background: '#1e1e1e',
|
background: '#ffffff',
|
||||||
foreground: '#d4d4d4',
|
foreground: '#1f1f1f',
|
||||||
cursor: '#aeafad',
|
cursor: '#3d3929',
|
||||||
black: '#000000',
|
black: '#1f1f1f',
|
||||||
red: '#cd3131',
|
red: '#c04a2f',
|
||||||
green: '#0dbc79',
|
green: '#4b8f60',
|
||||||
yellow: '#e5e510',
|
yellow: '#b48a2c',
|
||||||
blue: '#2472c8',
|
blue: '#4a6ea9',
|
||||||
magenta: '#bc3fbc',
|
magenta: '#9b4d88',
|
||||||
cyan: '#11a8cd',
|
cyan: '#2a8c8c',
|
||||||
white: '#e5e5e5'
|
white: '#f4f0ea'
|
||||||
},
|
},
|
||||||
scrollback: 10000,
|
scrollback: 10000,
|
||||||
cols: 120, // 设置固定列数
|
cols: 120, // 设置固定列数
|
||||||
@ -538,47 +598,49 @@
|
|||||||
// 终端输出事件
|
// 终端输出事件
|
||||||
socket.on('terminal_output', (data) => {
|
socket.on('terminal_output', (data) => {
|
||||||
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
||||||
if (data.session === currentSession || !currentSession) {
|
appendSessionLog(data.session, data.data);
|
||||||
// 如果没有当前会话,自动切换到这个会话
|
let switchedDuringEvent = false;
|
||||||
if (!currentSession && data.session) {
|
if (!currentSession && data.session) {
|
||||||
if (!sessions[data.session]) {
|
if (!sessions[data.session]) {
|
||||||
addSession(data.session, {
|
addSession(data.session, {
|
||||||
session: data.session,
|
session: data.session,
|
||||||
working_dir: 'unknown'
|
working_dir: 'unknown'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
switchToSession(data.session);
|
|
||||||
}
|
}
|
||||||
// 直接写入原始输出
|
switchToSession(data.session);
|
||||||
term.write(data.data);
|
switchedDuringEvent = true;
|
||||||
stats.outputLines++;
|
|
||||||
stats.dataReceived += data.data.length;
|
|
||||||
updateStats();
|
|
||||||
}
|
}
|
||||||
|
if (data.session === currentSession && !switchedDuringEvent) {
|
||||||
|
term.write(data.data);
|
||||||
|
}
|
||||||
|
stats.outputLines++;
|
||||||
|
stats.dataReceived += data.data.length;
|
||||||
|
updateStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 终端输入事件(AI发送的命令)
|
// 终端输入事件(AI发送的命令)
|
||||||
socket.on('terminal_input', (data) => {
|
socket.on('terminal_input', (data) => {
|
||||||
console.log('收到终端输入:', data.session, data.data);
|
console.log('收到终端输入:', data.session, data.data);
|
||||||
if (data.session === currentSession || !currentSession) {
|
const formattedInput = `\x1b[1;32m➜ ${data.data}\x1b[0m`;
|
||||||
// 如果没有当前会话,自动切换到这个会话
|
appendSessionLog(data.session, formattedInput);
|
||||||
if (!currentSession && data.session) {
|
let switchedDuringEvent = false;
|
||||||
if (!sessions[data.session]) {
|
if (!currentSession && data.session) {
|
||||||
addSession(data.session, {
|
if (!sessions[data.session]) {
|
||||||
session: data.session,
|
addSession(data.session, {
|
||||||
working_dir: 'unknown'
|
session: data.session,
|
||||||
});
|
working_dir: 'unknown'
|
||||||
}
|
});
|
||||||
switchToSession(data.session);
|
|
||||||
}
|
}
|
||||||
// 用绿色显示输入的命令
|
switchToSession(data.session);
|
||||||
term.write(`\x1b[1;32m➜ ${data.data}\x1b[0m`);
|
switchedDuringEvent = true;
|
||||||
|
|
||||||
// 添加到命令历史
|
|
||||||
addCommandToHistory(data.data.trim());
|
|
||||||
stats.commandCount++;
|
|
||||||
updateStats();
|
|
||||||
}
|
}
|
||||||
|
if (data.session === currentSession && !switchedDuringEvent) {
|
||||||
|
term.write(formattedInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommandToHistory(data.data.trim());
|
||||||
|
stats.commandCount++;
|
||||||
|
updateStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 终端关闭事件
|
// 终端关闭事件
|
||||||
@ -602,6 +664,16 @@
|
|||||||
}
|
}
|
||||||
switchToSession(data.session);
|
switchToSession(data.session);
|
||||||
}
|
}
|
||||||
|
const targetSession = data.session || currentSession;
|
||||||
|
if (targetSession) {
|
||||||
|
sessionLogs[targetSession] = '';
|
||||||
|
sessionHydrated[targetSession] = true;
|
||||||
|
pendingHistory[targetSession] = false;
|
||||||
|
appendSessionLog(targetSession, `\x1b[33m[终端已重置]\x1b[0m ${targetSession}\n`);
|
||||||
|
if (targetSession === currentSession) {
|
||||||
|
renderCurrentSessionLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
||||||
resetCommandHistory();
|
resetCommandHistory();
|
||||||
stats.commandCount = 0;
|
stats.commandCount = 0;
|
||||||
@ -610,6 +682,15 @@
|
|||||||
stats.startTime = Date.now();
|
stats.startTime = Date.now();
|
||||||
updateStats();
|
updateStats();
|
||||||
});
|
});
|
||||||
|
socket.on('terminal_output_history', (data) => {
|
||||||
|
console.log('收到终端历史输出:', data);
|
||||||
|
handleHistoryPayload(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('terminal_history', (data) => {
|
||||||
|
console.log('收到终端历史:', data);
|
||||||
|
handleHistoryPayload(data);
|
||||||
|
});
|
||||||
|
|
||||||
// 终端切换事件
|
// 终端切换事件
|
||||||
socket.on('terminal_switched', (data) => {
|
socket.on('terminal_switched', (data) => {
|
||||||
@ -640,8 +721,11 @@
|
|||||||
// 移除会话
|
// 移除会话
|
||||||
function removeSession(name) {
|
function removeSession(name) {
|
||||||
delete sessions[name];
|
delete sessions[name];
|
||||||
|
delete sessionLogs[name];
|
||||||
|
delete sessionHydrated[name];
|
||||||
|
delete pendingHistory[name];
|
||||||
updateSessionTabs();
|
updateSessionTabs();
|
||||||
|
|
||||||
// 如果是当前会话,切换到其他会话
|
// 如果是当前会话,切换到其他会话
|
||||||
if (currentSession === name) {
|
if (currentSession === name) {
|
||||||
const remaining = Object.keys(sessions);
|
const remaining = Object.keys(sessions);
|
||||||
@ -650,23 +734,25 @@
|
|||||||
} else {
|
} else {
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
updateSessionInfo();
|
updateSessionInfo();
|
||||||
|
renderCurrentSessionLog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换会话
|
// 切换会话
|
||||||
function switchToSession(name) {
|
function switchToSession(name) {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
currentSession = name;
|
currentSession = name;
|
||||||
updateSessionTabs();
|
updateSessionTabs();
|
||||||
updateSessionInfo();
|
updateSessionInfo();
|
||||||
|
const needsHistory = !sessionHydrated[name];
|
||||||
// 清空终端并显示切换信息
|
pendingHistory[name] = needsHistory;
|
||||||
term.clear();
|
renderCurrentSessionLog();
|
||||||
term.writeln(`\x1b[36m[切换到终端: ${name}]\x1b[0m`);
|
if (needsHistory && socket) {
|
||||||
term.writeln('');
|
socket.emit('get_terminal_output', { session: name });
|
||||||
|
}
|
||||||
// 请求该会话的历史输出
|
|
||||||
socket.emit('get_terminal_output', { session: name });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新会话标签
|
// 更新会话标签
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user