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

View File

@ -616,6 +616,7 @@
</transition>
</div>
</div>
</main>
<!-- 右侧拖拽手柄 -->
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>

View File

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