Persistir output do terminal no servidor e corrigir cursor do flow editor

- 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
This commit is contained in:
Frederico Castro
2026-02-28 11:02:46 -03:00
parent 1718c3c68e
commit 4a50fc9fd7
4 changed files with 64 additions and 10 deletions

View File

@@ -90,6 +90,8 @@ const App = {
history.pushState(null, '', `#${section}`); history.pushState(null, '', `#${section}`);
} }
if (typeof FlowEditor !== 'undefined') FlowEditor._teardown();
document.querySelectorAll('.section').forEach((el) => { document.querySelectorAll('.section').forEach((el) => {
const isActive = el.id === section; const isActive = el.id === section;
el.classList.toggle('active', isActive); el.classList.toggle('active', isActive);

View File

@@ -737,8 +737,12 @@ const FlowEditor = {
if (!leave) return; if (!leave) return;
} }
FlowEditor._teardown();
},
_teardown() {
const overlay = FlowEditor._overlay; const overlay = FlowEditor._overlay;
if (!overlay) return; if (!overlay || overlay.hidden) return;
overlay.classList.remove('active'); overlay.classList.remove('active');
setTimeout(() => { overlay.hidden = true; }, 200); setTimeout(() => { overlay.hidden = true; }, 200);
@@ -755,6 +759,7 @@ const FlowEditor = {
FlowEditor._selectedNode = null; FlowEditor._selectedNode = null;
FlowEditor._dragState = null; FlowEditor._dragState = null;
FlowEditor._panStart = null; FlowEditor._panStart = null;
FlowEditor._dirty = false;
}, },
}; };

View File

@@ -43,16 +43,35 @@ const Terminal = {
try { try {
const active = await API.system.activeExecutions(); const active = await API.system.activeExecutions();
const hasActive = Array.isArray(active) && active.length > 0; 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(); Terminal.render();
const startedAt = exec.startedAt ? new Date(exec.startedAt).getTime() : null;
const savedStart = sessionStorage.getItem(Terminal._timerStorageKey); const savedStart = sessionStorage.getItem(Terminal._timerStorageKey);
Terminal._startTimer(savedStart ? Number(savedStart) : null); Terminal._startTimer(savedStart ? Number(savedStart) : startedAt);
Terminal.startProcessing(active[0].agentConfig?.agent_name || 'Agente'); Terminal.startProcessing(exec.agentConfig?.agent_name || 'Agente');
try { try {
const chatData = sessionStorage.getItem(Terminal._chatStorageKey); const chatData = sessionStorage.getItem(Terminal._chatStorageKey);
if (chatData) Terminal._chatSession = JSON.parse(chatData); if (chatData) Terminal._chatSession = JSON.parse(chatData);
} catch {} } catch {}
} else if (!hasActive) { } else {
Terminal._clearStorage(); Terminal._clearStorage();
Terminal._hideTimer(); Terminal._hideTimer();
} }

View File

@@ -9,8 +9,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const AGENT_SETTINGS = path.resolve(__dirname, '..', '..', 'data', 'agent-settings.json'); const AGENT_SETTINGS = path.resolve(__dirname, '..', '..', 'data', 'agent-settings.json');
const CLAUDE_BIN = resolveBin(); const CLAUDE_BIN = resolveBin();
const activeExecutions = new Map(); const activeExecutions = new Map();
const executionOutputBuffers = new Map();
const MAX_OUTPUT_SIZE = 512 * 1024; const MAX_OUTPUT_SIZE = 512 * 1024;
const MAX_ERROR_SIZE = 100 * 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); const ALLOWED_DIRECTORIES = (process.env.ALLOWED_DIRECTORIES || '').split(',').map(d => d.trim()).filter(Boolean);
let maxConcurrent = settingsStore.get().maxConcurrent || 5; let maxConcurrent = settingsStore.get().maxConcurrent || 5;
@@ -52,6 +54,8 @@ function cleanEnv(agentSecrets) {
delete env.CLAUDECODE; delete env.CLAUDECODE;
delete env.ANTHROPIC_API_KEY; delete env.ANTHROPIC_API_KEY;
env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000'; 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') { if (agentSecrets && typeof agentSecrets === 'object') {
Object.assign(env, agentSecrets); Object.assign(env, agentSecrets);
} }
@@ -179,6 +183,16 @@ function extractSystemInfo(event) {
return null; 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 = {}) { function processChildOutput(child, executionId, callbacks, options = {}) {
const { onData, onError, onComplete } = callbacks; const { onData, onError, onComplete } = callbacks;
const timeoutMs = options.timeout || 1800000; const timeoutMs = options.timeout || 1800000;
@@ -202,7 +216,9 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
if (tools) { if (tools) {
for (const t of tools) { for (const t of tools) {
const msg = t.detail ? `${t.name}: ${t.detail}` : t.name; 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) { if (fullText.length < MAX_OUTPUT_SIZE) {
fullText += text; 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); const sysInfo = extractSystemInfo(parsed);
if (sysInfo) { 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') { if (parsed.type === 'assistant') {
turnCount++; 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') { if (parsed.type === 'result') {
@@ -251,7 +273,9 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
} }
const lines = str.split('\n').filter(l => l.trim()); const lines = str.split('\n').filter(l => l.trim());
for (const line of lines) { 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}`); console.log(`[executor][error] ${err.message}`);
hadError = true; hadError = true;
activeExecutions.delete(executionId); activeExecutions.delete(executionId);
executionOutputBuffers.delete(executionId);
if (onError) onError(err, executionId); if (onError) onError(err, executionId);
}); });
@@ -267,6 +292,7 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
clearTimeout(timeout); clearTimeout(timeout);
const wasCanceled = activeExecutions.get(executionId)?.canceled || false; const wasCanceled = activeExecutions.get(executionId)?.canceled || false;
activeExecutions.delete(executionId); activeExecutions.delete(executionId);
executionOutputBuffers.delete(executionId);
if (hadError) return; if (hadError) return;
if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer)); if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer));
@@ -426,6 +452,7 @@ export function cancel(executionId) {
export function cancelAllExecutions() { export function cancelAllExecutions() {
for (const [, exec] of activeExecutions) exec.process.kill('SIGTERM'); for (const [, exec] of activeExecutions) exec.process.kill('SIGTERM');
activeExecutions.clear(); activeExecutions.clear();
executionOutputBuffers.clear();
} }
export function getActiveExecutions() { export function getActiveExecutions() {
@@ -433,6 +460,7 @@ export function getActiveExecutions() {
executionId: exec.executionId, executionId: exec.executionId,
startedAt: exec.startedAt, startedAt: exec.startedAt,
agentConfig: exec.agentConfig, agentConfig: exec.agentConfig,
outputBuffer: executionOutputBuffers.get(exec.executionId) || [],
})); }));
} }