monomind 2.5.4 → 2.5.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.
- package/README.md +10 -3
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/commands/mastermind/topology.md +121 -0
- package/packages/@monomind/cli/.claude/commands/mastermind.md +121 -102
- package/packages/@monomind/cli/.claude/helpers/control-start.cjs +22 -2
- package/packages/@monomind/cli/.claude/helpers/control-stop.cjs +95 -0
- package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +6 -0
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +13 -5
- package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +33 -10
- package/packages/@monomind/cli/.claude/helpers/utils/system-pressure.cjs +46 -0
- package/packages/@monomind/cli/.claude/settings.json +448 -0
- package/packages/@monomind/cli/README.md +10 -3
- package/packages/@monomind/cli/dist/src/commands/agent-lifecycle.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/agent-ops.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/cleanup.js +53 -1
- package/packages/@monomind/cli/dist/src/commands/config.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/hooks-coverage-commands.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/hooks.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/index.js +4 -0
- package/packages/@monomind/cli/dist/src/commands/init-wizard.js +3 -3
- package/packages/@monomind/cli/dist/src/commands/init.js +2 -2
- package/packages/@monomind/cli/dist/src/commands/mcp.js +2 -2
- package/packages/@monomind/cli/dist/src/commands/memory-admin.js +8 -7
- package/packages/@monomind/cli/dist/src/commands/memory-list.js +2 -2
- package/packages/@monomind/cli/dist/src/commands/memory.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/commands/memory.js +4 -4
- package/packages/@monomind/cli/dist/src/commands/neural-core.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/performance.js +8 -9
- package/packages/@monomind/cli/dist/src/commands/status.js +2 -2
- package/packages/@monomind/cli/dist/src/commands/swarm.js +1 -1
- package/packages/@monomind/cli/dist/src/index.js +1 -1
- package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +2 -2
- package/packages/@monomind/cli/dist/src/init/executor.js +13 -6
- package/packages/@monomind/cli/dist/src/init/helpers-generator.js +66 -3
- package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/init/shared-instructions-generator.js +1 -1
- package/packages/@monomind/cli/dist/src/init/statusline-generator.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/init/statusline-generator.js +2 -2
- package/packages/@monomind/cli/dist/src/init/types.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +3 -3
- package/packages/@monomind/cli/dist/src/mcp-client.d.ts +13 -51
- package/packages/@monomind/cli/dist/src/mcp-client.js +79 -134
- package/packages/@monomind/cli/dist/src/mcp-server.js +4 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/browser-tools.js +45 -19
- package/packages/@monomind/cli/dist/src/mcp-tools/guidance-tools.js +1 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/hive-mind-tools.js +2 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.js +2 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/knowledge-tools.js +1 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-compat.js +2 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +22 -9
- package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/neural-tools.js +3 -3
- package/packages/@monomind/cli/dist/src/mcp-tools/system-tools.js +1 -1
- package/packages/@monomind/cli/dist/src/memory/embedding-operations.js +2 -2
- package/packages/@monomind/cli/dist/src/memory/hnsw-operations.js +53 -26
- package/packages/@monomind/cli/dist/src/memory/intelligence.js +2 -2
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.d.ts +7 -3
- package/packages/@monomind/cli/dist/src/memory/memory-bridge.js +24 -10
- package/packages/@monomind/cli/dist/src/memory/memory-crud.js +2 -2
- package/packages/@monomind/cli/dist/src/memory/memory-initializer.d.ts +2 -2
- package/packages/@monomind/cli/dist/src/memory/memory-initializer.js +6 -6
- package/packages/@monomind/cli/dist/src/memory/memory-read.js +4 -4
- package/packages/@monomind/cli/dist/src/orgrt/daemon.js +12 -6
- package/packages/@monomind/cli/dist/src/orgrt/mailbox.d.ts +1 -0
- package/packages/@monomind/cli/dist/src/orgrt/mailbox.js +4 -0
- package/packages/@monomind/cli/dist/src/orgrt/policy.js +1 -1
- package/packages/@monomind/cli/dist/src/orgrt/server.js +25 -0
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +327 -611
- package/packages/@monomind/cli/dist/src/ui/server.mjs +165 -22
- package/packages/@monomind/cli/package.json +1 -1
|
@@ -53,6 +53,14 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
53
53
|
.nav-proj-name { font-size: 12px; font-weight: 600; color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
54
54
|
.nav-proj-items { padding-left: 12px; border-left: 1px solid oklch(72% 0.18 75 / 0.2); margin-left: 11px; }
|
|
55
55
|
.nav-no-proj { display: flex; align-items: center; gap: 8px; padding: 8px 8px 6px 10px; font-size: 11px; color: var(--text-xs); font-style: italic; }
|
|
56
|
+
/* collapsible global nav section */
|
|
57
|
+
.nav-glob-hdr { display: flex; align-items: center; gap: 6px; padding: 8px 8px 4px; cursor: pointer; user-select: none; color: var(--text-xs); }
|
|
58
|
+
.nav-glob-hdr:hover { color: var(--text-hi); }
|
|
59
|
+
.nav-glob-arrow { font-size: 9px; width: 10px; text-align: center; flex-shrink: 0; transition: transform 0.12s; }
|
|
60
|
+
.nav-glob-hdr.open .nav-glob-arrow { transform: rotate(90deg); }
|
|
61
|
+
.nav-glob-lbl { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; }
|
|
62
|
+
.nav-glob-items { display: none; flex-direction: column; }
|
|
63
|
+
.nav-glob-items.open { display: flex; }
|
|
56
64
|
#sb-footer { padding: 10px 14px; border-top: 1px solid var(--border); }
|
|
57
65
|
#sb-user { font-size: 12px; font-weight: 500; color: var(--text-mid); }
|
|
58
66
|
#sb-path { font-size: 10px; color: var(--text-lo); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
|
|
@@ -349,6 +357,8 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
349
357
|
#odt-chat-send-btn { font-size:8px; letter-spacing:1.5px; padding:4px 10px; border-radius:3px; border:1px solid oklch(62% 0.2 186 / 0.4); color:oklch(62% 0.2 186); background:transparent; cursor:pointer; font-family:inherit; flex-shrink:0; align-self:flex-end; }
|
|
350
358
|
#odt-chat-send-btn:hover:not(:disabled) { background:oklch(62% 0.2 186 / 0.12); }
|
|
351
359
|
#odt-chat-send-btn:disabled { opacity:0.3; cursor:default; }
|
|
360
|
+
#odt-chat-role-sel { background:var(--surface); color:var(--text-mid); border:1px solid var(--border); border-radius:4px; font-size:10px; font-family:var(--mono); padding:4px 5px; align-self:flex-end; max-width:110px; flex-shrink:0; }
|
|
361
|
+
#odt-chat-role-sel:focus { outline:none; border-color:var(--accent); }
|
|
352
362
|
.cv-user-msg { align-self:flex-end; }
|
|
353
363
|
.cv-user-msg .cv-bub { background:oklch(20% 0.06 220); border-left:none; border-right:2px solid oklch(62% 0.2 186); }
|
|
354
364
|
.odt-mode-btn {
|
|
@@ -381,6 +391,10 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
381
391
|
@media (prefers-reduced-motion: reduce) { #org-agent-drawer { transition: none; } }
|
|
382
392
|
#oad-head { display: flex; align-items: center; gap: 11px; padding: 16px 16px 14px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
383
393
|
.oad-avatar { width: 46px; height: 46px; border-radius: 50%; flex-shrink: 0; background: var(--surface-hi); object-fit: cover; }
|
|
394
|
+
.oad-avatar-wrap { position: relative; width: 46px; height: 46px; flex-shrink: 0; cursor: pointer; }
|
|
395
|
+
.oad-avatar-wrap .oad-avatar { width: 100%; height: 100%; }
|
|
396
|
+
.oad-avatar-edit { position: absolute; bottom: -2px; right: -2px; width: 18px; height: 18px; border-radius: 50%; background: var(--surface-hi); border: 1.5px solid var(--bg); color: var(--text-lo); display: flex; align-items: center; justify-content: center; font-size: 9px; opacity: 0; transition: opacity 0.12s; }
|
|
397
|
+
.oad-avatar-wrap:hover .oad-avatar-edit { opacity: 1; }
|
|
384
398
|
.oad-id { flex: 1; min-width: 0; }
|
|
385
399
|
.oad-name { font-size: 15px; font-weight: 600; color: var(--text-hi); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
386
400
|
.oad-pills { display: flex; gap: 5px; margin-top: 5px; flex-wrap: wrap; }
|
|
@@ -795,27 +809,6 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
795
809
|
#rp-fill { height:100%; width:100%; background:var(--accent); border-radius:2px; transform:scaleX(0); transform-origin:left; transition:transform 0.15s; }
|
|
796
810
|
#rp-counter { font-size:11px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; }
|
|
797
811
|
|
|
798
|
-
/* ── agent chat view ─────────────────────────────────────── */
|
|
799
|
-
#view-chat .vscroll { display:flex; flex-direction:column; height:100%; overflow:hidden; padding:0; }
|
|
800
|
-
#chat-v-bar { display:flex; align-items:center; gap:8px; padding:14px 18px 10px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
|
|
801
|
-
#chat-v-bar-title { font-size:13px; font-weight:600; color:var(--text-hi); }
|
|
802
|
-
#chat-v-sel { background:var(--surface); color:var(--text-mid); border:1px solid var(--border); border-radius:4px; font-size:11px; font-family:var(--mono); padding:3px 7px; cursor:pointer; max-width:300px; }
|
|
803
|
-
#chat-v-sel:focus { outline:none; border-color:var(--accent); }
|
|
804
|
-
#chat-v-live-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; margin-left:auto; transition:background 0.4s; }
|
|
805
|
-
#chat-v-live-dot.on { background:oklch(68% 0.20 150); animation:livepulse-cv 2.2s ease-in-out infinite; }
|
|
806
|
-
@keyframes livepulse-cv { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
|
807
|
-
#chat-v-live-lbl { font-size:9px; color:var(--text-lo); }
|
|
808
|
-
#chat-v-feed { flex:1; overflow-y:auto; padding:12px 18px; display:flex; flex-direction:column; gap:5px; scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
|
|
809
|
-
#chat-v-feed::-webkit-scrollbar { width:4px; }
|
|
810
|
-
#chat-v-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
|
|
811
|
-
#chat-v-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
|
|
812
|
-
#chat-v-excerpt { flex-shrink:0; padding:7px 18px 8px; background:oklch(13% 0.01 295); border-bottom:1px solid var(--border); display:none; }
|
|
813
|
-
#chat-v-excerpt.visible { display:block; }
|
|
814
|
-
#chat-v-agent-bar { display:flex; align-items:center; gap:6px; padding:6px 18px 7px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
|
|
815
|
-
#chat-v-agent-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
|
|
816
|
-
.cv-agent-pill { font-size:8px; padding:2px 8px; border-radius:10px; border:1px solid var(--border); color:var(--text-lo); background:transparent; cursor:pointer; letter-spacing:0.3px; font-family:var(--mono); transition:border-color 0.15s,color 0.15s; }
|
|
817
|
-
.cv-agent-pill:hover { border-color:oklch(62% 0.20 186 / 0.4); color:var(--text-mid); }
|
|
818
|
-
.cv-agent-pill.active { border-color:oklch(62% 0.20 186); color:oklch(62% 0.20 186); background:oklch(62% 0.20 186 / 0.07); }
|
|
819
812
|
.cv-session-hdr { padding:10px 18px 8px; border-bottom:1px solid var(--border); margin-bottom:4px; background:oklch(13% 0.01 295); }
|
|
820
813
|
.cv-shdr-prompt { font-size:12px; color:var(--text-mid); margin-bottom:4px; font-weight:500; }
|
|
821
814
|
.cv-shdr-meta { display:flex; flex-wrap:wrap; gap:6px; align-items:center; font-size:9px; color:var(--text-lo); font-family:var(--mono); }
|
|
@@ -1582,22 +1575,25 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
1582
1575
|
</div>
|
|
1583
1576
|
<div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
|
|
1584
1577
|
<div class="nav-sect" style="margin-top:auto;padding-top:8px;">
|
|
1585
|
-
<div class="nav-
|
|
1586
|
-
<span class="
|
|
1578
|
+
<div class="nav-glob-hdr" id="nav-glob-hdr" onclick="toggleGlobalNav()">
|
|
1579
|
+
<span class="nav-glob-arrow" id="nav-glob-arrow">▸</span>
|
|
1580
|
+
<span class="nav-glob-lbl">Global</span>
|
|
1587
1581
|
</div>
|
|
1588
|
-
<div class="nav-
|
|
1589
|
-
<
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
<
|
|
1600
|
-
|
|
1582
|
+
<div class="nav-glob-items" id="nav-glob-items">
|
|
1583
|
+
<div class="nav-item" data-view="global">
|
|
1584
|
+
<span class="ico">⊕</span><span class="lbl">Global Feed</span>
|
|
1585
|
+
</div>
|
|
1586
|
+
<div class="nav-item" data-view="global-tokens" title="Token usage across all projects">
|
|
1587
|
+
<span class="ico">$</span><span class="lbl">Global Tokens</span>
|
|
1588
|
+
</div>
|
|
1589
|
+
<div class="nav-item" data-view="humaninput" title="Pending questions from agents across all orgs">
|
|
1590
|
+
<span class="ico">❓</span><span class="lbl">Human Input</span>
|
|
1591
|
+
<span class="bdg" id="bdg-humaninput">—</span>
|
|
1592
|
+
</div>
|
|
1593
|
+
<div class="nav-item" data-view="global-orgs" title="All orgs across all projects">
|
|
1594
|
+
<span class="ico">⬡</span><span class="lbl">All Orgs</span>
|
|
1595
|
+
<span class="bdg" id="bdg-global-orgs">—</span>
|
|
1596
|
+
</div>
|
|
1601
1597
|
</div>
|
|
1602
1598
|
</div>
|
|
1603
1599
|
</div>
|
|
@@ -2276,17 +2272,6 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
2276
2272
|
</div>
|
|
2277
2273
|
</div>
|
|
2278
2274
|
|
|
2279
|
-
<!-- GLOBAL LOOPS -->
|
|
2280
|
-
<div class="view" id="view-global-loops">
|
|
2281
|
-
<div class="vscroll">
|
|
2282
|
-
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
2283
|
-
<div class="pg-title" style="margin-bottom:0">Global Loops</div>
|
|
2284
|
-
<span class="pg-sub" id="gl-sub" style="margin-bottom:0">Loops across all projects</span>
|
|
2285
|
-
</div>
|
|
2286
|
-
<div id="gl-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
2287
|
-
</div>
|
|
2288
|
-
</div>
|
|
2289
|
-
|
|
2290
2275
|
<!-- GLOBAL TOKENS -->
|
|
2291
2276
|
<div class="view" id="view-global-tokens">
|
|
2292
2277
|
<div class="vscroll">
|
|
@@ -2298,23 +2283,14 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
2298
2283
|
</div>
|
|
2299
2284
|
</div>
|
|
2300
2285
|
|
|
2301
|
-
|
|
2286
|
+
<!-- GLOBAL ORGS -->
|
|
2287
|
+
<div class="view" id="view-global-orgs">
|
|
2302
2288
|
<div class="vscroll">
|
|
2303
|
-
<div
|
|
2304
|
-
<
|
|
2305
|
-
<
|
|
2306
|
-
<option value="">— select a session —</option>
|
|
2307
|
-
</select>
|
|
2308
|
-
<select id="chat-v-org-run-sel" style="display:none" onchange="chatVSelectOrgRun(this.value)" title="Select org run to replay communications">
|
|
2309
|
-
<option value="">— org run history —</option>
|
|
2310
|
-
</select>
|
|
2311
|
-
<div id="chat-v-live-dot"></div>
|
|
2312
|
-
<span id="chat-v-live-lbl">OFFLINE</span>
|
|
2313
|
-
</div>
|
|
2314
|
-
<div id="chat-v-excerpt" class="cv-excerpt-banner"></div>
|
|
2315
|
-
<div id="chat-v-feed">
|
|
2316
|
-
<div id="chat-v-empty">Select a session above to see agent communications.<br>Agent messages, intercom signals, and system events appear here in real time.</div>
|
|
2289
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
2290
|
+
<div class="pg-title" style="margin-bottom:0">All Orgs</div>
|
|
2291
|
+
<span class="pg-sub" id="go-sub" style="margin-bottom:0">Orgs across all projects</span>
|
|
2317
2292
|
</div>
|
|
2293
|
+
<div id="go-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
2318
2294
|
</div>
|
|
2319
2295
|
</div>
|
|
2320
2296
|
|
|
@@ -2325,6 +2301,10 @@ select.pb-cfg-inp { padding: 4px 7px; cursor: pointer; }
|
|
|
2325
2301
|
<div class="pg-title" style="margin-bottom:0">Human Input</div>
|
|
2326
2302
|
<span class="pg-sub" id="hi-sub" style="margin-bottom:0">Pending questions across all projects</span>
|
|
2327
2303
|
</div>
|
|
2304
|
+
<div class="hi-tab-bar" style="display:flex;gap:4px;margin-top:12px;border-bottom:1px solid var(--border);padding-bottom:6px">
|
|
2305
|
+
<button class="odt-btn active" data-hitab="pending" onclick="hiSwitchTab('pending')">Pending <span style="font-size:10px;background:var(--surface-hi);color:var(--text-lo);border-radius:8px;padding:1px 6px;min-width:18px;display:inline-block;text-align:center" id="hi-tab-count-pending">—</span></button>
|
|
2306
|
+
<button class="odt-btn" data-hitab="answered" onclick="hiSwitchTab('answered')">Answered <span style="font-size:10px;background:var(--surface-hi);color:var(--text-lo);border-radius:8px;padding:1px 6px;min-width:18px;display:inline-block;text-align:center" id="hi-tab-count-answered">—</span></button>
|
|
2307
|
+
</div>
|
|
2328
2308
|
<div id="hi-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
2329
2309
|
</div>
|
|
2330
2310
|
</div>
|
|
@@ -2493,25 +2473,44 @@ document.querySelectorAll('.nav-item[data-view]').forEach(el => {
|
|
|
2493
2473
|
el.addEventListener('click', () => switchView(el.dataset.view));
|
|
2494
2474
|
});
|
|
2495
2475
|
|
|
2476
|
+
// ── collapsible global nav section ──────────────────────────
|
|
2477
|
+
const GLOBAL_VIEWS = ['global', 'global-tokens', 'global-orgs', 'humaninput'];
|
|
2478
|
+
function setGlobalNavOpen(open) {
|
|
2479
|
+
const items = document.getElementById('nav-glob-items');
|
|
2480
|
+
const hdr = document.getElementById('nav-glob-hdr');
|
|
2481
|
+
const arrow = document.getElementById('nav-glob-arrow');
|
|
2482
|
+
if (!items || !hdr) return;
|
|
2483
|
+
items.classList.toggle('open', open);
|
|
2484
|
+
hdr.classList.toggle('open', open);
|
|
2485
|
+
if (arrow) arrow.textContent = open ? '▾' : '▸';
|
|
2486
|
+
}
|
|
2487
|
+
function toggleGlobalNav() {
|
|
2488
|
+
const items = document.getElementById('nav-glob-items');
|
|
2489
|
+
const open = !(items && items.classList.contains('open'));
|
|
2490
|
+
setGlobalNavOpen(open);
|
|
2491
|
+
try { localStorage.setItem('nav-global-open', open ? '1' : '0'); } catch(_) {}
|
|
2492
|
+
}
|
|
2493
|
+
try { setGlobalNavOpen(localStorage.getItem('nav-global-open') === '1'); } catch(_) { setGlobalNavOpen(false); }
|
|
2494
|
+
|
|
2496
2495
|
document.getElementById('feed-scroll').addEventListener('scroll', () => {
|
|
2497
2496
|
userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
|
|
2498
2497
|
});
|
|
2499
2498
|
|
|
2500
2499
|
function switchView(v, { updateHash = true } = {}) {
|
|
2501
2500
|
currentView = v;
|
|
2501
|
+
if (GLOBAL_VIEWS.includes(v)) setGlobalNavOpen(true);
|
|
2502
2502
|
document.querySelectorAll('.nav-item[data-view]').forEach(el =>
|
|
2503
2503
|
el.classList.toggle('active', el.dataset.view === v));
|
|
2504
2504
|
document.querySelectorAll('.view').forEach(el =>
|
|
2505
2505
|
el.classList.toggle('active', el.id === 'view-' + v));
|
|
2506
|
-
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed', 'global-
|
|
2506
|
+
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', tokens:'Tokens', memory:'Memory', orgs:'Orgs', monograph:'Monograph', global:'Global Feed', 'global-tokens':'Global Tokens', 'global-orgs':'All Orgs', humaninput:'Human Input' };
|
|
2507
2507
|
document.getElementById('view-title').textContent = titles[v] || v;
|
|
2508
2508
|
const PROJECT = DIR ? shortPath(DIR) : 'monomind';
|
|
2509
|
-
const VIEW_LABELS = { now: 'Now', sessions: 'Sessions', projects: 'Projects', loops: 'Loops', tokens: 'Tokens', memory: 'Memory', orgs: 'Orgs', monograph: 'Monograph', global: 'Global Feed', 'global-
|
|
2509
|
+
const VIEW_LABELS = { now: 'Now', sessions: 'Sessions', projects: 'Projects', loops: 'Loops', tokens: 'Tokens', memory: 'Memory', orgs: 'Orgs', monograph: 'Monograph', global: 'Global Feed', 'global-tokens': 'Global Tokens', 'global-orgs': 'All Orgs', humaninput: 'Human Input' };
|
|
2510
2510
|
document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
|
|
2511
2511
|
if (updateHash) _pushHash();
|
|
2512
2512
|
// Projects always re-fetches so onclick paths in cards stay current
|
|
2513
2513
|
if (v === 'projects') { renderProjects(); return; }
|
|
2514
|
-
if (v === 'chat') { initChatView(); return; }
|
|
2515
2514
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
2516
2515
|
}
|
|
2517
2516
|
|
|
@@ -2567,8 +2566,8 @@ function renderView(v) {
|
|
|
2567
2566
|
if (v === 'orgs') renderOrgs();
|
|
2568
2567
|
if (v === 'monograph') loadMonograph();
|
|
2569
2568
|
if (v === 'global') renderGlobalFeed();
|
|
2570
|
-
if (v === 'global-loops') renderGlobalLoops();
|
|
2571
2569
|
if (v === 'global-tokens') renderGlobalTokens();
|
|
2570
|
+
if (v === 'global-orgs') renderGlobalOrgs();
|
|
2572
2571
|
if (v === 'humaninput') renderHumanInputView();
|
|
2573
2572
|
}
|
|
2574
2573
|
|
|
@@ -3997,88 +3996,57 @@ function buildRecap(events, sess) {
|
|
|
3997
3996
|
recap.className = 'show';
|
|
3998
3997
|
}
|
|
3999
3998
|
|
|
4000
|
-
// ── global
|
|
4001
|
-
function
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
if (
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
3999
|
+
// ── global orgs (multi-project) ──────────────────────────
|
|
4000
|
+
function openGlobalOrg(name, projectDir) {
|
|
4001
|
+
switchProject(projectDir);
|
|
4002
|
+
switchView('orgs');
|
|
4003
|
+
const trySelect = (attempts) => {
|
|
4004
|
+
if (typeof _v2Orgs !== 'undefined' && _v2Orgs && _v2Orgs.some(o => o.name === name)) {
|
|
4005
|
+
v2SelectOrg(name);
|
|
4006
|
+
} else if (attempts > 0) {
|
|
4007
|
+
setTimeout(() => trySelect(attempts - 1), 200);
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
trySelect(20);
|
|
4010
4011
|
}
|
|
4011
4012
|
|
|
4012
|
-
async function
|
|
4013
|
-
const el = document.getElementById('
|
|
4013
|
+
async function renderGlobalOrgs() {
|
|
4014
|
+
const el = document.getElementById('go-content');
|
|
4014
4015
|
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
4015
4016
|
try {
|
|
4016
|
-
const data = await apiFetch('/api/
|
|
4017
|
-
const
|
|
4018
|
-
const
|
|
4019
|
-
if (!projects.length) {
|
|
4020
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">↺</div><div>No projects found</div></div>';
|
|
4021
|
-
document.getElementById('bdg-global-loops').textContent = '—';
|
|
4022
|
-
return;
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
const results = await Promise.allSettled(
|
|
4026
|
-
projects.map(p => apiFetch('/api/loops?dir=' + enc(p.path)).then(d => ({
|
|
4027
|
-
project: p,
|
|
4028
|
-
loops: deduplicateLoops(Array.isArray(d) ? d : (d.loops || []))
|
|
4029
|
-
})))
|
|
4030
|
-
);
|
|
4031
|
-
|
|
4032
|
-
let totalLoops = 0;
|
|
4033
|
-
const sections = [];
|
|
4034
|
-
for (const r of results) {
|
|
4035
|
-
if (r.status !== 'fulfilled') continue;
|
|
4036
|
-
const { project, loops } = r.value;
|
|
4037
|
-
if (!loops.length) continue;
|
|
4038
|
-
totalLoops += loops.length;
|
|
4039
|
-
const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
|
|
4040
|
-
const rows = loops.map(l => {
|
|
4041
|
-
const isTillend = l.type === 'tillend';
|
|
4042
|
-
const curRep = l.currentRep || 0;
|
|
4043
|
-
const maxReps = l.maxReps || 0;
|
|
4044
|
-
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
|
|
4045
|
-
const isHil = l.status === 'hil:pending';
|
|
4046
|
-
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
4047
|
-
const isOverdue = !l.status?.startsWith('hil') && !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
|
|
4048
|
-
const isFinished = isOverdue || (maxReps > 0 && curRep >= maxReps) ||
|
|
4049
|
-
['finished','done','complete','completed','expired'].includes(l.status);
|
|
4050
|
-
const running = !isFinished && l.status !== 'stopped' && l.status !== 'paused';
|
|
4051
|
-
const name = l.name || (l.prompt || 'loop').split('--')[0].trim().slice(0, 60);
|
|
4052
|
-
const intervalStr = fmtInterval(l.interval || l.schedule);
|
|
4053
|
-
const type = esc(l.type || 'repeat');
|
|
4054
|
-
const statusClass = isHil ? 'hil' : (running ? 'active' : 'stopped');
|
|
4055
|
-
const statusLabel = isHil ? '⚠ HIL' : (running ? 'active' : (isFinished ? 'done' : 'stopped'));
|
|
4056
|
-
const overdueSpan = isOverdue ? ' <span class="loop-cdown overdue">overdue</span>' : '';
|
|
4057
|
-
const metaStr = intervalStr ? esc(intervalStr) + overdueSpan : overdueSpan;
|
|
4058
|
-
return `<div class="loop-row" style="cursor:default">
|
|
4059
|
-
<div class="loop-ico">${isTillend ? '∞' : '↺'}</div>
|
|
4060
|
-
<div class="loop-body">
|
|
4061
|
-
<div class="loop-name" title="${esc(l.name || l.prompt || '')}"><span class="loop-type-badge${isTillend ? ' tillend' : ''}">${type}</span>${esc(name)}</div>
|
|
4062
|
-
<div class="loop-meta">${metaStr}</div>
|
|
4063
|
-
</div>
|
|
4064
|
-
<span class="gf-proj-tag">${esc(projName)}</span>
|
|
4065
|
-
<div class="loop-status ${statusClass}">${statusLabel}</div>
|
|
4066
|
-
</div>`;
|
|
4067
|
-
}).join('');
|
|
4068
|
-
sections.push(`<div style="margin-bottom:18px">
|
|
4069
|
-
<div class="m-group-title" style="margin-bottom:6px">${esc(projName)}</div>
|
|
4070
|
-
${rows}
|
|
4071
|
-
</div>`);
|
|
4072
|
-
}
|
|
4017
|
+
const data = await apiFetch('/api/orgs');
|
|
4018
|
+
const orgs = sortOrgList(Array.isArray(data) ? data : (data.orgs || []));
|
|
4019
|
+
const running = orgs.filter(o => o.running).length;
|
|
4073
4020
|
|
|
4074
|
-
document.getElementById('bdg-global-
|
|
4075
|
-
document.getElementById('
|
|
4021
|
+
document.getElementById('bdg-global-orgs').textContent = orgs.length || '—';
|
|
4022
|
+
document.getElementById('go-sub').textContent = running > 0
|
|
4023
|
+
? `${orgs.length} org${orgs.length !== 1 ? 's' : ''} · ${running} running`
|
|
4024
|
+
: `${orgs.length} org${orgs.length !== 1 ? 's' : ''} across all projects`;
|
|
4076
4025
|
|
|
4077
|
-
if (!
|
|
4078
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico"
|
|
4026
|
+
if (!orgs.length) {
|
|
4027
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⬡</div><div>No orgs found across projects</div></div>';
|
|
4079
4028
|
return;
|
|
4080
4029
|
}
|
|
4081
|
-
|
|
4030
|
+
|
|
4031
|
+
el.innerHTML = orgs.map(o => {
|
|
4032
|
+
const rolesArr = Array.isArray(o.roles) ? o.roles : [];
|
|
4033
|
+
const rolesN = rolesArr.length || (typeof o.roles === 'number' ? o.roles : 0);
|
|
4034
|
+
const goalSnip = (o.goal || '').slice(0, 60) + ((o.goal || '').length > 60 ? '…' : '');
|
|
4035
|
+
const projName = o.projectDir ? (o.projectDir.split('/').filter(Boolean).pop() || o.projectDir) : '?';
|
|
4036
|
+
return `<div class="org-item" data-org="${esc(o.name)}" data-projdir="${esc(o.projectDir || '')}" onclick="openGlobalOrg(this.dataset.org, this.dataset.projdir)">
|
|
4037
|
+
<div class="oi-dot ${o.running ? 'running' : ''}"></div>
|
|
4038
|
+
<div class="oi-body">
|
|
4039
|
+
<div class="oi-name">${esc(o.name)}</div>
|
|
4040
|
+
${goalSnip ? `<div class="oi-goal">${esc(goalSnip)}</div>` : ''}
|
|
4041
|
+
<div class="oi-chips">
|
|
4042
|
+
${o.running ? `<span class="oi-chip ${o.lastEventAt ? (Date.now()-o.lastEventAt<1800000?'live':'quiet') : 'quiet'}">${o.lastEventAt ? (Date.now()-o.lastEventAt<1800000?'🟢 LIVE':'🟡 QUIET') : '🟡 QUIET'}</span>` : ''}
|
|
4043
|
+
<span class="oi-chip">${rolesN} roles</span>
|
|
4044
|
+
<span class="oi-chip">${esc(o.topology || 'hierarchical')}</span>
|
|
4045
|
+
<span class="gf-proj-tag" title="${esc(o.projectDir || '')}">${esc(projName)}</span>
|
|
4046
|
+
</div>
|
|
4047
|
+
</div>
|
|
4048
|
+
</div>`;
|
|
4049
|
+
}).join('');
|
|
4082
4050
|
} catch (err) {
|
|
4083
4051
|
el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
|
|
4084
4052
|
}
|
|
@@ -4222,6 +4190,9 @@ async function renderGlobalFeed() {
|
|
|
4222
4190
|
}
|
|
4223
4191
|
}
|
|
4224
4192
|
|
|
4193
|
+
let _hiEntries = [];
|
|
4194
|
+
let _hiTab = 'pending';
|
|
4195
|
+
|
|
4225
4196
|
async function renderHumanInputView() {
|
|
4226
4197
|
const el = document.getElementById('hi-content');
|
|
4227
4198
|
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
@@ -4230,7 +4201,9 @@ async function renderHumanInputView() {
|
|
|
4230
4201
|
const projects = (data?.projects || []).slice(0, 8);
|
|
4231
4202
|
if (!projects.length) {
|
|
4232
4203
|
el.innerHTML = '<div class="empty"><div class="empty-ico">❓</div><div>No projects found</div></div>';
|
|
4204
|
+
_hiEntries = [];
|
|
4233
4205
|
updateHumanInputBadge(0);
|
|
4206
|
+
renderHiCurrentTab(projects.length);
|
|
4234
4207
|
return;
|
|
4235
4208
|
}
|
|
4236
4209
|
const results = await Promise.allSettled(
|
|
@@ -4242,31 +4215,55 @@ async function renderHumanInputView() {
|
|
|
4242
4215
|
const { project, questions } = r.value;
|
|
4243
4216
|
for (const q of questions) entries.push({ project, q });
|
|
4244
4217
|
}
|
|
4245
|
-
entries.sort((a, b) => (b.q.ts || 0) - (a.q.ts || 0));
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
: `No pending questions across ${projects.length} projects`;
|
|
4250
|
-
if (!entries.length) {
|
|
4251
|
-
el.innerHTML = '<div class="empty"><div class="empty-ico">❓</div><div>No pending questions</div></div>';
|
|
4252
|
-
return;
|
|
4253
|
-
}
|
|
4254
|
-
el.innerHTML = '<div class="sess-list">' + entries.map(({ project, q }) => `
|
|
4255
|
-
<div class="sess-row" data-question-id="${esc(q.questionId)}" style="flex-direction:column;align-items:stretch;gap:6px">
|
|
4256
|
-
<div style="display:flex;justify-content:space-between;gap:8px">
|
|
4257
|
-
<div><b>${esc(q.org)}</b> · ${esc(q.role)} <span style="color:var(--text-xs)">(${esc(project.name || project.path)})</span></div>
|
|
4258
|
-
<div style="color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(q.ts)}</div>
|
|
4259
|
-
</div>
|
|
4260
|
-
<div style="color:var(--text-hi)">${esc(q.question)}</div>
|
|
4261
|
-
<div style="display:flex;gap:6px">
|
|
4262
|
-
<input type="text" class="hi-answer-input" placeholder="Your answer…" style="flex:1;padding:4px 8px;font-size:12px" />
|
|
4263
|
-
<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>
|
|
4264
|
-
</div>
|
|
4265
|
-
</div>
|
|
4266
|
-
`).join('') + '</div>';
|
|
4218
|
+
entries.sort((a, b) => (b.q.answeredAt || b.q.ts || 0) - (a.q.answeredAt || a.q.ts || 0));
|
|
4219
|
+
_hiEntries = entries;
|
|
4220
|
+
updateHumanInputBadge(entries.filter(({ q }) => q.answer === null || q.answer === undefined).length);
|
|
4221
|
+
renderHiCurrentTab(projects.length);
|
|
4267
4222
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
4268
4223
|
}
|
|
4269
4224
|
|
|
4225
|
+
function hiSwitchTab(tab) {
|
|
4226
|
+
_hiTab = tab;
|
|
4227
|
+
document.querySelectorAll('.hi-tab-bar .odt-btn').forEach(b =>
|
|
4228
|
+
b.classList.toggle('active', b.dataset.hitab === tab));
|
|
4229
|
+
renderHiCurrentTab();
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
function renderHiCurrentTab(projectCount) {
|
|
4233
|
+
const el = document.getElementById('hi-content');
|
|
4234
|
+
const pending = _hiEntries.filter(({ q }) => q.answer === null || q.answer === undefined);
|
|
4235
|
+
const answered = _hiEntries.filter(({ q }) => q.answer !== null && q.answer !== undefined);
|
|
4236
|
+
document.getElementById('hi-tab-count-pending').textContent = pending.length || '—';
|
|
4237
|
+
document.getElementById('hi-tab-count-answered').textContent = answered.length || '—';
|
|
4238
|
+
const entries = _hiTab === 'answered' ? answered : pending;
|
|
4239
|
+
const projTxt = projectCount != null ? ` across ${projectCount} project${projectCount !== 1 ? 's' : ''}` : '';
|
|
4240
|
+
document.getElementById('hi-sub').textContent = _hiTab === 'answered'
|
|
4241
|
+
? (answered.length ? `${answered.length} answered question(s)${projTxt}` : `No answered questions${projTxt}`)
|
|
4242
|
+
: (pending.length ? `${pending.length} pending question(s)${projTxt}` : `No pending questions${projTxt}`);
|
|
4243
|
+
if (!entries.length) {
|
|
4244
|
+
el.innerHTML = `<div class="empty"><div class="empty-ico">❓</div><div>No ${_hiTab} questions</div></div>`;
|
|
4245
|
+
return;
|
|
4246
|
+
}
|
|
4247
|
+
el.innerHTML = '<div class="sess-list">' + entries.map(({ project, q }) => `
|
|
4248
|
+
<div class="sess-row" data-question-id="${esc(q.questionId)}" style="flex-direction:column;align-items:stretch;gap:6px">
|
|
4249
|
+
<div style="display:flex;justify-content:space-between;gap:8px">
|
|
4250
|
+
<div><b>${esc(q.org)}</b> · ${esc(q.role)} <span style="color:var(--text-xs)">(${esc(project.name || project.path)})</span></div>
|
|
4251
|
+
<div style="color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(q.ts)}</div>
|
|
4252
|
+
</div>
|
|
4253
|
+
<div style="color:var(--text-hi)">${esc(q.question)}</div>
|
|
4254
|
+
${_hiTab === 'answered' ? `
|
|
4255
|
+
<div style="display:flex;justify-content:space-between;gap:8px;padding:6px 8px;background:var(--surface-hi);border-radius:4px">
|
|
4256
|
+
<div><span style="color:var(--text-xs)">Answer:</span> ${esc(q.answer)}</div>
|
|
4257
|
+
<div style="color:var(--text-xs);font-size:11px;font-family:var(--mono);flex-shrink:0">${relTime(q.answeredAt)}</div>
|
|
4258
|
+
</div>` : `
|
|
4259
|
+
<div style="display:flex;gap:6px">
|
|
4260
|
+
<input type="text" class="hi-answer-input" placeholder="Your answer…" style="flex:1;padding:4px 8px;font-size:12px" />
|
|
4261
|
+
<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>
|
|
4262
|
+
</div>`}
|
|
4263
|
+
</div>
|
|
4264
|
+
`).join('') + '</div>';
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4270
4267
|
function updateHumanInputBadge(count) {
|
|
4271
4268
|
const b = document.getElementById('bdg-humaninput');
|
|
4272
4269
|
if (b) b.textContent = String(count);
|
|
@@ -4284,366 +4281,16 @@ async function submitHumanAnswer(dir, org, role, questionId, btnEl) {
|
|
|
4284
4281
|
body: JSON.stringify({ dir, org, role, questionId, answer }),
|
|
4285
4282
|
});
|
|
4286
4283
|
const data = await res.json();
|
|
4287
|
-
if (data.ok) {
|
|
4288
|
-
|
|
4284
|
+
if (data.ok) {
|
|
4285
|
+
showToast('Answered', `${org}:${role}`, 'ok');
|
|
4286
|
+
const entry = _hiEntries.find(({ q }) => q.questionId === questionId);
|
|
4287
|
+
if (entry) { entry.q.answer = answer; entry.q.answeredAt = Date.now(); }
|
|
4288
|
+
updateHumanInputBadge(_hiEntries.filter(({ q }) => q.answer === null || q.answer === undefined).length);
|
|
4289
|
+
renderHiCurrentTab();
|
|
4290
|
+
} else { showToast('Error', data.error || 'failed', 'err'); btnEl.disabled = false; }
|
|
4289
4291
|
} catch (e) { showToast('Error', e.message, 'err'); btnEl.disabled = false; }
|
|
4290
4292
|
}
|
|
4291
4293
|
|
|
4292
|
-
// ── agent chat view ────────────────────────────────────────
|
|
4293
|
-
let chatVSessions = {}; // sessionId or groupId → session/group object
|
|
4294
|
-
let chatVGroupMap = {}; // groupId → group object
|
|
4295
|
-
let chatVSessionGroupMap = {}; // sessionId → groupId (for sessions inside a group)
|
|
4296
|
-
let chatVCurrentId = null;
|
|
4297
|
-
let chatVCurrentGroupSessions = new Set(); // session IDs belonging to the currently selected group
|
|
4298
|
-
let chatVSseSource = null;
|
|
4299
|
-
let chatVSeenKeys = new Set();
|
|
4300
|
-
|
|
4301
|
-
function initChatView() {
|
|
4302
|
-
loadChatViewSessions();
|
|
4303
|
-
loadChatViewOrgRuns();
|
|
4304
|
-
if (!chatVSseSource) connectChatViewSSE();
|
|
4305
|
-
}
|
|
4306
|
-
|
|
4307
|
-
async function loadChatViewOrgRuns() {
|
|
4308
|
-
// Populate the org run history selector with recent org runs
|
|
4309
|
-
const runSel = document.getElementById('chat-v-org-run-sel');
|
|
4310
|
-
if (!runSel) return;
|
|
4311
|
-
try {
|
|
4312
|
-
const dir = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
|
|
4313
|
-
const orgs = await apiFetch('/api/orgs' + dir).catch(() => []);
|
|
4314
|
-
const orgList = Array.isArray(orgs) ? orgs : (orgs.orgs || []);
|
|
4315
|
-
const runEntries = [];
|
|
4316
|
-
await Promise.all(orgList.slice(0, 10).map(async org => {
|
|
4317
|
-
const name = org.name || org;
|
|
4318
|
-
if (!name) return;
|
|
4319
|
-
try {
|
|
4320
|
-
const runs = await apiFetch(`/api/org/${encodeURIComponent(name)}/runs${dir}`);
|
|
4321
|
-
(Array.isArray(runs) ? runs : (runs.runs || [])).slice(0, 5).forEach(r => {
|
|
4322
|
-
runEntries.push({ org: name, runId: r.runId || r.id, startedAt: r.startedAt || 0 });
|
|
4323
|
-
});
|
|
4324
|
-
} catch (_) {}
|
|
4325
|
-
}));
|
|
4326
|
-
runEntries.sort((a, b) => b.startedAt - a.startedAt);
|
|
4327
|
-
while (runSel.options.length > 1) runSel.remove(1);
|
|
4328
|
-
runEntries.slice(0, 20).forEach(r => {
|
|
4329
|
-
const opt = document.createElement('option');
|
|
4330
|
-
opt.value = JSON.stringify({ org: r.org, runId: r.runId });
|
|
4331
|
-
const ts = r.startedAt ? new Date(r.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
4332
|
-
opt.textContent = r.org + '/' + (r.runId || '').slice(0, 12) + (ts ? ' ' + ts : '');
|
|
4333
|
-
runSel.appendChild(opt);
|
|
4334
|
-
});
|
|
4335
|
-
runSel.style.display = runEntries.length ? '' : 'none';
|
|
4336
|
-
} catch(e) { console.warn('chat org runs load failed', e); }
|
|
4337
|
-
}
|
|
4338
|
-
|
|
4339
|
-
async function chatVSelectOrgRun(value) {
|
|
4340
|
-
if (!value) return;
|
|
4341
|
-
let org, runId;
|
|
4342
|
-
try { ({ org, runId } = JSON.parse(value)); } catch { return; }
|
|
4343
|
-
const feed = document.getElementById('chat-v-feed');
|
|
4344
|
-
const empty = document.getElementById('chat-v-empty');
|
|
4345
|
-
if (!feed) return;
|
|
4346
|
-
feed.innerHTML = '';
|
|
4347
|
-
// Clear session selection — org run replay is a different mode;
|
|
4348
|
-
// also set chatVCurrentId=null so appendChatViewEvent session-guard doesn't filter events
|
|
4349
|
-
chatVCurrentId = null;
|
|
4350
|
-
const sessEl = document.getElementById('chat-v-sel');
|
|
4351
|
-
if (sessEl) sessEl.value = '';
|
|
4352
|
-
try {
|
|
4353
|
-
const dir = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
|
|
4354
|
-
const events = await apiFetch(`/api/org/${encodeURIComponent(org)}/runs/${encodeURIComponent(runId)}${dir}`);
|
|
4355
|
-
const evArr = Array.isArray(events) ? events : (events.events || []);
|
|
4356
|
-
if (!evArr.length) { feed.appendChild(empty || document.createTextNode('No events in this run.')); return; }
|
|
4357
|
-
evArr.forEach(ev => appendChatViewEvent(ev, false));
|
|
4358
|
-
feed.scrollTop = feed.scrollHeight;
|
|
4359
|
-
} catch(e) {
|
|
4360
|
-
feed.textContent = 'Failed to load run events: ' + e.message;
|
|
4361
|
-
}
|
|
4362
|
-
}
|
|
4363
|
-
|
|
4364
|
-
async function loadChatViewSessions() {
|
|
4365
|
-
try {
|
|
4366
|
-
// Snapshot SSE-seeded sessions before the async fetch — the API response may not yet
|
|
4367
|
-
// include events that arrived via SSE right before the fetch, so we preserve their
|
|
4368
|
-
// buffered events and merge them back after (same pattern as _odtLoadChatSessions).
|
|
4369
|
-
const _prevChatSessions = { ...chatVSessions };
|
|
4370
|
-
const data = await apiFetch('/api/mastermind/sessions' + (DIR ? '?project=' + encodeURIComponent(DIR) : ''));
|
|
4371
|
-
chatVSessions = {};
|
|
4372
|
-
chatVGroupMap = {};
|
|
4373
|
-
chatVSessionGroupMap = {};
|
|
4374
|
-
const sel = document.getElementById('chat-v-sel');
|
|
4375
|
-
const prev = sel.value;
|
|
4376
|
-
while (sel.options.length > 1) sel.remove(1);
|
|
4377
|
-
// API returns a plain array (not {sessions:{...}}) — handle both shapes for safety
|
|
4378
|
-
const sessions = Array.isArray(data) ? data : Object.values(data.sessions || data || {});
|
|
4379
|
-
|
|
4380
|
-
// Normalize and merge SSE-seeded events before grouping
|
|
4381
|
-
sessions.forEach(s => {
|
|
4382
|
-
if (_prevChatSessions[s.id] && (_prevChatSessions[s.id].events || []).length > (s.events || []).length) {
|
|
4383
|
-
s.events = _prevChatSessions[s.id].events;
|
|
4384
|
-
}
|
|
4385
|
-
if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
|
|
4386
|
-
});
|
|
4387
|
-
|
|
4388
|
-
// Group consecutive sessions with same normalized prompt + gap < 20 min → loop group
|
|
4389
|
-
const _CV_MAX_LOOP_GAP = 20 * 60 * 1000;
|
|
4390
|
-
const _cvNormPrompt = p => (p || '').replace(/^running org:\s*/i, '').toLowerCase().replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 40);
|
|
4391
|
-
const sessAsc = [...sessions].sort((a, b) => (a.startedAt || a.ts || 0) - (b.startedAt || b.ts || 0));
|
|
4392
|
-
const cvGroups = []; // [{id, sessions, events, totalEvents, status, prompt, startedAt, _newestAt, _normKey, _isGroup}]
|
|
4393
|
-
sessAsc.forEach(s => {
|
|
4394
|
-
const key = _cvNormPrompt(s.prompt);
|
|
4395
|
-
const sAt = s.startedAt || s.ts || 0;
|
|
4396
|
-
const last = cvGroups[cvGroups.length - 1];
|
|
4397
|
-
if (last && last._normKey === key && key && (sAt - last._newestAt) < _CV_MAX_LOOP_GAP) {
|
|
4398
|
-
last.sessions.push(s);
|
|
4399
|
-
last._newestAt = sAt;
|
|
4400
|
-
last.totalEvents += (s.events || []).length;
|
|
4401
|
-
if (s.status === 'running') last.status = 'running';
|
|
4402
|
-
(s.events || []).forEach(ev => last.events.push(ev));
|
|
4403
|
-
} else {
|
|
4404
|
-
cvGroups.push({
|
|
4405
|
-
id: s.id,
|
|
4406
|
-
sessions: [s],
|
|
4407
|
-
_normKey: key,
|
|
4408
|
-
_newestAt: sAt,
|
|
4409
|
-
totalEvents: (s.events || []).length,
|
|
4410
|
-
status: s.status || 'complete',
|
|
4411
|
-
prompt: s.prompt,
|
|
4412
|
-
startedAt: sAt,
|
|
4413
|
-
domains: s.domains || [],
|
|
4414
|
-
events: [...(s.events || [])],
|
|
4415
|
-
_isGroup: false
|
|
4416
|
-
});
|
|
4417
|
-
}
|
|
4418
|
-
});
|
|
4419
|
-
// Finalize groups
|
|
4420
|
-
cvGroups.forEach(g => {
|
|
4421
|
-
if (g.sessions.length > 1) {
|
|
4422
|
-
g._isGroup = true;
|
|
4423
|
-
g.id = 'grp:' + g.sessions[0].id;
|
|
4424
|
-
g.events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
4425
|
-
}
|
|
4426
|
-
});
|
|
4427
|
-
// Sort groups newest first, register
|
|
4428
|
-
cvGroups.sort((a, b) => b._newestAt - a._newestAt);
|
|
4429
|
-
cvGroups.forEach(g => {
|
|
4430
|
-
chatVSessions[g.id] = g;
|
|
4431
|
-
chatVGroupMap[g.id] = g;
|
|
4432
|
-
g.sessions.forEach(s => {
|
|
4433
|
-
chatVSessions[s.id] = g; // session ID also routes to group
|
|
4434
|
-
if (g._isGroup) chatVSessionGroupMap[s.id] = g.id;
|
|
4435
|
-
});
|
|
4436
|
-
const opt = document.createElement('option');
|
|
4437
|
-
opt.value = g.id;
|
|
4438
|
-
const ts = g._newestAt ? new Date(g._newestAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
4439
|
-
const label = (g.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 30) || g.id.slice(0, 16);
|
|
4440
|
-
const evCount = g.totalEvents || (g.events || []).length;
|
|
4441
|
-
const msgPart = evCount ? ' ' + evCount + 'ev' : '';
|
|
4442
|
-
const repPart = g._isGroup ? ' ' + g.sessions.length + 'reps' : '';
|
|
4443
|
-
opt.textContent = label + (ts ? ' ' + ts : '') + repPart + msgPart + (g.status === 'running' ? ' ●' : '');
|
|
4444
|
-
sel.appendChild(opt);
|
|
4445
|
-
});
|
|
4446
|
-
|
|
4447
|
-
// Re-attach SSE-only sessions not yet in the API response (arrived during fetch)
|
|
4448
|
-
Object.values(_prevChatSessions).forEach(ps => {
|
|
4449
|
-
if (!chatVSessions[ps.id]) {
|
|
4450
|
-
chatVSessions[ps.id] = ps;
|
|
4451
|
-
const opt = document.createElement('option');
|
|
4452
|
-
opt.value = ps.id;
|
|
4453
|
-
const tsVal = ps.startedAt || 0;
|
|
4454
|
-
const ts = tsVal ? new Date(tsVal).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
4455
|
-
const label = (ps.prompt || '').replace(/^running org:\s*/i, '').slice(0, 40) || ps.id.slice(0, 16);
|
|
4456
|
-
opt.textContent = label + (ts ? ' ' + ts : '') + ' ●';
|
|
4457
|
-
sel.appendChild(opt);
|
|
4458
|
-
}
|
|
4459
|
-
});
|
|
4460
|
-
|
|
4461
|
-
// Resolve prev selection: may be a sessionId inside a group now
|
|
4462
|
-
const prevResolved = prev ? (chatVSessionGroupMap[prev] || (chatVSessions[prev] ? prev : null)) : null;
|
|
4463
|
-
if (prevResolved && chatVSessions[prevResolved]) {
|
|
4464
|
-
sel.value = prevResolved;
|
|
4465
|
-
if (!chatVCurrentId) chatVSelectSession(prevResolved);
|
|
4466
|
-
} else {
|
|
4467
|
-
const runningGroup = cvGroups.find(g => g.status === 'running');
|
|
4468
|
-
if (runningGroup) { sel.value = runningGroup.id; chatVSelectSession(runningGroup.id); }
|
|
4469
|
-
else if (cvGroups.length) { sel.value = cvGroups[0].id; chatVSelectSession(cvGroups[0].id); }
|
|
4470
|
-
}
|
|
4471
|
-
} catch(e) { console.warn('chat sessions load failed', e); }
|
|
4472
|
-
}
|
|
4473
|
-
|
|
4474
|
-
function chatVSelectSession(id) {
|
|
4475
|
-
chatVCurrentId = id;
|
|
4476
|
-
chatVCurrentGroupSessions.clear();
|
|
4477
|
-
const feed = document.getElementById('chat-v-feed');
|
|
4478
|
-
const empty = document.getElementById('chat-v-empty');
|
|
4479
|
-
if (!id || !chatVSessions[id]) {
|
|
4480
|
-
feed.innerHTML = '';
|
|
4481
|
-
feed.appendChild(empty);
|
|
4482
|
-
return;
|
|
4483
|
-
}
|
|
4484
|
-
feed.innerHTML = '';
|
|
4485
|
-
const session = chatVSessions[id];
|
|
4486
|
-
// Populate group session set so live SSE events from any rep appear in feed
|
|
4487
|
-
if (session._isGroup && session.sessions) {
|
|
4488
|
-
session.sessions.forEach(s => chatVCurrentGroupSessions.add(s.id));
|
|
4489
|
-
}
|
|
4490
|
-
// Inject session metadata header
|
|
4491
|
-
const tsVal = session.startedAt || session.ts || 0;
|
|
4492
|
-
const tsStr = tsVal ? new Date(tsVal).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
|
|
4493
|
-
const domains = (session.domains || []);
|
|
4494
|
-
const agents = domains.length ? domains.join(', ') : '—';
|
|
4495
|
-
const evCount = session._isGroup ? (session.totalEvents || (session.events || []).length) : (session.events || []).length;
|
|
4496
|
-
const agentNames = (() => {
|
|
4497
|
-
const names = new Set();
|
|
4498
|
-
(session.events || []).forEach(ev => {
|
|
4499
|
-
if (ev.agent) names.add(ev.agent);
|
|
4500
|
-
if (ev.from && ev.from !== 'system') names.add(ev.from);
|
|
4501
|
-
});
|
|
4502
|
-
return names.size ? [...names].join(', ') : agents;
|
|
4503
|
-
})();
|
|
4504
|
-
const prompt = (session.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 120) || id;
|
|
4505
|
-
const repInfo = session._isGroup ? ' · ' + session.sessions.length + ' reps' : '';
|
|
4506
|
-
const statusCls = session.status === 'running' ? 'color:oklch(65% 0.20 145)' : session.status === 'complete' ? 'color:var(--text-lo)' : 'color:oklch(65% 0.20 25)';
|
|
4507
|
-
// Compute cost + duration from events
|
|
4508
|
-
const _allEvs = session.events || [];
|
|
4509
|
-
const _totalCost = _allEvs.reduce((acc, ev) => acc + (Number(ev.cost_usd) || 0), 0);
|
|
4510
|
-
const _costStr = _totalCost > 0 ? '$' + _totalCost.toFixed(4) : '';
|
|
4511
|
-
const _startTs = session.startedAt || session.ts || 0;
|
|
4512
|
-
const _endTs = session.endedAt || (_allEvs[_allEvs.length-1]?.ts) || 0;
|
|
4513
|
-
const _durMs = _endTs > _startTs ? _endTs - _startTs : 0;
|
|
4514
|
-
const _durStr = _durMs > 60000 ? Math.round(_durMs/60000) + 'm' : _durMs > 0 ? Math.round(_durMs/1000) + 's' : '';
|
|
4515
|
-
const hdr = document.createElement('div');
|
|
4516
|
-
hdr.className = 'cv-session-hdr';
|
|
4517
|
-
hdr.innerHTML = `<div class="cv-shdr-prompt">${esc(prompt)}${repInfo ? '<span style="color:var(--text-lo);font-size:10px"> ' + esc(repInfo) + '</span>' : ''}</div>`+
|
|
4518
|
-
`<div class="cv-shdr-meta">`+
|
|
4519
|
-
`<span class="cv-shdr-id" title="${esc(id)}">${esc(id)}</span>`+
|
|
4520
|
-
`<span class="cv-shdr-sep">·</span>`+
|
|
4521
|
-
`<span style="${statusCls}">${esc(session.status || 'unknown')}</span>`+
|
|
4522
|
-
`<span class="cv-shdr-sep">·</span>`+
|
|
4523
|
-
`<span title="started">${esc(tsStr)}</span>`+
|
|
4524
|
-
(_durStr ? `<span class="cv-shdr-sep">·</span><span title="duration">${esc(_durStr)}</span>` : '')+
|
|
4525
|
-
`<span class="cv-shdr-sep">·</span>`+
|
|
4526
|
-
`<span title="agents">${esc(agentNames)}</span>`+
|
|
4527
|
-
`<span class="cv-shdr-sep">·</span>`+
|
|
4528
|
-
`<span title="event count">${evCount} events</span>`+
|
|
4529
|
-
(_costStr ? `<span class="cv-shdr-sep">·</span><span style="color:oklch(65% 0.18 50)" title="total cost">${esc(_costStr)}</span>` : '')+
|
|
4530
|
-
`</div>`;
|
|
4531
|
-
feed.appendChild(hdr);
|
|
4532
|
-
const events = session.events || [];
|
|
4533
|
-
events.forEach(ev => {
|
|
4534
|
-
appendChatViewEvent(ev, false);
|
|
4535
|
-
// Pre-seed dedup set so SSE reconnect replays of historical events are swallowed
|
|
4536
|
-
const _cvhk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.session||ev?.runId||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
|
|
4537
|
-
chatVSeenKeys.add(_cvhk);
|
|
4538
|
-
});
|
|
4539
|
-
// Populate excerpt banner with last activity for the selected session
|
|
4540
|
-
const _cvExEl = document.getElementById('chat-v-excerpt');
|
|
4541
|
-
if (_cvExEl) { if (events.length) renderCvExcerptBanner('chat-v-excerpt', events, session); else _cvExEl.innerHTML = ''; }
|
|
4542
|
-
feed.scrollTop = feed.scrollHeight;
|
|
4543
|
-
}
|
|
4544
|
-
|
|
4545
|
-
function appendChatViewEvent(ev, animate) {
|
|
4546
|
-
if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId && !chatVCurrentGroupSessions.has(ev.session)) return;
|
|
4547
|
-
const feed = document.getElementById('chat-v-feed');
|
|
4548
|
-
if (!feed) return;
|
|
4549
|
-
const empty = document.getElementById('chat-v-empty');
|
|
4550
|
-
if (empty && feed.contains(empty)) feed.removeChild(empty);
|
|
4551
|
-
|
|
4552
|
-
let el;
|
|
4553
|
-
const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
|
|
4554
|
-
const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
4555
|
-
if (ev.type === 'run:start') {
|
|
4556
|
-
el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + (ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + (ev.bossRole) : ''), ts);
|
|
4557
|
-
} else if (ev.type === 'run:cycle:complete') {
|
|
4558
|
-
el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + (String(ev.pending)) + ' tasks pending' : ''), ts);
|
|
4559
|
-
} else if (ev.type === 'intercom') {
|
|
4560
|
-
el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
|
|
4561
|
-
} else if (ev.type === 'org:comms') {
|
|
4562
|
-
const _cvCommsFrom = ev.from || ev.role || ev.org || '?';
|
|
4563
|
-
// Directed messages (to set, not broadcast) → intercom bubble; broadcasts → agent bubble
|
|
4564
|
-
if (ev.to && ev.to !== 'all') el = mkCVIntercom(_cvCommsFrom, ev.to, ev.msg || '', ts);
|
|
4565
|
-
else el = mkCVAgent(_cvCommsFrom, ev.msg || '', ts, 'org:comms');
|
|
4566
|
-
} else if (ev.type === 'org:agent:online') {
|
|
4567
|
-
el = mkCVSys('[' + (ev.org || '') + '] ' + (ev.role || ev.title || '?') + ' online' + (ev.title && ev.role && ev.title !== ev.role ? ' (' + (ev.title) + ')' : ''), ts);
|
|
4568
|
-
} else if (ev.type === 'org:checkpoint') {
|
|
4569
|
-
el = mkCVSys('[checkpoint] ' + (ev.summary || ev.progress || ev.msg || ''), ts);
|
|
4570
|
-
} else if (ev.type === 'org:start') {
|
|
4571
|
-
el = mkCVSys('[org:start] ' + (ev.org || ''), ts);
|
|
4572
|
-
} else if (ev.type === 'run:complete') {
|
|
4573
|
-
el = mkCVSys('■ Run complete' + (ev.status ? ' [' + (ev.status) + ']' : ''), ts);
|
|
4574
|
-
} else if (ev.type === 'org:complete') {
|
|
4575
|
-
el = mkCVSys('■ Org complete' + (ev.org ? ' — ' + (ev.org) : ''), ts);
|
|
4576
|
-
} else if (ev.type === 'agent:result') {
|
|
4577
|
-
el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
|
|
4578
|
-
} else if (ev.type === 'agent:spawn') {
|
|
4579
|
-
const _spawnTo = ev.to || ev.agentType || '?';
|
|
4580
|
-
const _spawnTask = ev.task || ev.briefing || '';
|
|
4581
|
-
el = mkCVSpawn(ev.from || 'orchestrator', _spawnTo, _spawnTask, ts);
|
|
4582
|
-
} else if (ev.type === 'agent:complete') {
|
|
4583
|
-
// Rich completion event from capture-handler (replaces/supplements org:comms)
|
|
4584
|
-
const _acAgent = ev.agentType || ev.role || ev.from || '?';
|
|
4585
|
-
const _acResult = ev.result || '';
|
|
4586
|
-
const _acCost = ev.cost_usd != null ? ' $' + Number(ev.cost_usd).toFixed(4) : '';
|
|
4587
|
-
const _acTok = (ev.tokens_in || ev.tokens_out) ? ' · ' + ((ev.tokens_in||0)+(ev.tokens_out||0)).toLocaleString() + ' tok' : '';
|
|
4588
|
-
const _acTools = (ev.toolCalls || []).length ? ' · [' + ev.toolCalls.slice(0,5).map(t => esc(t)).join(', ') + ']' : '';
|
|
4589
|
-
el = mkCVResult(_acAgent + _acCost + _acTok + _acTools, _acResult, ts);
|
|
4590
|
-
} else if (ev.type === 'agent:usage') {
|
|
4591
|
-
// Token/cost accounting event — show as compact system line
|
|
4592
|
-
const _auAgent = ev.role || ev.agentType || '?';
|
|
4593
|
-
const _auCost = ev.cost_usd != null ? '$' + Number(ev.cost_usd).toFixed(4) : '';
|
|
4594
|
-
const _auTok = (ev.tokens_in || ev.tokens_out) ? ((ev.tokens_in||0)).toLocaleString() + '↑ ' + ((ev.tokens_out||0)).toLocaleString() + '↓' : '';
|
|
4595
|
-
el = mkCVSys('◈ ' + (_auAgent) + (_auTok ? ' · ' + _auTok : '') + (_auCost ? ' · ' + _auCost : ''), ts);
|
|
4596
|
-
} else if (ev.type === 'agent:message') {
|
|
4597
|
-
el = mkCVAgent(ev.agent || ev.name || ev.role || '?', ev.msg || ev.message || ev.content || '', ts, ev.type);
|
|
4598
|
-
} else if (ev.type === 'session:start') {
|
|
4599
|
-
el = mkCVSys('Session started' + (ev.prompt ? ': ' + (ev.prompt.slice(0,80)) : ''), ts);
|
|
4600
|
-
} else if (ev.type === 'session:complete') {
|
|
4601
|
-
el = mkCVSys('Session complete' + (ev.status ? ' — ' + (ev.status) : ''), ts);
|
|
4602
|
-
} else if (ev.type === 'domain:dispatch') {
|
|
4603
|
-
el = mkCVSys('→ ' + (ev.domain || '') + (ev.cmd ? ': ' + (ev.cmd.slice(0,80)) : ''), ts);
|
|
4604
|
-
} else if (ev.type === 'domain:complete') {
|
|
4605
|
-
el = mkCVSys('✓ ' + (ev.domain || '') + (ev.status ? ' [' + (ev.status) + ']' : ''), ts);
|
|
4606
|
-
} else if (ev.type === 'loop:start') {
|
|
4607
|
-
el = mkCVSys('◎ Loop: ' + (ev.command || '') + (ev.repeat ? ' ×' + (String(ev.repeat)) : ''), ts);
|
|
4608
|
-
if (currentView === 'loops') renderLoops();
|
|
4609
|
-
} else if (ev.type === 'loop:complete') {
|
|
4610
|
-
el = mkCVSys('■ Loop done: ' + (ev.command || '') + (ev.ranReps ? ' (' + (String(ev.ranReps)) + ' runs)' : ''), ts);
|
|
4611
|
-
if (currentView === 'loops') renderLoops();
|
|
4612
|
-
} else if (ev.type === 'loop:tick') {
|
|
4613
|
-
const _tickLbl = ev.rep != null
|
|
4614
|
-
? 'rep ' + (String(ev.rep)) + ' done' + (ev.wait != null ? ' → next in ' + (String(ev.wait)) + 's' : '')
|
|
4615
|
-
: (ev.command || ev.loopId || ev.id || '');
|
|
4616
|
-
el = mkCVSys('◷ ' + _tickLbl, ts);
|
|
4617
|
-
el.dataset.evType = 'loop:tick';
|
|
4618
|
-
if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
|
|
4619
|
-
if (currentView === 'loops') renderLoops();
|
|
4620
|
-
} else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
|
|
4621
|
-
el = mkCVSys('⚠ Loop HIL: ' + (ev.command || ev.loopId || ev.id || ''), ts);
|
|
4622
|
-
if (currentView === 'loops') renderLoops();
|
|
4623
|
-
} else if (ev.type === 'loop:hil:resolved') {
|
|
4624
|
-
el = mkCVSys('✓ Loop HIL resolved: ' + (ev.loopId || ev.command || ''), ts);
|
|
4625
|
-
if (currentView === 'loops') renderLoops();
|
|
4626
|
-
} else if (ev.type === 'file:write') {
|
|
4627
|
-
el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
|
|
4628
|
-
} else if (ev.type === 'agent:edit' || ev.type === 'agent:write') {
|
|
4629
|
-
el = mkCVTool('EDIT', ev.payload || ev.file || ev.path || '', '', 'oklch(65% 0.20 270)', ts);
|
|
4630
|
-
} else if (ev.type === 'agent:bash') {
|
|
4631
|
-
el = mkCVTool('BASH', ev.payload || ev.command || '', ev.output || '', 'oklch(72% 0.18 40)', ts);
|
|
4632
|
-
} else if (ev.type === 'agent:browse') {
|
|
4633
|
-
el = mkCVTool('BROWSE', ev.payload || ev.url || '', '', 'oklch(65% 0.15 190)', ts);
|
|
4634
|
-
} else if (ev.type === 'org:agent:offline') {
|
|
4635
|
-
el = mkCVSys('◌ ' + (ev.title || ev.role || '?') + ' offline' + (ev.org ? ' [' + (ev.org) + ']' : ''), ts);
|
|
4636
|
-
} else if (ev.type === 'org:error') {
|
|
4637
|
-
el = mkCVSys('⚠ Error: ' + (ev.msg || ev.error || ev.message || ''), ts);
|
|
4638
|
-
el.classList.add('cv-err');
|
|
4639
|
-
} else {
|
|
4640
|
-
el = mkCVSys((ev.type || 'event'), ts);
|
|
4641
|
-
}
|
|
4642
|
-
if (animate) el.classList.add('cv-new');
|
|
4643
|
-
feed.appendChild(el);
|
|
4644
|
-
if (atBottom) feed.scrollTop = feed.scrollHeight;
|
|
4645
|
-
}
|
|
4646
|
-
|
|
4647
4294
|
function _cvExcerptText(ev) {
|
|
4648
4295
|
const t = ev.type || '';
|
|
4649
4296
|
if (t === 'intercom' || t === 'org:comms') return (ev.from || ev.role || '?') + ' → ' + (ev.to || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
|
|
@@ -4798,84 +4445,6 @@ function mkCVIntercom(from, to, text, ts) {
|
|
|
4798
4445
|
return d;
|
|
4799
4446
|
}
|
|
4800
4447
|
|
|
4801
|
-
function connectChatViewSSE() {
|
|
4802
|
-
if (chatVSseSource) return;
|
|
4803
|
-
const dot = document.getElementById('chat-v-live-dot');
|
|
4804
|
-
const lbl = document.getElementById('chat-v-live-lbl');
|
|
4805
|
-
chatVSseSource = window.mmEventSource('/api/mastermind-stream' + (DIR ? '?project=' + encodeURIComponent(DIR) : ''));
|
|
4806
|
-
chatVSseSource.onopen = () => { dot && dot.classList.add('on'); lbl && (lbl.textContent = 'LIVE'); };
|
|
4807
|
-
chatVSseSource.onmessage = e => {
|
|
4808
|
-
try { handleChatViewEvent(JSON.parse(e.data)); } catch(_) {}
|
|
4809
|
-
};
|
|
4810
|
-
chatVSseSource.onerror = () => {
|
|
4811
|
-
dot && dot.classList.remove('on');
|
|
4812
|
-
lbl && (lbl.textContent = 'OFFLINE');
|
|
4813
|
-
const _cvSrc = chatVSseSource;
|
|
4814
|
-
chatVSseSource = null;
|
|
4815
|
-
if (_cvSrc) _cvSrc.close();
|
|
4816
|
-
setTimeout(connectChatViewSSE, 5000);
|
|
4817
|
-
};
|
|
4818
|
-
}
|
|
4819
|
-
|
|
4820
|
-
function handleChatViewEvent(ev) {
|
|
4821
|
-
if (!ev) return;
|
|
4822
|
-
// Deduplicate SSE reconnect replays (server replays last 50 events on every reconnect)
|
|
4823
|
-
const dedupKey = (ev.ts||'') + '|' + (ev.type||'') + '|' + (ev.from||ev.role||ev.agent||'') + '|' + (ev.session||ev.runId||'') + '|' + (ev.result||ev.msg||ev.message||ev.summary||'').slice(0,20);
|
|
4824
|
-
if (chatVSeenKeys.has(dedupKey)) return;
|
|
4825
|
-
chatVSeenKeys.add(dedupKey);
|
|
4826
|
-
if (chatVSeenKeys.size > 2000) { const _a = [...chatVSeenKeys]; chatVSeenKeys = new Set(_a.slice(1000)); }
|
|
4827
|
-
// Org events (org:comms, org:agent:online, org:checkpoint, etc.) may carry a session field
|
|
4828
|
-
// from the org run's session ID — route them directly to the feed if the session matches,
|
|
4829
|
-
// or append them unconditionally when the chat view is showing the "live" placeholder.
|
|
4830
|
-
if (ev.project && DIR && ev.project !== DIR) return;
|
|
4831
|
-
const isOrgEvent = ev.type && (ev.type.startsWith('org:') || ev.type.startsWith('run:') || ev.type.startsWith('loop:'));
|
|
4832
|
-
if (!ev.session && !isOrgEvent) return;
|
|
4833
|
-
if (ev.session) {
|
|
4834
|
-
const existingEntry = chatVSessions[ev.session]; // may be a group object
|
|
4835
|
-
if (!existingEntry) {
|
|
4836
|
-
// Seed events with the triggering event so it isn't lost if loadChatViewSessions
|
|
4837
|
-
// runs before the event is flushed to disk (disk write and SSE broadcast race).
|
|
4838
|
-
chatVSessions[ev.session] = { id: ev.session, events: [ev], startedAt: ev.ts, status: 'running', prompt: ev.prompt || '' };
|
|
4839
|
-
loadChatViewSessions();
|
|
4840
|
-
// Auto-switch to new session on session:start — after loadChatViewSessions it may be grouped
|
|
4841
|
-
if (ev.type === 'session:start') {
|
|
4842
|
-
const _cvGroupId = chatVSessionGroupMap[ev.session] || ev.session;
|
|
4843
|
-
const _cvSel = document.getElementById('chat-v-sel');
|
|
4844
|
-
if (_cvSel) { _cvSel.value = _cvGroupId; chatVSelectSession(_cvGroupId); }
|
|
4845
|
-
}
|
|
4846
|
-
} else {
|
|
4847
|
-
// existingEntry may be a group — push event to it and update status
|
|
4848
|
-
const target = existingEntry;
|
|
4849
|
-
target.events = target.events || [];
|
|
4850
|
-
target.events.push(ev);
|
|
4851
|
-
if (ev.type === 'session:complete') {
|
|
4852
|
-
// Mark this individual session complete; group stays running until all sessions done
|
|
4853
|
-
if (!target._isGroup) target.status = 'complete';
|
|
4854
|
-
else {
|
|
4855
|
-
// Update the underlying session in group
|
|
4856
|
-
const sub = (target.sessions || []).find(s => s.id === ev.session);
|
|
4857
|
-
if (sub) sub.status = 'complete';
|
|
4858
|
-
// Group stays running if any session is still running
|
|
4859
|
-
if (!(target.sessions || []).some(s => s.status === 'running')) target.status = 'complete';
|
|
4860
|
-
}
|
|
4861
|
-
}
|
|
4862
|
-
if (target._isGroup) target.totalEvents = (target.totalEvents || 0) + 1;
|
|
4863
|
-
}
|
|
4864
|
-
// Show event in feed if this session (or its group) is currently selected
|
|
4865
|
-
const _cvGroupId = chatVSessionGroupMap[ev.session];
|
|
4866
|
-
const _cvIsSelected = chatVCurrentId === ev.session || (_cvGroupId && chatVCurrentId === _cvGroupId) || chatVCurrentGroupSessions.has(ev.session);
|
|
4867
|
-
if (_cvIsSelected) {
|
|
4868
|
-
appendChatViewEvent(ev, true);
|
|
4869
|
-
const _cvSess = chatVSessions[_cvGroupId || ev.session];
|
|
4870
|
-
if (_cvSess) renderCvExcerptBanner('chat-v-excerpt', _cvSess.events || [], _cvSess);
|
|
4871
|
-
}
|
|
4872
|
-
} else if (isOrgEvent) {
|
|
4873
|
-
// Org events without a Claude session ID — append to feed regardless of active view;
|
|
4874
|
-
// #chat-v-feed is in the DOM even when hidden, so events accumulate correctly.
|
|
4875
|
-
appendChatViewEvent(ev, true);
|
|
4876
|
-
}
|
|
4877
|
-
}
|
|
4878
|
-
|
|
4879
4448
|
// ── feature 4: budget cap + desktop notification ───────────
|
|
4880
4449
|
let budget = (function(){ try { return JSON.parse(localStorage.getItem('mm-budget') || '{}'); } catch { return {}; } })();
|
|
4881
4450
|
|
|
@@ -5893,11 +5462,23 @@ let _v2OrgTab = 'chart';
|
|
|
5893
5462
|
const _v2OrgEventLog = {};
|
|
5894
5463
|
let _v2ChartLayout = { pos: {}, edgeIdByPair: new Map() };
|
|
5895
5464
|
|
|
5465
|
+
function _orgActivityMs(o) {
|
|
5466
|
+
const t = o.lastEventAt || (o.loop && o.loop.last_run) || o.created_at || 0;
|
|
5467
|
+
return typeof t === 'number' ? t : (Number(t) || new Date(t).getTime() || 0);
|
|
5468
|
+
}
|
|
5469
|
+
// Running orgs first, then most-recently-active first
|
|
5470
|
+
function sortOrgList(orgs) {
|
|
5471
|
+
return [...orgs].sort((a, b) => {
|
|
5472
|
+
if (!!a.running !== !!b.running) return a.running ? -1 : 1;
|
|
5473
|
+
return _orgActivityMs(b) - _orgActivityMs(a);
|
|
5474
|
+
});
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5896
5477
|
async function renderOrgs() {
|
|
5897
5478
|
try {
|
|
5898
5479
|
const q = DIR ? '?dir=' + encodeURIComponent(DIR) : '';
|
|
5899
5480
|
const data = await apiFetch('/api/orgs' + q);
|
|
5900
|
-
_v2Orgs = Array.isArray(data) ? data : (data.orgs || []);
|
|
5481
|
+
_v2Orgs = sortOrgList(Array.isArray(data) ? data : (data.orgs || []));
|
|
5901
5482
|
} catch (_) { _v2Orgs = []; }
|
|
5902
5483
|
// Show current project name in sub-header
|
|
5903
5484
|
const orgProjLabel = document.getElementById('orgs-proj-label');
|
|
@@ -5928,8 +5509,9 @@ function v2RenderOrgList() {
|
|
|
5928
5509
|
const MAX_SHOW = 4;
|
|
5929
5510
|
const shownRoles = rolesArr.slice(0, MAX_SHOW);
|
|
5930
5511
|
const extraCount = rolesN - shownRoles.length;
|
|
5512
|
+
const _oiAvMap = v2AvatarMapFor(rolesArr);
|
|
5931
5513
|
const avatarsHtml = shownRoles.length ? `<div class="oi-avatars">
|
|
5932
|
-
${shownRoles.map(r => `<img class="oi-av" src="${v2RoleAvatar(r)}" alt="${esc(r.name||r.id||'')}" loading="lazy" title="${esc(r.name||r.id||'')}" onerror="this.src='data/avatars/coder.svg'"/>`).join('')}
|
|
5514
|
+
${shownRoles.map(r => `<img class="oi-av" src="${v2RoleAvatar(r, _oiAvMap)}" alt="${esc(r.name||r.id||'')}" loading="lazy" title="${esc(r.name||r.id||'')}" onerror="this.src='data/avatars/coder.svg'"/>`).join('')}
|
|
5933
5515
|
${extraCount > 0 ? `<div class="oi-av-more">+${extraCount}</div>` : ''}
|
|
5934
5516
|
</div>` : '';
|
|
5935
5517
|
return `<div class="org-item${active}" data-org="${esc(o.name)}" onclick="v2SelectOrg(this.dataset.org)">
|
|
@@ -6381,6 +5963,7 @@ function v2RenderOrgChart() {
|
|
|
6381
5963
|
|
|
6382
5964
|
// Overlay live running status from in-memory agent SSE state for chart node indicators
|
|
6383
5965
|
const _chartRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
5966
|
+
const _chartAvMap = v2AvatarMapFor(roles);
|
|
6384
5967
|
let nodesHTML = '';
|
|
6385
5968
|
roles.forEach((role, i) => {
|
|
6386
5969
|
const p = pos[role.id];
|
|
@@ -6397,13 +5980,13 @@ function v2RenderOrgChart() {
|
|
|
6397
5980
|
const runningDot = isRunning ? `<circle class="v2-chart-running-dot" r="5.5" cx="${(R * 0.65).toFixed(1)}" cy="${(-R * 0.65).toFixed(1)}" fill="oklch(68% 0.20 150)" stroke="oklch(12% 0.008 55)" stroke-width="1.5" style="animation:blink 2s ease-in-out infinite"/>` : '';
|
|
6398
5981
|
const nameY = subType ? (R * 0.55 + 2).toFixed(0) : (R * 0.55 + 8).toFixed(0);
|
|
6399
5982
|
const avR = Math.round(R * 0.52);
|
|
6400
|
-
const avatarSrc = v2RoleAvatar(role);
|
|
5983
|
+
const avatarSrc = v2RoleAvatar(role, _chartAvMap);
|
|
6401
5984
|
nodesHTML += `<g class="v2oc-node" data-role="${esc(role.id)}" onclick="v2OpenAgent('${String(role.id).replace(/['\\]/g,'\\$&')}')" transform="translate(${p.x.toFixed(1)},${p.y.toFixed(1)})">
|
|
6402
5985
|
${runningDot}
|
|
6403
5986
|
<title>${esc(displayName)}${subType ? ` · ${esc(subType)}` : ''} — click for details</title>
|
|
6404
5987
|
${outerRing}
|
|
6405
5988
|
<circle r="${R}" fill="oklch(12% 0.008 55)" stroke="${color}" stroke-width="${leader ? 2.5 : 1.8}" filter="url(#v2glow)"/>
|
|
6406
|
-
<image href="${avatarSrc}" x="${-avR}" y="${-
|
|
5989
|
+
<image href="${avatarSrc}" x="${-avR}" y="${-avR}" width="${avR * 2}" height="${avR * 2}" clip-path="url(#${nodeClipIds[i]})" preserveAspectRatio="xMidYMid meet" onerror="this.setAttribute('href','data/avatars/coder.svg')"/>
|
|
6407
5990
|
<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>
|
|
6408
5991
|
${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>` : ''}
|
|
6409
5992
|
<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">
|
|
@@ -6734,18 +6317,68 @@ const _v2TypeFallback = {
|
|
|
6734
6317
|
reporter:'analytics-reporter', strategist:'product-manager',
|
|
6735
6318
|
designer:'game-designer', writer:'technical-writer', auditor:'security-auditor',
|
|
6736
6319
|
};
|
|
6737
|
-
|
|
6320
|
+
// Ranked list of built-in avatar ids that could represent this role, best match first.
|
|
6321
|
+
function _v2AvatarCandidates(role) {
|
|
6738
6322
|
const id = ((role && role.id) || '').toLowerCase().replace(/\s+/g,'-');
|
|
6739
6323
|
const type = ((role && (role.type || role.agent_type)) || '').toLowerCase().replace(/\s+/g,'-');
|
|
6740
|
-
|
|
6741
|
-
if (
|
|
6324
|
+
const out = [];
|
|
6325
|
+
const add = k => { if (k && !out.includes(k)) out.push(k); };
|
|
6326
|
+
if (_v2AvatarKnown.has(id)) add(id);
|
|
6327
|
+
if (_v2AvatarKnown.has(type)) add(type);
|
|
6742
6328
|
for (const k of _v2AvatarKnown) {
|
|
6743
|
-
if (id && (id.includes(k) || k.includes(id)))
|
|
6329
|
+
if (id && (id.includes(k) || k.includes(id))) add(k);
|
|
6744
6330
|
}
|
|
6745
6331
|
for (const [t, av] of Object.entries(_v2TypeFallback)) {
|
|
6746
|
-
if ((type && type.includes(t)) || (id && id.includes(t)))
|
|
6332
|
+
if ((type && type.includes(t)) || (id && id.includes(t))) add(av);
|
|
6747
6333
|
}
|
|
6748
|
-
|
|
6334
|
+
add('coder');
|
|
6335
|
+
return out;
|
|
6336
|
+
}
|
|
6337
|
+
|
|
6338
|
+
// Small stable string hash — used to deterministically (not randomly) pick a fallback
|
|
6339
|
+
// avatar for roles whose entire candidate list is already taken by a sibling role.
|
|
6340
|
+
function _v2StrHash(s) {
|
|
6341
|
+
let h = 0;
|
|
6342
|
+
for (let i = 0; i < String(s).length; i++) h = (h * 31 + String(s).charCodeAt(i)) >>> 0;
|
|
6343
|
+
return h;
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6346
|
+
// Assigns each role in an org a distinct built-in avatar (no two roles in the same
|
|
6347
|
+
// org share one), while leaving any role with a custom uploaded avatar untouched.
|
|
6348
|
+
// Pure function of `roles` — cheap to recompute on every render, so callers should
|
|
6349
|
+
// NOT cache it across data reloads.
|
|
6350
|
+
function v2AvatarMapFor(roles) {
|
|
6351
|
+
const list = Array.isArray(roles) ? roles : [];
|
|
6352
|
+
const used = new Set();
|
|
6353
|
+
const map = new Map();
|
|
6354
|
+
const allKnown = Array.from(_v2AvatarKnown);
|
|
6355
|
+
list.forEach(r => {
|
|
6356
|
+
if (!r || !r.id) return;
|
|
6357
|
+
if (r.avatar) { map.set(r.id, r.avatar); return; } // custom upload — exempt from dedup
|
|
6358
|
+
const candidates = _v2AvatarCandidates(r);
|
|
6359
|
+
let pick = candidates.find(c => !used.has(c));
|
|
6360
|
+
if (!pick) {
|
|
6361
|
+
const free = allKnown.filter(k => !used.has(k));
|
|
6362
|
+
// Pool exhausted (org has more roles than the ~65 built-in avatars) — a genuine
|
|
6363
|
+
// duplicate is unavoidable at that point, but hash across the FULL pool rather
|
|
6364
|
+
// than always falling back to candidates[0] (which is guaranteed already taken,
|
|
6365
|
+
// since the .find() above just failed on it), so overflow roles at least spread
|
|
6366
|
+
// out instead of all collapsing onto the same one avatar.
|
|
6367
|
+
pick = free.length ? free[_v2StrHash(r.id) % free.length] : allKnown[_v2StrHash(r.id) % allKnown.length];
|
|
6368
|
+
}
|
|
6369
|
+
used.add(pick);
|
|
6370
|
+
map.set(r.id, `data/avatars/${pick}.svg`);
|
|
6371
|
+
});
|
|
6372
|
+
return map;
|
|
6373
|
+
}
|
|
6374
|
+
|
|
6375
|
+
// avatarMap (from v2AvatarMapFor) is optional — pass it whenever sibling roles are
|
|
6376
|
+
// known, so avatars stay unique within the org; without it this falls back to the
|
|
6377
|
+
// single best guess for this role alone.
|
|
6378
|
+
function v2RoleAvatar(role, avatarMap) {
|
|
6379
|
+
if (avatarMap && role && avatarMap.has(role.id)) return avatarMap.get(role.id);
|
|
6380
|
+
if (role && role.avatar) return role.avatar;
|
|
6381
|
+
return `data/avatars/${_v2AvatarCandidates(role)[0]}.svg`;
|
|
6749
6382
|
}
|
|
6750
6383
|
|
|
6751
6384
|
// ── org roles ───────────────────────────────
|
|
@@ -6764,6 +6397,7 @@ function v2RenderOrgRoles() {
|
|
|
6764
6397
|
if (!leaderIds.size) leaderIds.add(roles[0]?.id);
|
|
6765
6398
|
// Overlay live running status from in-memory agent SSE state
|
|
6766
6399
|
const _rolesRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
6400
|
+
const _rolesAvMap = v2AvatarMapFor(roles);
|
|
6767
6401
|
pane.innerHTML = `<div class="roles-v2-grid">${roles.map((r, i) => {
|
|
6768
6402
|
const color = V2_ROLE_COLORS[i % V2_ROLE_COLORS.length];
|
|
6769
6403
|
const isLeader = leaderIds.has(r.id);
|
|
@@ -6771,7 +6405,7 @@ function v2RenderOrgRoles() {
|
|
|
6771
6405
|
const resps = Array.isArray(r.responsibilities) ? r.responsibilities : [];
|
|
6772
6406
|
const displayName = r.name || r.title || r.id;
|
|
6773
6407
|
const roleType = r.type || r.agent_type || '';
|
|
6774
|
-
const avatarSrc = v2RoleAvatar(r);
|
|
6408
|
+
const avatarSrc = v2RoleAvatar(r, _rolesAvMap);
|
|
6775
6409
|
return `<div class="role-v2" style="border-top:2px solid ${color}" onclick="v2OpenAgent('${String(r.id).replace(/['\\]/g,'\\$&')}')" title="View agent characteristics, skills & instructions">
|
|
6776
6410
|
<div class="rv2-head">
|
|
6777
6411
|
<img class="rv2-avatar" src="${avatarSrc}" alt="${esc(displayName)}" loading="lazy" onerror="this.src='data/avatars/coder.svg'"/>
|
|
@@ -6828,10 +6462,15 @@ function v2RenderAgentDrawer(data) {
|
|
|
6828
6462
|
const name = role.name || role.title || role.id || 'Agent';
|
|
6829
6463
|
const type = role.agent_type || role.type || '';
|
|
6830
6464
|
const model = (role.adapter_config && role.adapter_config.model) || cap.model_preference || '';
|
|
6831
|
-
const
|
|
6465
|
+
const siblingRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [role];
|
|
6466
|
+
const avatar = v2RoleAvatar(role, v2AvatarMapFor(siblingRoles));
|
|
6832
6467
|
|
|
6833
6468
|
headEl.innerHTML = `
|
|
6834
|
-
<
|
|
6469
|
+
<div class="oad-avatar-wrap" onclick="v2TriggerAvatarUpload('${String(role.id).replace(/['\\]/g,'\\$&')}')" title="Change avatar">
|
|
6470
|
+
<img class="oad-avatar" id="oad-avatar-img" src="${avatar}" alt="${esc(name)}" onerror="this.src='data/avatars/coder.svg'"/>
|
|
6471
|
+
<div class="oad-avatar-edit">✎</div>
|
|
6472
|
+
</div>
|
|
6473
|
+
<input type="file" id="oad-avatar-file" accept="image/png,image/jpeg,image/webp,image/gif" style="display:none" onchange="v2HandleAvatarFile(this)"/>
|
|
6835
6474
|
<div class="oad-id">
|
|
6836
6475
|
<div class="oad-name">${esc(name)}</div>
|
|
6837
6476
|
<div class="oad-pills">
|
|
@@ -6871,6 +6510,62 @@ function v2RenderAgentDrawer(data) {
|
|
|
6871
6510
|
bodyEl.innerHTML = html;
|
|
6872
6511
|
bodyEl.scrollTop = 0;
|
|
6873
6512
|
}
|
|
6513
|
+
|
|
6514
|
+
// ── custom role avatar upload ────────────────
|
|
6515
|
+
function v2TriggerAvatarUpload(roleId) {
|
|
6516
|
+
const inp = document.getElementById('oad-avatar-file');
|
|
6517
|
+
if (!inp) return;
|
|
6518
|
+
inp.dataset.roleId = roleId;
|
|
6519
|
+
inp.value = '';
|
|
6520
|
+
inp.click();
|
|
6521
|
+
}
|
|
6522
|
+
async function v2HandleAvatarFile(inputEl) {
|
|
6523
|
+
const file = inputEl.files && inputEl.files[0];
|
|
6524
|
+
const roleId = inputEl.dataset.roleId;
|
|
6525
|
+
if (!file || !roleId || !_v2SelOrg) return;
|
|
6526
|
+
if (!/^image\//.test(file.type)) { showToast('Error', 'Please choose an image file', 'err'); return; }
|
|
6527
|
+
if (file.size > 5 * 1024 * 1024) { showToast('Error', 'Image too large (max 5MB)', 'err'); return; }
|
|
6528
|
+
try {
|
|
6529
|
+
const dataUrl = await v2DownscaleImageToDataUrl(file, 160);
|
|
6530
|
+
const res = await fetch(`/api/org/${encodeURIComponent(_v2SelOrg)}/agent/${encodeURIComponent(roleId)}/avatar${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, {
|
|
6531
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
6532
|
+
body: JSON.stringify({ avatarDataUrl: dataUrl }),
|
|
6533
|
+
});
|
|
6534
|
+
const data = await res.json();
|
|
6535
|
+
if (!data.ok) throw new Error(data.error || 'failed');
|
|
6536
|
+
const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
6537
|
+
const r = roles.find(x => x.id === roleId);
|
|
6538
|
+
if (r) r.avatar = dataUrl;
|
|
6539
|
+
const imgEl = document.getElementById('oad-avatar-img');
|
|
6540
|
+
if (imgEl) imgEl.src = dataUrl;
|
|
6541
|
+
showToast('Avatar updated', roleId, 'ok');
|
|
6542
|
+
if (document.getElementById('odt-roles')) v2RenderOrgRoles();
|
|
6543
|
+
if (_v2SelOrg) v2RenderOrgChart();
|
|
6544
|
+
} catch (e) { showToast('Error', e.message, 'err'); }
|
|
6545
|
+
}
|
|
6546
|
+
// Downscale/center-crop an uploaded image client-side before sending — avatars are
|
|
6547
|
+
// stored inline as data URLs in the org config JSON, so keeping them small matters.
|
|
6548
|
+
function v2DownscaleImageToDataUrl(file, size) {
|
|
6549
|
+
return new Promise((resolve, reject) => {
|
|
6550
|
+
const img = new Image();
|
|
6551
|
+
const reader = new FileReader();
|
|
6552
|
+
reader.onerror = () => reject(new Error('Could not read file'));
|
|
6553
|
+
reader.onload = () => {
|
|
6554
|
+
img.onerror = () => reject(new Error('Could not decode image'));
|
|
6555
|
+
img.onload = () => {
|
|
6556
|
+
const canvas = document.createElement('canvas');
|
|
6557
|
+
canvas.width = size; canvas.height = size;
|
|
6558
|
+
const ctx = canvas.getContext('2d');
|
|
6559
|
+
const s = Math.min(img.width, img.height);
|
|
6560
|
+
ctx.drawImage(img, (img.width - s) / 2, (img.height - s) / 2, s, s, 0, 0, size, size);
|
|
6561
|
+
resolve(canvas.toDataURL('image/png'));
|
|
6562
|
+
};
|
|
6563
|
+
img.src = reader.result;
|
|
6564
|
+
};
|
|
6565
|
+
reader.readAsDataURL(file);
|
|
6566
|
+
});
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6874
6569
|
// Minimal, XSS-safe markdown → HTML (headings, lists, code, bold/italic, inline code).
|
|
6875
6570
|
function v2RenderMarkdown(md) {
|
|
6876
6571
|
if (!md) return '';
|
|
@@ -7634,6 +7329,7 @@ async function v2RenderOrgChat() {
|
|
|
7634
7329
|
<div id="odt-chat-empty">Select a session to see agent communications.<br><span style="font-size:10px;opacity:0.5">Sessions appear after the first /mastermind:runorg</span></div>
|
|
7635
7330
|
</div>
|
|
7636
7331
|
<div id="odt-chat-compose">
|
|
7332
|
+
<select id="odt-chat-role-sel" title="Send to"><option value="">boss (default)</option></select>
|
|
7637
7333
|
<textarea id="odt-chat-input" rows="1" placeholder="Send a message to the org…" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();odtSendChatMessage();}"></textarea>
|
|
7638
7334
|
<button id="odt-chat-send-btn" onclick="odtSendChatMessage()" disabled>SEND</button>
|
|
7639
7335
|
</div>`;
|
|
@@ -7641,6 +7337,7 @@ async function v2RenderOrgChat() {
|
|
|
7641
7337
|
|
|
7642
7338
|
// Restore saved chat mode (runs on every tab open so buttons stay in sync)
|
|
7643
7339
|
initOdtChatMode();
|
|
7340
|
+
populateOdtChatRoleSelect();
|
|
7644
7341
|
|
|
7645
7342
|
// Reset state only when org changes (not on every tab-switch)
|
|
7646
7343
|
if (_odtChatForOrg !== _v2SelOrg) {
|
|
@@ -8017,16 +7714,34 @@ function _odtPopulateChatSel() {
|
|
|
8017
7714
|
}
|
|
8018
7715
|
}
|
|
8019
7716
|
|
|
7717
|
+
function populateOdtChatRoleSelect() {
|
|
7718
|
+
const sel = document.getElementById('odt-chat-role-sel');
|
|
7719
|
+
if (!sel) return;
|
|
7720
|
+
const roles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
7721
|
+
const prev = sel.value;
|
|
7722
|
+
sel.innerHTML = '<option value="">boss (default)</option>' +
|
|
7723
|
+
roles.map(r => `<option value="${esc(r.id)}">${esc(r.title || r.id)}</option>`).join('');
|
|
7724
|
+
if (roles.some(r => r.id === prev)) sel.value = prev;
|
|
7725
|
+
}
|
|
7726
|
+
|
|
8020
7727
|
window.odtSendChatMessage = async function() {
|
|
8021
7728
|
const inp = document.getElementById('odt-chat-input');
|
|
8022
7729
|
const btn = document.getElementById('odt-chat-send-btn');
|
|
7730
|
+
const roleSel = document.getElementById('odt-chat-role-sel');
|
|
8023
7731
|
if (!inp || !_v2SelOrg) return;
|
|
8024
7732
|
const text = inp.value.trim();
|
|
8025
7733
|
if (!text) return;
|
|
7734
|
+
const role = roleSel ? roleSel.value : '';
|
|
8026
7735
|
inp.value = '';
|
|
8027
7736
|
btn.disabled = true;
|
|
8028
7737
|
try {
|
|
8029
|
-
|
|
7738
|
+
const _sendQ = DIR ? '?dir=' + encodeURIComponent(DIR) : '';
|
|
7739
|
+
const res = await fetch('/api/orgs/'+encodeURIComponent(_v2SelOrg)+'/chat'+_sendQ, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ text, role: role || undefined, runId: _odtChatCurrentId || null }) });
|
|
7740
|
+
const data = await res.json().catch(() => ({}));
|
|
7741
|
+
if (!data.delivered) {
|
|
7742
|
+
const lbl = document.getElementById('odt-chat-live-lbl');
|
|
7743
|
+
if (lbl) { const prev = lbl.textContent; lbl.textContent = 'QUEUED (org offline)'; setTimeout(() => { lbl.textContent = prev; }, 4000); }
|
|
7744
|
+
}
|
|
8030
7745
|
} catch(e) {
|
|
8031
7746
|
inp.value = text;
|
|
8032
7747
|
} finally {
|
|
@@ -9183,6 +8898,7 @@ window.v2DeleteOrg = async function() {
|
|
|
9183
8898
|
if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.error || 'HTTP ' + r.status); }
|
|
9184
8899
|
showToast('Deleted', `Org "${name}" deleted`, 'ok');
|
|
9185
8900
|
_v2SelOrg = null;
|
|
8901
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Delete'; }
|
|
9186
8902
|
const inner = document.getElementById('org-detail-inner');
|
|
9187
8903
|
if (inner) inner.style.display = 'none';
|
|
9188
8904
|
const noSel = document.querySelector('.orgs-no-sel');
|