[object Object]

← 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 &middot; 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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)