Corrigir E2BIG em pipelines, adicionar diretório de projeto e retomada

- Instalar Claude CLI no container Docker (npm install -g)
- Pipar prompt via stdin ao invés de argumento -p (resolve E2BIG)
- Adicionar campo workingDirectory na criação/edição de pipeline
- Pre-preencher com /home/projetos/ como base path
- Auto-criar diretório se não existir ao executar agente
- Salvar failedAtStep e lastStepInput quando pipeline falha
- Implementar retomada de pipeline a partir do passo que falhou
- Adicionar botão Retomar no histórico para pipelines com erro
- Configurar trust proxy para Express atrás de reverse proxy
This commit is contained in:
Frederico Castro
2026-02-27 23:45:36 -03:00
parent 38556f9bf5
commit 275d74b18c
11 changed files with 274 additions and 16 deletions

View File

@@ -3489,6 +3489,25 @@ tbody tr:hover td {
}
}
.pipeline-workdir-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
background-color: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 6px;
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
}
.pipeline-workdir-badge code {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
}
.pipeline-flow {
display: flex;
flex-direction: column;

View File

@@ -1138,6 +1138,14 @@
<label class="form-label" for="pipeline-description">Descrição</label>
<textarea class="textarea" id="pipeline-description" rows="2" placeholder="Descreva o objetivo do pipeline..."></textarea>
</div>
<div class="form-group">
<label class="form-label" for="pipeline-workdir">
<i data-lucide="folder" style="width:14px;height:14px"></i>
Diretório do Projeto
</label>
<input class="input" type="text" id="pipeline-workdir" autocomplete="off">
<p class="form-hint">Caminho onde o projeto será criado/trabalhado. Todos os passos usarão este diretório.</p>
</div>
<div class="form-group">
<label class="form-label">
Passos do Pipeline
@@ -1194,7 +1202,7 @@
type="text"
class="input"
id="pipeline-execute-workdir"
placeholder="/home/fred/projetos/meu-projeto"
placeholder="/home/projetos/meu-projeto"
autocomplete="off"
/>
<p class="form-hint">Se vazio, cada agente usa seu próprio diretório configurado.</p>

View File

@@ -91,6 +91,7 @@ const API = {
cancel(id) { return API.request('POST', `/pipelines/${id}/cancel`); },
approve(id) { return API.request('POST', `/pipelines/${id}/approve`); },
reject(id) { return API.request('POST', `/pipelines/${id}/reject`); },
resume(executionId) { return API.request('POST', `/pipelines/resume/${executionId}`); },
},
webhooks: {

View File

@@ -276,6 +276,7 @@ const App = {
case 'pipeline_step_start':
Terminal.stopProcessing();
if (data.resumed) Terminal.addLine('(retomando execução anterior)', 'system');
Terminal.addLine(`Pipeline passo ${data.stepIndex + 1}/${data.totalSteps}: Executando agente "${data.agentName}"...`, 'system');
Terminal.startProcessing(data.agentName);
break;
@@ -732,6 +733,7 @@ const App = {
case 'view-execution': HistoryUI.viewDetail(id); break;
case 'delete-execution': HistoryUI.deleteExecution(id); break;
case 'retry': HistoryUI.retryExecution(id); break;
case 'resume-pipeline': HistoryUI.resumePipeline(id); break;
}
});

View File

@@ -102,6 +102,11 @@ const HistoryUI = {
<i data-lucide="eye"></i>
Ver detalhes
</button>
${(exec.status === 'error' && exec.type === 'pipeline' && exec.failedAtStep !== undefined) ? `
<button class="btn btn-ghost btn-sm" data-action="resume-pipeline" data-id="${exec.id}" type="button" title="Retomar do passo ${(exec.failedAtStep || 0) + 1}">
<i data-lucide="play"></i>
Retomar
</button>` : ''}
${(exec.status === 'error' || exec.status === 'canceled') ? `
<button class="btn btn-ghost btn-sm" data-action="retry" data-id="${exec.id}" type="button" title="Reexecutar">
<i data-lucide="refresh-cw"></i>
@@ -421,6 +426,16 @@ const HistoryUI = {
Toast.success('Download iniciado');
},
async resumePipeline(executionId) {
try {
await API.pipelines.resume(executionId);
Toast.info('Pipeline retomado');
App.navigateTo('terminal');
} catch (err) {
Toast.error(`Erro ao retomar pipeline: ${err.message}`);
}
},
async retryExecution(id) {
try {
await API.executions.retry(id);

View File

@@ -107,6 +107,8 @@ const PipelinesUI = {
${pipeline.description ? `<p class="agent-description">${Utils.escapeHtml(pipeline.description)}</p>` : ''}
${pipeline.workingDirectory ? `<div class="pipeline-workdir-badge"><i data-lucide="folder" style="width:12px;height:12px"></i> <code>${Utils.escapeHtml(pipeline.workingDirectory)}</code></div>` : ''}
<div class="pipeline-flow">
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
</div>
@@ -133,6 +135,8 @@ const PipelinesUI = {
`;
},
_basePath: '/home/projetos/',
openCreateModal() {
PipelinesUI._editingId = null;
PipelinesUI._steps = [
@@ -152,6 +156,9 @@ const PipelinesUI = {
const descEl = document.getElementById('pipeline-description');
if (descEl) descEl.value = '';
const workdirEl = document.getElementById('pipeline-workdir');
if (workdirEl) workdirEl.value = PipelinesUI._basePath;
PipelinesUI.renderSteps();
Modal.open('pipeline-modal-overlay');
},
@@ -183,6 +190,9 @@ const PipelinesUI = {
const descEl = document.getElementById('pipeline-description');
if (descEl) descEl.value = pipeline.description || '';
const workdirEl = document.getElementById('pipeline-workdir');
if (workdirEl) workdirEl.value = pipeline.workingDirectory || PipelinesUI._basePath;
PipelinesUI.renderSteps();
Modal.open('pipeline-modal-overlay');
} catch (err) {
@@ -391,9 +401,16 @@ const PipelinesUI = {
return;
}
const workingDirectory = document.getElementById('pipeline-workdir')?.value.trim() || '';
if (workingDirectory && !workingDirectory.startsWith('/')) {
Toast.warning('O diretório do projeto deve ser um caminho absoluto (começar com /)');
return;
}
const data = {
name,
description: document.getElementById('pipeline-description')?.value.trim() || '',
workingDirectory,
steps: PipelinesUI._steps.map((s, index) => {
const isSimple = s.promptMode !== 'advanced';
const inputTemplate = isSimple
@@ -455,7 +472,7 @@ const PipelinesUI = {
if (inputEl) inputEl.value = '';
const workdirEl = document.getElementById('pipeline-execute-workdir');
if (workdirEl) workdirEl.value = '';
if (workdirEl) workdirEl.value = (pipeline && pipeline.workingDirectory) || PipelinesUI._basePath;
if (App._pipelineDropzone) App._pipelineDropzone.reset();