@monoes/monomindcli 1.6.0 → 1.6.3

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.
@@ -145,18 +145,14 @@
145
145
  /* Row 0: all projects (full width) */
146
146
  #panel-projects { grid-column: 1 / 5; grid-row: 1; min-height: 200px; }
147
147
 
148
- /* Row 1: agents | agents | tokens | tokens */
149
- #panel-agents { grid-column: 1 / 3; grid-row: 2; }
150
- #panel-tokens { grid-column: 3 / 5; grid-row: 2; }
148
+ /* Row 1: tokens (full) */
149
+ #panel-tokens { grid-column: 1 / 5; grid-row: 2; }
151
150
 
152
- /* Row 2: memory (2 cols) | hooks (2 cols) */
153
- #panel-memory { grid-column: 1 / 3; grid-row: 3; }
154
- #panel-hooks { grid-column: 3 / 5; grid-row: 3; }
151
+ /* Row 2: loops (full width) */
152
+ #panel-loops { grid-column: 1 / 5; grid-row: 3; }
155
153
 
156
- /* Row 3: metrics (2 cols) | activity | system */
157
- #panel-metrics { grid-column: 1 / 3; grid-row: 4; }
158
- #panel-activity { grid-column: 3; grid-row: 4; }
159
- #panel-system { grid-column: 4; grid-row: 4; }
154
+ /* Row 3: activity (full width) */
155
+ #panel-activity { grid-column: 1 / 5; grid-row: 4; }
160
156
 
161
157
  /* All grid children must not overflow their columns */
162
158
  #grid > * { min-width: 0; }
@@ -306,11 +302,7 @@
306
302
  }
307
303
 
308
304
  /* Override display for specific panels that need block */
309
- #panel-memory.open .panel-body,
310
- #panel-hooks.open .panel-body { display: block; }
311
- #panel-knowledge.open .panel-body,
312
- #panel-metrics.open .panel-body,
313
- #panel-system.open .panel-body { display: block; }
305
+ #panel-knowledge.open .panel-body { display: block; }
314
306
 
315
307
  /* Graphify panel inner layout */
316
308
  .graphify-layout { display: grid; grid-template-columns: 220px 1fr; gap: 12px; }
@@ -525,27 +517,28 @@
525
517
  }
526
518
 
527
519
  /* ─── TOKENS PANEL ──────────────────────────────────────────────── */
520
+ #panel-tokens.open .panel-body { min-height: 0; padding: 6px 10px; }
528
521
  .token-summary {
529
522
  display: grid;
530
523
  grid-template-columns: 1fr 1fr 1fr 1fr;
531
- gap: 5px;
524
+ gap: 4px;
532
525
  }
533
526
  .token-stat {
534
527
  background: rgba(0, 0, 0, 0.3);
535
528
  border: 1px solid var(--border);
536
529
  border-radius: 4px;
537
- padding: 5px 6px;
530
+ padding: 4px 6px;
538
531
  text-align: center;
539
532
  }
540
533
  .token-stat-label {
541
- font-size: 9px;
534
+ font-size: 8px;
542
535
  color: var(--muted);
543
536
  letter-spacing: 0.08em;
544
537
  text-transform: uppercase;
545
- margin-bottom: 3px;
538
+ margin-bottom: 2px;
546
539
  }
547
540
  .token-stat-val {
548
- font-size: 12px;
541
+ font-size: 11px;
549
542
  font-weight: 500;
550
543
  color: var(--teal);
551
544
  }
@@ -783,6 +776,133 @@
783
776
  #po-chunk-edit-modal { display: none; position: absolute; inset: 0; background: rgba(0,0,0,0.65); z-index: 22; align-items: center; justify-content: center; }
784
777
  #po-chunk-edit-modal.open { display: flex; }
785
778
 
779
+ /* ─── LOOPS PANEL ──────────────────────────────────────────────── */
780
+ #loops-body { display: flex; flex-direction: column; gap: 8px; }
781
+ .loop-card {
782
+ background: rgba(0,0,0,0.3);
783
+ border: 1px solid var(--border);
784
+ border-radius: 4px;
785
+ padding: 10px 12px;
786
+ display: grid;
787
+ grid-template-columns: auto 1fr auto;
788
+ grid-template-rows: auto auto auto;
789
+ gap: 4px 10px;
790
+ transition: border-color 0.15s;
791
+ position: relative;
792
+ overflow: hidden;
793
+ }
794
+ .loop-card::before {
795
+ content: '';
796
+ position: absolute;
797
+ left: 0; top: 0; bottom: 0;
798
+ width: 3px;
799
+ border-radius: 4px 0 0 4px;
800
+ }
801
+ .loop-card.lc-repeat::before { background: var(--teal); }
802
+ .loop-card.lc-do::before { background: var(--amber); }
803
+ .loop-card.lc-stop-pending { opacity: 0.55; }
804
+ .loop-card:hover { border-color: rgba(255,255,255,0.15); }
805
+ .loop-type-pill {
806
+ grid-row: 1;
807
+ grid-column: 1;
808
+ font-size: 8px;
809
+ font-weight: 700;
810
+ letter-spacing: 0.12em;
811
+ padding: 2px 5px;
812
+ border-radius: 2px;
813
+ align-self: start;
814
+ }
815
+ .loop-type-pill.lp-repeat { background: rgba(0,229,200,0.12); color: var(--teal); border: 1px solid rgba(0,229,200,0.25); }
816
+ .loop-type-pill.lp-do { background: rgba(255,179,0,0.12); color: var(--amber); border: 1px solid rgba(255,179,0,0.25); }
817
+ .loop-prompt {
818
+ grid-row: 1;
819
+ grid-column: 2;
820
+ font-size: 10px;
821
+ color: var(--text);
822
+ font-weight: 600;
823
+ white-space: nowrap;
824
+ overflow: hidden;
825
+ text-overflow: ellipsis;
826
+ align-self: center;
827
+ }
828
+ .loop-stop-btn {
829
+ grid-row: 1 / 4;
830
+ grid-column: 3;
831
+ align-self: center;
832
+ background: rgba(220,60,60,0.08);
833
+ border: 1px solid rgba(220,60,60,0.3);
834
+ color: rgba(220,100,100,0.9);
835
+ font-family: 'Azeret Mono', monospace;
836
+ font-size: 8px;
837
+ font-weight: 700;
838
+ letter-spacing: 0.1em;
839
+ padding: 5px 8px;
840
+ cursor: pointer;
841
+ border-radius: 3px;
842
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
843
+ white-space: nowrap;
844
+ }
845
+ .loop-stop-btn:hover { background: rgba(220,60,60,0.2); border-color: rgba(220,80,80,0.6); color: #f88; }
846
+ .loop-stop-btn:disabled { opacity: 0.35; cursor: not-allowed; }
847
+ .loop-meta {
848
+ grid-row: 2;
849
+ grid-column: 1 / 3;
850
+ display: flex;
851
+ gap: 12px;
852
+ align-items: center;
853
+ }
854
+ .loop-meta-item { font-size: 9px; color: var(--muted); }
855
+ .loop-meta-item span { color: var(--dim); }
856
+ .loop-progress-row {
857
+ grid-row: 3;
858
+ grid-column: 1 / 3;
859
+ display: flex;
860
+ align-items: center;
861
+ gap: 8px;
862
+ }
863
+ .loop-progress-bar {
864
+ flex: 1;
865
+ height: 2px;
866
+ background: rgba(255,255,255,0.06);
867
+ border-radius: 1px;
868
+ overflow: hidden;
869
+ }
870
+ .loop-progress-fill {
871
+ height: 100%;
872
+ border-radius: 1px;
873
+ transition: width 0.4s ease;
874
+ }
875
+ .lc-repeat .loop-progress-fill { background: var(--teal); }
876
+ .lc-do .loop-progress-fill { background: var(--amber); }
877
+ .loop-countdown {
878
+ font-size: 9px;
879
+ color: var(--dim);
880
+ white-space: nowrap;
881
+ min-width: 70px;
882
+ text-align: right;
883
+ }
884
+ .loop-task-label {
885
+ grid-row: 2;
886
+ grid-column: 1 / 3;
887
+ font-size: 9px;
888
+ color: var(--muted);
889
+ white-space: nowrap;
890
+ overflow: hidden;
891
+ text-overflow: ellipsis;
892
+ }
893
+ .loop-status-dot {
894
+ display: inline-block;
895
+ width: 6px;
896
+ height: 6px;
897
+ border-radius: 50%;
898
+ margin-right: 4px;
899
+ vertical-align: middle;
900
+ }
901
+ .loop-status-dot.running { background: var(--teal); box-shadow: 0 0 5px var(--teal); animation: pulse 1.6s ease-in-out infinite; }
902
+ .loop-status-dot.waiting { background: var(--amber); box-shadow: 0 0 4px var(--amber); animation: pulse 2.5s ease-in-out infinite; }
903
+ .loop-status-dot.stopped { background: rgba(220,60,60,0.7); }
904
+ .loops-empty { font-size: 10px; color: var(--dim); text-align: center; padding: 18px 0; letter-spacing: 0.08em; }
905
+
786
906
  /* ─── METRICS PANEL ─────────────────────────────────────────────── */
787
907
  .metric-block {
788
908
  background: rgba(0,0,0,0.25);
@@ -812,30 +932,41 @@
812
932
  .security-warn { color: var(--amber); }
813
933
  .security-fail { color: var(--red); }
814
934
 
815
- /* ─── ACTIVITY PANEL ────────────────────────────────────────────── */
816
- #activity-log {
817
- display: flex;
818
- flex-direction: column;
819
- gap: 3px;
935
+ /* ─── SESSION JOURNAL ────────────────────────────────────────────── */
936
+ #journal-list { display: flex; flex-direction: column; gap: 1px; }
937
+ .jn-card {
938
+ border-left: 2px solid var(--border);
939
+ padding: 10px 12px;
940
+ cursor: pointer;
941
+ transition: border-color 0.12s, background 0.12s;
942
+ }
943
+ .jn-card:hover { border-left-color: var(--teal); background: rgba(0,229,200,0.03); }
944
+ .jn-card.open { border-left-color: var(--teal); background: rgba(0,229,200,0.04); }
945
+ .jn-header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; }
946
+ .jn-date { font-size: 9px; color: var(--teal); white-space: nowrap; font-family: 'Azeret Mono', monospace; }
947
+ .jn-id { font-size: 8px; color: var(--dim); font-family: 'Azeret Mono', monospace; }
948
+ .jn-stats { font-size: 8px; color: var(--muted); margin-left: auto; white-space: nowrap; }
949
+ .jn-prompt {
950
+ font-size: 10px; color: var(--text); font-style: italic;
951
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
952
+ margin-bottom: 4px;
820
953
  }
821
- .activity-entry {
822
- display: flex;
823
- align-items: flex-start;
824
- gap: 6px;
825
- font-size: 10px;
826
- line-height: 1.4;
827
- padding: 3px 5px;
828
- border-radius: 3px;
829
- border-left: 2px solid transparent;
830
- animation: fadeIn 0.2s ease-out;
954
+ .jn-excerpt { font-size: 9px; color: var(--muted); line-height: 1.5; }
955
+ .jn-summary-full {
956
+ display: none; margin-top: 8px; padding-top: 8px;
957
+ border-top: 1px solid var(--border);
958
+ font-size: 9px; color: var(--muted); line-height: 1.6;
959
+ white-space: pre-wrap; max-height: 320px; overflow-y: auto;
960
+ }
961
+ .jn-card.open .jn-summary-full { display: block; }
962
+ .jn-no-summary { font-size: 9px; color: var(--dim); font-style: italic; }
963
+ .jn-badge-compact {
964
+ font-size: 7px; letter-spacing: 0.06em;
965
+ background: rgba(0,229,200,0.1); color: var(--teal);
966
+ border: 1px solid rgba(0,229,200,0.2); border-radius: 2px;
967
+ padding: 1px 4px; flex-shrink: 0;
831
968
  }
832
- .activity-entry.type-session { border-left-color: var(--teal); background: rgba(0,229,200,0.03); }
833
- .activity-entry.type-swarm { border-left-color: var(--amber); background: rgba(255,183,0,0.03); }
834
- .activity-entry.type-agent { border-left-color: var(--green); background: rgba(0,229,135,0.03); }
835
- .activity-entry.type-error { border-left-color: var(--red); background: rgba(255,68,102,0.03); }
836
- .activity-entry.type-default { border-left-color: var(--muted); }
837
- .activity-time { color: var(--dim); white-space: nowrap; font-size: 9px; flex-shrink: 0; letter-spacing: 0.03em; }
838
- .activity-msg { color: var(--text); flex: 1; font-size: 10px; }
969
+ #journal-empty { padding: 20px; font-size: 10px; color: var(--dim); text-align: center; }
839
970
 
840
971
  /* ─── SYSTEM PANEL ──────────────────────────────────────────────── */
841
972
  .sys-row {
@@ -1160,15 +1291,165 @@
1160
1291
  .po-sd-sidebar-section { display: flex; flex-direction: column; gap: 4px; }
1161
1292
  .po-sd-sidebar-label { font-size: 9px; color: var(--muted); letter-spacing: 0.1em; margin-bottom: 2px; }
1162
1293
 
1163
- /* Graph pane */
1164
- #po-graph-tab { flex-direction: column; }
1165
- #po-graph-controls {
1166
- padding: 6px 12px; border-bottom: 1px solid var(--border);
1167
- display: flex; align-items: center; gap: 16px; flex-shrink: 0;
1168
- font-size: 10px; color: var(--muted); letter-spacing: 0.08em;
1294
+ /* Swarm pane */
1295
+ #po-swarm-tab { flex-direction: row; }
1296
+ #po-swarm-list {
1297
+ padding: 6px;
1298
+ }
1299
+ .po-swarm-item {
1300
+ display: flex; flex-direction: column; gap: 2px;
1301
+ padding: 7px 10px; border-radius: 4px; cursor: pointer; margin-bottom: 3px;
1302
+ border: 1px solid transparent; transition: all 0.12s;
1303
+ }
1304
+ .po-swarm-item:hover { background: rgba(0,229,200,0.04); border-color: var(--border); }
1305
+ .po-swarm-item.selected { background: rgba(0,229,200,0.08); border-color: rgba(0,229,200,0.3); }
1306
+ .po-swarm-item-row { display: flex; align-items: center; gap: 6px; }
1307
+ .po-swarm-item-id { font-size: 10px; font-weight: 600; color: var(--text); font-family: 'Azeret Mono', monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
1308
+ .po-swarm-item-badge { font-size: 8px; padding: 1px 5px; border-radius: 2px; font-family: 'Azeret Mono', monospace; letter-spacing: 0.05em; }
1309
+ .po-swarm-item-badge.topo { background: rgba(0,229,200,0.12); color: var(--teal); }
1310
+ .po-swarm-item-badge.status-completed { background: rgba(76,175,80,0.15); color: #4caf50; }
1311
+ .po-swarm-item-badge.status-error { background: rgba(239,83,80,0.15); color: #EF5350; }
1312
+ .po-swarm-item-badge.status-terminated { background: rgba(255,180,0,0.15); color: #ffb400; }
1313
+ .po-swarm-item-badge.status-stopped { background: rgba(150,150,180,0.15); color: var(--muted); }
1314
+ .po-swarm-item-badge.status-running { background: rgba(76,175,80,0.15); color: #4caf50; }
1315
+ .po-swarm-item-meta { font-size: 9px; color: var(--muted); }
1316
+ #po-swarm-detail {
1317
+ flex: 1; overflow: hidden; display: flex; flex-direction: column;
1318
+ }
1319
+ #po-swarm-header {
1320
+ padding: 10px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0;
1321
+ display: flex; flex-direction: column; gap: 3px;
1169
1322
  }
1170
- #po-graph-controls label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
1171
- #po-kg-canvas { flex: 1; width: 100%; display: block; }
1323
+ #po-swarm-stats-bar {
1324
+ display: flex; gap: 12px; font-size: 10px; color: var(--muted); padding: 6px 16px;
1325
+ border-bottom: 1px solid var(--border); flex-shrink: 0;
1326
+ }
1327
+ #po-swarm-stats-bar span { display: flex; align-items: center; gap: 4px; }
1328
+ #po-swarm-body {
1329
+ flex: 1; overflow-y: auto; display: flex; flex-direction: column;
1330
+ }
1331
+ #po-swarm-canvas-wrap {
1332
+ position: relative; min-height: 200px; transition: min-height 0.25s ease;
1333
+ flex-shrink: 0;
1334
+ }
1335
+ #po-swarm-canvas-wrap.shrunk { min-height: 120px; }
1336
+ #po-swarm-topo-label {
1337
+ position: absolute; top: 8px; left: 14px; font-size: 9px;
1338
+ color: var(--muted); letter-spacing: 0.1em; z-index: 2;
1339
+ }
1340
+ #po-swarm-canvas { width: 100%; height: 100%; display: block; }
1341
+ #po-swarm-agents-section { padding: 10px 14px; border-top: 1px solid var(--border); }
1342
+ .po-swarm-agent-item {
1343
+ display: flex; align-items: center; gap: 8px;
1344
+ padding: 5px 8px; border-radius: 3px; cursor: pointer; margin-bottom: 2px;
1345
+ transition: background 0.12s;
1346
+ }
1347
+ .po-swarm-agent-item:hover { background: rgba(0,229,200,0.05); }
1348
+ .po-swarm-agent-item.selected { background: rgba(255,180,0,0.08); }
1349
+ .po-swarm-agent-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
1350
+ .po-swarm-agent-type { font-size: 10px; font-family: 'Azeret Mono', monospace; color: var(--text); width: 100px; }
1351
+ .po-swarm-agent-role { font-size: 8px; font-family: 'Azeret Mono', monospace; color: var(--muted); }
1352
+ .po-swarm-agent-stats { font-size: 9px; color: var(--muted); font-family: 'Azeret Mono', monospace; margin-left: auto; }
1353
+ #po-swarm-agent-drawer {
1354
+ border-top: 1px solid var(--border); flex-shrink: 0;
1355
+ max-height: 240px; overflow: hidden;
1356
+ transition: max-height 0.25s ease;
1357
+ }
1358
+ #po-swarm-agent-header {
1359
+ display: flex; align-items: center; padding: 8px 14px;
1360
+ border-bottom: 1px solid var(--border); gap: 12px;
1361
+ }
1362
+ #po-swarm-agent-info { flex: 1; font-size: 10px; font-family: 'Azeret Mono', monospace; display: flex; gap: 14px; }
1363
+ #po-swarm-agent-close { background: none; border: none; color: var(--muted); font-size: 14px; cursor: pointer; padding: 0 2px; }
1364
+ #po-swarm-agent-close:hover { color: var(--text); }
1365
+ #po-swarm-agent-timeline {
1366
+ overflow-y: auto; padding: 8px 14px; max-height: 190px;
1367
+ display: flex; flex-direction: column; gap: 3px;
1368
+ }
1369
+ .po-swarm-msg {
1370
+ padding: 3px 8px; font-size: 9px; font-family: 'Azeret Mono', monospace;
1371
+ color: var(--muted); border-radius: 0 3px 3px 0;
1372
+ }
1373
+ .po-swarm-msg.sent { border-left: 2px solid var(--teal); }
1374
+ .po-swarm-msg.received { border-left: 2px solid #4488cc; }
1375
+ .po-swarm-msg-ts { color: rgba(100,100,140,0.6); }
1376
+ .po-swarm-msg-type { color: var(--text); font-weight: 600; }
1377
+ .po-swarm-msg-payload { color: rgba(180,180,220,0.7); }
1378
+ .po-swarm-event-row {
1379
+ display: flex; align-items: baseline; gap: 6px;
1380
+ padding: 2px 0; font-size: 9px; font-family: 'Azeret Mono', monospace;
1381
+ border-bottom: 1px solid rgba(58,58,90,0.1);
1382
+ }
1383
+ .po-swarm-event-ts { color: rgba(100,100,140,0.6); min-width: 60px; }
1384
+ .po-swarm-event-kind { color: var(--teal); font-weight: 600; min-width: 130px; }
1385
+ .po-swarm-event-detail { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1386
+ #po-swarm-clean-btn:hover { background: rgba(239,83,80,0.2); }
1387
+
1388
+ /* Agent graph pane */
1389
+ #po-ag-sidebar {
1390
+ width: 190px; min-width: 190px; flex-shrink: 0;
1391
+ border-right: 1px solid var(--border);
1392
+ display: flex; flex-direction: column; overflow: hidden;
1393
+ background: rgba(0,0,0,0.12);
1394
+ }
1395
+ #po-ag-sidebar-hdr {
1396
+ padding: 8px 12px; border-bottom: 1px solid var(--border);
1397
+ font-size: 9px; color: var(--muted); letter-spacing: 0.1em;
1398
+ display: flex; justify-content: space-between; align-items: center;
1399
+ flex-shrink: 0;
1400
+ }
1401
+ #po-ag-session-list { flex: 1; overflow-y: auto; }
1402
+ .po-ag-sess {
1403
+ padding: 8px 12px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.04);
1404
+ transition: background 0.1s;
1405
+ }
1406
+ .po-ag-sess:hover { background: rgba(255,255,255,0.04); }
1407
+ .po-ag-sess.active { background: rgba(0,229,200,0.07); border-left: 2px solid var(--teal); padding-left: 10px; }
1408
+ .po-ag-sess-id { font-size: 10px; color: var(--text); font-family: 'Azeret Mono', monospace; margin-bottom: 3px; }
1409
+ .po-ag-sess-meta { font-size: 9px; color: var(--muted); line-height: 1.5; }
1410
+ .po-ag-sess-meta .hi { color: var(--dim); }
1411
+ .po-ag-sess-spawns { display: inline-flex; align-items: center; gap: 3px; margin-top: 2px; }
1412
+ .po-ag-sess-spawn-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--amber); }
1413
+ #po-ag-main {
1414
+ flex: 1; display: flex; flex-direction: column; overflow: hidden;
1415
+ }
1416
+ #po-ag-summary-bar {
1417
+ padding: 8px 16px; border-bottom: 1px solid var(--border);
1418
+ display: flex; gap: 20px; flex-shrink: 0; flex-wrap: wrap;
1419
+ background: rgba(0,0,0,0.1);
1420
+ }
1421
+ .po-ag-stat { display: flex; flex-direction: column; }
1422
+ .po-ag-stat-lbl { font-size: 8px; color: var(--muted); letter-spacing: 0.08em; }
1423
+ .po-ag-stat-val { font-size: 12px; color: var(--text); font-weight: 700; font-family: 'Azeret Mono', monospace; }
1424
+ #po-ag-content { flex: 1; overflow-y: auto; padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
1425
+ .po-ag-section-lbl {
1426
+ font-size: 9px; color: var(--muted); letter-spacing: 0.1em;
1427
+ margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid var(--border);
1428
+ }
1429
+ .po-ag-agent-card {
1430
+ background: rgba(0,0,0,0.25); border: 1px solid var(--border);
1431
+ border-radius: 4px; padding: 10px 12px;
1432
+ display: flex; flex-direction: column; gap: 6px;
1433
+ transition: border-color 0.12s;
1434
+ }
1435
+ .po-ag-agent-card:hover { border-color: rgba(255,179,71,0.3); }
1436
+ .po-ag-agent-top { display: flex; align-items: center; gap: 8px; }
1437
+ .po-ag-agent-name { font-size: 11px; font-weight: 700; color: var(--text); flex: 1; }
1438
+ .po-ag-agent-type { font-size: 8px; color: var(--amber); background: rgba(255,179,71,0.1); border: 1px solid rgba(255,179,71,0.2); border-radius: 2px; padding: 1px 5px; letter-spacing: 0.06em; }
1439
+ .po-ag-spawn-row { display: flex; align-items: center; gap: 8px; }
1440
+ .po-ag-spawn-lbl { font-size: 9px; color: var(--muted); min-width: 60px; }
1441
+ .po-ag-bar-wrap { flex: 1; height: 3px; background: rgba(255,255,255,0.07); border-radius: 2px; overflow: hidden; }
1442
+ .po-ag-bar-fill { height: 100%; border-radius: 2px; background: var(--amber); transition: width 0.4s ease; }
1443
+ .po-ag-spawn-count { font-size: 9px; color: var(--dim); min-width: 40px; text-align: right; }
1444
+ .po-ag-tool-section { margin-top: 4px; }
1445
+ .po-ag-tool-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
1446
+ .po-ag-tool-name { font-size: 9px; color: var(--teal); min-width: 55px; font-family: 'Azeret Mono', monospace; }
1447
+ .po-ag-tool-bar-wrap { flex: 1; height: 2px; background: rgba(255,255,255,0.06); border-radius: 1px; overflow: hidden; }
1448
+ .po-ag-tool-bar-fill { height: 100%; border-radius: 1px; background: var(--teal); }
1449
+ .po-ag-tool-count { font-size: 9px; color: var(--dim); min-width: 36px; text-align: right; }
1450
+ .po-ag-sess-hdr { margin-bottom: 10px; }
1451
+ .po-ag-sess-title { font-size: 11px; font-weight: 700; color: var(--text); font-family: 'Azeret Mono', monospace; margin-bottom: 4px; }
1452
+ .po-ag-sess-subtitle { font-size: 9px; color: var(--muted); }
1172
1453
 
