monomind 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +18 -17
  2. package/package.json +1 -1
  3. package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +8 -9
  4. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +32 -15
  5. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +238 -23
  6. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +74 -1
  7. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +14 -12
  8. package/packages/@monomind/cli/.claude/helpers/router.cjs +12 -2
  9. package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +91 -701
  10. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +46 -47
  11. package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +12 -1
  12. package/packages/@monomind/cli/README.md +18 -17
  13. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +18 -0
  14. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +76 -1
  15. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +7 -3
  16. package/packages/@monomind/cli/dist/src/orgrt/server.js +24 -0
  17. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +1 -0
  18. package/packages/@monomind/cli/dist/src/orgrt/session.js +8 -0
  19. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +1 -1
  20. package/packages/@monomind/cli/dist/src/pricing/model-pricing.d.ts +1 -1
  21. package/packages/@monomind/cli/dist/src/ui/collector.mjs +20 -5
  22. package/packages/@monomind/cli/dist/src/ui/dashboard.html +270 -499
  23. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +1 -1
  24. package/packages/@monomind/cli/dist/src/ui/server.mjs +132 -158
  25. package/packages/@monomind/cli/package.json +3 -3
  26. package/packages/@monomind/cli/scripts/publish.sh +6 -0
  27. package/scripts/generate-agent-avatars.mjs +3 -3
@@ -440,6 +440,11 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
440
440
  overflow: hidden;
441
441
  text-overflow: ellipsis;
442
442
  }
443
+ .v2-bubble-inner.v2-bubble-question {
444
+ background: oklch(28% 0.04 75 / 0.95);
445
+ border-color: oklch(78% 0.16 75 / 0.7);
446
+ color: oklch(92% 0.03 75);
447
+ }
443
448
  .org-chart-legend { display: flex; gap: 14px; flex-wrap: wrap; padding: 0 2px; }
444
449
  .ocl-item { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--text-lo); }
445
450
 
@@ -1426,67 +1431,6 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1426
1431
  select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1427
1432
  @keyframes pb-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
1428
1433
 
1429
- /* ── systems grid ──────────────────────────────────────────── */
1430
- #wf-systems-grid { display: block; }
1431
- /* ── monoagent connections tile grid ─────────────────────────── */
1432
- .ma-cat-hdr { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
1433
- .ma-cat-label { font-family:var(--mono); font-size:10px; font-weight:700; color:var(--text-lo); text-transform:uppercase; letter-spacing:2px; white-space:nowrap; }
1434
- .ma-cat-line { flex:1; height:1px; background:var(--border); }
1435
- .ma-cat-count { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); }
1436
- .ma-tiles { display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:8px; margin-bottom:24px; }
1437
- .ma-tile { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:16px 8px 12px; cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:6px; transition:border-color 0.15s,box-shadow 0.15s; user-select:none; }
1438
- .ma-tile:hover { border-color:var(--border-hi); }
1439
- .ma-tile.connected { border-color:oklch(65% 0.15 150 / 0.5); box-shadow:0 0 10px oklch(65% 0.15 150 / 0.12); }
1440
- .ma-tile-icon { font-size:22px; line-height:1; }
1441
- .ma-tile-name { font-family:var(--mono); font-size:10px; font-weight:600; color:var(--text-mid); text-align:center; line-height:1.3; }
1442
- .ma-tile-dot { width:6px; height:6px; border-radius:50%; background:var(--border); }
1443
- .ma-tile.connected .ma-tile-dot { background:var(--green); box-shadow:0 0 5px oklch(65% 0.15 150 / 0.7); }
1444
- .ma-tile.expired .ma-tile-dot { background:#fbbf24; box-shadow:0 0 5px rgba(251,191,36,0.5); }
1445
- .ma-tile-acct { font-family:var(--mono); font-size:9px; color:var(--text-xs); max-width:84px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
1446
- /* modal */
1447
- .ma-modal-bg { position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:1200; display:flex; align-items:center; justify-content:center; padding:24px; }
1448
- .ma-modal { width:100%; max-width:440px; background:var(--surface); border:1px solid var(--border-hi); border-radius:14px; box-shadow:0 8px 40px rgba(0,0,0,0.5); overflow:hidden; }
1449
- .ma-modal-hdr { display:flex; align-items:center; justify-content:space-between; padding:16px 20px 12px; border-bottom:1px solid var(--border); }
1450
- .ma-modal-hdr-left { display:flex; align-items:center; gap:10px; }
1451
- .ma-modal-title { font-family:var(--mono); font-size:13px; font-weight:700; color:var(--text-hi); }
1452
- .ma-modal-status { font-family:var(--mono); font-size:10px; text-transform:uppercase; letter-spacing:1px; }
1453
- .ma-modal-body { padding:16px 20px 20px; display:flex; flex-direction:column; gap:14px; }
1454
- .ma-modal-info { background:var(--surface-hi); border:1px solid var(--border); border-radius:8px; padding:10px 14px; display:flex; flex-direction:column; gap:8px; }
1455
- .ma-modal-row { display:flex; justify-content:space-between; align-items:center; }
1456
- .ma-modal-lbl { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); text-transform:uppercase; letter-spacing:1px; }
1457
- .ma-modal-val { font-family:var(--mono); font-size:11px; color:var(--text-mid); }
1458
- .ma-modal-val.ok { color:var(--green); }
1459
- .ma-modal-val.warn { color:#fbbf24; }
1460
- .ma-field-label { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); text-transform:uppercase; letter-spacing:1.5px; margin-bottom:4px; }
1461
- .ma-field-input { width:100%; padding:7px 10px; border:1px solid var(--border-hi); border-radius:6px; background:var(--surface-hi); color:var(--text-hi); font-family:var(--mono); font-size:11px; box-sizing:border-box; outline:none; }
1462
- .ma-field-input:focus { border-color:var(--accent); }
1463
- .ma-steps { background:var(--surface-hi); border:1px solid var(--border); border-radius:8px; padding:8px 10px; max-height:130px; overflow-y:auto; display:flex; flex-direction:column; gap:5px; }
1464
- .ma-step { display:flex; align-items:center; gap:8px; }
1465
- .ma-step-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; }
1466
- .ma-step-dot.ok { background:var(--green); }
1467
- .ma-step-dot.err { background:oklch(60% 0.18 25); }
1468
- .ma-step-txt { font-family:var(--mono); font-size:10px; color:var(--text-lo); }
1469
- .ma-step-txt.ok { color:var(--green); }
1470
- .ma-step-txt.err { color:oklch(60% 0.18 25); }
1471
- .ma-btns { display:flex; gap:8px; flex-wrap:wrap; }
1472
- .ma-btn { padding:6px 14px; border-radius:6px; border:none; font-family:var(--mono); font-size:11px; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:5px; transition:opacity 0.15s; }
1473
- .ma-btn:disabled { opacity:0.45; cursor:default; }
1474
- .ma-btn-primary { background:var(--accent); color:#fff; }
1475
- .ma-btn-primary:hover:not(:disabled) { opacity:0.85; }
1476
- .ma-btn-secondary { background:var(--surface-hi); color:var(--text-mid); border:1px solid var(--border-hi); }
1477
- .ma-btn-secondary:hover:not(:disabled) { border-color:var(--accent); color:var(--accent); }
1478
- .ma-btn-danger { background:oklch(17% 0.015 25); color:oklch(60% 0.18 25); border:1px solid oklch(60% 0.18 25 / 0.3); }
1479
- .ma-btn-danger:hover:not(:disabled) { opacity:0.85; }
1480
- .ma-btn-ghost { background:none; color:var(--text-lo); border:1px solid var(--border); }
1481
- .ma-btn-ghost:hover:not(:disabled) { color:var(--text-mid); }
1482
- .ma-msg { font-family:var(--mono); font-size:11px; padding:7px 12px; border-radius:6px; }
1483
- .ma-msg.ok { background:oklch(65% 0.15 150 / 0.08); border:1px solid oklch(65% 0.15 150 / 0.25); color:var(--green); }
1484
- .ma-msg.err { background:oklch(60% 0.18 25 / 0.08); border:1px solid oklch(60% 0.18 25 / 0.25); color:oklch(60% 0.18 25); }
1485
- .ma-method-tabs { display:flex; gap:6px; flex-wrap:wrap; }
1486
- .ma-method-tab { font-family:var(--mono); font-size:10px; padding:3px 10px; border-radius:4px; background:var(--surface-hi); border:1px solid var(--border); color:var(--text-lo); cursor:pointer; }
1487
- .ma-method-tab.active { background:var(--accent-dim); border-color:var(--accent); color:var(--accent); }
1488
- .ma-browser-hint { font-size:11px; color:var(--text-lo); line-height:1.5; }
1489
-
1490
1434
  /* ── playbook runs view ────────────────────────────────────── */
1491
1435
  .wf-run-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 10px; }
1492
1436
  .wf-run-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
@@ -1590,10 +1534,6 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1590
1534
  </div>
1591
1535
  <div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
1592
1536
  <div class="nav-sect" style="margin-top:auto;padding-top:8px;">
1593
- <div class="nav-item" data-view="monoagent">
1594
- <span class="ico">🤖</span><span class="lbl">MonoAgent</span>
1595
- <span class="bdg" id="bdg-monoagent">—</span>
1596
- </div>
1597
1537
  <div class="nav-item" data-view="global">
1598
1538
  <span class="ico">⊕</span><span class="lbl">Global Feed</span>
1599
1539
  </div>
@@ -1607,6 +1547,10 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1607
1547
  <div class="nav-item" data-view="chat" title="All agent sessions across projects">
1608
1548
  <span class="ico">⌘</span><span class="lbl">Global Agent Chat</span>
1609
1549
  </div>
1550
+ <div class="nav-item" data-view="humaninput" title="Pending questions from agents across all orgs">
1551
+ <span class="ico">❓</span><span class="lbl">Human Input</span>
1552
+ <span class="bdg" id="bdg-humaninput">—</span>
1553
+ </div>
1610
1554
  </div>
1611
1555
  </div>
1612
1556
  <div id="sb-footer">
@@ -2262,30 +2206,6 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2262
2206
  </div>
2263
2207
  </div>
2264
2208
 
