diff --git a/modules/terminal_manager.py b/modules/terminal_manager.py index d33fb0b..811c62a 100644 --- a/modules/terminal_manager.py +++ b/modules/terminal_manager.py @@ -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( diff --git a/static/index.html b/static/index.html index ae7d0f8..9f39156 100644 --- a/static/index.html +++ b/static/index.html @@ -616,6 +616,7 @@ +
diff --git a/static/terminal.html b/static/terminal.html index f0a31f0..f93a1c5 100644 --- a/static/terminal.html +++ b/static/terminal.html @@ -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 @@
-
+
+
+
@@ -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 }); + } } // 更新会话标签