From a2f7c5f466782a560144c502e37c889457359f13 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Sat, 28 Feb 2026 08:34:27 -0300 Subject: [PATCH] =?UTF-8?q?Adicionar=20timer=20e=20melhorar=20espa=C3=A7am?= =?UTF-8?q?ento=20da=20toolbar=20do=20terminal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Contador de tempo (mm:ss ou hh:mm:ss) que inicia ao processar e persiste entre reloads via sessionStorage - Espaçamento maior entre elementos da toolbar principal e action bar - Timer para ao completar execução/pipeline ou ao limpar terminal --- public/app.html | 4 ++ public/css/styles.css | 28 ++++++++-- public/js/app.js | 15 +++++ public/js/components/terminal.js | 96 ++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 6 deletions(-) diff --git a/public/app.html b/public/app.html index 811d87a..ad05482 100644 --- a/public/app.html +++ b/public/app.html @@ -486,6 +486,10 @@ Output de Execução
+ diff --git a/public/css/styles.css b/public/css/styles.css index f9742c7..f47578d 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -2681,25 +2681,39 @@ tbody tr:hover td { .terminal-toolbar { background-color: var(--bg-secondary); - padding: 10px 16px; + padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--border-primary); flex-shrink: 0; - gap: 12px; + gap: 16px; } .terminal-toolbar-left { display: flex; align-items: center; - gap: 8px; + gap: 10px; } .terminal-toolbar-right { display: flex; align-items: center; - gap: 8px; + gap: 12px; +} + +.terminal-timer { + display: flex; + align-items: center; + gap: 6px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + font-weight: 500; + color: var(--success); + background: rgba(34, 197, 94, 0.1); + padding: 4px 10px; + border-radius: 6px; + border: 1px solid rgba(34, 197, 94, 0.2); } .terminal-dot--red { @@ -4489,13 +4503,15 @@ body, .sidebar, .header, .card, .modal-content, .input, .select, textarea, .metr .terminal-action-toolbar { display: flex; align-items: center; justify-content: space-between; - padding: 0.5rem 0.75rem; + padding: 0.5rem 1rem; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-bottom: none; border-radius: 8px 8px 0 0; + gap: 12px; } -.terminal-toolbar-left, .terminal-toolbar-right { display: flex; align-items: center; gap: 0.25rem; } +.terminal-action-toolbar .terminal-toolbar-left, +.terminal-action-toolbar .terminal-toolbar-right { display: flex; align-items: center; gap: 0.5rem; } .terminal-toggle-label { display: flex; align-items: center; gap: 0.375rem; font-size: 0.75rem; color: var(--text-secondary); cursor: pointer; user-select: none; diff --git a/public/js/app.js b/public/js/app.js index 85262c5..d657ba8 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -40,6 +40,8 @@ const App = { App._pipelineDropzone = Utils.initDropzone('pipeline-execute-dropzone', 'pipeline-execute-files', 'pipeline-execute-file-list'); App._initRepoSelectors(); + Terminal.restoreIfActive(); + const initialSection = location.hash.replace('#', '') || 'dashboard'; App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard'); App.startPeriodicRefresh(); @@ -235,6 +237,7 @@ const App = { Toast.success('Execução concluída'); App.refreshCurrentSection(); App._updateActiveBadge(); + App._checkStopTimer(); break; } @@ -249,6 +252,7 @@ const App = { Toast.error(`Erro na execução: ${data.data?.error || 'desconhecido'}`); App._updateActiveBadge(); + App._checkStopTimer(); break; case 'execution_retry': @@ -294,6 +298,7 @@ const App = { case 'pipeline_complete': Terminal.stopProcessing(); + Terminal._hideTimer(); Terminal.addLine('Pipeline concluído com sucesso.', 'success'); if (data.lastSessionId && data.lastAgentId) { Terminal.enableChat(data.lastAgentId, data.lastAgentName || 'Agente', data.lastSessionId); @@ -304,6 +309,7 @@ const App = { case 'pipeline_error': Terminal.stopProcessing(); + Terminal._hideTimer(); Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error'); Toast.error('Erro no pipeline'); break; @@ -1074,6 +1080,15 @@ const App = { } }, + async _checkStopTimer() { + try { + const active = await API.system.activeExecutions(); + if (!Array.isArray(active) || active.length === 0) { + Terminal._hideTimer(); + } + } catch {} + }, + startPeriodicRefresh() { setInterval(async () => { await App._updateActiveBadge(); diff --git a/public/js/components/terminal.js b/public/js/components/terminal.js index b47f257..3637abc 100644 --- a/public/js/components/terminal.js +++ b/public/js/components/terminal.js @@ -8,9 +8,60 @@ const Terminal = { searchMatches: [], searchIndex: -1, _toolbarInitialized: false, + _storageKey: 'terminal_lines', + _chatStorageKey: 'terminal_chat', + _timerInterval: null, + _timerStart: null, + _timerStorageKey: 'terminal_timer_start', + + _saveToStorage() { + try { + const data = JSON.stringify(Terminal.lines.slice(-Terminal.maxLines)); + sessionStorage.setItem(Terminal._storageKey, data); + } catch {} + }, + + _restoreFromStorage() { + try { + const data = sessionStorage.getItem(Terminal._storageKey); + if (data) { + Terminal.lines = JSON.parse(data); + return true; + } + } catch {} + return false; + }, + + _clearStorage() { + try { + sessionStorage.removeItem(Terminal._storageKey); + sessionStorage.removeItem(Terminal._chatStorageKey); + } catch {} + }, + + async restoreIfActive() { + try { + const active = await API.system.activeExecutions(); + const hasActive = Array.isArray(active) && active.length > 0; + if (hasActive && Terminal._restoreFromStorage()) { + Terminal.render(); + const savedStart = sessionStorage.getItem(Terminal._timerStorageKey); + Terminal._startTimer(savedStart ? Number(savedStart) : null); + Terminal.startProcessing(active[0].agentConfig?.agent_name || 'Agente'); + try { + const chatData = sessionStorage.getItem(Terminal._chatStorageKey); + if (chatData) Terminal._chatSession = JSON.parse(chatData); + } catch {} + } else if (!hasActive) { + Terminal._clearStorage(); + Terminal._hideTimer(); + } + } catch {} + }, enableChat(agentId, agentName, sessionId) { Terminal._chatSession = { agentId, agentName, sessionId }; + try { sessionStorage.setItem(Terminal._chatStorageKey, JSON.stringify(Terminal._chatSession)); } catch {} const bar = document.getElementById('terminal-input-bar'); const ctx = document.getElementById('terminal-input-context'); const input = document.getElementById('terminal-input'); @@ -21,6 +72,7 @@ const Terminal = { disableChat() { Terminal._chatSession = null; + try { sessionStorage.removeItem(Terminal._chatStorageKey); } catch {} const bar = document.getElementById('terminal-input-bar'); if (bar) bar.hidden = true; }, @@ -43,13 +95,55 @@ const Terminal = { Terminal.lines.shift(); } + Terminal._saveToStorage(); Terminal.render(); }, + _startTimer(fromTimestamp) { + Terminal._stopTimer(); + Terminal._timerStart = fromTimestamp || Date.now(); + try { sessionStorage.setItem(Terminal._timerStorageKey, String(Terminal._timerStart)); } catch {} + + const timerEl = document.getElementById('terminal-timer'); + const valueEl = document.getElementById('terminal-timer-value'); + if (timerEl) timerEl.hidden = false; + + const tick = () => { + if (!valueEl) return; + const elapsed = Math.floor((Date.now() - Terminal._timerStart) / 1000); + const h = Math.floor(elapsed / 3600); + const m = Math.floor((elapsed % 3600) / 60); + const s = elapsed % 60; + valueEl.textContent = h > 0 + ? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}` + : `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + }; + tick(); + Terminal._timerInterval = setInterval(tick, 1000); + }, + + _stopTimer() { + if (Terminal._timerInterval) { + clearInterval(Terminal._timerInterval); + Terminal._timerInterval = null; + } + try { sessionStorage.removeItem(Terminal._timerStorageKey); } catch {} + }, + + _hideTimer() { + Terminal._stopTimer(); + const timerEl = document.getElementById('terminal-timer'); + if (timerEl) timerEl.hidden = true; + }, + startProcessing(agentName) { Terminal.stopProcessing(); Terminal.addLine(`Agente "${agentName}" processando tarefa...`, 'system'); + if (!Terminal._timerInterval) { + Terminal._startTimer(); + } + let dots = 0; Terminal._processingInterval = setInterval(() => { dots = (dots + 1) % 4; @@ -71,8 +165,10 @@ const Terminal = { clear() { Terminal.stopProcessing(); + Terminal._hideTimer(); Terminal.lines = []; Terminal.executionFilter = null; + Terminal._clearStorage(); Terminal.render(); },