2265
- <!-- MONOAGENT -->
2266
- <div class="view" id="view-monoagent">
2267
- <div class="vscroll">
2268
- <div class="pg-title">MonoAgent</div>
2269
- <div class="pg-sub">Social media automation workflows &amp; run history</div>
2270
- <div class="mg-tab-bar" style="margin-top:14px">
2271
- <button class="odt-btn active" data-matab="workflows" onclick="maSwitchTab('workflows')">Workflows</button>
2272
- <button class="odt-btn" data-matab="history" onclick="maSwitchTab('history')">Action History</button>
2273
- </div>
2274
- <div class="wf-pane active" id="ma-tab-workflows">
2275
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;margin-top:4px">
2276
- <button class="btn" onclick="loadMonoagentData()">↺ Refresh</button>
2277
- </div>
2278
- <div id="ma-workflows-list"><div class="loading-txt">Loading…</div></div>
2279
- </div>
2280
- <div class="wf-pane" id="ma-tab-history">
2281
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;margin-top:4px">
2282
- <button class="btn" onclick="loadMonoagentData()">↺ Refresh</button>
2283
- </div>
2284
- <div id="ma-history-list"><div class="loading-txt">Loading…</div></div>
2285
- </div>
2286
- </div>
2287
- </div>
2288
-
2289
2209
  <!-- GLOBAL FEED -->
2290
2210
  <div class="view" id="view-global">
2291
2211
  <div class="vscroll">
@@ -2339,6 +2259,17 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2339
2259
  </div>
2340
2260
  </div>
2341
2261
 
2262
+ <!-- HUMAN INPUT (global, cross-org) -->
2263
+ <div class="view" id="view-humaninput">
2264
+ <div class="vscroll">
2265
+ <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
2266
+ <div class="pg-title" style="margin-bottom:0">Human Input</div>
2267
+ <span class="pg-sub" id="hi-sub" style="margin-bottom:0">Pending questions across all projects</span>
2268
+ </div>
2269
+ <div id="hi-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
2270
+ </div>
2271
+ </div>
2272
+
2342
2273
  </div><!-- /view-wrap -->
2343
2274
  </div><!-- /main -->
2344
2275
  <div id="app-ambient-hint">Press A to exit ambient mode</div>
@@ -2439,6 +2370,27 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2439
2370
  </div>
2440
2371
 
2441
2372
  <script>
2373
+ // ── auth: attach this session's dashboard credential to every non-GET fetch ──
2374
+ // server.mjs requires an x-monomind-token header on all non-GET routes and
2375
+ // injects the current value via a <meta name="mm-token"> tag on page load.
2376
+ // Wrapping fetch() once here (rather than editing every individual POST/
2377
+ // DELETE call site across this file) means every existing and future write
2378
+ // action is covered automatically.
2379
+ (function () {
2380
+ const _mmAuthTag = document.querySelector('meta[name="mm-token"]');
2381
+ const _mmAuthValue = _mmAuthTag ? _mmAuthTag.content : '';
2382
+ const _origFetch = window.fetch.bind(window);
2383
+ window.fetch = function (input, init) {
2384
+ const method = (init && init.method ? init.method : 'GET').toUpperCase();
2385
+ if (_mmAuthValue && method !== 'GET' && method !== 'HEAD') {
2386
+ init = init ? { ...init } : {};
2387
+ init.headers = new Headers(init.headers || {});
2388
+ if (!init.headers.has('x-monomind-token')) init.headers.set('x-monomind-token', _mmAuthValue);
2389
+ }
2390
+ return _origFetch(input, init);
2391
+ };
2392
+ })();
2393
+
2442
2394
  // ── state ──────────────────────────────────────────────────
2443
2395
  let DIR = '';
2444
2396
  let ORIGINAL_DIR = '';
@@ -2477,10 +2429,10 @@ function switchView(v, { updateHash = true } = {}) {
2477
2429
  el.classList.toggle('active', el.dataset.view === v));
2478
2430
  document.querySelectorAll('.view').forEach(el =>
2479
2431
  el.classList.toggle('active', el.id === 'view-' + v));
2480
- const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', monoagent:'MonoAgent', global:'Global Feed', 'global-loops':'Global Loops', 'global-tokens':'Global Tokens', chat:'Global Agent Chat' };
2432
+ const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed', 'global-loops':'Global Loops', 'global-tokens':'Global Tokens', chat:'Global Agent Chat', humaninput:'Human Input' };
2481
2433
  document.getElementById('view-title').textContent = titles[v] || v;
2482
2434
  const PROJECT = DIR ? shortPath(DIR) : 'monomind';
2483
- const VIEW_LABELS = { now: 'Now', sessions: 'Sessions', projects: 'Projects', loops: 'Loops', tokens: 'Tokens', memory: 'Memory', orgs: 'Orgs', monograph: 'Monograph', monoagent: 'MonoAgent', global: 'Global Feed', 'global-loops': 'Global Loops', 'global-tokens': 'Global Tokens', chat: 'Global Agent Chat' };
2435
+ const VIEW_LABELS = { now: 'Now', sessions: 'Sessions', projects: 'Projects', loops: 'Loops', tokens: 'Tokens', memory: 'Memory', orgs: 'Orgs', monograph: 'Monograph', global: 'Global Feed', 'global-loops': 'Global Loops', 'global-tokens': 'Global Tokens', chat: 'Global Agent Chat', humaninput: 'Human Input' };
2484
2436
  document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
2485
2437
  if (updateHash) _pushHash();
2486
2438
  // Projects always re-fetches so onclick paths in cards stay current
@@ -2540,10 +2492,10 @@ function renderView(v) {
2540
2492
  if (v === 'memory') renderMemory();
2541
2493
  if (v === 'orgs') renderOrgs();
2542
2494
  if (v === 'monograph') loadMonograph();
2543
- if (v === 'monoagent') { loadMonoagentData(); }
2544
2495
  if (v === 'global') renderGlobalFeed();
2545
2496
  if (v === 'global-loops') renderGlobalLoops();
2546
2497
  if (v === 'global-tokens') renderGlobalTokens();
2498
+ if (v === 'humaninput') renderHumanInputView();
2547
2499
  }
2548
2500
 
