feat(team): flow-demo playback + org-node role label (CEO/PO)
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
0d79e8fbd6
commit
2140a293ed
@ -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). */
|
||||
|
||||
@ -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) : '') || `<span class="mono">${monogram(n.label)}</span>`;
|
||||
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 ? `<span class="node-model" style="color:${modelColor}">${escapeHtml(model)}</span>` : '']
|
||||
@ -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 {
|
||||
<body>
|
||||
${pageHeader(config.projectName, 'team')}
|
||||
<div class="team-toolbar">
|
||||
<span class="flow-caption" id="flowCaption" hidden></span>
|
||||
<span class="tb-spacer"></span>
|
||||
<button class="fit-btn flow" id="flowBtn" type="button" title="Play the delegation-flow animation">▶ Flow</button>
|
||||
<button class="fit-btn" id="fitBtn" type="button" title="Scale the whole chart to fit the screen">Fit</button>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user