Integrar repositórios Git na execução de agentes e pipelines

- Módulo git-integration: clone/pull, commit/push automático, listagem de repos
- Seletor de repositório nos modais de execução (agente e pipeline)
- Seletor de branch carregado dinamicamente ao escolher repo
- Campo de diretório escondido quando repositório selecionado
- Auto-commit e push ao final da execução com mensagem descritiva
- Instrução injetada para agentes não fazerem operações git
- Rotas API: GET /repos, GET /repos/:name/branches
- Pipeline: commit automático ao final de todos os steps
This commit is contained in:
Frederico Castro
2026-02-28 04:24:47 -03:00
parent 2fae816162
commit 633b19f80d
10 changed files with 307 additions and 11 deletions

View File

@@ -38,9 +38,10 @@ const API = {
create(data) { return API.request('POST', '/agents', data); },
update(id, data) { return API.request('PUT', `/agents/${id}`, data); },
delete(id) { return API.request('DELETE', `/agents/${id}`); },
execute(id, task, instructions, contextFiles, workingDirectory) {
execute(id, task, instructions, contextFiles, workingDirectory, repoName, repoBranch) {
const body = { task, instructions };
if (workingDirectory) body.workingDirectory = workingDirectory;
if (repoName) { body.repoName = repoName; if (repoBranch) body.repoBranch = repoBranch; }
else if (workingDirectory) body.workingDirectory = workingDirectory;
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
return API.request('POST', `/agents/${id}/execute`, body);
},
@@ -83,9 +84,10 @@ const API = {
create(data) { return API.request('POST', '/pipelines', data); },
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
execute(id, input, workingDirectory, contextFiles) {
execute(id, input, workingDirectory, contextFiles, repoName, repoBranch) {
const body = { input };
if (workingDirectory) body.workingDirectory = workingDirectory;
if (repoName) { body.repoName = repoName; if (repoBranch) body.repoBranch = repoBranch; }
else if (workingDirectory) body.workingDirectory = workingDirectory;
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
return API.request('POST', `/pipelines/${id}/execute`, body);
},
@@ -142,6 +144,11 @@ const API = {
},
},
repos: {
list() { return API.request('GET', '/repos'); },
branches(name) { return API.request('GET', `/repos/${encodeURIComponent(name)}/branches`); },
},
files: {
list(path) { return API.request('GET', `/files${path ? '?path=' + encodeURIComponent(path) : ''}`); },
delete(path) { return API.request('DELETE', `/files?path=${encodeURIComponent(path)}`); },

View File

@@ -37,6 +37,7 @@ const App = {
App._executeDropzone = Utils.initDropzone('execute-dropzone', 'execute-files', 'execute-file-list');
App._pipelineDropzone = Utils.initDropzone('pipeline-execute-dropzone', 'pipeline-execute-files', 'pipeline-execute-file-list');
App._initRepoSelectors();
const initialSection = location.hash.replace('#', '') || 'dashboard';
App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard');
@@ -860,6 +861,61 @@ const App = {
});
},
_reposCache: null,
async _loadRepos(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
try {
if (!App._reposCache) App._reposCache = await API.repos.list();
const current = select.value;
select.innerHTML = '<option value="">Nenhum (usar diretório manual)</option>';
App._reposCache.forEach(r => {
select.insertAdjacentHTML('beforeend',
`<option value="${Utils.escapeHtml(r.name)}">${Utils.escapeHtml(r.name)}${r.description ? ' — ' + Utils.escapeHtml(r.description.slice(0, 40)) : ''}</option>`
);
});
if (current) select.value = current;
} catch { }
},
_initRepoSelectors() {
const pairs = [
['execute-repo', 'execute-repo-branch', 'execute-workdir-group'],
['pipeline-execute-repo', 'pipeline-execute-repo-branch', 'pipeline-execute-workdir-group'],
];
pairs.forEach(([repoId, branchId, workdirGroupId]) => {
const repoSelect = document.getElementById(repoId);
const branchSelect = document.getElementById(branchId);
const workdirGroup = document.getElementById(workdirGroupId);
if (!repoSelect) return;
repoSelect.addEventListener('change', async () => {
const repoName = repoSelect.value;
if (repoName) {
if (workdirGroup) workdirGroup.style.display = 'none';
if (branchSelect) {
branchSelect.style.display = '';
branchSelect.innerHTML = '<option value="">Branch padrão</option>';
try {
const branches = await API.repos.branches(repoName);
branches.forEach(b => {
branchSelect.insertAdjacentHTML('beforeend', `<option value="${Utils.escapeHtml(b)}">${Utils.escapeHtml(b)}</option>`);
});
} catch { }
}
} else {
if (workdirGroup) workdirGroup.style.display = '';
if (branchSelect) branchSelect.style.display = 'none';
}
});
repoSelect.addEventListener('focus', () => {
if (repoSelect.options.length <= 1) App._loadRepos(repoId);
});
});
},
async _handleExecute() {
const agentId = document.getElementById('execute-agent-select')?.value
|| document.getElementById('execute-agent-id')?.value;
@@ -877,6 +933,8 @@ const App = {
const instructions = document.getElementById('execute-instructions')?.value.trim() || '';
const workingDirectory = document.getElementById('execute-workdir')?.value.trim() || '';
const repoName = document.getElementById('execute-repo')?.value || '';
const repoBranch = document.getElementById('execute-repo-branch')?.value || '';
try {
const selectEl = document.getElementById('execute-agent-select');
@@ -893,7 +951,7 @@ const App = {
Terminal.disableChat();
App._lastAgentName = agentName;
await API.agents.execute(agentId, task, instructions, contextFiles, workingDirectory);
await API.agents.execute(agentId, task, instructions, contextFiles, workingDirectory, repoName, repoBranch);
if (dropzone) dropzone.reset();
Modal.close('execute-modal-overlay');

View File

@@ -374,6 +374,11 @@ const AgentsUI = {
AgentsUI._loadSavedTasks();
const repoSelect = document.getElementById('execute-repo');
if (repoSelect) { repoSelect.value = ''; repoSelect.dispatchEvent(new Event('change')); }
App._reposCache = null;
App._loadRepos('execute-repo');
Modal.open('execute-modal-overlay');
} catch (err) {
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);

View File

@@ -476,6 +476,11 @@ const PipelinesUI = {
if (App._pipelineDropzone) App._pipelineDropzone.reset();
const repoSelect = document.getElementById('pipeline-execute-repo');
if (repoSelect) { repoSelect.value = ''; repoSelect.dispatchEvent(new Event('change')); }
App._reposCache = null;
App._loadRepos('pipeline-execute-repo');
Modal.open('pipeline-execute-modal-overlay');
},
@@ -503,7 +508,9 @@ const PipelinesUI = {
contextFiles = uploadResult.files;
}
await API.pipelines.execute(pipelineId, input, workingDirectory, contextFiles);
const repoName = document.getElementById('pipeline-execute-repo')?.value || '';
const repoBranch = document.getElementById('pipeline-execute-repo-branch')?.value || '';
await API.pipelines.execute(pipelineId, input, workingDirectory, contextFiles, repoName, repoBranch);
if (dropzone) dropzone.reset();
Modal.close('pipeline-execute-modal-overlay');
App.navigateTo('terminal');