2549
2501
  function refreshCurrent() {
@@ -3670,7 +3622,7 @@ function renderProjectGrid(projects, query) {
3670
3622
  const isCurrent = p.path === DIR;
3671
3623
  const score = computeHealthScore(p);
3672
3624
  const hCls = healthClass(score);
3673
- return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
3625
+ return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(String(p.path || '').replace(/['\\]/g,'\\$&'))}')">
3674
3626
  ${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
3675
3627
  <div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
3676
3628
  <div class="proj-card-name">${esc(p.name || p.slug)}</div>
@@ -3747,7 +3699,7 @@ async function renderSessions() {
3747
3699
  let anomBadge = '';
3748
3700
  if (medianCost > 0.05 && sCost > medianCost * 3 && sCost > 0.5) {
3749
3701
  // f60: anomaly badge is clickable for explainer
3750
- anomBadge = `<span class="sess-anomaly anom-cost clickable" style="cursor:pointer" onclick="showCostExplainer('${esc(s.id)}',event)" title="Cost ${((sCost/medianCost).toFixed(1))}× above median — click for details">! costly</span>`;
3702
+ anomBadge = `<span class="sess-anomaly anom-cost clickable" style="cursor:pointer" onclick="showCostExplainer('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}',event)" title="Cost ${((sCost/medianCost).toFixed(1))}× above median — click for details">! costly</span>`;
3751
3703
  } else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
3752
3704
  anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
3753
3705
  }
@@ -3775,7 +3727,7 @@ async function renderSessions() {
3775
3727
  </div>` : '';
3776
3728
  // f51: error badge — clickable if errors exist
3777
3729
  const errBadge = (s.errorCount > 0 && s.toolCalls > 0 && (s.errorCount / s.toolCalls) > 0.3)
3778
- ? `<span class="sess-anomaly anom-err clickable" onclick="toggleErrDrawer('${esc(s.id)}',event)" title="Click to see ${s.errorCount} tool errors">${s.errorCount} err</span>`
3730
+ ? `<span class="sess-anomaly anom-err clickable" onclick="toggleErrDrawer('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}',event)" title="Click to see ${s.errorCount} tool errors">${s.errorCount} err</span>`
3779
3731
  : anomBadge;
3780
3732
  // f50: custom tags
3781
3733
  const ctags = getCustomTags(s.id);
@@ -3786,12 +3738,12 @@ async function renderSessions() {
3786
3738
  const summaryHtml = s.summary
3787
3739
  ? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
3788
3740
  : '';
3789
- return `<div class="sess-row" data-sess-idx="${idx}" data-sess-id="${esc(s.id)}" onclick="handleSessRowClick(event,this,'${esc(s.id)}')" data-sess-data='${sData}'>
3741
+ return `<div class="sess-row" data-sess-idx="${idx}" data-sess-id="${esc(s.id)}" onclick="handleSessRowClick(event,this,'${esc(String(s.id).replace(/['\\]/g,'\\$&'))}')" data-sess-data='${sData}'>
3790
3742
  <div class="sr-top">
3791
3743
  <div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3792
3744
  <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
3793
3745
  <button class="sr-copy-btn" data-prompt="${esc(s.lastPrompt || s.id)}" onclick="copyPrompt(this.dataset.prompt,event)" title="Copy prompt to clipboard">⎘</button>
3794
- <button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(s.id)}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
3746
+ <button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
3795
3747
  <span class="sr-view">→ view</span>
3796
3748
  </div>
3797
3749
  ${summaryHtml}
@@ -3803,7 +3755,7 @@ async function renderSessions() {
3803
3755
  ${ctxGauge}
3804
3756
  <div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
3805
3757
  <div class="sess-notes-wrap" onclick="event.stopPropagation()">
3806
- <button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(s.id)}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
3758
+ <button class="sess-notes-toggle${hasNote ? ' has-note' : ''}" onclick="toggleSessNote('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}',this)">✎ ${hasNote ? 'Note' : 'Add note'}</button>
3807
3759
  <div class="sess-notes-area" id="snote-${esc(s.id)}">
3808
3760
  <textarea class="sess-note-input" rows="2" placeholder="Session note…" oninput="saveSessNote('${esc(s.id)}',this.value,this.closest('.sess-notes-wrap').querySelector('.sess-notes-toggle'),this.closest('.sess-row').querySelector('.sess-note-saved'))">${esc(note)}</textarea>
3809
3761
  <div class="sess-note-saved"></div>
@@ -3930,7 +3882,7 @@ function buildTagFilterBar(sessions) {
3930
3882
  if (!allTags.common.size) return '';
3931
3883
  const sorted = [...allTags.common].sort();
3932
3884
  const chips = sorted.map(t =>
3933
- `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
3885
+ `<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(String(t).replace(/['\\]/g,'\\$&'))}')">${esc(t)}</button>`
3934
3886
  ).join('');
3935
3887
  return `<div class="tag-filter-bar">${chips}</div>`;
3936
3888
  }
@@ -4181,7 +4133,7 @@ async function renderGlobalFeed() {
4181
4133
  const gfSummaryHtml = s.summary
4182
4134
  ? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
4183
4135
  : '';
4184
- return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
4136
+ return `<div class="sess-row" onclick="switchProject('${esc(String(project.path).replace(/['\\]/g,'\\$&'))}');setTimeout(()=>jumpToSession('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}'),150)">
4185
4137
  <div class="sr-top">
4186
4138
  <div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
4187
4139
  <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${gfCompactBadge}
@@ -4196,6 +4148,73 @@ async function renderGlobalFeed() {
4196
4148
  }
4197
4149
  }
4198
4150
 
4151
+ async function renderHumanInputView() {
4152
+ const el = document.getElementById('hi-content');
4153
+ el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
4154
+ try {
4155
+ const data = await apiFetch('/api/projects');
4156
+ const projects = (data?.projects || []).slice(0, 8);
4157
+ if (!projects.length) {
4158
+ el.innerHTML = '<div class="empty"><div class="empty-ico">❓</div><div>No projects found</div></div>';
4159
+ updateHumanInputBadge(0);
4160
+ return;
4161
+ }
4162
+ const results = await Promise.allSettled(
4163
+ projects.map(p => apiFetch('/api/questions?dir=' + enc(p.path)).then(d => ({ project: p, questions: d.questions || [] })))
4164
+ );
4165
+ const entries = [];
4166
+ for (const r of results) {
4167
+ if (r.status !== 'fulfilled') continue;
4168
+ const { project, questions } = r.value;
4169
+ for (const q of questions) entries.push({ project, q });
4170
+ }
4171
+ entries.sort((a, b) => (b.q.ts || 0) - (a.q.ts || 0));
4172
+ updateHumanInputBadge(entries.length);
4173
+ document.getElementById('hi-sub').textContent = entries.length
4174
+ ? `${entries.length} pending question(s) across ${projects.length} projects`
4175
+ : `No pending questions across ${projects.length} projects`;
4176
+ if (!entries.length) {
4177
+ el.innerHTML = '<div class="empty"><div class="empty-ico">❓</div><div>No pending questions</div></div>';
4178
+ return;
4179
+ }
4180
+ el.innerHTML = '<div class="sess-list">' + entries.map(({ project, q }) => `
4181
+ <div class="sess-row" data-question-id="${esc(q.questionId)}" style="flex-direction:column;align-items:stretch;gap:6px">
4182
+ <div style="display:flex;justify-content:space-between;gap:8px">
4183
+ <div><b>${esc(q.org)}</b> · ${esc(q.role)} <span style="color:var(--text-xs)">(${esc(project.name || project.path)})</span></div>
4184
+ <div style="color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(q.ts)}</div>
4185
+ </div>
4186
+ <div style="color:var(--text-hi)">${esc(q.question)}</div>
4187
+ <div style="display:flex;gap:6px">
4188
+ <input type="text" class="hi-answer-input" placeholder="Your answer…" style="flex:1;padding:4px 8px;font-size:12px" />
4189
+ <button class="btn" style="font-size:11px" onclick="submitHumanAnswer('${esc(String(project.path).replace(/['\\]/g,'\\$&'))}','${esc(String(q.org).replace(/['\\]/g,'\\$&'))}','${esc(String(q.role).replace(/['\\]/g,'\\$&'))}','${esc(String(q.questionId).replace(/['\\]/g,'\\$&'))}',this)">Answer</button>
4190
+ </div>
4191
+ </div>
4192
+ `).join('') + '</div>';
4193
+ } catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
4194
+ }
4195
+
4196
+ function updateHumanInputBadge(count) {
4197
+ const b = document.getElementById('bdg-humaninput');
4198
+ if (b) b.textContent = String(count);
4199
+ }
4200
+
4201
+ async function submitHumanAnswer(dir, org, role, questionId, btnEl) {
4202
+ const row = btnEl.closest('[data-question-id]');
4203
+ const input = row.querySelector('.hi-answer-input');
4204
+ const answer = input.value.trim();
4205
+ if (!answer) return;
4206
+ btnEl.disabled = true;
4207
+ try {
4208
+ const res = await fetch('/api/questions/answer', {
4209
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
4210
+ body: JSON.stringify({ dir, org, role, questionId, answer }),
4211
+ });
4212
+ const data = await res.json();
4213
+ if (data.ok) { showToast('Answered', `${org}:${role}`, 'ok'); row.remove(); }
4214
+ else { showToast('Error', data.error || 'failed', 'err'); btnEl.disabled = false; }
4215
+ } catch (e) { showToast('Error', e.message, 'err'); btnEl.disabled = false; }
4216
+ }
4217
+
4199
4218
  // ── agent chat view ────────────────────────────────────────
4200
4219
  let chatVSessions = {}; // sessionId or groupId → session/group object
4201
4220
  let chatVGroupMap = {}; // groupId → group object
@@ -5087,7 +5106,7 @@ function renderLeaderboard() {
5087
5106
  const cost = '$' + s.totalCost.toFixed(2);
5088
5107
  const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
5089
5108
  const prompt = s.lastPrompt || s.id;
5090
- return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
5109
+ return `<tr onclick="jumpToSession('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}')" title="${esc(prompt)}">
5091
5110
  <td class="lb-rank">${i + 1}</td>
5092
5111
  <td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
5093
5112
  <td class="lb-cost">${cost}</td>
@@ -5337,7 +5356,7 @@ function buildSwimlane() {
5337
5356
  const color = `oklch(65% 0.14 ${hue})`;
5338
5357
  const name = (s.lastPrompt || s.id).slice(0, 16);
5339
5358
  return `<div class="sw-row">
5340
- <div class="sw-lbl" onclick="jumpToSession('${esc(s.id)}')" title="${esc(s.lastPrompt || s.id)}">${esc(name)}</div>
5359
+ <div class="sw-lbl" onclick="jumpToSession('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}')" title="${esc(s.lastPrompt || s.id)}">${esc(name)}</div>
5341
5360
  <div class="sw-track">
5342
5361
  <div class="sw-bar" style="left:${leftPct}%;width:${widthPct}%;background:${color}" title="${esc(name)} · ${fmtDur(dur)}"></div>
5343
5362
  </div>
@@ -6287,6 +6306,14 @@ function v2RenderOrgChart() {
6287
6306
  });
6288
6307
  _v2ChartLayout = { pos, edgeIdByPair };
6289
6308
  v2OrgActivityReset();
6309
+ // The org-chart's live bubbles (activity + pending-question) and flying letters are ALL
6310
+ // driven by _odtHandleLiveEvent, which only receives events while the odt SSE connections
6311
+ // are open. Those were previously established only from v2RenderOrgChat (the Chat tab), so a
6312
+ // user who opens the Chart tab without first visiting Chat got NO live chart updates — the
6313
+ // question bubble never lit. Establish them here too so the Chart tab is self-sufficient.
6314
+ // Both connect helpers are idempotent (guarded) and null-safe on their live-dot elements.
6315
+ _odtConnectChatSSE();
6316
+ if (_v2SelOrg) _odtConnectOrgTail(_v2SelOrg);
6290
6317
  }
6291
6318
 
6292
6319
  function _v2RoleName(id, roles) {
@@ -6403,14 +6430,23 @@ function v2OrgActivityReset() {
6403
6430
  _v2BubbleTimers = new Map();
6404
6431
  }
6405
6432
 
6433
+ function _v2FadeOutBubble(fo) {
6434
+ if (typeof gsap !== 'undefined') {
6435
+ gsap.to(fo, { autoAlpha: 0, duration: 0.4, ease: 'power2.in', onComplete: function() { fo.style.visibility = 'hidden'; } });
6436
+ } else {
6437
+ fo.style.opacity = '0';
6438
+ fo.style.visibility = 'hidden';
6439
+ }
6440
+ }
6441
+
6406
6442
  function v2ShowActivityBubble(roleId, text) {
6407
6443
  const fo = document.querySelector('.v2-activity-bubble[data-bubble-for="' + CSS.escape(roleId) + '"]');
6408
6444
  if (!fo) return;
6445
+ if (fo.dataset.questionPending === '1') return; // a pending question owns this bubble until answered
6409
6446
  const inner = fo.querySelector('.v2-bubble-inner');
6410
6447
  inner.textContent = text;
6411
6448
  if (typeof gsap !== 'undefined') {
6412
6449
  gsap.killTweensOf(fo);
6413
- fo.style.visibility = 'visible';
6414
6450
  gsap.fromTo(fo, { scale: 0.8, autoAlpha: 0 }, {
6415
6451
  scale: 1, autoAlpha: 1, duration: 0.3, ease: 'back.out(1.7)',
6416
6452
  transformOrigin: '50% 100%',
@@ -6422,16 +6458,47 @@ function v2ShowActivityBubble(roleId, text) {
6422
6458
  const existing = _v2BubbleTimers.get(roleId);
6423
6459
  if (existing) clearTimeout(existing);
6424
6460
  _v2BubbleTimers.set(roleId, setTimeout(function() {
6425
- if (typeof gsap !== 'undefined') {
6426
- gsap.to(fo, { autoAlpha: 0, duration: 0.4, ease: 'power2.in', onComplete: function() { fo.style.visibility = 'hidden'; } });
6427
- } else {
6428
- fo.style.opacity = '0';
6429
- fo.style.visibility = 'hidden';
6430
- }
6461
+ _v2FadeOutBubble(fo);
6431
6462
  _v2BubbleTimers.delete(roleId);
6432
6463
  }, 5000));
6433
6464
  }
6434
6465
 
6466
+ // Truncates by Unicode code point (via the string iterator), not UTF-16 code
6467
+ // unit — a plain .slice() can bisect a surrogate pair (e.g. an emoji in the
6468
+ // question text) and render a broken glyph.
6469
+ function _v2TruncateText(text, maxLen) {
6470
+ const chars = [...text];
6471
+ if (chars.length <= maxLen) return text;
6472
+ return chars.slice(0, maxLen - 1).join('') + '…';
6473
+ }
6474
+
6475
+ function v2ShowQuestionBubble(roleId, questionText) {
6476
+ const fo = document.querySelector('.v2-activity-bubble[data-bubble-for="' + CSS.escape(roleId) + '"]');
6477
+ if (!fo) return;
6478
+ const existing = _v2BubbleTimers.get(roleId);
6479
+ if (existing) { clearTimeout(existing); _v2BubbleTimers.delete(roleId); }
6480
+ fo.dataset.questionPending = '1';
6481
+ const inner = fo.querySelector('.v2-bubble-inner');
6482
+ inner.textContent = '❓ ' + _v2TruncateText(questionText, 40);
6483
+ inner.classList.add('v2-bubble-question');
6484
+ if (typeof gsap !== 'undefined') {
6485
+ gsap.killTweensOf(fo);
6486
+ gsap.fromTo(fo, { scale: 0.8, autoAlpha: 0 }, { scale: 1, autoAlpha: 1, duration: 0.3, ease: 'back.out(1.7)', transformOrigin: '50% 100%' });
6487
+ } else {
6488
+ fo.style.visibility = 'visible';
6489
+ fo.style.opacity = '1';
6490
+ }
6491
+ }
6492
+
6493
+ function v2ClearQuestionBubble(roleId) {
6494
+ const fo = document.querySelector('.v2-activity-bubble[data-bubble-for="' + CSS.escape(roleId) + '"]');
6495
+ if (!fo) return;
6496
+ delete fo.dataset.questionPending;
6497
+ const inner = fo.querySelector('.v2-bubble-inner');
6498
+ if (inner) inner.classList.remove('v2-bubble-question');
6499
+ _v2FadeOutBubble(fo);
6500
+ }
6501
+
6435
6502
  // ── in-place running dot updater (avoids full re-render + GSAP flicker on SSE events) ──
6436
6503
  function v2UpdateChartRunningDots() {
6437
6504
  if (!_v2OrgData) return;
@@ -8332,6 +8399,14 @@ function _odtHandleLiveEvent(ev) {
8332
8399
  const _arr = [..._odtChatSeenKeys];
8333
8400
  _odtChatSeenKeys = new Set(_arr.slice(1000));
8334
8401
  }
8402
+ if (ev?.type === 'org:question' && ev.from) {
8403
+ if (ev.org === _v2SelOrg) v2ShowQuestionBubble(ev.from, ev.question || '');
8404
+ if (viewRendered['humaninput']) renderHumanInputView();
8405
+ else updateHumanInputBadge((parseInt(document.getElementById('bdg-humaninput')?.textContent, 10) || 0) + 1);
8406
+ }
8407
+ if (ev?.type === 'org:question-answered' && ev.role) {
8408
+ if (ev.org === _v2SelOrg) v2ClearQuestionBubble(ev.role);
8409
+ }
8335
8410
  // Wire activity bubble for org chart visualization
8336
8411
  if (ev && ev.org === _v2SelOrg) {
8337
8412
  const roles = Array.isArray(_v2OrgData && _v2OrgData.roles) ? _v2OrgData.roles : [];
@@ -8552,23 +8627,65 @@ async function v2RenderOrgFiles() {
8552
8627
  '<table style="width:100%;border-collapse:collapse;font-size:12px">'+
8553
8628
  '<thead><tr style="color:var(--text-xs);font-size:9px;text-align:left"><th style="padding:4px 8px">File</th><th>Type</th><th>Size</th><th>Modified</th><th></th></tr></thead>'+
8554
8629
  '<tbody>'+g.items.map(f => {
8630
+ const isImg = OF_IMG_RE.test(f.name);
8555
8631
  return '<tr style="border-top:1px solid var(--border)" data-fp="'+esc(f.path)+'" data-fn="'+esc(f.name)+'">'+
8556
8632
  '<td style="padding:6px 8px"><span style="font-family:var(--mono);font-size:11px;color:var(--text-hi);cursor:pointer" onclick="v2ViewOrgFile(this.closest(\'tr\').dataset.fp,this.closest(\'tr\').dataset.fn)">'+esc(f.name)+'</span></td>'+
8557
8633
  '<td style="padding:6px 8px">'+badge(f.type)+'</td>'+
8558
8634
  '<td style="padding:6px 8px;font-size:10px;color:var(--text-lo);font-family:var(--mono)">'+fmtSize(f.size||0)+'</td>'+
8559
8635
  '<td style="padding:6px 8px;font-size:10px;color:var(--text-lo)">'+fmtTime(f.mtime)+'</td>'+
8560
- '<td style="padding:6px 8px"><button class="btn" style="font-size:9px;padding:2px 7px" onclick="v2ViewOrgFile(this.closest(\'tr\').dataset.fp,this.closest(\'tr\').dataset.fn)">View</button></td></tr>';
8636
+ '<td style="padding:6px 8px;white-space:nowrap">'+
8637
+ '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="v2ViewOrgFile(this.closest(\'tr\').dataset.fp,this.closest(\'tr\').dataset.fn)">View</button> '+
8638
+ '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofCopyPath(this.closest(\'tr\').dataset.fp)">Copy Path</button>'+
8639
+ (isImg ? '' : ' <button class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofCopyContent(this.closest(\'tr\').dataset.fp)">Copy</button>') +
8640
+ '</td></tr>';
8561
8641
  }).join('')+'</tbody></table></div>';
8562
8642
  }).join('')+'</div>';
8563
8643
  } catch(e) { el.innerHTML = '<div style="padding:20px;color:oklch(60% 0.22 25)">Error: '+e.message+'</div>'; }
8564
8644
  }
8565
8645
 
8646
+ function _ofCopyPath(filePath) {
8647
+ if (!filePath) return;
8648
+ navigator.clipboard.writeText(filePath).then(() => {
8649
+ if (typeof showToast === 'function') showToast('Path copied', '', 'ok');
8650
+ });
8651
+ }
8652
+
8653
+ async function _ofCopyContent(filePath) {
8654
+ if (!filePath) return;
8655
+ try {
8656
+ const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
8657
+ const r = await fetch('/api/file-content?path='+encodeURIComponent(filePath)+dirParam);
8658
+ if (!r.ok) { if (typeof showToast === 'function') showToast('Copy failed', 'Error '+r.status, 'error'); return; }
8659
+ const text = await r.text();
8660
+ await navigator.clipboard.writeText(text);
8661
+ if (typeof showToast === 'function') showToast('Content copied', '', 'ok');
8662
+ } catch (e) {
8663
+ if (typeof showToast === 'function') showToast('Copy failed', e.message, 'error');
8664
+ }
8665
+ }
8666
+
8566
8667
  let _ofModal = null;
8567
8668
  let _ofRawText = '';
8568
8669
 
8670
+ const OF_IMG_RE = /\.(png|jpe?g|gif|svg|webp)$/i;
8671
+
8672
+ function _ofMdTableRow(row) {
8673
+ var cells = row.trim().replace(/^\||\|$/g,'').split('|').map(function(c){ return c.trim(); });
8674
+ return cells;
8675
+ }
8676
+
8569
8677
  function _ofMdToHtml(md) {
8570
8678
  return md
8571
8679
  .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
8680
+ .replace(/^(\|.+\|)[ \t]*\n\|[ \t]*:?-{2,}:?[ \t]*(?:\|[ \t]*:?-{2,}:?[ \t]*)*\|?[ \t]*\n((?:\|.*\|[ \t]*\n?)*)/gm, function(_m, headerRow, bodyRows) {
8681
+ var head = _ofMdTableRow(headerRow);
8682
+ var thead = '<tr>' + head.map(function(c){ return '<th style="padding:6px 10px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-hi)">'+c+'</th>'; }).join('') + '</tr>';
8683
+ var tbody = bodyRows.split('\n').filter(function(r){ return r.trim(); }).map(function(r){
8684
+ var cells = _ofMdTableRow(r);
8685
+ return '<tr>' + cells.map(function(c){ return '<td style="padding:6px 10px;border-bottom:1px solid var(--border)">'+c+'</td>'; }).join('') + '</tr>';
8686
+ }).join('');
8687
+ return '<table style="border-collapse:collapse;width:100%;margin:8px 0;font-size:11px">'+thead+tbody+'</table>';
8688
+ })
8572
8689
  .replace(/^#{6}\s+(.+)$/gm,'<h6 style="font-size:11px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h6>')
8573
8690
  .replace(/^#{5}\s+(.+)$/gm,'<h5 style="font-size:12px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h5>')
8574
8691
  .replace(/^#{4}\s+(.+)$/gm,'<h4 style="font-size:13px;font-weight:700;color:var(--text-hi);margin:12px 0 5px">$1</h4>')
@@ -8583,6 +8700,9 @@ function _ofMdToHtml(md) {
8583
8700
  .replace(/^\s*[-*]\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
8584
8701
  .replace(/(<li.*<\/li>\n?)+/g,'<ul style="padding-left:18px;margin:6px 0">$&</ul>')
8585
8702
  .replace(/^\d+\.\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
8703
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function(_m, alt, url) {
8704
+ return '<img src="'+String(url).replace(/"/g,'&quot;')+'" alt="'+String(alt).replace(/"/g,'&quot;')+'" loading="lazy" style="max-width:100%;border-radius:4px;margin:8px 0;display:block">';
8705
+ })
8586
8706
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent);text-decoration:underline" target="_blank" rel="noopener">$1</a>')
8587
8707
  .replace(/\n\n+/g,'</p><p style="margin:0 0 8px">')
8588
8708
  .replace(/^(?!<[hupoli]|<pre|<hr)(.+)$/gm, '$1')
@@ -8593,6 +8713,7 @@ async function v2ViewOrgFile(filePath, fileName) {
8593
8713
  if (_ofModal) { _ofModal.remove(); _ofModal = null; }
8594
8714
  const isMd = /\.(md|markdown)$/i.test(fileName);
8595
8715
  const isJson = /\.json$/i.test(fileName);
8716
+ const isImage = OF_IMG_RE.test(fileName);
8596
8717
  _ofRawText = '';
8597
8718
  _ofModal = document.createElement('div');
8598
8719
  _ofModal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box';
@@ -8602,8 +8723,9 @@ async function v2ViewOrgFile(filePath, fileName) {
8602
8723
  '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);flex-shrink:0">' +
8603
8724
  '<span style="font-family:var(--mono);font-size:11px;color:var(--text-hi);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+esc(filePath)+'">'+esc(fileName)+'</span>' +
8604
8725
  (isMd ? '<button id="of-toggle" class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofToggleRender()">Raw</button>' : '') +
8605
- '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="navigator.clipboard.writeText(_ofRawText).then(()=>{ if(typeof showToast===\'function\') showToast(\'Copied\',\'\',\'ok\'); })">Copy</button>' +
8606
- '<button class="btn" style="font-size:9px;padding:2px 7px;border-color:oklch(68% 0.20 150);color:oklch(68% 0.20 150)" onclick="_ofSaveFile(\''+esc(fileName)+'\')">Save</button>' +
8726
+ (isImage ? '' : '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="navigator.clipboard.writeText(_ofRawText).then(()=>{ if(typeof showToast===\'function\') showToast(\'Copied\',\'\',\'ok\'); })">Copy</button>') +
8727
+ (isImage ? '<button class="btn" style="font-size:9px;padding:2px 7px;border-color:oklch(68% 0.20 150);color:oklch(68% 0.20 150)" onclick="_ofSaveImage(\''+esc(String(fileName).replace(/['\\]/g,'\\$&'))+'\')">Save</button>'
8728
+ : '<button class="btn" style="font-size:9px;padding:2px 7px;border-color:oklch(68% 0.20 150);color:oklch(68% 0.20 150)" onclick="_ofSaveFile(\''+esc(String(fileName).replace(/['\\]/g,'\\$&'))+'\')">Save</button>') +
8607
8729
  '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofModal&&(_ofModal.remove(),_ofModal=null)">✕</button>' +
8608
8730
  '</div>' +
8609
8731
  '<div id="of-content" style="flex:1;overflow-y:auto;padding:16px 18px;font-size:12px;line-height:1.7;color:var(--text-mid);scrollbar-width:thin;scrollbar-color:var(--border) transparent">' +
@@ -8611,9 +8733,18 @@ async function v2ViewOrgFile(filePath, fileName) {
8611
8733
  '</div>' +
8612
8734
  '</div>';
8613
8735
  document.body.appendChild(_ofModal);
8736
+ const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
8737
+ const fileUrl = '/api/file-content?path='+encodeURIComponent(filePath)+dirParam;
8738
+ if (isImage) {
8739
+ const el = document.getElementById('of-content');
8740
+ if (el) {
8741
+ _ofRawText = fileUrl;
8742
+ el.innerHTML = '<img src="'+esc(fileUrl)+'" alt="'+esc(fileName)+'" style="max-width:100%;display:block;margin:0 auto" onerror="this.replaceWith(document.createTextNode(\'Failed to load image\'))">';
8743
+ }
8744
+ return;
8745
+ }
8614
8746
  try {
8615
- const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
8616
- const r = await fetch('/api/file-content?path='+encodeURIComponent(filePath)+dirParam);
8747
+ const r = await fetch(fileUrl);
8617
8748
  const el = document.getElementById('of-content');
8618
8749
  if (!el) return;
8619
8750
  if (!r.ok) { el.textContent = 'Error '+r.status+': '+r.statusText; return; }
@@ -8660,6 +8791,14 @@ function _ofSaveFile(defaultName) {
8660
8791
  URL.revokeObjectURL(a.href);
8661
8792
  }
8662
8793
 
8794
+ function _ofSaveImage(defaultName) {
8795
+ if (!_ofRawText) return;
8796
+ const a = document.createElement('a');
8797
+ a.href = _ofRawText;
8798
+ a.download = defaultName || 'image';
8799
+ a.click();
8800
+ }
8801
+
8663
8802
  // ── WORKSPACES ─────────────────────────────────────────────
8664
8803
  async function v2RenderOrgWorkspaces() {
8665
8804
  const el = document.getElementById('odt-workspaces');
@@ -8901,7 +9040,7 @@ window.v2ShowCopyOrgDialog = async function() {
8901
9040
  const label = p.name || p.slug || p.path?.split('/').pop() || p.path;
8902
9041
  const shortP = (p.path || '').replace(/\/Users\/[^/]+\//, '~/');
8903
9042
  const isCurrent = p.path === (window._orgDir || '');
8904
- return `<div class="ocp-row${isCurrent ? ' ocp-sel' : ''}" title="${esc(p.path)}" onclick="document.querySelectorAll('.ocp-row').forEach(r=>r.classList.remove('ocp-sel'));this.classList.add('ocp-sel');document.getElementById('org-copy-dest').value='${esc(p.path).replace(/'/g,"\\'")}'" style="padding:7px 10px;font-size:12px;font-family:var(--mono);cursor:pointer;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:baseline">
9043
+ return `<div class="ocp-row${isCurrent ? ' ocp-sel' : ''}" title="${esc(p.path)}" onclick="document.querySelectorAll('.ocp-row').forEach(r=>r.classList.remove('ocp-sel'));this.classList.add('ocp-sel');document.getElementById('org-copy-dest').value='${esc(String(p.path).replace(/['\\]/g,'\\$&'))}'" style="padding:7px 10px;font-size:12px;font-family:var(--mono);cursor:pointer;border-bottom:1px solid var(--border);display:flex;gap:8px;align-items:baseline">
8905
9044
  <span style="color:var(--text-hi)">${esc(label)}</span>
8906
9045
  <span style="color:var(--text-lo);font-size:10px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(shortP)}</span>
8907
9046
  </div>`;
@@ -10109,7 +10248,7 @@ async function loadProjCosts() {
10109
10248
  const rows = data.projects.slice(0, 10).map((p, i) => {
10110
10249
  const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
10111
10250
  const name = p.path.split('/').filter(Boolean).pop() || p.path;
10112
- return `<tr onclick="switchProject('${esc(p.path)}')" style="cursor:pointer" title="${esc(p.path)}">
10251
+ return `<tr onclick="switchProject('${esc(String(p.path).replace(/['\\]/g,'\\$&'))}')" style="cursor:pointer" title="${esc(p.path)}">
10113
10252
  <td class="lb-rank">${i + 1}</td>
10114
10253
  <td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
10115
10254
  <td class="lb-cost">$${p.cost.toFixed(2)}</td>
@@ -10181,7 +10320,7 @@ function buildGanttTimeline() {
10181
10320
  const opacity = 0.3 + costRatio * 0.7;
10182
10321
  const color = DONUT_COLORS[i % DONUT_COLORS.length];
10183
10322
  const tip = `${s.lastPrompt ? s.lastPrompt.slice(0,60) : s.id} | $${(s.totalCost||0).toFixed(3)} | ${fmtDur(s.totalDurationMs||0)}`;
10184
- track += `<div class="tl-bar" style="left:${startPct}%;width:${durPct}%;background:${color};opacity:${opacity}" title="${esc(tip)}" onclick="jumpToSession('${esc(s.id)}')"></div>`;
10323
+ track += `<div class="tl-bar" style="left:${startPct}%;width:${durPct}%;background:${color};opacity:${opacity}" title="${esc(tip)}" onclick="jumpToSession('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}')"></div>`;
10185
10324
  }
10186
10325
  html += `<div class="tl-day-row"><div class="tl-day-lbl">${lbl}</div><div class="tl-track">${track}</div></div>`;
10187
10326
  }
@@ -10498,7 +10637,7 @@ function showCustomTagInput(sessId, event) {
10498
10637
  iw.className = 'ctag-input-wrap';
10499
10638
  iw.onclick = e => e.stopPropagation();
10500
10639
  iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
10501
- <button class="ctag-ok" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(sessId)}',inp.value,event);inp.value='';})()">Add</button>`;
10640
+ <button class="ctag-ok" onclick="(()=>{const inp=this.closest('.ctag-input-wrap').querySelector('input');addCustomTag('${esc(String(sessId).replace(/['\\]/g,'\\$&'))}',inp.value,event);inp.value='';})()">Add</button>`;
10502
10641
  wrap.appendChild(iw);
10503
10642
  const inp = iw.querySelector('input');
10504
10643
  inp.focus();
@@ -10510,11 +10649,11 @@ function showCustomTagInput(sessId, event) {
10510
10649
 
10511
10650
  function renderCustomTagsInline(sessId, tags) {
10512
10651
  const tagHtml = tags.map(t =>
10513
- `<span class="sr-ctag">${esc(t)}<span class="ctag-del" onclick="removeCustomTag('${esc(sessId)}','${esc(t)}',event)" title="Remove tag">✕</span></span>`
10652
+ `<span class="sr-ctag">${esc(t)}<span class="ctag-del" onclick="removeCustomTag('${esc(String(sessId).replace(/['\\]/g,'\\$&'))}','${esc(String(t).replace(/['\\]/g,'\\$&'))}',event)" title="Remove tag">✕</span></span>`
10514
10653
  ).join('');
10515
10654
  return `<div class="sr-custom-tags" data-sess="${esc(sessId)}" onclick="event.stopPropagation()">
10516
10655
  ${tagHtml}
10517
- <button class="ctag-add-btn" onclick="showCustomTagInput('${esc(sessId)}',event)" title="Add tag">+ tag</button>
10656
+ <button class="ctag-add-btn" onclick="showCustomTagInput('${esc(String(sessId).replace(/['\\]/g,'\\$&'))}',event)" title="Add tag">+ tag</button>
10518
10657
  </div>`;
10519
10658
  }
10520
10659
 
@@ -10529,14 +10668,14 @@ async function toggleErrDrawer(sessId, event) {
10529
10668
  try {
10530
10669
  const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
10531
10670
  if (!data.errors?.length) {
10532
- drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
10671
+ drawer.innerHTML = `<div class="err-drawer-head"><span>No error details found</span><button class="err-close" onclick="closeErrDrawer('${esc(String(sessId).replace(/['\\]/g,'\\$&'))}')">✕</button></div>`;
10533
10672
  return;
10534
10673
  }
10535
10674
  const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
10536
- drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>
10675
+ drawer.innerHTML = `<div class="err-drawer-head"><span>${data.errors.length} error${data.errors.length !== 1 ? 's' : ''}</span><button class="err-close" onclick="closeErrDrawer('${esc(String(sessId).replace(/['\\]/g,'\\$&'))}')">✕</button></div>
10537
10676
  <div class="err-drawer-body">${items}</div>`;
10538
10677
  } catch (err) {
10539
- drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(sessId)}')">✕</button></div>`;
10678
+ drawer.innerHTML = `<div class="err-drawer-head"><span>Could not load: ${esc(err.message)}</span><button class="err-close" onclick="closeErrDrawer('${esc(String(sessId).replace(/['\\]/g,'\\$&'))}')">✕</button></div>`;
10540
10679
  }
10541
10680
  }
10542
10681
 
@@ -10675,19 +10814,12 @@ let _mgWikiMode = 'all';
10675
10814
  let _mgWikiSearchTimer = null;
10676
10815
 
10677
10816
  /* ── Playbook View ─────────────────────────────────────────── */
10678
- // monoagent connections state
10679
- let _maPlatforms = []; // full registry from monoes
10680
- let _maConns = []; // active API/OAuth connections
10681
- let _maSessions = []; // browser sessions
10682
- let _maSelected = null; // currently open modal platform def
10683
- let _maLoginPoll = null; // active login poll interval (cleared on modal close)
10684
10817
 
10685
10818
  function wfSwitchTab(tab) {
10686
10819
  document.querySelectorAll('.wf-pane, #wf-tab-builder').forEach(p => p.classList.remove('active'));
10687
10820
  document.querySelectorAll('[data-wftab]').forEach(b => b.classList.remove('active'));
10688
10821
  document.getElementById('wf-tab-' + tab).classList.add('active');
10689
10822
  document.querySelector('[data-wftab="' + tab + '"]').classList.add('active');
10690
- if (tab === 'systems') loadPlatformSessions();
10691
10823
  if (tab === 'library') loadWorkflowDefs();
10692
10824
  if (tab === 'runs') loadWorkflowRuns();
10693
10825
  if (tab === 'builder') pbInit();
@@ -10721,31 +10853,13 @@ async function loadWorkflowDefs() {
10721
10853
  </div>
10722
10854
  </div>
10723
10855
  <div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
10724
- <button class="wf-def-run-btn" title="Copy run command" onclick="navigator.clipboard?.writeText('npx monomind browse playbook run .monomind/playbooks/${esc(d.file)}').then(()=>this.textContent='Copied!').catch(()=>{});setTimeout(()=>this.textContent='▶ Run',1500)">▶ Run</button>
10856
+ <button class="wf-def-run-btn" title="Copy run command" onclick="navigator.clipboard?.writeText('npx monomind browse playbook run .monomind/playbooks/${esc(String(d.file).replace(/['\\]/g,'\\$&'))}').then(()=>this.textContent='Copied!').catch(()=>{});setTimeout(()=>this.textContent='▶ Run',1500)">▶ Run</button>
10725
10857
  <button class="wf-def-run-btn" style="font-size:10px" title="Open in Builder" onclick="pbLoadDef(${JSON.stringify(d.id)});wfSwitchTab('builder')">✎ Edit</button>
10726
10858
  </div>
10727
10859
  </div>`;
10728
10860
  }).join('');
10729
10861
  }
10730
10862
 
10731
- async function loadPlatformSessions() {
10732
- const grid = document.getElementById('wf-systems-grid');
10733
- if (!grid) return;
10734
- grid.innerHTML = '<div class="loading-txt">Loading…</div>';
10735
- try {
10736
- const [plats, conns] = await Promise.all([
10737
- fetch('/api/monoagent/platforms').then(r => r.json()).catch(() => []),
10738
- fetch('/api/monoagent/connections').then(r => r.json()).catch(() => ({ connections: [], sessions: [] })),
10739
- ]);
10740
- _maPlatforms = Array.isArray(plats) ? plats : [];
10741
- _maConns = Array.isArray(conns.connections) ? conns.connections : [];
10742
- _maSessions = Array.isArray(conns.sessions) ? conns.sessions : [];
10743
- renderSystems();
10744
- } catch (e) {
10745
- grid.innerHTML = '<div class="loading-txt">Failed to load systems.</div>';
10746
- }
10747
- }
10748
-
10749
10863
  function relativeTime(ts) {
10750
10864
  if (!ts) return '—';
10751
10865
  const s = Math.floor((Date.now() - ts) / 1000);
@@ -10755,291 +10869,6 @@ function relativeTime(ts) {
10755
10869
  return Math.floor(s / 86400) + 'd ago';
10756
10870
  }
10757
10871
 
10758
- const MA_CATEGORY_ORDER = ['social', 'service', 'communication', 'database'];
10759
- const MA_CATEGORY_LABELS = { social: 'Social', service: 'Services & APIs', communication: 'Communication', database: 'Databases' };
10760
- const BROWSER_PLATFORMS = new Set(['instagram', 'linkedin', 'x', 'tiktok', 'gemini']);
10761
-
10762
- function maResolveConn(pid) {
10763
- pid = pid.toLowerCase();
10764
- if (BROWSER_PLATFORMS.has(pid)) {
10765
- const now = Date.now();
10766
- const s = _maSessions.find(x => (x.Platform || '').toLowerCase() === pid);
10767
- if (!s) return null;
10768
- const expiry = new Date(s.Expiry).getTime();
10769
- return { _type: 'session', id: String(s.ID), account: s.Username || '—', method: 'browser', status: expiry > now ? 'active' : 'expired' };
10770
- }
10771
- const c = _maConns.find(x => (x.platform || '').toLowerCase() === pid);
10772
- if (!c) return null;
10773
- return { _type: 'connection', id: c.id, account: c.account_id || c.label || '—', method: c.method || '—', status: c.status || 'active', lastTested: c.last_tested };
10774
- }
10775
-
10776
- function renderSystems() {
10777
- const grid = document.getElementById('wf-systems-grid');
10778
- if (!grid) return;
10779
-
10780
- if (!_maPlatforms.length) {
10781
- grid.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-xs);font-size:12px">
10782
- <div style="font-size:28px;margin-bottom:8px">🔌</div>
10783
- <div style="font-weight:600;color:var(--text-lo);margin-bottom:4px">monoagent not found</div>
10784
- <div style="font-size:11px">Install monoes: <code style="color:var(--accent)">go install github.com/monoes/mono-agent/cmd/monoes@latest</code></div>
10785
- </div>`;
10786
- return;
10787
- }
10788
-
10789
- const groups = {};
10790
- for (const p of _maPlatforms) {
10791
- const cat = (p.Category || p.category || 'service').toLowerCase();
10792
- if (!groups[cat]) groups[cat] = [];
10793
- groups[cat].push(p);
10794
- }
10795
-
10796
- const cats = [...MA_CATEGORY_ORDER, ...Object.keys(groups).filter(c => !MA_CATEGORY_ORDER.includes(c))].filter(c => groups[c]);
10797
- const totalConn = _maPlatforms.filter(p => maResolveConn(p.ID || p.id)).length;
10798
-
10799
- let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
10800
- <span style="font-size:11px;color:var(--text-xs)">${totalConn} / ${_maPlatforms.length} connected</span>
10801
- <button class="ma-btn ma-btn-ghost" style="font-size:10px;padding:3px 10px" onclick="loadPlatformSessions()">↺ Refresh</button>
10802
- </div>`;
10803
-
10804
- for (const cat of cats) {
10805
- const list = groups[cat];
10806
- const catConn = list.filter(p => maResolveConn(p.ID || p.id)).length;
10807
- html += `<div class="ma-cat-hdr">
10808
- <span class="ma-cat-label">${MA_CATEGORY_LABELS[cat] || cat}</span>
10809
- <div class="ma-cat-line"></div>
10810
- <span class="ma-cat-count">${catConn}/${list.length}</span>
10811
- </div><div class="ma-tiles">`;
10812
- for (const p of list) {
10813
- const pid = p.ID || p.id;
10814
- const conn = maResolveConn(pid);
10815
- const connected = conn && conn.status === 'active';
10816
- const expired = conn && conn.status !== 'active';
10817
- const cls = connected ? 'connected' : expired ? 'expired' : '';
10818
- html += `<div class="ma-tile ${cls}" data-pid="${esc(pid)}" onclick="maOpenModal(this.dataset.pid)">
10819
- <span class="ma-tile-icon">${esc(p.IconEmoji || p.iconEmoji || '🔌')}</span>
10820
- <span class="ma-tile-name">${esc(p.Name || p.name || pid)}</span>
10821
- <div style="display:flex;align-items:center;gap:4px">
10822
- <span class="ma-tile-dot"></span>
10823
- ${conn ? `<span class="ma-tile-acct">${esc(conn.account)}</span>` : ''}
10824
- </div>
10825
- </div>`;
10826
- }
10827
- html += `</div>`;
10828
- }
10829
-
10830
- grid.innerHTML = html;
10831
- }
10832
-
10833
- function maOpenModal(pid) {
10834
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10835
- if (!p) return;
10836
- _maSelected = p;
10837
- maRenderModal(p, maResolveConn(pid), []);
10838
- }
10839
-
10840
- function maRenderModal(p, conn, steps, selMethod, fields, state) {
10841
- const pid = p.ID || p.id;
10842
- const name = p.Name || p.name || pid;
10843
- const emoji = p.IconEmoji || p.iconEmoji || '🔌';
10844
- const methods = (p.Methods || p.methods || []);
10845
- const method = selMethod || methods[0] || '';
10846
- const isBrowser = method === 'browser';
10847
- const connected = conn && conn.status === 'active';
10848
- const expired = conn && conn.status !== 'active' && conn;
10849
- state = state || {};
10850
-
10851
- const existing = document.getElementById('ma-modal-overlay');
10852
- if (existing) existing.remove();
10853
-
10854
- const overlay = document.createElement('div');
10855
- overlay.id = 'ma-modal-overlay';
10856
- overlay.className = 'ma-modal-bg';
10857
- overlay.addEventListener('click', e => { if (e.target === overlay) maCloseModal(); });
10858
-
10859
- let bodyHtml = '';
10860
-
10861
- if (conn) {
10862
- // Connected state
10863
- const statusColor = connected ? 'var(--green)' : '#fbbf24';
10864
- bodyHtml = `
10865
- <div class="ma-modal-info">
10866
- ${[['Account', conn.account], ['Method', conn.method], ['Status', conn.status]].map(([l,v]) =>
10867
- `<div class="ma-modal-row"><span class="ma-modal-lbl">${l}</span><span class="ma-modal-val ${v==='active'?'ok':v==='expired'?'warn':''}">${esc(v)}</span></div>`
10868
- ).join('')}
10869
- ${conn.lastTested ? `<div class="ma-modal-row"><span class="ma-modal-lbl">Last tested</span><span class="ma-modal-val">${esc(relativeTime(new Date(conn.lastTested).getTime()))}</span></div>` : ''}
10870
- </div>
10871
- ${state.testMsg ? `<div class="ma-msg ${state.testOk ? 'ok' : 'err'}">${esc(state.testMsg)}</div>` : ''}
10872
- ${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}
10873
- ${steps.length ? `<div class="ma-steps">${steps.map(s=>`<div class="ma-step"><span class="ma-step-dot ${s.kind}"></span><span class="ma-step-txt ${s.kind}">${esc(s.msg)}</span></div>`).join('')}</div>` : ''}
10874
- <div class="ma-btns">
10875
- <button class="ma-btn ma-btn-secondary" id="ma-test-btn" onclick="maTest('${pid}','${conn.id}')">Test</button>
10876
- ${expired ? `<button class="ma-btn ma-btn-primary" onclick="maStartLogin('${pid}')">Log in again</button>` : ''}
10877
- ${state.confirmDisc ? `
10878
- <button class="ma-btn ma-btn-danger" onclick="maDisconnect('${pid}','${conn.id}','${conn._type}')">Yes, disconnect</button>
10879
- <button class="ma-btn ma-btn-ghost" data-pid="${esc(pid)}" onclick="maOpenModal(this.dataset.pid)">Cancel</button>
10880
- ` : `
10881
- <button class="ma-btn ma-btn-danger" onclick="maConfirmDisconnect('${pid}')">Disconnect</button>
10882
- <button class="ma-btn ma-btn-ghost" onclick="maCloseModal()">Close</button>
10883
- `}
10884
- </div>`;
10885
- } else {
10886
- // Not connected — show method tabs + connect form
10887
- const methodTabsHtml = methods.length > 1 ? `<div class="ma-method-tabs">${methods.map(m =>
10888
- `<button class="ma-method-tab ${m===method?'active':''}" data-pid="${esc(p.ID||p.id)}" data-method="${esc(m)}" onclick="maRenderModal(_maPlatforms.find(x=>(x.ID||x.id)===this.dataset.pid),null,[],this.dataset.method,{},{})">${esc(m)}</button>`
10889
- ).join('')}</div>` : '';
10890
-
10891
- let formHtml = '';
10892
- if (isBrowser) {
10893
- formHtml = `<div class="ma-browser-hint">A browser window will open. Log in to ${esc(name)} and the session will be saved automatically.</div>
10894
- ${steps.length ? `<div class="ma-steps">${steps.map(s=>`<div class="ma-step"><span class="ma-step-dot ${s.kind}"></span><span class="ma-step-txt ${s.kind}">${esc(s.msg)}</span></div>`).join('')}</div>` : ''}
10895
- ${state.running ? `<div style="display:flex;align-items:center;gap:8px;font-size:11px;color:var(--text-xs)"><span class="spinner" style="width:10px;height:10px;border-width:2px"></span>Waiting for login…</div>` : ''}
10896
- ${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}`;
10897
- } else {
10898
- const fieldDefs = (p.Fields || p.fields || {})[method] || [];
10899
- formHtml = fieldDefs.map(f => {
10900
- const key = f.Key || f.key;
10901
- const label = f.Label || f.label || key;
10902
- const secret = f.Secret || f.secret;
10903
- const req = f.Required || f.required;
10904
- const val = (fields || {})[key] || '';
10905
- return `<div>
10906
- <div class="ma-field-label">${esc(label)}${req ? ' <span style="color:oklch(60% 0.18 25)">*</span>' : ''}</div>
10907
- <input class="ma-field-input" type="${secret ? 'password' : 'text'}" data-key="${esc(key)}" value="${esc(val)}" autocomplete="${secret ? 'new-password' : 'off'}" placeholder="${esc(label)}">
10908
- ${f.HelpURL || f.helpURL ? `<a href="#" style="font-size:9.5px;color:var(--accent);text-decoration:none" onclick="event.preventDefault()">How to get this ↗</a>` : ''}
10909
- </div>`;
10910
- }).join('') || `<div class="ma-browser-hint">No configuration required.</div>`;
10911
- formHtml += `${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}
10912
- ${state.ok ? `<div class="ma-msg ok">Connected!</div>` : ''}`;
10913
- }
10914
-
10915
- const connectBtn = isBrowser
10916
- ? `<button class="ma-btn ma-btn-primary" id="ma-conn-btn" onclick="maStartLogin('${pid}')">Connect ${esc(name)}</button>`
10917
- : `<button class="ma-btn ma-btn-primary" id="ma-conn-btn" data-pid="${esc(pid)}" data-method="${esc(method)}" onclick="maConnectApiKey(this.dataset.pid,this.dataset.method)">Connect</button>`;
10918
-
10919
- bodyHtml = `
10920
- ${methodTabsHtml}
10921
- ${formHtml}
10922
- <div class="ma-btns">
10923
- ${state.ok ? '' : connectBtn}
10924
- <button class="ma-btn ma-btn-ghost" onclick="loadPlatformSessions();maCloseModal()">Refresh</button>
10925
- <button class="ma-btn ma-btn-ghost" onclick="maCloseModal()">Close</button>
10926
- </div>`;
10927
- }
10928
-
10929
- overlay.innerHTML = `<div class="ma-modal">
10930
- <div class="ma-modal-hdr">
10931
- <div class="ma-modal-hdr-left">
10932
- <span style="font-size:26px">${esc(emoji)}</span>
10933
- <div>
10934
- <div class="ma-modal-title">${esc(name)}</div>
10935
- <div style="display:flex;align-items:center;gap:5px;margin-top:2px">
10936
- <span style="width:6px;height:6px;border-radius:50%;background:${connected?'var(--green)':expired?'#fbbf24':'var(--border)'}"></span>
10937
- <span class="ma-modal-status" style="color:${connected?'var(--green)':expired?'#fbbf24':'var(--text-xs)'}">${connected?'Connected':expired?'Session expired':'Not connected'}</span>
10938
- </div>
10939
- </div>
10940
- </div>
10941
- <button class="ma-btn ma-btn-ghost" style="padding:4px 8px" onclick="maCloseModal()">✕</button>
10942
- </div>
10943
- <div class="ma-modal-body">${bodyHtml}</div>
10944
- </div>`;
10945
-
10946
- document.body.appendChild(overlay);
10947
- }
10948
-
10949
- function maCloseModal() {
10950
- if (_maLoginPoll) { clearInterval(_maLoginPoll); _maLoginPoll = null; }
10951
- const el = document.getElementById('ma-modal-overlay');
10952
- if (el) el.remove();
10953
- _maSelected = null;
10954
- }
10955
-
10956
- function maConfirmDisconnect(pid) {
10957
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10958
- const conn = maResolveConn(pid);
10959
- if (p && conn) maRenderModal(p, conn, [], null, {}, { confirmDisc: true });
10960
- }
10961
-
10962
- async function maStartLogin(pid) {
10963
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10964
- if (!p) return;
10965
- const steps = [{ msg: 'Opening browser…', kind: '' }];
10966
- maRenderModal(p, null, steps, 'browser', {}, { running: true });
10967
- try {
10968
- const r = await fetch('/api/monoagent/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: pid }) });
10969
- if (!r.ok) throw new Error('HTTP ' + r.status);
10970
- steps.push({ msg: 'Browser open — log in then come back', kind: 'ok' });
10971
- maRenderModal(p, null, steps, 'browser', {}, { running: false });
10972
- showToast('Browser opened', 'Log in to ' + (p.Name || pid) + ' and hit Refresh', 'ok');
10973
- // Poll for completion
10974
- let polls = 0;
10975
- if (_maLoginPoll) clearInterval(_maLoginPoll);
10976
- _maLoginPoll = setInterval(async () => {
10977
- polls++;
10978
- const d = await fetch('/api/monoagent/connections').then(r => r.json()).catch(() => null);
10979
- if (d) { _maConns = d.connections || []; _maSessions = d.sessions || []; }
10980
- const conn = maResolveConn(pid);
10981
- if (conn || polls >= 18) {
10982
- clearInterval(_maLoginPoll); _maLoginPoll = null;
10983
- if (conn) { renderSystems(); maOpenModal(pid); showToast('Connected', (p.Name||pid) + ' session saved', 'ok'); }
10984
- else { maRenderModal(p, null, [], 'browser', {}, { err: 'Login timed out. Close the browser tab and try again.' }); }
10985
- }
10986
- }, 5000);
10987
- } catch (e) {
10988
- maRenderModal(p, null, [], 'browser', {}, { err: e.message });
10989
- }
10990
- }
10991
-
10992
- async function maConnectApiKey(pid, method) {
10993
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10994
- if (!p) return;
10995
- const modal = document.getElementById('ma-modal-overlay');
10996
- const inputs = modal ? modal.querySelectorAll('.ma-field-input') : [];
10997
- const fields = {};
10998
- inputs.forEach(inp => { fields[inp.dataset.key] = inp.value.trim(); });
10999
- const btn = document.getElementById('ma-conn-btn');
11000
- if (btn) { btn.disabled = true; btn.textContent = 'Connecting…'; }
11001
- try {
11002
- const r = await fetch('/api/monoagent/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: pid, method, fields }) });
11003
- const d = await r.json();
11004
- if (d.ok) {
11005
- await loadPlatformSessions();
11006
- maRenderModal(p, maResolveConn(pid), [], method, fields, { ok: true });
11007
- showToast('Connected', (p.Name || pid) + ' connected', 'ok');
11008
- } else {
11009
- maRenderModal(p, null, [], method, fields, { err: d.stderr || d.error || 'Connection failed' });
11010
- }
11011
- } catch (e) {
11012
- maRenderModal(p, null, [], method, fields, { err: e.message });
11013
- }
11014
- }
11015
-
11016
- async function maTest(pid, connId) {
11017
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
11018
- const conn = maResolveConn(pid);
11019
- if (!p || !conn) return;
11020
- const btn = document.getElementById('ma-test-btn');
11021
- if (btn) { btn.disabled = true; btn.textContent = 'Testing…'; }
11022
- try {
11023
- const d = await fetch('/api/monoagent/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: connId }) }).then(r => r.json());
11024
- maRenderModal(p, conn, [], null, {}, { testOk: d.ok, testMsg: d.ok ? '✓ Connection OK' : ('✗ ' + (d.error || 'Test failed')) });
11025
- } catch (e) {
11026
- maRenderModal(p, conn, [], null, {}, { testMsg: '✗ ' + e.message, testOk: false });
11027
- }
11028
- }
11029
-
11030
- async function maDisconnect(pid, connId, type) {
11031
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
11032
- if (!p) return;
11033
- try {
11034
- await fetch('/api/monoagent/disconnect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: connId, type }) });
11035
- await loadPlatformSessions();
11036
- maCloseModal();
11037
- showToast('Disconnected', (p.Name || pid) + ' removed', 'ok');
11038
- } catch (e) {
11039
- showToast('Error', e.message, 'err');
11040
- }
11041
- }
11042
-
11043
10872
  let _wfRuns = [];
11044
10873
  async function loadWorkflowRuns() {
11045
10874
  const runsListEl = document.getElementById('wf-runs-list');
@@ -11541,7 +11370,7 @@ function pbRenderPalette() {
11541
11370
  <span class="pb-cat-arrow">▸</span><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${catColor};margin-right:5px;vertical-align:middle;flex-shrink:0"></span>${cat}
11542
11371
  </div>
11543
11372
  <div class="pb-cat-body ${cat === cats[0] ? '' : 'collapsed'}">
11544
- ${nodes.map((n,i) => `<button class="pb-node-btn" onclick="pbAddNode('${esc(n.type)}',PB_NODE_DEFS.filter(d=>d.cat==='${cat}')[${i}])" title="${esc(n.type)}${n.defaultConfig ? ' (op: '+n.defaultConfig.operation+')' : ''}">
11373
+ ${nodes.map((n,i) => `<button class="pb-node-btn" onclick="pbAddNode('${esc(String(n.type).replace(/['\\]/g,'\\$&'))}',PB_NODE_DEFS.filter(d=>d.cat==='${cat}')[${i}])" title="${esc(n.type)}${n.defaultConfig ? ' (op: '+n.defaultConfig.operation+')' : ''}">
11545
11374
  <span class="pb-node-icon">${n.icon}</span>${esc(n.label)}
11546
11375
  </button>`).join('')}
11547
11376
  </div>
@@ -11618,13 +11447,13 @@ function pbRenderCanvas() {
11618
11447
  <div class="pb-node-hdr">
11619
11448
  <span class="pb-node-type-icon">${def.icon}</span>
11620
11449
  <span class="pb-node-title" title="${esc(node.type)}">${esc(node.name || node.type)}</span>
11621
- <button class="pb-node-del" onclick="pbDeleteNode('${esc(node.id)}')" title="Remove">✕</button>
11450
+ <button class="pb-node-del" onclick="pbDeleteNode('${esc(String(node.id).replace(/['\\]/g,'\\$&'))}')" title="Remove">✕</button>
11622
11451
  </div>
11623
11452
  <div class="pb-node-body">
11624
11453
  ${st ? `<div class="pb-node-status"><div class="pb-ns-dot ${st}"></div><span style="color:${st==='running'?'var(--accent)':st==='completed'?'var(--green)':'var(--red)'}">${st}</span></div>` : ''}
11625
11454
  <div style="font-size:10px;color:var(--text-xs)">${esc(node.type)}</div>
11626
11455
  </div>
11627
- <button class="pb-node-conn-btn" title="Connect to another node" onclick="pbStartConnect('${esc(node.id)}',event)">→</button>
11456
+ <button class="pb-node-conn-btn" title="Connect to another node" onclick="pbStartConnect('${esc(String(node.id).replace(/['\\]/g,'\\$&'))}',event)">→</button>
11628
11457
  `;
11629
11458
  card.addEventListener('mousedown', (e) => pbDragStart(node.id, e));
11630
11459
  card.addEventListener('click', (e) => { if (!e.defaultPrevented) pbSelectNode(node.id); });
@@ -12268,12 +12097,12 @@ function mgRenderAnalyzeButtons() {
12268
12097
  const cEl = document.getElementById('mg-client-btns');
12269
12098
  const sEl = document.getElementById('mg-server-btns');
12270
12099
  cEl.innerHTML = MG_CLIENT_ANALYSES.map(a => `
12271
- <div class="mg-analyze-btn" id="mg-abtn-${esc(a.id)}" onclick="mgRunClient('${esc(a.id)}')">
12100
+ <div class="mg-analyze-btn" id="mg-abtn-${esc(a.id)}" onclick="mgRunClient('${esc(String(a.id).replace(/['\\]/g,'\\$&'))}')">
12272
12101
  <div class="mab-name">${esc(a.label)}</div>
12273
12102
  <div class="mab-desc">${esc(a.desc)}</div>
12274
12103
  </div>`).join('');
12275
12104
  sEl.innerHTML = MG_SERVER_ANALYSES.map(a => `
12276
- <div class="mg-analyze-btn mg-server" id="mg-abtn-${esc(a.id)}" onclick="mgRunServer('${esc(a.id)}')">
12105
+ <div class="mg-analyze-btn mg-server" id="mg-abtn-${esc(a.id)}" onclick="mgRunServer('${esc(String(a.id).replace(/['\\]/g,'\\$&'))}')">
12277
12106
  <div class="mab-name mg">${esc(a.label)}</div>
12278
12107
  <div class="mab-desc">${esc(a.desc)}</div>
12279
12108
  </div>`).join('');
@@ -12964,7 +12793,7 @@ function mgRenderExport() {
12964
12793
  <div class="mg-export-card">
12965
12794
  <div class="mec-name">${esc(x.name)}</div>
12966
12795
  <div class="mec-desc">${esc(x.desc)}</div>
12967
- <button class="btn" onclick="mgExport('${esc(x.id)}')" style="margin-top:auto">EXPORT</button>
12796
+ <button class="btn" onclick="mgExport('${esc(String(x.id).replace(/['\\]/g,'\\$&'))}')" style="margin-top:auto">EXPORT</button>
12968
12797
  </div>`).join('');
12969
12798
  }
12970
12799
 
@@ -13241,7 +13070,7 @@ function mgRenderWiki() {
13241
13070
  const types = [...new Set(nodes.map(n => n.type || n.kind || 'unknown'))].sort();
13242
13071
  const pillsEl = document.getElementById('mg-wiki-pills');
13243
13072
  pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" onclick="mgWikiFilterType('all')">All</span>` +
13244
- types.map(t => `<span class="mg-pill" data-type="${esc(t)}" onclick="mgWikiFilterType('${esc(t)}')">${esc(t)}</span>`).join('');
13073
+ types.map(t => `<span class="mg-pill" data-type="${esc(t)}" onclick="mgWikiFilterType('${esc(String(t).replace(/['\\]/g,'\\$&'))}')">${esc(t)}</span>`).join('');
13245
13074
 
13246
13075
  mgRenderWikiList('');
13247
13076
  }
@@ -14247,64 +14076,6 @@ async function mmRenderGraph(body) {
14247
14076
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
14248
14077
  }
14249
14078
 
14250
- /* ═══════════════════════════════════════════════════════════════
14251
- MONOAGENT TAB
14252
- ═══════════════════════════════════════════════════════════════ */
14253
- const MA_STATE_COLOR = { completed:'#22c55e', failed:'#ef4444', running:'#3b82f6', pending:'#a3a3a3' };
14254
- let _maWorkflows = [], _maActions = [];
14255
-
14256
- function maSwitchTab(tab) {
14257
- document.querySelectorAll('[data-matab]').forEach(b => b.classList.toggle('active', b.dataset.matab === tab));
14258
- document.querySelectorAll('#view-monoagent .wf-pane').forEach(p => p.classList.toggle('active', p.id === 'ma-tab-' + tab));
14259
- }
14260
-
14261
- async function loadMonoagentData() {
14262
- document.getElementById('ma-workflows-list').innerHTML = '<div class="loading-txt">Loading…</div>';
14263
- document.getElementById('ma-history-list').innerHTML = '<div class="loading-txt">Loading…</div>';
14264
- try {
14265
- const data = await apiFetch('/api/monoagent/data');
14266
- _maWorkflows = Array.isArray(data.workflows) ? data.workflows : [];
14267
- _maActions = Array.isArray(data.actions) ? data.actions : [];
14268
- } catch (e) {
14269
- _maWorkflows = []; _maActions = [];
14270
- }
14271
- const bdg = document.getElementById('bdg-monoagent');
14272
- if (bdg) bdg.textContent = _maWorkflows.length || '—';
14273
- renderMonoagentWorkflows();
14274
- renderMonoagentHistory();
14275
- }
14276
-
14277
- function renderMonoagentWorkflows() {
14278
- const el = document.getElementById('ma-workflows-list');
14279
- if (!el) return;
14280
- if (!_maWorkflows.length) { el.innerHTML = '<div class="empty">No monoagent workflows found. Create one with: <code>monoagent workflow create</code></div>'; return; }
14281
- el.innerHTML = _maWorkflows.map(w => `<div class="m-card" style="margin-bottom:10px;padding:12px 14px">
14282
- <div style="display:flex;justify-content:space-between;align-items:center">
14283
- <span style="font-weight:600;color:var(--text-hi);font-family:var(--mono);font-size:13px">${esc(w.name||w.id||'—')}</span>
14284
- <span style="font-size:11px;color:var(--text-lo)">${esc(w.id||'')}</span>
14285
- </div>
14286
- ${w.description ? `<div style="font-size:12px;color:var(--text-lo);margin-top:4px">${esc(w.description)}</div>` : ''}
14287
- <div style="font-size:11px;color:var(--text-xs);margin-top:6px">${(w.nodes||[]).length} nodes · trigger: ${esc(w.trigger||'manual')}</div>
14288
- </div>`).join('');
14289
- }
14290
-
14291
- function renderMonoagentHistory() {
14292
- const el = document.getElementById('ma-history-list');
14293
- if (!el) return;
14294
- if (!_maActions.length) { el.innerHTML = '<div class="empty">No action history yet.</div>'; return; }
14295
- el.innerHTML = _maActions.map(a => {
14296
- const color = MA_STATE_COLOR[a.status] || MA_STATE_COLOR.pending;
14297
- const ts = a.createdAt ? new Date(a.createdAt).toLocaleString() : '—';
14298
- return `<div class="m-card" style="margin-bottom:8px;padding:10px 14px;display:flex;justify-content:space-between;align-items:center">
14299
- <div>
14300
- <span style="font-weight:600;color:var(--text-hi);font-size:12px;font-family:var(--mono)">${esc(a.type||a.action||'—')}</span>
14301
- <span style="font-size:11px;color:var(--text-lo);margin-left:8px">${esc(a.platform||'')}</span>
14302
- <div style="font-size:11px;color:var(--text-xs);margin-top:2px">${ts}</div>
14303
- </div>
14304
- <span style="font-size:11px;font-weight:600;color:${color}">${esc(a.status||'—')}</span>
14305
- </div>`;
14306
- }).join('');
14307
- }
14308
14079
  </script>
14309
14080
  </body>
14310
14081
  </html>