@monoes/monomindcli 1.12.0 → 1.14.0
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/.claude/agents/generated/churn-analyst.md +53 -0
- package/.claude/agents/generated/code-reviewer.md +55 -0
- package/.claude/agents/generated/code-validator.md +57 -0
- package/.claude/agents/generated/complexity-scanner.md +56 -0
- package/.claude/agents/generated/devbot-orchestrator.md +58 -0
- package/.claude/agents/generated/devbot-planner.md +63 -0
- package/.claude/agents/generated/impact-assessor.md +54 -0
- package/.claude/commands/mastermind/master.md +88 -24
- package/.claude/helpers/control-start.cjs +60 -1
- package/.claude/helpers/event-logger.cjs +43 -2
- package/.claude/helpers/handlers/capture-handler.cjs +336 -0
- package/.claude/helpers/handlers/route-handler.cjs +20 -13
- package/.claude/helpers/handlers/session-restore-handler.cjs +14 -8
- package/.claude/helpers/hook-handler.cjs +57 -1
- package/.claude/helpers/intelligence.cjs +129 -57
- package/.claude/helpers/memory-palace.cjs +461 -0
- package/.claude/helpers/memory.cjs +134 -15
- package/.claude/helpers/metrics-db.mjs +87 -0
- package/.claude/helpers/router.cjs +296 -41
- package/.claude/helpers/session.cjs +107 -32
- package/.claude/helpers/statusline.cjs +138 -2
- package/.claude/helpers/toggle-statusline.cjs +73 -0
- package/.claude/helpers/token-tracker.cjs +934 -0
- package/.claude/helpers/utils/monograph.cjs +39 -4
- package/.claude/helpers/utils/telemetry.cjs +3 -3
- package/.claude/skills/mastermind/createorg.md +227 -16
- package/.claude/skills/mastermind/idea.md +15 -3
- package/.claude/skills/mastermind/runorg.md +2 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +96 -4
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/org.d.ts +4 -0
- package/dist/src/commands/org.d.ts.map +1 -0
- package/dist/src/commands/org.js +93 -0
- package/dist/src/commands/org.js.map +1 -0
- package/dist/src/mcp-tools/memory-tools.js +6 -6
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/monograph-tools.js +329 -37
- package/dist/src/mcp-tools/monograph-tools.js.map +1 -1
- package/dist/src/mcp-tools/session-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/session-tools.js +9 -10
- package/dist/src/mcp-tools/session-tools.js.map +1 -1
- package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/task-tools.js +7 -8
- package/dist/src/mcp-tools/task-tools.js.map +1 -1
- package/dist/src/mcp-tools/types.d.ts +1 -0
- package/dist/src/mcp-tools/types.d.ts.map +1 -1
- package/dist/src/mcp-tools/types.js +49 -0
- package/dist/src/mcp-tools/types.js.map +1 -1
- package/dist/src/services/worker-daemon.d.ts.map +1 -1
- package/dist/src/services/worker-daemon.js +295 -5
- package/dist/src/services/worker-daemon.js.map +1 -1
- package/dist/src/transfer/serialization/cfp.js +1 -1
- package/dist/src/transfer/serialization/cfp.js.map +1 -1
- package/dist/src/ui/dashboard.html +2235 -178
- package/dist/src/ui/orgs.html +1 -0
- package/dist/src/ui/server.mjs +532 -133
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -302,16 +302,46 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
302
302
|
.odh-pill { font-size: 11px; padding: 2px 8px; border-radius: 8px; background: var(--surface); color: var(--text-lo); border: 1px solid var(--border); }
|
|
303
303
|
.odh-right { margin-left: auto; display: flex; gap: 6px; }
|
|
304
304
|
|
|
305
|
-
#org-detail-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); flex-shrink: 0; padding: 0 16px; }
|
|
305
|
+
#org-detail-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); flex-shrink: 0; padding: 0 16px; overflow-x: auto; overflow-y: visible; scrollbar-width: none; }
|
|
306
|
+
#org-detail-tabs::-webkit-scrollbar { display: none; }
|
|
306
307
|
.odt-btn { font-size: 12px; padding: 8px 12px; background: none; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; color: var(--text-lo); font-family: var(--sans); transition: color 0.1s; }
|
|
307
308
|
.odt-btn:hover { color: var(--text-hi); }
|
|
308
|
-
.odt-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
309
|
+
.odt-btn.active { color: var(--accent); border-bottom-color: var(--accent); opacity: 1; }
|
|
310
|
+
.odt-btn.secondary { opacity: 0.6; }
|
|
311
|
+
.odt-btn.secondary.active { opacity: 1; }
|
|
312
|
+
.odt-tab-sep { width: 1px; background: var(--border); margin: 6px 4px; flex-shrink: 0; align-self: stretch; }
|
|
309
313
|
|
|
310
314
|
#org-detail-body { flex: 1; overflow-y: auto; padding: 18px 20px; }
|
|
311
315
|
#org-detail-body::-webkit-scrollbar { width: 3px; }
|
|
312
316
|
#org-detail-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
313
317
|
.odt-pane { display: none; }
|
|
314
318
|
.odt-pane.active { display: block; }
|
|
319
|
+
#odt-chat { display: none; }
|
|
320
|
+
#odt-chat.active { display: flex; flex-direction: column; }
|
|
321
|
+
#odt-chat-bar { display:flex; align-items:center; gap:8px; padding:10px 16px 8px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
|
|
322
|
+
#odt-chat-sess-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
|
|
323
|
+
#odt-chat-sel { background:var(--surface); color:var(--text-mid); border:1px solid var(--border); border-radius:4px; font-size:10px; font-family:var(--mono); padding:3px 7px; cursor:pointer; max-width:320px; }
|
|
324
|
+
#odt-chat-sel:focus { outline:none; border-color:var(--accent); }
|
|
325
|
+
#odt-chat-live-dot { width:5px; height:5px; border-radius:50%; background:var(--text-xs); flex-shrink:0; margin-left:auto; transition:background 0.4s; }
|
|
326
|
+
#odt-chat-live-dot.on { background:oklch(68% 0.20 150); animation:livepulse-cv 2.2s ease-in-out infinite; }
|
|
327
|
+
#odt-chat-live-lbl { font-size:9px; color:var(--text-lo); }
|
|
328
|
+
#odt-chat-agent-bar { display:flex; align-items:center; gap:6px; padding:5px 16px 6px; border-bottom:1px solid var(--border); flex-shrink:0; flex-wrap:wrap; }
|
|
329
|
+
#odt-chat-agent-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
|
|
330
|
+
.odt-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; }
|
|
331
|
+
.odt-agent-pill:hover { border-color:oklch(62% 0.20 186 / 0.4); color:var(--text-mid); }
|
|
332
|
+
.odt-agent-pill.dim { opacity:0.35; }
|
|
333
|
+
.odt-agent-pill.dim:hover { opacity:0.6; }
|
|
334
|
+
.odt-agent-pill.active { border-color:oklch(62% 0.20 186); color:oklch(62% 0.20 186); background:oklch(62% 0.20 186 / 0.07); }
|
|
335
|
+
#odt-chat-feed { flex:1; overflow-y:auto; padding:10px 16px; display:flex; flex-direction:column; gap:5px; scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
|
|
336
|
+
#odt-chat-feed::-webkit-scrollbar { width:4px; }
|
|
337
|
+
#odt-chat-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
|
|
338
|
+
#odt-chat-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
|
|
339
|
+
.cv-excerpt-banner { flex-shrink:0; padding:7px 16px 8px; background:oklch(13% 0.01 295); border-bottom:1px solid var(--border); display:none; }
|
|
340
|
+
.cv-excerpt-banner.visible { display:block; }
|
|
341
|
+
.cv-excerpt-label { font-size:8px; letter-spacing:2px; color:var(--text-xs); text-transform:uppercase; margin-bottom:4px; }
|
|
342
|
+
.cv-excerpt-items { display:flex; flex-direction:column; gap:2px; }
|
|
343
|
+
.cv-excerpt-item { font-size:10px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
|
|
344
|
+
.cv-excerpt-item .cv-ex-tag { font-size:8px; color:var(--text-xs); margin-right:5px; display:inline-block; min-width:30px; }
|
|
315
345
|
|
|
316
346
|
/* agent detail drawer (slides in from right within the org detail pane) */
|
|
317
347
|
#org-agent-drawer {
|
|
@@ -693,6 +723,18 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
693
723
|
#chat-v-feed::-webkit-scrollbar { width:4px; }
|
|
694
724
|
#chat-v-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
|
|
695
725
|
#chat-v-empty { font-size:11px; color:var(--text-lo); text-align:center; padding:32px 0; line-height:2; }
|
|
726
|
+
#chat-v-excerpt { flex-shrink:0; padding:7px 18px 8px; background:oklch(13% 0.01 295); border-bottom:1px solid var(--border); display:none; }
|
|
727
|
+
#chat-v-excerpt.visible { display:block; }
|
|
728
|
+
#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; }
|
|
729
|
+
#chat-v-agent-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
|
|
730
|
+
.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; }
|
|
731
|
+
.cv-agent-pill:hover { border-color:oklch(62% 0.20 186 / 0.4); color:var(--text-mid); }
|
|
732
|
+
.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); }
|
|
733
|
+
.cv-session-hdr { padding:10px 18px 8px; border-bottom:1px solid var(--border); margin-bottom:4px; background:oklch(13% 0.01 295); }
|
|
734
|
+
.cv-shdr-prompt { font-size:12px; color:var(--text-mid); margin-bottom:4px; font-weight:500; }
|
|
735
|
+
.cv-shdr-meta { display:flex; flex-wrap:wrap; gap:6px; align-items:center; font-size:9px; color:var(--text-lo); font-family:var(--mono); }
|
|
736
|
+
.cv-shdr-id { color:var(--text-xs); font-size:8px; }
|
|
737
|
+
.cv-shdr-sep { color:var(--border); }
|
|
696
738
|
.cv-msg { display:flex; flex-direction:column; max-width:90%; }
|
|
697
739
|
.cv-msg.cv-sys { align-self:center; max-width:100%; }
|
|
698
740
|
.cv-msg.cv-agent { align-self:flex-start; }
|
|
@@ -1367,9 +1409,6 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1367
1409
|
<div class="nav-item" data-view="monograph">
|
|
1368
1410
|
<span class="ico">⬡</span><span class="lbl">Monograph</span>
|
|
1369
1411
|
</div>
|
|
1370
|
-
<div class="nav-item" data-view="chat">
|
|
1371
|
-
<span class="ico">⌘</span><span class="lbl">Agent Chat</span>
|
|
1372
|
-
</div>
|
|
1373
1412
|
</div>
|
|
1374
1413
|
</div>
|
|
1375
1414
|
<div class="nav-no-proj" id="nav-no-proj-hint">Select a project above</div>
|
|
@@ -1377,13 +1416,16 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1377
1416
|
<div class="nav-item" data-view="global">
|
|
1378
1417
|
<span class="ico">⊕</span><span class="lbl">Global Feed</span>
|
|
1379
1418
|
</div>
|
|
1380
|
-
<div class="nav-item" data-view="global-loops" title="
|
|
1419
|
+
<div class="nav-item" data-view="global-loops" title="Loops across all projects">
|
|
1381
1420
|
<span class="ico">↺</span><span class="lbl">Global Loops</span>
|
|
1382
1421
|
<span class="bdg" id="bdg-global-loops">—</span>
|
|
1383
1422
|
</div>
|
|
1384
|
-
<div class="nav-item" data-view="global-tokens" title="
|
|
1423
|
+
<div class="nav-item" data-view="global-tokens" title="Token usage across all projects">
|
|
1385
1424
|
<span class="ico">$</span><span class="lbl">Global Tokens</span>
|
|
1386
1425
|
</div>
|
|
1426
|
+
<div class="nav-item" data-view="chat" title="All agent sessions across projects">
|
|
1427
|
+
<span class="ico">⌘</span><span class="lbl">Global Agent Chat</span>
|
|
1428
|
+
</div>
|
|
1387
1429
|
</div>
|
|
1388
1430
|
</div>
|
|
1389
1431
|
<div id="sb-footer">
|
|
@@ -1796,72 +1838,53 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1796
1838
|
<span class="odh-pill" id="odh-topo">—</span>
|
|
1797
1839
|
<span class="odh-pill" id="odh-roles">0 roles</span>
|
|
1798
1840
|
<div class="odh-right">
|
|
1841
|
+
<button class="btn" onclick="v2ExportOrg()" title="Export org as JSON file" style="color:var(--text-lo);border-color:var(--border)">Export</button>
|
|
1842
|
+
<button class="btn" onclick="v2ImportOrgTrigger()" title="Import org from JSON file" style="color:var(--text-lo);border-color:var(--border)">Import</button>
|
|
1799
1843
|
<button class="btn" id="org-copy-btn" onclick="v2ShowCopyOrgDialog()" style="color:var(--accent);border-color:var(--accent)">Copy to…</button>
|
|
1800
1844
|
<button class="btn" id="org-stop-btn" onclick="v2StopOrg()" style="display:none;color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Stop</button>
|
|
1845
|
+
<button class="btn" id="org-delete-btn" onclick="v2DeleteOrg()" title="Permanently delete this org and all its data" style="color:var(--red);border-color:oklch(60% 0.18 25 / 0.4)">Delete</button>
|
|
1801
1846
|
</div>
|
|
1802
1847
|
</div>
|
|
1803
1848
|
<div id="org-detail-tabs">
|
|
1804
1849
|
<button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
|
|
1805
|
-
<button class="odt-btn" data-tab="
|
|
1806
|
-
<button class="odt-btn" data-tab="
|
|
1807
|
-
<button class="odt-btn" data-tab="
|
|
1808
|
-
<
|
|
1809
|
-
<button class="odt-btn" data-tab="
|
|
1810
|
-
|
|
1850
|
+
<button class="odt-btn" data-tab="chat" onclick="v2SwitchOrgTab('chat')">Chat</button>
|
|
1851
|
+
<button class="odt-btn" data-tab="agents-full" onclick="v2SwitchOrgTab('agents-full')">Agents</button>
|
|
1852
|
+
<button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
|
|
1853
|
+
<div class="odt-tab-sep" title="Secondary tabs"></div>
|
|
1854
|
+
<button class="odt-btn secondary" data-tab="skills" onclick="v2SwitchOrgTab('skills')">Skills</button>
|
|
1855
|
+
|
|
1856
|
+
<button class="odt-btn secondary" data-tab="costs" onclick="v2SwitchOrgTab('costs')">Costs</button>
|
|
1857
|
+
<button class="odt-btn secondary" data-tab="config" onclick="v2SwitchOrgTab('config')">Config</button>
|
|
1858
|
+
<button class="odt-btn secondary" data-tab="files" onclick="v2SwitchOrgTab('files')">Files</button>
|
|
1811
1859
|
</div>
|
|
1812
1860
|
<div id="org-detail-body">
|
|
1813
1861
|
<div class="odt-pane active" id="odt-chart"></div>
|
|
1814
|
-
<div class="odt-pane" id="odt-
|
|
1862
|
+
<div class="odt-pane" id="odt-agents-full"></div>
|
|
1815
1863
|
<div class="odt-pane" id="odt-activity"></div>
|
|
1816
|
-
<div class="odt-pane" id="odt-
|
|
1817
|
-
<div class="odt-pane" id="odt-heartbeats"></div>
|
|
1818
|
-
<div class="odt-pane" id="odt-tasks"></div>
|
|
1819
|
-
<div class="odt-pane" id="odt-costs"></div>
|
|
1820
|
-
<div class="odt-pane" id="odt-members"></div>
|
|
1821
|
-
<div class="odt-pane" id="odt-goals"></div>
|
|
1822
|
-
<div class="odt-pane" id="odt-board"></div>
|
|
1823
|
-
<div class="odt-pane" id="odt-live"></div>
|
|
1824
|
-
<div class="odt-pane" id="odt-approvals"></div>
|
|
1825
|
-
<div class="odt-pane" id="odt-secrets"></div>
|
|
1826
|
-
<div class="odt-pane" id="odt-settings"></div>
|
|
1827
|
-
<div class="odt-pane" id="odt-routines"></div>
|
|
1828
|
-
<div class="odt-pane" id="odt-myissues"></div>
|
|
1829
|
-
<div class="odt-pane" id="odt-budgets"></div>
|
|
1830
|
-
<div class="odt-pane" id="odt-plugins"></div>
|
|
1831
|
-
<div class="odt-pane" id="odt-charts"></div>
|
|
1832
|
-
<div class="odt-pane" id="odt-projects"></div>
|
|
1864
|
+
<div class="odt-pane" id="odt-chat" style="flex-direction:column;height:100%;overflow:hidden;"></div>
|
|
1833
1865
|
<div class="odt-pane" id="odt-skills"></div>
|
|
1834
|
-
<div class="odt-pane" id="odt-
|
|
1835
|
-
<div class="odt-pane" id="odt-
|
|
1836
|
-
<div class="odt-pane" id="odt-
|
|
1837
|
-
<div class="odt-pane" id="odt-environments"></div>
|
|
1838
|
-
<div class="odt-pane" id="odt-access"></div>
|
|
1839
|
-
<div class="odt-pane" id="odt-issues-full"></div>
|
|
1840
|
-
<div class="odt-pane" id="odt-join-requests"></div>
|
|
1841
|
-
<div class="odt-pane" id="odt-threads"></div>
|
|
1866
|
+
<div class="odt-pane" id="odt-costs"></div>
|
|
1867
|
+
<div class="odt-pane" id="odt-config" style="overflow-y:auto"></div>
|
|
1868
|
+
<div class="odt-pane" id="odt-files" style="overflow-y:auto"></div>
|
|
1842
1869
|
</div>
|
|
1843
1870
|
</div>
|
|
1844
|
-
<div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.
|
|
1845
|
-
<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:
|
|
1871
|
+
<div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.72);z-index:90;align-items:center;justify-content:center">
|
|
1872
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:380px;max-width:96vw;display:flex;flex-direction:column;gap:12px">
|
|
1846
1873
|
<div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
|
|
1847
|
-
<div
|
|
1874
|
+
<div style="font-size:10px;color:var(--text-lo);letter-spacing:.05em;margin-bottom:2px">SELECT PROJECT</div>
|
|
1875
|
+
<div id="org-copy-proj-list" style="max-height:160px;overflow-y:auto;border:1px solid var(--border);border-radius:4px;background:var(--bg)"></div>
|
|
1876
|
+
<div style="font-size:10px;color:var(--text-lo);letter-spacing:.05em;margin-top:2px">OR ENTER PATH</div>
|
|
1877
|
+
<input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%;box-sizing:border-box"
|
|
1878
|
+
oninput="document.querySelectorAll('#org-copy-proj-list .ocp-row').forEach(r=>r.classList.remove('ocp-sel'))"
|
|
1879
|
+
onkeydown="if(event.key==='Enter')v2DoCopyOrg();if(event.key==='Escape')document.getElementById('org-copy-dialog').style.display='none'">
|
|
1880
|
+
<div id="org-copy-status" style="font-size:11px;min-height:16px"></div>
|
|
1848
1881
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
1849
1882
|
<button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
|
|
1850
|
-
<button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
|
|
1883
|
+
<button class="btn" id="org-copy-confirm-btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
|
|
1851
1884
|
</div>
|
|
1852
1885
|
</div>
|
|
1853
1886
|
</div>
|
|
1854
|
-
|
|
1855
|
-
<div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.55);z-index:90;align-items:center;justify-content:center">
|
|
1856
|
-
<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:320px;display:flex;flex-direction:column;gap:14px">
|
|
1857
|
-
<div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
|
|
1858
|
-
<input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%">
|
|
1859
|
-
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
1860
|
-
<button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
|
|
1861
|
-
<button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
|
|
1862
|
-
</div>
|
|
1863
|
-
</div>
|
|
1864
|
-
</div>
|
|
1887
|
+
<div id="org-import-input-wrap" style="display:none"><input type="file" id="org-import-file" accept=".json" style="display:none"></div>
|
|
1865
1888
|
<div id="org-agent-drawer" aria-hidden="true">
|
|
1866
1889
|
<div id="oad-head"></div>
|
|
1867
1890
|
<div id="oad-body"></div>
|
|
@@ -2100,9 +2123,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2100
2123
|
<select id="chat-v-sel" onchange="chatVSelectSession(this.value)">
|
|
2101
2124
|
<option value="">— select a session —</option>
|
|
2102
2125
|
</select>
|
|
2126
|
+
<select id="chat-v-org-run-sel" style="display:none" onchange="chatVSelectOrgRun(this.value)" title="Select org run to replay communications">
|
|
2127
|
+
<option value="">— org run history —</option>
|
|
2128
|
+
</select>
|
|
2103
2129
|
<div id="chat-v-live-dot"></div>
|
|
2104
2130
|
<span id="chat-v-live-lbl">OFFLINE</span>
|
|
2105
2131
|
</div>
|
|
2132
|
+
<div id="chat-v-excerpt" class="cv-excerpt-banner"></div>
|
|
2106
2133
|
<div id="chat-v-feed">
|
|
2107
2134
|
<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>
|
|
2108
2135
|
</div>
|
|
@@ -2241,23 +2268,66 @@ document.getElementById('feed-scroll').addEventListener('scroll', () => {
|
|
|
2241
2268
|
userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
|
|
2242
2269
|
});
|
|
2243
2270
|
|
|
2244
|
-
function switchView(v) {
|
|
2271
|
+
function switchView(v, { updateHash = true } = {}) {
|
|
2245
2272
|
currentView = v;
|
|
2246
2273
|
document.querySelectorAll('.nav-item[data-view]').forEach(el =>
|
|
2247
2274
|
el.classList.toggle('active', el.dataset.view === v));
|
|
2248
2275
|
document.querySelectorAll('.view').forEach(el =>
|
|
2249
2276
|
el.classList.toggle('active', el.id === 'view-' + v));
|
|
2250
|
-
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:'Agent Chat' };
|
|
2277
|
+
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' };
|
|
2251
2278
|
document.getElementById('view-title').textContent = titles[v] || v;
|
|
2252
2279
|
const PROJECT = DIR ? shortPath(DIR) : 'monomind';
|
|
2253
2280
|
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' };
|
|
2254
2281
|
document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
|
|
2282
|
+
if (updateHash) _pushHash();
|
|
2255
2283
|
// Projects always re-fetches so onclick paths in cards stay current
|
|
2256
2284
|
if (v === 'projects') { renderProjects(); return; }
|
|
2257
2285
|
if (v === 'chat') { initChatView(); return; }
|
|
2258
2286
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
2259
2287
|
}
|
|
2260
2288
|
|
|
2289
|
+
// ── Hash-based routing ────────────────────────────────────────────────────────
|
|
2290
|
+
// Format: #<view> or #orgs/<orgName>/<tab>
|
|
2291
|
+
let _suppressHashChange = false;
|
|
2292
|
+
|
|
2293
|
+
function _pushHash() {
|
|
2294
|
+
let hash = currentView || 'now';
|
|
2295
|
+
if (currentView === 'orgs' && typeof _v2SelOrg !== 'undefined' && _v2SelOrg) {
|
|
2296
|
+
const tab = (typeof _v2OrgTab !== 'undefined' && _v2OrgTab) ? _v2OrgTab : 'chart';
|
|
2297
|
+
hash = 'orgs/' + encodeURIComponent(_v2SelOrg) + '/' + tab;
|
|
2298
|
+
}
|
|
2299
|
+
_suppressHashChange = true;
|
|
2300
|
+
location.hash = '#' + hash;
|
|
2301
|
+
setTimeout(() => { _suppressHashChange = false; }, 50);
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function _restoreFromHash() {
|
|
2305
|
+
if (_suppressHashChange) return;
|
|
2306
|
+
const raw = location.hash.replace(/^#/, '');
|
|
2307
|
+
if (!raw) { _pushHash(); return; }
|
|
2308
|
+
const parts = raw.split('/');
|
|
2309
|
+
const view = parts[0];
|
|
2310
|
+
if (!view) return;
|
|
2311
|
+
if (view === 'orgs') {
|
|
2312
|
+
switchView('orgs', { updateHash: false });
|
|
2313
|
+
const orgName = parts[1] ? decodeURIComponent(parts[1]) : null;
|
|
2314
|
+
const tab = parts[2] || 'chart';
|
|
2315
|
+
if (orgName) {
|
|
2316
|
+
// _v2Orgs is populated async by renderOrgs(); retry until available
|
|
2317
|
+
const _trySelect = (attempts) => {
|
|
2318
|
+
if (typeof _v2Orgs !== 'undefined' && _v2Orgs && _v2Orgs.length > 0) {
|
|
2319
|
+
v2SelectOrg(orgName).then(() => { if (tab !== 'chart') v2SwitchOrgTab(tab); });
|
|
2320
|
+
} else if (attempts > 0) {
|
|
2321
|
+
setTimeout(() => _trySelect(attempts - 1), 250);
|
|
2322
|
+
}
|
|
2323
|
+
};
|
|
2324
|
+
_trySelect(20);
|
|
2325
|
+
}
|
|
2326
|
+
} else {
|
|
2327
|
+
switchView(view, { updateHash: false });
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2261
2331
|
function renderView(v) {
|
|
2262
2332
|
if (v === 'now') { refreshNow(); return; }
|
|
2263
2333
|
if (v === 'projects') renderProjects();
|
|
@@ -2327,6 +2397,9 @@ async function init() {
|
|
|
2327
2397
|
await refreshNow();
|
|
2328
2398
|
if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
|
|
2329
2399
|
initSSE();
|
|
2400
|
+
// Restore view from URL hash (after initial data load so orgs list is ready)
|
|
2401
|
+
_restoreFromHash();
|
|
2402
|
+
window.addEventListener('hashchange', () => _restoreFromHash());
|
|
2330
2403
|
}
|
|
2331
2404
|
|
|
2332
2405
|
function _setLiveMode(mode) {
|
|
@@ -3894,42 +3967,190 @@ async function renderGlobalFeed() {
|
|
|
3894
3967
|
}
|
|
3895
3968
|
|
|
3896
3969
|
// ── agent chat view ────────────────────────────────────────
|
|
3897
|
-
let chatVSessions = {};
|
|
3970
|
+
let chatVSessions = {}; // sessionId or groupId → session/group object
|
|
3971
|
+
let chatVGroupMap = {}; // groupId → group object
|
|
3972
|
+
let chatVSessionGroupMap = {}; // sessionId → groupId (for sessions inside a group)
|
|
3898
3973
|
let chatVCurrentId = null;
|
|
3974
|
+
let chatVCurrentGroupSessions = new Set(); // session IDs belonging to the currently selected group
|
|
3899
3975
|
let chatVSseSource = null;
|
|
3976
|
+
let chatVSeenKeys = new Set();
|
|
3900
3977
|
|
|
3901
3978
|
function initChatView() {
|
|
3902
3979
|
loadChatViewSessions();
|
|
3980
|
+
loadChatViewOrgRuns();
|
|
3903
3981
|
if (!chatVSseSource) connectChatViewSSE();
|
|
3904
3982
|
}
|
|
3905
3983
|
|
|
3984
|
+
async function loadChatViewOrgRuns() {
|
|
3985
|
+
// Populate the org run history selector with recent org runs
|
|
3986
|
+
const runSel = document.getElementById('chat-v-org-run-sel');
|
|
3987
|
+
if (!runSel) return;
|
|
3988
|
+
try {
|
|
3989
|
+
const dir = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
|
|
3990
|
+
const orgs = await apiFetch('/api/orgs' + dir).catch(() => []);
|
|
3991
|
+
const orgList = Array.isArray(orgs) ? orgs : (orgs.orgs || []);
|
|
3992
|
+
const runEntries = [];
|
|
3993
|
+
await Promise.all(orgList.slice(0, 10).map(async org => {
|
|
3994
|
+
const name = org.name || org;
|
|
3995
|
+
if (!name) return;
|
|
3996
|
+
try {
|
|
3997
|
+
const runs = await apiFetch(`/api/org/${encodeURIComponent(name)}/runs${dir}`);
|
|
3998
|
+
(Array.isArray(runs) ? runs : (runs.runs || [])).slice(0, 5).forEach(r => {
|
|
3999
|
+
runEntries.push({ org: name, runId: r.runId || r.id, startedAt: r.startedAt || 0 });
|
|
4000
|
+
});
|
|
4001
|
+
} catch (_) {}
|
|
4002
|
+
}));
|
|
4003
|
+
runEntries.sort((a, b) => b.startedAt - a.startedAt);
|
|
4004
|
+
while (runSel.options.length > 1) runSel.remove(1);
|
|
4005
|
+
runEntries.slice(0, 20).forEach(r => {
|
|
4006
|
+
const opt = document.createElement('option');
|
|
4007
|
+
opt.value = JSON.stringify({ org: r.org, runId: r.runId });
|
|
4008
|
+
const ts = r.startedAt ? new Date(r.startedAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
4009
|
+
opt.textContent = r.org + '/' + (r.runId || '').slice(0, 12) + (ts ? ' ' + ts : '');
|
|
4010
|
+
runSel.appendChild(opt);
|
|
4011
|
+
});
|
|
4012
|
+
runSel.style.display = runEntries.length ? '' : 'none';
|
|
4013
|
+
} catch(e) { console.warn('chat org runs load failed', e); }
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
async function chatVSelectOrgRun(value) {
|
|
4017
|
+
if (!value) return;
|
|
4018
|
+
let org, runId;
|
|
4019
|
+
try { ({ org, runId } = JSON.parse(value)); } catch { return; }
|
|
4020
|
+
const feed = document.getElementById('chat-v-feed');
|
|
4021
|
+
const empty = document.getElementById('chat-v-empty');
|
|
4022
|
+
if (!feed) return;
|
|
4023
|
+
feed.innerHTML = '';
|
|
4024
|
+
// Clear session selection — org run replay is a different mode;
|
|
4025
|
+
// also set chatVCurrentId=null so appendChatViewEvent session-guard doesn't filter events
|
|
4026
|
+
chatVCurrentId = null;
|
|
4027
|
+
const sessEl = document.getElementById('chat-v-sel');
|
|
4028
|
+
if (sessEl) sessEl.value = '';
|
|
4029
|
+
try {
|
|
4030
|
+
const dir = DIR ? `?dir=${encodeURIComponent(DIR)}` : '';
|
|
4031
|
+
const events = await apiFetch(`/api/org/${encodeURIComponent(org)}/runs/${encodeURIComponent(runId)}${dir}`);
|
|
4032
|
+
const evArr = Array.isArray(events) ? events : (events.events || []);
|
|
4033
|
+
if (!evArr.length) { feed.appendChild(empty || document.createTextNode('No events in this run.')); return; }
|
|
4034
|
+
evArr.forEach(ev => appendChatViewEvent(ev, false));
|
|
4035
|
+
feed.scrollTop = feed.scrollHeight;
|
|
4036
|
+
} catch(e) {
|
|
4037
|
+
feed.textContent = 'Failed to load run events: ' + e.message;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
|
|
3906
4041
|
async function loadChatViewSessions() {
|
|
3907
4042
|
try {
|
|
4043
|
+
// Snapshot SSE-seeded sessions before the async fetch — the API response may not yet
|
|
4044
|
+
// include events that arrived via SSE right before the fetch, so we preserve their
|
|
4045
|
+
// buffered events and merge them back after (same pattern as _odtLoadChatSessions).
|
|
4046
|
+
const _prevChatSessions = { ...chatVSessions };
|
|
3908
4047
|
const data = await apiFetch('/api/mastermind/sessions');
|
|
3909
4048
|
chatVSessions = {};
|
|
4049
|
+
chatVGroupMap = {};
|
|
4050
|
+
chatVSessionGroupMap = {};
|
|
3910
4051
|
const sel = document.getElementById('chat-v-sel');
|
|
3911
4052
|
const prev = sel.value;
|
|
3912
4053
|
while (sel.options.length > 1) sel.remove(1);
|
|
3913
|
-
|
|
3914
|
-
sessions.
|
|
4054
|
+
// API returns a plain array (not {sessions:{...}}) — handle both shapes for safety
|
|
4055
|
+
const sessions = Array.isArray(data) ? data : Object.values(data.sessions || data || {});
|
|
4056
|
+
|
|
4057
|
+
// Normalize and merge SSE-seeded events before grouping
|
|
3915
4058
|
sessions.forEach(s => {
|
|
4059
|
+
if (_prevChatSessions[s.id] && (_prevChatSessions[s.id].events || []).length > (s.events || []).length) {
|
|
4060
|
+
s.events = _prevChatSessions[s.id].events;
|
|
4061
|
+
}
|
|
4062
|
+
if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
|
|
4063
|
+
});
|
|
4064
|
+
|
|
4065
|
+
// Group consecutive sessions with same normalized prompt + gap < 20 min → loop group
|
|
4066
|
+
const _CV_MAX_LOOP_GAP = 20 * 60 * 1000;
|
|
4067
|
+
const _cvNormPrompt = p => (p || '').replace(/^running org:\s*/i, '').toLowerCase().replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 40);
|
|
4068
|
+
const sessAsc = [...sessions].sort((a, b) => (a.startedAt || a.ts || 0) - (b.startedAt || b.ts || 0));
|
|
4069
|
+
const cvGroups = []; // [{id, sessions, events, totalEvents, status, prompt, startedAt, _newestAt, _normKey, _isGroup}]
|
|
4070
|
+
sessAsc.forEach(s => {
|
|
4071
|
+
const key = _cvNormPrompt(s.prompt);
|
|
4072
|
+
const sAt = s.startedAt || s.ts || 0;
|
|
4073
|
+
const last = cvGroups[cvGroups.length - 1];
|
|
4074
|
+
if (last && last._normKey === key && key && (sAt - last._newestAt) < _CV_MAX_LOOP_GAP) {
|
|
4075
|
+
last.sessions.push(s);
|
|
4076
|
+
last._newestAt = sAt;
|
|
4077
|
+
last.totalEvents += (s.events || []).length;
|
|
4078
|
+
if (s.status === 'running') last.status = 'running';
|
|
4079
|
+
(s.events || []).forEach(ev => last.events.push(ev));
|
|
4080
|
+
} else {
|
|
4081
|
+
cvGroups.push({
|
|
4082
|
+
id: s.id,
|
|
4083
|
+
sessions: [s],
|
|
4084
|
+
_normKey: key,
|
|
4085
|
+
_newestAt: sAt,
|
|
4086
|
+
totalEvents: (s.events || []).length,
|
|
4087
|
+
status: s.status || 'complete',
|
|
4088
|
+
prompt: s.prompt,
|
|
4089
|
+
startedAt: sAt,
|
|
4090
|
+
domains: s.domains || [],
|
|
4091
|
+
events: [...(s.events || [])],
|
|
4092
|
+
_isGroup: false
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
});
|
|
4096
|
+
// Finalize groups
|
|
4097
|
+
cvGroups.forEach(g => {
|
|
4098
|
+
if (g.sessions.length > 1) {
|
|
4099
|
+
g._isGroup = true;
|
|
4100
|
+
g.id = 'grp:' + g.sessions[0].id;
|
|
4101
|
+
g.events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
4102
|
+
}
|
|
4103
|
+
});
|
|
4104
|
+
// Sort groups newest first, register
|
|
4105
|
+
cvGroups.sort((a, b) => b._newestAt - a._newestAt);
|
|
4106
|
+
cvGroups.forEach(g => {
|
|
4107
|
+
chatVSessions[g.id] = g;
|
|
4108
|
+
chatVGroupMap[g.id] = g;
|
|
4109
|
+
g.sessions.forEach(s => {
|
|
4110
|
+
chatVSessions[s.id] = g; // session ID also routes to group
|
|
4111
|
+
if (g._isGroup) chatVSessionGroupMap[s.id] = g.id;
|
|
4112
|
+
});
|
|
3916
4113
|
const opt = document.createElement('option');
|
|
3917
|
-
opt.value =
|
|
3918
|
-
const ts =
|
|
3919
|
-
|
|
4114
|
+
opt.value = g.id;
|
|
4115
|
+
const ts = g._newestAt ? new Date(g._newestAt).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
4116
|
+
const label = (g.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 30) || g.id.slice(0, 16);
|
|
4117
|
+
const evCount = g.totalEvents || (g.events || []).length;
|
|
4118
|
+
const msgPart = evCount ? ' ' + evCount + 'ev' : '';
|
|
4119
|
+
const repPart = g._isGroup ? ' ' + g.sessions.length + 'reps' : '';
|
|
4120
|
+
opt.textContent = label + (ts ? ' ' + ts : '') + repPart + msgPart + (g.status === 'running' ? ' ●' : '');
|
|
3920
4121
|
sel.appendChild(opt);
|
|
3921
|
-
chatVSessions[s.id] = s;
|
|
3922
4122
|
});
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
if (
|
|
4123
|
+
|
|
4124
|
+
// Re-attach SSE-only sessions not yet in the API response (arrived during fetch)
|
|
4125
|
+
Object.values(_prevChatSessions).forEach(ps => {
|
|
4126
|
+
if (!chatVSessions[ps.id]) {
|
|
4127
|
+
chatVSessions[ps.id] = ps;
|
|
4128
|
+
const opt = document.createElement('option');
|
|
4129
|
+
opt.value = ps.id;
|
|
4130
|
+
const tsVal = ps.startedAt || 0;
|
|
4131
|
+
const ts = tsVal ? new Date(tsVal).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
4132
|
+
const label = (ps.prompt || '').replace(/^running org:\s*/i, '').slice(0, 40) || ps.id.slice(0, 16);
|
|
4133
|
+
opt.textContent = label + (ts ? ' ' + ts : '') + ' ●';
|
|
4134
|
+
sel.appendChild(opt);
|
|
4135
|
+
}
|
|
4136
|
+
});
|
|
4137
|
+
|
|
4138
|
+
// Resolve prev selection: may be a sessionId inside a group now
|
|
4139
|
+
const prevResolved = prev ? (chatVSessionGroupMap[prev] || (chatVSessions[prev] ? prev : null)) : null;
|
|
4140
|
+
if (prevResolved && chatVSessions[prevResolved]) {
|
|
4141
|
+
sel.value = prevResolved;
|
|
4142
|
+
if (!chatVCurrentId) chatVSelectSession(prevResolved);
|
|
4143
|
+
} else {
|
|
4144
|
+
const runningGroup = cvGroups.find(g => g.status === 'running');
|
|
4145
|
+
if (runningGroup) { sel.value = runningGroup.id; chatVSelectSession(runningGroup.id); }
|
|
4146
|
+
else if (cvGroups.length) { sel.value = cvGroups[0].id; chatVSelectSession(cvGroups[0].id); }
|
|
3927
4147
|
}
|
|
3928
4148
|
} catch(e) { console.warn('chat sessions load failed', e); }
|
|
3929
4149
|
}
|
|
3930
4150
|
|
|
3931
4151
|
function chatVSelectSession(id) {
|
|
3932
4152
|
chatVCurrentId = id;
|
|
4153
|
+
chatVCurrentGroupSessions.clear();
|
|
3933
4154
|
const feed = document.getElementById('chat-v-feed');
|
|
3934
4155
|
const empty = document.getElementById('chat-v-empty');
|
|
3935
4156
|
if (!id || !chatVSessions[id]) {
|
|
@@ -3939,24 +4160,118 @@ function chatVSelectSession(id) {
|
|
|
3939
4160
|
}
|
|
3940
4161
|
feed.innerHTML = '';
|
|
3941
4162
|
const session = chatVSessions[id];
|
|
4163
|
+
// Populate group session set so live SSE events from any rep appear in feed
|
|
4164
|
+
if (session._isGroup && session.sessions) {
|
|
4165
|
+
session.sessions.forEach(s => chatVCurrentGroupSessions.add(s.id));
|
|
4166
|
+
}
|
|
4167
|
+
// Inject session metadata header
|
|
4168
|
+
const tsVal = session.startedAt || session.ts || 0;
|
|
4169
|
+
const tsStr = tsVal ? new Date(tsVal).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
|
|
4170
|
+
const domains = (session.domains || []);
|
|
4171
|
+
const agents = domains.length ? domains.join(', ') : '—';
|
|
4172
|
+
const evCount = session._isGroup ? (session.totalEvents || (session.events || []).length) : (session.events || []).length;
|
|
4173
|
+
const agentNames = (() => {
|
|
4174
|
+
const names = new Set();
|
|
4175
|
+
(session.events || []).forEach(ev => {
|
|
4176
|
+
if (ev.agent) names.add(ev.agent);
|
|
4177
|
+
if (ev.from && ev.from !== 'system') names.add(ev.from);
|
|
4178
|
+
});
|
|
4179
|
+
return names.size ? [...names].join(', ') : agents;
|
|
4180
|
+
})();
|
|
4181
|
+
const prompt = (session.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 120) || id;
|
|
4182
|
+
const repInfo = session._isGroup ? ' · ' + session.sessions.length + ' reps' : '';
|
|
4183
|
+
const statusCls = session.status === 'running' ? 'color:oklch(65% 0.20 145)' : session.status === 'complete' ? 'color:var(--text-lo)' : 'color:oklch(65% 0.20 25)';
|
|
4184
|
+
// Compute cost + duration from events
|
|
4185
|
+
const _allEvs = session.events || [];
|
|
4186
|
+
const _totalCost = _allEvs.reduce((acc, ev) => acc + (Number(ev.cost_usd) || 0), 0);
|
|
4187
|
+
const _costStr = _totalCost > 0 ? '$' + _totalCost.toFixed(4) : '';
|
|
4188
|
+
const _startTs = session.startedAt || session.ts || 0;
|
|
4189
|
+
const _endTs = session.endedAt || (_allEvs[_allEvs.length-1]?.ts) || 0;
|
|
4190
|
+
const _durMs = _endTs > _startTs ? _endTs - _startTs : 0;
|
|
4191
|
+
const _durStr = _durMs > 60000 ? Math.round(_durMs/60000) + 'm' : _durMs > 0 ? Math.round(_durMs/1000) + 's' : '';
|
|
4192
|
+
const hdr = document.createElement('div');
|
|
4193
|
+
hdr.className = 'cv-session-hdr';
|
|
4194
|
+
hdr.innerHTML = `<div class="cv-shdr-prompt">${esc(prompt)}${repInfo ? '<span style="color:var(--text-lo);font-size:10px"> ' + esc(repInfo) + '</span>' : ''}</div>`+
|
|
4195
|
+
`<div class="cv-shdr-meta">`+
|
|
4196
|
+
`<span class="cv-shdr-id" title="${esc(id)}">${esc(id)}</span>`+
|
|
4197
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
4198
|
+
`<span style="${statusCls}">${esc(session.status || 'unknown')}</span>`+
|
|
4199
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
4200
|
+
`<span title="started">${esc(tsStr)}</span>`+
|
|
4201
|
+
(_durStr ? `<span class="cv-shdr-sep">·</span><span title="duration">${esc(_durStr)}</span>` : '')+
|
|
4202
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
4203
|
+
`<span title="agents">${esc(agentNames)}</span>`+
|
|
4204
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
4205
|
+
`<span title="event count">${evCount} events</span>`+
|
|
4206
|
+
(_costStr ? `<span class="cv-shdr-sep">·</span><span style="color:oklch(65% 0.18 50)" title="total cost">${esc(_costStr)}</span>` : '')+
|
|
4207
|
+
`</div>`;
|
|
4208
|
+
feed.appendChild(hdr);
|
|
3942
4209
|
const events = session.events || [];
|
|
3943
|
-
events.forEach(ev =>
|
|
4210
|
+
events.forEach(ev => {
|
|
4211
|
+
appendChatViewEvent(ev, false);
|
|
4212
|
+
// Pre-seed dedup set so SSE reconnect replays of historical events are swallowed
|
|
4213
|
+
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);
|
|
4214
|
+
chatVSeenKeys.add(_cvhk);
|
|
4215
|
+
});
|
|
4216
|
+
// Populate excerpt banner with last activity for the selected session
|
|
4217
|
+
const _cvExEl = document.getElementById('chat-v-excerpt');
|
|
4218
|
+
if (_cvExEl) { if (events.length) renderCvExcerptBanner('chat-v-excerpt', events, session); else _cvExEl.innerHTML = ''; }
|
|
3944
4219
|
feed.scrollTop = feed.scrollHeight;
|
|
3945
4220
|
}
|
|
3946
4221
|
|
|
3947
4222
|
function appendChatViewEvent(ev, animate) {
|
|
3948
|
-
if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId) return;
|
|
4223
|
+
if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId && !chatVCurrentGroupSessions.has(ev.session)) return;
|
|
3949
4224
|
const feed = document.getElementById('chat-v-feed');
|
|
3950
4225
|
if (!feed) return;
|
|
3951
4226
|
const empty = document.getElementById('chat-v-empty');
|
|
3952
4227
|
if (empty && feed.contains(empty)) feed.removeChild(empty);
|
|
3953
4228
|
|
|
3954
4229
|
let el;
|
|
4230
|
+
const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
|
|
3955
4231
|
const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
3956
|
-
if (ev.type === '
|
|
4232
|
+
if (ev.type === 'run:start') {
|
|
4233
|
+
el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
|
|
4234
|
+
} else if (ev.type === 'run:cycle:complete') {
|
|
4235
|
+
el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
|
|
4236
|
+
} else if (ev.type === 'intercom') {
|
|
3957
4237
|
el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
|
|
3958
|
-
} else if (ev.type === '
|
|
3959
|
-
|
|
4238
|
+
} else if (ev.type === 'org:comms') {
|
|
4239
|
+
const _cvCommsFrom = ev.from || ev.role || ev.org || '?';
|
|
4240
|
+
// Directed messages (to set, not broadcast) → intercom bubble; broadcasts → agent bubble
|
|
4241
|
+
if (ev.to && ev.to !== 'all') el = mkCVIntercom(_cvCommsFrom, ev.to, ev.msg || '', ts);
|
|
4242
|
+
else el = mkCVAgent(_cvCommsFrom, ev.msg || '', ts, 'org:comms');
|
|
4243
|
+
} else if (ev.type === 'org:agent:online') {
|
|
4244
|
+
el = mkCVSys('[' + esc(ev.org || '') + '] ' + esc(ev.role || ev.title || '?') + ' online' + (ev.title && ev.role && ev.title !== ev.role ? ' (' + esc(ev.title) + ')' : ''), ts);
|
|
4245
|
+
} else if (ev.type === 'org:checkpoint') {
|
|
4246
|
+
el = mkCVSys('[checkpoint] ' + esc(ev.summary || ev.progress || ev.msg || ''), ts);
|
|
4247
|
+
} else if (ev.type === 'org:start') {
|
|
4248
|
+
el = mkCVSys('[org:start] ' + esc(ev.org || ''), ts);
|
|
4249
|
+
} else if (ev.type === 'run:complete') {
|
|
4250
|
+
el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
|
|
4251
|
+
} else if (ev.type === 'org:complete') {
|
|
4252
|
+
el = mkCVSys('■ Org complete' + (ev.org ? ' — ' + esc(ev.org) : ''), ts);
|
|
4253
|
+
} else if (ev.type === 'agent:result') {
|
|
4254
|
+
el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
|
|
4255
|
+
} else if (ev.type === 'agent:spawn') {
|
|
4256
|
+
const _spawnTo = ev.to || ev.agentType || '?';
|
|
4257
|
+
const _spawnTask = ev.task || ev.briefing || '';
|
|
4258
|
+
el = mkCVSpawn(ev.from || 'orchestrator', _spawnTo, _spawnTask, ts);
|
|
4259
|
+
} else if (ev.type === 'agent:complete') {
|
|
4260
|
+
// Rich completion event from capture-handler (replaces/supplements org:comms)
|
|
4261
|
+
const _acAgent = ev.agentType || ev.role || ev.from || '?';
|
|
4262
|
+
const _acResult = ev.result || '';
|
|
4263
|
+
const _acCost = ev.cost_usd != null ? ' $' + Number(ev.cost_usd).toFixed(4) : '';
|
|
4264
|
+
const _acTok = (ev.tokens_in || ev.tokens_out) ? ' · ' + ((ev.tokens_in||0)+(ev.tokens_out||0)).toLocaleString() + ' tok' : '';
|
|
4265
|
+
const _acTools = (ev.toolCalls || []).length ? ' · [' + ev.toolCalls.slice(0,5).map(t => esc(t)).join(', ') + ']' : '';
|
|
4266
|
+
el = mkCVResult(_acAgent + _acCost + _acTok + _acTools, _acResult, ts);
|
|
4267
|
+
} else if (ev.type === 'agent:usage') {
|
|
4268
|
+
// Token/cost accounting event — show as compact system line
|
|
4269
|
+
const _auAgent = ev.role || ev.agentType || '?';
|
|
4270
|
+
const _auCost = ev.cost_usd != null ? '$' + Number(ev.cost_usd).toFixed(4) : '';
|
|
4271
|
+
const _auTok = (ev.tokens_in || ev.tokens_out) ? ((ev.tokens_in||0)).toLocaleString() + '↑ ' + ((ev.tokens_out||0)).toLocaleString() + '↓' : '';
|
|
4272
|
+
el = mkCVSys('◈ ' + esc(_auAgent) + (_auTok ? ' · ' + _auTok : '') + (_auCost ? ' · ' + _auCost : ''), ts);
|
|
4273
|
+
} else if (ev.type === 'agent:message') {
|
|
4274
|
+
el = mkCVAgent(ev.agent || ev.name || '?', ev.msg || ev.message || '', ts, ev.type);
|
|
3960
4275
|
} else if (ev.type === 'session:start') {
|
|
3961
4276
|
el = mkCVSys('Session started' + (ev.prompt ? ': ' + esc(ev.prompt.slice(0,80)) : ''), ts);
|
|
3962
4277
|
} else if (ev.type === 'session:complete') {
|
|
@@ -3966,23 +4281,150 @@ function appendChatViewEvent(ev, animate) {
|
|
|
3966
4281
|
} else if (ev.type === 'domain:complete') {
|
|
3967
4282
|
el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
|
|
3968
4283
|
} else if (ev.type === 'loop:start') {
|
|
3969
|
-
el = mkCVSys('Loop
|
|
4284
|
+
el = mkCVSys('◎ Loop: ' + esc(ev.command || '') + (ev.repeat ? ' ×' + esc(String(ev.repeat)) : ''), ts);
|
|
3970
4285
|
if (currentView === 'loops') renderLoops();
|
|
3971
4286
|
} else if (ev.type === 'loop:complete') {
|
|
3972
|
-
el = mkCVSys('Loop
|
|
4287
|
+
el = mkCVSys('■ Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + esc(String(ev.ranReps)) + ' runs)' : ''), ts);
|
|
3973
4288
|
if (currentView === 'loops') renderLoops();
|
|
3974
4289
|
} else if (ev.type === 'loop:tick') {
|
|
3975
|
-
|
|
4290
|
+
const _tickLbl = ev.rep != null
|
|
4291
|
+
? 'rep ' + esc(String(ev.rep)) + ' done' + (ev.wait != null ? ' → next in ' + esc(String(ev.wait)) + 's' : '')
|
|
4292
|
+
: esc(ev.command || ev.loopId || ev.id || '');
|
|
4293
|
+
el = mkCVSys('◷ ' + _tickLbl, ts);
|
|
4294
|
+
el.dataset.evType = 'loop:tick';
|
|
4295
|
+
if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
|
|
4296
|
+
if (currentView === 'loops') renderLoops();
|
|
4297
|
+
} else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
|
|
4298
|
+
el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ev.id || ''), ts);
|
|
3976
4299
|
if (currentView === 'loops') renderLoops();
|
|
3977
|
-
} else if (ev.type === 'loop:hil') {
|
|
3978
|
-
el = mkCVSys('
|
|
4300
|
+
} else if (ev.type === 'loop:hil:resolved') {
|
|
4301
|
+
el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ev.command || ''), ts);
|
|
3979
4302
|
if (currentView === 'loops') renderLoops();
|
|
4303
|
+
} else if (ev.type === 'file:write') {
|
|
4304
|
+
el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
|
|
4305
|
+
} else if (ev.type === 'org:agent:offline') {
|
|
4306
|
+
el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline' + (ev.org ? ' [' + esc(ev.org) + ']' : ''), ts);
|
|
4307
|
+
} else if (ev.type === 'org:error') {
|
|
4308
|
+
el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
|
|
4309
|
+
el.classList.add('cv-err');
|
|
3980
4310
|
} else {
|
|
3981
4311
|
el = mkCVSys(esc(ev.type || 'event'), ts);
|
|
3982
4312
|
}
|
|
3983
4313
|
if (animate) el.classList.add('cv-new');
|
|
3984
4314
|
feed.appendChild(el);
|
|
3985
|
-
feed.scrollTop = feed.scrollHeight;
|
|
4315
|
+
if (atBottom) feed.scrollTop = feed.scrollHeight;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
function _cvExcerptText(ev) {
|
|
4319
|
+
const t = ev.type || '';
|
|
4320
|
+
if (t === 'intercom' || t === 'org:comms') return (ev.from || ev.role || '?') + ' → ' + (ev.to || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
|
|
4321
|
+
if (t === 'agent:message') return (ev.agent || ev.name || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
|
|
4322
|
+
if (t === 'agent:result') return (ev.agent || ev.from || '?') + ': ' + (ev.result || ev.msg || ev.message || ev.summary || '').slice(0, 80);
|
|
4323
|
+
if (t === 'agent:spawn') return 'spawned ' + (ev.to || ev.agent || ev.role || '?') + (ev.task || ev.briefing ? ': ' + (ev.task || ev.briefing || '').slice(0, 50) : '');
|
|
4324
|
+
if (t === 'org:agent:online') return (ev.title || ev.role || '?') + ' online';
|
|
4325
|
+
if (t === 'org:agent:offline') return (ev.title || ev.role || '?') + ' offline';
|
|
4326
|
+
if (t === 'org:error') return 'error: ' + (ev.msg || ev.error || ev.message || '').slice(0, 80);
|
|
4327
|
+
if (t === 'file:write') return (ev.name || ev.path || '?') + (ev.size ? ' (' + (ev.size > 1048576 ? (ev.size/1048576).toFixed(1)+'MB' : ev.size > 1024 ? (ev.size/1024).toFixed(1)+'KB' : ev.size+'B') + ')' : '');
|
|
4328
|
+
if (t === 'org:checkpoint') return 'checkpoint: ' + (ev.summary || ev.progress || ev.msg || '').slice(0, 80) + (ev.pending_tasks != null ? ' · ' + ev.pending_tasks + ' pending' : '');
|
|
4329
|
+
if (t === 'run:complete') return 'run complete' + (ev.status ? ' [' + ev.status + ']' : '');
|
|
4330
|
+
if (t === 'run:cycle:complete') return 'cycle done' + (ev.pending != null ? ' · ' + ev.pending + ' pending' : '');
|
|
4331
|
+
if (t === 'org:start') return 'org started: ' + (ev.goal || '').slice(0, 80);
|
|
4332
|
+
if (t === 'org:complete') return 'org complete';
|
|
4333
|
+
if (t === 'session:start') return 'started: ' + (ev.prompt || '').replace(/^running org:\s*/i, '').slice(0, 80);
|
|
4334
|
+
if (t === 'session:complete') return 'completed' + (ev.status ? ' [' + ev.status + ']' : '');
|
|
4335
|
+
if (t === 'domain:dispatch') return '→ ' + (ev.domain || '') + (ev.cmd ? ': ' + ev.cmd.slice(0, 60) : '');
|
|
4336
|
+
if (t === 'domain:complete') return '✓ ' + (ev.domain || '');
|
|
4337
|
+
if (t === 'loop:tick') { const _r = ev.rep ?? ev.fromRep; return _r != null ? 'rep ' + _r + ' done' : (ev.command || ev.loopId || t); }
|
|
4338
|
+
if (t === 'loop:start') return 'loop: ' + (ev.command || '');
|
|
4339
|
+
if (t === 'loop:complete') return 'loop done: ' + (ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : '');
|
|
4340
|
+
const _content = ev.result || ev.msg || ev.message || ev.error || ev.summary || '';
|
|
4341
|
+
if (_content) return _content.slice(0, 80);
|
|
4342
|
+
return t.slice(0, 60);
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
function _cvExcerptTag(ev) {
|
|
4346
|
+
const t = ev.type || '';
|
|
4347
|
+
if (t === 'intercom' || t === 'org:comms') return 'IC';
|
|
4348
|
+
if (t === 'agent:message') return 'MSG';
|
|
4349
|
+
if (t === 'agent:result') return 'RESULT';
|
|
4350
|
+
if (t === 'agent:spawn' || t === 'org:agent:online') return 'NEW';
|
|
4351
|
+
if (t === 'org:agent:offline') return 'OFF';
|
|
4352
|
+
if (t === 'org:error') return 'ERR';
|
|
4353
|
+
if (t === 'org:checkpoint') return 'CHK';
|
|
4354
|
+
if (t === 'file:write') return 'FILE';
|
|
4355
|
+
if (t.startsWith('org:')) return 'ORG';
|
|
4356
|
+
if (t.startsWith('run:')) return 'RUN';
|
|
4357
|
+
if (t.startsWith('session:')) return 'SES';
|
|
4358
|
+
if (t.startsWith('domain:')) return 'DOM';
|
|
4359
|
+
if (t.startsWith('loop:')) return 'LOOP';
|
|
4360
|
+
if (t.startsWith('agent:')) return 'AGT';
|
|
4361
|
+
return 'EVT';
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
function renderCvExcerptBanner(bannerId, events, sessionMeta) {
|
|
4365
|
+
const banner = document.getElementById(bannerId);
|
|
4366
|
+
if (!banner) return;
|
|
4367
|
+
const SKIP = new Set(['session:start', 'run:start', 'org:start']);
|
|
4368
|
+
const meaningful = (events || []).filter(ev => !SKIP.has(ev.type)).slice(-5).reverse();
|
|
4369
|
+
if (!meaningful.length) {
|
|
4370
|
+
const prompt = sessionMeta && (sessionMeta.prompt || '');
|
|
4371
|
+
const clean = prompt.replace(/^running org:\s*/i, '').slice(0, 120);
|
|
4372
|
+
if (!clean) { banner.classList.remove('visible'); banner.innerHTML = ''; return; }
|
|
4373
|
+
banner.innerHTML = `<div class="cv-excerpt-label">SESSION GOAL</div><div class="cv-excerpt-items"><div class="cv-excerpt-item"><span class="cv-ex-tag">ORG</span>${esc(clean)}</div></div>`;
|
|
4374
|
+
banner.classList.add('visible');
|
|
4375
|
+
return;
|
|
4376
|
+
}
|
|
4377
|
+
const items = meaningful.slice(0, 3).map(ev => {
|
|
4378
|
+
const tag = _cvExcerptTag(ev);
|
|
4379
|
+
const txt = _cvExcerptText(ev);
|
|
4380
|
+
return `<div class="cv-excerpt-item"><span class="cv-ex-tag">${tag}</span>${esc(txt)}</div>`;
|
|
4381
|
+
}).join('');
|
|
4382
|
+
banner.innerHTML = `<div class="cv-excerpt-label">LAST ACTIVITY</div><div class="cv-excerpt-items">${items}</div>`;
|
|
4383
|
+
banner.classList.add('visible');
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
function mkCVSpawn(from, to, task, ts) {
|
|
4387
|
+
const d = document.createElement('div');
|
|
4388
|
+
d.className = 'cv-msg cv-spawn';
|
|
4389
|
+
const short = task.replace(/\n/g,' ').slice(0,140);
|
|
4390
|
+
d.innerHTML = `<div class="cv-bub" style="border-left:2px solid oklch(65% 0.18 280)">`+
|
|
4391
|
+
`<span class="cv-tag" style="background:oklch(25% 0.10 280);color:oklch(80% 0.18 280)">${esc(from)}</span>`+
|
|
4392
|
+
`<span class="cv-arrow" style="color:oklch(65% 0.18 280)">→</span>`+
|
|
4393
|
+
`<span class="cv-tag" style="background:oklch(25% 0.15 160);color:oklch(75% 0.20 160)">${esc(to)}</span>`+
|
|
4394
|
+
`<span class="cv-etype" style="color:oklch(65% 0.18 280)">SPAWN</span>`+
|
|
4395
|
+
`<span class="cv-text">${esc(short)}${task.length>140?'…':''}</span>`+
|
|
4396
|
+
`<span class="cv-ts">${ts}</span></div>`;
|
|
4397
|
+
return d;
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
function mkCVResult(agent, text, ts) {
|
|
4401
|
+
const d = document.createElement('div');
|
|
4402
|
+
d.className = 'cv-msg cv-result';
|
|
4403
|
+
const isLong = text.length > 300;
|
|
4404
|
+
const preview = isLong ? text.slice(0, 300) + '…' : text;
|
|
4405
|
+
const uid = 'res-' + Math.random().toString(36).slice(2,8);
|
|
4406
|
+
d.innerHTML = `<div class="cv-bub" style="border-left:2px solid oklch(68% 0.20 150)">`+
|
|
4407
|
+
`<span class="cv-tag" style="background:oklch(20% 0.10 150);color:oklch(72% 0.20 150)">${esc(agent)}</span>`+
|
|
4408
|
+
`<span class="cv-etype" style="color:oklch(68% 0.20 150)">REPORT</span>`+
|
|
4409
|
+
`<span class="cv-text" id="${uid}" style="white-space:pre-wrap;line-height:1.6">${esc(preview)}</span>`+
|
|
4410
|
+
(isLong ? `<button class="btn" style="font-size:9px;padding:1px 6px;margin-left:6px;flex-shrink:0" data-uid="${uid}" data-full="${esc(text)}" onclick="(function(b){const el=document.getElementById(b.dataset.uid);const full=b.dataset.full;const show=el.dataset.expanded;el.textContent=show?full.slice(0,300)+'…':full;el.dataset.expanded=show?'':'1';b.textContent=show?'Expand':'Collapse';})(this)">Expand</button>` : '')+
|
|
4411
|
+
`<span class="cv-ts">${ts}</span></div>`;
|
|
4412
|
+
return d;
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4415
|
+
function mkCVFileCard(name, filePath, size, ts) {
|
|
4416
|
+
const d = document.createElement('div');
|
|
4417
|
+
d.className = 'cv-msg cv-file';
|
|
4418
|
+
const sizeStr = size > 1024*1024 ? (size/1024/1024).toFixed(1)+'MB' : size > 1024 ? (size/1024).toFixed(1)+'KB' : (size||'?')+'B';
|
|
4419
|
+
const isMd = /\.md$/i.test(name);
|
|
4420
|
+
d.innerHTML = `<div class="cv-bub" style="border-left:2px solid oklch(65% 0.18 50);align-items:center;gap:8px">`+
|
|
4421
|
+
`<span style="font-size:14px">${isMd?'📄':'📁'}</span>`+
|
|
4422
|
+
`<span class="cv-tag" style="background:oklch(22% 0.10 50);color:oklch(78% 0.18 50)">FILE</span>`+
|
|
4423
|
+
`<span class="cv-text" style="font-family:var(--mono);font-size:10px">${esc(name)}</span>`+
|
|
4424
|
+
`<span style="font-size:9px;color:var(--text-lo);flex-shrink:0">${sizeStr}</span>`+
|
|
4425
|
+
(filePath ? `<button class="btn" style="font-size:9px;padding:1px 6px;flex-shrink:0;border-color:oklch(65% 0.18 50);color:oklch(65% 0.18 50)" data-fp="${esc(filePath)}" data-fn="${esc(name)}" onclick="v2ViewOrgFile(this.dataset.fp,this.dataset.fn)">View</button>` : '')+
|
|
4426
|
+
`<span class="cv-ts">${ts}</span></div>`;
|
|
4427
|
+
return d;
|
|
3986
4428
|
}
|
|
3987
4429
|
|
|
3988
4430
|
function mkCVSys(html, ts) {
|
|
@@ -3996,14 +4438,14 @@ function mkCVAgent(name, text, ts, typeTag) {
|
|
|
3996
4438
|
const d = document.createElement('div');
|
|
3997
4439
|
d.className = 'cv-msg cv-agent';
|
|
3998
4440
|
const tag = typeTag === 'agent:spawn' ? 'SPAWN' : 'MSG';
|
|
3999
|
-
d.innerHTML = `<div class="cv-bub"><span class="cv-tag">${esc(name)}</span><span class="cv-etype">${tag}</span><span class="cv-text">${esc(String(text).slice(0,
|
|
4441
|
+
d.innerHTML = `<div class="cv-bub"><span class="cv-tag">${esc(name)}</span><span class="cv-etype">${tag}</span><span class="cv-text">${esc(String(text).slice(0,800))}</span><span class="cv-ts">${ts}</span></div>`;
|
|
4000
4442
|
return d;
|
|
4001
4443
|
}
|
|
4002
4444
|
|
|
4003
4445
|
function mkCVIntercom(from, to, text, ts) {
|
|
4004
4446
|
const d = document.createElement('div');
|
|
4005
4447
|
d.className = 'cv-msg cv-ic';
|
|
4006
|
-
d.innerHTML = `<div class="cv-bub"><span class="cv-tag cv-sender">${esc(from||'?')}</span><span class="cv-arrow">→</span><span class="cv-tag cv-receiver">${esc(to||'?')}</span><span class="cv-etype">IC</span><span class="cv-text">${esc(String(text).slice(0,
|
|
4448
|
+
d.innerHTML = `<div class="cv-bub"><span class="cv-tag cv-sender">${esc(from||'?')}</span><span class="cv-arrow">→</span><span class="cv-tag cv-receiver">${esc(to||'?')}</span><span class="cv-etype">IC</span><span class="cv-text">${esc(String(text).slice(0,800))}</span><span class="cv-ts">${ts}</span></div>`;
|
|
4007
4449
|
return d;
|
|
4008
4450
|
}
|
|
4009
4451
|
|
|
@@ -4019,24 +4461,69 @@ function connectChatViewSSE() {
|
|
|
4019
4461
|
chatVSseSource.onerror = () => {
|
|
4020
4462
|
dot && dot.classList.remove('on');
|
|
4021
4463
|
lbl && (lbl.textContent = 'OFFLINE');
|
|
4022
|
-
chatVSseSource
|
|
4464
|
+
const _cvSrc = chatVSseSource;
|
|
4023
4465
|
chatVSseSource = null;
|
|
4466
|
+
if (_cvSrc) _cvSrc.close();
|
|
4024
4467
|
setTimeout(connectChatViewSSE, 5000);
|
|
4025
4468
|
};
|
|
4026
4469
|
}
|
|
4027
4470
|
|
|
4028
4471
|
function handleChatViewEvent(ev) {
|
|
4029
|
-
if (!ev
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4472
|
+
if (!ev) return;
|
|
4473
|
+
// Deduplicate SSE reconnect replays (server replays last 50 events on every reconnect)
|
|
4474
|
+
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);
|
|
4475
|
+
if (chatVSeenKeys.has(dedupKey)) return;
|
|
4476
|
+
chatVSeenKeys.add(dedupKey);
|
|
4477
|
+
if (chatVSeenKeys.size > 2000) { const _a = [...chatVSeenKeys]; chatVSeenKeys = new Set(_a.slice(1000)); }
|
|
4478
|
+
// Org events (org:comms, org:agent:online, org:checkpoint, etc.) may carry a session field
|
|
4479
|
+
// from the org run's session ID — route them directly to the feed if the session matches,
|
|
4480
|
+
// or append them unconditionally when the chat view is showing the "live" placeholder.
|
|
4481
|
+
const isOrgEvent = ev.type && (ev.type.startsWith('org:') || ev.type.startsWith('run:') || ev.type.startsWith('loop:'));
|
|
4482
|
+
if (!ev.session && !isOrgEvent) return;
|
|
4483
|
+
if (ev.session) {
|
|
4484
|
+
const existingEntry = chatVSessions[ev.session]; // may be a group object
|
|
4485
|
+
if (!existingEntry) {
|
|
4486
|
+
// Seed events with the triggering event so it isn't lost if loadChatViewSessions
|
|
4487
|
+
// runs before the event is flushed to disk (disk write and SSE broadcast race).
|
|
4488
|
+
chatVSessions[ev.session] = { id: ev.session, events: [ev], startedAt: ev.ts, status: 'running', prompt: ev.prompt || '' };
|
|
4489
|
+
loadChatViewSessions();
|
|
4490
|
+
// Auto-switch to new session on session:start — after loadChatViewSessions it may be grouped
|
|
4491
|
+
if (ev.type === 'session:start') {
|
|
4492
|
+
const _cvGroupId = chatVSessionGroupMap[ev.session] || ev.session;
|
|
4493
|
+
const _cvSel = document.getElementById('chat-v-sel');
|
|
4494
|
+
if (_cvSel) { _cvSel.value = _cvGroupId; chatVSelectSession(_cvGroupId); }
|
|
4495
|
+
}
|
|
4496
|
+
} else {
|
|
4497
|
+
// existingEntry may be a group — push event to it and update status
|
|
4498
|
+
const target = existingEntry;
|
|
4499
|
+
target.events = target.events || [];
|
|
4500
|
+
target.events.push(ev);
|
|
4501
|
+
if (ev.type === 'session:complete') {
|
|
4502
|
+
// Mark this individual session complete; group stays running until all sessions done
|
|
4503
|
+
if (!target._isGroup) target.status = 'complete';
|
|
4504
|
+
else {
|
|
4505
|
+
// Update the underlying session in group
|
|
4506
|
+
const sub = (target.sessions || []).find(s => s.id === ev.session);
|
|
4507
|
+
if (sub) sub.status = 'complete';
|
|
4508
|
+
// Group stays running if any session is still running
|
|
4509
|
+
if (!(target.sessions || []).some(s => s.status === 'running')) target.status = 'complete';
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
if (target._isGroup) target.totalEvents = (target.totalEvents || 0) + 1;
|
|
4513
|
+
}
|
|
4514
|
+
// Show event in feed if this session (or its group) is currently selected
|
|
4515
|
+
const _cvGroupId = chatVSessionGroupMap[ev.session];
|
|
4516
|
+
const _cvIsSelected = chatVCurrentId === ev.session || (_cvGroupId && chatVCurrentId === _cvGroupId) || chatVCurrentGroupSessions.has(ev.session);
|
|
4517
|
+
if (_cvIsSelected) {
|
|
4518
|
+
appendChatViewEvent(ev, true);
|
|
4519
|
+
const _cvSess = chatVSessions[_cvGroupId || ev.session];
|
|
4520
|
+
if (_cvSess) renderCvExcerptBanner('chat-v-excerpt', _cvSess.events || [], _cvSess);
|
|
4521
|
+
}
|
|
4522
|
+
} else if (isOrgEvent) {
|
|
4523
|
+
// Org events without a Claude session ID — append to feed regardless of active view;
|
|
4524
|
+
// #chat-v-feed is in the DOM even when hidden, so events accumulate correctly.
|
|
4525
|
+
appendChatViewEvent(ev, true);
|
|
4038
4526
|
}
|
|
4039
|
-
if (chatVCurrentId === ev.session) appendChatViewEvent(ev, true);
|
|
4040
4527
|
}
|
|
4041
4528
|
|
|
4042
4529
|
// ── feature 4: budget cap + desktop notification ───────────
|
|
@@ -4685,7 +5172,7 @@ async function renderLoops() {
|
|
|
4685
5172
|
el.innerHTML = loops.map((l, idx) => {
|
|
4686
5173
|
const maxReps = l.maxReps || 0;
|
|
4687
5174
|
const curRep = l.currentRep || 0;
|
|
4688
|
-
const isTillend = !maxReps || l.
|
|
5175
|
+
const isTillend = !maxReps || l.type === 'tillend' || String(l.command || '').includes('--tillend');
|
|
4689
5176
|
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
|
|
4690
5177
|
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
4691
5178
|
const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
|
|
@@ -4704,7 +5191,7 @@ async function renderLoops() {
|
|
|
4704
5191
|
const runs = curRep != null ? curRep : '—';
|
|
4705
5192
|
const pct = (maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
|
|
4706
5193
|
const progBar = isTillend
|
|
4707
|
-
? `<div class="lp-bar" title="tillend loop"><div class="lp-fill lp-fill-inf" style="width:100%;opacity:0.3;background:linear-gradient(90deg,var(--accent),transparent)"></div><span style="position:absolute;left:6px;top:0;font-size:9px;color:var(--text-lo)">run ${curRep} / ∞${
|
|
5194
|
+
? `<div class="lp-bar" title="tillend loop"><div class="lp-fill lp-fill-inf" style="width:100%;opacity:0.3;background:linear-gradient(90deg,var(--accent),transparent)"></div><span style="position:absolute;left:6px;top:0;font-size:9px;color:var(--text-lo)">run ${curRep} / ∞${maxReps ? ' (cap: '+maxReps+')' : ''}</span></div>`
|
|
4708
5195
|
: (maxReps > 0 && running)
|
|
4709
5196
|
? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
|
|
4710
5197
|
: '';
|
|
@@ -4734,8 +5221,8 @@ async function renderLoops() {
|
|
|
4734
5221
|
${stopBtn}
|
|
4735
5222
|
</div>
|
|
4736
5223
|
<div class="loop-expand">
|
|
4737
|
-
${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(
|
|
4738
|
-
${command && command !== fullPrompt ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(
|
|
5224
|
+
${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="cursor:pointer" data-cp="${esc(fullPrompt)}" onclick="navigator.clipboard.writeText(this.dataset.cp).then(()=>showToast('Copied','','ok'))">${esc(fullPrompt.slice(0, 300))}</div></div>` : ''}
|
|
5225
|
+
${command && command !== fullPrompt ? `<div class="le-row"><div class="le-lbl">Command</div><div class="le-val mono" style="cursor:pointer" data-cp="${esc(command)}" onclick="navigator.clipboard.writeText(this.dataset.cp).then(()=>showToast('Copied','','ok'))">${esc(command.slice(0, 200))}</div></div>` : ''}
|
|
4739
5226
|
<div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${isTillend ? '∞ tillend' : '↺ repeat'}</div></div>
|
|
4740
5227
|
<div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${interval}</div></div>
|
|
4741
5228
|
<div class="le-row"><div class="le-lbl">Status</div><div class="le-val">${isFinished ? '✓ done' : isHil ? '⚠ hil:pending' : running ? '● running' : '○ stopped'}</div></div>
|
|
@@ -5112,7 +5599,9 @@ function v2RenderOrgList() {
|
|
|
5112
5599
|
|
|
5113
5600
|
async function v2SelectOrg(name) {
|
|
5114
5601
|
_v2SelOrg = name;
|
|
5602
|
+
_pushHash();
|
|
5115
5603
|
if (typeof v2CloseAgent === 'function') v2CloseAgent();
|
|
5604
|
+
if (_orgLiveInterval) { clearInterval(_orgLiveInterval); _orgLiveInterval = null; }
|
|
5116
5605
|
const requested = name;
|
|
5117
5606
|
const requestedDir = DIR;
|
|
5118
5607
|
document.querySelectorAll('.org-item').forEach(el => {
|
|
@@ -5147,11 +5636,14 @@ async function v2SelectOrg(name) {
|
|
|
5147
5636
|
_v2OrgData = mainR.config ? { ...mainR, ...mainR.config, running: mainR.running } : mainR;
|
|
5148
5637
|
_v2OrgData._activity = Array.isArray(actR) ? actR : [];
|
|
5149
5638
|
_v2OrgData._health = healthR;
|
|
5150
|
-
// Fetch supplemental data for new tabs
|
|
5151
|
-
const agentsR
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5639
|
+
// Fetch supplemental data for new tabs in parallel
|
|
5640
|
+
const [agentsR, budgetsR, membersR, issuesR] = await Promise.all([
|
|
5641
|
+
fetch(`/api/org/${_enc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
5642
|
+
fetch(`/api/org/${_enc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():null).catch(()=>null),
|
|
5643
|
+
fetch(`/api/org/${_enc}/members${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
5644
|
+
fetch(`/api/org/${_enc}/issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r=>r.ok?r.json():[]).catch(()=>[]),
|
|
5645
|
+
]);
|
|
5646
|
+
if (_v2SelOrg !== requested || DIR !== requestedDir) return;
|
|
5155
5647
|
_v2OrgData._agents = Array.isArray(agentsR) ? agentsR : (agentsR?.agents || []);
|
|
5156
5648
|
_v2OrgData._budgets = budgetsR;
|
|
5157
5649
|
_v2OrgData._members = Array.isArray(membersR) ? membersR : (membersR?.members || []);
|
|
@@ -5190,6 +5682,7 @@ window.v2SwitchOrgTab = function(tab) {
|
|
|
5190
5682
|
});
|
|
5191
5683
|
document.querySelectorAll('.odt-pane').forEach(p => p.classList.remove('active'));
|
|
5192
5684
|
document.getElementById('odt-' + tab)?.classList.add('active');
|
|
5685
|
+
_pushHash();
|
|
5193
5686
|
v2RenderOrgTab(tab);
|
|
5194
5687
|
};
|
|
5195
5688
|
|
|
@@ -5228,11 +5721,15 @@ function v2RenderOrgTab(tab) {
|
|
|
5228
5721
|
else if (tab === 'issues-full') v2RenderOrgIssuesFull();
|
|
5229
5722
|
else if (tab === 'join-requests') v2RenderOrgJoinRequests();
|
|
5230
5723
|
else if (tab === 'threads') v2RenderOrgThreads();
|
|
5724
|
+
else if (tab === 'chat') v2RenderOrgChat();
|
|
5725
|
+
else if (tab === 'config') v2RenderOrgConfig();
|
|
5726
|
+
else if (tab === 'files') v2RenderOrgFiles();
|
|
5231
5727
|
}
|
|
5232
5728
|
|
|
5233
5729
|
// ── org chart ───────────────────────────────
|
|
5234
5730
|
function _v2OrgIsLeader(r) {
|
|
5235
|
-
return r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner'
|
|
5731
|
+
return r.isBoss === true || r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner'
|
|
5732
|
+
|| r.agent_type === 'coordinator' || r.agent_type === 'devbot-orchestrator';
|
|
5236
5733
|
}
|
|
5237
5734
|
|
|
5238
5735
|
function v2RenderOrgChart() {
|
|
@@ -5420,22 +5917,27 @@ function v2RenderOrgChart() {
|
|
|
5420
5917
|
`<clipPath id="${nodeClipIds[i]}"><circle r="${Math.round(R * 0.52)}"/></clipPath>`
|
|
5421
5918
|
).join('');
|
|
5422
5919
|
|
|
5920
|
+
// Overlay live running status from in-memory agent SSE state for chart node indicators
|
|
5921
|
+
const _chartRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
5423
5922
|
let nodesHTML = '';
|
|
5424
5923
|
roles.forEach((role, i) => {
|
|
5425
5924
|
const p = pos[role.id];
|
|
5426
5925
|
if (!p) return;
|
|
5427
5926
|
const leader = _v2OrgIsLeader(role);
|
|
5428
5927
|
const color = colorOf(role, i);
|
|
5928
|
+
const isRunning = _chartRunning.has(role.id);
|
|
5429
5929
|
const displayName = role.name || role.title || role.id;
|
|
5430
5930
|
const subType = role.type || role.agent_type || '';
|
|
5431
5931
|
const MAX_LBL = Math.floor(R / 4.2);
|
|
5432
5932
|
const nameText = displayName.length > MAX_LBL ? displayName.slice(0, MAX_LBL - 1) + '…' : displayName;
|
|
5433
5933
|
const subTypeText = subType.length > MAX_LBL ? subType.slice(0, MAX_LBL - 1) + '…' : subType;
|
|
5434
5934
|
const outerRing = leader ? `<circle r="${R + 9}" fill="none" stroke="${color}" stroke-width="0.6" opacity="0.18"/>` : '';
|
|
5935
|
+
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"/>` : '';
|
|
5435
5936
|
const nameY = subType ? (R * 0.55 + 2).toFixed(0) : (R * 0.55 + 8).toFixed(0);
|
|
5436
5937
|
const avR = Math.round(R * 0.52);
|
|
5437
5938
|
const avatarSrc = v2RoleAvatar(role);
|
|
5438
5939
|
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)})">
|
|
5940
|
+
${runningDot}
|
|
5439
5941
|
<title>${esc(displayName)}${subType ? ` · ${esc(subType)}` : ''} — click for details</title>
|
|
5440
5942
|
${outerRing}
|
|
5441
5943
|
<circle r="${R}" fill="oklch(12% 0.008 55)" stroke="${color}" stroke-width="${leader ? 2.5 : 1.8}" filter="url(#v2glow)"/>
|
|
@@ -5503,6 +6005,31 @@ function v2RenderOrgChart() {
|
|
|
5503
6005
|
}
|
|
5504
6006
|
|
|
5505
6007
|
|
|
6008
|
+
// ── in-place running dot updater (avoids full re-render + GSAP flicker on SSE events) ──
|
|
6009
|
+
function v2UpdateChartRunningDots() {
|
|
6010
|
+
if (!_v2OrgData) return;
|
|
6011
|
+
const R = 42;
|
|
6012
|
+
const running = new Set((_v2OrgData._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
6013
|
+
document.querySelectorAll('#v2oc-nodes .v2oc-node[data-role]').forEach(g => {
|
|
6014
|
+
const roleId = g.dataset.role;
|
|
6015
|
+
const existing = g.querySelector('.v2-chart-running-dot');
|
|
6016
|
+
if (running.has(roleId) && !existing) {
|
|
6017
|
+
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
6018
|
+
c.setAttribute('class', 'v2-chart-running-dot');
|
|
6019
|
+
c.setAttribute('r', '5.5');
|
|
6020
|
+
c.setAttribute('cx', (R * 0.65).toFixed(1));
|
|
6021
|
+
c.setAttribute('cy', (-R * 0.65).toFixed(1));
|
|
6022
|
+
c.setAttribute('fill', 'oklch(68% 0.20 150)');
|
|
6023
|
+
c.setAttribute('stroke', 'oklch(12% 0.008 55)');
|
|
6024
|
+
c.setAttribute('stroke-width', '1.5');
|
|
6025
|
+
c.style.animation = 'blink 2s ease-in-out infinite';
|
|
6026
|
+
g.insertBefore(c, g.firstChild);
|
|
6027
|
+
} else if (!running.has(roleId) && existing) {
|
|
6028
|
+
existing.remove();
|
|
6029
|
+
}
|
|
6030
|
+
});
|
|
6031
|
+
}
|
|
6032
|
+
|
|
5506
6033
|
// ── avatar lookup ────────────────────────────
|
|
5507
6034
|
const _v2AvatarKnown = new Set([
|
|
5508
6035
|
'coder','senior-developer','reviewer','tester','planner','researcher',
|
|
@@ -5570,9 +6097,12 @@ function v2RenderOrgRoles() {
|
|
|
5570
6097
|
// Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
|
|
5571
6098
|
const leaderIds = new Set(roles.filter(r => r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner').map(r => r.id));
|
|
5572
6099
|
if (!leaderIds.size) leaderIds.add(roles[0]?.id);
|
|
6100
|
+
// Overlay live running status from in-memory agent SSE state
|
|
6101
|
+
const _rolesRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
5573
6102
|
pane.innerHTML = `<div class="roles-v2-grid">${roles.map((r, i) => {
|
|
5574
6103
|
const color = V2_ROLE_COLORS[i % V2_ROLE_COLORS.length];
|
|
5575
6104
|
const isLeader = leaderIds.has(r.id);
|
|
6105
|
+
const isRunning = _rolesRunning.has(r.id);
|
|
5576
6106
|
const resps = Array.isArray(r.responsibilities) ? r.responsibilities : [];
|
|
5577
6107
|
const displayName = r.name || r.title || r.id;
|
|
5578
6108
|
const roleType = r.type || r.agent_type || '';
|
|
@@ -5581,7 +6111,7 @@ function v2RenderOrgRoles() {
|
|
|
5581
6111
|
<div class="rv2-head">
|
|
5582
6112
|
<img class="rv2-avatar" src="${avatarSrc}" alt="${esc(displayName)}" loading="lazy" onerror="this.src='data/avatars/coder.svg'"/>
|
|
5583
6113
|
<div class="rv2-info">
|
|
5584
|
-
<div class="rv2-title">${esc(displayName)}${isLeader ? ' <span style="font-size:10px;color:var(--accent);font-weight:400;opacity:0.8">lead</span>' : ''}</div>
|
|
6114
|
+
<div class="rv2-title">${esc(displayName)}${isLeader ? ' <span style="font-size:10px;color:var(--accent);font-weight:400;opacity:0.8">lead</span>' : ''}${isRunning ? ' <span class="live-dot" style="display:inline-block;vertical-align:middle;margin-left:4px"></span>' : ''}</div>
|
|
5585
6115
|
${roleType ? `<span class="rv2-agent">${esc(roleType)}</span>` : ''}
|
|
5586
6116
|
</div>
|
|
5587
6117
|
</div>
|
|
@@ -5609,16 +6139,18 @@ async function v2OpenAgent(roleId) {
|
|
|
5609
6139
|
const bodyEl = document.getElementById('oad-body');
|
|
5610
6140
|
const headEl = document.getElementById('oad-head');
|
|
5611
6141
|
if (!drawer || !bodyEl || !headEl) return;
|
|
6142
|
+
const _agentOrg = _v2SelOrg, _agentRole = roleId;
|
|
5612
6143
|
drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false');
|
|
5613
6144
|
v2HighlightAgentNode(roleId);
|
|
5614
6145
|
headEl.innerHTML = '<button class="oad-close" onclick="v2CloseAgent()" aria-label="Close">✕</button>';
|
|
5615
6146
|
bodyEl.innerHTML = '<div class="oad-loading">Loading agent…</div>';
|
|
5616
6147
|
try {
|
|
5617
|
-
const r = await fetch(`/api/org/${encodeURIComponent(
|
|
6148
|
+
const r = await fetch(`/api/org/${encodeURIComponent(_agentOrg)}/agent/${encodeURIComponent(_agentRole)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`);
|
|
5618
6149
|
if (!r.ok) throw new Error('not found');
|
|
6150
|
+
if (_v2SelOrg !== _agentOrg) return;
|
|
5619
6151
|
v2RenderAgentDrawer(await r.json());
|
|
5620
6152
|
} catch (_) {
|
|
5621
|
-
bodyEl.innerHTML = '<div class="oad-loading">Could not load agent detail.</div>';
|
|
6153
|
+
if (_v2SelOrg === _agentOrg) bodyEl.innerHTML = '<div class="oad-loading">Could not load agent detail.</div>';
|
|
5622
6154
|
}
|
|
5623
6155
|
}
|
|
5624
6156
|
function v2RenderAgentDrawer(data) {
|
|
@@ -5710,16 +6242,38 @@ function v2RenderMarkdown(md) {
|
|
|
5710
6242
|
}
|
|
5711
6243
|
|
|
5712
6244
|
// ── org activity ────────────────────────────
|
|
6245
|
+
function fmtOrgEvDetail(ev) {
|
|
6246
|
+
if (ev.type === 'org:comms' && (ev.from || ev.role || ev.to || ev.msg)) {
|
|
6247
|
+
const sender = ev.from || ev.role || '';
|
|
6248
|
+
const route = [sender, ev.to].filter(Boolean).join(' → ');
|
|
6249
|
+
return route ? route + (ev.msg ? ': ' + ev.msg : '') : ev.msg || '';
|
|
6250
|
+
}
|
|
6251
|
+
if (ev.type === 'org:checkpoint') return ev.summary || ev.progress || '';
|
|
6252
|
+
if (ev.type === 'agent:result') return (ev.agent || ev.from || '') + (ev.result || ev.msg || ev.message || ev.summary ? ': ' + (ev.result || ev.msg || ev.message || ev.summary) : '');
|
|
6253
|
+
if (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline') return ev.title || ev.role || ev.agent || ev.id || ev.from || '';
|
|
6254
|
+
if (ev.type === 'org:error') return ev.error || ev.msg || ev.message || '';
|
|
6255
|
+
if (ev.type === 'file:write') return ev.path || ev.file || ev.msg || '';
|
|
6256
|
+
return ev.role||ev.result||ev.msg||ev.message||ev.progress||ev.agent||ev.status||'';
|
|
6257
|
+
}
|
|
6258
|
+
|
|
5713
6259
|
function v2RenderOrgActivity() {
|
|
5714
6260
|
if (!_v2SelOrg) return;
|
|
5715
6261
|
const activity = _v2OrgData?._activity || [];
|
|
5716
6262
|
const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
|
|
5717
|
-
const
|
|
6263
|
+
const _actSeen = new Set();
|
|
6264
|
+
const events = [...activity, ...orgEvents]
|
|
6265
|
+
.filter(e => { const k=(e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||'')+'|'+(e.from||e.role||e.agent||'')+'|'+(e.result||e.msg||e.message||e.summary||'').slice(0,20); return _actSeen.has(k)?false:(_actSeen.add(k),true); })
|
|
6266
|
+
.sort((a,b) => (b.ts||0)-(a.ts||0)).slice(0,80);
|
|
5718
6267
|
const pane = document.getElementById('odt-activity');
|
|
5719
6268
|
if (!pane) return;
|
|
5720
6269
|
const fmtOrgEvType = t => {
|
|
5721
|
-
const m={
|
|
5722
|
-
|
|
6270
|
+
const m={
|
|
6271
|
+
'org:start':'start','org:stop':'stop','org:complete':'done','org:create':'create',
|
|
6272
|
+
'org:heartbeat':'hb','org:agent:online':'agent-on','org:comms':'comms',
|
|
6273
|
+
'org:checkpoint':'checkpoint','run:cycle:complete':'cycle','run:complete':'run-done',
|
|
6274
|
+
'org:error':'error','org:agent:offline':'agent-off',
|
|
6275
|
+
};
|
|
6276
|
+
return m[t]||(t||'').replace(/^(?:org|run):/,'');
|
|
5723
6277
|
};
|
|
5724
6278
|
if (!events.length) {
|
|
5725
6279
|
pane.innerHTML = '<div class="empty">No activity recorded</div>';
|
|
@@ -5727,7 +6281,7 @@ function v2RenderOrgActivity() {
|
|
|
5727
6281
|
}
|
|
5728
6282
|
pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
|
|
5729
6283
|
const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
5730
|
-
const detail = ev
|
|
6284
|
+
const detail = fmtOrgEvDetail(ev) || ev.role || ev.agent || '';
|
|
5731
6285
|
return `<div class="av2-row"><span class="av2-time">${esc(t)}</span><span class="av2-type">${esc(fmtOrgEvType(ev.type))}</span><span class="av2-msg">${esc(detail)}</span></div>`;
|
|
5732
6286
|
}).join('')}</div>`;
|
|
5733
6287
|
}
|
|
@@ -5747,7 +6301,11 @@ function v2RenderOrgHealth() {
|
|
|
5747
6301
|
<div class="hv2-cell"><div class="hv2-lbl">Topology</div><div class="hv2-val" style="font-size:14px">${esc((_v2OrgData?.topology||'—').toUpperCase())}</div></div>
|
|
5748
6302
|
${health?.agents_active!=null?`<div class="hv2-cell"><div class="hv2-lbl">Agents</div><div class="hv2-val ${health.agents_active>0?'green':''}">${health.agents_active}</div></div>`:''}
|
|
5749
6303
|
${health?.tasks_pending!=null?`<div class="hv2-cell"><div class="hv2-lbl">Tasks</div><div class="hv2-val ${health.tasks_pending>5?'amber':''}">${health.tasks_pending}</div></div>`:''}
|
|
6304
|
+
${health?.run_success_rate_7d!=null?`<div class="hv2-cell"><div class="hv2-lbl">7d Success</div><div class="hv2-val ${health.run_success_rate_7d>=80?'green':health.run_success_rate_7d<50?'amber':''}">${health.run_success_rate_7d}%</div></div>`:''}
|
|
6305
|
+
${health?.total_runs_7d!=null?`<div class="hv2-cell"><div class="hv2-lbl">7d Runs</div><div class="hv2-val">${health.total_runs_7d}</div></div>`:''}
|
|
6306
|
+
${health?.budget_used_pct!=null?`<div class="hv2-cell"><div class="hv2-lbl">Budget Used</div><div class="hv2-val ${health.budget_used_pct>=90?'amber':''}">${health.budget_used_pct}%</div></div>`:''}
|
|
5750
6307
|
</div>
|
|
6308
|
+
${health?.budget_used_pct!=null&&health?.budget_max_tokens>0?`<div style="margin:10px 0 12px"><div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden"><div style="width:${Math.min(100,health.budget_used_pct)}%;height:100%;background:${health.budget_used_pct>=90?'var(--red)':health.budget_used_pct>=70?'oklch(70% 0.18 60)':'var(--accent)'};border-radius:2px"></div></div><div style="font-size:10px;color:var(--text-lo);margin-top:4px">${(health.budget_used_tokens??0).toLocaleString()} / ${health.budget_max_tokens.toLocaleString()} tokens</div></div>`:''}
|
|
5751
6309
|
${health?.errors?.length?`<div style="margin-top:12px"><div class="m-group-title">Errors</div>${health.errors.slice(0,5).map(e=>`<div style="font-size:11px;color:var(--red);padding:4px 0;border-bottom:1px solid var(--border)">${esc(e)}</div>`).join('')}</div>`:`<div style="margin-top:10px;font-size:12px;color:var(--text-lo)">${listOrg.running?'Running — no errors detected':'Org is idle'}</div>`}
|
|
5752
6310
|
`;
|
|
5753
6311
|
}
|
|
@@ -5783,11 +6341,17 @@ function v2RenderOrgTasks() {
|
|
|
5783
6341
|
if (!pane) return;
|
|
5784
6342
|
// Normalize: API returns {todo:[],doing:[],done:[]} (columns) or a flat array
|
|
5785
6343
|
const raw = _v2OrgData?.tasks;
|
|
6344
|
+
// Server puts completed/failed/cancelled in the 'done' column but keeps original status string
|
|
6345
|
+
const _normTaskStatus = (st, col) => {
|
|
6346
|
+
if (st === 'completed' || st === 'failed' || st === 'cancelled') return 'done';
|
|
6347
|
+
if (st === 'in_progress') return 'doing';
|
|
6348
|
+
return st || col;
|
|
6349
|
+
};
|
|
5786
6350
|
let tasks = [];
|
|
5787
|
-
if (Array.isArray(raw)) tasks = raw.
|
|
6351
|
+
if (Array.isArray(raw)) tasks = raw.map(t => ({ ...t, status: _normTaskStatus(t.status, 'todo') }));
|
|
5788
6352
|
else if (raw && typeof raw === 'object') {
|
|
5789
6353
|
for (const [col, items] of Object.entries(raw)) {
|
|
5790
|
-
(Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: t.status
|
|
6354
|
+
(Array.isArray(items) ? items : []).forEach(t => tasks.push({ ...t, status: _normTaskStatus(t.status, col) }));
|
|
5791
6355
|
}
|
|
5792
6356
|
}
|
|
5793
6357
|
if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks</div>'; return; }
|
|
@@ -5822,7 +6386,7 @@ function v2RenderOrgCosts() {
|
|
|
5822
6386
|
? c.map(r => ({ label: r.label ?? r.name ?? '—', cost: Number(r.value ?? r.cost ?? 0), tin: 0, tout: 0 }))
|
|
5823
6387
|
: Object.entries(c).map(([k, v]) => ({ label: k, cost: Number(v) || 0, tin: 0, tout: 0 }));
|
|
5824
6388
|
}
|
|
5825
|
-
if (!rows.length) { pane.innerHTML = '<div class="empty">No cost data</div>'; return; }
|
|
6389
|
+
if (!rows.length) { pane.innerHTML = '<div class="empty" style="text-align:center;padding:24px 16px"><div style="font-size:13px;color:var(--text-lo);margin-bottom:6px">No cost data tracked yet</div><div style="font-size:11px;color:var(--text-lo);opacity:0.7">Agents must emit <code>agent:usage</code> events to populate this tab</div></div>'; return; }
|
|
5826
6390
|
const cur = (b && b.currency) || 'USD';
|
|
5827
6391
|
const period = (b && b.period) || '';
|
|
5828
6392
|
const total = rows.reduce((s, r) => s + r.cost, 0);
|
|
@@ -5853,7 +6417,9 @@ function v2RenderOrgMembers() {
|
|
|
5853
6417
|
const joinReqs = Array.isArray(_v2OrgData?._joinRequests) ? _v2OrgData._joinRequests : [];
|
|
5854
6418
|
if (!list.length) { pane.innerHTML = '<div class="empty">No members</div>'; return; }
|
|
5855
6419
|
const src = members.length ? 'joined members' : 'defined roles';
|
|
5856
|
-
|
|
6420
|
+
// Overlay live running status from in-memory agent SSE state (updated by org:agent:online/offline events)
|
|
6421
|
+
const _liveRunning = new Set((_v2OrgData?._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
6422
|
+
const active = (r) => r.running || r.active || r.status === 'active' || _liveRunning.has(r.id || r.name || r.user);
|
|
5857
6423
|
pane.innerHTML = `<div class="m-group-title">Members (${list.length}) · ${src}</div>` +
|
|
5858
6424
|
list.map(r => `<div style="display:flex;gap:10px;align-items:center;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px">
|
|
5859
6425
|
<span style="width:28px;height:28px;border-radius:50%;background:var(--surface-hi);display:inline-flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0">◈</span>
|
|
@@ -5878,7 +6444,7 @@ function v2RenderOrgGoals() {
|
|
|
5878
6444
|
const indent = depth * 20;
|
|
5879
6445
|
const pct = g.total > 0 ? Math.min(100, Math.round(g.filled / g.total * 100)) : 0;
|
|
5880
6446
|
const status = g.status || 'pending';
|
|
5881
|
-
const statusCls = status === 'done' ? 'on' : status === 'blocked' ? 'warn' : '';
|
|
6447
|
+
const statusCls = status === 'done' ? 'on' : (status === 'blocked' || status === 'in_progress') ? 'warn' : '';
|
|
5882
6448
|
return `<div style="padding:8px 0;border-bottom:1px solid var(--border);padding-left:${indent}px">
|
|
5883
6449
|
<div style="display:flex;align-items:center;gap:10px">
|
|
5884
6450
|
<span style="flex:1;font-size:13px;color:var(--text-hi)">${esc(g.text || g.goal || g.title || '—')}</span>
|
|
@@ -5899,9 +6465,10 @@ function v2RenderOrgBoard() {
|
|
|
5899
6465
|
if (!issues.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
|
|
5900
6466
|
const cols = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
5901
6467
|
const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
|
|
6468
|
+
const _normIssueStatus = s => (s === 'closed' || s === 'resolved') ? 'done' : (s || 'open');
|
|
5902
6469
|
el.innerHTML = `<div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">` +
|
|
5903
6470
|
cols.map(col => {
|
|
5904
|
-
const cards = issues.filter(i => (i.status || i.state
|
|
6471
|
+
const cards = issues.filter(i => _normIssueStatus(i.status || i.state) === col);
|
|
5905
6472
|
return `<div style="min-width:160px;flex:1">
|
|
5906
6473
|
<div style="font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;color:var(--text-lo);padding:6px 0;border-bottom:1px solid var(--border);margin-bottom:8px">${esc(col.replace('_', ' '))} <span style="background:var(--surface-hi);padding:1px 6px;border-radius:8px">${cards.length}</span></div>
|
|
5907
6474
|
${cards.slice(0, 15).map(i => `<div style="background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:7px 9px;margin-bottom:5px;font-size:12px">
|
|
@@ -5923,27 +6490,41 @@ function v2RenderOrgLive() {
|
|
|
5923
6490
|
${running.length
|
|
5924
6491
|
? running.map(a => `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)">
|
|
5925
6492
|
<span class="live-dot" style="flex-shrink:0"></span>
|
|
5926
|
-
<span style="font-size:13px;color:var(--text-hi)">${esc(a.
|
|
5927
|
-
<span style="margin-left:auto;font-size:11px;color:var(--text-lo)">${esc(a.adapter || '')}</span>
|
|
6493
|
+
<span style="font-size:13px;color:var(--text-hi)">${esc(a.title || a.id || '—')}</span>${a.adapterType?`<span style="font-size:10px;color:var(--text-lo);margin-left:4px">[${esc(a.adapterType)}]</span>`:''}
|
|
6494
|
+
<span style="margin-left:auto;font-size:11px;color:var(--text-lo)">${esc(a.adapterModel || a.adapter || '')}</span>
|
|
5928
6495
|
</div>`).join('')
|
|
5929
6496
|
: '<div style="color:var(--text-lo);font-size:13px;padding:10px 0">No agents currently running</div>'}
|
|
5930
6497
|
</div>
|
|
5931
6498
|
<div class="m-group-title" style="margin-bottom:6px">Activity Feed</div>
|
|
5932
6499
|
<div style="max-height:240px;overflow-y:auto;font-size:11px;font-family:var(--mono)">
|
|
5933
|
-
${(_v2OrgData._activity || []).slice(-30).reverse().map(e
|
|
5934
|
-
${esc(relTime(e.ts || e.timestamp || e.created_at))}
|
|
5935
|
-
<span style="color:var(--text-mid);margin-left:6px">${esc(e.type || e.kind || e.event || '—')}</span>
|
|
5936
|
-
${e.message ? `<span style="color:var(--text-hi);margin-left:6px">${esc(e.message.toString().slice(0, 80))}</span>` : ''}
|
|
5937
|
-
</div>`).join('')}
|
|
6500
|
+
${(()=>{const _la=_v2OrgData._activity||[];const _ls=_v2OrgEventLog[_v2SelOrg]||[];const _lseen=new Set();const _lmerged=[..._la,..._ls].filter(e=>{const k=(e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||'')+'|'+(e.from||e.role||e.agent||'')+'|'+(e.result||e.msg||e.message||e.summary||'').slice(0,20);return _lseen.has(k)?false:(_lseen.add(k),true);}).sort((a,b)=>(a.ts||0)-(b.ts||0));return _lmerged.slice(-30).reverse().map(e=>`<div style="padding:3px 0;border-bottom:1px solid var(--border);color:var(--text-lo)">${esc(relTime(e.ts||e.timestamp||e.created_at))}<span style="color:var(--text-mid);margin-left:6px">${esc(e.type||e.kind||e.event||'—')}</span>${fmtOrgEvDetail(e)?`<span style="color:var(--text-hi);margin-left:6px">${esc(fmtOrgEvDetail(e).slice(0,80))}</span>`:''}</div>`).join('');})()}
|
|
5938
6501
|
</div>`;
|
|
5939
6502
|
// auto-refresh every 5s while tab is active
|
|
5940
6503
|
if (!_orgLiveInterval) {
|
|
5941
6504
|
_orgLiveInterval = setInterval(async () => {
|
|
5942
|
-
if (_v2OrgTab !== 'live' || !_v2SelOrg) { clearInterval(_orgLiveInterval); _orgLiveInterval = null; return; }
|
|
5943
|
-
const
|
|
6505
|
+
if (_v2OrgTab !== 'live' || !_v2SelOrg || currentView !== 'orgs') { clearInterval(_orgLiveInterval); _orgLiveInterval = null; return; }
|
|
6506
|
+
const _liveOrg = _v2SelOrg;
|
|
6507
|
+
const _enc = encodeURIComponent(_liveOrg);
|
|
5944
6508
|
try {
|
|
5945
|
-
const
|
|
5946
|
-
|
|
6509
|
+
const [actData, agentsData] = await Promise.all([
|
|
6510
|
+
fetch(`/api/org/${_enc}/activity${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []),
|
|
6511
|
+
fetch(`/api/org/${_enc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : {}).catch(() => {}),
|
|
6512
|
+
]);
|
|
6513
|
+
if (_v2SelOrg !== _liveOrg || !_v2OrgData) return;
|
|
6514
|
+
// Merge fresh API data with SSE-pushed events to avoid clobbering live events
|
|
6515
|
+
// that haven't flushed to the JSONL file yet when this poll ran.
|
|
6516
|
+
const _freshAct = Array.isArray(actData) ? actData : [];
|
|
6517
|
+
const _sseAct = (_v2OrgData._activity || []).filter(e => e._sseOnly);
|
|
6518
|
+
const _mergeSeen = new Set(_freshAct.map(e => (e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||'')));
|
|
6519
|
+
_sseAct.forEach(e => { const k=(e.ts||'')+'|'+(e.type||'')+'|'+(e.runId||e.session||''); if (!_mergeSeen.has(k)) _freshAct.push(e); });
|
|
6520
|
+
_v2OrgData._activity = _freshAct.sort((a,b)=>(a.ts||0)-(b.ts||0));
|
|
6521
|
+
const freshAgents = Array.isArray(agentsData) ? agentsData : (agentsData?.agents || []);
|
|
6522
|
+
if (freshAgents.length) {
|
|
6523
|
+
// Preserve SSE-driven running status: state file lags behind real-time SSE updates
|
|
6524
|
+
const sseRunning = new Set((_v2OrgData._agents || []).filter(a => a.status === 'running').map(a => a.id));
|
|
6525
|
+
_v2OrgData._agents = freshAgents.map(a => sseRunning.has(a.id) ? { ...a, status: 'running' } : a);
|
|
6526
|
+
}
|
|
6527
|
+
v2RenderOrgLive();
|
|
5947
6528
|
} catch (_) {}
|
|
5948
6529
|
}, 5000);
|
|
5949
6530
|
}
|
|
@@ -5953,10 +6534,12 @@ function v2RenderOrgLive() {
|
|
|
5953
6534
|
async function v2RenderOrgApprovals() {
|
|
5954
6535
|
const el = document.getElementById('odt-approvals');
|
|
5955
6536
|
if (!el || !_v2SelOrg) return;
|
|
6537
|
+
const _rendOrg = _v2SelOrg;
|
|
5956
6538
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
5957
6539
|
try {
|
|
5958
|
-
const _enc = encodeURIComponent(
|
|
6540
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
5959
6541
|
const data = await fetch(`/api/org/${_enc}/approvals${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
6542
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
5960
6543
|
const approvals = Array.isArray(data) ? data : (data.approvals || []);
|
|
5961
6544
|
if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals</div>'; return; }
|
|
5962
6545
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -5974,8 +6557,8 @@ async function v2RenderOrgApprovals() {
|
|
|
5974
6557
|
<td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
|
|
5975
6558
|
<td style="padding:6px 8px;white-space:nowrap">
|
|
5976
6559
|
${pending
|
|
5977
|
-
? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve"
|
|
5978
|
-
<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject"
|
|
6560
|
+
? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve" data-aid="${esc(aid)}" onclick="orgApprovalAction(this.dataset.aid,'approve')">✓</button>
|
|
6561
|
+
<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red);margin-left:3px" title="Reject" aria-label="Reject" data-aid="${esc(aid)}" onclick="orgApprovalAction(this.dataset.aid,'reject')">✕</button>`
|
|
5979
6562
|
: ''}
|
|
5980
6563
|
</td>
|
|
5981
6564
|
</tr>`;
|
|
@@ -5987,7 +6570,7 @@ async function orgApprovalAction(id, action) {
|
|
|
5987
6570
|
if (!confirm(action + ' this request?')) return;
|
|
5988
6571
|
try {
|
|
5989
6572
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
5990
|
-
await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}`, {
|
|
6573
|
+
await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, {
|
|
5991
6574
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action })
|
|
5992
6575
|
});
|
|
5993
6576
|
showToast('Done', action + 'd', 'ok');
|
|
@@ -5999,10 +6582,12 @@ async function orgApprovalAction(id, action) {
|
|
|
5999
6582
|
async function v2RenderOrgSecrets() {
|
|
6000
6583
|
const el = document.getElementById('odt-secrets');
|
|
6001
6584
|
if (!el || !_v2SelOrg) return;
|
|
6585
|
+
const _rendOrg = _v2SelOrg;
|
|
6002
6586
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6003
6587
|
try {
|
|
6004
|
-
const _enc = encodeURIComponent(
|
|
6588
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6005
6589
|
const data = await fetch(`/api/org/${_enc}/secrets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
6590
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6006
6591
|
const secrets = Array.isArray(data) ? data : (data.secrets || []);
|
|
6007
6592
|
el.innerHTML = '<div style="font-size:11px;color:var(--text-lo);margin-bottom:10px">Secret values are never transmitted or displayed.</div>' +
|
|
6008
6593
|
(secrets.length
|
|
@@ -6065,10 +6650,12 @@ function generateOrgSettingsCmd() {
|
|
|
6065
6650
|
async function v2RenderOrgRoutines() {
|
|
6066
6651
|
const el = document.getElementById('odt-routines');
|
|
6067
6652
|
if (!el || !_v2SelOrg) return;
|
|
6653
|
+
const _rendOrg = _v2SelOrg;
|
|
6068
6654
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6069
6655
|
try {
|
|
6070
|
-
const _enc = encodeURIComponent(
|
|
6656
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6071
6657
|
const data = await fetch(`/api/org/${_enc}/routines${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
6658
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6072
6659
|
const routines = Array.isArray(data) ? data : (data.routines || _v2OrgData?.config?.routines || []);
|
|
6073
6660
|
if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured</div>'; return; }
|
|
6074
6661
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6078,8 +6665,8 @@ async function v2RenderOrgRoutines() {
|
|
|
6078
6665
|
routines.map(r => `<tr style="border-top:1px solid var(--border)">
|
|
6079
6666
|
<td style="padding:6px 8px;color:var(--text-hi)">${esc(r.name || '—')}</td>
|
|
6080
6667
|
<td style="padding:6px 8px;font-family:var(--mono);color:var(--text-lo)">${esc(r.cron || r.schedule || '—')}</td>
|
|
6081
|
-
<td style="padding:6px 8px;color:var(--text-lo)">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
|
|
6082
|
-
<td style="padding:6px 8px"><span class="ss-pill ${r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || '—')}</span></td>
|
|
6668
|
+
<td style="padding:6px 8px;color:var(--text-lo)">${(r.lastRun || r.last_run) ? relTime(r.lastRun || r.last_run) : '—'}</td>
|
|
6669
|
+
<td style="padding:6px 8px"><span class="ss-pill ${r.enabled || r.active || r.status === 'active' ? 'on' : ''}">${esc(r.status || (r.enabled != null ? (r.enabled ? 'active' : 'inactive') : '—'))}</span></td>
|
|
6083
6670
|
</tr>`).join('') + '</tbody></table>';
|
|
6084
6671
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
6085
6672
|
}
|
|
@@ -6088,10 +6675,12 @@ async function v2RenderOrgRoutines() {
|
|
|
6088
6675
|
async function v2RenderOrgMyIssues() {
|
|
6089
6676
|
const el = document.getElementById('odt-myissues');
|
|
6090
6677
|
if (!el || !_v2SelOrg) return;
|
|
6678
|
+
const _rendOrg = _v2SelOrg;
|
|
6091
6679
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6092
6680
|
try {
|
|
6093
|
-
const _enc = encodeURIComponent(
|
|
6681
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6094
6682
|
const data = await fetch(`/api/org/${_enc}/my-issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
6683
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6095
6684
|
const issues = Array.isArray(data) ? data : (data.issues || []);
|
|
6096
6685
|
if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you</div>'; return; }
|
|
6097
6686
|
const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
|
|
@@ -6100,7 +6689,7 @@ async function v2RenderOrgMyIssues() {
|
|
|
6100
6689
|
<th style="padding:5px 4px">P</th><th style="padding:5px 8px">Title</th><th>Status</th><th>Updated</th>
|
|
6101
6690
|
</tr></thead><tbody>` +
|
|
6102
6691
|
issues.slice(0, 50).map(i => {
|
|
6103
|
-
const cls = i.status === 'done' ? 'on' : i.status === 'blocked' ? 'warn' : '';
|
|
6692
|
+
const cls = i.status === 'done' || i.status === 'closed' || i.status === 'resolved' ? 'on' : (i.status === 'blocked' || i.status === 'in_progress') ? 'warn' : '';
|
|
6104
6693
|
return `<tr style="border-top:1px solid var(--border)">
|
|
6105
6694
|
<td style="padding:5px 4px">${PRIORITY[i.priority] || '·'}</td>
|
|
6106
6695
|
<td style="padding:5px 8px;color:var(--text-hi);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((i.title || i.description || '—').slice(0, 80))}</td>
|
|
@@ -6116,7 +6705,14 @@ function v2RenderOrgBudgets() {
|
|
|
6116
6705
|
const el = document.getElementById('odt-budgets');
|
|
6117
6706
|
if (!el || !_v2OrgData) return;
|
|
6118
6707
|
const b = _v2OrgData._budgets || _v2OrgData.budgets || {};
|
|
6119
|
-
|
|
6708
|
+
// Server returns { org_budget: {limit_tokens, limit_usd}, agents: [{id, title, tokens_in, tokens_out, total_cost_usd}] }
|
|
6709
|
+
const orgBudget = b.org_budget || {};
|
|
6710
|
+
const tokenLimit = orgBudget.limit_tokens || b.tokenLimit || null;
|
|
6711
|
+
const usdLimit = orgBudget.limit_usd || b.usdLimit || null;
|
|
6712
|
+
// Per-agent data comes from the budgets endpoint (has cost); fall back to agents roster
|
|
6713
|
+
const budgetAgents = Array.isArray(b.agents) && b.agents.length ? b.agents : [];
|
|
6714
|
+
const usedTokens = budgetAgents.reduce((s, a) => s + (a.tokens_in || 0) + (a.tokens_out || 0), 0);
|
|
6715
|
+
const usedUsd = budgetAgents.reduce((s, a) => s + (a.total_cost_usd || 0), 0);
|
|
6120
6716
|
|
|
6121
6717
|
function fillBar(used, limit) {
|
|
6122
6718
|
if (!limit) return '';
|
|
@@ -6127,22 +6723,25 @@ function v2RenderOrgBudgets() {
|
|
|
6127
6723
|
}
|
|
6128
6724
|
|
|
6129
6725
|
let html = '';
|
|
6130
|
-
if (
|
|
6131
|
-
html += `<div class="m-group-title">Tokens</div>${fillBar(
|
|
6726
|
+
if (tokenLimit != null || usedTokens > 0) {
|
|
6727
|
+
html += `<div class="m-group-title">Tokens</div>${fillBar(usedTokens, tokenLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">${usedTokens.toLocaleString()} / ${tokenLimit ? tokenLimit.toLocaleString() : '∞'}</div>`;
|
|
6132
6728
|
}
|
|
6133
|
-
if (
|
|
6134
|
-
html += `<div class="m-group-title">USD Budget</div>${fillBar(
|
|
6729
|
+
if (usdLimit != null || usedUsd > 0) {
|
|
6730
|
+
html += `<div class="m-group-title">USD Budget</div>${fillBar(usedUsd, usdLimit)}<div style="font-size:12px;color:var(--text-lo);margin:4px 0 14px">$${usedUsd.toFixed(4)} / ${usdLimit ? '$' + Number(usdLimit).toFixed(2) : '∞'}</div>`;
|
|
6135
6731
|
}
|
|
6136
|
-
if (
|
|
6732
|
+
if (budgetAgents.length) {
|
|
6137
6733
|
html += '<div class="m-group-title" style="margin-bottom:6px">Per Agent</div>' +
|
|
6138
6734
|
`<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr style="color:var(--text-xs);text-align:left"><th style="padding:4px 8px">Agent</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th></tr></thead><tbody>` +
|
|
6139
|
-
|
|
6140
|
-
const
|
|
6735
|
+
budgetAgents.slice(0, 20).map(a => {
|
|
6736
|
+
const tIn = a.tokens_in || 0, tOut = a.tokens_out || 0;
|
|
6737
|
+
const cost = a.total_cost_usd || 0;
|
|
6738
|
+
const limit = a.limit_usd || orgBudget.limit_usd || 0;
|
|
6739
|
+
const over = limit && cost > limit;
|
|
6141
6740
|
return `<tr style="border-top:1px solid var(--border)${over ? ';color:var(--red)' : ''}">
|
|
6142
|
-
<td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}">${esc((a.
|
|
6143
|
-
<td style="padding:4px 8px;color:var(--text-lo)">${
|
|
6144
|
-
<td style="padding:4px 8px;color:var(--text-lo)">${
|
|
6145
|
-
<td style="padding:4px 8px;color:var(--accent)">$${
|
|
6741
|
+
<td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}">${esc((a.title || a.id || '—').toString().slice(0, 14))}</td>
|
|
6742
|
+
<td style="padding:4px 8px;color:var(--text-lo)">${tIn.toLocaleString()}</td>
|
|
6743
|
+
<td style="padding:4px 8px;color:var(--text-lo)">${tOut.toLocaleString()}</td>
|
|
6744
|
+
<td style="padding:4px 8px;color:var(--accent)">$${cost.toFixed(4)}${over ? ' ⚠' : ''}</td>
|
|
6146
6745
|
</tr>`;
|
|
6147
6746
|
}).join('') + '</tbody></table>';
|
|
6148
6747
|
}
|
|
@@ -6153,10 +6752,12 @@ function v2RenderOrgBudgets() {
|
|
|
6153
6752
|
async function v2RenderOrgPlugins() {
|
|
6154
6753
|
const el = document.getElementById('odt-plugins');
|
|
6155
6754
|
if (!el || !_v2SelOrg) return;
|
|
6755
|
+
const _rendOrg = _v2SelOrg;
|
|
6156
6756
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6157
6757
|
try {
|
|
6158
|
-
const _enc = encodeURIComponent(
|
|
6758
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6159
6759
|
const data = await fetch(`/api/org/${_enc}/plugins${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
6760
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6160
6761
|
const plugins = Array.isArray(data) ? data : (data.plugins || []);
|
|
6161
6762
|
if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
|
|
6162
6763
|
el.innerHTML = `<div class="proj-grid">` +
|
|
@@ -6184,7 +6785,7 @@ function v2RenderOrgCharts() {
|
|
|
6184
6785
|
const DAY = 86400000;
|
|
6185
6786
|
const buckets = Array.from({length: 14}, (_, i) => ({
|
|
6186
6787
|
day: new Date(now - (13 - i) * DAY).toLocaleDateString('en', {month:'short', day:'numeric'}),
|
|
6187
|
-
ts: now - (
|
|
6788
|
+
ts: now - (14 - i) * DAY,
|
|
6188
6789
|
events: 0, errors: 0,
|
|
6189
6790
|
}));
|
|
6190
6791
|
activity.forEach(e => {
|
|
@@ -6244,10 +6845,12 @@ function v2RenderOrgCharts() {
|
|
|
6244
6845
|
async function v2RenderOrgProjects() {
|
|
6245
6846
|
const el = document.getElementById('odt-projects');
|
|
6246
6847
|
if (!el || !_v2SelOrg) return;
|
|
6848
|
+
const _rendOrg = _v2SelOrg;
|
|
6247
6849
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6248
6850
|
try {
|
|
6249
|
-
const _enc = encodeURIComponent(
|
|
6851
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6250
6852
|
const data = await fetch(`/api/org/${_enc}/projects${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
6853
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6251
6854
|
const projects = Array.isArray(data) ? data : (data.projects || []);
|
|
6252
6855
|
if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured</div>'; return; }
|
|
6253
6856
|
el.innerHTML = `<div class="proj-grid">${projects.map(p => `<div class="proj-card">
|
|
@@ -6295,14 +6898,1211 @@ async function v2RenderOrgSkills() {
|
|
|
6295
6898
|
</div>`).join('');
|
|
6296
6899
|
}
|
|
6297
6900
|
|
|
6901
|
+
// ── ORG CHAT ───────────────────────────────────────────────
|
|
6902
|
+
let _odtChatSessions = [];
|
|
6903
|
+
let _odtChatCurrentId = null;
|
|
6904
|
+
let _odtChatCurrentAgent = 'all';
|
|
6905
|
+
let _odtChatSseSource = null;
|
|
6906
|
+
let _odtChatSeenKeys = new Set();
|
|
6907
|
+
let _odtChatForOrg = '';
|
|
6908
|
+
// Loop grouping: [S] sessions with same prompt + <20 min gap → one entry
|
|
6909
|
+
let _odtSessionGroups = {}; // groupId → {sessions, events, status, ts, _isGroup}
|
|
6910
|
+
let _odtSessionGroupMap = {}; // sessionId → groupId
|
|
6911
|
+
// Run grouping: [R] runs with same goal (or no goal) + <30 min gap → one entry
|
|
6912
|
+
let _odtRunGroups = {}; // groupId → {runs, status, ts, _isRunGroup}
|
|
6913
|
+
let _odtRunGroupMap = {}; // runId → groupId
|
|
6914
|
+
|
|
6915
|
+
function _odtOrgSessionMatch(s) {
|
|
6916
|
+
if (!_v2SelOrg) return true;
|
|
6917
|
+
const orgLc = _v2SelOrg.toLowerCase();
|
|
6918
|
+
const prompt = (s.prompt || '').toLowerCase();
|
|
6919
|
+
// Use a word-boundary check so "monomind-improver" doesn't match sessions for
|
|
6920
|
+
// "monomind-improver-state" (the shorter name is a prefix of the longer one).
|
|
6921
|
+
// After the matched org name, the next char must be end-of-string or a character
|
|
6922
|
+
// that cannot be part of an org name (not alphanumeric, hyphen, or underscore).
|
|
6923
|
+
const _orgBound = (str, prefix) => {
|
|
6924
|
+
const idx = str.indexOf(prefix);
|
|
6925
|
+
if (idx === -1) return false;
|
|
6926
|
+
const after = str[idx + prefix.length];
|
|
6927
|
+
return after === undefined || !/[a-z0-9_-]/.test(after);
|
|
6928
|
+
};
|
|
6929
|
+
if (_orgBound(prompt, 'running org: ' + orgLc) || _orgBound(prompt, 'org: ' + orgLc)) return true;
|
|
6930
|
+
return (s.events || []).some(ev => (ev.org || '').toLowerCase() === orgLc);
|
|
6931
|
+
}
|
|
6932
|
+
|
|
6933
|
+
function _odtChatAgentMatches(ev) {
|
|
6934
|
+
if (_odtChatCurrentAgent === 'all') return true;
|
|
6935
|
+
// Structural events always show regardless of agent filter
|
|
6936
|
+
const structural = new Set(['session:start','session:complete','domain:dispatch','domain:complete','file:write','run:start','run:complete','run:cycle:complete','org:start','org:complete','org:agent:online','org:agent:offline','org:checkpoint','org:error','loop:start','loop:complete','loop:tick','loop:hil','loop:hil:waiting','loop:hil:resolved']);
|
|
6937
|
+
if (structural.has(ev.type)) return true;
|
|
6938
|
+
return ev.agent === _odtChatCurrentAgent || ev.from === _odtChatCurrentAgent || ev.to === _odtChatCurrentAgent || ev.role === _odtChatCurrentAgent;
|
|
6939
|
+
}
|
|
6940
|
+
|
|
6941
|
+
async function v2RenderOrgChat() {
|
|
6942
|
+
const pane = document.getElementById('odt-chat');
|
|
6943
|
+
if (!pane) return;
|
|
6944
|
+
|
|
6945
|
+
// Build the pane HTML once
|
|
6946
|
+
if (!pane.querySelector('#odt-chat-bar')) {
|
|
6947
|
+
pane.innerHTML = `
|
|
6948
|
+
<div id="odt-chat-bar">
|
|
6949
|
+
<span id="odt-chat-sess-lbl">RUN</span>
|
|
6950
|
+
<select id="odt-chat-sel" onchange="odtChatSelectSession(this.value)">
|
|
6951
|
+
<option value="">— loading runs —</option>
|
|
6952
|
+
</select>
|
|
6953
|
+
<div id="odt-chat-live-dot"></div>
|
|
6954
|
+
<span id="odt-chat-live-lbl">OFFLINE</span>
|
|
6955
|
+
</div>
|
|
6956
|
+
<div id="odt-chat-agent-bar" style="display:none">
|
|
6957
|
+
<span id="odt-chat-agent-lbl">AGENT</span>
|
|
6958
|
+
</div>
|
|
6959
|
+
<div id="odt-chat-excerpt" class="cv-excerpt-banner"></div>
|
|
6960
|
+
<div id="odt-chat-feed">
|
|
6961
|
+
<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>
|
|
6962
|
+
</div>`;
|
|
6963
|
+
}
|
|
6964
|
+
|
|
6965
|
+
// Reset state only when org changes (not on every tab-switch)
|
|
6966
|
+
if (_odtChatForOrg !== _v2SelOrg) {
|
|
6967
|
+
_odtChatForOrg = _v2SelOrg;
|
|
6968
|
+
_odtChatCurrentId = null;
|
|
6969
|
+
_odtChatCurrentAgent = 'all';
|
|
6970
|
+
_odtChatSeenKeys = new Set();
|
|
6971
|
+
// Clear sessions from the previous org so _odtLoadChatSessions doesn't snapshot them
|
|
6972
|
+
// into _prevSessions and risk contaminating the new org's event buffers. SSE-injected
|
|
6973
|
+
// sessions for the NEW org arriving during the upcoming fetch are still captured via
|
|
6974
|
+
// _sseDuringFetch in _odtLoadChatSessions.
|
|
6975
|
+
_odtChatSessions = [];
|
|
6976
|
+
document.getElementById('odt-chat-agent-bar').style.display = 'none';
|
|
6977
|
+
document.getElementById('odt-chat-feed').querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
|
|
6978
|
+
const emptyEl = document.getElementById('odt-chat-empty');
|
|
6979
|
+
if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'Select a session to see agent communications.'; }
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6982
|
+
await _odtLoadChatSessions();
|
|
6983
|
+
// Returning to chat tab (same org, session already selected): re-render feed to catch
|
|
6984
|
+
// up on events buffered in sess.events while user was on another tab. _odtLoadChatSessions
|
|
6985
|
+
// refreshes the run list but won't re-render the feed when _odtChatCurrentId is already set.
|
|
6986
|
+
if (_odtChatCurrentId) {
|
|
6987
|
+
const _catchSess = _odtChatSessions.find(s => s.id === _odtChatCurrentId);
|
|
6988
|
+
if (_catchSess && (_catchSess._eventsLoaded || !_catchSess._isOrgRun)) {
|
|
6989
|
+
const _catchFeed = document.getElementById('odt-chat-feed');
|
|
6990
|
+
if (_catchFeed) {
|
|
6991
|
+
_catchFeed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
|
|
6992
|
+
// Re-inject session header for session-based (non-_isOrgRun) sessions — was removed above
|
|
6993
|
+
if (!_catchSess._isOrgRun) {
|
|
6994
|
+
const _cTs = _catchSess.ts || _catchSess.startedAt || 0;
|
|
6995
|
+
const _cTsStr = _cTs ? new Date(_cTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
|
|
6996
|
+
const _cPrompt = (_catchSess.prompt || '').replace(/^running org:\s*/i, '').slice(0, 100) || _odtChatCurrentId;
|
|
6997
|
+
const _cSt = _catchSess.status || 'unknown';
|
|
6998
|
+
const _cStCls = _cSt === 'running' ? 'color:oklch(65% 0.20 145)' : _cSt === 'complete' ? 'color:var(--text-lo)' : 'color:var(--text-xs)';
|
|
6999
|
+
const _cHdr = document.createElement('div');
|
|
7000
|
+
_cHdr.className = 'cv-session-hdr';
|
|
7001
|
+
_cHdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_cPrompt)}</div>`+
|
|
7002
|
+
`<div class="cv-shdr-meta">`+
|
|
7003
|
+
`<span class="cv-shdr-id" title="${esc(_odtChatCurrentId)}">${esc(_odtChatCurrentId.slice(0,22))}</span>`+
|
|
7004
|
+
`<span class="cv-shdr-sep">·</span><span style="${_cStCls}">${esc(_cSt)}</span>`+
|
|
7005
|
+
`<span class="cv-shdr-sep">·</span><span>${esc(_cTsStr)}</span>`+
|
|
7006
|
+
`<span class="cv-shdr-sep">·</span><span class="cv-shdr-evcount">${(_catchSess.events||[]).length} events</span>`+
|
|
7007
|
+
`</div>`;
|
|
7008
|
+
_catchFeed.appendChild(_cHdr);
|
|
7009
|
+
}
|
|
7010
|
+
(_catchSess.events || []).forEach(ev => {
|
|
7011
|
+
_odtAppendEvent(ev, false);
|
|
7012
|
+
const _ck = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
|
|
7013
|
+
_odtChatSeenKeys.add(_ck);
|
|
7014
|
+
});
|
|
7015
|
+
// Re-inject [S]→[R] link card on tab-return (same as first-select path)
|
|
7016
|
+
if (!_catchSess._isOrgRun) {
|
|
7017
|
+
const _crLinkedId = 'run-' + (_catchSess.id || '').replace(/^mm-/, '');
|
|
7018
|
+
const _crLinked = _odtChatSessions.find(s => s._isOrgRun && s.id === _crLinkedId);
|
|
7019
|
+
if (_crLinked) {
|
|
7020
|
+
const _crLinkEl = document.createElement('div');
|
|
7021
|
+
_crLinkEl.className = 'cv-msg cv-sys';
|
|
7022
|
+
const _crGoal = (_crLinked.prompt || '').slice(0, 60) || _crLinkedId;
|
|
7023
|
+
_crLinkEl.innerHTML = `<div class="cv-bub" style="cursor:pointer;border-left:2px solid var(--accent);background:oklch(15% 0.05 220)">`+
|
|
7024
|
+
`<span class="cv-etype" style="color:var(--accent)">RUN</span>`+
|
|
7025
|
+
`<span class="cv-text">→ ${esc(_crGoal)}${_crGoal.length >= 60 ? '…' : ''}</span>`+
|
|
7026
|
+
`<span class="cv-ts" style="color:var(--accent)">[${esc(_crLinked.status||'?')}]</span></div>`;
|
|
7027
|
+
_crLinkEl.onclick = () => {
|
|
7028
|
+
const _crSel = document.getElementById('odt-chat-sel');
|
|
7029
|
+
if (_crSel) { _crSel.value = _crLinkedId; odtChatSelectSession(_crLinkedId); }
|
|
7030
|
+
};
|
|
7031
|
+
_catchFeed.appendChild(_crLinkEl);
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
_catchFeed.scrollTop = _catchFeed.scrollHeight;
|
|
7035
|
+
}
|
|
7036
|
+
// Refresh agent bar — new agents may have come online while on another tab
|
|
7037
|
+
const _catchOrgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
7038
|
+
_odtPopulateAgentBar(_catchSess.events || [], _catchOrgRoles);
|
|
7039
|
+
// Refresh excerpt banner for all session types so last-activity stays current after tab-return
|
|
7040
|
+
if ((_catchSess.events || []).length) {
|
|
7041
|
+
renderCvExcerptBanner('odt-chat-excerpt', _catchSess.events, _catchSess);
|
|
7042
|
+
}
|
|
7043
|
+
} else if (_catchSess && _catchSess._isOrgRun && !_catchSess._eventsLoaded && !_catchSess._loading) {
|
|
7044
|
+
// Session selected but events not yet loaded (object reference may have been replaced by
|
|
7045
|
+
// _odtLoadChatSessions while lazy-load was in-flight). Retrigger select to lazy-load events
|
|
7046
|
+
// so returning to Chat tab never shows a permanently empty feed.
|
|
7047
|
+
// Guard _loading: if a fetch is already in-flight (user tabbed away/back during the fetch)
|
|
7048
|
+
// do NOT fire a second parallel fetch — it would race and overwrite sess.events on settle.
|
|
7049
|
+
odtChatSelectSession(_odtChatCurrentId);
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
_odtConnectChatSSE();
|
|
7053
|
+
}
|
|
7054
|
+
|
|
7055
|
+
async function _odtLoadChatSessions() {
|
|
7056
|
+
if (!_v2SelOrg) return;
|
|
7057
|
+
const _loadedForOrg = _v2SelOrg;
|
|
7058
|
+
// Snapshot existing sessions to preserve event caches across refreshes.
|
|
7059
|
+
// Do NOT reset _odtChatSessions to [] here — SSE handlers may inject new
|
|
7060
|
+
// sessions into the live array during the await below, and a reset would
|
|
7061
|
+
// cause them to be lost when we overwrite with the API response.
|
|
7062
|
+
const _prevSessions = _odtChatSessions.slice();
|
|
7063
|
+
try {
|
|
7064
|
+
const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
|
|
7065
|
+
// Primary: structured org run files
|
|
7066
|
+
const runs = await apiFetch('/api/org/'+encodeURIComponent(_loadedForOrg)+'/runs?x=1'+dirParam);
|
|
7067
|
+
if (_v2SelOrg !== _loadedForOrg) return;
|
|
7068
|
+
if (Array.isArray(runs) && runs.length) {
|
|
7069
|
+
// Capture any sessions SSE-injected DURING the await (not in pre-fetch snapshot)
|
|
7070
|
+
const _sseDuringFetch = _odtChatSessions.filter(s => !_prevSessions.find(p => p.id === s.id));
|
|
7071
|
+
_odtChatSessions = runs.map(r => {
|
|
7072
|
+
const prev = _prevSessions.find(s => s.id === r.runId) || _sseDuringFetch.find(s => s.id === r.runId);
|
|
7073
|
+
// Preserve the SSE-incremented cycleCount/eventCount when they exceed the disk-based API values.
|
|
7074
|
+
// The JSONL file may not have flushed the latest checkpoint yet when this fetch resolves,
|
|
7075
|
+
// so the API count can be lower than what SSE has already observed — causing a visible
|
|
7076
|
+
// backwards jump in the "Ncyc" label if we blindly replace with the API value.
|
|
7077
|
+
const _prevMeta = prev?._runMeta;
|
|
7078
|
+
const _mergedMeta = _prevMeta
|
|
7079
|
+
? { ...r, cycleCount: Math.max(r.cycleCount || 0, _prevMeta.cycleCount || 0), eventCount: Math.max(r.eventCount || 0, _prevMeta.eventCount || 0) }
|
|
7080
|
+
: r;
|
|
7081
|
+
return {
|
|
7082
|
+
id: r.runId,
|
|
7083
|
+
ts: r.startedAt,
|
|
7084
|
+
prompt: r.goal || r.runId,
|
|
7085
|
+
status: r.status,
|
|
7086
|
+
_isOrgRun: true,
|
|
7087
|
+
_runMeta: _mergedMeta,
|
|
7088
|
+
events: prev ? prev.events : [],
|
|
7089
|
+
_eventsLoaded: prev ? prev._eventsLoaded : false,
|
|
7090
|
+
};
|
|
7091
|
+
});
|
|
7092
|
+
// Re-attach SSE-only sessions from original snapshot not in API response
|
|
7093
|
+
_prevSessions.forEach(ps => {
|
|
7094
|
+
if (!_odtChatSessions.find(s => s.id === ps.id)) _odtChatSessions.push(ps);
|
|
7095
|
+
});
|
|
7096
|
+
// Re-attach sessions SSE-injected DURING the fetch (not in snapshot, not in API)
|
|
7097
|
+
_sseDuringFetch.forEach(ss => {
|
|
7098
|
+
if (!_odtChatSessions.find(s => s.id === ss.id)) _odtChatSessions.push(ss);
|
|
7099
|
+
});
|
|
7100
|
+
// Sort most-recent-first so SSE-injected new runs (appended above) appear at top of dropdown
|
|
7101
|
+
_odtChatSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
|
|
7102
|
+
_odtPopulateChatSel();
|
|
7103
|
+
// Also merge mastermind sessions matching this org — they are distinct from org
|
|
7104
|
+
// runs (no runId/JSONL file) but contain lifecycle events the user needs to see.
|
|
7105
|
+
// Orgs running via _repeat loops emit session:start/complete events to the mastermind
|
|
7106
|
+
// event store; these never appear in the run list and are otherwise invisible.
|
|
7107
|
+
try {
|
|
7108
|
+
const _mmSessR = await apiFetch('/api/mastermind/sessions');
|
|
7109
|
+
if (_v2SelOrg === _loadedForOrg) {
|
|
7110
|
+
const _mmAll = Array.isArray(_mmSessR) ? _mmSessR : Object.values(_mmSessR.sessions || {});
|
|
7111
|
+
const _mmMatch = _mmAll.filter(_odtOrgSessionMatch).filter(s => !_odtChatSessions.find(x => x.id === s.id));
|
|
7112
|
+
if (_mmMatch.length) {
|
|
7113
|
+
_mmMatch.forEach(s => {
|
|
7114
|
+
if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
|
|
7115
|
+
// Mark events as loaded for complete sessions that already carry the full events[]
|
|
7116
|
+
// from /api/mastermind/sessions — avoids a redundant /api/mastermind/session/:id
|
|
7117
|
+
// fetch (and the "Loading session events…" spinner) on first select.
|
|
7118
|
+
// Running sessions are NOT marked loaded so they still get a fresh fetch.
|
|
7119
|
+
if (s.status !== 'running' && Array.isArray(s.events) && s.events.length > 0) s._eventsLoaded = true;
|
|
7120
|
+
});
|
|
7121
|
+
_odtChatSessions = _odtChatSessions.concat(_mmMatch);
|
|
7122
|
+
_odtChatSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
|
|
7123
|
+
_odtPopulateChatSel();
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
} catch(_) {}
|
|
7127
|
+
if (!_odtChatCurrentId && _odtChatSessions.length) {
|
|
7128
|
+
const first = _odtChatSessions[0];
|
|
7129
|
+
const sel = document.getElementById('odt-chat-sel');
|
|
7130
|
+
if (sel) { sel.value = first.id; odtChatSelectSession(first.id); }
|
|
7131
|
+
}
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
} catch(_) {}
|
|
7135
|
+
if (_v2SelOrg !== _loadedForOrg) return;
|
|
7136
|
+
// Fallback: mastermind lifecycle events (pre-run-file era sessions)
|
|
7137
|
+
try {
|
|
7138
|
+
const r = await apiFetch('/api/mastermind/sessions');
|
|
7139
|
+
if (_v2SelOrg !== _loadedForOrg) return;
|
|
7140
|
+
const all = Array.isArray(r) ? r : Object.values(r.sessions || {});
|
|
7141
|
+
_odtChatSessions = all.filter(_odtOrgSessionMatch).map(s => {
|
|
7142
|
+
if (s.status && s.status !== 'running' && s.status !== 'complete') s.status = 'complete';
|
|
7143
|
+
// Mark complete sessions with events already loaded — avoids redundant lazy-load fetch
|
|
7144
|
+
if (s.status !== 'running' && Array.isArray(s.events) && s.events.length > 0) s._eventsLoaded = true;
|
|
7145
|
+
return s;
|
|
7146
|
+
});
|
|
7147
|
+
// Re-attach SSE-injected org run placeholders that aren't in the mastermind sessions API.
|
|
7148
|
+
// These are created by _odtHandleLiveEvent when a run:start arrives before the JSONL file
|
|
7149
|
+
// flushes to disk — without this, switching to the fallback path drops them.
|
|
7150
|
+
_prevSessions.forEach(ps => {
|
|
7151
|
+
if (ps._isOrgRun && !_odtChatSessions.find(s => s.id === ps.id)) _odtChatSessions.push(ps);
|
|
7152
|
+
});
|
|
7153
|
+
_odtChatSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
|
|
7154
|
+
_odtPopulateChatSel();
|
|
7155
|
+
// Auto-select most recent session when nothing is running (mirrors primary path behaviour).
|
|
7156
|
+
// _odtPopulateChatSel only picks running sessions; for all-complete orgs the feed stays
|
|
7157
|
+
// blank without this.
|
|
7158
|
+
if (!_odtChatCurrentId && _odtChatSessions.length) {
|
|
7159
|
+
const _fbFirst = _odtChatSessions[0];
|
|
7160
|
+
const _fbSel = document.getElementById('odt-chat-sel');
|
|
7161
|
+
if (_fbSel) { _fbSel.value = _fbFirst.id; odtChatSelectSession(_fbFirst.id); }
|
|
7162
|
+
}
|
|
7163
|
+
} catch(_) {}
|
|
7164
|
+
}
|
|
7165
|
+
|
|
7166
|
+
function _odtRebuildRunGroups() {
|
|
7167
|
+
// Group consecutive [R] runs within 30 min with the same goal (or both without a goal).
|
|
7168
|
+
const _ODT_RUN_GAP = 30 * 60 * 1000;
|
|
7169
|
+
const _norm = p => (p || '').toLowerCase().trim().slice(0, 40);
|
|
7170
|
+
_odtRunGroups = {};
|
|
7171
|
+
_odtRunGroupMap = {};
|
|
7172
|
+
const runs = _odtChatSessions.filter(s => s._isOrgRun)
|
|
7173
|
+
.slice().sort((a, b) => (a.ts||a.startedAt||0) - (b.ts||b.startedAt||0));
|
|
7174
|
+
const groups = [];
|
|
7175
|
+
runs.forEach(s => {
|
|
7176
|
+
// No-goal runs (prompt === id) get key '' — group all consecutive ones
|
|
7177
|
+
const key = (s.prompt && s.prompt !== s.id) ? _norm(s.prompt) : '';
|
|
7178
|
+
const sAt = s.ts || s.startedAt || 0;
|
|
7179
|
+
const last = groups[groups.length - 1];
|
|
7180
|
+
if (last && last._normKey === key && (sAt - last._newestAt) < _ODT_RUN_GAP) {
|
|
7181
|
+
last.runs.push(s);
|
|
7182
|
+
last._newestAt = sAt;
|
|
7183
|
+
if (s.status === 'running') last.status = 'running';
|
|
7184
|
+
else if (s.status === 'stale' && last.status !== 'running' && last.status !== 'complete') last.status = 'stale';
|
|
7185
|
+
else if (s.status === 'complete' && last.status !== 'running') last.status = 'complete';
|
|
7186
|
+
} else {
|
|
7187
|
+
groups.push({ id: s.id, runs: [s], _normKey: key, _newestAt: sAt,
|
|
7188
|
+
status: s.status || 'complete', prompt: s.prompt, ts: sAt, _isRunGroup: false });
|
|
7189
|
+
}
|
|
7190
|
+
});
|
|
7191
|
+
groups.forEach(g => {
|
|
7192
|
+
if (g.runs.length > 1) {
|
|
7193
|
+
g._isRunGroup = true;
|
|
7194
|
+
g.id = 'rgrp:' + g.runs[0].id;
|
|
7195
|
+
g.ts = g._newestAt;
|
|
7196
|
+
// Use the most informative goal among sub-runs
|
|
7197
|
+
const withGoal = g.runs.find(r => r.prompt && r.prompt !== r.id);
|
|
7198
|
+
if (withGoal) g.prompt = withGoal.prompt;
|
|
7199
|
+
}
|
|
7200
|
+
_odtRunGroups[g.id] = g;
|
|
7201
|
+
g.runs.forEach(r => { _odtRunGroupMap[r.id] = g.id; });
|
|
7202
|
+
});
|
|
7203
|
+
}
|
|
7204
|
+
|
|
7205
|
+
function _odtGetMergedRunGroupEvents(group) {
|
|
7206
|
+
const all = [];
|
|
7207
|
+
(group.runs || []).forEach(r => (r.events || []).forEach(ev => all.push(ev)));
|
|
7208
|
+
all.sort((a, b) => (a.ts||0) - (b.ts||0));
|
|
7209
|
+
return all;
|
|
7210
|
+
}
|
|
7211
|
+
|
|
7212
|
+
function _odtRebuildSessionGroups() {
|
|
7213
|
+
// Group [S] sessions (loop reps) that share same prompt + gap <20 min into one entry.
|
|
7214
|
+
// [R] runs are never grouped — each is its own run.
|
|
7215
|
+
const _ODT_MAX_GAP = 20 * 60 * 1000;
|
|
7216
|
+
const _odtNorm = p => (p || '').replace(/^running org:\s*/i, '').toLowerCase().replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 40);
|
|
7217
|
+
_odtSessionGroups = {};
|
|
7218
|
+
_odtSessionGroupMap = {};
|
|
7219
|
+
// Process [S] sessions only, sorted oldest first
|
|
7220
|
+
const sSessions = _odtChatSessions.filter(s => !s._isOrgRun)
|
|
7221
|
+
.slice().sort((a, b) => (a.ts||a.startedAt||0) - (b.ts||b.startedAt||0));
|
|
7222
|
+
const groups = [];
|
|
7223
|
+
sSessions.forEach(s => {
|
|
7224
|
+
const key = _odtNorm(s.prompt);
|
|
7225
|
+
const sAt = s.ts || s.startedAt || 0;
|
|
7226
|
+
const last = groups[groups.length - 1];
|
|
7227
|
+
if (last && last._normKey === key && key && (sAt - last._newestAt) < _ODT_MAX_GAP) {
|
|
7228
|
+
last.sessions.push(s);
|
|
7229
|
+
last._newestAt = sAt;
|
|
7230
|
+
last.totalEvents += (s.events || []).length;
|
|
7231
|
+
if (s.status === 'running') last.status = 'running';
|
|
7232
|
+
} else {
|
|
7233
|
+
groups.push({ id: s.id, sessions: [s], _normKey: key, _newestAt: sAt,
|
|
7234
|
+
totalEvents: (s.events || []).length, status: s.status || 'complete',
|
|
7235
|
+
prompt: s.prompt, ts: sAt, _isGroup: false });
|
|
7236
|
+
}
|
|
7237
|
+
});
|
|
7238
|
+
// Finalize: multi-session groups get a virtual ID + merged events
|
|
7239
|
+
groups.forEach(g => {
|
|
7240
|
+
if (g.sessions.length > 1) {
|
|
7241
|
+
g._isGroup = true;
|
|
7242
|
+
g.id = 'grp:' + g.sessions[0].id;
|
|
7243
|
+
g.ts = g._newestAt; // show newest timestamp in dropdown
|
|
7244
|
+
}
|
|
7245
|
+
_odtSessionGroups[g.id] = g;
|
|
7246
|
+
g.sessions.forEach(s => { _odtSessionGroupMap[s.id] = g.id; });
|
|
7247
|
+
});
|
|
7248
|
+
}
|
|
7249
|
+
|
|
7250
|
+
function _odtGetMergedGroupEvents(group) {
|
|
7251
|
+
// Merge events from all sessions in a group, time-sorted
|
|
7252
|
+
const all = [];
|
|
7253
|
+
(group.sessions || []).forEach(s => (s.events || []).forEach(ev => all.push(ev)));
|
|
7254
|
+
all.sort((a, b) => (a.ts||0) - (b.ts||0));
|
|
7255
|
+
return all;
|
|
7256
|
+
}
|
|
7257
|
+
|
|
7258
|
+
function _odtPopulateChatSel() {
|
|
7259
|
+
const sel = document.getElementById('odt-chat-sel');
|
|
7260
|
+
if (!sel) return;
|
|
7261
|
+
if (document.activeElement === sel) return;
|
|
7262
|
+
// Rebuild groups from current sessions + runs
|
|
7263
|
+
_odtRebuildSessionGroups();
|
|
7264
|
+
_odtRebuildRunGroups();
|
|
7265
|
+
const prev = _odtChatCurrentId || sel.value;
|
|
7266
|
+
const runGroups = Object.values(_odtRunGroups).sort((a, b) => b.ts - a.ts);
|
|
7267
|
+
const sessGroups = Object.values(_odtSessionGroups).sort((a, b) => b.ts - a.ts);
|
|
7268
|
+
const totalEntries = runGroups.length + sessGroups.length;
|
|
7269
|
+
const hasRuns = runGroups.length > 0;
|
|
7270
|
+
const hasSess = sessGroups.length > 0;
|
|
7271
|
+
const entryLabel = (hasRuns && hasSess) ? 'entries' : hasRuns ? 'run' + (runGroups.length !== 1 ? 's' : '') : 'loop' + (sessGroups.length !== 1 ? 's' : '');
|
|
7272
|
+
sel.innerHTML = `<option value="">${totalEntries ? `— ${totalEntries} ${entryLabel} for ${_v2SelOrg} —` : '— no runs yet —'}</option>`;
|
|
7273
|
+
|
|
7274
|
+
// Render [R] run groups
|
|
7275
|
+
runGroups.forEach(g => {
|
|
7276
|
+
const d = new Date(g.ts || 0);
|
|
7277
|
+
const ts = d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
7278
|
+
const date = d.toLocaleDateString([],{month:'short',day:'numeric'});
|
|
7279
|
+
const _rawPrompt = (g.prompt || '').replace(/^running org:\s*/i, '').trim();
|
|
7280
|
+
const prompt = (_rawPrompt && _rawPrompt !== g.runs[0]?.id) ? _rawPrompt.slice(0, 30) : '(auto-run)';
|
|
7281
|
+
const runsPart = g._isRunGroup ? ` · ${g.runs.length} runs` : '';
|
|
7282
|
+
// Total event count from run metadata
|
|
7283
|
+
const totalEvs = g.runs.reduce((sum, r) => sum + ((r._runMeta?.eventCount || 0) || (r.events || []).length), 0);
|
|
7284
|
+
const totalCyc = g.runs.reduce((sum, r) => sum + (r._runMeta?.cycleCount || 0), 0);
|
|
7285
|
+
const evPart = totalEvs > 0 ? ` · ${totalCyc}cyc · ${totalEvs}ev` : '';
|
|
7286
|
+
const opt = document.createElement('option');
|
|
7287
|
+
opt.value = g.id;
|
|
7288
|
+
opt.textContent = `[R] ${date} ${ts} ${prompt}${_rawPrompt.length>30?'…':''}${runsPart}${evPart} [${g.status||'?'}]`;
|
|
7289
|
+
if (g.id === prev || g.runs.some(r => r.id === prev)) opt.selected = true;
|
|
7290
|
+
sel.appendChild(opt);
|
|
7291
|
+
});
|
|
7292
|
+
|
|
7293
|
+
// Render [S] groups (collapsed loop reps)
|
|
7294
|
+
groups.forEach(g => {
|
|
7295
|
+
const d = new Date(g.ts || 0);
|
|
7296
|
+
const ts = d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
7297
|
+
const date = d.toLocaleDateString([],{month:'short',day:'numeric'});
|
|
7298
|
+
const _rawPrompt = (g.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim();
|
|
7299
|
+
const prompt = _rawPrompt.slice(0, 28) || '(loop)';
|
|
7300
|
+
const repPart = g._isGroup ? ` · ${g.sessions.length} reps` : '';
|
|
7301
|
+
const evPart = g.totalEvents > 0 ? ` · ${g.totalEvents}ev` : '';
|
|
7302
|
+
const opt = document.createElement('option');
|
|
7303
|
+
opt.value = g.id;
|
|
7304
|
+
opt.textContent = `[S] ${date} ${ts} ${prompt}${_rawPrompt.length>28?'…':''}${repPart}${evPart} [${g.status||'?'}]`;
|
|
7305
|
+
// Selection match: group ID OR any session in the group
|
|
7306
|
+
if (g.id === prev || g.sessions.some(s => s.id === prev)) opt.selected = true;
|
|
7307
|
+
sel.appendChild(opt);
|
|
7308
|
+
});
|
|
7309
|
+
|
|
7310
|
+
// Update label
|
|
7311
|
+
const _lblEl = document.getElementById('odt-chat-sess-lbl');
|
|
7312
|
+
if (_lblEl) _lblEl.textContent = (hasRuns && hasSess) ? 'HISTORY' : hasRuns ? 'RUN' : 'SESSION';
|
|
7313
|
+
// Auto-select running entry
|
|
7314
|
+
if (!_odtChatCurrentId) {
|
|
7315
|
+
const runningRunGrp = runGroups.find(g => g.status === 'running');
|
|
7316
|
+
const runningSessGrp = sessGroups.find(g => g.status === 'running');
|
|
7317
|
+
const first = runningRunGrp || runningSessGrp || runGroups[0] || sessGroups[0];
|
|
7318
|
+
if (first) { sel.value = first.id; odtChatSelectSession(first.id); }
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
window.odtChatSelectSession = async function(id) {
|
|
7323
|
+
_odtChatCurrentId = id;
|
|
7324
|
+
_odtChatCurrentAgent = 'all';
|
|
7325
|
+
const feed = document.getElementById('odt-chat-feed');
|
|
7326
|
+
const emptyEl = document.getElementById('odt-chat-empty');
|
|
7327
|
+
const bar = document.getElementById('odt-chat-agent-bar');
|
|
7328
|
+
feed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
|
|
7329
|
+
if (!id) {
|
|
7330
|
+
if (emptyEl) emptyEl.style.display = 'block';
|
|
7331
|
+
if (bar) bar.style.display = 'none';
|
|
7332
|
+
return;
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
// ── Run group path: render merged events from all runs in the group ──
|
|
7336
|
+
if (id.startsWith('rgrp:') && _odtRunGroups[id]) {
|
|
7337
|
+
const rgrp = _odtRunGroups[id];
|
|
7338
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7339
|
+
// Lazy-load each sub-run's events that haven't been loaded yet
|
|
7340
|
+
for (const r of rgrp.runs) {
|
|
7341
|
+
if (!r._eventsLoaded && !r._loading) {
|
|
7342
|
+
const _ldr = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
|
|
7343
|
+
_ldr.textContent = `Loading ${rgrp.runs.length} runs…`;
|
|
7344
|
+
if (feed) feed.appendChild(_ldr);
|
|
7345
|
+
r._loading = true;
|
|
7346
|
+
try {
|
|
7347
|
+
const _dp = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
|
|
7348
|
+
const evs = await apiFetch('/api/org/'+encodeURIComponent(_v2SelOrg)+'/runs/'+encodeURIComponent(r.id)+_dp);
|
|
7349
|
+
r.events = Array.isArray(evs) ? evs : (r.events || []);
|
|
7350
|
+
r._eventsLoaded = true;
|
|
7351
|
+
} catch(_) { r._eventsLoaded = true; }
|
|
7352
|
+
r._loading = false;
|
|
7353
|
+
_ldr.remove();
|
|
7354
|
+
if (_odtChatCurrentId !== id) return;
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
// Re-render from scratch (all runs now loaded)
|
|
7358
|
+
feed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
|
|
7359
|
+
const mergedEvs = _odtGetMergedRunGroupEvents(rgrp);
|
|
7360
|
+
const _rgrpLabel = (rgrp.prompt && rgrp.prompt !== rgrp.runs[0]?.id)
|
|
7361
|
+
? (rgrp.prompt || '').replace(/^running org:\s*/i, '').trim().slice(0, 80)
|
|
7362
|
+
: '(auto-run)';
|
|
7363
|
+
const _rgrpTs = rgrp.ts || 0;
|
|
7364
|
+
const _rgrpTsStr = _rgrpTs ? new Date(_rgrpTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
|
|
7365
|
+
const _rShdr = document.createElement('div');
|
|
7366
|
+
_rShdr.className = 'cv-session-hdr';
|
|
7367
|
+
_rShdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_rgrpLabel)}</div>`+
|
|
7368
|
+
`<div class="cv-shdr-meta">`+
|
|
7369
|
+
`<span class="cv-shdr-id">${rgrp.runs.length} run${rgrp.runs.length!==1?'s':''}</span>`+
|
|
7370
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7371
|
+
`<span>${esc(_rgrpTsStr)}</span>`+
|
|
7372
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7373
|
+
`<span class="cv-shdr-evcount">${mergedEvs.length} events</span>`+
|
|
7374
|
+
`</div>`;
|
|
7375
|
+
feed.appendChild(_rShdr);
|
|
7376
|
+
const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
7377
|
+
_odtPopulateAgentBar(mergedEvs, orgRoles);
|
|
7378
|
+
mergedEvs.forEach(ev => {
|
|
7379
|
+
_odtAppendEvent(ev, false);
|
|
7380
|
+
const _hk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
|
|
7381
|
+
_odtChatSeenKeys.add(_hk);
|
|
7382
|
+
});
|
|
7383
|
+
const ex = document.getElementById('odt-chat-excerpt');
|
|
7384
|
+
if (ex && mergedEvs.length) renderCvExcerptBanner('odt-chat-excerpt', mergedEvs, rgrp);
|
|
7385
|
+
if (!feed.querySelector('.cv-msg') && emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'No events yet.'; }
|
|
7386
|
+
feed.scrollTop = feed.scrollHeight;
|
|
7387
|
+
return;
|
|
7388
|
+
}
|
|
7389
|
+
|
|
7390
|
+
// ── Group path: render merged events from all sessions in the group ──
|
|
7391
|
+
if (id.startsWith('grp:') && _odtSessionGroups[id]) {
|
|
7392
|
+
const grp = _odtSessionGroups[id];
|
|
7393
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7394
|
+
// Lazy-load each session's events that haven't been loaded yet
|
|
7395
|
+
for (const s of grp.sessions) {
|
|
7396
|
+
if (!s._eventsLoaded) {
|
|
7397
|
+
const _fbLoader = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
|
|
7398
|
+
_fbLoader.textContent = `Loading ${grp.sessions.length} loop sessions…`;
|
|
7399
|
+
if (feed) feed.appendChild(_fbLoader);
|
|
7400
|
+
s._loading = true;
|
|
7401
|
+
try {
|
|
7402
|
+
const _fbData = await apiFetch('/api/mastermind/session/'+encodeURIComponent(s.id));
|
|
7403
|
+
s.events = Array.isArray(_fbData.events) ? _fbData.events : (s.events || []);
|
|
7404
|
+
s._eventsLoaded = true;
|
|
7405
|
+
} catch(_) { s._eventsLoaded = true; }
|
|
7406
|
+
s._loading = false;
|
|
7407
|
+
_fbLoader.remove();
|
|
7408
|
+
if (_odtChatCurrentId !== id) return;
|
|
7409
|
+
break; // Load one at a time; subsequent repaint will finish others
|
|
7410
|
+
}
|
|
7411
|
+
}
|
|
7412
|
+
const mergedEvs = _odtGetMergedGroupEvents(grp);
|
|
7413
|
+
const _normKey = (grp.prompt || '').replace(/^running org:\s*/i, '').replace(/\s*rep\s+\d+.*$/i, '').trim().slice(0, 80) || id;
|
|
7414
|
+
const _shdrTs = grp.ts || 0;
|
|
7415
|
+
const _shdrTsStr = _shdrTs ? new Date(_shdrTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
|
|
7416
|
+
const _shdr = document.createElement('div');
|
|
7417
|
+
_shdr.className = 'cv-session-hdr';
|
|
7418
|
+
_shdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_normKey)}</div>`+
|
|
7419
|
+
`<div class="cv-shdr-meta">`+
|
|
7420
|
+
`<span class="cv-shdr-id" title="${esc(id)}">${grp.sessions.length} reps</span>`+
|
|
7421
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7422
|
+
`<span>${esc(_shdrTsStr)}</span>`+
|
|
7423
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7424
|
+
`<span class="cv-shdr-evcount">${mergedEvs.length} events</span>`+
|
|
7425
|
+
`</div>`;
|
|
7426
|
+
feed.appendChild(_shdr);
|
|
7427
|
+
const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
7428
|
+
_odtPopulateAgentBar(mergedEvs, orgRoles);
|
|
7429
|
+
mergedEvs.forEach(ev => {
|
|
7430
|
+
_odtAppendEvent(ev, false);
|
|
7431
|
+
const _hk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
|
|
7432
|
+
_odtChatSeenKeys.add(_hk);
|
|
7433
|
+
});
|
|
7434
|
+
const ex = document.getElementById('odt-chat-excerpt');
|
|
7435
|
+
if (ex && mergedEvs.length) renderCvExcerptBanner('odt-chat-excerpt', mergedEvs, grp);
|
|
7436
|
+
if (!feed.querySelector('.cv-msg') && emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = 'No events yet.'; }
|
|
7437
|
+
feed.scrollTop = feed.scrollHeight;
|
|
7438
|
+
return;
|
|
7439
|
+
}
|
|
7440
|
+
|
|
7441
|
+
const sess = _odtChatSessions.find(s => s.id === id);
|
|
7442
|
+
if (!sess) return;
|
|
7443
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7444
|
+
// Lazy-load events for fallback (non-_isOrgRun) sessions from /api/mastermind/session/:id
|
|
7445
|
+
if (!sess._isOrgRun && !sess._eventsLoaded) {
|
|
7446
|
+
const _fbFeed = document.getElementById('odt-chat-feed');
|
|
7447
|
+
const _fbLoader = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
|
|
7448
|
+
_fbLoader.textContent = 'Loading session events…';
|
|
7449
|
+
if (_fbFeed) _fbFeed.appendChild(_fbLoader);
|
|
7450
|
+
const _fbPreLoad = (sess.events || []).slice();
|
|
7451
|
+
sess._loading = true;
|
|
7452
|
+
try {
|
|
7453
|
+
const _fbData = await apiFetch('/api/mastermind/session/'+encodeURIComponent(id));
|
|
7454
|
+
const _fbSseDuring = (sess.events || []).filter(e => !_fbPreLoad.find(p => p === e));
|
|
7455
|
+
sess.events = Array.isArray(_fbData.events) ? _fbData.events : _fbPreLoad;
|
|
7456
|
+
_fbSseDuring.forEach(e => {
|
|
7457
|
+
if (!sess.events.find(s => s.ts === e.ts && s.type === e.type && (s.from||'') === (e.from||''))) sess.events.push(e);
|
|
7458
|
+
});
|
|
7459
|
+
sess._eventsLoaded = true;
|
|
7460
|
+
} catch(_) {
|
|
7461
|
+
sess._eventsLoaded = true;
|
|
7462
|
+
}
|
|
7463
|
+
sess._loading = false;
|
|
7464
|
+
// Sync back — _odtLoadChatSessions may have replaced the object reference during the await
|
|
7465
|
+
const _fbLive = _odtChatSessions.find(s => s.id === id);
|
|
7466
|
+
if (_fbLive && _fbLive !== sess) { _fbLive._eventsLoaded = true; _fbLive._loading = false; _fbLive.events = sess.events; }
|
|
7467
|
+
_fbLoader.remove();
|
|
7468
|
+
if (_odtChatCurrentId !== id) return;
|
|
7469
|
+
}
|
|
7470
|
+
// Lazy-load events for org run sessions
|
|
7471
|
+
if (sess._isOrgRun && !sess._eventsLoaded) {
|
|
7472
|
+
const feed2 = document.getElementById('odt-chat-feed');
|
|
7473
|
+
const loadingEl = Object.assign(document.createElement('div'), { id: 'odt-chat-loading', style: 'text-align:center;padding:24px 0;font-size:11px;opacity:0.5;color:var(--text-mid)' });
|
|
7474
|
+
loadingEl.textContent = 'Loading run events…';
|
|
7475
|
+
if (feed2) feed2.appendChild(loadingEl);
|
|
7476
|
+
// Guard SSE handler: prevent it from appending to the feed while fetch is in-flight.
|
|
7477
|
+
// Without this, SSE events get appended immediately AND replayed by the bulk render
|
|
7478
|
+
// below (since they were also pushed to sess.events) → duplicate messages.
|
|
7479
|
+
const _preLoadEvents = (sess.events || []).slice();
|
|
7480
|
+
sess._loading = true;
|
|
7481
|
+
try {
|
|
7482
|
+
const dirParam = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
|
|
7483
|
+
const evs = await apiFetch('/api/org/'+encodeURIComponent(_v2SelOrg)+'/runs/'+encodeURIComponent(id)+dirParam);
|
|
7484
|
+
// Merge: SSE events pushed to sess.events DURING the fetch are not in the API response
|
|
7485
|
+
// (they haven't been flushed to disk yet). Re-attach them so they aren't overwritten.
|
|
7486
|
+
const _sseDuringLoad = (sess.events || []).filter(e => !_preLoadEvents.find(p => p === e));
|
|
7487
|
+
sess.events = Array.isArray(evs) ? evs : _preLoadEvents;
|
|
7488
|
+
_sseDuringLoad.forEach(e => {
|
|
7489
|
+
if (!sess.events.find(s => s.ts === e.ts && s.type === e.type && (s.from||'') === (e.from||''))) sess.events.push(e);
|
|
7490
|
+
});
|
|
7491
|
+
sess._eventsLoaded = true;
|
|
7492
|
+
} catch(_) {
|
|
7493
|
+
// On failure keep whatever was buffered so SSE events aren't lost
|
|
7494
|
+
sess._eventsLoaded = true;
|
|
7495
|
+
}
|
|
7496
|
+
sess._loading = false;
|
|
7497
|
+
// Sync back — _odtLoadChatSessions may have replaced the object reference during the await
|
|
7498
|
+
const _runLive = _odtChatSessions.find(s => s.id === id);
|
|
7499
|
+
if (_runLive && _runLive !== sess) { _runLive._eventsLoaded = true; _runLive._loading = false; _runLive.events = sess.events; }
|
|
7500
|
+
loadingEl.remove();
|
|
7501
|
+
if (_odtChatCurrentId !== id) return; // user switched sessions during fetch
|
|
7502
|
+
}
|
|
7503
|
+
// Agent pills from org config roles (authoritative list, not derived from events)
|
|
7504
|
+
const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
7505
|
+
_odtPopulateAgentBar(sess.events || [], orgRoles);
|
|
7506
|
+
// Inject session metadata header for session-based (non-_isOrgRun) sessions so the
|
|
7507
|
+
// user can see prompt, ID, start time, and status without opening a separate tab.
|
|
7508
|
+
// Org run sessions already get a summary header from the dropdown label + run meta.
|
|
7509
|
+
const ex = document.getElementById('odt-chat-excerpt');
|
|
7510
|
+
if (!sess._isOrgRun) {
|
|
7511
|
+
const _shdrTs = sess.ts || sess.startedAt || 0;
|
|
7512
|
+
const _shdrTsStr = _shdrTs ? new Date(_shdrTs).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : '—';
|
|
7513
|
+
const _shdrPrompt = (sess.prompt || '').replace(/^running org:\s*/i, '').slice(0, 100) || id;
|
|
7514
|
+
const _shdrSt = sess.status || 'unknown';
|
|
7515
|
+
const _shdrStCls = _shdrSt === 'running' ? 'color:oklch(65% 0.20 145)' : _shdrSt === 'complete' ? 'color:var(--text-lo)' : 'color:var(--text-xs)';
|
|
7516
|
+
const _shdr = document.createElement('div');
|
|
7517
|
+
_shdr.className = 'cv-session-hdr';
|
|
7518
|
+
_shdr.innerHTML = `<div class="cv-shdr-prompt">${esc(_shdrPrompt)}</div>`+
|
|
7519
|
+
`<div class="cv-shdr-meta">`+
|
|
7520
|
+
`<span class="cv-shdr-id" title="${esc(id)}">${esc(id.slice(0,22))}</span>`+
|
|
7521
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7522
|
+
`<span style="${_shdrStCls}">${esc(_shdrSt)}</span>`+
|
|
7523
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7524
|
+
`<span>${esc(_shdrTsStr)}</span>`+
|
|
7525
|
+
`<span class="cv-shdr-sep">·</span>`+
|
|
7526
|
+
`<span class="cv-shdr-evcount">${(sess.events || []).length} events</span>`+
|
|
7527
|
+
`</div>`;
|
|
7528
|
+
feed.appendChild(_shdr);
|
|
7529
|
+
// Populate excerpt banner to show last activity for historical sessions
|
|
7530
|
+
if (ex) { if ((sess.events || []).length) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess); else ex.innerHTML = ''; }
|
|
7531
|
+
} else {
|
|
7532
|
+
// Org run sessions: populate excerpt with last activity (mirrors non-_isOrgRun path).
|
|
7533
|
+
// Previous code always cleared this, leaving the banner permanently blank for org runs.
|
|
7534
|
+
if (ex && (sess.events || []).length > 0) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess);
|
|
7535
|
+
else if (ex) ex.innerHTML = '';
|
|
7536
|
+
}
|
|
7537
|
+
(sess.events || []).forEach(ev => {
|
|
7538
|
+
_odtAppendEvent(ev, false);
|
|
7539
|
+
// Pre-seed dedup set so SSE reconnect replays of these historical events are swallowed
|
|
7540
|
+
// rather than appended again. Without this, a reconnect replays ~50 events from the server
|
|
7541
|
+
// and they all pass _odtChatSeenKeys (which only tracks events that arrived via SSE).
|
|
7542
|
+
const _hk = (ev?.ts||'')+'|'+(ev?.type||'')+'|'+(ev?.runId||ev?.session||'')+'|'+(ev?.from||ev?.role||ev?.agent||'')+'|'+(ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
|
|
7543
|
+
_odtChatSeenKeys.add(_hk);
|
|
7544
|
+
});
|
|
7545
|
+
// For [S] sessions (mastermind wrappers that triggered an org run), show a link card to the
|
|
7546
|
+
// corresponding [R] run so users can navigate there directly. The runId is always
|
|
7547
|
+
// "run-" + same timestamp suffix as the session ID (e.g. mm-20260617T235959 → run-20260617T235959).
|
|
7548
|
+
if (!sess._isOrgRun) {
|
|
7549
|
+
const _linkedRunId = 'run-' + (sess.id || '').replace(/^mm-/, '');
|
|
7550
|
+
const _linkedRun = _odtChatSessions.find(s => s._isOrgRun && s.id === _linkedRunId);
|
|
7551
|
+
if (_linkedRun) {
|
|
7552
|
+
const _linkEl = document.createElement('div');
|
|
7553
|
+
_linkEl.className = 'cv-msg cv-sys';
|
|
7554
|
+
const _lrGoal = (_linkedRun.prompt || '').slice(0, 60) || _linkedRunId;
|
|
7555
|
+
_linkEl.innerHTML = `<div class="cv-bub" style="cursor:pointer;border-left:2px solid var(--accent);background:oklch(15% 0.05 220)">`+
|
|
7556
|
+
`<span class="cv-etype" style="color:var(--accent)">RUN</span>`+
|
|
7557
|
+
`<span class="cv-text">→ ${esc(_lrGoal)}${_lrGoal.length >= 60 ? '…' : ''}</span>`+
|
|
7558
|
+
`<span class="cv-ts" style="color:var(--accent)">[${esc(_linkedRun.status||'?')}]</span></div>`;
|
|
7559
|
+
_linkEl.title = 'Click to view the org run triggered by this session';
|
|
7560
|
+
_linkEl.onclick = () => {
|
|
7561
|
+
const _lSel = document.getElementById('odt-chat-sel');
|
|
7562
|
+
if (_lSel) { _lSel.value = _linkedRunId; odtChatSelectSession(_linkedRunId); }
|
|
7563
|
+
};
|
|
7564
|
+
feed.appendChild(_linkEl);
|
|
7565
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7566
|
+
}
|
|
7567
|
+
}
|
|
7568
|
+
// If nothing was rendered (empty run or all events unknown type), show a clear empty state
|
|
7569
|
+
// instead of leaving the user with a blank white feed and no feedback.
|
|
7570
|
+
if (!feed.querySelector('.cv-msg') && emptyEl) {
|
|
7571
|
+
emptyEl.style.display = 'block';
|
|
7572
|
+
emptyEl.textContent = (sess.events && sess.events.length)
|
|
7573
|
+
? 'No displayable events.'
|
|
7574
|
+
: 'No events yet.';
|
|
7575
|
+
}
|
|
7576
|
+
feed.scrollTop = feed.scrollHeight;
|
|
7577
|
+
};
|
|
7578
|
+
|
|
7579
|
+
function _odtPopulateAgentBar(events, orgRoles) {
|
|
7580
|
+
const bar = document.getElementById('odt-chat-agent-bar');
|
|
7581
|
+
if (!bar) return;
|
|
7582
|
+
const agentSet = new Set();
|
|
7583
|
+
// Org config roles are the authoritative agent list — always use them when available
|
|
7584
|
+
const roles = Array.isArray(orgRoles) ? orgRoles : (Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
|
|
7585
|
+
roles.forEach(r => { const n = typeof r === 'string' ? r : (r.id || r.role || r.name || ''); if (n) agentSet.add(n); });
|
|
7586
|
+
// For runs without org config, derive from events (agent:online events are reliable)
|
|
7587
|
+
if (!agentSet.size) {
|
|
7588
|
+
(events || []).forEach(ev => {
|
|
7589
|
+
if (ev.type === 'org:agent:online' && ev.role) agentSet.add(ev.role);
|
|
7590
|
+
else if (ev.type === 'org:comms') {
|
|
7591
|
+
// Support both ev.from (new format) and ev.role (old format) as sender
|
|
7592
|
+
const _sf = ev.from || ev.role || '';
|
|
7593
|
+
if (_sf && _sf.length < 40) agentSet.add(_sf);
|
|
7594
|
+
if (ev.to && ev.to !== 'all' && ev.to.length < 40) agentSet.add(ev.to);
|
|
7595
|
+
}
|
|
7596
|
+
});
|
|
7597
|
+
}
|
|
7598
|
+
// Count messages per agent so pills can show activity level
|
|
7599
|
+
const msgCount = {};
|
|
7600
|
+
(events || []).forEach(ev => {
|
|
7601
|
+
if (ev.type === 'org:comms' || ev.type === 'agent:message') {
|
|
7602
|
+
const sender = ev.from || ev.role || ev.agent || '';
|
|
7603
|
+
if (sender) msgCount[sender] = (msgCount[sender] || 0) + 1;
|
|
7604
|
+
}
|
|
7605
|
+
});
|
|
7606
|
+
const agents = [...agentSet].sort();
|
|
7607
|
+
if (!agents.length) { bar.style.display = 'none'; return; }
|
|
7608
|
+
bar.style.display = 'flex';
|
|
7609
|
+
bar.innerHTML = '<span id="odt-chat-agent-lbl">AGENT</span>';
|
|
7610
|
+
['all', ...agents].forEach(name => {
|
|
7611
|
+
const pill = document.createElement('button');
|
|
7612
|
+
const cnt = name === 'all' ? null : (msgCount[name] || 0);
|
|
7613
|
+
const active = _odtChatCurrentAgent === name;
|
|
7614
|
+
pill.className = 'odt-agent-pill' + (active ? ' active' : '') + (cnt === 0 ? ' dim' : '');
|
|
7615
|
+
pill.textContent = name === 'all' ? 'ALL' : (cnt > 0 ? `${name} (${cnt})` : name);
|
|
7616
|
+
pill.dataset.agent = name;
|
|
7617
|
+
pill.title = name === 'all' ? 'Show all events' : (cnt > 0 ? `${cnt} message${cnt!==1?'s':''} from ${name}` : `${name} was online but sent no messages`);
|
|
7618
|
+
pill.onclick = () => odtChatSelectAgent(name);
|
|
7619
|
+
bar.appendChild(pill);
|
|
7620
|
+
});
|
|
7621
|
+
}
|
|
7622
|
+
|
|
7623
|
+
window.odtChatSelectAgent = function(name) {
|
|
7624
|
+
_odtChatCurrentAgent = name;
|
|
7625
|
+
document.querySelectorAll('#odt-chat-agent-bar .odt-agent-pill').forEach(p => {
|
|
7626
|
+
p.classList.toggle('active', p.dataset.agent === name);
|
|
7627
|
+
});
|
|
7628
|
+
const feed = document.getElementById('odt-chat-feed');
|
|
7629
|
+
const emptyEl = document.getElementById('odt-chat-empty');
|
|
7630
|
+
const sess = _odtChatSessions.find(s => s.id === _odtChatCurrentId);
|
|
7631
|
+
feed.querySelectorAll('.cv-msg').forEach(e => e.remove());
|
|
7632
|
+
if (!sess) { if (emptyEl) emptyEl.style.display = 'block'; return; }
|
|
7633
|
+
const visible = (sess.events || []).filter(ev => _odtChatAgentMatches(ev));
|
|
7634
|
+
if (!visible.length) {
|
|
7635
|
+
if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = name === 'all' ? 'No events.' : `${name} was online in this run but sent no messages.`; }
|
|
7636
|
+
} else {
|
|
7637
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7638
|
+
visible.forEach(ev => _odtAppendEvent(ev, false));
|
|
7639
|
+
}
|
|
7640
|
+
if (!sess._isOrgRun) {
|
|
7641
|
+
const _linkedRunId = 'run-' + (sess.id || '').replace(/^mm-/, '');
|
|
7642
|
+
const _linkedRun = _odtChatSessions.find(s => s._isOrgRun && s.id === _linkedRunId);
|
|
7643
|
+
if (_linkedRun) {
|
|
7644
|
+
const _linkEl = document.createElement('div');
|
|
7645
|
+
_linkEl.className = 'cv-msg cv-sys';
|
|
7646
|
+
const _lrGoal = (_linkedRun.prompt || '').slice(0, 60) || _linkedRunId;
|
|
7647
|
+
_linkEl.innerHTML = `<div class="cv-bub" style="cursor:pointer;border-left:2px solid var(--accent);background:oklch(15% 0.05 220)">`+
|
|
7648
|
+
`<span class="cv-etype" style="color:var(--accent)">RUN</span>`+
|
|
7649
|
+
`<span class="cv-text">→ ${esc(_lrGoal)}${_lrGoal.length >= 60 ? '…' : ''}</span>`+
|
|
7650
|
+
`<span class="cv-ts" style="color:var(--accent)">[${esc(_linkedRun.status||'?')}]</span></div>`;
|
|
7651
|
+
_linkEl.title = 'Click to view the org run triggered by this session';
|
|
7652
|
+
_linkEl.onclick = () => {
|
|
7653
|
+
const _lSel = document.getElementById('odt-chat-sel');
|
|
7654
|
+
if (_lSel) { _lSel.value = _linkedRunId; odtChatSelectSession(_linkedRunId); }
|
|
7655
|
+
};
|
|
7656
|
+
feed.appendChild(_linkEl);
|
|
7657
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7658
|
+
}
|
|
7659
|
+
}
|
|
7660
|
+
feed.scrollTop = feed.scrollHeight;
|
|
7661
|
+
};
|
|
7662
|
+
|
|
7663
|
+
function _odtAppendEvent(ev, animate) {
|
|
7664
|
+
if (!_odtChatAgentMatches(ev)) return;
|
|
7665
|
+
const feed = document.getElementById('odt-chat-feed');
|
|
7666
|
+
if (!feed) return;
|
|
7667
|
+
// Capture scroll position BEFORE appending: only auto-scroll for live events when
|
|
7668
|
+
// the user is already at (or near) the bottom. If they scrolled up to read earlier
|
|
7669
|
+
// messages a new event must not forcibly jump them back to the bottom.
|
|
7670
|
+
const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
|
|
7671
|
+
const emptyEl = document.getElementById('odt-chat-empty');
|
|
7672
|
+
if (emptyEl) emptyEl.style.display = 'none';
|
|
7673
|
+
const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
7674
|
+
let el;
|
|
7675
|
+
if (ev.type === 'run:start') {
|
|
7676
|
+
el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
|
|
7677
|
+
} else if (ev.type === 'run:cycle:complete') {
|
|
7678
|
+
el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
|
|
7679
|
+
} else if (ev.type === 'run:complete') {
|
|
7680
|
+
el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
|
|
7681
|
+
} else if (ev.type === 'org:comms') {
|
|
7682
|
+
// Some older runs use ev.role instead of ev.from — normalise before rendering
|
|
7683
|
+
const _commsFrom = ev.from || ev.role || 'boss';
|
|
7684
|
+
if (ev.to && ev.to !== 'all') el = mkCVIntercom(_commsFrom, ev.to, ev.msg || '', ts);
|
|
7685
|
+
else el = mkCVAgent(_commsFrom, ev.msg || '', ts, 'org:comms');
|
|
7686
|
+
} else if (ev.type === 'org:agent:online') {
|
|
7687
|
+
el = mkCVSys('◉ ' + esc(ev.title || ev.role || '?') + ' online', ts);
|
|
7688
|
+
} else if (ev.type === 'org:checkpoint') {
|
|
7689
|
+
el = mkCVAgent('boss', ev.summary || ev.progress || ev.msg || '', ts, 'org:checkpoint');
|
|
7690
|
+
} else if (ev.type === 'org:start' || ev.type === 'org:complete') {
|
|
7691
|
+
el = mkCVSys(ev.type === 'org:start' ? '▶ Org started' : '■ Org complete', ts);
|
|
7692
|
+
} else if (ev.type === 'agent:spawn') {
|
|
7693
|
+
el = mkCVSpawn(ev.from || 'orchestrator', ev.to || '?', ev.task || ev.briefing || '', ts);
|
|
7694
|
+
} else if (ev.type === 'agent:result') {
|
|
7695
|
+
el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
|
|
7696
|
+
} else if (ev.type === 'file:write') {
|
|
7697
|
+
el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
|
|
7698
|
+
} else if (ev.type === 'intercom') {
|
|
7699
|
+
el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
|
|
7700
|
+
} else if (ev.type === 'agent:message') {
|
|
7701
|
+
el = mkCVAgent(ev.agent || ev.name || '?', ev.msg || ev.message || '', ts, ev.type);
|
|
7702
|
+
} else if (ev.type === 'session:start') {
|
|
7703
|
+
el = mkCVSys('▶ ' + esc((ev.prompt || '').replace(/^running org:\s*/i,'').slice(0,80) || 'Session started'), ts);
|
|
7704
|
+
} else if (ev.type === 'session:complete') {
|
|
7705
|
+
el = mkCVSys('■ Complete' + (ev.status ? ' — ' + esc(ev.status) : ''), ts);
|
|
7706
|
+
} else if (ev.type === 'domain:dispatch') {
|
|
7707
|
+
el = mkCVSys('→ ' + esc(ev.domain || '') + (ev.cmd ? ': ' + esc(ev.cmd.slice(0,80)) : ''), ts);
|
|
7708
|
+
} else if (ev.type === 'domain:complete') {
|
|
7709
|
+
el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
|
|
7710
|
+
} else if (ev.type === 'loop:start') {
|
|
7711
|
+
el = mkCVSys('Loop: ' + esc(ev.command || ''), ts);
|
|
7712
|
+
} else if (ev.type === 'loop:complete') {
|
|
7713
|
+
el = mkCVSys('Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + ev.ranReps + ' runs)' : ''), ts);
|
|
7714
|
+
} else if (ev.type === 'loop:tick') {
|
|
7715
|
+
const _trep = ev.rep ?? ev.fromRep;
|
|
7716
|
+
const _tnext = ev.nextRep ?? ev.toRep;
|
|
7717
|
+
const tickLabel = _trep != null
|
|
7718
|
+
? 'rep ' + esc(String(_trep)) + ' done' + (_tnext != null ? ' → rep ' + esc(String(_tnext)) + (ev.wait != null ? ' in ' + esc(String(ev.wait)) + 's' : '') : '')
|
|
7719
|
+
: esc(ev.command || ev.loopId || '');
|
|
7720
|
+
el = mkCVSys('◷ ' + tickLabel, ts);
|
|
7721
|
+
el.dataset.evType = 'loop:tick';
|
|
7722
|
+
// Collapse: replace the previous loop:tick if it's the last element — prevents the
|
|
7723
|
+
// feed from flooding with one entry per rep in long-running loops (e.g. --repeat 9999).
|
|
7724
|
+
if (feed.lastElementChild?.dataset?.evType === 'loop:tick') feed.lastElementChild.remove();
|
|
7725
|
+
} else if (ev.type === 'loop:hil' || ev.type === 'loop:hil:waiting') {
|
|
7726
|
+
el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.loopId || ''), ts);
|
|
7727
|
+
} else if (ev.type === 'loop:hil:resolved') {
|
|
7728
|
+
el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ''), ts);
|
|
7729
|
+
} else if (ev.type === 'org:error') {
|
|
7730
|
+
el = mkCVSys('⚠ Error: ' + esc(ev.msg || ev.error || ev.message || ''), ts);
|
|
7731
|
+
el.classList.add('cv-err');
|
|
7732
|
+
} else if (ev.type === 'org:agent:offline') {
|
|
7733
|
+
el = mkCVSys('◌ ' + esc(ev.title || ev.role || '?') + ' offline', ts);
|
|
7734
|
+
} else {
|
|
7735
|
+
el = mkCVSys(esc(ev.type || 'event'), ts);
|
|
7736
|
+
}
|
|
7737
|
+
if (animate) el.classList.add('cv-new');
|
|
7738
|
+
feed.appendChild(el);
|
|
7739
|
+
if (atBottom) feed.scrollTop = feed.scrollHeight;
|
|
7740
|
+
}
|
|
7741
|
+
|
|
7742
|
+
function _odtConnectChatSSE() {
|
|
7743
|
+
if (_odtChatSseSource) return;
|
|
7744
|
+
const dot = document.getElementById('odt-chat-live-dot');
|
|
7745
|
+
const lbl = document.getElementById('odt-chat-live-lbl');
|
|
7746
|
+
_odtChatSseSource = new EventSource('/api/mastermind-stream');
|
|
7747
|
+
_odtChatSseSource.onopen = () => { dot?.classList.add('on'); if (lbl) lbl.textContent = 'LIVE'; };
|
|
7748
|
+
_odtChatSseSource.onmessage = e => {
|
|
7749
|
+
try { _odtHandleLiveEvent(JSON.parse(e.data)); } catch(_) {}
|
|
7750
|
+
};
|
|
7751
|
+
_odtChatSseSource.onerror = () => {
|
|
7752
|
+
dot?.classList.remove('on');
|
|
7753
|
+
if (lbl) lbl.textContent = 'OFFLINE';
|
|
7754
|
+
// Close before nulling: EventSource has built-in auto-retry, so without close()
|
|
7755
|
+
// the browser keeps reconnecting the old source. We'd then create a second one
|
|
7756
|
+
// via setTimeout → two SSE connections delivering the same events simultaneously.
|
|
7757
|
+
const _src = _odtChatSseSource;
|
|
7758
|
+
_odtChatSseSource = null;
|
|
7759
|
+
if (_src) _src.close();
|
|
7760
|
+
setTimeout(_odtConnectChatSSE, 5000);
|
|
7761
|
+
};
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
function _odtHandleLiveEvent(ev) {
|
|
7765
|
+
// Deduplicate SSE reconnect replays (server replays last 50 events on every reconnect)
|
|
7766
|
+
const _odtDedupKey = (ev?.ts||'') + '|' + (ev?.type||'') + '|' + (ev?.runId||ev?.session||'') + '|' + (ev?.from||ev?.role||ev?.agent||'') + '|' + (ev?.result||ev?.msg||ev?.message||ev?.summary||'').slice(0,20);
|
|
7767
|
+
if (_odtChatSeenKeys.has(_odtDedupKey)) return;
|
|
7768
|
+
_odtChatSeenKeys.add(_odtDedupKey);
|
|
7769
|
+
// Cap the dedup Set to prevent unbounded memory growth in long-running sessions.
|
|
7770
|
+
// Server replays only the last ~50 events on reconnect, so pruning entries older
|
|
7771
|
+
// than 1000 events never risks a false-pass through the dedup check.
|
|
7772
|
+
if (_odtChatSeenKeys.size > 2000) {
|
|
7773
|
+
const _arr = [..._odtChatSeenKeys];
|
|
7774
|
+
_odtChatSeenKeys = new Set(_arr.slice(1000));
|
|
7775
|
+
}
|
|
7776
|
+
// Route org run events (have runId + org) to the active run session
|
|
7777
|
+
if (ev?.org && ev?.runId && ev.org === _v2SelOrg) {
|
|
7778
|
+
let runSess = _odtChatSessions.find(s => s.id === ev.runId);
|
|
7779
|
+
if (!runSess) {
|
|
7780
|
+
// Create a session placeholder for any unknown runId so SSE events aren't dropped
|
|
7781
|
+
// while _odtLoadChatSessions fetch is still in-flight (opening Chat tab mid-run).
|
|
7782
|
+
// For run:start: full setup + auto-switch. For other types: minimal placeholder
|
|
7783
|
+
// with _eventsLoaded:false so lazy-load fetches full history when selected.
|
|
7784
|
+
runSess = { id: ev.runId, ts: ev.ts || Date.now(), prompt: ev.goal || '', status: 'running', _isOrgRun: true, _eventsLoaded: ev.type === 'run:start', _runMeta: { cycleCount: 0, eventCount: 1 }, events: [ev] };
|
|
7785
|
+
_odtChatSessions.unshift(runSess);
|
|
7786
|
+
// Always update the dropdown so the run is selectable even when opened mid-run
|
|
7787
|
+
_odtPopulateChatSel();
|
|
7788
|
+
if (ev.type === 'run:start') {
|
|
7789
|
+
// After _odtPopulateChatSel above rebuilt groups, check if the new run joined
|
|
7790
|
+
// the currently-selected group. If so, keep the group selected. Otherwise switch.
|
|
7791
|
+
const _newGrpId = _odtRunGroupMap[ev.runId];
|
|
7792
|
+
const _alreadySelected = _newGrpId && _odtChatCurrentId === _newGrpId;
|
|
7793
|
+
if (!_alreadySelected) {
|
|
7794
|
+
const _rsSel = document.getElementById('odt-chat-sel');
|
|
7795
|
+
// Switch to the run's group if it has one, otherwise to the individual run
|
|
7796
|
+
const _switchId = _newGrpId || ev.runId;
|
|
7797
|
+
if (_rsSel) { _rsSel.value = _switchId; odtChatSelectSession(_switchId); }
|
|
7798
|
+
}
|
|
7799
|
+
}
|
|
7800
|
+
} else if (runSess) {
|
|
7801
|
+
(runSess.events = runSess.events || []).push(ev);
|
|
7802
|
+
if (runSess._runMeta) runSess._runMeta.eventCount = (runSess._runMeta.eventCount || 0) + 1;
|
|
7803
|
+
if (ev.type === 'run:cycle:complete' || ev.type === 'org:checkpoint') { if (runSess._runMeta) runSess._runMeta.cycleCount = (runSess._runMeta.cycleCount || 0) + 1; _odtPopulateChatSel(); }
|
|
7804
|
+
// Backfill prompt from first boss directive when run:start had no goal field
|
|
7805
|
+
if (!runSess.prompt && ev.type === 'org:comms' && (ev.from === 'boss' || ev.role === 'boss') && ev.msg) {
|
|
7806
|
+
runSess.prompt = ev.msg.slice(0, 80);
|
|
7807
|
+
_odtPopulateChatSel();
|
|
7808
|
+
}
|
|
7809
|
+
if (ev.type === 'run:complete' || ev.type === 'org:complete') { runSess.status = 'complete'; _odtPopulateChatSel(); }
|
|
7810
|
+
// Check if this run's group (or the run itself) is currently selected
|
|
7811
|
+
const _evRunGrpId = _odtRunGroupMap[ev.runId];
|
|
7812
|
+
const _isSelRun = _odtChatCurrentId === ev.runId;
|
|
7813
|
+
const _isSelRunGrp = _evRunGrpId && _odtChatCurrentId === _evRunGrpId;
|
|
7814
|
+
if ((_isSelRun || _isSelRunGrp) && !runSess._loading) {
|
|
7815
|
+
const hasNewRunAgent = (ev.role && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.role)}"]`))
|
|
7816
|
+
|| (ev.from && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.from)}"]`))
|
|
7817
|
+
|| (ev.to && ev.to !== 'all' && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.to)}"]`));
|
|
7818
|
+
if (hasNewRunAgent) _odtPopulateAgentBar(runSess.events, Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
|
|
7819
|
+
_odtAppendEvent(ev, true);
|
|
7820
|
+
renderCvExcerptBanner('odt-chat-excerpt', runSess.events, runSess);
|
|
7821
|
+
const _rEvCnt = document.querySelector('#odt-chat-feed .cv-session-hdr .cv-shdr-evcount');
|
|
7822
|
+
if (_rEvCnt) {
|
|
7823
|
+
const _rgrpEvs = _isSelRunGrp ? _odtGetMergedRunGroupEvents(_odtRunGroups[_evRunGrpId]) : runSess.events;
|
|
7824
|
+
_rEvCnt.textContent = _rgrpEvs.length + ' events';
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
return;
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7831
|
+
if (!ev?.session) return;
|
|
7832
|
+
// Find existing session first — if already in our list it's been validated as belonging to
|
|
7833
|
+
// this org, so skip the prompt-based org check that would falsely drop events whose
|
|
7834
|
+
// individual payloads don't carry an ev.org field (e.g. intercom, agent:message).
|
|
7835
|
+
let sess = _odtChatSessions.find(s => s.id === ev.session);
|
|
7836
|
+
if (!sess) {
|
|
7837
|
+
const isOrgSess = _odtOrgSessionMatch({ id: ev.session, prompt: ev.prompt || '', events: [ev] });
|
|
7838
|
+
if (!isOrgSess) return;
|
|
7839
|
+
}
|
|
7840
|
+
if (!sess && ev.type === 'session:start') {
|
|
7841
|
+
sess = { id: ev.session, ts: ev.ts || Date.now(), prompt: ev.prompt || '', status: 'running', events: [ev] };
|
|
7842
|
+
_odtChatSessions.unshift(sess);
|
|
7843
|
+
_odtPopulateChatSel();
|
|
7844
|
+
// Auto-select if nothing selected (tab-guard removed: feed is in DOM regardless of active tab)
|
|
7845
|
+
if (!_odtChatCurrentId) {
|
|
7846
|
+
const sel = document.getElementById('odt-chat-sel');
|
|
7847
|
+
if (sel) { sel.value = ev.session; odtChatSelectSession(ev.session); }
|
|
7848
|
+
}
|
|
7849
|
+
return;
|
|
7850
|
+
}
|
|
7851
|
+
if (sess) {
|
|
7852
|
+
(sess.events = sess.events || []).push(ev);
|
|
7853
|
+
if (ev.type === 'session:complete') { sess.status = 'complete'; _odtPopulateChatSel(); }
|
|
7854
|
+
// Determine if a group containing this session is currently selected
|
|
7855
|
+
const _evGrpId = _odtSessionGroupMap[ev.session];
|
|
7856
|
+
const _isCurrentSess = _odtChatCurrentId === ev.session;
|
|
7857
|
+
const _isCurrentGrp = _evGrpId && _odtChatCurrentId === _evGrpId;
|
|
7858
|
+
const _notLoading = !sess._loading && (!_evGrpId || !(_odtSessionGroups[_evGrpId]?.sessions || []).some(s => s._loading));
|
|
7859
|
+
if ((_isCurrentSess || _isCurrentGrp) && _notLoading) {
|
|
7860
|
+
// sess._loading is true while odtChatSelectSession's lazy-load fetch is in-flight.
|
|
7861
|
+
const hasNewAgent = (ev.agent && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.agent)}"]`))
|
|
7862
|
+
|| (ev.from && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.from)}"]`))
|
|
7863
|
+
|| (ev.to && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.to)}"]`));
|
|
7864
|
+
if (hasNewAgent) _odtPopulateAgentBar(sess.events, Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
|
|
7865
|
+
if (!sess._isJsonl) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess);
|
|
7866
|
+
_odtAppendEvent(ev, true);
|
|
7867
|
+
// Keep event count in session header current (frozen at load time otherwise)
|
|
7868
|
+
const _sEvCnt = document.querySelector('#odt-chat-feed .cv-session-hdr .cv-shdr-evcount');
|
|
7869
|
+
if (_sEvCnt) _sEvCnt.textContent = ((_isCurrentGrp ? _odtGetMergedGroupEvents(_odtSessionGroups[_evGrpId]) : sess.events).length) + ' events';
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7873
|
+
|
|
7874
|
+
// ── ORG CONFIG ─────────────────────────────────────────────
|
|
7875
|
+
function v2RenderOrgConfig() {
|
|
7876
|
+
const el = document.getElementById('odt-config');
|
|
7877
|
+
if (!el || !_v2OrgData || !_v2SelOrg) return;
|
|
7878
|
+
const d = _v2OrgData;
|
|
7879
|
+
const rc = d.run_config || {};
|
|
7880
|
+
const gov = (d.governance && typeof d.governance === 'object') ? d.governance : { policy: d.governance || 'auto' };
|
|
7881
|
+
const topos = ['hierarchical','hierarchical-mesh','mesh','star','ring','adaptive','hybrid'];
|
|
7882
|
+
const modes = ['daemon','once','scheduled'];
|
|
7883
|
+
const statuses = ['active','paused','archived'];
|
|
7884
|
+
const govPolicies = ['auto','board','strict'];
|
|
7885
|
+
const inp = (id,val,type,extra) => '<input class="filter-input" id="'+id+'" type="'+(type||'text')+'" value="'+esc(String(val??''))+'" '+(extra||'')+' style="width:100%;box-sizing:border-box">';
|
|
7886
|
+
const sel = (id,opts,cur) => '<select class="filter-input" id="'+id+'" style="width:100%;box-sizing:border-box;cursor:pointer">'+opts.map(o=>'<option'+(cur===o?' selected':'')+'>'+esc(o)+'</option>').join('')+'</select>';
|
|
7887
|
+
const fld = (lbl,html) => '<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:10px;color:var(--text-lo)">'+lbl+'</div>'+html+'</div>';
|
|
7888
|
+
const sec = (title,inner) => '<div style="margin-bottom:22px"><div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin-bottom:10px;padding-bottom:5px;border-bottom:1px solid var(--border)">'+title+'</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:11px">'+inner+'</div></div>';
|
|
7889
|
+
el.innerHTML = '<div style="padding:18px;overflow-y:auto;max-width:660px">'
|
|
7890
|
+
+sec('GENERAL',
|
|
7891
|
+
fld('Name', inp('oc-name', d.name||'','text','readonly style="opacity:0.5"'))
|
|
7892
|
+
+fld('Status', sel('oc-status', statuses, d.status||'active'))
|
|
7893
|
+
+'<div style="grid-column:1/-1">'+fld('Goal', '<textarea id="oc-goal" class="filter-input" rows="3" style="width:100%;box-sizing:border-box;resize:vertical;line-height:1.5">'+esc(d.goal||'')+'</textarea>')+'</div>'
|
|
7894
|
+
+fld('Mode', sel('oc-mode', modes, d.mode||'daemon'))
|
|
7895
|
+
+fld('Topology', sel('oc-topology', topos, d.topology||'hierarchical'))
|
|
7896
|
+
+'<div style="grid-column:1/-1">'+fld('Schedule', inp('oc-schedule', d.schedule||''))+'</div>'
|
|
7897
|
+
)
|
|
7898
|
+
+sec('RUN CONFIG',
|
|
7899
|
+
fld('Max Concurrent Agents', inp('oc-max-agents', rc.max_concurrent_agents??6,'number','min="1" max="32"'))
|
|
7900
|
+
+fld('Checkpoint Interval (min)', inp('oc-checkpoint', rc.checkpoint_interval_min??30,'number','min="5"'))
|
|
7901
|
+
+fld('Budget Tokens (0=unlimited)', inp('oc-budget', rc.budget_tokens??0,'number','min="0"'))
|
|
7902
|
+
+fld('Alert Threshold (0-1)', inp('oc-alert', rc.alert_threshold??0.8,'number','step="0.05" min="0" max="1"'))
|
|
7903
|
+
+fld('Boss Role ID', inp('oc-boss', rc.boss_role||''))
|
|
7904
|
+
+fld('Memory Namespace', inp('oc-namespace', rc.memory_namespace||('org:'+d.name)))
|
|
7905
|
+
+'<div style="grid-column:1/-1;display:flex;align-items:center;gap:8px;margin-top:4px"><input type="checkbox" id="oc-spawn-all" '+(rc.spawn_all_roles?'checked':'')+' style="accent-color:var(--accent);width:14px;height:14px;cursor:pointer"><label for="oc-spawn-all" style="font-size:12px;color:var(--text-mid);cursor:pointer">Spawn all roles as separate agents</label></div>'
|
|
7906
|
+
)
|
|
7907
|
+
+sec('GOVERNANCE',
|
|
7908
|
+
fld('Approval Policy', sel('oc-gov-policy', govPolicies, gov.policy||'auto'))
|
|
7909
|
+
+fld('Approvals File', inp('oc-gov-file', gov.approvals_file||('.monomind/orgs/'+d.name+'-approvals.json')))
|
|
7910
|
+
)
|
|
7911
|
+
+'<div style="display:flex;align-items:center;gap:10px"><button class="btn" style="border-color:var(--accent);color:var(--accent)" onclick="v2SaveOrgConfig()">Save Config</button><span id="oc-status-msg" style="font-size:11px;color:var(--text-lo)"></span></div>'
|
|
7912
|
+
+'</div>';
|
|
7913
|
+
}
|
|
7914
|
+
|
|
7915
|
+
async function v2SaveOrgConfig() {
|
|
7916
|
+
if (!_v2SelOrg || !_v2OrgData) return;
|
|
7917
|
+
const g = id => document.getElementById(id);
|
|
7918
|
+
// Only include org config fields — exclude runtime data (state, goals, routines, approvals,
|
|
7919
|
+
// running, tasks, config nested copy) that would corrupt the org .json file.
|
|
7920
|
+
const _RUNTIME_KEYS = new Set(['config','state','goals','routines','approvals','running','tasks']);
|
|
7921
|
+
const _baseConfig = Object.fromEntries(
|
|
7922
|
+
Object.entries(_v2OrgData).filter(([k]) => !k.startsWith('_') && !_RUNTIME_KEYS.has(k))
|
|
7923
|
+
);
|
|
7924
|
+
const cfg = {
|
|
7925
|
+
..._baseConfig,
|
|
7926
|
+
status: (g('oc-status')?.value) || _v2OrgData.status,
|
|
7927
|
+
goal: (g('oc-goal')?.value) || _v2OrgData.goal,
|
|
7928
|
+
mode: (g('oc-mode')?.value) || _v2OrgData.mode,
|
|
7929
|
+
topology: (g('oc-topology')?.value) || _v2OrgData.topology,
|
|
7930
|
+
schedule: (g('oc-schedule')?.value||'').trim() || null,
|
|
7931
|
+
run_config: { ...(_v2OrgData.run_config||{}),
|
|
7932
|
+
max_concurrent_agents: parseInt(g('oc-max-agents')?.value)||6,
|
|
7933
|
+
checkpoint_interval_min: parseInt(g('oc-checkpoint')?.value)||30,
|
|
7934
|
+
budget_tokens: parseInt(g('oc-budget')?.value)||0,
|
|
7935
|
+
alert_threshold: parseFloat(g('oc-alert')?.value)||0.8,
|
|
7936
|
+
boss_role: (g('oc-boss')?.value||'').trim(),
|
|
7937
|
+
memory_namespace: (g('oc-namespace')?.value||'').trim()||('org:'+_v2SelOrg),
|
|
7938
|
+
spawn_all_roles: !!(g('oc-spawn-all')?.checked),
|
|
7939
|
+
},
|
|
7940
|
+
governance: {
|
|
7941
|
+
policy: (g('oc-gov-policy')?.value)||'auto',
|
|
7942
|
+
approvals_file: (g('oc-gov-file')?.value||'').trim()||('.monomind/orgs/'+_v2SelOrg+'-approvals.json'),
|
|
7943
|
+
},
|
|
7944
|
+
};
|
|
7945
|
+
const msg = document.getElementById('oc-status-msg');
|
|
7946
|
+
if (msg) { msg.style.color='var(--text-lo)'; msg.textContent='Saving...'; }
|
|
7947
|
+
try {
|
|
7948
|
+
const qs = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
|
|
7949
|
+
const r = await fetch('/api/orgs'+qs, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(cfg) });
|
|
7950
|
+
const json = await r.json();
|
|
7951
|
+
if (json.ok) {
|
|
7952
|
+
_v2OrgData = Object.assign({}, _v2OrgData, cfg);
|
|
7953
|
+
if (msg) { msg.style.color='oklch(68% 0.20 150)'; msg.textContent='Saved'; setTimeout(()=>{ if(msg) msg.textContent=''; }, 2500); }
|
|
7954
|
+
if (typeof showToast === 'function') showToast('Config saved','','ok');
|
|
7955
|
+
} else { throw new Error(json.error||'Save failed'); }
|
|
7956
|
+
} catch(e) {
|
|
7957
|
+
if (msg) { msg.style.color='oklch(60% 0.22 25)'; msg.textContent='Error: '+e.message; }
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7961
|
+
// ── ORG FILES ──────────────────────────────────────────────
|
|
7962
|
+
async function v2RenderOrgFiles() {
|
|
7963
|
+
const el = document.getElementById('odt-files');
|
|
7964
|
+
if (!el || !_v2SelOrg) return;
|
|
7965
|
+
const _rendOrg = _v2SelOrg;
|
|
7966
|
+
el.innerHTML = '<div style="padding:20px;color:var(--text-lo)">Loading files...</div>';
|
|
7967
|
+
try {
|
|
7968
|
+
const enc = encodeURIComponent(_rendOrg);
|
|
7969
|
+
const qs = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
|
|
7970
|
+
const files = await fetch('/api/org/'+enc+'/files'+qs).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
7971
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
7972
|
+
if (!files.length) { el.innerHTML = '<div style="padding:20px;color:var(--text-lo)">No files found.</div>'; return; }
|
|
7973
|
+
const fmtSize = b => b < 1024 ? b+'B' : b < 1048576 ? (b/1024).toFixed(1)+'K' : (b/1048576).toFixed(1)+'M';
|
|
7974
|
+
const fmtTime = s => { const d = new Date(s); return d.toLocaleDateString([],{month:'short',day:'numeric'})+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); };
|
|
7975
|
+
const typeColor = { config:'oklch(62% 0.20 186)', generated:'oklch(68% 0.20 150)', 'agent-definition':'oklch(65% 0.20 280)', approvals:'oklch(70% 0.18 60)', state:'oklch(70% 0.18 60)', goals:'oklch(70% 0.18 60)' };
|
|
7976
|
+
const groupOrder = ['config','generated','agent-definition','approvals','state','goals','routines','other'];
|
|
7977
|
+
const groups = {};
|
|
7978
|
+
groupOrder.forEach(k => { groups[k] = { label: k==='agent-definition'?'AGENT DEFINITIONS':k.toUpperCase().replace(/-/g,' '), items:[] }; });
|
|
7979
|
+
files.forEach(f => { const k = groups[f.type] ? f.type : 'other'; if (!groups[k]) groups[k]={label:f.type.toUpperCase(),items:[]}; groups[k].items.push(f); });
|
|
7980
|
+
const color = t => typeColor[t]||'var(--text-lo)';
|
|
7981
|
+
const badge = t => '<span style="font-size:9px;padding:1px 6px;border-radius:8px;border:1px solid '+color(t)+';color:'+color(t)+'">'+t+'</span>';
|
|
7982
|
+
el.innerHTML = '<div style="padding:16px">'+groupOrder.filter(k => groups[k]&&groups[k].items.length).map(k => {
|
|
7983
|
+
const g = groups[k];
|
|
7984
|
+
return '<div style="margin-bottom:20px"><div style="font-size:9px;letter-spacing:2px;color:var(--text-xs);margin-bottom:8px;padding-bottom:5px;border-bottom:1px solid var(--border)">'+g.label+' ('+g.items.length+')</div>'+
|
|
7985
|
+
'<table style="width:100%;border-collapse:collapse;font-size:12px">'+
|
|
7986
|
+
'<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>'+
|
|
7987
|
+
'<tbody>'+g.items.map(f => {
|
|
7988
|
+
return '<tr style="border-top:1px solid var(--border)" data-fp="'+esc(f.path)+'" data-fn="'+esc(f.name)+'">'+
|
|
7989
|
+
'<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>'+
|
|
7990
|
+
'<td style="padding:6px 8px">'+badge(f.type)+'</td>'+
|
|
7991
|
+
'<td style="padding:6px 8px;font-size:10px;color:var(--text-lo);font-family:var(--mono)">'+fmtSize(f.size||0)+'</td>'+
|
|
7992
|
+
'<td style="padding:6px 8px;font-size:10px;color:var(--text-lo)">'+fmtTime(f.mtime)+'</td>'+
|
|
7993
|
+
'<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>';
|
|
7994
|
+
}).join('')+'</tbody></table></div>';
|
|
7995
|
+
}).join('')+'</div>';
|
|
7996
|
+
} catch(e) { el.innerHTML = '<div style="padding:20px;color:oklch(60% 0.22 25)">Error: '+e.message+'</div>'; }
|
|
7997
|
+
}
|
|
7998
|
+
|
|
7999
|
+
let _ofModal = null;
|
|
8000
|
+
let _ofRawText = '';
|
|
8001
|
+
|
|
8002
|
+
function _ofMdToHtml(md) {
|
|
8003
|
+
return md
|
|
8004
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
8005
|
+
.replace(/^#{6}\s+(.+)$/gm,'<h6 style="font-size:11px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h6>')
|
|
8006
|
+
.replace(/^#{5}\s+(.+)$/gm,'<h5 style="font-size:12px;font-weight:700;color:var(--text-hi);margin:10px 0 4px">$1</h5>')
|
|
8007
|
+
.replace(/^#{4}\s+(.+)$/gm,'<h4 style="font-size:13px;font-weight:700;color:var(--text-hi);margin:12px 0 5px">$1</h4>')
|
|
8008
|
+
.replace(/^#{3}\s+(.+)$/gm,'<h3 style="font-size:14px;font-weight:700;color:var(--text-hi);margin:14px 0 6px">$1</h3>')
|
|
8009
|
+
.replace(/^#{2}\s+(.+)$/gm,'<h2 style="font-size:16px;font-weight:700;color:var(--accent);margin:16px 0 7px;padding-bottom:4px;border-bottom:1px solid var(--border)">$1</h2>')
|
|
8010
|
+
.replace(/^#{1}\s+(.+)$/gm,'<h1 style="font-size:18px;font-weight:700;color:var(--accent);margin:18px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--border)">$1</h1>')
|
|
8011
|
+
.replace(/```[\w]*\n?([\s\S]*?)```/g,'<pre style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:10px 12px;font-size:10px;overflow-x:auto;margin:8px 0;line-height:1.6">$1</pre>')
|
|
8012
|
+
.replace(/`([^`]+)`/g,'<code style="background:var(--surface);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-size:10px;font-family:var(--mono)">$1</code>')
|
|
8013
|
+
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
|
|
8014
|
+
.replace(/\*(.+?)\*/g,'<em>$1</em>')
|
|
8015
|
+
.replace(/^---+$/gm,'<hr style="border:none;border-top:1px solid var(--border);margin:12px 0">')
|
|
8016
|
+
.replace(/^\s*[-*]\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
|
|
8017
|
+
.replace(/(<li.*<\/li>\n?)+/g,'<ul style="padding-left:18px;margin:6px 0">$&</ul>')
|
|
8018
|
+
.replace(/^\d+\.\s+(.+)$/gm,'<li style="margin:2px 0;padding-left:4px">$1</li>')
|
|
8019
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" style="color:var(--accent);text-decoration:underline" target="_blank" rel="noopener">$1</a>')
|
|
8020
|
+
.replace(/\n\n+/g,'</p><p style="margin:0 0 8px">')
|
|
8021
|
+
.replace(/^(?!<[hupoli]|<pre|<hr)(.+)$/gm, '$1')
|
|
8022
|
+
.replace(/\n/g,'<br>');
|
|
8023
|
+
}
|
|
8024
|
+
|
|
8025
|
+
async function v2ViewOrgFile(filePath, fileName) {
|
|
8026
|
+
if (_ofModal) { _ofModal.remove(); _ofModal = null; }
|
|
8027
|
+
const isMd = /\.(md|markdown)$/i.test(fileName);
|
|
8028
|
+
const isJson = /\.json$/i.test(fileName);
|
|
8029
|
+
_ofRawText = '';
|
|
8030
|
+
_ofModal = document.createElement('div');
|
|
8031
|
+
_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';
|
|
8032
|
+
_ofModal.onclick = e => { if (e.target===_ofModal) { _ofModal.remove(); _ofModal=null; } };
|
|
8033
|
+
_ofModal.innerHTML =
|
|
8034
|
+
'<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;max-width:900px;width:100%;max-height:88vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 40px rgba(0,0,0,0.6)">' +
|
|
8035
|
+
'<div style="display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--border);flex-shrink:0">' +
|
|
8036
|
+
'<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>' +
|
|
8037
|
+
(isMd ? '<button id="of-toggle" class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofToggleRender()">Raw</button>' : '') +
|
|
8038
|
+
'<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>' +
|
|
8039
|
+
'<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>' +
|
|
8040
|
+
'<button class="btn" style="font-size:9px;padding:2px 7px" onclick="_ofModal&&(_ofModal.remove(),_ofModal=null)">✕</button>' +
|
|
8041
|
+
'</div>' +
|
|
8042
|
+
'<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">' +
|
|
8043
|
+
'<span style="color:var(--text-lo)">Loading…</span>' +
|
|
8044
|
+
'</div>' +
|
|
8045
|
+
'</div>';
|
|
8046
|
+
document.body.appendChild(_ofModal);
|
|
8047
|
+
try {
|
|
8048
|
+
const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
|
|
8049
|
+
const r = await fetch('/api/file-content?path='+encodeURIComponent(filePath)+dirParam);
|
|
8050
|
+
const el = document.getElementById('of-content');
|
|
8051
|
+
if (!el) return;
|
|
8052
|
+
if (!r.ok) { el.textContent = 'Error '+r.status+': '+r.statusText; return; }
|
|
8053
|
+
_ofRawText = await r.text();
|
|
8054
|
+
if (isMd) {
|
|
8055
|
+
el.innerHTML = '<div style="max-width:720px">'+_ofMdToHtml(_ofRawText)+'</div>';
|
|
8056
|
+
el.dataset.mode = 'rendered';
|
|
8057
|
+
} else if (isJson) {
|
|
8058
|
+
try { el.textContent = JSON.stringify(JSON.parse(_ofRawText), null, 2); } catch(_) { el.textContent = _ofRawText; }
|
|
8059
|
+
el.style.fontFamily = 'var(--mono)'; el.style.fontSize = '11px'; el.style.whiteSpace = 'pre-wrap';
|
|
8060
|
+
} else {
|
|
8061
|
+
el.textContent = _ofRawText;
|
|
8062
|
+
el.style.fontFamily = 'var(--mono)'; el.style.fontSize = '11px'; el.style.whiteSpace = 'pre-wrap';
|
|
8063
|
+
}
|
|
8064
|
+
} catch(e) {
|
|
8065
|
+
const el = document.getElementById('of-content');
|
|
8066
|
+
if (el) el.textContent = 'Error: '+e.message;
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
|
|
8070
|
+
function _ofToggleRender() {
|
|
8071
|
+
const el = document.getElementById('of-content');
|
|
8072
|
+
const btn = document.getElementById('of-toggle');
|
|
8073
|
+
if (!el || !_ofRawText) return;
|
|
8074
|
+
if (el.dataset.mode === 'rendered') {
|
|
8075
|
+
el.textContent = _ofRawText;
|
|
8076
|
+
el.style.fontFamily = 'var(--mono)'; el.style.fontSize = '11px'; el.style.whiteSpace = 'pre-wrap';
|
|
8077
|
+
el.dataset.mode = 'raw';
|
|
8078
|
+
if (btn) btn.textContent = 'Preview';
|
|
8079
|
+
} else {
|
|
8080
|
+
el.innerHTML = '<div style="max-width:720px">'+_ofMdToHtml(_ofRawText)+'</div>';
|
|
8081
|
+
el.style.fontFamily = ''; el.style.fontSize = '12px'; el.style.whiteSpace = '';
|
|
8082
|
+
el.dataset.mode = 'rendered';
|
|
8083
|
+
if (btn) btn.textContent = 'Raw';
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
function _ofSaveFile(defaultName) {
|
|
8088
|
+
if (!_ofRawText) return;
|
|
8089
|
+
const a = document.createElement('a');
|
|
8090
|
+
a.href = URL.createObjectURL(new Blob([_ofRawText], { type: 'text/plain' }));
|
|
8091
|
+
a.download = defaultName || 'file.txt';
|
|
8092
|
+
a.click();
|
|
8093
|
+
URL.revokeObjectURL(a.href);
|
|
8094
|
+
}
|
|
8095
|
+
|
|
6298
8096
|
// ── WORKSPACES ─────────────────────────────────────────────
|
|
6299
8097
|
async function v2RenderOrgWorkspaces() {
|
|
6300
8098
|
const el = document.getElementById('odt-workspaces');
|
|
6301
8099
|
if (!el || !_v2SelOrg) return;
|
|
8100
|
+
const _rendOrg = _v2SelOrg;
|
|
6302
8101
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6303
8102
|
try {
|
|
6304
|
-
const _enc = encodeURIComponent(
|
|
8103
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6305
8104
|
const data = await fetch(`/api/org/${_enc}/workspaces${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
8105
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6306
8106
|
const ws = Array.isArray(data) ? data : (data.workspaces || []);
|
|
6307
8107
|
if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured</div>'; return; }
|
|
6308
8108
|
el.innerHTML = ws.map(w => `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
|
|
@@ -6320,10 +8120,12 @@ async function v2RenderOrgWorkspaces() {
|
|
|
6320
8120
|
async function v2RenderOrgInvites() {
|
|
6321
8121
|
const el = document.getElementById('odt-invites');
|
|
6322
8122
|
if (!el || !_v2SelOrg) return;
|
|
8123
|
+
const _rendOrg = _v2SelOrg;
|
|
6323
8124
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6324
8125
|
try {
|
|
6325
|
-
const _enc = encodeURIComponent(
|
|
8126
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6326
8127
|
const data = await fetch(`/api/org/${_enc}/invites${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
8128
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6327
8129
|
const invites = Array.isArray(data) ? data : (data.invites || []);
|
|
6328
8130
|
if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens</div>'; return; }
|
|
6329
8131
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6352,12 +8154,12 @@ function v2RenderOrgAgentsFull() {
|
|
|
6352
8154
|
</tr></thead>
|
|
6353
8155
|
<tbody>${agents.map(a => `<tr style="border-top:1px solid var(--border)">
|
|
6354
8156
|
<td style="padding:5px 8px;color:var(--text-hi);font-family:var(--mono);font-size:11px">${esc((a.id||'—').toString().slice(0,12))}</td>
|
|
6355
|
-
<td style="padding:5px 8px;color:var(--text-hi)">${esc(a.type||a.title||'—')}</td>
|
|
6356
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapter||'—')}</td>
|
|
8157
|
+
<td style="padding:5px 8px;color:var(--text-hi)">${esc(a.adapterType||a.type||a.title||'—')}</td>
|
|
8158
|
+
<td style="padding:5px 8px;color:var(--text-lo)">${esc(a.adapterModel||a.adapter||'—')}</td>
|
|
6357
8159
|
<td style="padding:5px 8px"><span class="ss-pill ${(a.status==='running'||a.running)?'on':''}">${esc(a.status||'idle')}</span></td>
|
|
6358
8160
|
<td style="padding:5px 8px;color:var(--text-lo)">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
|
|
6359
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${a.tokensIn != null ? Number(a.tokensIn).toLocaleString() : '—'}</td>
|
|
6360
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${a.tokensOut != null ? Number(a.tokensOut).toLocaleString() : '—'}</td>
|
|
8161
|
+
<td style="padding:5px 8px;color:var(--text-lo)">${(a.tokensIn ?? a.tokens_in) != null ? Number(a.tokensIn ?? a.tokens_in).toLocaleString() : '—'}</td>
|
|
8162
|
+
<td style="padding:5px 8px;color:var(--text-lo)">${(a.tokensOut ?? a.tokens_out) != null ? Number(a.tokensOut ?? a.tokens_out).toLocaleString() : '—'}</td>
|
|
6361
8163
|
</tr>`).join('')}</tbody>
|
|
6362
8164
|
</table>`;
|
|
6363
8165
|
}
|
|
@@ -6366,10 +8168,12 @@ function v2RenderOrgAgentsFull() {
|
|
|
6366
8168
|
async function v2RenderOrgEnvironments() {
|
|
6367
8169
|
const el = document.getElementById('odt-environments');
|
|
6368
8170
|
if (!el || !_v2SelOrg) return;
|
|
8171
|
+
const _rendOrg = _v2SelOrg;
|
|
6369
8172
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6370
8173
|
try {
|
|
6371
|
-
const _enc = encodeURIComponent(
|
|
8174
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6372
8175
|
const data = await fetch(`/api/org/${_enc}/environments${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
8176
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6373
8177
|
const envs = Array.isArray(data) ? data : (data.environments || []);
|
|
6374
8178
|
if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured</div>'; return; }
|
|
6375
8179
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6423,7 +8227,7 @@ function v2RenderOrgIssuesFull() {
|
|
|
6423
8227
|
<td style="padding:5px 4px">${PRIORITY[i.priority]||'·'}</td>
|
|
6424
8228
|
<td style="padding:5px 8px;color:var(--text-hi);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((i.title||i.description||'—').slice(0,80))}</td>
|
|
6425
8229
|
<td style="padding:5px 8px;color:var(--text-lo)">${esc(i.assignee||'—')}</td>
|
|
6426
|
-
<td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'?'on':i.status==='blocked'?'warn':''}">${esc(i.status||'open')}</span></td>
|
|
8230
|
+
<td style="padding:5px 8px"><span class="ss-pill ${i.status==='done'||i.status==='closed'||i.status==='resolved'?'on':(i.status==='blocked'||i.status==='in_progress')?'warn':''}">${esc(i.status||'open')}</span></td>
|
|
6427
8231
|
<td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px">${relTime(i.updated_at||i.ts)}</td>
|
|
6428
8232
|
</tr>`).join('')}</tbody>
|
|
6429
8233
|
</table>`;
|
|
@@ -6433,10 +8237,12 @@ function v2RenderOrgIssuesFull() {
|
|
|
6433
8237
|
async function v2RenderOrgJoinRequests() {
|
|
6434
8238
|
const el = document.getElementById('odt-join-requests');
|
|
6435
8239
|
if (!el || !_v2SelOrg) return;
|
|
8240
|
+
const _rendOrg = _v2SelOrg;
|
|
6436
8241
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6437
8242
|
try {
|
|
6438
|
-
const _enc = encodeURIComponent(
|
|
8243
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6439
8244
|
const data = await fetch(`/api/org/${_enc}/join-requests${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
8245
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6440
8246
|
const reqs = Array.isArray(data) ? data : (data.joinRequests || data.join_requests || []);
|
|
6441
8247
|
if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests</div>'; return; }
|
|
6442
8248
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6457,10 +8263,12 @@ async function v2RenderOrgJoinRequests() {
|
|
|
6457
8263
|
async function v2RenderOrgThreads() {
|
|
6458
8264
|
const el = document.getElementById('odt-threads');
|
|
6459
8265
|
if (!el || !_v2SelOrg) return;
|
|
8266
|
+
const _rendOrg = _v2SelOrg;
|
|
6460
8267
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6461
8268
|
try {
|
|
6462
|
-
const _enc = encodeURIComponent(
|
|
8269
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6463
8270
|
const data = await fetch(`/api/org/${_enc}/threads${DIR?'?dir='+encodeURIComponent(DIR):''}`).then(r=>r.ok?r.json():[]).catch(()=>[]);
|
|
8271
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6464
8272
|
const threads = Array.isArray(data) ? data : (data.threads || []);
|
|
6465
8273
|
if (!threads.length) { el.innerHTML = '<div class="empty">No threads found</div>'; return; }
|
|
6466
8274
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6482,28 +8290,123 @@ async function v2RenderOrgThreads() {
|
|
|
6482
8290
|
window.v2StopOrg = async function() {
|
|
6483
8291
|
if (!_v2SelOrg) return;
|
|
6484
8292
|
const stopped = _v2SelOrg;
|
|
6485
|
-
try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop`, {method:'POST'}); } catch(_) {}
|
|
8293
|
+
try { await fetch(`/api/orgs/${encodeURIComponent(stopped)}/stop${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, {method:'POST'}); } catch(_) {}
|
|
6486
8294
|
setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
|
|
6487
8295
|
};
|
|
6488
8296
|
|
|
6489
|
-
window.
|
|
8297
|
+
window.v2DeleteOrg = async function() {
|
|
8298
|
+
if (!_v2SelOrg) return;
|
|
8299
|
+
const name = _v2SelOrg;
|
|
8300
|
+
if (!confirm(`Delete org "${name}"?\n\nThis removes the config and all associated data files. This cannot be undone.`)) return;
|
|
8301
|
+
const btn = document.getElementById('org-delete-btn');
|
|
8302
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Deleting…'; }
|
|
8303
|
+
try {
|
|
8304
|
+
const r = await fetch(`/api/orgs/${encodeURIComponent(name)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, { method: 'DELETE' });
|
|
8305
|
+
if (!r.ok) { const d = await r.json().catch(() => ({})); throw new Error(d.error || 'HTTP ' + r.status); }
|
|
8306
|
+
showToast('Deleted', `Org "${name}" deleted`, 'ok');
|
|
8307
|
+
_v2SelOrg = null;
|
|
8308
|
+
const inner = document.getElementById('org-detail-inner');
|
|
8309
|
+
if (inner) inner.style.display = 'none';
|
|
8310
|
+
const noSel = document.querySelector('.orgs-no-sel');
|
|
8311
|
+
if (noSel) noSel.style.display = '';
|
|
8312
|
+
await renderOrgs();
|
|
8313
|
+
} catch(e) {
|
|
8314
|
+
showToast('Error', String(e.message || e), 'err');
|
|
8315
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Delete'; }
|
|
8316
|
+
}
|
|
8317
|
+
};
|
|
8318
|
+
|
|
8319
|
+
window.v2ShowCopyOrgDialog = async function() {
|
|
6490
8320
|
const d = document.getElementById('org-copy-dialog');
|
|
6491
|
-
if (d)
|
|
8321
|
+
if (!d) return;
|
|
8322
|
+
d.style.display = 'flex';
|
|
8323
|
+
document.getElementById('org-copy-dest').value = '';
|
|
8324
|
+
document.getElementById('org-copy-status').textContent = '';
|
|
8325
|
+
document.getElementById('org-copy-confirm-btn').disabled = false;
|
|
8326
|
+
const listEl = document.getElementById('org-copy-proj-list');
|
|
8327
|
+
listEl.innerHTML = '<div style="padding:8px 10px;font-size:11px;color:var(--text-lo)">Loading projects…</div>';
|
|
8328
|
+
try {
|
|
8329
|
+
const data = await apiFetch('/api/projects');
|
|
8330
|
+
const projects = (data?.projects || []);
|
|
8331
|
+
if (!projects.length) { listEl.innerHTML = '<div style="padding:8px 10px;font-size:11px;color:var(--text-lo)">No projects found</div>'; }
|
|
8332
|
+
else {
|
|
8333
|
+
listEl.innerHTML = projects.map(p => {
|
|
8334
|
+
const label = p.name || p.slug || p.path?.split('/').pop() || p.path;
|
|
8335
|
+
const shortP = (p.path || '').replace(/\/Users\/[^/]+\//, '~/');
|
|
8336
|
+
const isCurrent = p.path === (window._orgDir || '');
|
|
8337
|
+
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">
|
|
8338
|
+
<span style="color:var(--text-hi)">${esc(label)}</span>
|
|
8339
|
+
<span style="color:var(--text-lo);font-size:10px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(shortP)}</span>
|
|
8340
|
+
</div>`;
|
|
8341
|
+
}).join('');
|
|
8342
|
+
}
|
|
8343
|
+
} catch(e) { listEl.innerHTML = '<div style="padding:8px;font-size:11px;color:var(--text-lo)">Could not load projects</div>'; }
|
|
8344
|
+
setTimeout(() => document.getElementById('org-copy-dest')?.focus(), 60);
|
|
6492
8345
|
};
|
|
6493
8346
|
window.v2DoCopyOrg = async function() {
|
|
6494
8347
|
const dest = document.getElementById('org-copy-dest')?.value.trim();
|
|
6495
|
-
|
|
8348
|
+
const statusEl = document.getElementById('org-copy-status');
|
|
8349
|
+
const confirmBtn = document.getElementById('org-copy-confirm-btn');
|
|
8350
|
+
if (!dest) { if (statusEl) { statusEl.textContent = 'Enter or select a destination'; statusEl.style.color = 'var(--red)'; } return; }
|
|
6496
8351
|
if (!_v2SelOrg) return;
|
|
8352
|
+
confirmBtn.disabled = true;
|
|
8353
|
+
if (statusEl) { statusEl.textContent = 'Copying…'; statusEl.style.color = 'var(--text-lo)'; }
|
|
6497
8354
|
try {
|
|
6498
|
-
const r = await fetch('/api/
|
|
8355
|
+
const r = await fetch('/api/orgs/' + enc(_v2SelOrg) + '/copy?dir=' + enc(DIR), {
|
|
6499
8356
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
6500
8357
|
body: JSON.stringify({ destination: dest }),
|
|
6501
8358
|
});
|
|
6502
|
-
|
|
6503
|
-
|
|
8359
|
+
const data = await r.json().catch(() => ({}));
|
|
8360
|
+
if (!r.ok) throw new Error(data.error || 'HTTP ' + r.status);
|
|
8361
|
+
showToast('Copied', `"${_v2SelOrg}" → ${dest}`, 'ok');
|
|
6504
8362
|
document.getElementById('org-copy-dialog').style.display = 'none';
|
|
6505
|
-
} catch (e) {
|
|
8363
|
+
} catch (e) {
|
|
8364
|
+
if (statusEl) { statusEl.textContent = e.message; statusEl.style.color = 'var(--red)'; }
|
|
8365
|
+
confirmBtn.disabled = false;
|
|
8366
|
+
}
|
|
8367
|
+
};
|
|
8368
|
+
window.v2ExportOrg = async function() {
|
|
8369
|
+
if (!_v2SelOrg) return;
|
|
8370
|
+
try {
|
|
8371
|
+
const r = await fetch('/api/orgs/' + encodeURIComponent(_v2SelOrg) + (DIR ? '?dir=' + encodeURIComponent(DIR) : ''));
|
|
8372
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
8373
|
+
const data = await r.json();
|
|
8374
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
8375
|
+
const url = URL.createObjectURL(blob);
|
|
8376
|
+
const a = document.createElement('a');
|
|
8377
|
+
a.href = url; a.download = _v2SelOrg + '.json'; a.click();
|
|
8378
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
8379
|
+
showToast('Exported', `${_v2SelOrg}.json downloaded`, 'ok');
|
|
8380
|
+
} catch(e) { showToast('Export failed', e.message, 'err'); }
|
|
8381
|
+
};
|
|
8382
|
+
window.v2ImportOrgTrigger = function() {
|
|
8383
|
+
let inp = document.getElementById('org-import-file');
|
|
8384
|
+
if (!inp) {
|
|
8385
|
+
inp = document.createElement('input');
|
|
8386
|
+
inp.type = 'file'; inp.id = 'org-import-file'; inp.accept = '.json'; inp.style.display = 'none';
|
|
8387
|
+
document.body.appendChild(inp);
|
|
8388
|
+
}
|
|
8389
|
+
inp.onchange = v2HandleImportFile;
|
|
8390
|
+
inp.click();
|
|
6506
8391
|
};
|
|
8392
|
+
async function v2HandleImportFile(e) {
|
|
8393
|
+
const file = e.target.files?.[0];
|
|
8394
|
+
e.target.value = '';
|
|
8395
|
+
if (!file) return;
|
|
8396
|
+
try {
|
|
8397
|
+
const text = await file.text();
|
|
8398
|
+
const data = JSON.parse(text);
|
|
8399
|
+
const name = data.name || file.name.replace(/\.json$/, '');
|
|
8400
|
+
const r = await fetch('/api/orgs', {
|
|
8401
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
8402
|
+
body: JSON.stringify({ ...data, name, dir: DIR || undefined }),
|
|
8403
|
+
});
|
|
8404
|
+
if (!r.ok) { const d = await r.json().catch(()=>({})); throw new Error(d.error || 'HTTP ' + r.status); }
|
|
8405
|
+
showToast('Imported', `Org "${name}" imported`, 'ok');
|
|
8406
|
+
await renderOrgs();
|
|
8407
|
+
if (name) v2SelectOrg(name);
|
|
8408
|
+
} catch(e) { showToast('Import failed', e.message, 'err'); }
|
|
8409
|
+
}
|
|
6507
8410
|
|
|
6508
8411
|
function filterLoopList(q) {
|
|
6509
8412
|
const query = q.trim().toLowerCase();
|
|
@@ -6525,13 +8428,23 @@ function filterOrgList(q) {
|
|
|
6525
8428
|
// live SSE for org events
|
|
6526
8429
|
(function v2OrgSSE() {
|
|
6527
8430
|
let src;
|
|
8431
|
+
let seenKeys = new Set();
|
|
8432
|
+
let reconnectTimer = null;
|
|
6528
8433
|
function connect() {
|
|
8434
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
6529
8435
|
if (src) src.close();
|
|
8436
|
+
// Do NOT reset seenKeys here — the server replays ~50 events on every reconnect and
|
|
8437
|
+
// clearing the set lets all replays through, duplicating _v2OrgEventLog and _activity entries.
|
|
6530
8438
|
src = new EventSource('/api/mastermind-stream');
|
|
6531
8439
|
src.onmessage = e => {
|
|
6532
8440
|
try {
|
|
6533
8441
|
const ev = JSON.parse(e.data);
|
|
6534
|
-
if (!ev?.org
|
|
8442
|
+
if (!ev?.org) return;
|
|
8443
|
+
if (!ev?.type?.startsWith('org:') && ev?.type !== 'run:start' && ev?.type !== 'run:complete' && ev?.type !== 'run:cycle:complete') return;
|
|
8444
|
+
const _sk = (ev.ts||'') + '|' + (ev.type||'') + '|' + (ev.org||'') + '|' + (ev.runId||ev.session||'') + '|' + (ev.from||ev.role||ev.agent||'') + '|' + (ev.result||ev.msg||ev.message||ev.summary||'').slice(0,20);
|
|
8445
|
+
if (seenKeys.has(_sk)) return;
|
|
8446
|
+
seenKeys.add(_sk);
|
|
8447
|
+
if (seenKeys.size > 2000) { const _a = [...seenKeys]; seenKeys = new Set(_a.slice(1000)); }
|
|
6535
8448
|
const n = ev.org;
|
|
6536
8449
|
// Filter by project dir if the event carries one; skip events from other projects.
|
|
6537
8450
|
// Events without a project field are treated as belonging to the current project.
|
|
@@ -6541,7 +8454,63 @@ function filterOrgList(q) {
|
|
|
6541
8454
|
_v2OrgEventLog[n].push(ev);
|
|
6542
8455
|
if (_v2OrgEventLog[n].length > 50) _v2OrgEventLog[n].shift();
|
|
6543
8456
|
if (n === _v2SelOrg && _v2OrgTab === 'activity') v2RenderOrgActivity();
|
|
6544
|
-
|
|
8457
|
+
// Update in-memory agent status so Live tab running-agents section reflects reality.
|
|
8458
|
+
// org:agent:online → mark that agent running; org:complete/run:complete → reset all to idle.
|
|
8459
|
+
if (n === _v2SelOrg && _v2OrgData && Array.isArray(_v2OrgData._agents)) {
|
|
8460
|
+
if (ev.type === 'org:agent:online' && ev.role) {
|
|
8461
|
+
const _liveAgent = _v2OrgData._agents.find(a => a.id === ev.role);
|
|
8462
|
+
if (_liveAgent) { _liveAgent.status = 'running'; _liveAgent.title = _liveAgent.title || ev.title || ev.role; }
|
|
8463
|
+
else _v2OrgData._agents.push({ id: ev.role, title: ev.title || ev.role, adapterType: ev.agent_type || null, status: 'running' });
|
|
8464
|
+
} else if (ev.type === 'org:agent:offline' && ev.role) {
|
|
8465
|
+
const _offAgent = _v2OrgData._agents.find(a => a.id === ev.role);
|
|
8466
|
+
if (_offAgent) _offAgent.status = 'idle';
|
|
8467
|
+
} else if (ev.type === 'org:complete' || ev.type === 'run:complete') {
|
|
8468
|
+
_v2OrgData._agents.forEach(a => { a.status = 'idle'; });
|
|
8469
|
+
}
|
|
8470
|
+
if (_v2OrgTab === 'agents-full' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgAgentsFull();
|
|
8471
|
+
else if (_v2OrgTab === 'live' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgLive();
|
|
8472
|
+
else if (_v2OrgTab === 'heartbeats' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgHeartbeats();
|
|
8473
|
+
else if (_v2OrgTab === 'members' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgMembers();
|
|
8474
|
+
else if (_v2OrgTab === 'roles' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2RenderOrgRoles();
|
|
8475
|
+
else if (_v2OrgTab === 'chart' && (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete')) v2UpdateChartRunningDots();
|
|
8476
|
+
// Health tab: update KPI counters live from SSE events so agents_active and
|
|
8477
|
+
// tasks_pending reflect current state without waiting for a run-event re-fetch.
|
|
8478
|
+
if (_v2OrgTab === 'health' && _v2OrgData._health) {
|
|
8479
|
+
if (ev.type === 'org:agent:online' || ev.type === 'org:agent:offline' || ev.type === 'org:complete' || ev.type === 'run:complete') {
|
|
8480
|
+
_v2OrgData._health.agents_active = _v2OrgData._agents.filter(a => a.status === 'running').length;
|
|
8481
|
+
v2RenderOrgHealth();
|
|
8482
|
+
}
|
|
8483
|
+
if (ev.type === 'org:checkpoint' && ev.pending_tasks != null) {
|
|
8484
|
+
_v2OrgData._health.tasks_pending = ev.pending_tasks;
|
|
8485
|
+
v2RenderOrgHealth();
|
|
8486
|
+
}
|
|
8487
|
+
if (ev.type === 'run:cycle:complete' && ev.pending != null) {
|
|
8488
|
+
_v2OrgData._health.tasks_pending = ev.pending;
|
|
8489
|
+
v2RenderOrgHealth();
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
}
|
|
8493
|
+
// Also push live org:comms / org:checkpoint events directly into the Live tab
|
|
8494
|
+
// activity feed so they appear in real-time without waiting for the 5s poll.
|
|
8495
|
+
if (n === _v2SelOrg && _v2OrgTab === 'live' && _v2OrgData) {
|
|
8496
|
+
if (!Array.isArray(_v2OrgData._activity)) _v2OrgData._activity = [];
|
|
8497
|
+
_v2OrgData._activity.push({ ...ev, _sseOnly: true });
|
|
8498
|
+
v2RenderOrgLive();
|
|
8499
|
+
}
|
|
8500
|
+
// Pulse the org list dot on comms/checkpoint events so the user can see the org is active
|
|
8501
|
+
// without switching to the activity tab
|
|
8502
|
+
if (ev.type === 'org:comms' || ev.type === 'org:checkpoint') {
|
|
8503
|
+
const _orgDot = document.querySelector(`.org-item[data-org="${CSS.escape(n)}"] .oi-dot`);
|
|
8504
|
+
if (_orgDot) {
|
|
8505
|
+
_orgDot.classList.add('running');
|
|
8506
|
+
// Brief flash to indicate activity — class stays if org is running
|
|
8507
|
+
setTimeout(() => {
|
|
8508
|
+
const _listOrg = _v2Orgs.find(o => o.name === n) || {};
|
|
8509
|
+
if (!_listOrg.running) _orgDot.classList.remove('running');
|
|
8510
|
+
}, 2000);
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
if (ev.type === 'org:start' || ev.type === 'org:stop' || ev.type === 'org:delete' || ev.type === 'org:create' || ev.type === 'run:start' || ev.type === 'run:complete') {
|
|
6545
8514
|
setTimeout(async () => {
|
|
6546
8515
|
await renderOrgs();
|
|
6547
8516
|
if (_v2SelOrg === n) {
|
|
@@ -6556,14 +8525,102 @@ function filterOrgList(q) {
|
|
|
6556
8525
|
const listOrg = _v2Orgs.find(o => o.name === _v2SelOrg) || {};
|
|
6557
8526
|
if (_v2OrgData) _v2OrgData.running = listOrg.running;
|
|
6558
8527
|
v2UpdateOrgHeader(listOrg, _v2OrgData);
|
|
6559
|
-
if (_v2OrgTab === 'health')
|
|
8528
|
+
if (_v2OrgTab === 'health') {
|
|
8529
|
+
// Re-fetch health after run events so stats (agents_active, success_rate, total_runs) are current
|
|
8530
|
+
const _hEnc = encodeURIComponent(_v2SelOrg);
|
|
8531
|
+
fetch(`/api/org/${_hEnc}/health${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8532
|
+
.then(r => r.ok ? r.json() : null)
|
|
8533
|
+
.then(h => { if (h && _v2OrgData && _v2SelOrg === n) { _v2OrgData._health = h; v2RenderOrgHealth(); } })
|
|
8534
|
+
.catch(() => { v2RenderOrgHealth(); });
|
|
8535
|
+
}
|
|
8536
|
+
if (_v2OrgTab === 'tasks') {
|
|
8537
|
+
// Re-fetch org data after run events so tasks (todo/doing/done) reflect current store state
|
|
8538
|
+
const _tEnc = encodeURIComponent(_v2SelOrg);
|
|
8539
|
+
fetch(`/api/org/${_tEnc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8540
|
+
.then(r => r.ok ? r.json() : null)
|
|
8541
|
+
.then(d => { if (d && _v2OrgData && _v2SelOrg === n && d.tasks !== undefined) { _v2OrgData.tasks = d.tasks; v2RenderOrgTasks(); } })
|
|
8542
|
+
.catch(() => { v2RenderOrgTasks(); });
|
|
8543
|
+
}
|
|
8544
|
+
if (_v2OrgTab === 'budgets' || _v2OrgTab === 'costs') {
|
|
8545
|
+
// Re-fetch budgets after run events so token/cost counters stay current
|
|
8546
|
+
const _bEnc = encodeURIComponent(_v2SelOrg);
|
|
8547
|
+
fetch(`/api/org/${_bEnc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8548
|
+
.then(r => r.ok ? r.json() : null)
|
|
8549
|
+
.then(b => { if (b && _v2OrgData && _v2SelOrg === n) { _v2OrgData._budgets = b; _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); } })
|
|
8550
|
+
.catch(() => { _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); });
|
|
8551
|
+
}
|
|
8552
|
+
if (_v2OrgTab === 'board' || _v2OrgTab === 'issues-full') {
|
|
8553
|
+
// Re-fetch issues after run events so board/issues-full reflect current state
|
|
8554
|
+
const _iEnc = encodeURIComponent(_v2SelOrg);
|
|
8555
|
+
fetch(`/api/org/${_iEnc}/issues${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8556
|
+
.then(r => r.ok ? r.json() : null)
|
|
8557
|
+
.then(d => { if (d && _v2OrgData && _v2SelOrg === n) { _v2OrgData._issues = Array.isArray(d) ? d : (d?.issues || []); _v2OrgTab === 'board' ? v2RenderOrgBoard() : v2RenderOrgIssuesFull(); } })
|
|
8558
|
+
.catch(() => { _v2OrgTab === 'board' ? v2RenderOrgBoard() : v2RenderOrgIssuesFull(); });
|
|
8559
|
+
}
|
|
8560
|
+
if (_v2OrgTab === 'goals') {
|
|
8561
|
+
// Re-fetch org data after run events so goal progress (filled/total/status) reflects current state
|
|
8562
|
+
const _glEnc = encodeURIComponent(_v2SelOrg);
|
|
8563
|
+
fetch(`/api/org/${_glEnc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8564
|
+
.then(r => r.ok ? r.json() : null)
|
|
8565
|
+
.then(d => { if (d && _v2OrgData && _v2SelOrg === n && d.goals !== undefined) { _v2OrgData.goals = d.goals; v2RenderOrgGoals(); } })
|
|
8566
|
+
.catch(() => { v2RenderOrgGoals(); });
|
|
8567
|
+
}
|
|
8568
|
+
if (_v2OrgTab === 'chat') {
|
|
8569
|
+
// Re-load chat run list after run events so cycleCount/eventCount in dropdown reflect disk state
|
|
8570
|
+
_odtLoadChatSessions().catch(() => {});
|
|
8571
|
+
}
|
|
8572
|
+
if (_v2OrgTab === 'agents-full' || _v2OrgTab === 'heartbeats') {
|
|
8573
|
+
// Re-fetch agents after run events to get fresh tokensIn/tokensOut/lastHeartbeat
|
|
8574
|
+
// Heartbeats tab derives from _v2OrgData._agents so shares this re-fetch.
|
|
8575
|
+
const _agEnc = encodeURIComponent(_v2SelOrg);
|
|
8576
|
+
fetch(`/api/org/${_agEnc}/agents${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8577
|
+
.then(r => r.ok ? r.json() : null)
|
|
8578
|
+
.then(d => { if (d && _v2OrgData && _v2SelOrg === n) { _v2OrgData._agents = Array.isArray(d) ? d : (d?.agents || []); _v2OrgTab === 'agents-full' ? v2RenderOrgAgentsFull() : v2RenderOrgHeartbeats(); } })
|
|
8579
|
+
.catch(() => { _v2OrgTab === 'agents-full' ? v2RenderOrgAgentsFull() : v2RenderOrgHeartbeats(); });
|
|
8580
|
+
}
|
|
8581
|
+
if (_v2OrgTab === 'myissues') {
|
|
8582
|
+
// Re-render myissues after run events — it fetches its own data on every render
|
|
8583
|
+
v2RenderOrgMyIssues();
|
|
8584
|
+
}
|
|
8585
|
+
if (_v2OrgTab === 'charts') {
|
|
8586
|
+
// Re-fetch activity after run events so the 14-day heatmap reflects the completed run
|
|
8587
|
+
const _chEnc = encodeURIComponent(_v2SelOrg);
|
|
8588
|
+
fetch(`/api/org/${_chEnc}/activity${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8589
|
+
.then(r => r.ok ? r.json() : [])
|
|
8590
|
+
.then(d => { if (_v2OrgData && _v2SelOrg === n) { _v2OrgData._activity = Array.isArray(d) ? d : []; v2RenderOrgCharts(); } })
|
|
8591
|
+
.catch(() => { v2RenderOrgCharts(); });
|
|
8592
|
+
}
|
|
6560
8593
|
}
|
|
6561
8594
|
}
|
|
6562
8595
|
}, 400);
|
|
6563
8596
|
}
|
|
8597
|
+
// run:cycle:complete: lightweight re-fetch for budgets/costs/tasks without renderOrgs()
|
|
8598
|
+
// — cycles fire frequently and only affect token counters and task state, not org-list status.
|
|
8599
|
+
if (ev.type === 'run:cycle:complete' && _v2SelOrg === n && _v2OrgData) {
|
|
8600
|
+
if (_v2OrgTab === 'budgets' || _v2OrgTab === 'costs') {
|
|
8601
|
+
const _bcEnc = encodeURIComponent(_v2SelOrg);
|
|
8602
|
+
fetch(`/api/org/${_bcEnc}/budgets${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8603
|
+
.then(r => r.ok ? r.json() : null)
|
|
8604
|
+
.then(b => { if (b && _v2OrgData && _v2SelOrg === n) { _v2OrgData._budgets = b; _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); } })
|
|
8605
|
+
.catch(() => { _v2OrgTab === 'budgets' ? v2RenderOrgBudgets() : v2RenderOrgCosts(); });
|
|
8606
|
+
}
|
|
8607
|
+
if (_v2OrgTab === 'tasks') {
|
|
8608
|
+
const _tcEnc = encodeURIComponent(_v2SelOrg);
|
|
8609
|
+
fetch(`/api/org/${_tcEnc}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`)
|
|
8610
|
+
.then(r => r.ok ? r.json() : null)
|
|
8611
|
+
.then(d => { if (d && _v2OrgData && _v2SelOrg === n && d.tasks !== undefined) { _v2OrgData.tasks = d.tasks; v2RenderOrgTasks(); } })
|
|
8612
|
+
.catch(() => { v2RenderOrgTasks(); });
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
6564
8615
|
} catch(_) {}
|
|
6565
8616
|
};
|
|
6566
|
-
src.onerror = () =>
|
|
8617
|
+
src.onerror = () => {
|
|
8618
|
+
// Close immediately to stop browser's built-in auto-retry (zombie window).
|
|
8619
|
+
// Guard reconnectTimer so multiple onerror firings don't stack timers that
|
|
8620
|
+
// would then cancel each other's freshly-created connections.
|
|
8621
|
+
const _s = src; src = null; if (_s) _s.close();
|
|
8622
|
+
if (!reconnectTimer) reconnectTimer = setTimeout(connect, 5000);
|
|
8623
|
+
};
|
|
6567
8624
|
}
|
|
6568
8625
|
connect();
|
|
6569
8626
|
})();
|
|
@@ -9580,8 +11637,8 @@ function selectMem(filename) {
|
|
|
9580
11637
|
const actions = (f.readonly || f.source === 'backend')
|
|
9581
11638
|
? '<div class="mem-actions"><span style="font-size:11px;color:var(--text-lo)">Read-only — stored in the backend memory store (not a file)</span></div>'
|
|
9582
11639
|
: '<div class="mem-actions">' +
|
|
9583
|
-
'<button class="btn"
|
|
9584
|
-
'<button class="btn" style="color:var(--red);border-color:var(--red)"
|
|
11640
|
+
'<button class="btn" data-fn="' + esc(filename) + '" onclick="openEditMemModal(this.dataset.fn)">✎ Edit</button>' +
|
|
11641
|
+
'<button class="btn" style="color:var(--red);border-color:var(--red)" data-fn="' + esc(filename) + '" onclick="deleteMem(this.dataset.fn)">✕ Delete</button>' +
|
|
9585
11642
|
'</div>';
|
|
9586
11643
|
detail.innerHTML =
|
|
9587
11644
|
'<span class="mem-badge" style="background:' + col + '22;color:' + col + '">' + esc(f.type || '?') + '</span>' + srcBadge +
|
|
@@ -10135,7 +12192,7 @@ function mmRenderOrgs(body) {
|
|
|
10135
12192
|
if (!orgs.length) { body.innerHTML = '<div class="empty">No orgs found. Run /mastermind:createorg to create one.</div>'; return; }
|
|
10136
12193
|
body.innerHTML = orgs.map(o => {
|
|
10137
12194
|
const running = o.running;
|
|
10138
|
-
return `<div class="mm-skill-item" onclick="closeMastermind();v2SelectOrg(
|
|
12195
|
+
return `<div class="mm-skill-item" data-org="${esc(o.name)}" onclick="closeMastermind();v2SelectOrg(this.dataset.org);switchView('orgs')">
|
|
10139
12196
|
<span class="mm-skill-name">${esc(o.name)}</span>
|
|
10140
12197
|
<span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${running ? '⬤ LIVE' : ''}</span>
|
|
10141
12198
|
</div>`;
|
|
@@ -10147,7 +12204,7 @@ function mmRenderSkills(body) {
|
|
|
10147
12204
|
const q = _mmSkillFilter.toLowerCase();
|
|
10148
12205
|
const filtered = q ? _MM_SKILLS_CATALOG.filter(s => s.name.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : _MM_SKILLS_CATALOG;
|
|
10149
12206
|
body.innerHTML = `<div class="filter-bar" style="margin-bottom:12px"><input class="filter-input" type="text" placeholder="Search skills…" value="${esc(_mmSkillFilter)}" oninput="_mmSkillFilter=this.value;mmRenderSkills(document.getElementById('mm-body'))"></div>` +
|
|
10150
|
-
filtered.map(s => `<div class="mm-skill-item" onclick="navigator.clipboard.writeText(
|
|
12207
|
+
filtered.map(s => `<div class="mm-skill-item" data-sn="${esc(s.name)}" onclick="navigator.clipboard.writeText(this.dataset.sn).then(()=>showToast('Copied',this.dataset.sn,'ok'))">
|
|
10151
12208
|
<span class="mm-skill-name">${esc(s.name)}</span>
|
|
10152
12209
|
<span class="mm-skill-desc">${esc(s.desc)}</span>
|
|
10153
12210
|
</div>`).join('');
|
|
@@ -10162,7 +12219,7 @@ async function mmRenderLoops(body) {
|
|
|
10162
12219
|
body.innerHTML = loops.map(l => {
|
|
10163
12220
|
const maxReps = l.maxReps || 0;
|
|
10164
12221
|
const curRep = l.currentRep || 0;
|
|
10165
|
-
const isTillend = !maxReps || l.
|
|
12222
|
+
const isTillend = !maxReps || l.type === 'tillend' || String(l.command || '').includes('--tillend');
|
|
10166
12223
|
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
|
|
10167
12224
|
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
10168
12225
|
const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
|
|
@@ -10180,7 +12237,7 @@ async function mmRenderLoops(body) {
|
|
|
10180
12237
|
<span style="font-size:13px;color:var(--text-hi);flex:1">${esc(name)}</span>
|
|
10181
12238
|
${isHil ? '<span style="color:oklch(78% 0.18 80);font-size:10px">⚠ HIL</span>' : ''}
|
|
10182
12239
|
<span class="ss-pill ${running ? 'on' : ''}">${running ? 'active' : isFinished ? 'done' : 'stopped'}</span>
|
|
10183
|
-
${(running || isHil) ? `<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red)"
|
|
12240
|
+
${(running || isHil) ? `<button class="btn" style="font-size:10px;color:var(--red);border-color:var(--red)" data-lid="${esc(l.id||l.name||'')}" onclick="stopLoop(event,this.dataset.lid);mmSwitchTab('loops')">■ Stop</button>` : ''}
|
|
10184
12241
|
</div>
|
|
10185
12242
|
<div style="font-size:11px;color:var(--text-lo);margin-top:4px;font-family:var(--mono)">${esc(fmtInterval(l.interval||l.schedule||''))} ${cdown ? '· ' + cdown : ''} ${curRep != null ? '· run ' + curRep + (isTillend ? '/∞' : maxReps ? '/'+maxReps : '') : ''}</div>
|
|
10186
12243
|
</div>`;
|