monomind 2.1.0 → 2.1.4

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 (57) hide show
  1. package/README.md +77 -94
  2. package/package.json +3 -3
  3. package/packages/@monomind/cli/.claude/commands/mastermind/createorg.md +8 -9
  4. package/packages/@monomind/cli/.claude/helpers/handlers/capture-handler.cjs +24 -10
  5. package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +2 -2
  6. package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +29 -0
  7. package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +35 -23
  8. package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +21 -0
  9. package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +73 -2
  10. package/packages/@monomind/cli/.claude/skills/mastermind-skills/createorg.md +91 -701
  11. package/packages/@monomind/cli/.claude/skills/mastermind-skills/org-settings.md +46 -47
  12. package/packages/@monomind/cli/.claude/skills/mastermind-skills/runorg.md +12 -1
  13. package/packages/@monomind/cli/README.md +77 -94
  14. package/packages/@monomind/cli/dist/src/browser/workflow/store.d.ts +2 -2
  15. package/packages/@monomind/cli/dist/src/commands/browse.d.ts +1 -1
  16. package/packages/@monomind/cli/dist/src/commands/doctor-env-checks.js +5 -0
  17. package/packages/@monomind/cli/dist/src/commands/org.js +46 -29
  18. package/packages/@monomind/cli/dist/src/commands/platforms.d.ts +1 -1
  19. package/packages/@monomind/cli/dist/src/init/executor.js +30 -6
  20. package/packages/@monomind/cli/dist/src/mcp-tools/auto-install.d.ts +8 -8
  21. package/packages/@monomind/cli/dist/src/mcp-tools/coherence/types.d.ts +31 -31
  22. package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +5 -1
  23. package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +28 -3
  24. package/packages/@monomind/cli/dist/src/mcp-tools/quality/coverage-analysis/prioritize-gaps.d.ts +36 -36
  25. package/packages/@monomind/cli/dist/src/mcp-tools/quality/security-compliance/detect-secrets.d.ts +4 -4
  26. package/packages/@monomind/cli/dist/src/memory/ewc-consolidation.d.ts +12 -0
  27. package/packages/@monomind/cli/dist/src/memory/sona-optimizer.d.ts +12 -0
  28. package/packages/@monomind/cli/dist/src/orgrt/broker.d.ts +3 -1
  29. package/packages/@monomind/cli/dist/src/orgrt/broker.js +8 -3
  30. package/packages/@monomind/cli/dist/src/orgrt/bus.d.ts +5 -1
  31. package/packages/@monomind/cli/dist/src/orgrt/bus.js +5 -1
  32. package/packages/@monomind/cli/dist/src/orgrt/daemon.d.ts +35 -1
  33. package/packages/@monomind/cli/dist/src/orgrt/daemon.js +190 -21
  34. package/packages/@monomind/cli/dist/src/orgrt/forwarder.js +47 -10
  35. package/packages/@monomind/cli/dist/src/orgrt/inbox.d.ts +11 -0
  36. package/packages/@monomind/cli/dist/src/orgrt/inbox.js +56 -0
  37. package/packages/@monomind/cli/dist/src/orgrt/policy.d.ts +5 -1
  38. package/packages/@monomind/cli/dist/src/orgrt/policy.js +58 -5
  39. package/packages/@monomind/cli/dist/src/orgrt/server.d.ts +3 -2
  40. package/packages/@monomind/cli/dist/src/orgrt/server.js +31 -40
  41. package/packages/@monomind/cli/dist/src/orgrt/session.d.ts +11 -1
  42. package/packages/@monomind/cli/dist/src/orgrt/session.js +32 -1
  43. package/packages/@monomind/cli/dist/src/orgrt/test-loop.js +8 -8
  44. package/packages/@monomind/cli/dist/src/orgrt/types.d.ts +53 -53
  45. package/packages/@monomind/cli/dist/src/output.d.ts +29 -29
  46. package/packages/@monomind/cli/dist/src/output.js +10 -2
  47. package/packages/@monomind/cli/dist/src/pricing/model-pricing.d.ts +1 -1
  48. package/packages/@monomind/cli/dist/src/types.d.ts +2 -2
  49. package/packages/@monomind/cli/dist/src/ui/collector.mjs +20 -5
  50. package/packages/@monomind/cli/dist/src/ui/dashboard.html +465 -500
  51. package/packages/@monomind/cli/dist/src/ui/orgs-files.js +192 -0
  52. package/packages/@monomind/cli/dist/src/ui/orgs.html +21 -52
  53. package/packages/@monomind/cli/dist/src/ui/server.mjs +174 -164
  54. package/packages/@monomind/cli/package.json +12 -16
  55. package/packages/@monomind/cli/scripts/publish.sh +6 -0
  56. package/scripts/generate-agent-avatars.mjs +3 -3
  57. package/packages/@monomind/cli/dist/src/orgrt/live.html +0 -56
@@ -428,6 +428,23 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
428
428
  /* SVG org chart */
429
429
  .org-chart-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; margin-bottom: 12px; }
430
430
  .org-chart-svg { width: 100%; display: block; }
431
+ .v2-bubble-inner {
432
+ background: oklch(18% 0.01 55 / 0.95);
433
+ border: 1px solid oklch(72% 0.18 75 / 0.4);
434
+ border-radius: 8px;
435
+ padding: 4px 8px;
436
+ font: 500 9.5px 'Inter', system-ui, sans-serif;
437
+ color: oklch(88% 0.01 75);
438
+ text-align: center;
439
+ white-space: nowrap;
440
+ overflow: hidden;
441
+ text-overflow: ellipsis;
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
+ }
431
448
  .org-chart-legend { display: flex; gap: 14px; flex-wrap: wrap; padding: 0 2px; }
432
449
  .ocl-item { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--text-lo); }
433
450
 
@@ -1414,67 +1431,6 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
1414
1431
  select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1415
1432
  @keyframes pb-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
1416
1433
 
1417
- /* ── systems grid ──────────────────────────────────────────── */
1418
- #wf-systems-grid { display: block; }
1419
- /* ── monoagent connections tile grid ─────────────────────────── */
1420
- .ma-cat-hdr { display:flex; align-items:center; gap:10px; margin-bottom:10px; }
1421
- .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; }
1422
- .ma-cat-line { flex:1; height:1px; background:var(--border); }
1423
- .ma-cat-count { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); }
1424
- .ma-tiles { display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:8px; margin-bottom:24px; }
1425
- .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; }
1426
- .ma-tile:hover { border-color:var(--border-hi); }
1427
- .ma-tile.connected { border-color:oklch(65% 0.15 150 / 0.5); box-shadow:0 0 10px oklch(65% 0.15 150 / 0.12); }
1428
- .ma-tile-icon { font-size:22px; line-height:1; }
1429
- .ma-tile-name { font-family:var(--mono); font-size:10px; font-weight:600; color:var(--text-mid); text-align:center; line-height:1.3; }
1430
- .ma-tile-dot { width:6px; height:6px; border-radius:50%; background:var(--border); }
1431
- .ma-tile.connected .ma-tile-dot { background:var(--green); box-shadow:0 0 5px oklch(65% 0.15 150 / 0.7); }
1432
- .ma-tile.expired .ma-tile-dot { background:#fbbf24; box-shadow:0 0 5px rgba(251,191,36,0.5); }
1433
- .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; }
1434
- /* modal */
1435
- .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; }
1436
- .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; }
1437
- .ma-modal-hdr { display:flex; align-items:center; justify-content:space-between; padding:16px 20px 12px; border-bottom:1px solid var(--border); }
1438
- .ma-modal-hdr-left { display:flex; align-items:center; gap:10px; }
1439
- .ma-modal-title { font-family:var(--mono); font-size:13px; font-weight:700; color:var(--text-hi); }
1440
- .ma-modal-status { font-family:var(--mono); font-size:10px; text-transform:uppercase; letter-spacing:1px; }
1441
- .ma-modal-body { padding:16px 20px 20px; display:flex; flex-direction:column; gap:14px; }
1442
- .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; }
1443
- .ma-modal-row { display:flex; justify-content:space-between; align-items:center; }
1444
- .ma-modal-lbl { font-family:var(--mono); font-size:9.5px; color:var(--text-xs); text-transform:uppercase; letter-spacing:1px; }
1445
- .ma-modal-val { font-family:var(--mono); font-size:11px; color:var(--text-mid); }
1446
- .ma-modal-val.ok { color:var(--green); }
1447
- .ma-modal-val.warn { color:#fbbf24; }
1448
- .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; }
1449
- .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; }
1450
- .ma-field-input:focus { border-color:var(--accent); }
1451
- .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; }
1452
- .ma-step { display:flex; align-items:center; gap:8px; }
1453
- .ma-step-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; }
1454
- .ma-step-dot.ok { background:var(--green); }
1455
- .ma-step-dot.err { background:oklch(60% 0.18 25); }
1456
- .ma-step-txt { font-family:var(--mono); font-size:10px; color:var(--text-lo); }
1457
- .ma-step-txt.ok { color:var(--green); }
1458
- .ma-step-txt.err { color:oklch(60% 0.18 25); }
1459
- .ma-btns { display:flex; gap:8px; flex-wrap:wrap; }
1460
- .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; }
1461
- .ma-btn:disabled { opacity:0.45; cursor:default; }
1462
- .ma-btn-primary { background:var(--accent); color:#fff; }
1463
- .ma-btn-primary:hover:not(:disabled) { opacity:0.85; }
1464
- .ma-btn-secondary { background:var(--surface-hi); color:var(--text-mid); border:1px solid var(--border-hi); }
1465
- .ma-btn-secondary:hover:not(:disabled) { border-color:var(--accent); color:var(--accent); }
1466
- .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); }
1467
- .ma-btn-danger:hover:not(:disabled) { opacity:0.85; }
1468
- .ma-btn-ghost { background:none; color:var(--text-lo); border:1px solid var(--border); }
1469
- .ma-btn-ghost:hover:not(:disabled) { color:var(--text-mid); }
1470
- .ma-msg { font-family:var(--mono); font-size:11px; padding:7px 12px; border-radius:6px; }
1471
- .ma-msg.ok { background:oklch(65% 0.15 150 / 0.08); border:1px solid oklch(65% 0.15 150 / 0.25); color:var(--green); }
1472
- .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); }
1473
- .ma-method-tabs { display:flex; gap:6px; flex-wrap:wrap; }
1474
- .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; }
1475
- .ma-method-tab.active { background:var(--accent-dim); border-color:var(--accent); color:var(--accent); }
1476
- .ma-browser-hint { font-size:11px; color:var(--text-lo); line-height:1.5; }
1477
-
1478
1434
  /* ── playbook runs view ────────────────────────────────────── */
