← back to Agent Cabinet
cabinet: click any node → inline expandable modal (description, tools, source, open/run)
c58af7f66cf8a5a726f257f1dafe4257827994e4 · 2026-07-23 07:31:28 -0700 · Steve Abrams
Files touched
Diff
commit c58af7f66cf8a5a726f257f1dafe4257827994e4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 23 07:31:28 2026 -0700
cabinet: click any node → inline expandable modal (description, tools, source, open/run)
---
server.js | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 110 insertions(+), 2 deletions(-)
diff --git a/server.js b/server.js
index a46d781..49efab2 100644
--- a/server.js
+++ b/server.js
@@ -326,13 +326,20 @@ function parseYaml(text) {
function buildMermaidFlowchart(cabinet) {
const lines = ['flowchart TD'];
+ const meta = {}; // nodeId -> { kind, name, label, domain?, triggers?, directors? } for the click-to-drill modal
// Root
lines.push(' Steve(["👔 STEVE<br/>President"]):::president');
+ lines.push(' click Steve nodeClick');
+ meta['Steve'] = { kind: 'root', name: 'president', label: '👔 Steve — President' };
// VPs
for (const v of cabinet.cabinet) {
const cls = v.vp.replace(/-/g, '_');
lines.push(` ${cls}["${v.vp}<br/><i>${(v.domain || '').slice(0, 60)}…</i>"]:::vp`);
lines.push(` Steve --> ${cls}`);
+ lines.push(` click ${cls} nodeClick`);
+ meta[cls] = { kind: 'subagent', name: v.vp, label: v.vp,
+ domain: v.domain || '', triggers: v.triggers || [],
+ directors: v.directors.map(d => ({ kind: d.kind, name: d.name })) };
for (const d of v.directors.slice(0, 12)) {
const safe = d.name.replace(/[^a-zA-Z0-9_]/g, '_');
const id = `${safe}__${cls}`;
@@ -340,6 +347,8 @@ function buildMermaidFlowchart(cabinet) {
const klass = d.kind === 'skill' ? 'skill' : 'subagent';
lines.push(` ${id}["${label}"]:::${klass}`);
lines.push(` ${cls} --> ${id}`);
+ lines.push(` click ${id} nodeClick`);
+ meta[id] = { kind: d.kind, name: d.name, label, under: v.vp };
}
}
// Class styles
@@ -347,11 +356,11 @@ function buildMermaidFlowchart(cabinet) {
lines.push(' classDef vp fill:#fef3c7,stroke:#000,stroke-width:3px,color:#000,font-weight:800;');
lines.push(' classDef skill fill:#dcfce7,stroke:#16a34a,color:#14532d;');
lines.push(' classDef subagent fill:#ede9fe,stroke:#7c3aed,color:#4c1d95;');
- return lines.join('\n');
+ return { mm: lines.join('\n'), meta };
}
function render(cabinet) {
- const mm = buildMermaidFlowchart(cabinet);
+ const { mm, meta } = buildMermaidFlowchart(cabinet);
const stats = {
vpCount: cabinet.cabinet.length,
skillCount: cabinet.cabinet.reduce((a, v) => a + v.directors.filter(d => d.kind === 'skill').length, 0),
@@ -377,6 +386,32 @@ function render(cabinet) {
table { border-collapse: collapse; margin-top: 12px; font-size: 12px; }
td, th { border: 1px solid #000; padding: 6px 10px; text-align: left; vertical-align: top; }
th { background: #f5f5f0; }
+ /* click-to-drill: nodes look clickable */
+ .mermaid .node { cursor: pointer; }
+ .mermaid .node:hover { filter: brightness(0.94); }
+ .hint { color:#888; font-size:12px; margin-top:8px; }
+ /* modal */
+ #nm-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.45); display: none; align-items: center; justify-content: center; z-index: 9999; }
+ #nm-overlay.open { display: flex; }
+ #nm-card { background:#fff; border:4px solid #000; box-shadow: 8px 8px 0 #000; width: min(720px, 92vw); max-height: 86vh; display:flex; flex-direction:column; }
+ #nm-card.max { width: 96vw; max-height: 94vh; }
+ #nm-head { display:flex; align-items:center; gap:10px; padding:14px 18px; border-bottom:3px solid #000; background:#fafaf6; }
+ #nm-head h2 { font-size:18px; margin:0; font-weight:900; flex:1; letter-spacing:-.01em; }
+ .nm-badge { font-size:11px; font-weight:800; padding:2px 8px; border:2px solid #000; border-radius:10px; text-transform:uppercase; }
+ .nm-badge.skill { background:#dcfce7; color:#14532d; border-color:#16a34a; }
+ .nm-badge.subagent { background:#ede9fe; color:#4c1d95; border-color:#7c3aed; }
+ .nm-badge.root { background:#000; color:#fff; }
+ #nm-body { padding:16px 18px; overflow:auto; }
+ #nm-body h3 { font-size:12px; text-transform:uppercase; letter-spacing:.04em; color:#666; margin:16px 0 6px; }
+ #nm-body h3:first-child { margin-top:0; }
+ .nm-inv { font-family: ui-monospace, monospace; background:#f5f5f0; border:2px solid #000; padding:6px 10px; display:inline-block; font-size:13px; }
+ .nm-desc { font-size:14px; line-height:1.5; }
+ .nm-tools span, .nm-trig span { display:inline-block; font-size:11px; background:#eef; border:1px solid #99c; border-radius:8px; padding:2px 8px; margin:2px 4px 2px 0; }
+ .nm-trig span { background:#fff7ed; border-color:#fdba74; }
+ .nm-btn { font: inherit; font-weight:700; font-size:12px; padding:6px 12px; border:2px solid #000; background:#fff; cursor:pointer; box-shadow:2px 2px 0 #000; }
+ .nm-btn:active { transform: translate(2px,2px); box-shadow:none; }
+ .nm-btn.icon { padding:6px 9px; }
+ #nm-src { white-space:pre-wrap; font-family:ui-monospace,monospace; font-size:11px; background:#f5f5f0; border:2px solid #000; padding:10px; max-height:40vh; overflow:auto; }
</style>
</head>
<body>
@@ -394,6 +429,7 @@ function render(cabinet) {
<div class="mermaid">
${mm}
</div>
+<div class="hint">💡 Click any node — VP, skill, or subagent — to open its details inline (description, tools, source). Use ⛶ to expand.</div>
<details>
<summary>Cabinet roster (table view)</summary>
@@ -421,6 +457,78 @@ ${mm}
<footer>Steve = President · Last rendered ${new Date().toLocaleString()}</footer>
<script>mermaid.initialize({startOnLoad:true, theme:'default', securityLevel:'loose', flowchart:{padding:18, useMaxWidth:false, htmlLabels:true, curve:'basis', nodeSpacing:30, rankSpacing:55}});</script>
+
+<!-- ── click-to-drill modal ── -->
+<div id="nm-overlay" onclick="if(event.target===this)nmClose()">
+ <div id="nm-card">
+ <div id="nm-head">
+ <span id="nm-badge" class="nm-badge"></span>
+ <h2 id="nm-title">…</h2>
+ <button class="nm-btn icon" title="Expand" onclick="document.getElementById('nm-card').classList.toggle('max')">⛶</button>
+ <button class="nm-btn icon" title="Close" onclick="nmClose()">✕</button>
+ </div>
+ <div id="nm-body"></div>
+ </div>
+</div>
+
+<script>
+const NODE_META = ${JSON.stringify(meta)};
+const esc = s => String(s==null?'':s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
+
+// pull description + tools out of an agent/skill markdown frontmatter block
+function parseFront(md){
+ const out = { desc:'', tools:'', body: md||'' };
+ const m = /^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/.exec(md||'');
+ if(!m) return out;
+ out.body = m[2] || '';
+ const fm = m[1];
+ const d = /(^|\\n)description:\\s*(.+?)(\\n[a-zA-Z_]+:|$)/s.exec(fm);
+ if(d) out.desc = d[2].replace(/^["'>|]\\s*/,'').replace(/\\n\\s+/g,' ').trim();
+ const t = /(^|\\n)tools:\\s*(.+)/.exec(fm);
+ if(t) out.tools = t[2].trim();
+ return out;
+}
+
+async function nodeClick(id){
+ const m = NODE_META[id]; if(!m) return;
+ const card = document.getElementById('nm-card'); card.classList.remove('max');
+ document.getElementById('nm-badge').className = 'nm-badge ' + m.kind;
+ document.getElementById('nm-badge').textContent = m.kind==='root'?'president':m.kind;
+ document.getElementById('nm-title').textContent = m.label || m.name;
+ const body = document.getElementById('nm-body');
+ const inv = m.kind==='skill' ? ('/'+m.name) : ('Use the '+m.name+' subagent — tell it what you need.');
+ body.innerHTML = '<h3>Invoke</h3><span class="nm-inv">'+esc(inv)+'</span>' +
+ (m.domain ? '<h3>Domain</h3><div class="nm-desc">'+esc(m.domain)+'</div>' : '') +
+ (m.triggers&&m.triggers.length ? '<h3>Triggers</h3><div class="nm-trig">'+m.triggers.map(t=>'<span>'+esc(t)+'</span>').join('')+'</div>' : '') +
+ (m.directors&&m.directors.length ? '<h3>Directors ('+m.directors.length+')</h3><div class="nm-trig">'+m.directors.map(d=>'<span>'+(d.kind==='skill'?'/':'@')+esc(d.name)+'</span>').join('')+'</div>' : '') +
+ '<h3>Description</h3><div class="nm-desc" id="nm-desc">loading…</div>' +
+ '<div id="nm-actions" style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap"></div>';
+ document.getElementById('nm-overlay').classList.add('open');
+ // fetch the on-disk source for description + tools + body
+ if(m.kind==='root'){ document.getElementById('nm-desc').textContent = 'The President. Everything rolls up to Steve; each VP owns a domain below.'; return; }
+ try{
+ const kind = m.kind==='skill'?'skill':'subagent';
+ const r = await fetch('/api/source?kind='+encodeURIComponent(kind)+'&name='+encodeURIComponent(m.name));
+ const data = await r.json();
+ const f = parseFront(data.content||'');
+ document.getElementById('nm-desc').innerHTML = f.desc ? esc(f.desc) : '<i>(no description in source' + (data.exists?'':' — no on-disk file found') + ')</i>';
+ let extra = '';
+ if(f.tools) extra += '<h3>Tools</h3><div class="nm-tools">'+f.tools.split(',').map(t=>'<span>'+esc(t.trim())+'</span>').join('')+'</div>';
+ if(data.exists){
+ extra += '<h3>Source</h3><details><summary style="cursor:pointer;font-size:12px">'+esc(data.path)+'</summary><div id="nm-src">'+esc((f.body||data.content).slice(0,20000))+'</div></details>';
+ }
+ if(extra){ const d=document.createElement('div'); d.innerHTML=extra; document.getElementById('nm-body').insertBefore(d, document.getElementById('nm-actions')); }
+ const acts = document.getElementById('nm-actions');
+ if(data.exists) acts.innerHTML =
+ '<button class="nm-btn" onclick="nmOpen(\\''+kind+'\\',\\''+esc(m.name)+'\\')">📂 Open source file</button>' +
+ '<button class="nm-btn" onclick="nmRun(\\''+kind+'\\',\\''+esc(m.name)+'\\')">▶ Run (new claude tab)</button>';
+ }catch(e){ document.getElementById('nm-desc').textContent = 'could not load source'; }
+}
+function nmClose(){ document.getElementById('nm-overlay').classList.remove('open'); }
+async function nmOpen(kind,name){ try{ await fetch('/api/open?kind='+encodeURIComponent(kind)+'&name='+encodeURIComponent(name)); }catch(e){} }
+async function nmRun(kind,name){ try{ await fetch('/api/run?kind='+encodeURIComponent(kind)+'&name='+encodeURIComponent(name)); }catch(e){} }
+document.addEventListener('keydown', e => { if(e.key==='Escape') nmClose(); });
+</script>
</body></html>`;
}
← 8058934 cabinet.aa vhost: IP-bind :80 too (agentabrams default_serve
·
back to Agent Cabinet
·
(newest)