1173
1454
  /* Knowledge graph pane */
1174
1455
  #po-knowledge-tab { flex-direction: row; }
@@ -1266,6 +1547,7 @@
1266
1547
  <button class="po-tab active" onclick="switchPalaceTab('drawers')">MEMORIES</button>
1267
1548
  <button class="po-tab" onclick="switchPalaceTab('sessions')">SESSIONS</button>
1268
1549
  <button class="po-tab" onclick="switchPalaceTab('chunks')">KNOWLEDGE</button>
1550
+ <button class="po-tab" onclick="switchPalaceTab('routing')">ROUTING</button>
1269
1551
  <button class="po-tab" onclick="switchPalaceTab('swarm')">SWARM</button>
1270
1552
  <button class="po-tab" onclick="switchPalaceTab('graph')">AGENT GRAPH</button>
1271
1553
  <button class="po-tab" onclick="switchPalaceTab('knowledge')">CODE GRAPH</button>
@@ -1318,23 +1600,73 @@
1318
1600
  </div>
1319
1601
  </div>
1320
1602
  </div>
1321
- <div id="po-graph-tab" class="po-tab-pane">
1322
- <div id="po-graph-controls">
1323
- <span id="po-graph-info"></span>
1324
- <label><input type="checkbox" id="po-graph-labels" checked onchange="kgGraph.toggleLabels(this.checked)"> LABELS</label>
1603
+ <div id="po-graph-tab" class="po-tab-pane" style="flex-direction:row;overflow:hidden;">
1604
+ <div id="po-ag-sidebar">
1605
+ <div id="po-ag-sidebar-hdr">
1606
+ <span>SESSIONS</span>
1607
+ <span id="po-ag-session-count" style="color:var(--dim);font-size:9px;"></span>
1608
+ </div>
1609
+ <div id="po-ag-session-list"></div>
1610
+ </div>
1611
+ <div id="po-ag-main">
1612
+ <div id="po-ag-summary-bar"></div>
1613
+ <div id="po-ag-content">
1614
+ <div class="po-select-hint" id="po-ag-hint">SELECT A SESSION</div>
1615
+ </div>
1616
+ </div>
1617
+ </div>
1618
+ <div id="po-routing-tab" class="po-tab-pane" style="flex-direction:row;overflow:hidden;">
1619
+ <div style="width:240px;min-width:240px;border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;background:rgba(0,0,0,0.1);">
1620
+ <div style="padding:8px 12px;border-bottom:1px solid var(--border);font-size:9px;color:var(--muted);letter-spacing:0.1em;flex-shrink:0;">LAST ROUTE</div>
1621
+ <div id="po-hooks-left" style="flex:1;overflow-y:auto;padding:10px 12px;"></div>
1622
+ </div>
1623
+ <div style="flex:1;display:flex;flex-direction:column;overflow:hidden;">
1624
+ <div style="padding:8px 12px;border-bottom:1px solid var(--border);font-size:9px;color:var(--muted);letter-spacing:0.1em;flex-shrink:0;">ROUTING HISTORY</div>
1625
+ <div id="po-hooks-right" style="flex:1;overflow-y:auto;padding:10px 12px;"></div>
1325
1626
  </div>
1326
- <canvas id="po-kg-canvas"></canvas>
1327
1627
  </div>
1328
1628
  <div id="po-swarm-tab" class="po-tab-pane" style="flex-direction:row;overflow:hidden;">
1329
- <div id="po-swarm-sidebar" style="width:200px;flex-shrink:0;border-right:1px solid var(--border);padding:20px 16px;display:flex;flex-direction:column;gap:12px;">
1330
- <div class="section-label">TOPOLOGY</div>
1331
- <div id="po-swarm-meta" style="display:flex;flex-direction:column;gap:8px;">
1332
- <div style="color:var(--muted);font-size:10px;">Loading…</div>
1629
+ <div style="width:220px;min-width:220px;display:flex;flex-direction:column;border-right:1px solid var(--border);">
1630
+ <div id="po-swarm-list" style="flex:1;overflow-y:auto;"></div>
1631
+ <div id="po-swarm-footer" style="padding:6px 8px;border-top:1px solid var(--border);display:flex;align-items:center;gap:6px;">
1632
+ <span id="po-swarm-data-size" style="font-size:8px;color:var(--muted);font-family:'Azeret Mono',monospace;flex:1;"></span>
1633
+ <button id="po-swarm-clean-btn" onclick="cleanSwarmDataUI()" style="font-size:8px;font-family:'Azeret Mono',monospace;background:rgba(239,83,80,0.1);color:#EF5350;border:1px solid rgba(239,83,80,0.3);border-radius:3px;padding:2px 6px;cursor:pointer;">CLEAN</button>
1333
1634
  </div>
