Pipeline-vaiheiden visuaalinen seuranta agenttinäkymässä

Terminaalin yläpuolelle ilmestyy pipeline-progress-palkki:
  ✓ Suunnittelu → ✓ models.py → ◷ main.py → ◯ Review

Jokainen vaihe on hover-tooltip joka näyttää:
- Vaiheen nimi ja agentti (värikoodattu)
- Input: mitä agentti sai syötteeksi
- Output: mitä agentti tuotti (esikatselu 150 merkkiä)

Myös agenttien avatar-korttien tooltip päivittyy reaaliaikaisesti
näyttämään viimeisimmän vaiheen input/output.

Palkki tyhjenee automaattisesti uuden pipelinen alkaessa.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-05 20:32:59 +03:00
parent a6e49870d6
commit 2e7b86deeb

View File

@@ -1103,6 +1103,7 @@
<button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button> <button id="agent-compute-btn" style="margin-left:4px;padding:2px 10px;border-radius:4px;border:1px solid #30363d;background:#161b22;color:#58a6ff;font-size:12px;font-family:inherit;cursor:pointer" title="Käynnistä kielimalli omalla koneellasi laskentaa varten">Alusta laskentasolmu</button>
</span> </span>
</div> </div>
<div id="pipeline-steps" style="display:none;background:#0d1117;border:1px solid var(--border-color);border-top:none;padding:8px 14px;font-family:'Courier New',monospace;font-size:12px;overflow-x:auto;white-space:nowrap"></div>
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0"> <div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
</div> </div>
<div style="position:relative;display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px"> <div style="position:relative;display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
@@ -1819,12 +1820,64 @@
} }
} }
// Pipeline-vaiheiden seuranta ja visualisointi
const pipelineSteps = [];
function pipelineStep(agent, label, status, input, output) {
const step = { agent, label, status, input: input || '', output: output || '' };
// Päivitetään olemassaoleva tai lisätään uusi
const existing = pipelineSteps.find(s => s.label === label && s.status !== 'done');
if (existing && status !== 'done') {
Object.assign(existing, step);
} else if (status === 'done' && existing) {
existing.status = 'done';
existing.output = output || existing.output;
} else {
pipelineSteps.push(step);
}
renderPipelineSteps();
// Päivitetään agentin avatar tooltip
const avatarMap = { manager: 'avatar-kpn', coder: 'avatar-coder', tester: 'avatar-tester', qa: 'avatar-qa', data: 'avatar-data' };
const avatarId = avatarMap[agent];
if (avatarId) {
const el = document.getElementById(avatarId);
if (el) {
const truncOut = (output || '').substring(0, 200).replace(/\n/g, ' ');
el.title = `${label}\n${status === 'active' ? '⏳ Käsittelee...' : '✓ Valmis'}\n\nInput: ${(input || '').substring(0, 100)}...\nOutput: ${truncOut}...`;
}
}
}
function renderPipelineSteps() {
const container = document.getElementById('pipeline-steps');
if (!container) return;
if (pipelineSteps.length === 0) { container.style.display = 'none'; return; }
container.style.display = 'block';
container.innerHTML = pipelineSteps.map((s, i) => {
const colors = { manager: '#d29922', coder: '#3fb950', tester: '#58a6ff', qa: '#a371f7', data: '#d2a8ff' };
const color = colors[s.agent] || '#8b949e';
const icon = s.status === 'done' ? '✓' : s.status === 'active' ? '◷' : '◯';
const iconColor = s.status === 'done' ? '#3fb950' : s.status === 'active' ? '#d29922' : '#8b949e';
const arrow = i < pipelineSteps.length - 1 ? ' <span style="color:#30363d">→</span> ' : '';
// Tooltip: input/output esikatselu
const tip = esc(`${s.label}\nInput: ${(s.input || '').substring(0, 150)}\nOutput: ${(s.output || '').substring(0, 150)}`).replace(/\n/g, '&#10;');
return `<span title="${tip}" style="cursor:help"><span style="color:${iconColor}">${icon}</span> <span style="color:${color}">${esc(s.label)}</span></span>${arrow}`;
}).join('');
}
function pipelineClear() {
pipelineSteps.length = 0;
const container = document.getElementById('pipeline-steps');
if (container) container.style.display = 'none';
}
// Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi // Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi
async function kpnPipeline(task) { async function kpnPipeline(task) {
pipelineClear();
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`); termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
// Vaihe 1: Manageri pilkkoo projektin tiedostoiksi // Vaihe 1: Manageri pilkkoo projektin tiedostoiksi
termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`); termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`);
pipelineStep('manager', 'Suunnittelu', 'active', task);
const managerPrompt = `You are a project manager. Break this task into individual source files. const managerPrompt = `You are a project manager. Break this task into individual source files.
For each file, write one line: FILENAME: description For each file, write one line: FILENAME: description
List only the essential files (max 5). No explanations. List only the essential files (max 5). No explanations.
@@ -1832,6 +1885,7 @@ List only the essential files (max 5). No explanations.
Task: ${task}`; Task: ${task}`;
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt); const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; } if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
pipelineStep('manager', 'Suunnittelu', 'done', task, plan);
// Parsitaan tiedostolista: "FILENAME: description" -rivit // Parsitaan tiedostolista: "FILENAME: description" -rivit
const fileList = plan.split('\n') const fileList = plan.split('\n')
@@ -1861,6 +1915,7 @@ Task: ${task}`;
for (let i = 0; i < fileList.length; i++) { for (let i = 0; i < fileList.length; i++) {
const file = fileList[i]; const file = fileList[i];
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`); termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`);
pipelineStep('coder', file.name, 'active', file.desc);
// Rakennetaan konteksti: aiemmin generoidut tiedostot // Rakennetaan konteksti: aiemmin generoidut tiedostot
let context = ''; let context = '';
@@ -1879,6 +1934,7 @@ Project: ${task}`;
return; return;
} }
generatedFiles[file.name] = code; generatedFiles[file.name] = code;
pipelineStep('coder', file.name, 'done', file.desc, code);
} }
// Vaihe 3: Testaaja arvioi koko projektin // Vaihe 3: Testaaja arvioi koko projektin
@@ -1887,15 +1943,18 @@ Project: ${task}`;
.join('\n\n'); .join('\n\n');
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — arviointi`); termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — arviointi`);
pipelineStep('tester', 'Review', 'active', `${Object.keys(generatedFiles).length} tiedostoa`);
const reviewPrompt = `Review this project. List bugs or issues. Be brief. const reviewPrompt = `Review this project. List bugs or issues. Be brief.
If the code is correct, say "LGTM". If the code is correct, say "LGTM".
${allCode}`; ${allCode}`;
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt); const review = await kpnRun(agentPrompts.tester.model, reviewPrompt);
pipelineStep('tester', 'Review', 'done', `${Object.keys(generatedFiles).length} tiedostoa`, review);
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia // Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
if (review && !review.toLowerCase().includes('lgtm') && !review.toLowerCase().includes('looks good')) { if (review && !review.toLowerCase().includes('lgtm') && !review.toLowerCase().includes('looks good')) {
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length + 3}] Koodari</span> — korjaukset`); termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length + 3}] Koodari</span> — korjaukset`);
pipelineStep('coder', 'Korjaukset', 'active', review);
const fixPrompt = `Fix the issues found in the review. const fixPrompt = `Fix the issues found in the review.
Review feedback: ${review} Review feedback: ${review}
@@ -1904,9 +1963,12 @@ ${allCode}
Write the corrected code.`; Write the corrected code.`;
const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt); const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt);
pipelineStep('coder', 'Korjaukset', 'done', review, fixedCode);
if (fixedCode) { if (fixedCode) {
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`); termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`);
await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`); pipelineStep('tester', 'Re-review', 'active', fixedCode);
const reReview = await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`);
pipelineStep('tester', 'Re-review', 'done', fixedCode, reReview);
} }
} }