From 0230265ca1dae5b4e958226115fa695626303fa9 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Sat, 28 Feb 2026 11:02:46 -0300 Subject: [PATCH] Persistir output do terminal no servidor e corrigir cursor do flow editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Buffer server-side no executor para manter até 1000 linhas por execução ativa - Terminal restaura output do servidor ao recarregar a página (F5) - Fechar overlay do flow editor ao navegar para outra seção - Garantir SHELL e HOME no ambiente dos processos filhos --- public/js/app.js | 2 ++ public/js/components/flow-editor.js | 7 +++++- public/js/components/terminal.js | 27 +++++++++++++++++--- src/agents/executor.js | 38 +++++++++++++++++++++++++---- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 275ac14..84b53a8 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -90,6 +90,8 @@ const App = { history.pushState(null, '', `#${section}`); } + if (typeof FlowEditor !== 'undefined') FlowEditor._teardown(); + document.querySelectorAll('.section').forEach((el) => { const isActive = el.id === section; el.classList.toggle('active', isActive); diff --git a/public/js/components/flow-editor.js b/public/js/components/flow-editor.js index 1f503ae..8d4d67b 100644 --- a/public/js/components/flow-editor.js +++ b/public/js/components/flow-editor.js @@ -737,8 +737,12 @@ const FlowEditor = { if (!leave) return; } + FlowEditor._teardown(); + }, + + _teardown() { const overlay = FlowEditor._overlay; - if (!overlay) return; + if (!overlay || overlay.hidden) return; overlay.classList.remove('active'); setTimeout(() => { overlay.hidden = true; }, 200); @@ -755,6 +759,7 @@ const FlowEditor = { FlowEditor._selectedNode = null; FlowEditor._dragState = null; FlowEditor._panStart = null; + FlowEditor._dirty = false; }, }; diff --git a/public/js/components/terminal.js b/public/js/components/terminal.js index 3637abc..99628c1 100644 --- a/public/js/components/terminal.js +++ b/public/js/components/terminal.js @@ -43,16 +43,35 @@ const Terminal = { try { const active = await API.system.activeExecutions(); const hasActive = Array.isArray(active) && active.length > 0; - if (hasActive && Terminal._restoreFromStorage()) { + if (hasActive) { + const exec = active[0]; + const serverBuffer = Array.isArray(exec.outputBuffer) ? exec.outputBuffer : []; + + if (serverBuffer.length > 0) { + Terminal.lines = serverBuffer.map((item) => { + const time = new Date(); + return { + content: item.content || '', + type: item.type || 'default', + timestamp: time.toTimeString().slice(0, 8), + executionId: exec.executionId, + }; + }); + Terminal._saveToStorage(); + } else { + Terminal._restoreFromStorage(); + } + Terminal.render(); + const startedAt = exec.startedAt ? new Date(exec.startedAt).getTime() : null; const savedStart = sessionStorage.getItem(Terminal._timerStorageKey); - Terminal._startTimer(savedStart ? Number(savedStart) : null); - Terminal.startProcessing(active[0].agentConfig?.agent_name || 'Agente'); + Terminal._startTimer(savedStart ? Number(savedStart) : startedAt); + Terminal.startProcessing(exec.agentConfig?.agent_name || 'Agente'); try { const chatData = sessionStorage.getItem(Terminal._chatStorageKey); if (chatData) Terminal._chatSession = JSON.parse(chatData); } catch {} - } else if (!hasActive) { + } else { Terminal._clearStorage(); Terminal._hideTimer(); } diff --git a/src/agents/executor.js b/src/agents/executor.js index 3d92bbb..fd4ebf8 100644 --- a/src/agents/executor.js +++ b/src/agents/executor.js @@ -9,8 +9,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const AGENT_SETTINGS = path.resolve(__dirname, '..', '..', 'data', 'agent-settings.json'); const CLAUDE_BIN = resolveBin(); const activeExecutions = new Map(); +const executionOutputBuffers = new Map(); const MAX_OUTPUT_SIZE = 512 * 1024; const MAX_ERROR_SIZE = 100 * 1024; +const MAX_BUFFER_LINES = 1000; const ALLOWED_DIRECTORIES = (process.env.ALLOWED_DIRECTORIES || '').split(',').map(d => d.trim()).filter(Boolean); let maxConcurrent = settingsStore.get().maxConcurrent || 5; @@ -52,6 +54,8 @@ function cleanEnv(agentSecrets) { delete env.CLAUDECODE; delete env.ANTHROPIC_API_KEY; env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000'; + if (!env.SHELL) env.SHELL = '/bin/bash'; + if (!env.HOME) env.HOME = process.env.HOME || '/root'; if (agentSecrets && typeof agentSecrets === 'object') { Object.assign(env, agentSecrets); } @@ -179,6 +183,16 @@ function extractSystemInfo(event) { return null; } +function bufferLine(executionId, data) { + let buf = executionOutputBuffers.get(executionId); + if (!buf) { + buf = []; + executionOutputBuffers.set(executionId, buf); + } + buf.push(data); + if (buf.length > MAX_BUFFER_LINES) buf.shift(); +} + function processChildOutput(child, executionId, callbacks, options = {}) { const { onData, onError, onComplete } = callbacks; const timeoutMs = options.timeout || 1800000; @@ -202,7 +216,9 @@ function processChildOutput(child, executionId, callbacks, options = {}) { if (tools) { for (const t of tools) { const msg = t.detail ? `${t.name}: ${t.detail}` : t.name; - if (onData) onData({ type: 'tool', content: msg, toolName: t.name }, executionId); + const data = { type: 'tool', content: msg, toolName: t.name }; + bufferLine(executionId, data); + if (onData) onData(data, executionId); } } @@ -211,17 +227,23 @@ function processChildOutput(child, executionId, callbacks, options = {}) { if (fullText.length < MAX_OUTPUT_SIZE) { fullText += text; } - if (onData) onData({ type: 'chunk', content: text }, executionId); + const data = { type: 'chunk', content: text }; + bufferLine(executionId, data); + if (onData) onData(data, executionId); } const sysInfo = extractSystemInfo(parsed); if (sysInfo) { - if (onData) onData({ type: 'system', content: sysInfo }, executionId); + const data = { type: 'system', content: sysInfo }; + bufferLine(executionId, data); + if (onData) onData(data, executionId); } if (parsed.type === 'assistant') { turnCount++; - if (onData) onData({ type: 'turn', content: `Turno ${turnCount}`, turn: turnCount }, executionId); + const data = { type: 'turn', content: `Turno ${turnCount}`, turn: turnCount }; + bufferLine(executionId, data); + if (onData) onData(data, executionId); } if (parsed.type === 'result') { @@ -251,7 +273,9 @@ function processChildOutput(child, executionId, callbacks, options = {}) { } const lines = str.split('\n').filter(l => l.trim()); for (const line of lines) { - if (onData) onData({ type: 'stderr', content: line.trim() }, executionId); + const data = { type: 'stderr', content: line.trim() }; + bufferLine(executionId, data); + if (onData) onData(data, executionId); } }); @@ -260,6 +284,7 @@ function processChildOutput(child, executionId, callbacks, options = {}) { console.log(`[executor][error] ${err.message}`); hadError = true; activeExecutions.delete(executionId); + executionOutputBuffers.delete(executionId); if (onError) onError(err, executionId); }); @@ -267,6 +292,7 @@ function processChildOutput(child, executionId, callbacks, options = {}) { clearTimeout(timeout); const wasCanceled = activeExecutions.get(executionId)?.canceled || false; activeExecutions.delete(executionId); + executionOutputBuffers.delete(executionId); if (hadError) return; if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer)); @@ -426,6 +452,7 @@ export function cancel(executionId) { export function cancelAllExecutions() { for (const [, exec] of activeExecutions) exec.process.kill('SIGTERM'); activeExecutions.clear(); + executionOutputBuffers.clear(); } export function getActiveExecutions() { @@ -433,6 +460,7 @@ export function getActiveExecutions() { executionId: exec.executionId, startedAt: exec.startedAt, agentConfig: exec.agentConfig, + outputBuffer: executionOutputBuffers.get(exec.executionId) || [], })); }