1334
1635
  </div>
1335
- <div style="flex:1;position:relative;display:flex;flex-direction:column;">
1336
- <div id="po-swarm-label" style="position:absolute;top:12px;left:20px;font-size:10px;color:var(--muted);letter-spacing:0.1em;z-index:2;"></div>
1337
- <canvas id="po-swarm-canvas" style="flex:1;width:100%;height:100%;display:block;"></canvas>
1636
+ <div id="po-swarm-detail">
1637
+ <div id="po-swarm-hint" class="po-select-hint">SELECT A SWARM RUN</div>
1638
+ <div id="po-swarm-header" style="display:none;">
1639
+ <div style="font-size:11px;font-weight:700;color:var(--text);font-family:'Azeret Mono',monospace;" id="po-swarm-title"></div>
1640
+ <div style="font-size:9px;color:var(--muted);" id="po-swarm-subtitle"></div>
1641
+ </div>
1642
+ <div id="po-swarm-stats-bar" style="display:none;">
1643
+ <span id="po-swarm-stat-topo"></span>
1644
+ <span id="po-swarm-stat-consensus"></span>
1645
+ <span id="po-swarm-stat-agents"></span>
1646
+ <span id="po-swarm-stat-status"></span>
1647
+ <span id="po-swarm-stat-duration"></span>
1648
+ </div>
1649
+ <div id="po-swarm-body" style="display:none;">
1650
+ <div id="po-swarm-canvas-wrap">
1651
+ <div id="po-swarm-topo-label"></div>
1652
+ <canvas id="po-swarm-canvas"></canvas>
1653
+ </div>
1654
+ <div id="po-swarm-agents-section">
1655
+ <div class="po-sd-sidebar-label">AGENTS (click to inspect)</div>
1656
+ <div id="po-swarm-agent-list"></div>
1657
+ </div>
1658
+ <div id="po-swarm-agent-drawer" style="display:none;">
1659
+ <div id="po-swarm-agent-header">
1660
+ <div id="po-swarm-agent-info"></div>
1661
+ <button id="po-swarm-agent-close" onclick="closeSwarmAgent()">&#10005;</button>
1662
+ </div>
1663
+ <div id="po-swarm-agent-timeline"></div>
1664
+ </div>
1665
+ <div id="po-swarm-events-section" style="padding:10px 14px;border-top:1px solid var(--border);">
1666
+ <div class="po-sd-sidebar-label">EVENT LOG</div>
1667
+ <div id="po-swarm-events-list" style="max-height:200px;overflow-y:auto;"></div>
1668
+ </div>
1669
+ </div>
1338
1670
  </div>
1339
1671
  </div>
1340
1672
  <div id="po-chunks-tab" class="po-tab-pane" style="flex-direction:column;overflow:hidden;">
@@ -1415,6 +1747,7 @@
1415
1747
  <h1>MONOMIND CONTROL</h1>
1416
1748
  <div id="header-meta">
1417
1749
  <span><span id="conn-dot"></span><span id="conn-label">CONNECTING</span></span>
1750
+ <button onclick="openPalaceOverlay()" style="background:none;border:1px solid rgba(0,229,200,0.3);color:var(--teal);font-family:'Azeret Mono',monospace;font-size:9px;letter-spacing:0.1em;padding:3px 9px;cursor:pointer;border-radius:3px;transition:background 0.15s;" onmouseover="this.style.background='rgba(0,229,200,0.1)'" onmouseout="this.style.background='none'">⬡ MEMORY PALACE</button>
1418
1751
  </div>
1419
1752
  </div>
1420
1753
  <div id="header-right">
@@ -1439,7 +1772,7 @@
1439
1772
  <div class="proj-header">Project</div>
1440
1773
  <div class="proj-header">Path</div>
1441
1774
  <div class="proj-header" style="text-align:right">Sessions</div>
1442
- <div class="proj-header" style="text-align:right">Drawers</div>
1775
+ <div class="proj-header" style="text-align:right">Memories</div>
1443
1776
  <div class="proj-header" style="text-align:right">Size</div>
1444
1777
  <div class="proj-header">Last Active</div>
1445
1778
  <div class="proj-cell" style="grid-column:1/7;color:var(--muted)">Loading projects…</div>
@@ -1449,17 +1782,6 @@
1449
1782
  </div>
1450
1783
 
1451
1784
  <!-- ─── AGENTS ──────────────────────────────────────────────── -->
1452
- <div class="panel open" id="panel-agents">
1453
- <div class="panel-header" onclick="togglePanel('panel-agents')">
1454
- <div class="panel-title"><span class="live-dot green"></span>AGENTS</div>
1455
- <span class="panel-badge" id="agents-badge">0</span>
1456
- <span class="panel-chevron">›</span>
1457
- </div>
1458
- <div class="panel-body" id="agents-body">
1459
- <div class="placeholder">NO AGENTS</div>
1460
- </div>
1461
- </div>
1462
-
1463
1785
  <!-- ─── TOKENS ──────────────────────────────────────────────── -->
1464
1786
  <div class="panel open" id="panel-tokens">
1465
1787
  <div class="panel-header" onclick="togglePanel('panel-tokens')">
@@ -1473,76 +1795,35 @@
1473
1795
  </div>
1474
1796
  </div>
1475
1797
 
1476
- <!-- ─── MEMORY PALACE ───────────────────────────────────────── -->
1477
- <div class="panel open" id="panel-memory">
1478
- <div class="panel-header" onclick="togglePanel('panel-memory')">
1479
- <div class="panel-title"><span class="live-dot"></span>MEMORY PALACE</div>
1480
- <div style="display:flex;gap:6px;align-items:center;">
1481
- <span id="hnsw-indicator"></span>
1482
- <button onclick="event.stopPropagation();openPalaceOverlay()" style="background:none;border:1px solid rgba(0,229,200,0.3);color:var(--teal);font-family:'Azeret Mono',monospace;font-size:9px;letter-spacing:0.08em;padding:2px 7px;cursor:pointer;border-radius:3px;transition:background 0.15s;" onmouseover="this.style.background='rgba(0,229,200,0.08)'" onmouseout="this.style.background='none'">EXPLORE</button>
1483
- <span class="panel-badge" id="memory-badge">0</span>
1484
- <span class="panel-chevron">›</span>
1485
- </div>
1486
- </div>
1487
- <div class="panel-body" id="memory-body">
1488
- <div class="memory-layout">
1489
- <div class="memory-left" id="memory-left"></div>
1490
- <div class="memory-right" id="memory-right"></div>
1491
- </div>
1492
- </div>
1493
- </div>
1494
1798
 
1495
- <!-- ─── HOOKS & ROUTING ─────────────────────────────────────── -->
1496
- <div class="panel open" id="panel-hooks">
1497
- <div class="panel-header" onclick="togglePanel('panel-hooks')">
1498
- <div class="panel-title"><span class="live-dot amber"></span>HOOKS &amp; ROUTING</div>
1799
+ <!-- ─── SCHEDULED LOOPS ──────────────────────────────────────── -->
1800
+ <div class="panel open" id="panel-loops">
1801
+ <div class="panel-header" onclick="togglePanel('panel-loops')">
1802
+ <div class="panel-title"><span class="live-dot" id="loops-live-dot"></span>SCHEDULED LOOPS</div>
1499
1803
  <div style="display:flex;align-items:center;gap:6px;">
1500
- <span class="panel-badge" id="hooks-badge">—</span>
1804
+ <span class="panel-badge" id="loops-badge">0</span>
1501
1805
  <span class="panel-chevron">›</span>
1502
1806
  </div>
1503
1807
  </div>
1504
- <div class="panel-body" id="hooks-body">
1505
- <div class="hooks-layout">
1506
- <div class="hooks-col" id="hooks-left"></div>
1507
- <div class="hooks-col" id="hooks-right"></div>
1508
- </div>
1808
+ <div class="panel-body" id="loops-body">
1809
+ <div class="loops-empty">NO ACTIVE LOOPS</div>
1509
1810
  </div>
1510
1811
  </div>
1511
1812
 
1512
- <!-- ─── METRICS ─────────────────────────────────────────────── -->
1513
- <div class="panel open" id="panel-metrics">
1514
- <div class="panel-header" onclick="togglePanel('panel-metrics')">
1515
- <div class="panel-title"><span class="live-dot muted"></span>METRICS</div>
1516
- <span class="panel-chevron">›</span>
1517
- </div>
1518
- <div class="panel-body" id="metrics-body">
1519
- <div class="placeholder">—</div>
1520
- </div>
1521
- </div>
1522
-
1523
- <!-- ─── ACTIVITY ────────────────────────────────────────────── -->
1813
+ <!-- ─── SESSION JOURNAL ────────────────────────────────────────── -->
1524
1814
  <div class="panel open" id="panel-activity">
1525
1815
  <div class="panel-header" onclick="togglePanel('panel-activity')">
1526
- <div class="panel-title"><span class="live-dot"></span>ACTIVITY</div>
1816
+ <div class="panel-title"><span class="live-dot"></span>SESSION JOURNAL</div>
1527
1817
  <div style="display:flex;align-items:center;gap:6px;">
1528
1818
  <span class="panel-badge" id="activity-badge">0</span>
1529
1819
  <span class="panel-chevron">›</span>
1530
1820
  </div>
1531
1821
  </div>
1532
1822
  <div class="panel-body" id="activity-body">
1533
- <div id="activity-log"></div>
1823
+ <div id="journal-list"><div id="journal-empty">Loading sessions…</div></div>
1534
1824
  </div>
1535
1825
  </div>
1536
1826
 
1537
- <div class="panel" id="panel-system">
1538
- <div class="panel-header" onclick="togglePanel('panel-system')">
1539
- <div class="panel-title"><span class="live-dot muted"></span>SYSTEM</div>
1540
- <span class="panel-chevron">›</span>
1541
- </div>
1542
- <div class="panel-body" id="system-body">
1543
- <div class="placeholder">—</div>
1544
- </div>
1545
- </div>
1546
1827
 
1547
1828
 
1548
1829
  </div><!-- end #grid -->
@@ -1561,14 +1842,12 @@ const MAX_ACTIVITY = 80;
1561
1842
  // Track which panels are currently open (match initial HTML .open classes)
1562
1843
  const expandedPanels = new Set([
1563
1844
  'panel-projects',
1564
- 'panel-agents', 'panel-tokens', 'panel-activity', 'panel-hooks', 'panel-metrics', 'panel-memory'
1845
+ 'panel-tokens', 'panel-activity'
1565
1846
  ]);
1566
1847
 
1567
1848
  // Section name → panel id mapping
1568
1849
  const SECTION_PANEL = {
1569
- agents: 'panel-agents',
1570
- tokens: 'panel-tokens', memory: 'panel-memory', hooks: 'panel-hooks',
1571
- knowledge: 'panel-memory', metrics: 'panel-metrics', system: 'panel-system',
1850
+ tokens: 'panel-tokens',
1572
1851
  };
1573
1852
 
1574
1853
  // ═══════════════════════════════════════════════════════════════════
@@ -1596,13 +1875,7 @@ window.togglePanel = function(panelId) {
1596
1875
  function renderPanelById(panelId) {
1597
1876
  if (!appData) return;
1598
1877
  switch (panelId) {
1599
- case 'panel-agents': renderAgents(appData); break;
1600
1878
  case 'panel-tokens': renderTokens(appData); break;
1601
- case 'panel-memory': renderMemory(appData); break;
1602
- case 'panel-hooks': renderHooks(appData); break;
1603
- case 'panel-knowledge': renderMemory(appData); break;
1604
- case 'panel-metrics': renderMetrics(appData); break;
1605
- case 'panel-system': renderSystem(appData); break;
1606
1879
  }
1607
1880
  }
1608
1881
 
@@ -1625,10 +1898,7 @@ async function fetchAndRenderSections(sections) {
1625
1898
  renderPanelById(panelId);
1626
1899
  } catch (e) { /* ignore transient fetch errors */ }
1627
1900
  }
1628
- // Re-render metrics panel when its source data changed
1629
- if (metricsStale && expandedPanels.has('panel-metrics') && appData) {
1630
- renderMetrics(appData);
1631
- }
1901
+
1632
1902
  }
1633
1903
 
1634
1904
  // ═══════════════════════════════════════════════════════════════════
