From 2140a293edb7a29c573063c8920f1f357e448715 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Wed, 8 Jul 2026 05:28:32 +0200 Subject: [PATCH] feat(team): flow-demo playback + org-node role label (CEO/PO) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrgNode gains an optional 'role' field so org-only nodes (CEO, Product Owners) show a role under their name; agents still fall back to their roster role. Fixes the missing CEO label on the root. - New 'Flow' button plays a choreographed delegation lifecycle as directional pulses with step captions: claude defines → PO Native → agent → back to PO → review at architect → CI/CD → ahmed test → back to architect (approve/push), plus architect→strategist for some tasks. pulseBetween() walks any two nodes via their lowest common ancestor, colouring each edge by its real direction (down=green delegation, up=amber return), so the animation always shows the direction. Co-Authored-By: Claude Opus 4.8 --- src/core/schema.ts | 3 ++ src/server/team.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/core/schema.ts b/src/core/schema.ts index 88b592b..f55a68b 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -138,6 +138,9 @@ export const OrgNodeSchema = z.object({ id: z.string().min(1), /** Editable display name. */ label: z.string().min(1), + /** Display role/title shown under the name (e.g. "CEO", "Product Owner"). + * For linked agents it falls back to the agent's roster role. */ + role: z.string().optional(), /** One-line "what this node does" — shown behind the info button. */ title: z.string().optional(), /** Linked roster agent name (for model/provider/logo + live task state). */ diff --git a/src/server/team.ts b/src/server/team.ts index 902ae99..73757aa 100644 --- a/src/server/team.ts +++ b/src/server/team.ts @@ -92,7 +92,7 @@ export function renderTeamHtml(cwd: string): string { const kind = n.kind ?? linked?.kind; const meta = providerMeta(kind); const icon = (kind ? providerLogo(kind, 18) : '') || `${monogram(n.label)}`; - const role = linked?.role; + const role = n.role ?? linked?.role; const model = linked?.model; const modelColor = meta ? meta.color : 'var(--muted)'; const sub = [role ? escapeHtml(role) : '', model ? `${escapeHtml(model)}` : ''] @@ -161,9 +161,15 @@ export function renderTeamHtml(cwd: string): string { .children.wrap { display: grid; grid-template-columns: repeat(var(--cols, 3), 168px); justify-content: center; column-gap: 11px; row-gap: 46px; } .children.wrap .subtree { width: 168px; } - .team-toolbar { display: flex; justify-content: flex-end; gap: 8px; padding: 0 6px 8px; } - .fit-btn { min-height: 30px; padding: 0 13px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); color: var(--muted); font: 600 12px/1 var(--font-sans); cursor: pointer; transition: color 150ms, border-color 150ms; } + .team-toolbar { display: flex; align-items: center; gap: 8px; padding: 0 6px 8px; } + .tb-spacer { flex: 1; } + .fit-btn { min-height: 30px; padding: 0 13px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface); color: var(--muted); font: 600 12px/1 var(--font-sans); cursor: pointer; transition: color 150ms, border-color 150ms; white-space: nowrap; } .fit-btn:hover { color: var(--text); border-color: var(--accent); } + .fit-btn.flow { color: #adf2c7; border-color: rgba(34,197,94,.4); } + .fit-btn.flow:hover { border-color: var(--green); } + .fit-btn.flow[disabled] { opacity: .6; cursor: default; } + .flow-caption { font: 12px/1.3 var(--font-mono); color: var(--text); background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--green); border-radius: 8px; padding: 6px 11px; max-width: 62%; } + .flow-caption.review { border-left-color: var(--status-review); } .node { position: relative; @@ -223,6 +229,9 @@ export function renderTeamHtml(cwd: string): string { ${pageHeader(config.projectName, 'team')}
+ + +
@@ -417,6 +426,62 @@ export function renderTeamHtml(cwd: string): string { var fb = document.getElementById('fitBtn'); if (fb) fb.addEventListener('click', toggleFit); + // ── Flow demo: choreographed pulses that walk the delegation lifecycle ── + function sleep(ms) { return new Promise(function(r){ setTimeout(r, ms); }); } + function ancestors(id) { var a = [id], c = parentOf[id]; while (c) { a.push(c); c = parentOf[c]; } return a; } + // Pulse from one node to another along the tree (up to the common ancestor, + // then down). Each edge is coloured by its real direction. Returns duration. + function pulseBetween(fromId, toId) { + if (!edgeMap || (!edgeMap[fromId] && !edgeMap[toId] && fromId !== toId)) { /* still fire best-effort */ } + var A = ancestors(fromId), B = ancestors(toId); + var Bidx = {}; B.forEach(function(id, i) { Bidx[id] = i; }); + var lca = null, ai = 0; + for (var i = 0; i < A.length; i++) { if (Bidx[A[i]] != null) { lca = A[i]; ai = i; break; } } + if (lca == null) return EDGE_DUR; + var seq = []; + for (var u = 0; u < ai; u++) seq.push({ nodeId: A[u], dir: 'up' }); // from → LCA + for (var d = Bidx[lca] - 1; d >= 0; d--) seq.push({ nodeId: B[d], dir: 'down' }); // LCA → to + seq.forEach(function(step, k) { setTimeout(function(){ firePulseEdge(step.nodeId, step.dir); }, k * SEG); }); + return Math.max(EDGE_DUR, seq.length * SEG + EDGE_DUR); + } + + var DEMO = [ + { from: 'claude', to: 'po-native', cap: 'claude definiert den Task und delegiert an PO Native' }, + { from: 'po-native', to: 'backyard', cap: 'PO Native delegiert an den richtigen Agenten (backyard)' }, + { from: 'backyard', to: 'po-native', cap: 'backyard liefert die Arbeit zurück an PO Native', rev: true }, + { from: 'po-native', to: 'claude', cap: 'PO Native validiert → zurück zum Architekt-Review', rev: true }, + { from: 'claude', to: 'cicd', cap: 'Architekt: Review OK → an CI/CD (sonst: reopen an den Agenten)' }, + { from: 'cicd', to: 'ahmed', cap: 'CI/CD gibt an ahmed zum Testen' }, + { from: 'ahmed', to: 'cicd', cap: 'ahmed meldet das Testergebnis zurück an CI/CD', rev: true }, + { from: 'cicd', to: 'claude', cap: 'CI/CD zurück an Architekt → Approve & Push', rev: true }, + { from: 'claude', to: 'strategist', cap: 'Manche Tasks: Architekt zieht den Strategist hinzu' } + ]; + function showCaption(text, review) { + var el = document.getElementById('flowCaption'); + if (!el) return; + if (!text) { el.hidden = true; el.textContent = ''; return; } + el.hidden = false; el.className = 'flow-caption' + (review ? ' review' : ''); el.textContent = text; + } + var demoRunning = false; + async function runDemo() { + if (demoRunning) return; + demoRunning = true; + var btn = document.getElementById('flowBtn'); + if (btn) { btn.setAttribute('disabled', 'true'); } + layoutEdges(); // make sure edge geometry is current + for (var i = 0; i < DEMO.length; i++) { + var s = DEMO[i]; + showCaption((i + 1) + '/' + DEMO.length + ' · ' + s.cap, s.rev); + var dur = pulseBetween(s.from, s.to); + await sleep(dur + 550); + } + showCaption(''); + if (btn) btn.removeAttribute('disabled'); + demoRunning = false; + } + var flowBtn = document.getElementById('flowBtn'); + if (flowBtn) flowBtn.addEventListener('click', runDemo); + var rt; function relayout() { clearTimeout(rt); rt = setTimeout(function(){ if (fitted) { fitted = false; toggleFit(); } else layoutEdges(); }, 120); } window.addEventListener('resize', relayout); layoutEdges(); centerScroll();