revert: restore terminal monitor layout

This commit is contained in:
JOJO 2025-11-21 14:28:00 +08:00
parent d71c935d3c
commit 21f206559e
3 changed files with 266 additions and 173 deletions

View File

@ -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(

View File

@ -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>

View File

@ -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 });
} }
// 更新会话标签 // 更新会话标签