@@ -1716,9 +1986,8 @@ function markLive(panelId) {
1716
1986
 
1717
1987
  function updateLiveBorders() {
1718
1988
  const panelIds = [
1719
- 'panel-agents','panel-tokens',
1720
- 'panel-memory','panel-hooks','panel-metrics',
1721
- 'panel-activity','panel-system'
1989
+ 'panel-tokens',
1990
+ 'panel-activity'
1722
1991
  ];
1723
1992
  const isLive = lastUpdateTime > 0 && (Date.now() - lastUpdateTime) < 5000;
1724
1993
  panelIds.forEach(id => {
@@ -1774,13 +2043,12 @@ async function selectProject(dir) {
1774
2043
  if (selectedProjectDir === dir) return;
1775
2044
  selectedProjectDir = dir;
1776
2045
  _codeGraphLoaded = false; kgCodeGraph.reset();
2046
+ renderJournal();
1777
2047
  try {
1778
2048
  const res = await fetch(`/api/data?dir=${encodeURIComponent(dir)}`);
1779
2049
  const data = await res.json();
1780
2050
  updateAllPanels(data);
1781
- } catch (e) {
1782
- appendActivity(`Failed to load project: ${e.message}`, 'error');
1783
- }
2051
+ } catch (e) {}
1784
2052
  }
1785
2053
 
1786
2054
  function renderProjects(data) {
@@ -1803,7 +2071,7 @@ function renderProjects(data) {
1803
2071
  <div class="proj-header">Project</div>
1804
2072
  <div class="proj-header">Path</div>
1805
2073
  <div class="proj-header" style="text-align:right">Sessions</div>
1806
- <div class="proj-header" style="text-align:right">Drawers</div>
2074
+ <div class="proj-header" style="text-align:right">Memories</div>
1807
2075
  <div class="proj-header" style="text-align:right">Size</div>
1808
2076
  <div class="proj-header">Last Active</div>
1809
2077
  `;
@@ -1822,7 +2090,7 @@ function renderProjects(data) {
1822
2090
  </div>
1823
2091
  <div class="proj-cell proj-path" style="${highlight}${cursor}" ${rowClick}>${p.path.replace(/\/Users\/[^/]+/, '~')}</div>
1824
2092
  <div class="proj-cell proj-count" style="${highlight}${cursor};justify-content:flex-end" ${rowClick}>${p.sessionCount}</div>
1825
- <div class="proj-cell proj-dots" style="${highlight}${cursor};justify-content:flex-end" ${rowClick}>${p.drawerCount || '—'}</div>
2093
+ <div class="proj-cell proj-dots" style="${highlight}${cursor};justify-content:flex-end" ${rowClick}>${p.memoryCount || '—'}</div>
1826
2094
  <div class="proj-cell proj-size" style="${highlight}${cursor};justify-content:flex-end" ${rowClick}>${formatBytes(p.totalSize)}</div>
1827
2095
  <div class="proj-cell proj-age" style="${highlight}${cursor}" ${rowClick}>${timeAgo(p.lastActivity)}</div>
1828
2096
  `;
@@ -2085,12 +2353,12 @@ function renderTokens(data) {
2085
2353
  <div class="token-stat">
2086
2354
  <div class="token-stat-label">DAILY</div>
2087
2355
  <div class="token-stat-val">${fmtCost(todayCost)}</div>
2088
- ${todayCalls != null ? `<div style="font-size:9px;color:var(--muted);margin-top:2px;">${fmtNum(todayCalls)} calls</div>` : ''}
2356
+ ${todayCalls != null ? `<div style="font-size:8px;color:var(--muted);">${fmtNum(todayCalls)} calls</div>` : ''}
2089
2357
  </div>
2090
2358
  <div class="token-stat">
2091
2359
  <div class="token-stat-label">MONTHLY</div>
2092
2360
  <div class="token-stat-val">${fmtCost(monthlyCost)}</div>
2093
- ${monthlyCalls != null ? `<div style="font-size:9px;color:var(--muted);margin-top:2px;">${fmtNum(monthlyCalls)} calls</div>` : ''}
2361
+ ${monthlyCalls != null ? `<div style="font-size:8px;color:var(--muted);">${fmtNum(monthlyCalls)} calls</div>` : ''}
2094
2362
  </div>
2095
2363
  <div class="token-stat">
2096
2364
  <div class="token-stat-label">ALL TIME $</div>
@@ -2623,6 +2891,123 @@ function renderMemory(data) {
2623
2891
  right.innerHTML = rightHtml;
2624
2892
  }
2625
2893
 
2894
+ // ═══════════════════════════════════════════════════════════════════
2895
+ // SCHEDULED LOOPS PANEL
2896
+ // ═══════════════════════════════════════════════════════════════════
2897
+ let _loopCountdownTimers = [];
2898
+
2899
+ function fmtCountdown(nextRunAt) {
2900
+ if (!nextRunAt) return '';
2901
+ const diff = nextRunAt - Date.now();
2902
+ if (diff <= 0) return 'running now';
2903
+ const s = Math.floor(diff / 1000);
2904
+ if (s < 60) return `next in ${s}s`;
2905
+ const m = Math.floor(s / 60), rs = s % 60;
2906
+ return `next in ${m}m ${rs}s`;
2907
+ }
2908
+
2909
+ function fmtRelTime(ts) {
2910
+ if (!ts) return '—';
2911
+ const diff = Date.now() - ts;
2912
+ if (diff < 5000) return 'just now';
2913
+ if (diff < 60000) return `${Math.floor(diff/1000)}s ago`;
2914
+ if (diff < 3600000) return `${Math.floor(diff/60000)}m ago`;
2915
+ return new Date(ts).toLocaleTimeString();
2916
+ }
2917
+
2918
+ async function renderLoops() {
2919
+ try {
2920
+ const r = await fetch('/api/loops');
2921
+ const { loops = [] } = await r.json();
2922
+ const body = document.getElementById('loops-body');
2923
+ const badge = document.getElementById('loops-badge');
2924
+ const dot = document.getElementById('loops-live-dot');
2925
+ if (!body) return;
2926
+
2927
+ // Clear old countdown timers
2928
+ _loopCountdownTimers.forEach(clearInterval);
2929
+ _loopCountdownTimers = [];
2930
+
2931
+ const active = loops.filter(l => l.status !== 'complete');
2932
+ badge.textContent = active.length || '0';
2933
+
2934
+ if (dot) {
2935
+ dot.className = 'live-dot' + (active.length > 0 ? ' green' : '');
2936
+ }
2937
+
2938
+ if (!loops.length) {
2939
+ body.innerHTML = '<div class="loops-empty">NO ACTIVE LOOPS</div>';
2940
+ return;
2941
+ }
2942
+
2943
+ body.innerHTML = loops.map(loop => {
2944
+ const isRepeat = loop.type === 'repeat';
2945
+ const isDo = loop.type === 'do';
2946
+ const typeClass = isRepeat ? 'lc-repeat' : 'lc-do';
2947
+ const pillClass = isRepeat ? 'lp-repeat' : 'lp-do';
2948
+ const typeLabel = isRepeat ? 'REPEAT' : 'DO';
2949
+ const stopPending = loop.stopRequested;
2950
+ const statusDotClass = stopPending ? 'stopped' : (loop.status === 'waiting' ? 'waiting' : 'running');
2951
+
2952
+ const progress = isRepeat && loop.maxReps > 0
2953
+ ? Math.round((loop.currentRep - 1) / loop.maxReps * 100)
2954
+ : null;
2955
+ const progressBar = progress !== null
2956
+ ? `<div class="loop-progress-row">
2957
+ <div class="loop-progress-bar"><div class="loop-progress-fill" style="width:${progress}%"></div></div>
2958
+ <div class="loop-countdown" id="lcd-${loop.id}">${fmtCountdown(loop.nextRunAt)}</div>
2959
+ </div>`
2960
+ : `<div class="loop-progress-row">
2961
+ <div style="flex:1"></div>
2962
+ <div class="loop-countdown" id="lcd-${loop.id}">${fmtCountdown(loop.nextRunAt)}</div>
2963
+ </div>`;
2964
+
2965
+ const metaItems = isRepeat
2966
+ ? `<span class="loop-meta-item">run <span>${loop.currentRep || 1}/${loop.maxReps || '?'}</span></span>
2967
+ <span class="loop-meta-item">every <span>${loop.interval || '?'}m</span></span>
2968
+ <span class="loop-meta-item">started <span>${fmtRelTime(loop.startedAt)}</span></span>`
2969
+ : `<span class="loop-meta-item">task <span>${escHtml(String(loop.currentTask || '—').slice(0,40))}</span></span>
2970
+ <span class="loop-meta-item">last run <span>${fmtRelTime(loop.lastRunAt)}</span></span>`;
2971
+
2972
+ const stopLabel = stopPending ? 'STOPPING…' : 'STOP';
2973
+ const disabledAttr = stopPending ? 'disabled' : '';
2974
+
2975
+ return `<div class="loop-card ${typeClass}${stopPending ? ' lc-stop-pending' : ''}" data-loop-id="${escHtml(loop.id)}">
2976
+ <span class="loop-type-pill ${pillClass}"><span class="loop-status-dot ${statusDotClass}"></span>${typeLabel}</span>
2977
+ <div class="loop-prompt" title="${escHtml(loop.prompt || '')}">${escHtml((loop.prompt || '').slice(0,80))}</div>
2978
+ <button class="loop-stop-btn" onclick="stopLoop('${escHtml(loop.id)}')" ${disabledAttr}>${stopLabel}</button>
2979
+ <div class="loop-meta">${metaItems}</div>
2980
+ ${progressBar}
2981
+ </div>`;
2982
+ }).join('');
2983
+
2984
+ // Live countdown update
2985
+ const timer = setInterval(() => {
2986
+ loops.forEach(loop => {
2987
+ const el = document.getElementById(`lcd-${loop.id}`);
2988
+ if (el) el.textContent = fmtCountdown(loop.nextRunAt);
2989
+ });
2990
+ }, 1000);
2991
+ _loopCountdownTimers.push(timer);
2992
+ } catch {}
2993
+ }
2994
+
2995
+ async function stopLoop(id) {
2996
+ const card = document.querySelector(`[data-loop-id="${id}"]`);
2997
+ const btn = card?.querySelector('.loop-stop-btn');
2998
+ if (btn) { btn.disabled = true; btn.textContent = 'STOPPING…'; }
2999
+ try {
3000
+ await fetch('/api/loops/stop', {
3001
+ method: 'POST',
3002
+ headers: { 'Content-Type': 'application/json' },
3003
+ body: JSON.stringify({ id })
3004
+ });
3005
+ setTimeout(renderLoops, 400);
3006
+ } catch {
3007
+ if (btn) { btn.disabled = false; btn.textContent = 'STOP'; }
3008
+ }
3009
+ }
3010
+
2626
3011
  // ═══════════════════════════════════════════════════════════════════
2627
3012
  // HOOKS & ROUTING PANEL
2628
3013
  // ═══════════════════════════════════════════════════════════════════
@@ -2632,11 +3017,8 @@ function renderHooks(data) {
2632
3017
  const feedback = h.feedback || [];
2633
3018
  const workers = h.workerDispatch || [];
2634
3019
 
2635
- const badge = document.getElementById('hooks-badge');
2636
- if (badge) badge.textContent = feedback.length || '';
2637
-
2638
- const left = document.getElementById('hooks-left');
2639
- const right = document.getElementById('hooks-right');
3020
+ const left = document.getElementById('po-hooks-left');
3021
+ const right = document.getElementById('po-hooks-right');
2640
3022
  if (!left || !right) return;
2641
3023
 
2642
3024
  // Left: last route — actual fields: agent, confidence, reason, semanticRouting, updatedAt
@@ -2823,45 +3205,77 @@ function renderMetrics(data) {
2823
3205
  }
2824
3206
 
2825
3207
  // ═══════════════════════════════════════════════════════════════════
2826
- // ACTIVITY PANEL
3208
+ // SESSION JOURNAL
2827
3209
  // ═══════════════════════════════════════════════════════════════════
2828
- function appendActivity(msg, type) {
2829
- const log = document.getElementById('activity-log');
2830
- if (!log) return;
2831
- activityCount++;
2832
-
2833
- const entry = document.createElement('div');
2834
- entry.className = 'activity-entry type-' + (type || 'default');
2835
- entry.innerHTML = `<span class="activity-time">${now()}</span><span class="activity-msg">${escHtml(String(msg).slice(0,100))}</span>`;
2836
- log.insertBefore(entry, log.firstChild);
2837
-
2838
- // Trim old entries
2839
- while (log.children.length > MAX_ACTIVITY) {
2840
- log.removeChild(log.lastChild);
2841
- }
3210
+ function appendActivity() {} // no-op: panel replaced by Session Journal
3211
+ function inferActivityType() { return 'default'; }
3212
+
3213
+ function fmtDur(ms) {
3214
+ if (!ms) return '';
3215
+ const m = Math.round(ms / 60000);
3216
+ if (m < 60) return m + 'm';
3217
+ return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
3218
+ }
2842
3219
 
2843
- const badge = document.getElementById('activity-badge');
2844
- if (badge) badge.textContent = Math.min(activityCount, MAX_ACTIVITY);
3220
+ function toggleJournalCard(el) {
3221
+ el.classList.toggle('open');
2845
3222
  }
2846
3223
 
2847
- function inferActivityType(data, prevData) {
2848
- if (!prevData) return 'default';
2849
- // Detect what changed
2850
- const sessNew = (data.sessions && data.sessions.count) || 0;
2851
- const sessPrev = (prevData.sessions && prevData.sessions.count) || 0;
2852
- if (sessNew !== sessPrev) return 'session';
3224
+ async function renderJournal() {
3225
+ const dir = selectedProjectDir || '';
3226
+ const url = `/api/session-journal${dir ? '?dir=' + encodeURIComponent(dir) : ''}`;
3227
+ let sessions = [];
3228
+ try {
3229
+ const res = await fetch(url);
3230
+ if (res.ok) ({ sessions } = await res.json());
3231
+ } catch {}
2853
3232
 
2854
- const swNew = JSON.stringify(data.swarm || {});
2855
- const swPrev = JSON.stringify(prevData.swarm || {});
2856
- if (swNew !== swPrev) return 'swarm';
3233
+ const list = document.getElementById('journal-list');
3234
+ const badge = document.getElementById('activity-badge');
3235
+ if (!list) return;
3236
+ if (badge) badge.textContent = sessions.length;
2857
3237
 
2858
- const agNew = (data.agents && data.agents.count) || 0;
2859
- const agPrev = (prevData.agents && prevData.agents.count) || 0;
2860
- if (agNew !== agPrev) return 'agent';
3238
+ if (!sessions.length) {
3239
+ list.innerHTML = '<div id="journal-empty">NO SESSIONS FOUND</div>';
3240
+ return;
3241
+ }
2861
3242
 
2862
- return 'default';
3243
+ list.innerHTML = sessions.map(s => {
3244
+ const dt = s.lastTs ? new Date(s.lastTs) : (s.mtime ? new Date(s.mtime) : null);
3245
+ const dateStr = dt ? dt.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : '';
3246
+ const dur = fmtDur(s.totalDurationMs);
3247
+ const msgs = s.totalMessages ? s.totalMessages + ' msgs' : '';
3248
+ const stats = [dur, msgs].filter(Boolean).join(' · ');
3249
+ const shortId = s.id.slice(0, 8) + '…';
3250
+ const prompt = s.lastPrompt ? escHtml(s.lastPrompt.slice(0, 120)) : '';
3251
+
3252
+ const latestSummary = s.summaries && s.summaries.length ? s.summaries[s.summaries.length - 1] : null;
3253
+ const excerpt = latestSummary ? escHtml(latestSummary.text.slice(0, 200).replace(/\n+/g, ' ')) : '';
3254
+ const fullText = latestSummary ? escHtml(latestSummary.text) : '';
3255
+ const compactBadge = s.summaries && s.summaries.length > 0
3256
+ ? `<span class="jn-badge-compact">${s.summaries.length > 1 ? s.summaries.length + ' compacts' : 'compacted'}</span>` : '';
3257
+
3258
+ return `<div class="jn-card" onclick="toggleJournalCard(this)">
3259
+ <div class="jn-header">
3260
+ <span class="jn-date">${dateStr}</span>
3261
+ <span class="jn-id">${shortId}</span>
3262
+ ${compactBadge}
3263
+ ${stats ? `<span class="jn-stats">${stats}</span>` : ''}
3264
+ </div>
3265
+ ${prompt ? `<div class="jn-prompt">"${prompt}${s.lastPrompt && s.lastPrompt.length > 120 ? '…' : ''}"</div>` : ''}
3266
+ ${excerpt
3267
+ ? `<div class="jn-excerpt">${excerpt}${latestSummary && latestSummary.text.length > 200 ? '…' : ''}</div>
3268
+ <div class="jn-summary-full">${fullText}</div>`
3269
+ : `<div class="jn-no-summary">No compact summary available</div>`
3270
+ }
3271
+ </div>`;
3272
+ }).join('');
2863
3273
  }
2864
3274
 
3275
+ // Initial load + refresh every 2 minutes
3276
+ renderJournal();
3277
+ setInterval(renderJournal, 120000);
3278
+
2865
3279
  // ═══════════════════════════════════════════════════════════════════
2866
3280
  // SYSTEM PANEL
2867
3281
  // ═══════════════════════════════════════════════════════════════════
@@ -2939,14 +3353,7 @@ function updateAllPanels(data) {
2939
3353
  renderProjects(data); // always render (navigation panel)
2940
3354
  renderProjectStats(data);
2941
3355
  // Only render panels that are currently expanded
2942
- if (expandedPanels.has('panel-agents')) renderAgents(data);
2943
3356
  if (expandedPanels.has('panel-tokens')) renderTokens(data);
2944
- if (expandedPanels.has('panel-memory')) renderMemory(data);
2945
- // Always update hooks badge even when panel is collapsed
2946
- { const h = data.hooks || {}; const lr = h.lastRoute || {}; const badge = document.getElementById('hooks-badge'); if (badge) badge.textContent = lr.agent || lr.suggestedAgent || (h.feedback && h.feedback.length) || '—'; }
2947
- if (expandedPanels.has('panel-hooks')) renderHooks(data);
2948
- if (expandedPanels.has('panel-metrics')) renderMetrics(data);
2949
- if (expandedPanels.has('panel-system')) renderSystem(data);
2950
3357
  // Activity log entry
2951
3358
  if (prev) {
2952
3359
  const type = inferActivityType(data, prev);
@@ -3046,6 +3453,12 @@ async function fetchInitial() {
3046
3453
  }
3047
3454
  }
3048
3455
 
3456
+ // ═══════════════════════════════════════════════════════════════════
3457
+ // LOOPS POLLING — every 10 seconds
3458
+ // ═══════════════════════════════════════════════════════════════════
3459
+ renderLoops();
3460
+ setInterval(renderLoops, 10000);
3461
+
3049
3462
  // ═══════════════════════════════════════════════════════════════════
3050
3463
  // LIVE BORDER REFRESH TIMER
3051
3464
  // ═══════════════════════════════════════════════════════════════════
@@ -3114,10 +3527,9 @@ window.switchPalaceTab = function(tab) {
3114
3527
  b.classList.toggle('active', !!(btnTab && btnTab[1] === tab));
3115
3528
  });
3116
3529
  document.querySelectorAll('.po-tab-pane').forEach(p => p.classList.remove('active'));
3117
- const paneIds = { drawers: 'po-drawers-tab', sessions: 'po-sessions-tab', chunks: 'po-chunks-tab', swarm: 'po-swarm-tab', graph: 'po-graph-tab', knowledge: 'po-knowledge-tab' };
3530
+ const paneIds = { drawers: 'po-drawers-tab', sessions: 'po-sessions-tab', chunks: 'po-chunks-tab', routing: 'po-routing-tab', swarm: 'po-swarm-tab', graph: 'po-graph-tab', knowledge: 'po-knowledge-tab' };
3118
3531
  const pane = document.getElementById(paneIds[tab]);
3119
3532
  if (pane) pane.classList.add('active');
3120
- if (tab !== 'graph') kgGraph.stop();
3121
3533
  if (tab !== 'knowledge') kgCodeGraph.stop();
3122
3534
  if (palaceData) renderPalaceTab(tab);
3123
3535
  };
@@ -3213,8 +3625,9 @@ function renderPalaceTab(tab) {
3213
3625
  if (tab === 'drawers') renderMemories();
3214
3626
  else if (tab === 'sessions') renderPalaceSessions();
3215
3627
  else if (tab === 'chunks') renderPalaceChunks();
3628
+ else if (tab === 'routing') renderHooks(appData || {});
3216
3629
  else if (tab === 'swarm') renderPalaceSwarm();
3217
- else if (tab === 'graph') { kgGraph.init(); kgGraph.render(palaceData.graph || { nodes: [], edges: [] }); }
3630
+ else if (tab === 'graph') { renderAgentGraph(palaceData.graph || { nodes: [], edges: [] }); }
3218
3631
  else if (tab === 'knowledge') { renderPalaceKnowledge(); renderPalaceCodeGraph(); }
3219
3632
  }
3220
3633
 
@@ -3355,165 +3768,483 @@ window.deleteKnowledgeChunk = async function(chunkId) {
3355
3768
  }
3356
3769
  };
3357
3770
 
3358
- function renderPalaceSwarm() {
3359
- if (!palaceData) return;
3360
- // Pull swarm data — prefer appData (live SSE feed) over the palace section fetch
3361
- const src = (appData && appData.swarm) ? appData : { swarm: palaceData.swarmSection || {} };
3362
- const sw = src.swarm || {};
3771
+ // ─── Swarm History Tab ─────────────────────────────────────────────────────
3772
+ let _swarmHistoryEntries = [];
3773
+ let _swarmSelectedIdx = null;
3774
+ let _swarmSelectedAgentId = null;
3775
+ let _swarmLiveState = null;
3776
+
3777
+ async function renderSwarmHistory() {
3778
+ const list = document.getElementById('po-swarm-list');
3779
+ if (!list) return;
3780
+
3781
+ const dir = selectedProjectDir || '';
3782
+ try {
3783
+ const r = await fetch(`/api/swarm-history${dir ? '?dir=' + encodeURIComponent(dir) : ''}`);
3784
+ const d = r.ok ? await r.json() : { entries: [] };
3785
+ _swarmHistoryEntries = d.entries || [];
3786
+ } catch {
3787
+ _swarmHistoryEntries = [];
3788
+ }
3789
+
3790
+ const sw = (appData && appData.swarm) ? appData.swarm : {};
3363
3791
  const state = sw.state || {};
3364
- const activity = sw.activity || {};
3365
- const config = sw.config || {};
3366
- const suggestion = sw.suggestion || {};
3367
- const agentRegs = (src.agents && src.agents.registrations) ? src.agents.registrations : [];
3792
+ _swarmLiveState = (state.status === 'running' || state.status === 'initializing') ? state : null;
3368
3793
 
3369
- const hasLiveSwarm = !!(state.id || state.swarmId || state.active);
3370
- const activitySwarm = activity.swarm || {};
3371
- const topology = config.topology || state.topology || suggestion.topology || '—';
3372
- const consensus = config.consensus || state.consensus || suggestion.consensus || '';
3373
- const agentCount = hasLiveSwarm
3374
- ? (state.agents || []).length
3375
- : (activitySwarm.agent_count || agentRegs.length || 0);
3376
- const score = suggestion.score != null ? suggestion.score + '/7' : null;
3377
- const lastRun = activity.timestamp ? fmtAge(new Date(activity.timestamp).getTime()) : null;
3378
- const recommended = suggestion.recommended || null;
3379
-
3380
- // Render sidebar meta
3381
- const meta = document.getElementById('po-swarm-meta');
3382
- if (meta) {
3383
- const rows = [
3384
- ['TOPOLOGY', fmt(topology).toUpperCase()],
3385
- ['CONSENSUS', fmt(consensus).toUpperCase()],
3386
- ['STATUS', hasLiveSwarm ? '<span style="color:var(--green)">ACTIVE</span>' : '<span style="color:var(--muted)">IDLE</span>'],
3387
- ['AGENTS', agentCount || '—'],
3388
- ];
3389
- if (score) rows.push(['COMPLEXITY', score]);
3390
- if (recommended) rows.push(['RECOMMENDED', recommended]);
3391
- if (lastRun) rows.push(['LAST RUN', `<span style="color:var(--muted)">${escHtml(lastRun)}</span>`]);
3392
- meta.innerHTML = rows.map(([k, v]) =>
3393
- `<div class="swarm-row"><span class="swarm-key">${k}</span><span class="swarm-val">${v}</span></div>`
3394
- ).join('');
3395
- }
3396
-
3397
- // Update label
3398
- const labelEl = document.getElementById('po-swarm-label');
3399
- if (labelEl) labelEl.textContent = hasLiveSwarm ? 'LIVE TOPOLOGY' : 'LAST KNOWN TOPOLOGY';
3400
-
3401
- // Draw on canvas — defer one frame so layout is flushed and offsetWidth/Height are non-zero
3794
+ let html = '';
3795
+
3796
+ if (_swarmLiveState) {
3797
+ const id = _swarmLiveState.swarmId || _swarmLiveState.id || 'live';
3798
+ const topo = (_swarmLiveState.topology || '—').toUpperCase();
3799
+ const agentCount = Array.isArray(_swarmLiveState.agents) ? _swarmLiveState.agents.length : (_swarmLiveState.agents || 0);
3800
+ html += `<div class="po-swarm-item${_swarmSelectedIdx === 'live' ? ' selected' : ''}" onclick="selectSwarmRun('live')">
3801
+ <div class="po-swarm-item-row">
3802
+ <span class="live-dot green"></span>
3803
+ <span class="po-swarm-item-id">${escHtml(String(id).slice(0, 18))}</span>
3804
+ <span class="po-swarm-item-badge status-running">LIVE</span>
3805
+ </div>
3806
+ <div class="po-swarm-item-row">
3807
+ <span class="po-swarm-item-badge topo">${escHtml(topo)}</span>
3808
+ <span class="po-swarm-item-meta">${agentCount} agents</span>
3809
+ </div>
3810
+ </div>`;
3811
+ }
3812
+
3813
+ _swarmHistoryEntries.forEach((entry, i) => {
3814
+ const id = (entry.swarmId || '?').slice(0, 18);
3815
+ const topo = (entry.topology || '').toUpperCase();
3816
+ const status = entry.status || 'unknown';
3817
+ const statusClass = 'status-' + status;
3818
+ const agentCount = (entry.agents || []).length;
3819
+ const age = entry.endedAt ? fmtAge(new Date(entry.endedAt).getTime()) : '';
3820
+ html += `<div class="po-swarm-item${_swarmSelectedIdx === i ? ' selected' : ''}" onclick="selectSwarmRun(${i})">
3821
+ <div class="po-swarm-item-row">
3822
+ <span class="po-swarm-item-id">${escHtml(id)}</span>
3823
+ <span class="po-swarm-item-badge ${escHtml(statusClass)}">${escHtml(status.toUpperCase())}</span>
3824
+ </div>
3825
+ <div class="po-swarm-item-row">
3826
+ <span class="po-swarm-item-badge topo">${escHtml(topo)}</span>
3827
+ <span class="po-swarm-item-meta">${agentCount} agents · ${escHtml(age)}</span>
3828
+ </div>
3829
+ </div>`;
3830
+ });
3831
+
3832
+ if (!_swarmLiveState && _swarmHistoryEntries.length === 0) {
3833
+ html = '<div style="padding:16px;color:var(--muted);font-size:10px;text-align:center;">NO SWARM HISTORY</div>';
3834
+ }
3835
+
3836
+ list.innerHTML = html;
3837
+
3838
+ // Update data size in footer
3839
+ try {
3840
+ const sr = await fetch(`/api/swarm-data-size${dir ? '?dir=' + encodeURIComponent(dir) : ''}`);
3841
+ if (sr.ok) {
3842
+ const sz = await sr.json();
3843
+ const sizeEl = document.getElementById('po-swarm-data-size');
3844
+ if (sizeEl) sizeEl.textContent = sz.humanSize || '0 B';
3845
+ const cleanBtn = document.getElementById('po-swarm-clean-btn');
3846
+ if (cleanBtn) cleanBtn.style.display = sz.totalBytes > 0 ? '' : 'none';
3847
+ }
3848
+ } catch {}
3849
+ }
3850
+
3851
+ async function cleanSwarmDataUI() {
3852
+ if (!confirm('Remove all swarm event recordings? This cannot be undone.')) return;
3853
+ const dir = selectedProjectDir || '';
3854
+ try {
3855
+ await fetch(`/api/swarm-clean${dir ? '?dir=' + encodeURIComponent(dir) : ''}`, { method: 'DELETE' });
3856
+ } catch {}
3857
+ _swarmHistoryEntries = [];
3858
+ _swarmSelectedIdx = null;
3859
+ _swarmSelectedAgentId = null;
3860
+ renderSwarmHistory();
3861
+ const hint = document.getElementById('po-swarm-hint');
3862
+ const header = document.getElementById('po-swarm-header');
3863
+ const statsBar = document.getElementById('po-swarm-stats-bar');
3864
+ const body = document.getElementById('po-swarm-body');
3865
+ if (hint) hint.style.display = '';
3866
+ if (header) header.style.display = 'none';
3867
+ if (statsBar) statsBar.style.display = 'none';
3868
+ if (body) body.style.display = 'none';
3869
+ }
3870
+
3871
+ function renderPalaceSwarm() { renderSwarmHistory(); }
3872
+
3873
+ async function selectSwarmRun(idx) {
3874
+ _swarmSelectedIdx = idx;
3875
+ _swarmSelectedAgentId = null;
3876
+
3877
+ const items = document.querySelectorAll('.po-swarm-item');
3878
+ items.forEach((el, i) => {
3879
+ const itemIdx = _swarmLiveState ? (i === 0 ? 'live' : i - 1) : i;
3880
+ el.classList.toggle('selected', itemIdx === idx);
3881
+ });
3882
+
3883
+ let entry;
3884
+ if (idx === 'live') {
3885
+ entry = _buildLiveEntry();
3886
+ } else {
3887
+ entry = _swarmHistoryEntries[idx];
3888
+ }
3889
+ if (!entry) return;
3890
+
3891
+ const hint = document.getElementById('po-swarm-hint');
3892
+ const header = document.getElementById('po-swarm-header');
3893
+ const statsBar = document.getElementById('po-swarm-stats-bar');
3894
+ const body = document.getElementById('po-swarm-body');
3895
+ if (hint) hint.style.display = 'none';
3896
+ if (header) header.style.display = '';
3897
+ if (statsBar) statsBar.style.display = '';
3898
+ if (body) body.style.display = '';
3899
+
3900
+ const title = document.getElementById('po-swarm-title');
3901
+ const subtitle = document.getElementById('po-swarm-subtitle');
3902
+ if (title) title.textContent = entry.swarmId || '—';
3903
+ if (subtitle) {
3904
+ const dur = entry.durationMs ? fmtDuration(entry.durationMs) : '—';
3905
+ subtitle.textContent = `${entry.topology || '—'} · ${entry.consensus || '—'} · ${(entry.agents || []).length} agents · ${dur}`;
3906
+ }
3907
+
3908
+ const topo = document.getElementById('po-swarm-stat-topo');
3909
+ const cons = document.getElementById('po-swarm-stat-consensus');
3910
+ const agts = document.getElementById('po-swarm-stat-agents');
3911
+ const stat = document.getElementById('po-swarm-stat-status');
3912
+ const durEl = document.getElementById('po-swarm-stat-duration');
3913
+ if (topo) topo.innerHTML = `<span style="color:var(--teal)">${escHtml((entry.topology || '—').toUpperCase())}</span>`;
3914
+ if (cons) cons.textContent = (entry.consensus || '—').toUpperCase();
3915
+ if (agts) agts.textContent = `${(entry.agents || []).length} AGENTS`;
3916
+ if (stat) {
3917
+ const s = (entry.status || '—').toUpperCase();
3918
+ const c = entry.status === 'completed' ? 'var(--green,#4caf50)' : entry.status === 'error' ? '#EF5350' : entry.status === 'running' ? '#4caf50' : 'var(--muted)';
3919
+ stat.innerHTML = `<span style="color:${c}">${escHtml(s)}</span>`;
3920
+ }
3921
+ if (durEl) {
3922
+ const d = entry.durationMs ? fmtDuration(entry.durationMs) : (entry.startedAt && entry.endedAt ? fmtDuration(new Date(entry.endedAt) - new Date(entry.startedAt)) : '—');
3923
+ durEl.textContent = d;
3924
+ }
3925
+
3926
+ closeSwarmAgent();
3927
+ drawSwarmTopology(entry, null);
3928
+
3929
+ const agentList = document.getElementById('po-swarm-agent-list');
3930
+ if (agentList) {
3931
+ const agents = entry.agents || [];
3932
+ if (agents.length === 0) {
3933
+ agentList.innerHTML = '<div style="color:var(--muted);font-size:9px;">No agents recorded</div>';
3934
+ } else {
3935
+ agentList.innerHTML = agents.map(a => {
3936
+ const dotColor = (a.tasksFailed || 0) > 0 ? '#EF5350' : '#4caf50';
3937
+ const isQueen = (a.role || '').toLowerCase().includes('queen') || (a.role || '').toLowerCase().includes('coordinator') || (a.role || '').toLowerCase().includes('lead');
3938
+ return `<div class="po-swarm-agent-item" data-agent-id="${escHtml(a.id || a.type)}" onclick="selectSwarmAgent('${escHtml(a.id || a.type)}')">
3939
+ <span class="po-swarm-agent-dot" style="background:${dotColor}"></span>
3940
+ <span class="po-swarm-agent-type">${escHtml(a.type || '?')}</span>
3941
+ <span class="po-swarm-agent-role">${isQueen ? 'QUEEN' : 'WORKER'}</span>
3942
+ <span class="po-swarm-agent-stats">${a.tasksCompleted || 0} tasks · ${a.messageCount || 0} msgs</span>
3943
+ </div>`;
3944
+ }).join('');
3945
+ }
3946
+ }
3947
+
3948
+ // Populate event log section
3949
+ const evList = document.getElementById('po-swarm-events-list');
3950
+ if (evList) {
3951
+ const dir = selectedProjectDir || '';
3952
+ try {
3953
+ const evr = await fetch(`/api/swarm-events${dir ? '?dir=' + encodeURIComponent(dir) : ''}`);
3954
+ if (evr.ok) {
3955
+ const ed = await evr.json();
3956
+ const evts = (ed.events || []).reverse();
3957
+ if (evts.length === 0) {
3958
+ evList.innerHTML = '<div style="color:var(--muted);font-size:9px;">No events recorded yet</div>';
3959
+ } else {
3960
+ evList.innerHTML = evts.slice(0, 200).map(ev => {
3961
+ const ts = ev.ts ? new Date(ev.ts).toTimeString().slice(0, 8) : '—';
3962
+ const kind = ev.kind || '?';
3963
+ const detail = ev.agentId || ev.swarmId || ev.message || ev.proposalId || '';
3964
+ return `<div class="po-swarm-event-row">
3965
+ <span class="po-swarm-event-ts">${escHtml(ts)}</span>
3966
+ <span class="po-swarm-event-kind">${escHtml(kind)}</span>
3967
+ <span class="po-swarm-event-detail">${escHtml(String(detail).slice(0, 80))}</span>
3968
+ </div>`;
3969
+ }).join('');
3970
+ }
3971
+ }
3972
+ } catch {
3973
+ evList.innerHTML = '<div style="color:var(--muted);font-size:9px;">Failed to load events</div>';
3974
+ }
3975
+ }
3976
+ }
3977
+
3978
+ function fmtDuration(ms) {
3979
+ if (!ms || ms < 0) return '—';
3980
+ const sec = Math.floor(ms / 1000);
3981
+ if (sec < 60) return sec + 's';
3982
+ const min = Math.floor(sec / 60);
3983
+ const remSec = sec % 60;
3984
+ if (min < 60) return min + 'm ' + remSec + 's';
3985
+ const hr = Math.floor(min / 60);
3986
+ return hr + 'h ' + (min % 60) + 'm';
3987
+ }
3988
+
3989
+ function drawSwarmTopology(entry, highlightAgentId) {
3990
+ const wrap = document.getElementById('po-swarm-canvas-wrap');
3402
3991
  const canvas = document.getElementById('po-swarm-canvas');
3403
- if (!canvas) return;
3404
- if (!canvas.offsetWidth) { requestAnimationFrame(renderPalaceSwarm); return; }
3992
+ const label = document.getElementById('po-swarm-topo-label');
3993
+ if (!canvas || !wrap) return;
3994
+
3995
+ if (label) label.textContent = entry.status === 'running' ? 'LIVE TOPOLOGY' : 'TOPOLOGY';
3996
+
3997
+ if (!wrap.offsetWidth || !wrap.offsetHeight) {
3998
+ requestAnimationFrame(() => drawSwarmTopology(entry, highlightAgentId));
3999
+ return;
4000
+ }
4001
+
3405
4002
  const dpr = window.devicePixelRatio || 1;
3406
- canvas.width = canvas.offsetWidth * dpr;
3407
- canvas.height = canvas.offsetHeight * dpr;
4003
+ canvas.width = wrap.offsetWidth * dpr;
4004
+ canvas.height = wrap.offsetHeight * dpr;
4005
+ canvas.style.width = wrap.offsetWidth + 'px';
4006
+ canvas.style.height = wrap.offsetHeight + 'px';
3408
4007
  const ctx = canvas.getContext('2d');
3409
4008
  ctx.clearRect(0, 0, canvas.width, canvas.height);
3410
4009
  const CW = canvas.width, CH = canvas.height;
3411
4010
 
3412
- // Grid background
3413
4011
  const step = 30 * dpr;
3414
4012
  ctx.strokeStyle = 'rgba(58,58,90,0.15)';
3415
4013
  ctx.lineWidth = dpr * 0.5;
3416
- for (let gx = 0; gx <= CW; gx += step) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,CH); ctx.stroke(); }
3417
- for (let gy = 0; gy <= CH; gy += step) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(CW,gy); ctx.stroke(); }
4014
+ for (let gx = 0; gx <= CW; gx += step) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, CH); ctx.stroke(); }
4015
+ for (let gy = 0; gy <= CH; gy += step) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(CW, gy); ctx.stroke(); }
4016
+
4017
+ const agents = entry.agents || [];
4018
+ const n = agents.length;
4019
+ if (n === 0) {
4020
+ ctx.fillStyle = 'rgba(100,100,140,0.6)';
4021
+ ctx.font = `500 ${9 * dpr}px "Azeret Mono", monospace`;
4022
+ ctx.textAlign = 'center';
4023
+ ctx.fillText('NO AGENTS', CW / 2, CH / 2);
4024
+ return;
4025
+ }
3418
4026
 
3419
- const consensusLow = consensus.toLowerCase();
3420
4027
  const cx = CW / 2, cy = CH / 2;
4028
+ const r = Math.min(CW, CH) * 0.36;
4029
+ const topo = (entry.topology || '').toLowerCase();
3421
4030
 
3422
- if (hasLiveSwarm) {
3423
- // Live: draw actual agent nodes
3424
- const agentList = (state.agents || []).slice(0, 50);
3425
- const n = agentList.length || 1;
3426
- const r = Math.min(CW, CH) * 0.36;
3427
- const positions = agentList.map((_, i) => {
3428
- const angle = (i / n) * Math.PI * 2 - Math.PI / 2;
3429
- return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];
3430
- });
3431
- // Edges
3432
- ctx.strokeStyle = 'rgba(0,229,200,0.15)';
3433
- ctx.lineWidth = dpr;
3434
- if (consensusLow === 'byzantine' || consensusLow === 'mesh') {
3435
- for (let i = 0; i < n; i++) {
3436
- for (let j = i+1; j <= i+2 && j < n; j++) {
3437
- ctx.beginPath(); ctx.moveTo(positions[i][0], positions[i][1]); ctx.lineTo(positions[j][0], positions[j][1]); ctx.stroke();
3438
- }
4031
+ const queenIdx = agents.findIndex(a => {
4032
+ const role = (a.role || '').toLowerCase();
4033
+ return role.includes('queen') || role.includes('coordinator') || role.includes('lead');
4034
+ });
4035
+
4036
+ const workers = agents.filter((_, i) => i !== queenIdx);
4037
+ const wn = workers.length;
4038
+
4039
+ const positions = new Array(n);
4040
+ if (queenIdx >= 0) positions[queenIdx] = [cx, cy];
4041
+ let wi = 0;
4042
+ agents.forEach((_, i) => {
4043
+ if (i === queenIdx) return;
4044
+ const angle = (wi / wn) * Math.PI * 2 - Math.PI / 2;
4045
+ positions[i] = [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];
4046
+ wi++;
4047
+ });
4048
+
4049
+ ctx.lineWidth = dpr;
4050
+ if (topo === 'mesh' || topo === 'hierarchical-mesh') {
4051
+ ctx.strokeStyle = 'rgba(0,229,200,0.12)';
4052
+ const workerPositions = agents.map((_, i) => i !== queenIdx ? positions[i] : null).filter(Boolean);
4053
+ for (let i = 0; i < workerPositions.length; i++) {
4054
+ for (let j = i + 1; j <= i + 2 && j < workerPositions.length; j++) {
4055
+ ctx.beginPath(); ctx.moveTo(workerPositions[i][0], workerPositions[i][1]); ctx.lineTo(workerPositions[j][0], workerPositions[j][1]); ctx.stroke();
3439
4056
  }
3440
- } else {
3441
- positions.forEach(([nx, ny]) => { ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(nx, ny); ctx.stroke(); });
3442
4057
  }
3443
- // Nodes
3444
- positions.forEach(([nx, ny], i) => {
3445
- ctx.beginPath(); ctx.arc(nx, ny, 5 * dpr, 0, Math.PI * 2);
3446
- ctx.fillStyle = 'rgba(0,229,200,0.4)'; ctx.fill();
3447
- ctx.strokeStyle = 'rgba(0,229,200,0.7)'; ctx.lineWidth = dpr; ctx.stroke();
3448
- // Agent label
3449
- const a = agentList[i];
3450
- const lbl = (a.type || a.role || '?').slice(0, 6).toUpperCase();
3451
- ctx.fillStyle = 'rgba(180,180,220,0.6)';
3452
- ctx.font = `${7 * dpr}px "Azeret Mono", monospace`;
3453
- ctx.textAlign = 'center';
3454
- ctx.fillText(lbl, nx, ny + 10 * dpr);
4058
+ }
4059
+ if (topo === 'ring') {
4060
+ ctx.strokeStyle = 'rgba(0,229,200,0.15)';
4061
+ const workerPositions = agents.map((_, i) => i !== queenIdx ? positions[i] : null).filter(Boolean);
4062
+ for (let i = 0; i < workerPositions.length; i++) {
4063
+ const j = (i + 1) % workerPositions.length;
4064
+ ctx.beginPath(); ctx.moveTo(workerPositions[i][0], workerPositions[i][1]); ctx.lineTo(workerPositions[j][0], workerPositions[j][1]); ctx.stroke();
4065
+ }
4066
+ }
4067
+ if (topo === 'hierarchical' || topo === 'star' || topo === 'hierarchical-mesh') {
4068
+ ctx.strokeStyle = 'rgba(0,229,200,0.15)';
4069
+ agents.forEach((_, i) => {
4070
+ if (i === queenIdx) return;
4071
+ ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(positions[i][0], positions[i][1]); ctx.stroke();
3455
4072
  });
3456
- // Coordinator
3457
- ctx.beginPath(); ctx.arc(cx, cy, 9 * dpr, 0, Math.PI * 2);
3458
- ctx.fillStyle = 'rgba(0,229,200,0.7)'; ctx.fill();
3459
- ctx.strokeStyle = 'rgba(0,229,200,1)'; ctx.lineWidth = dpr * 2; ctx.stroke();
4073
+ }
4074
+
4075
+ agents.forEach((a, i) => {
4076
+ if (i === queenIdx) return;
4077
+ const [nx, ny] = positions[i];
4078
+ const agentId = a.id || a.type;
4079
+ const isHighlighted = highlightAgentId && agentId === highlightAgentId;
4080
+
4081
+ const nodeR = isHighlighted ? 7 * dpr : 5 * dpr;
4082
+ ctx.beginPath(); ctx.arc(nx, ny, nodeR, 0, Math.PI * 2);
4083
+ if (isHighlighted) {
4084
+ ctx.fillStyle = 'rgba(255,180,0,0.4)'; ctx.fill();
4085
+ ctx.strokeStyle = 'rgba(255,180,0,0.8)'; ctx.lineWidth = dpr * 1.5;
4086
+ } else {
4087
+ ctx.fillStyle = 'rgba(0,229,200,0.3)'; ctx.fill();
4088
+ ctx.strokeStyle = 'rgba(0,229,200,0.6)'; ctx.lineWidth = dpr;
4089
+ }
4090
+ ctx.stroke();
4091
+
4092
+ const lbl = (a.type || '?').slice(0, 8).toUpperCase();
4093
+ ctx.fillStyle = isHighlighted ? 'rgba(255,180,0,0.7)' : 'rgba(180,180,220,0.6)';
4094
+ ctx.font = `${7 * dpr}px "Azeret Mono", monospace`;
4095
+ ctx.textAlign = 'center';
4096
+ ctx.fillText(lbl, nx, ny + 10 * dpr);
4097
+ });
4098
+
4099
+ if (queenIdx >= 0) {
4100
+ const queen = agents[queenIdx];
4101
+ const queenId = queen.id || queen.type;
4102
+ const isHighlighted = highlightAgentId && queenId === highlightAgentId;
4103
+ const qR = isHighlighted ? 11 * dpr : 9 * dpr;
4104
+ ctx.beginPath(); ctx.arc(cx, cy, qR, 0, Math.PI * 2);
4105
+ if (isHighlighted) {
4106
+ ctx.fillStyle = 'rgba(255,180,0,0.7)'; ctx.fill();
4107
+ ctx.strokeStyle = 'rgba(255,180,0,1)';
4108
+ } else {
4109
+ ctx.fillStyle = 'rgba(0,229,200,0.7)'; ctx.fill();
4110
+ ctx.strokeStyle = 'rgba(0,229,200,1)';
4111
+ }
4112
+ ctx.lineWidth = dpr * 2; ctx.stroke();
3460
4113
  ctx.fillStyle = '#fff';
3461
4114
  ctx.font = `bold ${8 * dpr}px "Azeret Mono", monospace`;
3462
4115
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
3463
4116
  ctx.fillText('Q', cx, cy);
3464
4117
  ctx.textBaseline = 'alphabetic';
3465
- } else {
3466
- // Idle: representative static diagram
3467
- const nodeCount = Math.min(10, Math.max(4, agentCount > 0 ? 7 : 4));
3468
- const r = Math.min(CW, CH) * 0.36;
3469
- const positions = [];
3470
- for (let i = 0; i < nodeCount; i++) {
3471
- const angle = (i / nodeCount) * Math.PI * 2 - Math.PI / 2;
3472
- positions.push([cx + r * Math.cos(angle), cy + r * Math.sin(angle)]);
3473
- }
3474
- // Edges
3475
- ctx.strokeStyle = 'rgba(0,229,200,0.10)';
3476
- ctx.lineWidth = dpr;
3477
- if (consensusLow === 'byzantine' || consensusLow === 'mesh') {
3478
- for (let i = 0; i < nodeCount; i++) {
3479
- for (let j = i+1; j <= i+3 && j < nodeCount; j++) {
3480
- ctx.beginPath(); ctx.moveTo(positions[i][0], positions[i][1]); ctx.lineTo(positions[j][0], positions[j][1]); ctx.stroke();
3481
- }
3482
- }
4118
+ }
4119
+ }
4120
+
4121
+ async function selectSwarmAgent(agentId) {
4122
+ _swarmSelectedAgentId = agentId;
4123
+
4124
+ const entry = _swarmSelectedIdx === 'live'
4125
+ ? _buildLiveEntry()
4126
+ : _swarmHistoryEntries[_swarmSelectedIdx];
4127
+ if (!entry) return;
4128
+
4129
+ document.querySelectorAll('.po-swarm-agent-item').forEach(el => {
4130
+ el.classList.toggle('selected', el.dataset.agentId === agentId);
4131
+ });
4132
+
4133
+ const wrap = document.getElementById('po-swarm-canvas-wrap');
4134
+ if (wrap) wrap.classList.add('shrunk');
4135
+
4136
+ drawSwarmTopology(entry, agentId);
4137
+
4138
+ const agent = (entry.agents || []).find(a => (a.id || a.type) === agentId);
4139
+ if (!agent) return;
4140
+
4141
+ const drawer = document.getElementById('po-swarm-agent-drawer');
4142
+ const info = document.getElementById('po-swarm-agent-info');
4143
+ const timeline = document.getElementById('po-swarm-agent-timeline');
4144
+ if (!drawer) return;
4145
+ drawer.style.display = '';
4146
+
4147
+ const isQueen = (agent.role || '').toLowerCase().includes('queen') || (agent.role || '').toLowerCase().includes('coordinator') || (agent.role || '').toLowerCase().includes('lead');
4148
+ if (info) {
4149
+ info.innerHTML = `
4150
+ <span style="color:var(--teal);font-weight:700;">${escHtml(agent.type || '?')}</span>
4151
+ <span style="color:var(--muted);">${isQueen ? 'QUEEN' : 'WORKER'}</span>
4152
+ <span><span style="color:var(--muted);">TASKS</span> <span style="color:var(--teal);">${agent.tasksCompleted || 0}/${(agent.tasksCompleted || 0) + (agent.tasksFailed || 0)}</span></span>
4153
+ <span><span style="color:var(--muted);">MSGS</span> <span style="color:var(--teal);">${agent.messageCount || 0}</span></span>
4154
+ <span><span style="color:var(--muted);">UTIL</span> <span style="color:#4caf50;">${agent.utilization || 0}%</span></span>
4155
+ `;
4156
+ }
4157
+
4158
+ if (timeline) {
4159
+ // Fetch events from API for this agent
4160
+ const dir = selectedProjectDir || '';
4161
+ let events = [];
4162
+ try {
4163
+ const evr = await fetch(`/api/swarm-events?agentId=${encodeURIComponent(agentId)}${dir ? '&dir=' + encodeURIComponent(dir) : ''}`);
4164
+ if (evr.ok) { const ed = await evr.json(); events = ed.events || []; }
4165
+ } catch {}
4166
+
4167
+ // Also include entry-level messages (legacy)
4168
+ const msgs = (entry.messages || []).filter(m => m.from === agentId || m.to === agentId);
4169
+
4170
+ if (events.length === 0 && msgs.length === 0) {
4171
+ timeline.innerHTML = '<div style="color:var(--muted);font-size:9px;padding:4px;">No communication recorded</div>';
3483
4172
  } else {
3484
- positions.forEach(([nx, ny]) => { ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(nx, ny); ctx.stroke(); });
3485
- }
3486
- // Outer nodes
3487
- positions.forEach(([nx, ny]) => {
3488
- ctx.beginPath(); ctx.arc(nx, ny, 6 * dpr, 0, Math.PI * 2);
3489
- ctx.fillStyle = 'rgba(0,229,200,0.2)'; ctx.fill();
3490
- ctx.strokeStyle = 'rgba(0,229,200,0.45)'; ctx.lineWidth = dpr; ctx.stroke();
3491
- });
3492
- // Center coordinator (if not pure mesh)
3493
- if (consensusLow !== 'mesh') {
3494
- ctx.beginPath(); ctx.arc(cx, cy, 9 * dpr, 0, Math.PI * 2);
3495
- ctx.fillStyle = 'rgba(0,229,200,0.35)'; ctx.fill();
3496
- ctx.strokeStyle = 'rgba(0,229,200,0.7)'; ctx.lineWidth = dpr * 1.5; ctx.stroke();
3497
- }
3498
- // Footer label
3499
- ctx.fillStyle = 'rgba(100,100,140,0.6)';
3500
- ctx.font = `500 ${9 * dpr}px "Azeret Mono", monospace`;
3501
- ctx.textAlign = 'center';
3502
- const footerLabel = agentCount > 0
3503
- ? `LAST SWARM · ${agentCount} AGENTS`
3504
- : 'NO SWARM HISTORY';
3505
- ctx.fillText(footerLabel, CW / 2, CH - 14 * dpr);
3506
- // Topology label in center for mesh
3507
- if (consensusLow === 'mesh' || consensusLow === 'byzantine') {
3508
- ctx.fillStyle = 'rgba(0,229,200,0.25)';
3509
- ctx.font = `bold ${10 * dpr}px "Syne", sans-serif`;
3510
- ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
3511
- ctx.fillText(consensusLow.toUpperCase(), cx, cy);
3512
- ctx.textBaseline = 'alphabetic';
4173
+ let html = '';
4174
+ // Render events from events.jsonl
4175
+ if (events.length > 0) {
4176
+ html += events.map(ev => {
4177
+ const ts = ev.ts ? new Date(ev.ts).toTimeString().slice(0, 8) : '—';
4178
+ const kind = ev.kind || '?';
4179
+ const detail = ev.message || ev.task || ev.topology || '';
4180
+ const isSent = kind.includes('spawn') || kind.includes('init') || kind.includes('broadcast');
4181
+ return `<div class="po-swarm-msg ${isSent ? 'sent' : 'received'}">
4182
+ <span class="po-swarm-msg-ts">${escHtml(ts)}</span>
4183
+ <span class="po-swarm-msg-type">${escHtml(kind)}</span>
4184
+ ${detail ? `<span class="po-swarm-msg-payload">${escHtml(String(detail).slice(0, 120))}</span>` : ''}
4185
+ </div>`;
4186
+ }).join('');
4187
+ }
4188
+ // Render legacy messages
4189
+ if (msgs.length > 0) {
4190
+ html += msgs.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0)).map(m => {
4191
+ const isSent = m.from === agentId;
4192
+ const dir = isSent ? '→' : '←';
4193
+ const peer = isSent ? m.to : m.from;
4194
+ const ts = m.timestamp ? new Date(m.timestamp).toTimeString().slice(0, 8) : '—';
4195
+ const payload = (m.payload || '').slice(0, 100);
4196
+ return `<div class="po-swarm-msg ${isSent ? 'sent' : 'received'}">
4197
+ <span class="po-swarm-msg-ts">${escHtml(ts)}</span>
4198
+ ${escHtml(dir)} ${escHtml(peer || 'broadcast')}:
4199
+ <span class="po-swarm-msg-type">${escHtml(m.type || '?')}</span>
4200
+ ${payload ? `<span class="po-swarm-msg-payload">${escHtml(payload)}</span>` : ''}
4201
+ </div>`;
4202
+ }).join('');
4203
+ }
4204
+ timeline.innerHTML = html;
3513
4205
  }
3514
4206
  }
3515
4207
  }
3516
4208
 
4209
+ function _buildLiveEntry() {
4210
+ const sw = (appData && appData.swarm) ? appData.swarm : {};
4211
+ const state = sw.state || {};
4212
+ return {
4213
+ swarmId: state.swarmId || state.id || 'live',
4214
+ topology: state.topology || sw.config?.topology || '—',
4215
+ consensus: state.consensus || sw.config?.consensus || '—',
4216
+ status: state.status || 'running',
4217
+ agents: (state.agents || state.agentPlan || []).map(a => ({
4218
+ id: a.id || a.type || a.role,
4219
+ type: a.type || a.role || '?',
4220
+ role: a.role || 'worker',
4221
+ tasksCompleted: a.tasksCompleted || 0,
4222
+ tasksFailed: a.tasksFailed || 0,
4223
+ messageCount: a.messageCount || 0,
4224
+ utilization: a.utilization || 0,
4225
+ })),
4226
+ messages: state.messages || [],
4227
+ errors: state.errors || [],
4228
+ startedAt: state.startedAt || state.createdAt,
4229
+ endedAt: null,
4230
+ durationMs: state.startedAt ? Date.now() - new Date(state.startedAt).getTime() : 0,
4231
+ };
4232
+ }
4233
+
4234
+ function closeSwarmAgent() {
4235
+ _swarmSelectedAgentId = null;
4236
+ const drawer = document.getElementById('po-swarm-agent-drawer');
4237
+ if (drawer) drawer.style.display = 'none';
4238
+ const wrap = document.getElementById('po-swarm-canvas-wrap');
4239
+ if (wrap) wrap.classList.remove('shrunk');
4240
+ document.querySelectorAll('.po-swarm-agent-item').forEach(el => el.classList.remove('selected'));
4241
+
4242
+ const entry = _swarmSelectedIdx === 'live'
4243
+ ? _buildLiveEntry()
4244
+ : _swarmHistoryEntries[_swarmSelectedIdx];
4245
+ if (entry) drawSwarmTopology(entry, null);
4246
+ }
4247
+
3517
4248
  async function renderPalaceKnowledge() {
3518
4249
  const statsEl = document.getElementById('po-knowledge-stats');
3519
4250
  const bodyEl = document.getElementById('po-knowledge-body');
@@ -4020,194 +4751,126 @@ window.selectTemplate = function(type) {
4020
4751
  setTimeout(() => document.getElementById('po-edit-textarea').focus(), 50);
4021
4752
  };
4022
4753
 
4023
- // Agent Graph renderer — sessions (inner ring, teal) + agent types (outer ring, amber)
4024
- const kgGraph = (function() {
4025
- let canvas, ctx, nodes = [], edges = [], animRAF = null, showLabels = true;
4026
- let mouseX = -999, mouseY = -999, hoveredNode = null;
4027
-
4028
- function init() {
4029
- canvas = document.getElementById('po-kg-canvas');
4030
- if (!canvas) return;
4031
- ctx = canvas.getContext('2d');
4032
- canvas.onmousemove = e => {
4033
- const r = canvas.getBoundingClientRect();
4034
- mouseX = e.clientX - r.left;
4035
- mouseY = e.clientY - r.top;
4036
- };
4037
- canvas.onmouseleave = () => { mouseX = -999; mouseY = -999; hoveredNode = null; };
4038
- }
4039
-
4040
- function render(graphData) {
4041
- if (!canvas) init();
4042
- if (!canvas) return;
4043
- if (!graphData || !graphData.nodes) return;
4044
-
4045
- const sessionNodes = graphData.nodes.filter(n => n.type === 'session')
4046
- .sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
4047
- const agentNodes = graphData.nodes.filter(n => n.type === 'agenttype')
4048
- .sort((a, b) => (b.totalSpawns || 0) - (a.totalSpawns || 0));
4049
-
4050
- canvas.width = canvas.offsetWidth;
4051
- canvas.height = canvas.offsetHeight;
4052
- const W = canvas.width, H = canvas.height;
4053
- const cx = W / 2, cy = H / 2;
4054
- const innerR = Math.min(W, H) * 0.22;
4055
- const outerR = Math.min(W, H) * 0.40;
4056
-
4057
- const maxTools = Math.max(...sessionNodes.map(n => n.totalTools || 0), 1);
4058
- const maxSpawns = Math.max(...agentNodes.map(n => n.totalSpawns || 0), 1);
4059
-
4060
- // Session nodes on inner ring, sized by tool count
4061
- sessionNodes.forEach((n, i) => {
4062
- const angle = (i / Math.max(sessionNodes.length, 1)) * Math.PI * 2 - Math.PI / 2;
4063
- n._x = cx + Math.cos(angle) * innerR;
4064
- n._y = cy + Math.sin(angle) * innerR;
4065
- n._size = 4 + (n.totalTools / maxTools) * 9;
4066
- });
4067
-
4068
- // Agent type nodes on outer ring, sized by spawn count
4069
- agentNodes.forEach((n, i) => {
4070
- const angle = (i / Math.max(agentNodes.length, 1)) * Math.PI * 2 - Math.PI / 2;
4071
- n._x = cx + Math.cos(angle) * outerR;
4072
- n._y = cy + Math.sin(angle) * outerR;
4073
- n._size = 3 + (n.totalSpawns / maxSpawns) * 7;
4074
- });
4075
-
4076
- nodes = [...sessionNodes, ...agentNodes];
4077
- const nodeIds = new Set(nodes.map(n => n.id));
4078
- edges = graphData.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target));
4079
-
4080
- const totalSpawns = agentNodes.reduce((s, n) => s + (n.totalSpawns || 0), 0);
4081
- const info = document.getElementById('po-graph-info');
4082
- if (info) info.textContent = `${sessionNodes.length} sessions · ${agentNodes.length} agent types · ${totalSpawns} total spawns`;
4083
-
4084
- if (animRAF) cancelAnimationFrame(animRAF);
4085
- draw();
4086
- animRAF = requestAnimationFrame(function loop() {
4087
- if (palaceCurrentTab !== 'graph') { animRAF = null; return; }
4088
- const prev = hoveredNode;
4089
- hoveredNode = null;
4090
- for (const n of nodes) {
4091
- const dx = n._x - mouseX, dy = n._y - mouseY;
4092
- const r = (n._size || 5) + 4;
4093
- if (dx*dx + dy*dy < r*r) { hoveredNode = n; break; }
4094
- }
4095
- if (hoveredNode !== prev) draw();
4096
- animRAF = requestAnimationFrame(loop);
4097
- });
4098
- }
4099
-
4100
- function draw() {
4101
- if (!canvas || !ctx) return;
4102
- const W = canvas.width, H = canvas.height;
4103
- if (!W || !H) return;
4104
- ctx.clearRect(0, 0, W, H);
4105
- const cx = W/2, cy = H/2;
4106
- const innerR = Math.min(W,H)*0.22, outerR = Math.min(W,H)*0.40;
4107
-
4108
- // Guide rings
4109
- ctx.beginPath(); ctx.arc(cx, cy, innerR, 0, Math.PI*2);
4110
- ctx.strokeStyle = 'rgba(0,229,200,0.06)'; ctx.lineWidth = 1; ctx.stroke();
4111
- ctx.beginPath(); ctx.arc(cx, cy, outerR, 0, Math.PI*2);
4112
- ctx.strokeStyle = 'rgba(255,179,71,0.06)'; ctx.lineWidth = 1; ctx.stroke();
4113
-
4114
- // Ring labels at top
4115
- ctx.font = '8px monospace'; ctx.textAlign = 'center';
4116
- ctx.fillStyle = 'rgba(0,229,200,0.3)';
4117
- ctx.fillText('SESSIONS', cx, cy - innerR - 6);
4118
- ctx.fillStyle = 'rgba(255,179,71,0.3)';
4119
- ctx.fillText('AGENT TYPES', cx, cy - outerR - 6);
4120
-
4121
- // Edges
4122
- const maxW = Math.max(...edges.map(e => e.weight || 1), 1);
4123
- const idToNode = {};
4124
- for (const n of nodes) idToNode[n.id] = n;
4125
- for (const e of edges) {
4126
- const a = idToNode[e.source], b = idToNode[e.target];
4127
- if (!a || !b) continue;
4128
- const hl = hoveredNode && (hoveredNode.id === e.source || hoveredNode.id === e.target);
4129
- const alpha = 0.05 + (e.weight / maxW) * 0.22;
4130
- ctx.beginPath(); ctx.moveTo(a._x, a._y); ctx.lineTo(b._x, b._y);
4131
- ctx.strokeStyle = hl ? 'rgba(0,229,200,0.65)' : `rgba(120,128,168,${alpha.toFixed(2)})`;
4132
- ctx.lineWidth = hl ? Math.max(1, (e.weight / maxW) * 3) : Math.max(0.4, (e.weight / maxW) * 1.5);
4133
- ctx.stroke();
4134
- if (showLabels && hl) {
4135
- ctx.font = '8px monospace'; ctx.fillStyle = 'rgba(0,229,200,0.9)'; ctx.textAlign = 'center';
4136
- ctx.fillText('×' + e.label, (a._x+b._x)/2, (a._y+b._y)/2 - 4);
4137
- }
4138
- }
4139
-
4140
- // Nodes
4141
- for (const n of nodes) {
4142
- const isHov = hoveredNode && hoveredNode.id === n.id;
4143
- const isSession = n.type === 'session';
4144
- const r = (n._size || 5) + (isHov ? 2 : 0);
4145
- if (isHov) { ctx.shadowBlur = 18; ctx.shadowColor = isSession ? '#00E5C8' : '#FFB347'; }
4146
- ctx.beginPath(); ctx.arc(n._x, n._y, r, 0, Math.PI*2);
4147
- ctx.fillStyle = isSession
4148
- ? (isHov ? '#00E5C8' : 'rgba(0,229,200,0.82)')
4149
- : (isHov ? '#FFB347' : 'rgba(255,179,71,0.78)');
4150
- ctx.fill(); ctx.shadowBlur = 0;
4151
- if (showLabels) {
4152
- const label = (n.label || n.id).length > 18 ? (n.label || n.id).slice(0, 16) + '…' : (n.label || n.id);
4153
- ctx.font = isHov ? '10px monospace' : '8px monospace';
4154
- ctx.fillStyle = isHov ? 'rgba(255,255,255,0.95)'
4155
- : (isSession ? 'rgba(0,229,200,0.65)' : 'rgba(255,179,71,0.6)');
4156
- ctx.textAlign = 'center';
4157
- ctx.fillText(label, n._x, n._y + r + 11);
4158
- }
4159
- }
4160
-
4161
- // Tooltip
4162
- if (hoveredNode) {
4163
- const n = hoveredNode;
4164
- const isSession = n.type === 'session';
4165
- const lines = [isSession ? n.label + ' (session)' : n.label + ' (agent type)'];
4166
- if (isSession) {
4167
- lines.push(`${n.turns || 0} turns · ${n.totalTools || 0} tools · $${(n.cost || 0).toFixed(3)}`);
4168
- const ts = n.mtime ? new Date(n.mtime).toLocaleDateString() : '';
4169
- if (ts) lines.push(ts);
4170
- const spawned = Object.entries(n.agentSpawns || {}).sort((a,b) => b[1]-a[1]).slice(0, 5);
4171
- spawned.forEach(([k,v]) => lines.push(`→ ${k.slice(0,22)} ×${v}`));
4172
- } else {
4173
- lines.push(`${n.totalSpawns} total spawns`);
4174
- const ne = edges.filter(e => e.source === n.id || e.target === n.id).slice(0, 5);
4175
- ne.forEach(e => {
4176
- const other = e.source === n.id ? e.target : e.source;
4177
- lines.push(`${other.slice(0, 8)}… ×${e.weight}`);
4178
- });
4179
- }
4180
- const tw = 290, th = lines.length * 14 + 16;
4181
- let tx = n._x + 14, ty = n._y - th / 2;
4182
- if (tx + tw > W - 4) tx = n._x - tw - 14;
4183
- if (ty < 4) ty = 4;
4184
- if (ty + th > H - 4) ty = H - th - 4;
4185
- ctx.fillStyle = 'rgba(7,8,15,0.95)'; ctx.strokeStyle = 'rgba(0,229,200,0.28)'; ctx.lineWidth = 1;
4186
- roundRect(ctx, tx, ty, tw, th, 4); ctx.fill(); ctx.stroke();
4187
- ctx.font = '9px monospace'; ctx.textAlign = 'left';
4188
- lines.forEach((l, i) => {
4189
- ctx.fillStyle = i === 0 ? 'rgba(255,255,255,0.92)'
4190
- : i === 1 ? (isSession ? 'rgba(0,229,200,0.65)' : 'rgba(255,179,71,0.65)')
4191
- : 'rgba(212,212,232,0.8)';
4192
- ctx.fillText(l, tx + 8, ty + 13 + i * 14);
4193
- });
4194
- }
4754
+ // Agent Graph renderer — HTML two-pane: session list on left, details on right
4755
+ let _agGraphData = null;
4756
+
4757
+ function renderAgentGraph(graphData) {
4758
+ _agGraphData = graphData;
4759
+
4760
+ const sessionNodes = (graphData.nodes || []).filter(n => n.type === 'session')
4761
+ .sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
4762
+ const agentTypeNodes = (graphData.nodes || []).filter(n => n.type === 'agenttype')
4763
+ .sort((a, b) => (b.totalSpawns || 0) - (a.totalSpawns || 0));
4764
+
4765
+ const countEl = document.getElementById('po-ag-session-count');
4766
+ if (countEl) countEl.textContent = sessionNodes.length + ' sessions';
4767
+
4768
+ const totalTools = sessionNodes.reduce((s, n) => s + (n.totalTools || 0), 0);
4769
+ const totalCost = sessionNodes.reduce((s, n) => s + (n.cost || 0), 0);
4770
+ const totalTurns = sessionNodes.reduce((s, n) => s + (n.turns || 0), 0);
4771
+ const totalSpawns = agentTypeNodes.reduce((s, n) => s + (n.totalSpawns || 0), 0);
4772
+ const summaryBar = document.getElementById('po-ag-summary-bar');
4773
+ if (summaryBar) {
4774
+ summaryBar.innerHTML = [
4775
+ [sessionNodes.length, 'SESSIONS'],
4776
+ [agentTypeNodes.length, 'AGENT TYPES'],
4777
+ [totalSpawns, 'SPAWNS'],
4778
+ [totalTools, 'TOOL CALLS'],
4779
+ [totalTurns, 'TURNS'],
4780
+ ['$' + totalCost.toFixed(3), 'COST'],
4781
+ ].map(([v, l]) => `<div class="po-ag-stat"><span class="po-ag-stat-val">${v}</span><span class="po-ag-stat-lbl">${l}</span></div>`).join('');
4782
+ }
4783
+
4784
+ const listEl = document.getElementById('po-ag-session-list');
4785
+ if (!listEl) return;
4786
+
4787
+ if (!sessionNodes.length) {
4788
+ listEl.innerHTML = '<div style="padding:16px 12px;color:var(--dim);font-size:9px;text-align:center;">NO SESSIONS RECORDED</div>';
4789
+ const content = document.getElementById('po-ag-content');
4790
+ if (content) content.innerHTML = '<div class="po-select-hint">SELECT A SESSION</div>';
4791
+ return;
4195
4792
  }
4196
4793
 
4197
- function roundRect(c, x, y, w, h, r) {
4198
- c.beginPath();
4199
- c.moveTo(x+r,y); c.lineTo(x+w-r,y); c.quadraticCurveTo(x+w,y,x+w,y+r);
4200
- c.lineTo(x+w,y+h-r); c.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
4201
- c.lineTo(x+r,y+h); c.quadraticCurveTo(x,y+h,x,y+h-r);
4202
- c.lineTo(x,y+r); c.quadraticCurveTo(x,y,x+r,y);
4203
- c.closePath();
4204
- }
4794
+ listEl.innerHTML = sessionNodes.map(n => {
4795
+ const dt = n.mtime ? new Date(n.mtime) : null;
4796
+ const dateStr = dt ? dt.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
4797
+ const spawns = Object.values(n.agentSpawns || {}).reduce((s, v) => s + v, 0);
4798
+ return `<div class="po-ag-sess" data-id="${n.id}" onclick="selectAgentSession('${n.id}')">
4799
+ <div style="display:flex;justify-content:space-between;align-items:baseline;gap:4px;">
4800
+ <div class="po-ag-sess-id">${(n.label || n.id).slice(0, 20)}</div>
4801
+ <div style="font-size:8px;color:var(--dim);white-space:nowrap;">${dateStr}</div>
4802
+ </div>
4803
+ <div style="display:flex;gap:8px;margin-top:2px;">
4804
+ <span style="font-size:8px;color:var(--teal);">${n.totalTools || 0} tools</span>
4805
+ ${spawns > 0 ? `<span style="font-size:8px;color:var(--amber);">${spawns} spawns</span>` : ''}
4806
+ ${(n.cost || 0) > 0 ? `<span style="font-size:8px;color:var(--dim);">$${(n.cost || 0).toFixed(3)}</span>` : ''}
4807
+ </div>
4808
+ </div>`;
4809
+ }).join('');
4205
4810
 
4206
- function stop() { if (animRAF) { cancelAnimationFrame(animRAF); animRAF = null; } }
4207
- function toggleLabels(v) { showLabels = v; draw(); }
4811
+ const content = document.getElementById('po-ag-content');
4812
+ if (content) content.innerHTML = '<div class="po-select-hint">SELECT A SESSION</div>';
4813
+ }
4208
4814
 
4209
- return { init, render, stop, toggleLabels };
4210
- })();
4815
+ function selectAgentSession(id) {
4816
+ if (!_agGraphData) return;
4817
+ const n = _agGraphData.nodes.find(x => x.id === id);
4818
+ if (!n) return;
4819
+
4820
+ document.querySelectorAll('#po-ag-session-list .po-ag-sess').forEach(el =>
4821
+ el.classList.toggle('active', el.dataset.id === id));
4822
+
4823
+ const agentSpawns = n.agentSpawns || {};
4824
+ const toolCounts = n.toolCounts || {};
4825
+ const totalSpawns = Object.values(agentSpawns).reduce((s, v) => s + v, 0);
4826
+ const maxSpawn = Math.max(...Object.values(agentSpawns).concat([1]));
4827
+ const maxTool = Math.max(...Object.values(toolCounts).concat([1]));
4828
+ const dt = n.mtime ? new Date(n.mtime) : null;
4829
+ const dateStr = dt ? dt.toLocaleString() : '';
4830
+
4831
+ const spawnEntries = Object.entries(agentSpawns).sort((a, b) => b[1] - a[1]);
4832
+ const toolEntries = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]);
4833
+
4834
+ const content = document.getElementById('po-ag-content');
4835
+ if (!content) return;
4836
+
4837
+ content.innerHTML = `
4838
+ <div>
4839
+ <div class="po-ag-sess-title">${n.label || n.id}</div>
4840
+ <div class="po-ag-sess-subtitle">${dateStr}</div>
4841
+ <div style="display:flex;gap:14px;margin-top:8px;flex-wrap:wrap;">
4842
+ <span style="font-size:9px;"><span style="color:var(--teal)">${n.turns || 0}</span> <span style="color:var(--dim)">turns</span></span>
4843
+ <span style="font-size:9px;"><span style="color:var(--amber)">${totalSpawns}</span> <span style="color:var(--dim)">spawns</span></span>
4844
+ <span style="font-size:9px;"><span style="color:var(--teal)">${n.totalTools || 0}</span> <span style="color:var(--dim)">tool calls</span></span>
4845
+ ${(n.cost || 0) > 0 ? `<span style="font-size:9px;color:var(--muted);">$${(n.cost || 0).toFixed(4)}</span>` : ''}
4846
+ </div>
4847
+ </div>
4848
+ ${spawnEntries.length > 0 ? `
4849
+ <div>
4850
+ <div class="po-ag-section-lbl">AGENT TYPES SPAWNED</div>
4851
+ ${spawnEntries.map(([type, count]) => `
4852
+ <div class="po-ag-agent-card">
4853
+ <div class="po-ag-spawn-row">
4854
+ <span class="po-ag-agent-type">${type}</span>
4855
+ <div class="po-ag-bar-wrap"><div class="po-ag-bar-fill" style="width:${Math.round(count/maxSpawn*100)}%"></div></div>
4856
+ <span class="po-ag-spawn-count">×${count}</span>
4857
+ </div>
4858
+ </div>
4859
+ `).join('')}
4860
+ </div>` : ''}
4861
+ ${toolEntries.length > 0 ? `
4862
+ <div>
4863
+ <div class="po-ag-section-lbl">TOOL USAGE (top ${Math.min(toolEntries.length, 15)})</div>
4864
+ ${toolEntries.slice(0, 15).map(([tool, count]) => `
4865
+ <div class="po-ag-tool-row">
4866
+ <span class="po-ag-tool-name">${tool.slice(0, 18)}</span>
4867
+ <div class="po-ag-tool-bar-wrap"><div class="po-ag-tool-bar-fill" style="width:${Math.round(count/maxTool*100)}%"></div></div>
4868
+ <span class="po-ag-tool-count">${count}</span>
4869
+ </div>
4870
+ `).join('')}
4871
+ </div>` : '<div style="font-size:9px;color:var(--dim);text-align:center;padding:20px 0;">NO TOOL DATA</div>'}
4872
+ `;
4873
+ }
4211
4874
 
4212
4875
  // ══════════════════ CODE KNOWLEDGE GRAPH — force-directed ══════════════════
4213
4876
  const kgCodeGraph = (function() {
@@ -4388,7 +5051,6 @@ window.openPalaceOverlay = async function() {
4388
5051
  // Stop KG graph animations when overlay closes
4389
5052
  const _origClose = window.closePalaceOverlay;
4390
5053
  window.closePalaceOverlay = function() {
4391
- kgGraph.stop();
4392
5054
  kgCodeGraph.stop();
4393
5055
  _origClose();
4394
5056
  };