Continuação de conversa no terminal, histórico de agendamentos, webhooks e melhorias gerais

- Terminal com input de chat: após execução, permite continuar conversa com o agente
  via --resume do CLI, mantendo contexto da sessão (sessionId persistido)
- Nova rota POST /api/agents/:id/continue para retomar sessões
- Executor com função resume() para spawnar claude com --resume <sessionId>
- Histórico de agendamentos agora busca do executionsStore (persistente) com dados
  completos: agente, tarefa, status, duração, custo e link para detalhes no modal
- Execuções de agendamento tagueadas com source:'schedule' e scheduleId
- Correção da expressão cron duplicada na UI de agendamentos
- cronToHuman trata expressões com minuto específico (ex: 37 3 * * * → Todo dia às 03:37)
- Botão "Copiar cURL" nos cards de webhook com payload de exemplo contextual
- Webhooks component (webhooks.js) adicionado ao repositório
This commit is contained in:
Frederico Castro
2026-02-26 04:01:12 -03:00
parent 22a3ce9262
commit 93d9027e2c
18 changed files with 1609 additions and 75 deletions

View File

@@ -4,6 +4,7 @@ const App = {
wsReconnectAttempts: 0,
wsReconnectTimer: null,
_initialized: false,
_lastAgentName: '',
sectionTitles: {
dashboard: 'Dashboard',
@@ -11,6 +12,7 @@ const App = {
tasks: 'Tarefas',
schedules: 'Agendamentos',
pipelines: 'Pipelines',
webhooks: 'Webhooks',
terminal: 'Terminal',
history: 'Histórico',
settings: 'Configurações',
@@ -71,6 +73,7 @@ const App = {
case 'tasks': await TasksUI.load(); break;
case 'schedules': await SchedulesUI.load(); break;
case 'pipelines': await PipelinesUI.load(); break;
case 'webhooks': await WebhooksUI.load(); break;
case 'history': await HistoryUI.load(); break;
case 'settings': await SettingsUI.load(); break;
}
@@ -154,6 +157,23 @@ const App = {
if (data.data?.stderr) {
Terminal.addLine(data.data.stderr, 'error', data.executionId);
}
const costUsd = data.data?.costUsd || 0;
const numTurns = data.data?.numTurns || 0;
if (costUsd > 0) {
Terminal.addLine(`Custo: $${costUsd.toFixed(4)} | Turnos: ${numTurns}`, 'info', data.executionId);
}
const sessionId = data.data?.sessionId || '';
if (sessionId && data.agentId) {
if (Terminal.getChatSession()?.sessionId === sessionId || !Terminal.getChatSession()) {
const agentName = App._lastAgentName || 'Agente';
Terminal.enableChat(data.agentId, agentName, sessionId);
}
if (Terminal.getChatSession()) {
Terminal.updateSessionId(sessionId);
}
}
Toast.success('Execução concluída');
App.refreshCurrentSection();
App._updateActiveBadge();
@@ -200,6 +220,82 @@ const App = {
Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error');
Toast.error('Erro no pipeline');
break;
case 'pipeline_approval_required':
Terminal.stopProcessing();
Terminal.addLine(`Passo ${data.stepIndex + 1} requer aprovação antes de executar.`, 'system');
if (data.previousOutput) {
Terminal.addLine(`Output do passo anterior:\n${data.previousOutput.slice(0, 1000)}`, 'info');
}
App._showApprovalNotification(data.pipelineId, data.stepIndex, data.agentName);
Toast.warning('Pipeline aguardando aprovação');
break;
case 'pipeline_rejected':
Terminal.stopProcessing();
Terminal.addLine(`Pipeline rejeitado no passo ${data.stepIndex + 1}.`, 'error');
App._hideApprovalNotification();
Toast.info('Pipeline rejeitado');
App.refreshCurrentSection();
break;
case 'pipeline_status':
break;
}
},
_showApprovalNotification(pipelineId, stepIndex, agentName) {
const container = document.getElementById('approval-notification');
if (!container) return;
container.innerHTML = `
<div class="approval-content">
<div class="approval-icon"><i data-lucide="shield-alert"></i></div>
<div class="approval-text">
<strong>Aprovação necessária</strong>
<span>Passo ${stepIndex + 1} (${agentName || 'agente'}) aguardando autorização</span>
</div>
<div class="approval-actions">
<button class="btn btn--primary btn--sm" id="approval-approve-btn" type="button">Aprovar</button>
<button class="btn btn--danger btn--sm" id="approval-reject-btn" type="button">Rejeitar</button>
</div>
</div>
`;
container.hidden = false;
container.dataset.pipelineId = pipelineId;
if (window.lucide) lucide.createIcons({ nodes: [container] });
document.getElementById('approval-approve-btn')?.addEventListener('click', () => {
App._handleApproval(pipelineId, true);
});
document.getElementById('approval-reject-btn')?.addEventListener('click', () => {
App._handleApproval(pipelineId, false);
});
},
_hideApprovalNotification() {
const container = document.getElementById('approval-notification');
if (container) {
container.hidden = true;
container.innerHTML = '';
}
},
async _handleApproval(pipelineId, approve) {
try {
if (approve) {
await API.pipelines.approve(pipelineId);
Terminal.addLine('Passo aprovado. Continuando pipeline...', 'success');
Toast.success('Passo aprovado');
} else {
await API.pipelines.reject(pipelineId);
Terminal.addLine('Pipeline rejeitado pelo usuário.', 'error');
Toast.info('Pipeline rejeitado');
}
App._hideApprovalNotification();
} catch (err) {
Toast.error(`Erro: ${err.message}`);
}
},
@@ -306,6 +402,17 @@ const App = {
SchedulesUI.save();
});
on('webhooks-new-btn', 'click', () => WebhooksUI.openCreateModal());
on('webhook-form-submit', 'click', (e) => {
e.preventDefault();
WebhooksUI.save();
});
on('webhook-target-type', 'change', (e) => {
WebhooksUI._updateTargetSelect(e.target.value);
});
on('pipelines-new-btn', 'click', () => PipelinesUI.openCreateModal());
on('pipeline-form-submit', 'click', (e) => {
@@ -317,7 +424,19 @@ const App = {
on('pipeline-execute-submit', 'click', () => PipelinesUI._executeFromModal());
on('terminal-clear-btn', 'click', () => Terminal.clear());
on('terminal-clear-btn', 'click', () => {
Terminal.clear();
Terminal.disableChat();
});
on('terminal-send-btn', 'click', () => App._sendChatMessage());
on('terminal-input', 'keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
App._sendChatMessage();
}
});
on('export-copy-btn', 'click', () => App._copyExportJson());
@@ -374,6 +493,10 @@ const App = {
);
});
on('webhooks-search', 'input', () => {
WebhooksUI.filter(document.getElementById('webhooks-search')?.value);
});
on('pipelines-search', 'input', () => {
PipelinesUI.filter(document.getElementById('pipelines-search')?.value);
});
@@ -443,6 +566,13 @@ const App = {
}
});
document.getElementById('schedules-history')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const { action, id } = btn.dataset;
if (action === 'view-schedule-exec') HistoryUI.viewDetail(id);
});
document.getElementById('pipelines-grid')?.addEventListener('click', (e) => {
if (e.target.closest('#pipelines-empty-new-btn')) {
PipelinesUI.openCreateModal();
@@ -471,6 +601,18 @@ const App = {
}
});
document.getElementById('webhooks-list')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const { action, id, url } = btn.dataset;
switch (action) {
case 'toggle-webhook': WebhooksUI.toggleActive(id); break;
case 'delete-webhook': WebhooksUI.delete(id); break;
case 'copy-webhook-url': WebhooksUI.copyUrl(url); break;
case 'copy-webhook-curl': WebhooksUI.copyCurl(id); break;
}
});
document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-step-action]');
if (!btn) return;
@@ -572,6 +714,9 @@ const App = {
const selectEl = document.getElementById('execute-agent-select');
const agentName = selectEl?.selectedOptions[0]?.text || 'Agente';
Terminal.disableChat();
App._lastAgentName = agentName;
await API.agents.execute(agentId, task, instructions);
Modal.close('execute-modal-overlay');
@@ -583,6 +728,27 @@ const App = {
}
},
async _sendChatMessage() {
const session = Terminal.getChatSession();
if (!session) return;
const input = document.getElementById('terminal-input');
const message = input?.value.trim();
if (!message) return;
input.value = '';
Terminal.addLine(` ${message}`, 'user-message', null);
try {
await API.agents.continue(session.agentId, session.sessionId, message);
Terminal.startProcessing(session.agentName);
} catch (err) {
Terminal.addLine(`Erro: ${err.message}`, 'error');
Toast.error(`Erro ao continuar conversa: ${err.message}`);
}
},
async _copyExportJson() {
const jsonEl = document.getElementById('export-code-content');
if (!jsonEl) return;