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
+
+
+ 00:00
+
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();
},