1479
1435
  .wf-run-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 10px; }
1480
1436
  .wf-run-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
@@ -1578,10 +1534,6 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1578
1534
  </div>
1579
1535
  <div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
1580
1536
  <div class="nav-sect" style="margin-top:auto;padding-top:8px;">
1581
- <div class="nav-item" data-view="monoagent">
1582
- <span class="ico">🤖</span><span class="lbl">MonoAgent</span>
1583
- <span class="bdg" id="bdg-monoagent">—</span>
1584
- </div>
1585
1537
  <div class="nav-item" data-view="global">
1586
1538
  <span class="ico">⊕</span><span class="lbl">Global Feed</span>
1587
1539
  </div>
@@ -1595,6 +1547,10 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
1595
1547
  <div class="nav-item" data-view="chat" title="All agent sessions across projects">
1596
1548
  <span class="ico">⌘</span><span class="lbl">Global Agent Chat</span>
1597
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>
1598
1554
  </div>
1599
1555
  </div>
1600
1556
  <div id="sb-footer">
@@ -2250,30 +2206,6 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2250
2206
  </div>
2251
2207
  </div>
2252
2208
 
2253
- <!-- MONOAGENT -->
2254
- <div class="view" id="view-monoagent">
2255
- <div class="vscroll">
2256
- <div class="pg-title">MonoAgent</div>
2257
- <div class="pg-sub">Social media automation workflows &amp; run history</div>
2258
- <div class="mg-tab-bar" style="margin-top:14px">
2259
- <button class="odt-btn active" data-matab="workflows" onclick="maSwitchTab('workflows')">Workflows</button>
2260
- <button class="odt-btn" data-matab="history" onclick="maSwitchTab('history')">Action History</button>
2261
- </div>
2262
- <div class="wf-pane active" id="ma-tab-workflows">
2263
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;margin-top:4px">
2264
- <button class="btn" onclick="loadMonoagentData()">↺ Refresh</button>
2265
- </div>
2266
- <div id="ma-workflows-list"><div class="loading-txt">Loading…</div></div>
2267
- </div>
2268
- <div class="wf-pane" id="ma-tab-history">
2269
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;margin-top:4px">
2270
- <button class="btn" onclick="loadMonoagentData()">↺ Refresh</button>
2271
- </div>
2272
- <div id="ma-history-list"><div class="loading-txt">Loading…</div></div>
2273
- </div>
2274
- </div>
2275
- </div>
2276
-
2277
2209
  <!-- GLOBAL FEED -->
2278
2210
  <div class="view" id="view-global">
2279
2211
  <div class="vscroll">
@@ -2327,6 +2259,17 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2327
2259
  </div>
2328
2260
  </div>
2329
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
+
2330
2273
  </div><!-- /view-wrap -->
2331
2274
  </div><!-- /main -->
2332
2275
  <div id="app-ambient-hint">Press A to exit ambient mode</div>
@@ -2427,6 +2370,27 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
2427
2370
  </div>
2428
2371
 
2429
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
+
2430
2394
  // ── state ──────────────────────────────────────────────────
2431
2395
  let DIR = '';
2432
2396
  let ORIGINAL_DIR = '';
@@ -2465,10 +2429,10 @@ function switchView(v, { updateHash = true } = {}) {
2465
2429
  el.classList.toggle('active', el.dataset.view === v));
2466
2430
  document.querySelectorAll('.view').forEach(el =>
2467
2431
  el.classList.toggle('active', el.id === 'view-' + v));
2468
- 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' };
2469
2433
  document.getElementById('view-title').textContent = titles[v] || v;
2470
2434
  const PROJECT = DIR ? shortPath(DIR) : 'monomind';
2471
- 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' };
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' };
2472
2436
  document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
2473
2437
  if (updateHash) _pushHash();
2474
2438
  // Projects always re-fetches so onclick paths in cards stay current
@@ -2528,10 +2492,10 @@ function renderView(v) {
2528
2492
  if (v === 'memory') renderMemory();
2529
2493
  if (v === 'orgs') renderOrgs();
2530
2494
  if (v === 'monograph') loadMonograph();
2531
- if (v === 'monoagent') { loadMonoagentData(); }
2532
2495
  if (v === 'global') renderGlobalFeed();
2533
2496
  if (v === 'global-loops') renderGlobalLoops();
2534
2497
  if (v === 'global-tokens') renderGlobalTokens();
2498
+ if (v === 'humaninput') renderHumanInputView();
2535
2499
  }
2536
2500
 
