@monoes/monomindcli 1.13.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 +11 -11
- package/.claude/helpers/hook-handler.cjs +17 -1
- package/.claude/helpers/session.cjs +20 -2
- 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/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/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/ui/dashboard.html +1639 -249
- package/dist/src/ui/orgs.html +1 -0
- package/dist/src/ui/server.mjs +389 -132
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -302,10 +302,14 @@ 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; }
|
|
@@ -325,11 +329,19 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
325
329
|
#odt-chat-agent-lbl { font-size:8px; letter-spacing:2px; color:var(--text-xs); flex-shrink:0; }
|
|
326
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; }
|
|
327
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; }
|
|
328
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); }
|
|
329
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; }
|
|
330
336
|
#odt-chat-feed::-webkit-scrollbar { width:4px; }
|
|
331
337
|
#odt-chat-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
|
|
332
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; }
|
|
333
345
|
|
|
334
346
|
/* agent detail drawer (slides in from right within the org detail pane) */
|
|
335
347
|
#org-agent-drawer {
|
|
@@ -711,6 +723,18 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
711
723
|
#chat-v-feed::-webkit-scrollbar { width:4px; }
|
|
712
724
|
#chat-v-feed::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
|
|
713
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); }
|
|
714
738
|
.cv-msg { display:flex; flex-direction:column; max-width:90%; }
|
|
715
739
|
.cv-msg.cv-sys { align-self:center; max-width:100%; }
|
|
716
740
|
.cv-msg.cv-agent { align-self:flex-start; }
|
|
@@ -1814,78 +1838,53 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
1814
1838
|
<span class="odh-pill" id="odh-topo">—</span>
|
|
1815
1839
|
<span class="odh-pill" id="odh-roles">0 roles</span>
|
|
1816
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>
|
|
1817
1843
|
<button class="btn" id="org-copy-btn" onclick="v2ShowCopyOrgDialog()" style="color:var(--accent);border-color:var(--accent)">Copy to…</button>
|
|
1818
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>
|
|
1819
1846
|
</div>
|
|
1820
1847
|
</div>
|
|
1821
1848
|
<div id="org-detail-tabs">
|
|
1822
1849
|
<button class="odt-btn active" data-tab="chart" onclick="v2SwitchOrgTab('chart')">Chart</button>
|
|
1823
|
-
<button class="odt-btn" data-tab="activity" onclick="v2SwitchOrgTab('activity')">Activity</button>
|
|
1824
|
-
<button class="odt-btn" data-tab="health" onclick="v2SwitchOrgTab('health')">Health</button>
|
|
1825
|
-
<button class="odt-btn" data-tab="approvals" onclick="v2SwitchOrgTab('approvals')">Approvals</button>
|
|
1826
|
-
<button class="odt-btn" data-tab="budgets" onclick="v2SwitchOrgTab('budgets')">Budgets</button>
|
|
1827
|
-
<button class="odt-btn" data-tab="charts" onclick="v2SwitchOrgTab('charts')">Charts</button>
|
|
1828
|
-
<button class="odt-btn" data-tab="skills" onclick="v2SwitchOrgTab('skills')">Skills</button>
|
|
1829
1850
|
<button class="odt-btn" data-tab="chat" onclick="v2SwitchOrgTab('chat')">Chat</button>
|
|
1830
|
-
<button class="odt-btn" data-tab="
|
|
1831
|
-
<button class="odt-btn" data-tab="
|
|
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>
|
|
1832
1859
|
</div>
|
|
1833
1860
|
<div id="org-detail-body">
|
|
1834
1861
|
<div class="odt-pane active" id="odt-chart"></div>
|
|
1835
|
-
<div class="odt-pane" id="odt-roles"></div>
|
|
1836
|
-
<div class="odt-pane" id="odt-activity"></div>
|
|
1837
|
-
<div class="odt-pane" id="odt-health"></div>
|
|
1838
|
-
<div class="odt-pane" id="odt-heartbeats"></div>
|
|
1839
|
-
<div class="odt-pane" id="odt-tasks"></div>
|
|
1840
|
-
<div class="odt-pane" id="odt-costs"></div>
|
|
1841
|
-
<div class="odt-pane" id="odt-members"></div>
|
|
1842
|
-
<div class="odt-pane" id="odt-goals"></div>
|
|
1843
|
-
<div class="odt-pane" id="odt-board"></div>
|
|
1844
|
-
<div class="odt-pane" id="odt-live"></div>
|
|
1845
|
-
<div class="odt-pane" id="odt-approvals"></div>
|
|
1846
|
-
<div class="odt-pane" id="odt-secrets"></div>
|
|
1847
|
-
<div class="odt-pane" id="odt-settings"></div>
|
|
1848
|
-
<div class="odt-pane" id="odt-routines"></div>
|
|
1849
|
-
<div class="odt-pane" id="odt-myissues"></div>
|
|
1850
|
-
<div class="odt-pane" id="odt-budgets"></div>
|
|
1851
|
-
<div class="odt-pane" id="odt-plugins"></div>
|
|
1852
|
-
<div class="odt-pane" id="odt-charts"></div>
|
|
1853
|
-
<div class="odt-pane" id="odt-projects"></div>
|
|
1854
|
-
<div class="odt-pane" id="odt-skills"></div>
|
|
1855
|
-
<div class="odt-pane" id="odt-workspaces"></div>
|
|
1856
|
-
<div class="odt-pane" id="odt-invites"></div>
|
|
1857
1862
|
<div class="odt-pane" id="odt-agents-full"></div>
|
|
1863
|
+
<div class="odt-pane" id="odt-activity"></div>
|
|
1858
1864
|
<div class="odt-pane" id="odt-chat" style="flex-direction:column;height:100%;overflow:hidden;"></div>
|
|
1865
|
+
<div class="odt-pane" id="odt-skills"></div>
|
|
1866
|
+
<div class="odt-pane" id="odt-costs"></div>
|
|
1859
1867
|
<div class="odt-pane" id="odt-config" style="overflow-y:auto"></div>
|
|
1860
1868
|
<div class="odt-pane" id="odt-files" style="overflow-y:auto"></div>
|
|
1861
|
-
<div class="odt-pane" id="odt-environments"></div>
|
|
1862
|
-
<div class="odt-pane" id="odt-access"></div>
|
|
1863
|
-
<div class="odt-pane" id="odt-issues-full"></div>
|
|
1864
|
-
<div class="odt-pane" id="odt-join-requests"></div>
|
|
1865
|
-
<div class="odt-pane" id="odt-threads"></div>
|
|
1866
1869
|
</div>
|
|
1867
1870
|
</div>
|
|
1868
|
-
<div id="org-copy-dialog" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,0.
|
|
1869
|
-
<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">
|
|
1870
1873
|
<div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
|
|
1871
|
-
<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>
|
|
1872
1881
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
1873
1882
|
<button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
|
|
1874
|
-
<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>
|
|
1875
1884
|
</div>
|
|
1876
1885
|
</div>
|
|
1877
1886
|
</div>
|
|
1878
|
-
|
|
1879
|
-
<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">
|
|
1880
|
-
<div style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;width:320px;display:flex;flex-direction:column;gap:14px">
|
|
1881
|
-
<div style="font-size:13px;font-weight:600;color:var(--text-hi)">Copy org to project</div>
|
|
1882
|
-
<input id="org-copy-dest" class="filter-input" placeholder="Destination project path…" style="width:100%">
|
|
1883
|
-
<div style="display:flex;gap:8px;justify-content:flex-end">
|
|
1884
|
-
<button class="btn" onclick="document.getElementById('org-copy-dialog').style.display='none'">Cancel</button>
|
|
1885
|
-
<button class="btn" onclick="v2DoCopyOrg()" style="color:var(--accent);border-color:var(--accent)">Copy</button>
|
|
1886
|
-
</div>
|
|
1887
|
-
</div>
|
|
1888
|
-
</div>
|
|
1887
|
+
<div id="org-import-input-wrap" style="display:none"><input type="file" id="org-import-file" accept=".json" style="display:none"></div>
|
|
1889
1888
|
<div id="org-agent-drawer" aria-hidden="true">
|
|
1890
1889
|
<div id="oad-head"></div>
|
|
1891
1890
|
<div id="oad-body"></div>
|
|
@@ -2124,9 +2123,13 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
|
|
|
2124
2123
|
<select id="chat-v-sel" onchange="chatVSelectSession(this.value)">
|
|
2125
2124
|
<option value="">— select a session —</option>
|
|
2126
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>
|
|
2127
2129
|
<div id="chat-v-live-dot"></div>
|
|
2128
2130
|
<span id="chat-v-live-lbl">OFFLINE</span>
|
|
2129
2131
|
</div>
|
|
2132
|
+
<div id="chat-v-excerpt" class="cv-excerpt-banner"></div>
|
|
2130
2133
|
<div id="chat-v-feed">
|
|
2131
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>
|
|
2132
2135
|
</div>
|
|
@@ -2265,7 +2268,7 @@ document.getElementById('feed-scroll').addEventListener('scroll', () => {
|
|
|
2265
2268
|
userScrolled = document.getElementById('feed-scroll').scrollTop > 50;
|
|
2266
2269
|
});
|
|
2267
2270
|
|
|
2268
|
-
function switchView(v) {
|
|
2271
|
+
function switchView(v, { updateHash = true } = {}) {
|
|
2269
2272
|
currentView = v;
|
|
2270
2273
|
document.querySelectorAll('.nav-item[data-view]').forEach(el =>
|
|
2271
2274
|
el.classList.toggle('active', el.dataset.view === v));
|
|
@@ -2276,12 +2279,55 @@ function switchView(v) {
|
|
|
2276
2279
|
const PROJECT = DIR ? shortPath(DIR) : 'monomind';
|
|
2277
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' };
|
|
2278
2281
|
document.title = `monomind · ${PROJECT} · ${VIEW_LABELS[v] || v}`;
|
|
2282
|
+
if (updateHash) _pushHash();
|
|
2279
2283
|
// Projects always re-fetches so onclick paths in cards stay current
|
|
2280
2284
|
if (v === 'projects') { renderProjects(); return; }
|
|
2281
2285
|
if (v === 'chat') { initChatView(); return; }
|
|
2282
2286
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
2283
2287
|
}
|
|
2284
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
|
+
|
|
2285
2331
|
function renderView(v) {
|
|
2286
2332
|
if (v === 'now') { refreshNow(); return; }
|
|
2287
2333
|
if (v === 'projects') renderProjects();
|
|
@@ -2351,6 +2397,9 @@ async function init() {
|
|
|
2351
2397
|
await refreshNow();
|
|
2352
2398
|
if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
|
|
2353
2399
|
initSSE();
|
|
2400
|
+
// Restore view from URL hash (after initial data load so orgs list is ready)
|
|
2401
|
+
_restoreFromHash();
|
|
2402
|
+
window.addEventListener('hashchange', () => _restoreFromHash());
|
|
2354
2403
|
}
|
|
2355
2404
|
|
|
2356
2405
|
function _setLiveMode(mode) {
|
|
@@ -3918,42 +3967,190 @@ async function renderGlobalFeed() {
|
|
|
3918
3967
|
}
|
|
3919
3968
|
|
|
3920
3969
|
// ── agent chat view ────────────────────────────────────────
|
|
3921
|
-
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)
|
|
3922
3973
|
let chatVCurrentId = null;
|
|
3974
|
+
let chatVCurrentGroupSessions = new Set(); // session IDs belonging to the currently selected group
|
|
3923
3975
|
let chatVSseSource = null;
|
|
3976
|
+
let chatVSeenKeys = new Set();
|
|
3924
3977
|
|
|
3925
3978
|
function initChatView() {
|
|
3926
3979
|
loadChatViewSessions();
|
|
3980
|
+
loadChatViewOrgRuns();
|
|
3927
3981
|
if (!chatVSseSource) connectChatViewSSE();
|
|
3928
3982
|
}
|
|
3929
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
|
+
|
|
3930
4041
|
async function loadChatViewSessions() {
|
|
3931
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 };
|
|
3932
4047
|
const data = await apiFetch('/api/mastermind/sessions');
|
|
3933
4048
|
chatVSessions = {};
|
|
4049
|
+
chatVGroupMap = {};
|
|
4050
|
+
chatVSessionGroupMap = {};
|
|
3934
4051
|
const sel = document.getElementById('chat-v-sel');
|
|
3935
4052
|
const prev = sel.value;
|
|
3936
4053
|
while (sel.options.length > 1) sel.remove(1);
|
|
3937
|
-
|
|
3938
|
-
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
|
|
3939
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
|
+
});
|
|
3940
4113
|
const opt = document.createElement('option');
|
|
3941
|
-
opt.value =
|
|
3942
|
-
const ts =
|
|
3943
|
-
|
|
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' ? ' ●' : '');
|
|
3944
4121
|
sel.appendChild(opt);
|
|
3945
|
-
chatVSessions[s.id] = s;
|
|
3946
4122
|
});
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
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); }
|
|
3951
4147
|
}
|
|
3952
4148
|
} catch(e) { console.warn('chat sessions load failed', e); }
|
|
3953
4149
|
}
|
|
3954
4150
|
|
|
3955
4151
|
function chatVSelectSession(id) {
|
|
3956
4152
|
chatVCurrentId = id;
|
|
4153
|
+
chatVCurrentGroupSessions.clear();
|
|
3957
4154
|
const feed = document.getElementById('chat-v-feed');
|
|
3958
4155
|
const empty = document.getElementById('chat-v-empty');
|
|
3959
4156
|
if (!id || !chatVSessions[id]) {
|
|
@@ -3963,24 +4160,118 @@ function chatVSelectSession(id) {
|
|
|
3963
4160
|
}
|
|
3964
4161
|
feed.innerHTML = '';
|
|
3965
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);
|
|
3966
4209
|
const events = session.events || [];
|
|
3967
|
-
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 = ''; }
|
|
3968
4219
|
feed.scrollTop = feed.scrollHeight;
|
|
3969
4220
|
}
|
|
3970
4221
|
|
|
3971
4222
|
function appendChatViewEvent(ev, animate) {
|
|
3972
|
-
if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId) return;
|
|
4223
|
+
if (chatVCurrentId && ev.session && ev.session !== chatVCurrentId && !chatVCurrentGroupSessions.has(ev.session)) return;
|
|
3973
4224
|
const feed = document.getElementById('chat-v-feed');
|
|
3974
4225
|
if (!feed) return;
|
|
3975
4226
|
const empty = document.getElementById('chat-v-empty');
|
|
3976
4227
|
if (empty && feed.contains(empty)) feed.removeChild(empty);
|
|
3977
4228
|
|
|
3978
4229
|
let el;
|
|
4230
|
+
const atBottom = !animate || (feed.scrollHeight - feed.scrollTop - feed.clientHeight < 80);
|
|
3979
4231
|
const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
3980
|
-
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') {
|
|
3981
4237
|
el = mkCVIntercom(ev.from, ev.to, ev.msg || '', ts);
|
|
3982
|
-
} else if (ev.type === '
|
|
3983
|
-
|
|
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);
|
|
3984
4275
|
} else if (ev.type === 'session:start') {
|
|
3985
4276
|
el = mkCVSys('Session started' + (ev.prompt ? ': ' + esc(ev.prompt.slice(0,80)) : ''), ts);
|
|
3986
4277
|
} else if (ev.type === 'session:complete') {
|
|
@@ -3990,38 +4281,64 @@ function appendChatViewEvent(ev, animate) {
|
|
|
3990
4281
|
} else if (ev.type === 'domain:complete') {
|
|
3991
4282
|
el = mkCVSys('✓ ' + esc(ev.domain || '') + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
|
|
3992
4283
|
} else if (ev.type === 'loop:start') {
|
|
3993
|
-
el = mkCVSys('Loop
|
|
4284
|
+
el = mkCVSys('◎ Loop: ' + esc(ev.command || '') + (ev.repeat ? ' ×' + esc(String(ev.repeat)) : ''), ts);
|
|
3994
4285
|
if (currentView === 'loops') renderLoops();
|
|
3995
4286
|
} else if (ev.type === 'loop:complete') {
|
|
3996
|
-
el = mkCVSys('Loop
|
|
4287
|
+
el = mkCVSys('■ Loop done: ' + esc(ev.command || '') + (ev.ranReps ? ' (' + esc(String(ev.ranReps)) + ' runs)' : ''), ts);
|
|
3997
4288
|
if (currentView === 'loops') renderLoops();
|
|
3998
4289
|
} else if (ev.type === 'loop:tick') {
|
|
3999
|
-
|
|
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();
|
|
4000
4296
|
if (currentView === 'loops') renderLoops();
|
|
4001
|
-
} else if (ev.type === 'loop:hil') {
|
|
4002
|
-
el = mkCVSys('⚠ Loop HIL: ' + esc(ev.command || ev.id || ''), ts);
|
|
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);
|
|
4003
4299
|
if (currentView === 'loops') renderLoops();
|
|
4300
|
+
} else if (ev.type === 'loop:hil:resolved') {
|
|
4301
|
+
el = mkCVSys('✓ Loop HIL resolved: ' + esc(ev.loopId || ev.command || ''), ts);
|
|
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');
|
|
4004
4310
|
} else {
|
|
4005
4311
|
el = mkCVSys(esc(ev.type || 'event'), ts);
|
|
4006
4312
|
}
|
|
4007
4313
|
if (animate) el.classList.add('cv-new');
|
|
4008
4314
|
feed.appendChild(el);
|
|
4009
|
-
feed.scrollTop = feed.scrollHeight;
|
|
4315
|
+
if (atBottom) feed.scrollTop = feed.scrollHeight;
|
|
4010
4316
|
}
|
|
4011
4317
|
|
|
4012
4318
|
function _cvExcerptText(ev) {
|
|
4013
4319
|
const t = ev.type || '';
|
|
4014
|
-
if (t === 'intercom' || t === 'org:comms') return (ev.from || '?') + ' → ' + (ev.to || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
|
|
4320
|
+
if (t === 'intercom' || t === 'org:comms') return (ev.from || ev.role || '?') + ' → ' + (ev.to || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
|
|
4015
4321
|
if (t === 'agent:message') return (ev.agent || ev.name || '?') + ': ' + (ev.msg || ev.message || '').slice(0, 80);
|
|
4016
|
-
if (t === 'agent:
|
|
4017
|
-
if (t === '
|
|
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' : '');
|
|
4018
4331
|
if (t === 'org:start') return 'org started: ' + (ev.goal || '').slice(0, 80);
|
|
4019
4332
|
if (t === 'org:complete') return 'org complete';
|
|
4020
4333
|
if (t === 'session:start') return 'started: ' + (ev.prompt || '').replace(/^running org:\s*/i, '').slice(0, 80);
|
|
4021
4334
|
if (t === 'session:complete') return 'completed' + (ev.status ? ' [' + ev.status + ']' : '');
|
|
4022
4335
|
if (t === 'domain:dispatch') return '→ ' + (ev.domain || '') + (ev.cmd ? ': ' + ev.cmd.slice(0, 60) : '');
|
|
4023
4336
|
if (t === 'domain:complete') return '✓ ' + (ev.domain || '');
|
|
4024
|
-
if (ev.
|
|
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);
|
|
4025
4342
|
return t.slice(0, 60);
|
|
4026
4343
|
}
|
|
4027
4344
|
|
|
@@ -4029,18 +4346,25 @@ function _cvExcerptTag(ev) {
|
|
|
4029
4346
|
const t = ev.type || '';
|
|
4030
4347
|
if (t === 'intercom' || t === 'org:comms') return 'IC';
|
|
4031
4348
|
if (t === 'agent:message') return 'MSG';
|
|
4349
|
+
if (t === 'agent:result') return 'RESULT';
|
|
4032
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';
|
|
4033
4353
|
if (t === 'org:checkpoint') return 'CHK';
|
|
4354
|
+
if (t === 'file:write') return 'FILE';
|
|
4034
4355
|
if (t.startsWith('org:')) return 'ORG';
|
|
4356
|
+
if (t.startsWith('run:')) return 'RUN';
|
|
4035
4357
|
if (t.startsWith('session:')) return 'SES';
|
|
4036
4358
|
if (t.startsWith('domain:')) return 'DOM';
|
|
4359
|
+
if (t.startsWith('loop:')) return 'LOOP';
|
|
4360
|
+
if (t.startsWith('agent:')) return 'AGT';
|
|
4037
4361
|
return 'EVT';
|
|
4038
4362
|
}
|
|
4039
4363
|
|
|
4040
4364
|
function renderCvExcerptBanner(bannerId, events, sessionMeta) {
|
|
4041
4365
|
const banner = document.getElementById(bannerId);
|
|
4042
4366
|
if (!banner) return;
|
|
4043
|
-
const SKIP = new Set(['session:start']);
|
|
4367
|
+
const SKIP = new Set(['session:start', 'run:start', 'org:start']);
|
|
4044
4368
|
const meaningful = (events || []).filter(ev => !SKIP.has(ev.type)).slice(-5).reverse();
|
|
4045
4369
|
if (!meaningful.length) {
|
|
4046
4370
|
const prompt = sessionMeta && (sessionMeta.prompt || '');
|
|
@@ -4083,7 +4407,7 @@ function mkCVResult(agent, text, ts) {
|
|
|
4083
4407
|
`<span class="cv-tag" style="background:oklch(20% 0.10 150);color:oklch(72% 0.20 150)">${esc(agent)}</span>`+
|
|
4084
4408
|
`<span class="cv-etype" style="color:oklch(68% 0.20 150)">REPORT</span>`+
|
|
4085
4409
|
`<span class="cv-text" id="${uid}" style="white-space:pre-wrap;line-height:1.6">${esc(preview)}</span>`+
|
|
4086
|
-
(isLong ? `<button class="btn" style="font-size:9px;padding:1px 6px;margin-left:6px;flex-shrink:0" onclick="(function(b
|
|
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>` : '')+
|
|
4087
4411
|
`<span class="cv-ts">${ts}</span></div>`;
|
|
4088
4412
|
return d;
|
|
4089
4413
|
}
|
|
@@ -4098,7 +4422,7 @@ function mkCVFileCard(name, filePath, size, ts) {
|
|
|
4098
4422
|
`<span class="cv-tag" style="background:oklch(22% 0.10 50);color:oklch(78% 0.18 50)">FILE</span>`+
|
|
4099
4423
|
`<span class="cv-text" style="font-family:var(--mono);font-size:10px">${esc(name)}</span>`+
|
|
4100
4424
|
`<span style="font-size:9px;color:var(--text-lo);flex-shrink:0">${sizeStr}</span>`+
|
|
4101
|
-
(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)"
|
|
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>` : '')+
|
|
4102
4426
|
`<span class="cv-ts">${ts}</span></div>`;
|
|
4103
4427
|
return d;
|
|
4104
4428
|
}
|
|
@@ -4114,14 +4438,14 @@ function mkCVAgent(name, text, ts, typeTag) {
|
|
|
4114
4438
|
const d = document.createElement('div');
|
|
4115
4439
|
d.className = 'cv-msg cv-agent';
|
|
4116
4440
|
const tag = typeTag === 'agent:spawn' ? 'SPAWN' : 'MSG';
|
|
4117
|
-
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>`;
|
|
4118
4442
|
return d;
|
|
4119
4443
|
}
|
|
4120
4444
|
|
|
4121
4445
|
function mkCVIntercom(from, to, text, ts) {
|
|
4122
4446
|
const d = document.createElement('div');
|
|
4123
4447
|
d.className = 'cv-msg cv-ic';
|
|
4124
|
-
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>`;
|
|
4125
4449
|
return d;
|
|
4126
4450
|
}
|
|
4127
4451
|
|
|
@@ -4137,24 +4461,69 @@ function connectChatViewSSE() {
|
|
|
4137
4461
|
chatVSseSource.onerror = () => {
|
|
4138
4462
|
dot && dot.classList.remove('on');
|
|
4139
4463
|
lbl && (lbl.textContent = 'OFFLINE');
|
|
4140
|
-
chatVSseSource
|
|
4464
|
+
const _cvSrc = chatVSseSource;
|
|
4141
4465
|
chatVSseSource = null;
|
|
4466
|
+
if (_cvSrc) _cvSrc.close();
|
|
4142
4467
|
setTimeout(connectChatViewSSE, 5000);
|
|
4143
4468
|
};
|
|
4144
4469
|
}
|
|
4145
4470
|
|
|
4146
4471
|
function handleChatViewEvent(ev) {
|
|
4147
|
-
if (!ev
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
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);
|
|
4156
4526
|
}
|
|
4157
|
-
if (chatVCurrentId === ev.session) appendChatViewEvent(ev, true);
|
|
4158
4527
|
}
|
|
4159
4528
|
|
|
4160
4529
|
// ── feature 4: budget cap + desktop notification ───────────
|
|
@@ -4803,7 +5172,7 @@ async function renderLoops() {
|
|
|
4803
5172
|
el.innerHTML = loops.map((l, idx) => {
|
|
4804
5173
|
const maxReps = l.maxReps || 0;
|
|
4805
5174
|
const curRep = l.currentRep || 0;
|
|
4806
|
-
const isTillend = !maxReps || l.
|
|
5175
|
+
const isTillend = !maxReps || l.type === 'tillend' || String(l.command || '').includes('--tillend');
|
|
4807
5176
|
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
|
|
4808
5177
|
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
4809
5178
|
const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
|
|
@@ -4822,7 +5191,7 @@ async function renderLoops() {
|
|
|
4822
5191
|
const runs = curRep != null ? curRep : '—';
|
|
4823
5192
|
const pct = (maxReps > 0) ? Math.min(100, Math.round(curRep / maxReps * 100)) : 0;
|
|
4824
5193
|
const progBar = isTillend
|
|
4825
|
-
? `<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>`
|
|
4826
5195
|
: (maxReps > 0 && running)
|
|
4827
5196
|
? `<div class="lp-bar"><div class="lp-fill" style="width:${pct}%"></div></div>`
|
|
4828
5197
|
: '';
|
|
@@ -4852,8 +5221,8 @@ async function renderLoops() {
|
|
|
4852
5221
|
${stopBtn}
|
|
4853
5222
|
</div>
|
|
4854
5223
|
<div class="loop-expand">
|
|
4855
|
-
${fullPrompt ? `<div class="le-row"><div class="le-lbl">Prompt</div><div class="le-val mono" style="cursor:pointer" onclick="navigator.clipboard.writeText(
|
|
4856
|
-
${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>` : ''}
|
|
4857
5226
|
<div class="le-row"><div class="le-lbl">Type</div><div class="le-val">${isTillend ? '∞ tillend' : '↺ repeat'}</div></div>
|
|
4858
5227
|
<div class="le-row"><div class="le-lbl">Interval</div><div class="le-val">${interval}</div></div>
|
|
4859
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>
|
|
@@ -5230,7 +5599,9 @@ function v2RenderOrgList() {
|
|
|
5230
5599
|
|
|
5231
5600
|
async function v2SelectOrg(name) {
|
|
5232
5601
|
_v2SelOrg = name;
|
|
5602
|
+
_pushHash();
|
|
5233
5603
|
if (typeof v2CloseAgent === 'function') v2CloseAgent();
|
|
5604
|
+
if (_orgLiveInterval) { clearInterval(_orgLiveInterval); _orgLiveInterval = null; }
|
|
5234
5605
|
const requested = name;
|
|
5235
5606
|
const requestedDir = DIR;
|
|
5236
5607
|
document.querySelectorAll('.org-item').forEach(el => {
|
|
@@ -5265,11 +5636,14 @@ async function v2SelectOrg(name) {
|
|
|
5265
5636
|
_v2OrgData = mainR.config ? { ...mainR, ...mainR.config, running: mainR.running } : mainR;
|
|
5266
5637
|
_v2OrgData._activity = Array.isArray(actR) ? actR : [];
|
|
5267
5638
|
_v2OrgData._health = healthR;
|
|
5268
|
-
// Fetch supplemental data for new tabs
|
|
5269
|
-
const agentsR
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
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;
|
|
5273
5647
|
_v2OrgData._agents = Array.isArray(agentsR) ? agentsR : (agentsR?.agents || []);
|
|
5274
5648
|
_v2OrgData._budgets = budgetsR;
|
|
5275
5649
|
_v2OrgData._members = Array.isArray(membersR) ? membersR : (membersR?.members || []);
|
|
@@ -5308,6 +5682,7 @@ window.v2SwitchOrgTab = function(tab) {
|
|
|
5308
5682
|
});
|
|
5309
5683
|
document.querySelectorAll('.odt-pane').forEach(p => p.classList.remove('active'));
|
|
5310
5684
|
document.getElementById('odt-' + tab)?.classList.add('active');
|
|
5685
|
+
_pushHash();
|
|
5311
5686
|
v2RenderOrgTab(tab);
|
|
5312
5687
|
};
|
|
5313
5688
|
|
|
@@ -5353,7 +5728,8 @@ function v2RenderOrgTab(tab) {
|
|
|
5353
5728
|
|
|
5354
5729
|
// ── org chart ───────────────────────────────
|
|
5355
5730
|
function _v2OrgIsLeader(r) {
|
|
5356
|
-
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';
|
|
5357
5733
|
}
|
|
5358
5734
|
|
|
5359
5735
|
function v2RenderOrgChart() {
|
|
@@ -5541,22 +5917,27 @@ function v2RenderOrgChart() {
|
|
|
5541
5917
|
`<clipPath id="${nodeClipIds[i]}"><circle r="${Math.round(R * 0.52)}"/></clipPath>`
|
|
5542
5918
|
).join('');
|
|
5543
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));
|
|
5544
5922
|
let nodesHTML = '';
|
|
5545
5923
|
roles.forEach((role, i) => {
|
|
5546
5924
|
const p = pos[role.id];
|
|
5547
5925
|
if (!p) return;
|
|
5548
5926
|
const leader = _v2OrgIsLeader(role);
|
|
5549
5927
|
const color = colorOf(role, i);
|
|
5928
|
+
const isRunning = _chartRunning.has(role.id);
|
|
5550
5929
|
const displayName = role.name || role.title || role.id;
|
|
5551
5930
|
const subType = role.type || role.agent_type || '';
|
|
5552
5931
|
const MAX_LBL = Math.floor(R / 4.2);
|
|
5553
5932
|
const nameText = displayName.length > MAX_LBL ? displayName.slice(0, MAX_LBL - 1) + '…' : displayName;
|
|
5554
5933
|
const subTypeText = subType.length > MAX_LBL ? subType.slice(0, MAX_LBL - 1) + '…' : subType;
|
|
5555
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"/>` : '';
|
|
5556
5936
|
const nameY = subType ? (R * 0.55 + 2).toFixed(0) : (R * 0.55 + 8).toFixed(0);
|
|
5557
5937
|
const avR = Math.round(R * 0.52);
|
|
5558
5938
|
const avatarSrc = v2RoleAvatar(role);
|
|
5559
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}
|
|
5560
5941
|
<title>${esc(displayName)}${subType ? ` · ${esc(subType)}` : ''} — click for details</title>
|
|
5561
5942
|
${outerRing}
|
|
5562
5943
|
<circle r="${R}" fill="oklch(12% 0.008 55)" stroke="${color}" stroke-width="${leader ? 2.5 : 1.8}" filter="url(#v2glow)"/>
|
|
@@ -5624,6 +6005,31 @@ function v2RenderOrgChart() {
|
|
|
5624
6005
|
}
|
|
5625
6006
|
|
|
5626
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
|
+
|
|
5627
6033
|
// ── avatar lookup ────────────────────────────
|
|
5628
6034
|
const _v2AvatarKnown = new Set([
|
|
5629
6035
|
'coder','senior-developer','reviewer','tester','planner','researcher',
|
|
@@ -5691,9 +6097,12 @@ function v2RenderOrgRoles() {
|
|
|
5691
6097
|
// Determine leader: explicit reports_to=undefined + type=planner/coordinator, or first role, or id=boss
|
|
5692
6098
|
const leaderIds = new Set(roles.filter(r => r.id === 'boss' || r.type === 'coordinator' || r.type === 'planner').map(r => r.id));
|
|
5693
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));
|
|
5694
6102
|
pane.innerHTML = `<div class="roles-v2-grid">${roles.map((r, i) => {
|
|
5695
6103
|
const color = V2_ROLE_COLORS[i % V2_ROLE_COLORS.length];
|
|
5696
6104
|
const isLeader = leaderIds.has(r.id);
|
|
6105
|
+
const isRunning = _rolesRunning.has(r.id);
|
|
5697
6106
|
const resps = Array.isArray(r.responsibilities) ? r.responsibilities : [];
|
|
5698
6107
|
const displayName = r.name || r.title || r.id;
|
|
5699
6108
|
const roleType = r.type || r.agent_type || '';
|
|
@@ -5702,7 +6111,7 @@ function v2RenderOrgRoles() {
|
|
|
5702
6111
|
<div class="rv2-head">
|
|
5703
6112
|
<img class="rv2-avatar" src="${avatarSrc}" alt="${esc(displayName)}" loading="lazy" onerror="this.src='data/avatars/coder.svg'"/>
|
|
5704
6113
|
<div class="rv2-info">
|
|
5705
|
-
<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>
|
|
5706
6115
|
${roleType ? `<span class="rv2-agent">${esc(roleType)}</span>` : ''}
|
|
5707
6116
|
</div>
|
|
5708
6117
|
</div>
|
|
@@ -5730,16 +6139,18 @@ async function v2OpenAgent(roleId) {
|
|
|
5730
6139
|
const bodyEl = document.getElementById('oad-body');
|
|
5731
6140
|
const headEl = document.getElementById('oad-head');
|
|
5732
6141
|
if (!drawer || !bodyEl || !headEl) return;
|
|
6142
|
+
const _agentOrg = _v2SelOrg, _agentRole = roleId;
|
|
5733
6143
|
drawer.classList.add('open'); drawer.setAttribute('aria-hidden', 'false');
|
|
5734
6144
|
v2HighlightAgentNode(roleId);
|
|
5735
6145
|
headEl.innerHTML = '<button class="oad-close" onclick="v2CloseAgent()" aria-label="Close">✕</button>';
|
|
5736
6146
|
bodyEl.innerHTML = '<div class="oad-loading">Loading agent…</div>';
|
|
5737
6147
|
try {
|
|
5738
|
-
const r = await fetch(`/api/org/${encodeURIComponent(
|
|
6148
|
+
const r = await fetch(`/api/org/${encodeURIComponent(_agentOrg)}/agent/${encodeURIComponent(_agentRole)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`);
|
|
5739
6149
|
if (!r.ok) throw new Error('not found');
|
|
6150
|
+
if (_v2SelOrg !== _agentOrg) return;
|
|
5740
6151
|
v2RenderAgentDrawer(await r.json());
|
|
5741
6152
|
} catch (_) {
|
|
5742
|
-
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>';
|
|
5743
6154
|
}
|
|
5744
6155
|
}
|
|
5745
6156
|
function v2RenderAgentDrawer(data) {
|
|
@@ -5831,16 +6242,38 @@ function v2RenderMarkdown(md) {
|
|
|
5831
6242
|
}
|
|
5832
6243
|
|
|
5833
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
|
+
|
|
5834
6259
|
function v2RenderOrgActivity() {
|
|
5835
6260
|
if (!_v2SelOrg) return;
|
|
5836
6261
|
const activity = _v2OrgData?._activity || [];
|
|
5837
6262
|
const orgEvents = _v2OrgEventLog[_v2SelOrg] || [];
|
|
5838
|
-
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);
|
|
5839
6267
|
const pane = document.getElementById('odt-activity');
|
|
5840
6268
|
if (!pane) return;
|
|
5841
6269
|
const fmtOrgEvType = t => {
|
|
5842
|
-
const m={
|
|
5843
|
-
|
|
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):/,'');
|
|
5844
6277
|
};
|
|
5845
6278
|
if (!events.length) {
|
|
5846
6279
|
pane.innerHTML = '<div class="empty">No activity recorded</div>';
|
|
@@ -5848,7 +6281,7 @@ function v2RenderOrgActivity() {
|
|
|
5848
6281
|
}
|
|
5849
6282
|
pane.innerHTML = `<div class="act-v2-list">${events.map(ev => {
|
|
5850
6283
|
const t = ev.ts ? new Date(ev.ts).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
5851
|
-
const detail = ev
|
|
6284
|
+
const detail = fmtOrgEvDetail(ev) || ev.role || ev.agent || '';
|
|
5852
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>`;
|
|
5853
6286
|
}).join('')}</div>`;
|
|
5854
6287
|
}
|
|
@@ -5868,7 +6301,11 @@ function v2RenderOrgHealth() {
|
|
|
5868
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>
|
|
5869
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>`:''}
|
|
5870
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>`:''}
|
|
5871
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>`:''}
|
|
5872
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>`}
|
|
5873
6310
|
`;
|
|
5874
6311
|
}
|
|
@@ -5904,11 +6341,17 @@ function v2RenderOrgTasks() {
|
|
|
5904
6341
|
if (!pane) return;
|
|
5905
6342
|
// Normalize: API returns {todo:[],doing:[],done:[]} (columns) or a flat array
|
|
5906
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
|
+
};
|
|
5907
6350
|
let tasks = [];
|
|
5908
|
-
if (Array.isArray(raw)) tasks = raw.
|
|
6351
|
+
if (Array.isArray(raw)) tasks = raw.map(t => ({ ...t, status: _normTaskStatus(t.status, 'todo') }));
|
|
5909
6352
|
else if (raw && typeof raw === 'object') {
|
|
5910
6353
|
for (const [col, items] of Object.entries(raw)) {
|
|
5911
|
-
(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) }));
|
|
5912
6355
|
}
|
|
5913
6356
|
}
|
|
5914
6357
|
if (!tasks.length) { pane.innerHTML = '<div class="empty">No tasks</div>'; return; }
|
|
@@ -5943,7 +6386,7 @@ function v2RenderOrgCosts() {
|
|
|
5943
6386
|
? c.map(r => ({ label: r.label ?? r.name ?? '—', cost: Number(r.value ?? r.cost ?? 0), tin: 0, tout: 0 }))
|
|
5944
6387
|
: Object.entries(c).map(([k, v]) => ({ label: k, cost: Number(v) || 0, tin: 0, tout: 0 }));
|
|
5945
6388
|
}
|
|
5946
|
-
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; }
|
|
5947
6390
|
const cur = (b && b.currency) || 'USD';
|
|
5948
6391
|
const period = (b && b.period) || '';
|
|
5949
6392
|
const total = rows.reduce((s, r) => s + r.cost, 0);
|
|
@@ -5974,7 +6417,9 @@ function v2RenderOrgMembers() {
|
|
|
5974
6417
|
const joinReqs = Array.isArray(_v2OrgData?._joinRequests) ? _v2OrgData._joinRequests : [];
|
|
5975
6418
|
if (!list.length) { pane.innerHTML = '<div class="empty">No members</div>'; return; }
|
|
5976
6419
|
const src = members.length ? 'joined members' : 'defined roles';
|
|
5977
|
-
|
|
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);
|
|
5978
6423
|
pane.innerHTML = `<div class="m-group-title">Members (${list.length}) · ${src}</div>` +
|
|
5979
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">
|
|
5980
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>
|
|
@@ -5999,7 +6444,7 @@ function v2RenderOrgGoals() {
|
|
|
5999
6444
|
const indent = depth * 20;
|
|
6000
6445
|
const pct = g.total > 0 ? Math.min(100, Math.round(g.filled / g.total * 100)) : 0;
|
|
6001
6446
|
const status = g.status || 'pending';
|
|
6002
|
-
const statusCls = status === 'done' ? 'on' : status === 'blocked' ? 'warn' : '';
|
|
6447
|
+
const statusCls = status === 'done' ? 'on' : (status === 'blocked' || status === 'in_progress') ? 'warn' : '';
|
|
6003
6448
|
return `<div style="padding:8px 0;border-bottom:1px solid var(--border);padding-left:${indent}px">
|
|
6004
6449
|
<div style="display:flex;align-items:center;gap:10px">
|
|
6005
6450
|
<span style="flex:1;font-size:13px;color:var(--text-hi)">${esc(g.text || g.goal || g.title || '—')}</span>
|
|
@@ -6020,9 +6465,10 @@ function v2RenderOrgBoard() {
|
|
|
6020
6465
|
if (!issues.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
|
|
6021
6466
|
const cols = ['open', 'in_progress', 'blocked', 'done', 'cancelled'];
|
|
6022
6467
|
const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
|
|
6468
|
+
const _normIssueStatus = s => (s === 'closed' || s === 'resolved') ? 'done' : (s || 'open');
|
|
6023
6469
|
el.innerHTML = `<div style="display:flex;gap:10px;overflow-x:auto;padding-bottom:8px">` +
|
|
6024
6470
|
cols.map(col => {
|
|
6025
|
-
const cards = issues.filter(i => (i.status || i.state
|
|
6471
|
+
const cards = issues.filter(i => _normIssueStatus(i.status || i.state) === col);
|
|
6026
6472
|
return `<div style="min-width:160px;flex:1">
|
|
6027
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>
|
|
6028
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">
|
|
@@ -6044,27 +6490,41 @@ function v2RenderOrgLive() {
|
|
|
6044
6490
|
${running.length
|
|
6045
6491
|
? running.map(a => `<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border)">
|
|
6046
6492
|
<span class="live-dot" style="flex-shrink:0"></span>
|
|
6047
|
-
<span style="font-size:13px;color:var(--text-hi)">${esc(a.
|
|
6048
|
-
<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>
|
|
6049
6495
|
</div>`).join('')
|
|
6050
6496
|
: '<div style="color:var(--text-lo);font-size:13px;padding:10px 0">No agents currently running</div>'}
|
|
6051
6497
|
</div>
|
|
6052
6498
|
<div class="m-group-title" style="margin-bottom:6px">Activity Feed</div>
|
|
6053
6499
|
<div style="max-height:240px;overflow-y:auto;font-size:11px;font-family:var(--mono)">
|
|
6054
|
-
${(_v2OrgData._activity || []).slice(-30).reverse().map(e
|
|
6055
|
-
${esc(relTime(e.ts || e.timestamp || e.created_at))}
|
|
6056
|
-
<span style="color:var(--text-mid);margin-left:6px">${esc(e.type || e.kind || e.event || '—')}</span>
|
|
6057
|
-
${e.message ? `<span style="color:var(--text-hi);margin-left:6px">${esc(e.message.toString().slice(0, 80))}</span>` : ''}
|
|
6058
|
-
</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('');})()}
|
|
6059
6501
|
</div>`;
|
|
6060
6502
|
// auto-refresh every 5s while tab is active
|
|
6061
6503
|
if (!_orgLiveInterval) {
|
|
6062
6504
|
_orgLiveInterval = setInterval(async () => {
|
|
6063
|
-
if (_v2OrgTab !== 'live' || !_v2SelOrg) { clearInterval(_orgLiveInterval); _orgLiveInterval = null; return; }
|
|
6064
|
-
const
|
|
6505
|
+
if (_v2OrgTab !== 'live' || !_v2SelOrg || currentView !== 'orgs') { clearInterval(_orgLiveInterval); _orgLiveInterval = null; return; }
|
|
6506
|
+
const _liveOrg = _v2SelOrg;
|
|
6507
|
+
const _enc = encodeURIComponent(_liveOrg);
|
|
6065
6508
|
try {
|
|
6066
|
-
const
|
|
6067
|
-
|
|
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();
|
|
6068
6528
|
} catch (_) {}
|
|
6069
6529
|
}, 5000);
|
|
6070
6530
|
}
|
|
@@ -6074,10 +6534,12 @@ function v2RenderOrgLive() {
|
|
|
6074
6534
|
async function v2RenderOrgApprovals() {
|
|
6075
6535
|
const el = document.getElementById('odt-approvals');
|
|
6076
6536
|
if (!el || !_v2SelOrg) return;
|
|
6537
|
+
const _rendOrg = _v2SelOrg;
|
|
6077
6538
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6078
6539
|
try {
|
|
6079
|
-
const _enc = encodeURIComponent(
|
|
6540
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6080
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;
|
|
6081
6543
|
const approvals = Array.isArray(data) ? data : (data.approvals || []);
|
|
6082
6544
|
if (!approvals.length) { el.innerHTML = '<div class="empty">No pending approvals</div>'; return; }
|
|
6083
6545
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6095,8 +6557,8 @@ async function v2RenderOrgApprovals() {
|
|
|
6095
6557
|
<td style="padding:6px 8px;color:var(--text-xs);font-size:11px;font-family:var(--mono)">${relTime(a.created_at || a.ts)}</td>
|
|
6096
6558
|
<td style="padding:6px 8px;white-space:nowrap">
|
|
6097
6559
|
${pending
|
|
6098
|
-
? `<button class="btn" style="font-size:10px;color:var(--green);border-color:var(--green)" title="Approve" aria-label="Approve"
|
|
6099
|
-
<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>`
|
|
6100
6562
|
: ''}
|
|
6101
6563
|
</td>
|
|
6102
6564
|
</tr>`;
|
|
@@ -6108,7 +6570,7 @@ async function orgApprovalAction(id, action) {
|
|
|
6108
6570
|
if (!confirm(action + ' this request?')) return;
|
|
6109
6571
|
try {
|
|
6110
6572
|
const _enc = encodeURIComponent(_v2SelOrg);
|
|
6111
|
-
await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}`, {
|
|
6573
|
+
await fetch(`/api/org/${_enc}/approvals/${encodeURIComponent(id)}${DIR ? '?dir=' + encodeURIComponent(DIR) : ''}`, {
|
|
6112
6574
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action })
|
|
6113
6575
|
});
|
|
6114
6576
|
showToast('Done', action + 'd', 'ok');
|
|
@@ -6120,10 +6582,12 @@ async function orgApprovalAction(id, action) {
|
|
|
6120
6582
|
async function v2RenderOrgSecrets() {
|
|
6121
6583
|
const el = document.getElementById('odt-secrets');
|
|
6122
6584
|
if (!el || !_v2SelOrg) return;
|
|
6585
|
+
const _rendOrg = _v2SelOrg;
|
|
6123
6586
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6124
6587
|
try {
|
|
6125
|
-
const _enc = encodeURIComponent(
|
|
6588
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6126
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;
|
|
6127
6591
|
const secrets = Array.isArray(data) ? data : (data.secrets || []);
|
|
6128
6592
|
el.innerHTML = '<div style="font-size:11px;color:var(--text-lo);margin-bottom:10px">Secret values are never transmitted or displayed.</div>' +
|
|
6129
6593
|
(secrets.length
|
|
@@ -6186,10 +6650,12 @@ function generateOrgSettingsCmd() {
|
|
|
6186
6650
|
async function v2RenderOrgRoutines() {
|
|
6187
6651
|
const el = document.getElementById('odt-routines');
|
|
6188
6652
|
if (!el || !_v2SelOrg) return;
|
|
6653
|
+
const _rendOrg = _v2SelOrg;
|
|
6189
6654
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6190
6655
|
try {
|
|
6191
|
-
const _enc = encodeURIComponent(
|
|
6656
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6192
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;
|
|
6193
6659
|
const routines = Array.isArray(data) ? data : (data.routines || _v2OrgData?.config?.routines || []);
|
|
6194
6660
|
if (!routines.length) { el.innerHTML = '<div class="empty">No routines configured</div>'; return; }
|
|
6195
6661
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -6199,8 +6665,8 @@ async function v2RenderOrgRoutines() {
|
|
|
6199
6665
|
routines.map(r => `<tr style="border-top:1px solid var(--border)">
|
|
6200
6666
|
<td style="padding:6px 8px;color:var(--text-hi)">${esc(r.name || '—')}</td>
|
|
6201
6667
|
<td style="padding:6px 8px;font-family:var(--mono);color:var(--text-lo)">${esc(r.cron || r.schedule || '—')}</td>
|
|
6202
|
-
<td style="padding:6px 8px;color:var(--text-lo)">${r.lastRun ? relTime(r.lastRun) : '—'}</td>
|
|
6203
|
-
<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>
|
|
6204
6670
|
</tr>`).join('') + '</tbody></table>';
|
|
6205
6671
|
} catch (e) { el.innerHTML = '<div class="empty">Failed: ' + esc(e.message) + '</div>'; }
|
|
6206
6672
|
}
|
|
@@ -6209,10 +6675,12 @@ async function v2RenderOrgRoutines() {
|
|
|
6209
6675
|
async function v2RenderOrgMyIssues() {
|
|
6210
6676
|
const el = document.getElementById('odt-myissues');
|
|
6211
6677
|
if (!el || !_v2SelOrg) return;
|
|
6678
|
+
const _rendOrg = _v2SelOrg;
|
|
6212
6679
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6213
6680
|
try {
|
|
6214
|
-
const _enc = encodeURIComponent(
|
|
6681
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6215
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;
|
|
6216
6684
|
const issues = Array.isArray(data) ? data : (data.issues || []);
|
|
6217
6685
|
if (!issues.length) { el.innerHTML = '<div class="empty">No issues assigned to you</div>'; return; }
|
|
6218
6686
|
const PRIORITY = { urgent: '🔴', high: '🟠', medium: '🟡', low: '🟢' };
|
|
@@ -6221,7 +6689,7 @@ async function v2RenderOrgMyIssues() {
|
|
|
6221
6689
|
<th style="padding:5px 4px">P</th><th style="padding:5px 8px">Title</th><th>Status</th><th>Updated</th>
|
|
6222
6690
|
</tr></thead><tbody>` +
|
|
6223
6691
|
issues.slice(0, 50).map(i => {
|
|
6224
|
-
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' : '';
|
|
6225
6693
|
return `<tr style="border-top:1px solid var(--border)">
|
|
6226
6694
|
<td style="padding:5px 4px">${PRIORITY[i.priority] || '·'}</td>
|
|
6227
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>
|
|
@@ -6237,7 +6705,14 @@ function v2RenderOrgBudgets() {
|
|
|
6237
6705
|
const el = document.getElementById('odt-budgets');
|
|
6238
6706
|
if (!el || !_v2OrgData) return;
|
|
6239
6707
|
const b = _v2OrgData._budgets || _v2OrgData.budgets || {};
|
|
6240
|
-
|
|
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);
|
|
6241
6716
|
|
|
6242
6717
|
function fillBar(used, limit) {
|
|
6243
6718
|
if (!limit) return '';
|
|
@@ -6248,22 +6723,25 @@ function v2RenderOrgBudgets() {
|
|
|
6248
6723
|
}
|
|
6249
6724
|
|
|
6250
6725
|
let html = '';
|
|
6251
|
-
if (
|
|
6252
|
-
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>`;
|
|
6253
6728
|
}
|
|
6254
|
-
if (
|
|
6255
|
-
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>`;
|
|
6256
6731
|
}
|
|
6257
|
-
if (
|
|
6732
|
+
if (budgetAgents.length) {
|
|
6258
6733
|
html += '<div class="m-group-title" style="margin-bottom:6px">Per Agent</div>' +
|
|
6259
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>` +
|
|
6260
|
-
|
|
6261
|
-
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;
|
|
6262
6740
|
return `<tr style="border-top:1px solid var(--border)${over ? ';color:var(--red)' : ''}">
|
|
6263
|
-
<td style="padding:4px 8px;font-family:var(--mono);font-size:11px;color:${over ? 'var(--red)' : 'var(--text-hi)'}">${esc((a.
|
|
6264
|
-
<td style="padding:4px 8px;color:var(--text-lo)">${
|
|
6265
|
-
<td style="padding:4px 8px;color:var(--text-lo)">${
|
|
6266
|
-
<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>
|
|
6267
6745
|
</tr>`;
|
|
6268
6746
|
}).join('') + '</tbody></table>';
|
|
6269
6747
|
}
|
|
@@ -6274,10 +6752,12 @@ function v2RenderOrgBudgets() {
|
|
|
6274
6752
|
async function v2RenderOrgPlugins() {
|
|
6275
6753
|
const el = document.getElementById('odt-plugins');
|
|
6276
6754
|
if (!el || !_v2SelOrg) return;
|
|
6755
|
+
const _rendOrg = _v2SelOrg;
|
|
6277
6756
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6278
6757
|
try {
|
|
6279
|
-
const _enc = encodeURIComponent(
|
|
6758
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6280
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;
|
|
6281
6761
|
const plugins = Array.isArray(data) ? data : (data.plugins || []);
|
|
6282
6762
|
if (!plugins.length) { el.innerHTML = '<div class="empty">No plugins installed</div>'; return; }
|
|
6283
6763
|
el.innerHTML = `<div class="proj-grid">` +
|
|
@@ -6305,7 +6785,7 @@ function v2RenderOrgCharts() {
|
|
|
6305
6785
|
const DAY = 86400000;
|
|
6306
6786
|
const buckets = Array.from({length: 14}, (_, i) => ({
|
|
6307
6787
|
day: new Date(now - (13 - i) * DAY).toLocaleDateString('en', {month:'short', day:'numeric'}),
|
|
6308
|
-
ts: now - (
|
|
6788
|
+
ts: now - (14 - i) * DAY,
|
|
6309
6789
|
events: 0, errors: 0,
|
|
6310
6790
|
}));
|
|
6311
6791
|
activity.forEach(e => {
|
|
@@ -6365,10 +6845,12 @@ function v2RenderOrgCharts() {
|
|
|
6365
6845
|
async function v2RenderOrgProjects() {
|
|
6366
6846
|
const el = document.getElementById('odt-projects');
|
|
6367
6847
|
if (!el || !_v2SelOrg) return;
|
|
6848
|
+
const _rendOrg = _v2SelOrg;
|
|
6368
6849
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6369
6850
|
try {
|
|
6370
|
-
const _enc = encodeURIComponent(
|
|
6851
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6371
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;
|
|
6372
6854
|
const projects = Array.isArray(data) ? data : (data.projects || []);
|
|
6373
6855
|
if (!projects.length) { el.innerHTML = '<div class="empty">No projects configured</div>'; return; }
|
|
6374
6856
|
el.innerHTML = `<div class="proj-grid">${projects.map(p => `<div class="proj-card">
|
|
@@ -6421,19 +6903,37 @@ let _odtChatSessions = [];
|
|
|
6421
6903
|
let _odtChatCurrentId = null;
|
|
6422
6904
|
let _odtChatCurrentAgent = 'all';
|
|
6423
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
|
|
6424
6914
|
|
|
6425
6915
|
function _odtOrgSessionMatch(s) {
|
|
6426
6916
|
if (!_v2SelOrg) return true;
|
|
6427
6917
|
const orgLc = _v2SelOrg.toLowerCase();
|
|
6428
6918
|
const prompt = (s.prompt || '').toLowerCase();
|
|
6429
|
-
|
|
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;
|
|
6430
6930
|
return (s.events || []).some(ev => (ev.org || '').toLowerCase() === orgLc);
|
|
6431
6931
|
}
|
|
6432
6932
|
|
|
6433
6933
|
function _odtChatAgentMatches(ev) {
|
|
6434
6934
|
if (_odtChatCurrentAgent === 'all') return true;
|
|
6435
6935
|
// Structural events always show regardless of agent filter
|
|
6436
|
-
const structural = new Set(['session:start','session:complete','file:write','run:start','run:complete','run:cycle:complete','org:start','org:complete','org:agent:online']);
|
|
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']);
|
|
6437
6937
|
if (structural.has(ev.type)) return true;
|
|
6438
6938
|
return ev.agent === _odtChatCurrentAgent || ev.from === _odtChatCurrentAgent || ev.to === _odtChatCurrentAgent || ev.role === _odtChatCurrentAgent;
|
|
6439
6939
|
}
|
|
@@ -6458,39 +6958,172 @@ async function v2RenderOrgChat() {
|
|
|
6458
6958
|
</div>
|
|
6459
6959
|
<div id="odt-chat-excerpt" class="cv-excerpt-banner"></div>
|
|
6460
6960
|
<div id="odt-chat-feed">
|
|
6461
|
-
<div id="odt-chat-empty">Select a
|
|
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>
|
|
6462
6962
|
</div>`;
|
|
6463
6963
|
}
|
|
6464
6964
|
|
|
6465
|
-
// Reset state when org changes
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
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
|
+
}
|
|
6472
6981
|
|
|
6473
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
|
+
}
|
|
6474
7052
|
_odtConnectChatSSE();
|
|
6475
7053
|
}
|
|
6476
7054
|
|
|
6477
7055
|
async function _odtLoadChatSessions() {
|
|
6478
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();
|
|
6479
7063
|
try {
|
|
6480
7064
|
const dirParam = (typeof DIR !== 'undefined' && DIR) ? '&dir='+encodeURIComponent(DIR) : '';
|
|
6481
7065
|
// Primary: structured org run files
|
|
6482
|
-
const runs = await apiFetch('/api/org/'+encodeURIComponent(
|
|
7066
|
+
const runs = await apiFetch('/api/org/'+encodeURIComponent(_loadedForOrg)+'/runs?x=1'+dirParam);
|
|
7067
|
+
if (_v2SelOrg !== _loadedForOrg) return;
|
|
6483
7068
|
if (Array.isArray(runs) && runs.length) {
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
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));
|
|
6493
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(_) {}
|
|
6494
7127
|
if (!_odtChatCurrentId && _odtChatSessions.length) {
|
|
6495
7128
|
const first = _odtChatSessions[0];
|
|
6496
7129
|
const sel = document.getElementById('odt-chat-sel');
|
|
@@ -6499,38 +7132,190 @@ async function _odtLoadChatSessions() {
|
|
|
6499
7132
|
return;
|
|
6500
7133
|
}
|
|
6501
7134
|
} catch(_) {}
|
|
7135
|
+
if (_v2SelOrg !== _loadedForOrg) return;
|
|
6502
7136
|
// Fallback: mastermind lifecycle events (pre-run-file era sessions)
|
|
6503
7137
|
try {
|
|
6504
7138
|
const r = await apiFetch('/api/mastermind/sessions');
|
|
7139
|
+
if (_v2SelOrg !== _loadedForOrg) return;
|
|
6505
7140
|
const all = Array.isArray(r) ? r : Object.values(r.sessions || {});
|
|
6506
|
-
_odtChatSessions = all.filter(_odtOrgSessionMatch)
|
|
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));
|
|
6507
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
|
+
}
|
|
6508
7163
|
} catch(_) {}
|
|
6509
7164
|
}
|
|
6510
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
|
+
|
|
6511
7258
|
function _odtPopulateChatSel() {
|
|
6512
7259
|
const sel = document.getElementById('odt-chat-sel');
|
|
6513
7260
|
if (!sel) return;
|
|
7261
|
+
if (document.activeElement === sel) return;
|
|
7262
|
+
// Rebuild groups from current sessions + runs
|
|
7263
|
+
_odtRebuildSessionGroups();
|
|
7264
|
+
_odtRebuildRunGroups();
|
|
6514
7265
|
const prev = _odtChatCurrentId || sel.value;
|
|
6515
|
-
const
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
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);
|
|
6519
7277
|
const ts = d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
6520
7278
|
const date = d.toLocaleDateString([],{month:'short',day:'numeric'});
|
|
6521
|
-
const
|
|
6522
|
-
const
|
|
6523
|
-
const
|
|
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` : '';
|
|
6524
7286
|
const opt = document.createElement('option');
|
|
6525
|
-
opt.value =
|
|
6526
|
-
opt.textContent =
|
|
6527
|
-
if (
|
|
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;
|
|
6528
7290
|
sel.appendChild(opt);
|
|
6529
7291
|
});
|
|
6530
|
-
|
|
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
|
|
6531
7314
|
if (!_odtChatCurrentId) {
|
|
6532
|
-
const
|
|
6533
|
-
|
|
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); }
|
|
6534
7319
|
}
|
|
6535
7320
|
}
|
|
6536
7321
|
|
|
@@ -6540,36 +7325,254 @@ window.odtChatSelectSession = async function(id) {
|
|
|
6540
7325
|
const feed = document.getElementById('odt-chat-feed');
|
|
6541
7326
|
const emptyEl = document.getElementById('odt-chat-empty');
|
|
6542
7327
|
const bar = document.getElementById('odt-chat-agent-bar');
|
|
6543
|
-
feed.querySelectorAll('.cv-msg').forEach(e => e.remove());
|
|
7328
|
+
feed.querySelectorAll('.cv-msg, .cv-session-hdr').forEach(e => e.remove());
|
|
6544
7329
|
if (!id) {
|
|
6545
7330
|
if (emptyEl) emptyEl.style.display = 'block';
|
|
6546
7331
|
if (bar) bar.style.display = 'none';
|
|
6547
7332
|
return;
|
|
6548
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
|
+
|
|
6549
7441
|
const sess = _odtChatSessions.find(s => s.id === id);
|
|
6550
7442
|
if (!sess) return;
|
|
6551
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
|
+
}
|
|
6552
7470
|
// Lazy-load events for org run sessions
|
|
6553
7471
|
if (sess._isOrgRun && !sess._eventsLoaded) {
|
|
6554
7472
|
const feed2 = document.getElementById('odt-chat-feed');
|
|
6555
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)' });
|
|
6556
7474
|
loadingEl.textContent = 'Loading run events…';
|
|
6557
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;
|
|
6558
7481
|
try {
|
|
6559
7482
|
const dirParam = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
|
|
6560
7483
|
const evs = await apiFetch('/api/org/'+encodeURIComponent(_v2SelOrg)+'/runs/'+encodeURIComponent(id)+dirParam);
|
|
6561
|
-
sess.events
|
|
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
|
|
6562
7494
|
sess._eventsLoaded = true;
|
|
6563
|
-
}
|
|
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; }
|
|
6564
7500
|
loadingEl.remove();
|
|
7501
|
+
if (_odtChatCurrentId !== id) return; // user switched sessions during fetch
|
|
6565
7502
|
}
|
|
6566
7503
|
// Agent pills from org config roles (authoritative list, not derived from events)
|
|
6567
7504
|
const orgRoles = Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : [];
|
|
6568
7505
|
_odtPopulateAgentBar(sess.events || [], orgRoles);
|
|
6569
|
-
//
|
|
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.
|
|
6570
7509
|
const ex = document.getElementById('odt-chat-excerpt');
|
|
6571
|
-
if (
|
|
6572
|
-
|
|
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
|
+
}
|
|
6573
7576
|
feed.scrollTop = feed.scrollHeight;
|
|
6574
7577
|
};
|
|
6575
7578
|
|
|
@@ -6585,20 +7588,33 @@ function _odtPopulateAgentBar(events, orgRoles) {
|
|
|
6585
7588
|
(events || []).forEach(ev => {
|
|
6586
7589
|
if (ev.type === 'org:agent:online' && ev.role) agentSet.add(ev.role);
|
|
6587
7590
|
else if (ev.type === 'org:comms') {
|
|
6588
|
-
|
|
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);
|
|
6589
7594
|
if (ev.to && ev.to !== 'all' && ev.to.length < 40) agentSet.add(ev.to);
|
|
6590
7595
|
}
|
|
6591
7596
|
});
|
|
6592
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
|
+
});
|
|
6593
7606
|
const agents = [...agentSet].sort();
|
|
6594
7607
|
if (!agents.length) { bar.style.display = 'none'; return; }
|
|
6595
7608
|
bar.style.display = 'flex';
|
|
6596
7609
|
bar.innerHTML = '<span id="odt-chat-agent-lbl">AGENT</span>';
|
|
6597
7610
|
['all', ...agents].forEach(name => {
|
|
6598
7611
|
const pill = document.createElement('button');
|
|
6599
|
-
|
|
6600
|
-
|
|
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);
|
|
6601
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`);
|
|
6602
7618
|
pill.onclick = () => odtChatSelectAgent(name);
|
|
6603
7619
|
bar.appendChild(pill);
|
|
6604
7620
|
});
|
|
@@ -6616,11 +7632,31 @@ window.odtChatSelectAgent = function(name) {
|
|
|
6616
7632
|
if (!sess) { if (emptyEl) emptyEl.style.display = 'block'; return; }
|
|
6617
7633
|
const visible = (sess.events || []).filter(ev => _odtChatAgentMatches(ev));
|
|
6618
7634
|
if (!visible.length) {
|
|
6619
|
-
if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = name === 'all' ? 'No events.' :
|
|
7635
|
+
if (emptyEl) { emptyEl.style.display = 'block'; emptyEl.textContent = name === 'all' ? 'No events.' : `${name} was online in this run but sent no messages.`; }
|
|
6620
7636
|
} else {
|
|
6621
7637
|
if (emptyEl) emptyEl.style.display = 'none';
|
|
6622
7638
|
visible.forEach(ev => _odtAppendEvent(ev, false));
|
|
6623
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
|
+
}
|
|
6624
7660
|
feed.scrollTop = feed.scrollHeight;
|
|
6625
7661
|
};
|
|
6626
7662
|
|
|
@@ -6628,6 +7664,10 @@ function _odtAppendEvent(ev, animate) {
|
|
|
6628
7664
|
if (!_odtChatAgentMatches(ev)) return;
|
|
6629
7665
|
const feed = document.getElementById('odt-chat-feed');
|
|
6630
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);
|
|
6631
7671
|
const emptyEl = document.getElementById('odt-chat-empty');
|
|
6632
7672
|
if (emptyEl) emptyEl.style.display = 'none';
|
|
6633
7673
|
const ts = ev.ts ? new Date(ev.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '';
|
|
@@ -6635,22 +7675,24 @@ function _odtAppendEvent(ev, animate) {
|
|
|
6635
7675
|
if (ev.type === 'run:start') {
|
|
6636
7676
|
el = mkCVSys('▶ Run started' + (ev.goal ? ': ' + esc(ev.goal.slice(0,80)) : '') + (ev.bossRole ? ' · boss: ' + esc(ev.bossRole) : ''), ts);
|
|
6637
7677
|
} else if (ev.type === 'run:cycle:complete') {
|
|
6638
|
-
el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + ev.pending + ' tasks pending' : ''), ts);
|
|
7678
|
+
el = mkCVSys('◎ Cycle complete' + (ev.pending != null ? ' · ' + esc(String(ev.pending)) + ' tasks pending' : ''), ts);
|
|
6639
7679
|
} else if (ev.type === 'run:complete') {
|
|
6640
7680
|
el = mkCVSys('■ Run complete' + (ev.status ? ' [' + esc(ev.status) + ']' : ''), ts);
|
|
6641
7681
|
} else if (ev.type === 'org:comms') {
|
|
6642
|
-
|
|
6643
|
-
|
|
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');
|
|
6644
7686
|
} else if (ev.type === 'org:agent:online') {
|
|
6645
7687
|
el = mkCVSys('◉ ' + esc(ev.title || ev.role || '?') + ' online', ts);
|
|
6646
7688
|
} else if (ev.type === 'org:checkpoint') {
|
|
6647
|
-
el = mkCVAgent('boss', ev.progress || '', ts, 'org:checkpoint');
|
|
7689
|
+
el = mkCVAgent('boss', ev.summary || ev.progress || ev.msg || '', ts, 'org:checkpoint');
|
|
6648
7690
|
} else if (ev.type === 'org:start' || ev.type === 'org:complete') {
|
|
6649
7691
|
el = mkCVSys(ev.type === 'org:start' ? '▶ Org started' : '■ Org complete', ts);
|
|
6650
7692
|
} else if (ev.type === 'agent:spawn') {
|
|
6651
7693
|
el = mkCVSpawn(ev.from || 'orchestrator', ev.to || '?', ev.task || ev.briefing || '', ts);
|
|
6652
7694
|
} else if (ev.type === 'agent:result') {
|
|
6653
|
-
el = mkCVResult(ev.agent || '?', ev.msg || '', ts);
|
|
7695
|
+
el = mkCVResult(ev.agent || ev.from || '?', ev.result || ev.msg || ev.message || ev.summary || '', ts);
|
|
6654
7696
|
} else if (ev.type === 'file:write') {
|
|
6655
7697
|
el = mkCVFileCard(ev.name || ev.path || '?', ev.path || '', ev.size || 0, ts);
|
|
6656
7698
|
} else if (ev.type === 'intercom') {
|
|
@@ -6669,12 +7711,32 @@ function _odtAppendEvent(ev, animate) {
|
|
|
6669
7711
|
el = mkCVSys('Loop: ' + esc(ev.command || ''), ts);
|
|
6670
7712
|
} else if (ev.type === 'loop:complete') {
|
|
6671
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);
|
|
6672
7734
|
} else {
|
|
6673
|
-
|
|
7735
|
+
el = mkCVSys(esc(ev.type || 'event'), ts);
|
|
6674
7736
|
}
|
|
6675
7737
|
if (animate) el.classList.add('cv-new');
|
|
6676
7738
|
feed.appendChild(el);
|
|
6677
|
-
feed.scrollTop = feed.scrollHeight;
|
|
7739
|
+
if (atBottom) feed.scrollTop = feed.scrollHeight;
|
|
6678
7740
|
}
|
|
6679
7741
|
|
|
6680
7742
|
function _odtConnectChatSSE() {
|
|
@@ -6689,46 +7751,98 @@ function _odtConnectChatSSE() {
|
|
|
6689
7751
|
_odtChatSseSource.onerror = () => {
|
|
6690
7752
|
dot?.classList.remove('on');
|
|
6691
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;
|
|
6692
7758
|
_odtChatSseSource = null;
|
|
7759
|
+
if (_src) _src.close();
|
|
6693
7760
|
setTimeout(_odtConnectChatSSE, 5000);
|
|
6694
7761
|
};
|
|
6695
7762
|
}
|
|
6696
7763
|
|
|
6697
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
|
+
}
|
|
6698
7776
|
// Route org run events (have runId + org) to the active run session
|
|
6699
7777
|
if (ev?.org && ev?.runId && ev.org === _v2SelOrg) {
|
|
6700
7778
|
let runSess = _odtChatSessions.find(s => s.id === ev.runId);
|
|
6701
|
-
if (!runSess
|
|
6702
|
-
|
|
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] };
|
|
6703
7785
|
_odtChatSessions.unshift(runSess);
|
|
7786
|
+
// Always update the dropdown so the run is selectable even when opened mid-run
|
|
6704
7787
|
_odtPopulateChatSel();
|
|
6705
|
-
if (
|
|
6706
|
-
|
|
6707
|
-
|
|
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
|
+
}
|
|
6708
7799
|
}
|
|
6709
7800
|
} else if (runSess) {
|
|
6710
7801
|
(runSess.events = runSess.events || []).push(ev);
|
|
6711
7802
|
if (runSess._runMeta) runSess._runMeta.eventCount = (runSess._runMeta.eventCount || 0) + 1;
|
|
6712
|
-
if (ev.type === 'run:cycle:complete') { if (runSess._runMeta) runSess._runMeta.cycleCount = (runSess._runMeta.cycleCount || 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
|
+
}
|
|
6713
7809
|
if (ev.type === 'run:complete' || ev.type === 'org:complete') { runSess.status = 'complete'; _odtPopulateChatSel(); }
|
|
6714
|
-
if
|
|
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 : []);
|
|
6715
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
|
+
}
|
|
6716
7826
|
}
|
|
6717
7827
|
}
|
|
6718
7828
|
return;
|
|
6719
7829
|
}
|
|
6720
7830
|
|
|
6721
7831
|
if (!ev?.session) return;
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
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).
|
|
6725
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
|
+
}
|
|
6726
7840
|
if (!sess && ev.type === 'session:start') {
|
|
6727
7841
|
sess = { id: ev.session, ts: ev.ts || Date.now(), prompt: ev.prompt || '', status: 'running', events: [ev] };
|
|
6728
7842
|
_odtChatSessions.unshift(sess);
|
|
6729
7843
|
_odtPopulateChatSel();
|
|
6730
|
-
// Auto-select if nothing selected
|
|
6731
|
-
if (!_odtChatCurrentId
|
|
7844
|
+
// Auto-select if nothing selected (tab-guard removed: feed is in DOM regardless of active tab)
|
|
7845
|
+
if (!_odtChatCurrentId) {
|
|
6732
7846
|
const sel = document.getElementById('odt-chat-sel');
|
|
6733
7847
|
if (sel) { sel.value = ev.session; odtChatSelectSession(ev.session); }
|
|
6734
7848
|
}
|
|
@@ -6736,14 +7850,23 @@ function _odtHandleLiveEvent(ev) {
|
|
|
6736
7850
|
}
|
|
6737
7851
|
if (sess) {
|
|
6738
7852
|
(sess.events = sess.events || []).push(ev);
|
|
6739
|
-
if (ev.type === 'session:complete') { sess.status =
|
|
6740
|
-
if
|
|
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.
|
|
6741
7861
|
const hasNewAgent = (ev.agent && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.agent)}"]`))
|
|
6742
7862
|
|| (ev.from && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.from)}"]`))
|
|
6743
7863
|
|| (ev.to && !document.querySelector(`#odt-chat-agent-bar [data-agent="${CSS.escape(ev.to)}"]`));
|
|
6744
7864
|
if (hasNewAgent) _odtPopulateAgentBar(sess.events, Array.isArray(_v2OrgData?.roles) ? _v2OrgData.roles : []);
|
|
6745
7865
|
if (!sess._isJsonl) renderCvExcerptBanner('odt-chat-excerpt', sess.events, sess);
|
|
6746
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';
|
|
6747
7870
|
}
|
|
6748
7871
|
}
|
|
6749
7872
|
}
|
|
@@ -6792,8 +7915,14 @@ function v2RenderOrgConfig() {
|
|
|
6792
7915
|
async function v2SaveOrgConfig() {
|
|
6793
7916
|
if (!_v2SelOrg || !_v2OrgData) return;
|
|
6794
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
|
+
);
|
|
6795
7924
|
const cfg = {
|
|
6796
|
-
...
|
|
7925
|
+
..._baseConfig,
|
|
6797
7926
|
status: (g('oc-status')?.value) || _v2OrgData.status,
|
|
6798
7927
|
goal: (g('oc-goal')?.value) || _v2OrgData.goal,
|
|
6799
7928
|
mode: (g('oc-mode')?.value) || _v2OrgData.mode,
|
|
@@ -6833,11 +7962,13 @@ async function v2SaveOrgConfig() {
|
|
|
6833
7962
|
async function v2RenderOrgFiles() {
|
|
6834
7963
|
const el = document.getElementById('odt-files');
|
|
6835
7964
|
if (!el || !_v2SelOrg) return;
|
|
7965
|
+
const _rendOrg = _v2SelOrg;
|
|
6836
7966
|
el.innerHTML = '<div style="padding:20px;color:var(--text-lo)">Loading files...</div>';
|
|
6837
7967
|
try {
|
|
6838
|
-
const enc = encodeURIComponent(
|
|
7968
|
+
const enc = encodeURIComponent(_rendOrg);
|
|
6839
7969
|
const qs = (typeof DIR !== 'undefined' && DIR) ? '?dir='+encodeURIComponent(DIR) : '';
|
|
6840
7970
|
const files = await fetch('/api/org/'+enc+'/files'+qs).then(r => r.ok ? r.json() : []).catch(() => []);
|
|
7971
|
+
if (_v2SelOrg !== _rendOrg) return;
|
|
6841
7972
|
if (!files.length) { el.innerHTML = '<div style="padding:20px;color:var(--text-lo)">No files found.</div>'; return; }
|
|
6842
7973
|
const fmtSize = b => b < 1024 ? b+'B' : b < 1048576 ? (b/1024).toFixed(1)+'K' : (b/1048576).toFixed(1)+'M';
|
|
6843
7974
|
const fmtTime = s => { const d = new Date(s); return d.toLocaleDateString([],{month:'short',day:'numeric'})+' '+d.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}); };
|
|
@@ -6966,10 +8097,12 @@ function _ofSaveFile(defaultName) {
|
|
|
6966
8097
|
async function v2RenderOrgWorkspaces() {
|
|
6967
8098
|
const el = document.getElementById('odt-workspaces');
|
|
6968
8099
|
if (!el || !_v2SelOrg) return;
|
|
8100
|
+
const _rendOrg = _v2SelOrg;
|
|
6969
8101
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6970
8102
|
try {
|
|
6971
|
-
const _enc = encodeURIComponent(
|
|
8103
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6972
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;
|
|
6973
8106
|
const ws = Array.isArray(data) ? data : (data.workspaces || []);
|
|
6974
8107
|
if (!ws.length) { el.innerHTML = '<div class="empty">No workspaces configured</div>'; return; }
|
|
6975
8108
|
el.innerHTML = ws.map(w => `<div style="padding:10px 0;border-bottom:1px solid var(--border)">
|
|
@@ -6987,10 +8120,12 @@ async function v2RenderOrgWorkspaces() {
|
|
|
6987
8120
|
async function v2RenderOrgInvites() {
|
|
6988
8121
|
const el = document.getElementById('odt-invites');
|
|
6989
8122
|
if (!el || !_v2SelOrg) return;
|
|
8123
|
+
const _rendOrg = _v2SelOrg;
|
|
6990
8124
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
6991
8125
|
try {
|
|
6992
|
-
const _enc = encodeURIComponent(
|
|
8126
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
6993
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;
|
|
6994
8129
|
const invites = Array.isArray(data) ? data : (data.invites || []);
|
|
6995
8130
|
if (!invites.length) { el.innerHTML = '<div class="empty">No active invite tokens</div>'; return; }
|
|
6996
8131
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -7019,12 +8154,12 @@ function v2RenderOrgAgentsFull() {
|
|
|
7019
8154
|
</tr></thead>
|
|
7020
8155
|
<tbody>${agents.map(a => `<tr style="border-top:1px solid var(--border)">
|
|
7021
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>
|
|
7022
|
-
<td style="padding:5px 8px;color:var(--text-hi)">${esc(a.type||a.title||'—')}</td>
|
|
7023
|
-
<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>
|
|
7024
8159
|
<td style="padding:5px 8px"><span class="ss-pill ${(a.status==='running'||a.running)?'on':''}">${esc(a.status||'idle')}</span></td>
|
|
7025
8160
|
<td style="padding:5px 8px;color:var(--text-lo)">${a.lastHeartbeat ? relTime(a.lastHeartbeat) : '—'}</td>
|
|
7026
|
-
<td style="padding:5px 8px;color:var(--text-lo)">${a.tokensIn != null ? Number(a.tokensIn).toLocaleString() : '—'}</td>
|
|
7027
|
-
<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>
|
|
7028
8163
|
</tr>`).join('')}</tbody>
|
|
7029
8164
|
</table>`;
|
|
7030
8165
|
}
|
|
@@ -7033,10 +8168,12 @@ function v2RenderOrgAgentsFull() {
|
|
|
7033
8168
|
async function v2RenderOrgEnvironments() {
|
|
7034
8169
|
const el = document.getElementById('odt-environments');
|
|
7035
8170
|
if (!el || !_v2SelOrg) return;
|
|
8171
|
+
const _rendOrg = _v2SelOrg;
|
|
7036
8172
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
7037
8173
|
try {
|
|
7038
|
-
const _enc = encodeURIComponent(
|
|
8174
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
7039
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;
|
|
7040
8177
|
const envs = Array.isArray(data) ? data : (data.environments || []);
|
|
7041
8178
|
if (!envs.length) { el.innerHTML = '<div class="empty">No environments configured</div>'; return; }
|
|
7042
8179
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -7090,7 +8227,7 @@ function v2RenderOrgIssuesFull() {
|
|
|
7090
8227
|
<td style="padding:5px 4px">${PRIORITY[i.priority]||'·'}</td>
|
|
7091
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>
|
|
7092
8229
|
<td style="padding:5px 8px;color:var(--text-lo)">${esc(i.assignee||'—')}</td>
|
|
7093
|
-
<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>
|
|
7094
8231
|
<td style="padding:5px 8px;color:var(--text-xs);font-family:var(--mono);font-size:11px">${relTime(i.updated_at||i.ts)}</td>
|
|
7095
8232
|
</tr>`).join('')}</tbody>
|
|
7096
8233
|
</table>`;
|
|
@@ -7100,10 +8237,12 @@ function v2RenderOrgIssuesFull() {
|
|
|
7100
8237
|
async function v2RenderOrgJoinRequests() {
|
|
7101
8238
|
const el = document.getElementById('odt-join-requests');
|
|
7102
8239
|
if (!el || !_v2SelOrg) return;
|
|
8240
|
+
const _rendOrg = _v2SelOrg;
|
|
7103
8241
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
7104
8242
|
try {
|
|
7105
|
-
const _enc = encodeURIComponent(
|
|
8243
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
7106
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;
|
|
7107
8246
|
const reqs = Array.isArray(data) ? data : (data.joinRequests || data.join_requests || []);
|
|
7108
8247
|
if (!reqs.length) { el.innerHTML = '<div class="empty">No pending join requests</div>'; return; }
|
|
7109
8248
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -7124,10 +8263,12 @@ async function v2RenderOrgJoinRequests() {
|
|
|
7124
8263
|
async function v2RenderOrgThreads() {
|
|
7125
8264
|
const el = document.getElementById('odt-threads');
|
|
7126
8265
|
if (!el || !_v2SelOrg) return;
|
|
8266
|
+
const _rendOrg = _v2SelOrg;
|
|
7127
8267
|
el.innerHTML = '<div class="loading-txt">Loading…</div>';
|
|
7128
8268
|
try {
|
|
7129
|
-
const _enc = encodeURIComponent(
|
|
8269
|
+
const _enc = encodeURIComponent(_rendOrg);
|
|
7130
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;
|
|
7131
8272
|
const threads = Array.isArray(data) ? data : (data.threads || []);
|
|
7132
8273
|
if (!threads.length) { el.innerHTML = '<div class="empty">No threads found</div>'; return; }
|
|
7133
8274
|
el.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
@@ -7149,28 +8290,123 @@ async function v2RenderOrgThreads() {
|
|
|
7149
8290
|
window.v2StopOrg = async function() {
|
|
7150
8291
|
if (!_v2SelOrg) return;
|
|
7151
8292
|
const stopped = _v2SelOrg;
|
|
7152
|
-
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(_) {}
|
|
7153
8294
|
setTimeout(async () => { await renderOrgs(); if (_v2SelOrg === stopped) v2SelectOrg(stopped); }, 600);
|
|
7154
8295
|
};
|
|
7155
8296
|
|
|
7156
|
-
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() {
|
|
7157
8320
|
const d = document.getElementById('org-copy-dialog');
|
|
7158
|
-
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);
|
|
7159
8345
|
};
|
|
7160
8346
|
window.v2DoCopyOrg = async function() {
|
|
7161
8347
|
const dest = document.getElementById('org-copy-dest')?.value.trim();
|
|
7162
|
-
|
|
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; }
|
|
7163
8351
|
if (!_v2SelOrg) return;
|
|
8352
|
+
confirmBtn.disabled = true;
|
|
8353
|
+
if (statusEl) { statusEl.textContent = 'Copying…'; statusEl.style.color = 'var(--text-lo)'; }
|
|
7164
8354
|
try {
|
|
7165
|
-
const r = await fetch('/api/
|
|
8355
|
+
const r = await fetch('/api/orgs/' + enc(_v2SelOrg) + '/copy?dir=' + enc(DIR), {
|
|
7166
8356
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
7167
8357
|
body: JSON.stringify({ destination: dest }),
|
|
7168
8358
|
});
|
|
7169
|
-
|
|
7170
|
-
|
|
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');
|
|
7171
8362
|
document.getElementById('org-copy-dialog').style.display = 'none';
|
|
7172
|
-
} catch (e) {
|
|
8363
|
+
} catch (e) {
|
|
8364
|
+
if (statusEl) { statusEl.textContent = e.message; statusEl.style.color = 'var(--red)'; }
|
|
8365
|
+
confirmBtn.disabled = false;
|
|
8366
|
+
}
|
|
7173
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();
|
|
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
|
+
}
|
|
7174
8410
|
|
|
7175
8411
|
function filterLoopList(q) {
|
|
7176
8412
|
const query = q.trim().toLowerCase();
|
|
@@ -7192,13 +8428,23 @@ function filterOrgList(q) {
|
|
|
7192
8428
|
// live SSE for org events
|
|
7193
8429
|
(function v2OrgSSE() {
|
|
7194
8430
|
let src;
|
|
8431
|
+
let seenKeys = new Set();
|
|
8432
|
+
let reconnectTimer = null;
|
|
7195
8433
|
function connect() {
|
|
8434
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
7196
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.
|
|
7197
8438
|
src = new EventSource('/api/mastermind-stream');
|
|
7198
8439
|
src.onmessage = e => {
|
|
7199
8440
|
try {
|
|
7200
8441
|
const ev = JSON.parse(e.data);
|
|
7201
|
-
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)); }
|
|
7202
8448
|
const n = ev.org;
|
|
7203
8449
|
// Filter by project dir if the event carries one; skip events from other projects.
|
|
7204
8450
|
// Events without a project field are treated as belonging to the current project.
|
|
@@ -7208,7 +8454,63 @@ function filterOrgList(q) {
|
|
|
7208
8454
|
_v2OrgEventLog[n].push(ev);
|
|
7209
8455
|
if (_v2OrgEventLog[n].length > 50) _v2OrgEventLog[n].shift();
|
|
7210
8456
|
if (n === _v2SelOrg && _v2OrgTab === 'activity') v2RenderOrgActivity();
|
|
7211
|
-
|
|
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') {
|
|
7212
8514
|
setTimeout(async () => {
|
|
7213
8515
|
await renderOrgs();
|
|
7214
8516
|
if (_v2SelOrg === n) {
|
|
@@ -7223,14 +8525,102 @@ function filterOrgList(q) {
|
|
|
7223
8525
|
const listOrg = _v2Orgs.find(o => o.name === _v2SelOrg) || {};
|
|
7224
8526
|
if (_v2OrgData) _v2OrgData.running = listOrg.running;
|
|
7225
8527
|
v2UpdateOrgHeader(listOrg, _v2OrgData);
|
|
7226
|
-
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
|
+
}
|
|
7227
8593
|
}
|
|
7228
8594
|
}
|
|
7229
8595
|
}, 400);
|
|
7230
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
|
+
}
|
|
7231
8615
|
} catch(_) {}
|
|
7232
8616
|
};
|
|
7233
|
-
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
|
+
};
|
|
7234
8624
|
}
|
|
7235
8625
|
connect();
|
|
7236
8626
|
})();
|
|
@@ -10247,8 +11637,8 @@ function selectMem(filename) {
|
|
|
10247
11637
|
const actions = (f.readonly || f.source === 'backend')
|
|
10248
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>'
|
|
10249
11639
|
: '<div class="mem-actions">' +
|
|
10250
|
-
'<button class="btn"
|
|
10251
|
-
'<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>' +
|
|
10252
11642
|
'</div>';
|
|
10253
11643
|
detail.innerHTML =
|
|
10254
11644
|
'<span class="mem-badge" style="background:' + col + '22;color:' + col + '">' + esc(f.type || '?') + '</span>' + srcBadge +
|
|
@@ -10802,7 +12192,7 @@ function mmRenderOrgs(body) {
|
|
|
10802
12192
|
if (!orgs.length) { body.innerHTML = '<div class="empty">No orgs found. Run /mastermind:createorg to create one.</div>'; return; }
|
|
10803
12193
|
body.innerHTML = orgs.map(o => {
|
|
10804
12194
|
const running = o.running;
|
|
10805
|
-
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')">
|
|
10806
12196
|
<span class="mm-skill-name">${esc(o.name)}</span>
|
|
10807
12197
|
<span class="mm-skill-desc">${esc((o.goal || '').slice(0, 60))} ${running ? '⬤ LIVE' : ''}</span>
|
|
10808
12198
|
</div>`;
|
|
@@ -10814,7 +12204,7 @@ function mmRenderSkills(body) {
|
|
|
10814
12204
|
const q = _mmSkillFilter.toLowerCase();
|
|
10815
12205
|
const filtered = q ? _MM_SKILLS_CATALOG.filter(s => s.name.toLowerCase().includes(q) || s.desc.toLowerCase().includes(q)) : _MM_SKILLS_CATALOG;
|
|
10816
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>` +
|
|
10817
|
-
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'))">
|
|
10818
12208
|
<span class="mm-skill-name">${esc(s.name)}</span>
|
|
10819
12209
|
<span class="mm-skill-desc">${esc(s.desc)}</span>
|
|
10820
12210
|
</div>`).join('');
|
|
@@ -10829,7 +12219,7 @@ async function mmRenderLoops(body) {
|
|
|
10829
12219
|
body.innerHTML = loops.map(l => {
|
|
10830
12220
|
const maxReps = l.maxReps || 0;
|
|
10831
12221
|
const curRep = l.currentRep || 0;
|
|
10832
|
-
const isTillend = !maxReps || l.
|
|
12222
|
+
const isTillend = !maxReps || l.type === 'tillend' || String(l.command || '').includes('--tillend');
|
|
10833
12223
|
const nextAt = l.nextRunAt ? parseInt(l.nextRunAt, 10) : 0;
|
|
10834
12224
|
const isExplicitlyActive = l.status === 'running' || l.status === 'waiting' || l.status === 'active';
|
|
10835
12225
|
const isOverdue = !isExplicitlyActive && nextAt > 0 && nextAt <= Date.now();
|
|
@@ -10847,7 +12237,7 @@ async function mmRenderLoops(body) {
|
|
|
10847
12237
|
<span style="font-size:13px;color:var(--text-hi);flex:1">${esc(name)}</span>
|
|
10848
12238
|
${isHil ? '<span style="color:oklch(78% 0.18 80);font-size:10px">⚠ HIL</span>' : ''}
|
|
10849
12239
|
<span class="ss-pill ${running ? 'on' : ''}">${running ? 'active' : isFinished ? 'done' : 'stopped'}</span>
|
|
10850
|
-
${(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>` : ''}
|
|
10851
12241
|
</div>
|
|
10852
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>
|
|
10853
12243
|
</div>`;
|