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