2537
2501
  function refreshCurrent() {
@@ -3658,7 +3622,7 @@ function renderProjectGrid(projects, query) {
3658
3622
  const isCurrent = p.path === DIR;
3659
3623
  const score = computeHealthScore(p);
3660
3624
  const hCls = healthClass(score);
3661
- 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,'\\$&'))}')">
3662
3626
  ${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
3663
3627
  <div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
3664
3628
  <div class="proj-card-name">${esc(p.name || p.slug)}</div>
@@ -3735,7 +3699,7 @@ async function renderSessions() {
3735
3699
  let anomBadge = '';
3736
3700
  if (medianCost > 0.05 && sCost > medianCost * 3 && sCost > 0.5) {
3737
3701
  // f60: anomaly badge is clickable for explainer
3738
- 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>`;
3739
3703
  } else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
3740
3704
  anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
3741
3705
  }
@@ -3763,7 +3727,7 @@ async function renderSessions() {
3763
3727
  </div>` : '';
3764
3728
  // f51: error badge — clickable if errors exist
3765
3729
  const errBadge = (s.errorCount > 0 && s.toolCalls > 0 && (s.errorCount / s.toolCalls) > 0.3)
3766
- ? `<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>`
3767
3731
  : anomBadge;
3768
3732
  // f50: custom tags
3769
3733
  const ctags = getCustomTags(s.id);
@@ -3774,12 +3738,12 @@ async function renderSessions() {
3774
3738
  const summaryHtml = s.summary
3775
3739
  ? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
3776
3740
  : '';
3777
- 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}'>
3778
3742
  <div class="sr-top">
3779
3743
  <div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
3780
3744
  <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${compactBadge}
3781
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>
3782
- <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>
3783
3747
  <span class="sr-view">→ view</span>
3784
3748
  </div>
3785
3749
  ${summaryHtml}
@@ -3791,7 +3755,7 @@ async function renderSessions() {
3791
3755
  ${ctxGauge}
3792
3756
  <div class="err-drawer" id="err-drawer-${esc(s.id)}"></div>
3793
3757
  <div class="sess-notes-wrap" onclick="event.stopPropagation()">
3794
- <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>
3795
3759
  <div class="sess-notes-area" id="snote-${esc(s.id)}">
3796
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>
3797
3761
  <div class="sess-note-saved"></div>
@@ -3918,7 +3882,7 @@ function buildTagFilterBar(sessions) {
3918
3882
  if (!allTags.common.size) return '';
3919
3883
  const sorted = [...allTags.common].sort();
3920
3884
  const chips = sorted.map(t =>
3921
- `<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>`
3922
3886
  ).join('');
3923
3887
  return `<div class="tag-filter-bar">${chips}</div>`;
3924
3888
  }
@@ -4169,7 +4133,7 @@ async function renderGlobalFeed() {
4169
4133
  const gfSummaryHtml = s.summary
4170
4134
  ? `<div class="sr-summary">${esc(s.summary.slice(0, 180))}</div>`
4171
4135
  : '';
4172
- 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)">
4173
4137
  <div class="sr-top">
4174
4138
  <div class="sr-prompt">${esc(s.lastPrompt || s.id?.slice(0,8) || '—')}</div>
4175
4139
  <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>${gfCompactBadge}
@@ -4184,6 +4148,73 @@ async function renderGlobalFeed() {
4184
4148
  }
4185
4149
  }
4186
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
+
4187
4218
  // ── agent chat view ────────────────────────────────────────
4188
4219
  let chatVSessions = {}; // sessionId or groupId → session/group object
4189
4220
  let chatVGroupMap = {}; // groupId → group object
@@ -5075,7 +5106,7 @@ function renderLeaderboard() {
5075
5106
  const cost = '$' + s.totalCost.toFixed(2);
5076
5107
  const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
5077
5108
  const prompt = s.lastPrompt || s.id;
5078
- return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
5109
+ return `<tr onclick="jumpToSession('${esc(String(s.id).replace(/['\\]/g,'\\$&'))}')" title="${esc(prompt)}">
5079
5110
  <td class="lb-rank">${i + 1}</td>
5080
5111
  <td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
5081
5112
  <td class="lb-cost">${cost}</td>
@@ -5325,7 +5356,7 @@ function buildSwimlane() {
5325
5356
  const color = `oklch(65% 0.14 ${hue})`;
5326
5357
  const name = (s.lastPrompt || s.id).slice(0, 16);
5327
5358
  return `<div class="sw-row">
5328
- <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>
5329
5360
  <div class="sw-track">
5330
5361
  <div class="sw-bar" style="left:${leftPct}%;width:${widthPct}%;background:${color}" title="${esc(name)} · ${fmtDur(dur)}"></div>
5331
5362
  </div>
@@ -5786,6 +5817,7 @@ let _v2SelOrg = null;
5786
5817
  let _v2OrgData = null;
5787
5818
  let _v2OrgTab = 'chart';
5788
5819
  const _v2OrgEventLog = {};
5820
+ let _v2ChartLayout = { pos: {}, edgeIdByPair: new Map() };
5789
5821
 
5790
5822
  async function renderOrgs() {
5791
5823
  try {
@@ -6204,6 +6236,9 @@ function v2RenderOrgChart() {
6204
6236
  <image href="${avatarSrc}" x="${-avR}" y="${-R + 2}" width="${avR * 2}" height="${avR * 2}" clip-path="url(#${nodeClipIds[i]})" preserveAspectRatio="xMidYMid meet" onerror="this.setAttribute('href','data/avatars/coder.svg')"/>
6205
6237
  <text text-anchor="middle" y="${nameY}" font-size="9" font-weight="${leader?'600':'500'}" font-family="'Inter',system-ui,sans-serif" fill="${color}">${esc(nameText)}</text>
6206
6238
  ${subTypeText ? `<text text-anchor="middle" y="${+nameY + 11}" font-size="7.5" font-family="'Inter',system-ui,sans-serif" fill="oklch(48% 0.005 75)">${esc(subTypeText)}</text>` : ''}
6239
+ <foreignObject class="v2-activity-bubble" data-bubble-for="${esc(role.id)}" x="-70" y="${-R - 46}" width="140" height="34" style="overflow:visible;pointer-events:none;visibility:hidden;opacity:0">
6240
+ <div class="v2-bubble-inner" xmlns="http://www.w3.org/1999/xhtml"></div>
6241
+ </foreignObject>
6207
6242
  </g>`;
6208
6243
  });
6209
6244
 
@@ -6230,6 +6265,7 @@ function v2RenderOrgChart() {
6230
6265
  <g id="v2oc-edges">${edgesHTML}</g>
6231
6266
  <g id="v2oc-nodes">${nodesHTML}</g>
6232
6267
  <g id="v2oc-particles" opacity="${isRunning ? 1 : 0}">${particlesHTML}</g>
6268
+ <g id="v2oc-letters"></g>
6233
6269
  </svg>
6234
6270
  </div>
6235
6271
  <div class="org-chart-legend" style="margin-top:10px">
@@ -6262,8 +6298,206 @@ function v2RenderOrgChart() {
6262
6298
  el.style.opacity = '0';
6263
6299
  });
6264
6300
  }
6301
+
6302
+ const edgeIdByPair = new Map();
6303
+ comms.forEach((edge, ei) => {
6304
+ if (edge.from === edge.to) return;
6305
+ edgeIdByPair.set(edge.from + '->' + edge.to, 'v2ep' + ei);
6306
+ });
6307
+ _v2ChartLayout = { pos, edgeIdByPair };
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);
6317
+ }
6318
+
6319
+ function _v2RoleName(id, roles) {
6320
+ const r = (roles || []).find(x => x.id === id);
6321
+ return r ? (r.name || r.title || r.id) : id;
6322
+ }
6323
+
6324
+ function v2DeriveActivityText(ev, roles) {
6325
+ if (!ev || !ev.from) return null;
6326
+ const type = ev.type || '';
6327
+ if (type === 'org:tool') return ev.tool ? ('running ' + ev.tool) : 'active';
6328
+ if (type === 'org:checkpoint') {
6329
+ const p = ev.progress || '';
6330
+ return p ? (p.length > 40 ? p.slice(0, 39) + '...' : p) : 'active';
6331
+ }
6332
+ if (type === 'org:comms') {
6333
+ if (!ev.to || ev.to === 'all') return 'broadcasting';
6334
+ return 'messaging ' + _v2RoleName(ev.to, roles);
6335
+ }
6336
+ if (type === 'org:artifact') {
6337
+ const label = ev.artifact && ev.artifact.label;
6338
+ return label ? ('writing ' + label) : 'active';
6339
+ }
6340
+ return 'active';
6341
+ }
6342
+
6343
+ function v2DeriveLetterTarget(ev, roles) {
6344
+ if (!ev || !ev.from) return null;
6345
+ const type = ev.type || '';
6346
+ if (type === 'org:comms') {
6347
+ if (!ev.to || ev.to === 'all') return null;
6348
+ return { from: ev.from, to: ev.to };
6349
+ }
6350
+ if (type === 'org:checkpoint' || type === 'org:artifact') {
6351
+ const leader = (roles || []).find(_v2OrgIsLeader);
6352
+ if (!leader || leader.id === ev.from) return null;
6353
+ return { from: ev.from, to: leader.id };
6354
+ }
6355
+ return null;
6356
+ }
6357
+
6358
+ let _v2LetterBurst = { lastTs: 0, count: 0 };
6359
+
6360
+ function v2SpawnLetter(fromId, toId) {
6361
+ const layout = _v2ChartLayout;
6362
+ const fp = layout.pos[fromId], tp = layout.pos[toId];
6363
+ if (!fp || !tp) return;
6364
+ const group = document.getElementById('v2oc-letters');
6365
+ if (!group) return;
6366
+
6367
+ const now = Date.now();
6368
+ if (now - _v2LetterBurst.lastTs < 150) _v2LetterBurst.count++;
6369
+ else _v2LetterBurst.count = 0;
6370
+ _v2LetterBurst.lastTs = now;
6371
+ const delay = (_v2LetterBurst.count * 0.08).toFixed(2);
6372
+
6373
+ const svgNS = 'http://www.w3.org/2000/svg';
6374
+ let pathId = layout.edgeIdByPair.get(fromId + '->' + toId);
6375
+ let synthesizedPath = null;
6376
+ if (!pathId) {
6377
+ pathId = 'v2lp' + Math.random().toString(36).slice(2, 9);
6378
+ synthesizedPath = document.createElementNS(svgNS, 'path');
6379
+ synthesizedPath.setAttribute('id', pathId);
6380
+ synthesizedPath.setAttribute('d', 'M' + fp.x + ',' + fp.y + ' L' + tp.x + ',' + tp.y);
6381
+ synthesizedPath.setAttribute('fill', 'none');
6382
+ synthesizedPath.setAttribute('stroke', 'none');
6383
+ document.getElementById('v2oc-edges').appendChild(synthesizedPath);
6384
+ }
6385
+
6386
+ const letter = document.createElementNS(svgNS, 'g');
6387
+ const circ = document.createElementNS(svgNS, 'circle');
6388
+ circ.setAttribute('r', '6');
6389
+ circ.setAttribute('fill', 'oklch(85% 0.16 85)');
6390
+ circ.setAttribute('stroke', 'oklch(30% 0.02 55)');
6391
+ circ.setAttribute('stroke-width', '1');
6392
+ letter.appendChild(circ);
6393
+ const flap = document.createElementNS(svgNS, 'path');
6394
+ flap.setAttribute('d', 'M-3.5,-2 L0,1 L3.5,-2');
6395
+ flap.setAttribute('stroke', 'oklch(30% 0.02 55)');
6396
+ flap.setAttribute('stroke-width', '1');
6397
+ flap.setAttribute('fill', 'none');
6398
+ letter.appendChild(flap);
6399
+
6400
+ const anim = document.createElementNS(svgNS, 'animateMotion');
6401
+ anim.setAttribute('dur', '0.8s');
6402
+ anim.setAttribute('begin', delay + 's');
6403
+ anim.setAttribute('fill', 'freeze');
6404
+ const mpath = document.createElementNS(svgNS, 'mpath');
6405
+ mpath.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '#' + pathId);
6406
+ anim.appendChild(mpath);
6407
+ letter.appendChild(anim);
6408
+ group.appendChild(letter);
6409
+
6410
+ if (typeof gsap !== 'undefined') {
6411
+ gsap.fromTo(letter, { scale: 0, autoAlpha: 0 }, { scale: 1, autoAlpha: 1, duration: 0.15, delay: Number(delay), ease: 'back.out(1.7)' });
6412
+ }
6413
+
6414
+ const totalMs = (Number(delay) + 0.8) * 1000;
6415
+ setTimeout(function() {
6416
+ function cleanup() {
6417
+ letter.remove();
6418
+ if (synthesizedPath) synthesizedPath.remove();
6419
+ }
6420
+ if (typeof gsap !== 'undefined') {
6421
+ gsap.to(letter, { autoAlpha: 0, duration: 0.15, onComplete: cleanup });
6422
+ } else cleanup();
6423
+ }, totalMs);
6424
+ }
6425
+
6426
+ let _v2BubbleTimers = new Map();
6427
+
6428
+ function v2OrgActivityReset() {
6429
+ _v2BubbleTimers.forEach(t => clearTimeout(t));
6430
+ _v2BubbleTimers = new Map();
6431
+ }
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
+
6442
+ function v2ShowActivityBubble(roleId, text) {
6443
+ const fo = document.querySelector('.v2-activity-bubble[data-bubble-for="' + CSS.escape(roleId) + '"]');
6444
+ if (!fo) return;
6445
+ if (fo.dataset.questionPending === '1') return; // a pending question owns this bubble until answered
6446
+ const inner = fo.querySelector('.v2-bubble-inner');
6447
+ inner.textContent = text;
6448
+ if (typeof gsap !== 'undefined') {
6449
+ gsap.killTweensOf(fo);
6450
+ gsap.fromTo(fo, { scale: 0.8, autoAlpha: 0 }, {
6451
+ scale: 1, autoAlpha: 1, duration: 0.3, ease: 'back.out(1.7)',
6452
+ transformOrigin: '50% 100%',
6453
+ });
6454
+ } else {
6455
+ fo.style.visibility = 'visible';
6456
+ fo.style.opacity = '1';
6457
+ }
6458
+ const existing = _v2BubbleTimers.get(roleId);
6459
+ if (existing) clearTimeout(existing);
6460
+ _v2BubbleTimers.set(roleId, setTimeout(function() {
6461
+ _v2FadeOutBubble(fo);
6462
+ _v2BubbleTimers.delete(roleId);
6463
+ }, 5000));
6464
+ }
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
+ }
6265
6491
  }
6266
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
+ }
6267
6501
 
6268
6502
  // ── in-place running dot updater (avoids full re-render + GSAP flicker on SSE events) ──
6269
6503
  function v2UpdateChartRunningDots() {
@@ -7940,17 +8174,28 @@ window.odtChatSelectAgent = function(name) {
7940
8174
  });
7941
8175
  const feed = document.getElementById('odt-chat-feed');
7942
8176
  const emptyEl = document.getElementById('odt-chat-empty');
7943
- const sess = _odtChatSessions.find(s => s.id === _odtChatCurrentId);
7944
8177
  feed.querySelectorAll('.cv-msg').forEach(e => e.remove());
7945
- if (!sess) { if (emptyEl) emptyEl.style.display = 'block'; return; }
7946
- const visible = (sess.events || []).filter(ev => _odtChatAgentMatches(ev));
8178
+ const id = _odtChatCurrentId;
8179
+ let events = null;
8180
+ let sess = null;
8181
+ if (id && id.startsWith('rgrp:') && _odtRunGroups[id]) {
8182
+ events = _odtGetMergedRunGroupEvents(_odtRunGroups[id]);
8183
+ } else if (id && id.startsWith('grp:') && _odtSessionGroups[id]) {
8184
+ events = _odtGetMergedGroupEvents(_odtSessionGroups[id]);
8185
+ } else {
8186
+ sess = _odtChatSessions.find(s => s.id === id);
8187
+ events = sess ? (sess.events || []) : null;
8188
+ }
8189
+ if (events === null) { if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'Select a session to see agent communications.'; } return; }
8190
+ const visible = events.filter(ev => _odtChatAgentMatches(ev));
7947
8191
  if (!visible.length) {
7948
8192
  if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = name === 'all' ? 'No events.' : `${name} was online in this run but sent no messages.`; }
7949
8193
  } else {
7950
8194
  if (emptyEl) emptyEl.style.display = 'none';
7951
8195
  visible.forEach(ev => _odtAppendEvent(ev, false));
8196
+ if (!feed.querySelector('.cv-msg') && emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'No displayable events.'; }
7952
8197
  }
7953
- if (!sess._isOrgRun) {
8198
+ if (sess && !sess._isOrgRun) {
7954
8199
  const _linkedRunId = 'run-' + (sess.id || '').replace(/^mm-/, '');
7955
8200
  const _linkedRun = _odtChatSessions.find(s => s._isOrgRun && s.id === _linkedRunId);
7956
8201
  if (_linkedRun) {
@@ -7976,17 +8221,25 @@ window.odtChatSelectAgent = function(name) {
7976
8221
  // Chat tab render mode (Summary/Detailed/Raw) — persists to localStorage
7977
8222
  let _odtChatMode = localStorage.getItem('odt-chat-mode') || 'detailed';
7978
8223
 
7979
- function setOdtChatMode(mode) {
7980
- _odtChatMode = mode;
7981
- localStorage.setItem('odt-chat-mode', mode);
8224
+ function _odtApplyChatModeUI(mode) {
7982
8225
  document.querySelectorAll('.odt-mode-btn').forEach(b => b.classList.remove('active'));
7983
8226
  const btn = document.getElementById('odt-mode-' + mode);
7984
8227
  if (btn) btn.classList.add('active');
7985
8228
  }
7986
8229
 
8230
+ function setOdtChatMode(mode) {
8231
+ _odtChatMode = mode;
8232
+ localStorage.setItem('odt-chat-mode', mode);
8233
+ _odtApplyChatModeUI(mode);
8234
+ // Re-render the current selection so the mode change is reflected immediately —
8235
+ // previously this only affected events appended after the switch.
8236
+ if (_odtChatCurrentId) window.odtChatSelectAgent(_odtChatCurrentAgent);
8237
+ }
8238
+
7987
8239
  function initOdtChatMode() {
7988
8240
  const saved = localStorage.getItem('odt-chat-mode') || 'detailed';
7989
- setOdtChatMode(saved);
8241
+ _odtChatMode = saved;
8242
+ _odtApplyChatModeUI(saved);
7990
8243
  }
7991
8244
 
7992
8245
  function _odtAppendEvent(ev, animate) {
@@ -8146,6 +8399,22 @@ function _odtHandleLiveEvent(ev) {
8146
8399
  const _arr = [..._odtChatSeenKeys];
8147
8400
  _odtChatSeenKeys = new Set(_arr.slice(1000));
8148
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
+ }
8410
+ // Wire activity bubble for org chart visualization
8411
+ if (ev && ev.org === _v2SelOrg) {
8412
+ const roles = Array.isArray(_v2OrgData && _v2OrgData.roles) ? _v2OrgData.roles : [];
8413
+ const text = v2DeriveActivityText(ev, roles);
8414
+ if (text && ev.from) v2ShowActivityBubble(ev.from, text);
8415
+ const letterTarget = v2DeriveLetterTarget(ev, roles);
8416
+ if (letterTarget) v2SpawnLetter(letterTarget.from, letterTarget.to);
8417
+ }
8149
8418
  // Route org run events (have runId + org) to the active run session
8150
8419
  if (ev?.org && ev?.runId && ev.org === _v2SelOrg) {
8151
8420
  let runSess = _odtChatSessions.find(s => s.id === ev.runId);
@@ -8358,23 +8627,65 @@ async function v2RenderOrgFiles() {
8358
8627
  '<table style="width:100%;border-collapse:collapse;font-size:12px">'+
8359
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>'+
8360
8629
  '<tbody>'+g.items.map(f => {
8630
+ const isImg = OF_IMG_RE.test(f.name);
8361
8631
  return '<tr style="border-top:1px solid var(--border)" data-fp="'+esc(f.path)+'" data-fn="'+esc(f.name)+'">'+
8362
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>'+
8363
8633
  '<td style="padding:6px 8px">'+badge(f.type)+'</td>'+
8364
8634
  '<td style="padding:6px 8px;font-size:10px;color:var(--text-lo);font-family:var(--mono)">'+fmtSize(f.size||0)+'</td>'+
8365
8635
  '<td style="padding:6px 8px;font-size:10px;color:var(--text-lo)">'+fmtTime(f.mtime)+'</td>'+
8366
- '<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>';
8367
8641
  }).join('')+'</tbody></table></div>';
8368
8642
  }).join('')+'</div>';
8369
8643
  } catch(e) { el.innerHTML = '<div style="padding:20px;color:oklch(60% 0.22 25)">Error: '+e.message+'</div>'; }
8370
8644
  }
8371
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
+
8372
8667
  let _ofModal = null;
8373
8668
  let _ofRawText = '';
8374
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
+
8375
8677
  function _ofMdToHtml(md) {
8376
8678
  return md
8377
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
+ })
8378
8689
  .replace(/^#{6}\s+(.+)$/gm,'<h6 style="font-size:11px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h6>')
8379
8690
  .replace(/^#{5}\s+(.+)$/gm,'<h5 style="font-size:12px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h5>')
8380
8691
  .replace(/^#{4}\s+(.+)$/gm,'<h4 style="font-size:13px;font-weight:700;color:var(--text-hi);margin:12px 0 5px">$1</h4>')
@@ -8389,6 +8700,9 @@ function _ofMdToHtml(md) {
8389
8700
  .replace(/^\s*[-*]\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
8390
8701
  .replace(/(<li.*<\/li>\n?)+/g,'<ul style="padding-left:18px;margin:6px 0">$&</ul>')
8391
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
+ })
8392
8706
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent);text-decoration:underline" target="_blank" rel="noopener">$1</a>')
8393
8707
  .replace(/\n\n+/g,'</p><p style="margin:0 0 8px">')
8394
8708
  .replace(/^(?!<[hupoli]|<pre|<hr)(.+)$/gm, '$1')
@@ -8399,6 +8713,7 @@ async function v2ViewOrgFile(filePath, fileName) {
8399
8713
  if (_ofModal) { _ofModal.remove(); _ofModal = null; }
8400
8714
  const isMd = /\.(md|markdown)$/i.test(fileName);
8401
8715
  const isJson = /\.json$/i.test(fileName);
8716
+ const isImage = OF_IMG_RE.test(fileName);
8402
8717
  _ofRawText = '';
8403
8718
  _ofModal = document.createElement('div');
8404
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';
@@ -8408,8 +8723,9 @@ async function v2ViewOrgFile(filePath, fileName) {
8408
8723
  '<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);flex-shrink:0">' +
8409
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>' +
8410
8725
  (isMd ? '<button id="of-toggle" class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofToggleRender()">Raw</button>' : '') +
8411
- '<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>' +
8412
- '<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>') +
8413
8729
  '<button class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofModal&&(_ofModal.remove(),_ofModal=null)">✕</button>' +
8414
8730
  '</div>' +
8415
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">' +
@@ -8417,9 +8733,18 @@ async function v2ViewOrgFile(filePath, fileName) {
8417
8733
  '</div>' +
8418
8734
  '</div>';
8419
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
+ }
8420
8746
  try {
8421
- const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
8422
- const r = await fetch('/api/file-content?path='+encodeURIComponent(filePath)+dirParam);
8747
+ const r = await fetch(fileUrl);
8423
8748
  const el = document.getElementById('of-content');
8424
8749
  if (!el) return;
8425
8750
  if (!r.ok) { el.textContent = 'Error '+r.status+': '+r.statusText; return; }
@@ -8466,6 +8791,14 @@ function _ofSaveFile(defaultName) {
8466
8791
  URL.revokeObjectURL(a.href);
8467
8792
  }
8468
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
+
8469
8802
  // ── WORKSPACES ─────────────────────────────────────────────
8470
8803
  async function v2RenderOrgWorkspaces() {
8471
8804
  const el = document.getElementById('odt-workspaces');
@@ -8707,7 +9040,7 @@ window.v2ShowCopyOrgDialog = async function() {
8707
9040
  const label = p.name || p.slug || p.path?.split('/').pop() || p.path;
8708
9041
  const shortP = (p.path || '').replace(/\/Users\/[^/]+\//, '~/');
8709
9042
  const isCurrent = p.path === (window._orgDir || '');
8710
- 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">
8711
9044
  <span style="color:var(--text-hi)">${esc(label)}</span>
8712
9045
  <span style="color:var(--text-lo);font-size:10px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(shortP)}</span>
8713
9046
  </div>`;
@@ -9915,7 +10248,7 @@ async function loadProjCosts() {
9915
10248
  const rows = data.projects.slice(0, 10).map((p, i) => {
9916
10249
  const barW = maxCost > 0 ? Math.round((p.cost / maxCost) * 100) : 0;
9917
10250
  const name = p.path.split('/').filter(Boolean).pop() || p.path;
9918
- 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)}">
9919
10252
  <td class="lb-rank">${i + 1}</td>
9920
10253
  <td style="font-size:12px;color:var(--text-mid);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</td>
9921
10254
  <td class="lb-cost">$${p.cost.toFixed(2)}</td>
@@ -9987,7 +10320,7 @@ function buildGanttTimeline() {
9987
10320
  const opacity = 0.3 + costRatio * 0.7;
9988
10321
  const color = DONUT_COLORS[i % DONUT_COLORS.length];
9989
10322
  const tip = `${s.lastPrompt ? s.lastPrompt.slice(0,60) : s.id} | $${(s.totalCost||0).toFixed(3)} | ${fmtDur(s.totalDurationMs||0)}`;
9990
- 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>`;
9991
10324
  }
9992
10325
  html += `<div class="tl-day-row"><div class="tl-day-lbl">${lbl}</div><div class="tl-track">${track}</div></div>`;
9993
10326
  }
@@ -10304,7 +10637,7 @@ function showCustomTagInput(sessId, event) {
10304
10637
  iw.className = 'ctag-input-wrap';
10305
10638
  iw.onclick = e => e.stopPropagation();
10306
10639
  iw.innerHTML = `<input class="ctag-input" type="text" placeholder="tag name…" maxlength="20" autofocus>
10307
- <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>`;
10308
10641
  wrap.appendChild(iw);
10309
10642
  const inp = iw.querySelector('input');
10310
10643
  inp.focus();
@@ -10316,11 +10649,11 @@ function showCustomTagInput(sessId, event) {
10316
10649
 
10317
10650
  function renderCustomTagsInline(sessId, tags) {
10318
10651
  const tagHtml = tags.map(t =>
10319
- `<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>`
10320
10653
  ).join('');
10321
10654
  return `<div class="sr-custom-tags" data-sess="${esc(sessId)}" onclick="event.stopPropagation()">
10322
10655
  ${tagHtml}
10323
- <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>
10324
10657
  </div>`;
10325
10658
  }
10326
10659
 
@@ -10335,14 +10668,14 @@ async function toggleErrDrawer(sessId, event) {
10335
10668
  try {
10336
10669
  const data = await apiFetch('/api/session-errors?dir=' + enc(DIR) + '&id=' + enc(sessId));
10337
10670
  if (!data.errors?.length) {
10338
- 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>`;
10339
10672
  return;
10340
10673
  }
10341
10674
  const items = data.errors.map(e => `<div class="err-item">${esc(e.text)}</div>`).join('');
10342
- 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>
10343
10676
  <div class="err-drawer-body">${items}</div>`;
10344
10677
  } catch (err) {
10345
- 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>`;
10346
10679
  }
10347
10680
  }
10348
10681
 
@@ -10481,19 +10814,12 @@ let _mgWikiMode = 'all';
10481
10814
  let _mgWikiSearchTimer = null;
10482
10815
 
10483
10816
  /* ── Playbook View ─────────────────────────────────────────── */
10484
- // monoagent connections state
10485
- let _maPlatforms = []; // full registry from monoes
10486
- let _maConns = []; // active API/OAuth connections
10487
- let _maSessions = []; // browser sessions
10488
- let _maSelected = null; // currently open modal platform def
10489
- let _maLoginPoll = null; // active login poll interval (cleared on modal close)
10490
10817
 
10491
10818
  function wfSwitchTab(tab) {
10492
10819
  document.querySelectorAll('.wf-pane, #wf-tab-builder').forEach(p => p.classList.remove('active'));
10493
10820
  document.querySelectorAll('[data-wftab]').forEach(b => b.classList.remove('active'));
10494
10821
  document.getElementById('wf-tab-' + tab).classList.add('active');
10495
10822
  document.querySelector('[data-wftab="' + tab + '"]').classList.add('active');
10496
- if (tab === 'systems') loadPlatformSessions();
10497
10823
  if (tab === 'library') loadWorkflowDefs();
10498
10824
  if (tab === 'runs') loadWorkflowRuns();
10499
10825
  if (tab === 'builder') pbInit();
@@ -10527,31 +10853,13 @@ async function loadWorkflowDefs() {
10527
10853
  </div>
10528
10854
  </div>
10529
10855
  <div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
10530
- <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>
10531
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>
10532
10858
  </div>
10533
10859
  </div>`;
10534
10860
  }).join('');
10535
10861
  }
10536
10862
 
10537
- async function loadPlatformSessions() {
10538
- const grid = document.getElementById('wf-systems-grid');
10539
- if (!grid) return;
10540
- grid.innerHTML = '<div class="loading-txt">Loading…</div>';
10541
- try {
10542
- const [plats, conns] = await Promise.all([
10543
- fetch('/api/monoagent/platforms').then(r => r.json()).catch(() => []),
10544
- fetch('/api/monoagent/connections').then(r => r.json()).catch(() => ({ connections: [], sessions: [] })),
10545
- ]);
10546
- _maPlatforms = Array.isArray(plats) ? plats : [];
10547
- _maConns = Array.isArray(conns.connections) ? conns.connections : [];
10548
- _maSessions = Array.isArray(conns.sessions) ? conns.sessions : [];
10549
- renderSystems();
10550
- } catch (e) {
10551
- grid.innerHTML = '<div class="loading-txt">Failed to load systems.</div>';
10552
- }
10553
- }
10554
-
10555
10863
  function relativeTime(ts) {
10556
10864
  if (!ts) return '—';
10557
10865
  const s = Math.floor((Date.now() - ts) / 1000);
@@ -10561,291 +10869,6 @@ function relativeTime(ts) {
10561
10869
  return Math.floor(s / 86400) + 'd ago';
10562
10870
  }
10563
10871
 
10564
- const MA_CATEGORY_ORDER = ['social', 'service', 'communication', 'database'];
10565
- const MA_CATEGORY_LABELS = { social: 'Social', service: 'Services & APIs', communication: 'Communication', database: 'Databases' };
10566
- const BROWSER_PLATFORMS = new Set(['instagram', 'linkedin', 'x', 'tiktok', 'gemini']);
10567
-
10568
- function maResolveConn(pid) {
10569
- pid = pid.toLowerCase();
10570
- if (BROWSER_PLATFORMS.has(pid)) {
10571
- const now = Date.now();
10572
- const s = _maSessions.find(x => (x.Platform || '').toLowerCase() === pid);
10573
- if (!s) return null;
10574
- const expiry = new Date(s.Expiry).getTime();
10575
- return { _type: 'session', id: String(s.ID), account: s.Username || '—', method: 'browser', status: expiry > now ? 'active' : 'expired' };
10576
- }
10577
- const c = _maConns.find(x => (x.platform || '').toLowerCase() === pid);
10578
- if (!c) return null;
10579
- return { _type: 'connection', id: c.id, account: c.account_id || c.label || '—', method: c.method || '—', status: c.status || 'active', lastTested: c.last_tested };
10580
- }
10581
-
10582
- function renderSystems() {
10583
- const grid = document.getElementById('wf-systems-grid');
10584
- if (!grid) return;
10585
-
10586
- if (!_maPlatforms.length) {
10587
- grid.innerHTML = `<div style="padding:24px;text-align:center;color:var(--text-xs);font-size:12px">
10588
- <div style="font-size:28px;margin-bottom:8px">🔌</div>
10589
- <div style="font-weight:600;color:var(--text-lo);margin-bottom:4px">monoagent not found</div>
10590
- <div style="font-size:11px">Install monoes: <code style="color:var(--accent)">go install github.com/monoes/mono-agent/cmd/monoes@latest</code></div>
10591
- </div>`;
10592
- return;
10593
- }
10594
-
10595
- const groups = {};
10596
- for (const p of _maPlatforms) {
10597
- const cat = (p.Category || p.category || 'service').toLowerCase();
10598
- if (!groups[cat]) groups[cat] = [];
10599
- groups[cat].push(p);
10600
- }
10601
-
10602
- const cats = [...MA_CATEGORY_ORDER, ...Object.keys(groups).filter(c => !MA_CATEGORY_ORDER.includes(c))].filter(c => groups[c]);
10603
- const totalConn = _maPlatforms.filter(p => maResolveConn(p.ID || p.id)).length;
10604
-
10605
- let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
10606
- <span style="font-size:11px;color:var(--text-xs)">${totalConn} / ${_maPlatforms.length} connected</span>
10607
- <button class="ma-btn ma-btn-ghost" style="font-size:10px;padding:3px 10px" onclick="loadPlatformSessions()">↺ Refresh</button>
10608
- </div>`;
10609
-
10610
- for (const cat of cats) {
10611
- const list = groups[cat];
10612
- const catConn = list.filter(p => maResolveConn(p.ID || p.id)).length;
10613
- html += `<div class="ma-cat-hdr">
10614
- <span class="ma-cat-label">${MA_CATEGORY_LABELS[cat] || cat}</span>
10615
- <div class="ma-cat-line"></div>
10616
- <span class="ma-cat-count">${catConn}/${list.length}</span>
10617
- </div><div class="ma-tiles">`;
10618
- for (const p of list) {
10619
- const pid = p.ID || p.id;
10620
- const conn = maResolveConn(pid);
10621
- const connected = conn && conn.status === 'active';
10622
- const expired = conn && conn.status !== 'active';
10623
- const cls = connected ? 'connected' : expired ? 'expired' : '';
10624
- html += `<div class="ma-tile ${cls}" data-pid="${esc(pid)}" onclick="maOpenModal(this.dataset.pid)">
10625
- <span class="ma-tile-icon">${esc(p.IconEmoji || p.iconEmoji || '🔌')}</span>
10626
- <span class="ma-tile-name">${esc(p.Name || p.name || pid)}</span>
10627
- <div style="display:flex;align-items:center;gap:4px">
10628
- <span class="ma-tile-dot"></span>
10629
- ${conn ? `<span class="ma-tile-acct">${esc(conn.account)}</span>` : ''}
10630
- </div>
10631
- </div>`;
10632
- }
10633
- html += `</div>`;
10634
- }
10635
-
10636
- grid.innerHTML = html;
10637
- }
10638
-
10639
- function maOpenModal(pid) {
10640
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10641
- if (!p) return;
10642
- _maSelected = p;
10643
- maRenderModal(p, maResolveConn(pid), []);
10644
- }
10645
-
10646
- function maRenderModal(p, conn, steps, selMethod, fields, state) {
10647
- const pid = p.ID || p.id;
10648
- const name = p.Name || p.name || pid;
10649
- const emoji = p.IconEmoji || p.iconEmoji || '🔌';
10650
- const methods = (p.Methods || p.methods || []);
10651
- const method = selMethod || methods[0] || '';
10652
- const isBrowser = method === 'browser';
10653
- const connected = conn && conn.status === 'active';
10654
- const expired = conn && conn.status !== 'active' && conn;
10655
- state = state || {};
10656
-
10657
- const existing = document.getElementById('ma-modal-overlay');
10658
- if (existing) existing.remove();
10659
-
10660
- const overlay = document.createElement('div');
10661
- overlay.id = 'ma-modal-overlay';
10662
- overlay.className = 'ma-modal-bg';
10663
- overlay.addEventListener('click', e => { if (e.target === overlay) maCloseModal(); });
10664
-
10665
- let bodyHtml = '';
10666
-
10667
- if (conn) {
10668
- // Connected state
10669
- const statusColor = connected ? 'var(--green)' : '#fbbf24';
10670
- bodyHtml = `
10671
- <div class="ma-modal-info">
10672
- ${[['Account', conn.account], ['Method', conn.method], ['Status', conn.status]].map(([l,v]) =>
10673
- `<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>`
10674
- ).join('')}
10675
- ${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>` : ''}
10676
- </div>
10677
- ${state.testMsg ? `<div class="ma-msg ${state.testOk ? 'ok' : 'err'}">${esc(state.testMsg)}</div>` : ''}
10678
- ${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}
10679
- ${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>` : ''}
10680
- <div class="ma-btns">
10681
- <button class="ma-btn ma-btn-secondary" id="ma-test-btn" onclick="maTest('${pid}','${conn.id}')">Test</button>
10682
- ${expired ? `<button class="ma-btn ma-btn-primary" onclick="maStartLogin('${pid}')">Log in again</button>` : ''}
10683
- ${state.confirmDisc ? `
10684
- <button class="ma-btn ma-btn-danger" onclick="maDisconnect('${pid}','${conn.id}','${conn._type}')">Yes, disconnect</button>
10685
- <button class="ma-btn ma-btn-ghost" data-pid="${esc(pid)}" onclick="maOpenModal(this.dataset.pid)">Cancel</button>
10686
- ` : `
10687
- <button class="ma-btn ma-btn-danger" onclick="maConfirmDisconnect('${pid}')">Disconnect</button>
10688
- <button class="ma-btn ma-btn-ghost" onclick="maCloseModal()">Close</button>
10689
- `}
10690
- </div>`;
10691
- } else {
10692
- // Not connected — show method tabs + connect form
10693
- const methodTabsHtml = methods.length > 1 ? `<div class="ma-method-tabs">${methods.map(m =>
10694
- `<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>`
10695
- ).join('')}</div>` : '';
10696
-
10697
- let formHtml = '';
10698
- if (isBrowser) {
10699
- formHtml = `<div class="ma-browser-hint">A browser window will open. Log in to ${esc(name)} and the session will be saved automatically.</div>
10700
- ${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>` : ''}
10701
- ${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>` : ''}
10702
- ${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}`;
10703
- } else {
10704
- const fieldDefs = (p.Fields || p.fields || {})[method] || [];
10705
- formHtml = fieldDefs.map(f => {
10706
- const key = f.Key || f.key;
10707
- const label = f.Label || f.label || key;
10708
- const secret = f.Secret || f.secret;
10709
- const req = f.Required || f.required;
10710
- const val = (fields || {})[key] || '';
10711
- return `<div>
10712
- <div class="ma-field-label">${esc(label)}${req ? ' <span style="color:oklch(60% 0.18 25)">*</span>' : ''}</div>
10713
- <input class="ma-field-input" type="${secret ? 'password' : 'text'}" data-key="${esc(key)}" value="${esc(val)}" autocomplete="${secret ? 'new-password' : 'off'}" placeholder="${esc(label)}">
10714
- ${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>` : ''}
10715
- </div>`;
10716
- }).join('') || `<div class="ma-browser-hint">No configuration required.</div>`;
10717
- formHtml += `${state.err ? `<div class="ma-msg err">${esc(state.err)}</div>` : ''}
10718
- ${state.ok ? `<div class="ma-msg ok">Connected!</div>` : ''}`;
10719
- }
10720
-
10721
- const connectBtn = isBrowser
10722
- ? `<button class="ma-btn ma-btn-primary" id="ma-conn-btn" onclick="maStartLogin('${pid}')">Connect ${esc(name)}</button>`
10723
- : `<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>`;
10724
-
10725
- bodyHtml = `
10726
- ${methodTabsHtml}
10727
- ${formHtml}
10728
- <div class="ma-btns">
10729
- ${state.ok ? '' : connectBtn}
10730
- <button class="ma-btn ma-btn-ghost" onclick="loadPlatformSessions();maCloseModal()">Refresh</button>
10731
- <button class="ma-btn ma-btn-ghost" onclick="maCloseModal()">Close</button>
10732
- </div>`;
10733
- }
10734
-
10735
- overlay.innerHTML = `<div class="ma-modal">
10736
- <div class="ma-modal-hdr">
10737
- <div class="ma-modal-hdr-left">
10738
- <span style="font-size:26px">${esc(emoji)}</span>
10739
- <div>
10740
- <div class="ma-modal-title">${esc(name)}</div>
10741
- <div style="display:flex;align-items:center;gap:5px;margin-top:2px">
10742
- <span style="width:6px;height:6px;border-radius:50%;background:${connected?'var(--green)':expired?'#fbbf24':'var(--border)'}"></span>
10743
- <span class="ma-modal-status" style="color:${connected?'var(--green)':expired?'#fbbf24':'var(--text-xs)'}">${connected?'Connected':expired?'Session expired':'Not connected'}</span>
10744
- </div>
10745
- </div>
10746
- </div>
10747
- <button class="ma-btn ma-btn-ghost" style="padding:4px 8px" onclick="maCloseModal()">✕</button>
10748
- </div>
10749
- <div class="ma-modal-body">${bodyHtml}</div>
10750
- </div>`;
10751
-
10752
- document.body.appendChild(overlay);
10753
- }
10754
-
10755
- function maCloseModal() {
10756
- if (_maLoginPoll) { clearInterval(_maLoginPoll); _maLoginPoll = null; }
10757
- const el = document.getElementById('ma-modal-overlay');
10758
- if (el) el.remove();
10759
- _maSelected = null;
10760
- }
10761
-
10762
- function maConfirmDisconnect(pid) {
10763
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10764
- const conn = maResolveConn(pid);
10765
- if (p && conn) maRenderModal(p, conn, [], null, {}, { confirmDisc: true });
10766
- }
10767
-
10768
- async function maStartLogin(pid) {
10769
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10770
- if (!p) return;
10771
- const steps = [{ msg: 'Opening browser…', kind: '' }];
10772
- maRenderModal(p, null, steps, 'browser', {}, { running: true });
10773
- try {
10774
- const r = await fetch('/api/monoagent/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: pid }) });
10775
- if (!r.ok) throw new Error('HTTP ' + r.status);
10776
- steps.push({ msg: 'Browser open — log in then come back', kind: 'ok' });
10777
- maRenderModal(p, null, steps, 'browser', {}, { running: false });
10778
- showToast('Browser opened', 'Log in to ' + (p.Name || pid) + ' and hit Refresh', 'ok');
10779
- // Poll for completion
10780
- let polls = 0;
10781
- if (_maLoginPoll) clearInterval(_maLoginPoll);
10782
- _maLoginPoll = setInterval(async () => {
10783
- polls++;
10784
- const d = await fetch('/api/monoagent/connections').then(r => r.json()).catch(() => null);
10785
- if (d) { _maConns = d.connections || []; _maSessions = d.sessions || []; }
10786
- const conn = maResolveConn(pid);
10787
- if (conn || polls >= 18) {
10788
- clearInterval(_maLoginPoll); _maLoginPoll = null;
10789
- if (conn) { renderSystems(); maOpenModal(pid); showToast('Connected', (p.Name||pid) + ' session saved', 'ok'); }
10790
- else { maRenderModal(p, null, [], 'browser', {}, { err: 'Login timed out. Close the browser tab and try again.' }); }
10791
- }
10792
- }, 5000);
10793
- } catch (e) {
10794
- maRenderModal(p, null, [], 'browser', {}, { err: e.message });
10795
- }
10796
- }
10797
-
10798
- async function maConnectApiKey(pid, method) {
10799
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10800
- if (!p) return;
10801
- const modal = document.getElementById('ma-modal-overlay');
10802
- const inputs = modal ? modal.querySelectorAll('.ma-field-input') : [];
10803
- const fields = {};
10804
- inputs.forEach(inp => { fields[inp.dataset.key] = inp.value.trim(); });
10805
- const btn = document.getElementById('ma-conn-btn');
10806
- if (btn) { btn.disabled = true; btn.textContent = 'Connecting…'; }
10807
- try {
10808
- const r = await fetch('/api/monoagent/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: pid, method, fields }) });
10809
- const d = await r.json();
10810
- if (d.ok) {
10811
- await loadPlatformSessions();
10812
- maRenderModal(p, maResolveConn(pid), [], method, fields, { ok: true });
10813
- showToast('Connected', (p.Name || pid) + ' connected', 'ok');
10814
- } else {
10815
- maRenderModal(p, null, [], method, fields, { err: d.stderr || d.error || 'Connection failed' });
10816
- }
10817
- } catch (e) {
10818
- maRenderModal(p, null, [], method, fields, { err: e.message });
10819
- }
10820
- }
10821
-
10822
- async function maTest(pid, connId) {
10823
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10824
- const conn = maResolveConn(pid);
10825
- if (!p || !conn) return;
10826
- const btn = document.getElementById('ma-test-btn');
10827
- if (btn) { btn.disabled = true; btn.textContent = 'Testing…'; }
10828
- try {
10829
- const d = await fetch('/api/monoagent/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: connId }) }).then(r => r.json());
10830
- maRenderModal(p, conn, [], null, {}, { testOk: d.ok, testMsg: d.ok ? '✓ Connection OK' : ('✗ ' + (d.error || 'Test failed')) });
10831
- } catch (e) {
10832
- maRenderModal(p, conn, [], null, {}, { testMsg: '✗ ' + e.message, testOk: false });
10833
- }
10834
- }
10835
-
10836
- async function maDisconnect(pid, connId, type) {
10837
- const p = _maPlatforms.find(x => (x.ID || x.id) === pid);
10838
- if (!p) return;
10839
- try {
10840
- await fetch('/api/monoagent/disconnect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: connId, type }) });
10841
- await loadPlatformSessions();
10842
- maCloseModal();
10843
- showToast('Disconnected', (p.Name || pid) + ' removed', 'ok');
10844
- } catch (e) {
10845
- showToast('Error', e.message, 'err');
10846
- }
10847
- }
10848
-
10849
10872
  let _wfRuns = [];
10850
10873
  async function loadWorkflowRuns() {
10851
10874
  const runsListEl = document.getElementById('wf-runs-list');
@@ -11347,7 +11370,7 @@ function pbRenderPalette() {
11347
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}
11348
11371
  </div>
11349
11372
  <div class="pb-cat-body ${cat === cats[0] ? '' : 'collapsed'}">
11350
- ${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+')' : ''}">
11351
11374
  <span class="pb-node-icon">${n.icon}</span>${esc(n.label)}
11352
11375
  </button>`).join('')}
11353
11376
  </div>
@@ -11424,13 +11447,13 @@ function pbRenderCanvas() {
11424
11447
  <div class="pb-node-hdr">
11425
11448
  <span class="pb-node-type-icon">${def.icon}</span>
11426
11449
  <span class="pb-node-title" title="${esc(node.type)}">${esc(node.name || node.type)}</span>
11427
- <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>
11428
11451
  </div>
11429
11452
  <div class="pb-node-body">
11430
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>` : ''}
11431
11454
  <div style="font-size:10px;color:var(--text-xs)">${esc(node.type)}</div>
11432
11455
  </div>
11433
- <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>
11434
11457
  `;
11435
11458
  card.addEventListener('mousedown', (e) => pbDragStart(node.id, e));
11436
11459
  card.addEventListener('click', (e) => { if (!e.defaultPrevented) pbSelectNode(node.id); });
@@ -12074,12 +12097,12 @@ function mgRenderAnalyzeButtons() {
12074
12097
  const cEl = document.getElementById('mg-client-btns');
12075
12098
  const sEl = document.getElementById('mg-server-btns');
12076
12099
  cEl.innerHTML = MG_CLIENT_ANALYSES.map(a => `
12077
- <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,'\\$&'))}')">
12078
12101
  <div class="mab-name">${esc(a.label)}</div>
12079
12102
  <div class="mab-desc">${esc(a.desc)}</div>
12080
12103
  </div>`).join('');
12081
12104
  sEl.innerHTML = MG_SERVER_ANALYSES.map(a => `
12082
- <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,'\\$&'))}')">
12083
12106
  <div class="mab-name mg">${esc(a.label)}</div>
12084
12107
  <div class="mab-desc">${esc(a.desc)}</div>
12085
12108
  </div>`).join('');
@@ -12770,7 +12793,7 @@ function mgRenderExport() {
12770
12793
  <div class="mg-export-card">
12771
12794
  <div class="mec-name">${esc(x.name)}</div>
12772
12795
  <div class="mec-desc">${esc(x.desc)}</div>
12773
- <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>
12774
12797
  </div>`).join('');
12775
12798
  }
12776
12799
 
@@ -13047,7 +13070,7 @@ function mgRenderWiki() {
13047
13070
  const types = [...new Set(nodes.map(n => n.type || n.kind || 'unknown'))].sort();
13048
13071
  const pillsEl = document.getElementById('mg-wiki-pills');
13049
13072
  pillsEl.innerHTML = `<span class="mg-pill active" data-type="all" onclick="mgWikiFilterType('all')">All</span>` +
13050
- 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('');
13051
13074
 
13052
13075
  mgRenderWikiList('');
13053
13076
  }
@@ -14053,64 +14076,6 @@ async function mmRenderGraph(body) {
14053
14076
  } catch (e) { body.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
14054
14077
  }
14055
14078
 
14056
- /* ═══════════════════════════════════════════════════════════════
14057
- MONOAGENT TAB
14058
- ═══════════════════════════════════════════════════════════════ */
14059
- const MA_STATE_COLOR = { completed:'#22c55e', failed:'#ef4444', running:'#3b82f6', pending:'#a3a3a3' };
14060
- let _maWorkflows = [], _maActions = [];
14061
-
14062
- function maSwitchTab(tab) {
14063
- document.querySelectorAll('[data-matab]').forEach(b => b.classList.toggle('active', b.dataset.matab === tab));
14064
- document.querySelectorAll('#view-monoagent .wf-pane').forEach(p => p.classList.toggle('active', p.id === 'ma-tab-' + tab));
14065
- }
14066
-
14067
- async function loadMonoagentData() {
14068
- document.getElementById('ma-workflows-list').innerHTML = '<div class="loading-txt">Loading…</div>';
14069
- document.getElementById('ma-history-list').innerHTML = '<div class="loading-txt">Loading…</div>';
14070
- try {
14071
- const data = await apiFetch('/api/monoagent/data');
14072
- _maWorkflows = Array.isArray(data.workflows) ? data.workflows : [];
14073
- _maActions = Array.isArray(data.actions) ? data.actions : [];
14074
- } catch (e) {
14075
- _maWorkflows = []; _maActions = [];
14076
- }
14077
- const bdg = document.getElementById('bdg-monoagent');
14078
- if (bdg) bdg.textContent = _maWorkflows.length || '—';
14079
- renderMonoagentWorkflows();
14080
- renderMonoagentHistory();
14081
- }
14082
-
14083
- function renderMonoagentWorkflows() {
14084
- const el = document.getElementById('ma-workflows-list');
14085
- if (!el) return;
14086
- if (!_maWorkflows.length) { el.innerHTML = '<div class="empty">No monoagent workflows found. Create one with: <code>monoagent workflow create</code></div>'; return; }
14087
- el.innerHTML = _maWorkflows.map(w => `<div class="m-card" style="margin-bottom:10px;padding:12px 14px">
14088
- <div style="display:flex;justify-content:space-between;align-items:center">
14089
- <span style="font-weight:600;color:var(--text-hi);font-family:var(--mono);font-size:13px">${esc(w.name||w.id||'—')}</span>
14090
- <span style="font-size:11px;color:var(--text-lo)">${esc(w.id||'')}</span>
14091
- </div>
14092
- ${w.description ? `<div style="font-size:12px;color:var(--text-lo);margin-top:4px">${esc(w.description)}</div>` : ''}
14093
- <div style="font-size:11px;color:var(--text-xs);margin-top:6px">${(w.nodes||[]).length} nodes · trigger: ${esc(w.trigger||'manual')}</div>
14094
- </div>`).join('');
14095
- }
14096
-
14097
- function renderMonoagentHistory() {
14098
- const el = document.getElementById('ma-history-list');
14099
- if (!el) return;
14100
- if (!_maActions.length) { el.innerHTML = '<div class="empty">No action history yet.</div>'; return; }
14101
- el.innerHTML = _maActions.map(a => {
14102
- const color = MA_STATE_COLOR[a.status] || MA_STATE_COLOR.pending;
14103
- const ts = a.createdAt ? new Date(a.createdAt).toLocaleString() : '—';
14104
- return `<div class="m-card" style="margin-bottom:8px;padding:10px 14px;display:flex;justify-content:space-between;align-items:center">
14105
- <div>
14106
- <span style="font-weight:600;color:var(--text-hi);font-size:12px;font-family:var(--mono)">${esc(a.type||a.action||'—')}</span>
14107
- <span style="font-size:11px;color:var(--text-lo);margin-left:8px">${esc(a.platform||'')}</span>
14108
- <div style="font-size:11px;color:var(--text-xs);margin-top:2px">${ts}</div>
14109
- </div>
14110
- <span style="font-size:11px;font-weight:600;color:${color}">${esc(a.status||'—')}</span>
14111
- </div>`;
14112
- }).join('');
14113
- }
14114
14079
  </script>
14115
14080
  </body>
14116
14081
  </html>