let-them-talk 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
package/dashboard.html CHANGED
@@ -321,6 +321,7 @@
321
321
  cursor: pointer;
322
322
  max-width: 80px;
323
323
  }
324
+ .agent-filter-bar select option { background: #1e2028; color: #e0e0e0; }
324
325
 
325
326
  /* ===== ROLE GROUP ===== */
326
327
  .role-group {
@@ -1144,6 +1145,7 @@
1144
1145
  }
1145
1146
 
1146
1147
  .input-target select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
1148
+ .input-target select option { background: #1e2028; color: #e0e0e0; }
1147
1149
 
1148
1150
  .input-msg {
1149
1151
  flex: 1;
@@ -1276,6 +1278,7 @@
1276
1278
  display: flex;
1277
1279
  gap: 8px;
1278
1280
  align-items: center;
1281
+ flex-wrap: wrap;
1279
1282
  }
1280
1283
 
1281
1284
  .search-input {
@@ -1294,6 +1297,12 @@
1294
1297
  .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
1295
1298
  .search-input::placeholder { color: var(--text-muted); }
1296
1299
 
1300
+ .search-input.omnibox-active {
1301
+ border-color: var(--accent);
1302
+ background: var(--accent-dim);
1303
+ box-shadow: 0 0 0 3px var(--accent-dim);
1304
+ }
1305
+
1297
1306
  .compact-toggle {
1298
1307
  background: var(--surface-2);
1299
1308
  border: 1px solid var(--border);
@@ -1308,6 +1317,52 @@
1308
1317
  .compact-toggle:hover { background: var(--surface-3); color: var(--text); }
1309
1318
  .compact-toggle.active { background: var(--accent-dim); color: var(--accent); border-color: var(--accent); }
1310
1319
 
1320
+ .workspace-layout-controls {
1321
+ display: flex;
1322
+ align-items: center;
1323
+ gap: 8px;
1324
+ margin-left: auto;
1325
+ flex-wrap: wrap;
1326
+ }
1327
+
1328
+ .workspace-layout-select,
1329
+ .workspace-layout-btn {
1330
+ background: var(--surface-2);
1331
+ border: 1px solid var(--border);
1332
+ border-radius: 6px;
1333
+ color: var(--text-dim);
1334
+ font-size: 11px;
1335
+ padding: 4px 8px;
1336
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
1337
+ }
1338
+
1339
+ .workspace-layout-select {
1340
+ min-width: 150px;
1341
+ cursor: pointer;
1342
+ outline: none;
1343
+ }
1344
+
1345
+ .workspace-layout-select:focus {
1346
+ border-color: var(--accent);
1347
+ box-shadow: 0 0 0 3px var(--accent-dim);
1348
+ }
1349
+
1350
+ .workspace-layout-btn {
1351
+ cursor: pointer;
1352
+ white-space: nowrap;
1353
+ }
1354
+
1355
+ .workspace-layout-btn:hover {
1356
+ background: var(--surface-3);
1357
+ color: var(--text);
1358
+ border-color: var(--border-light);
1359
+ }
1360
+
1361
+ .workspace-layout-btn:disabled {
1362
+ opacity: 0.5;
1363
+ cursor: not-allowed;
1364
+ }
1365
+
1311
1366
  /* Compact mode styles */
1312
1367
  .messages-area.compact-mode { gap: 0; padding: 8px 12px; }
1313
1368
  .messages-area.compact-mode .message { padding: 3px 8px; gap: 6px; }
@@ -1326,6 +1381,129 @@
1326
1381
  white-space: nowrap;
1327
1382
  }
1328
1383
 
1384
+ .omnibox-panel {
1385
+ display: none;
1386
+ flex: 1 0 100%;
1387
+ background: var(--surface-2);
1388
+ background-image: var(--gradient-surface);
1389
+ border: 1px solid var(--border-light);
1390
+ border-radius: 12px;
1391
+ box-shadow: var(--shadow-lg);
1392
+ overflow: hidden;
1393
+ }
1394
+
1395
+ .omnibox-panel.open {
1396
+ display: block;
1397
+ }
1398
+
1399
+ .omnibox-header {
1400
+ display: flex;
1401
+ align-items: center;
1402
+ justify-content: space-between;
1403
+ gap: 12px;
1404
+ padding: 10px 12px;
1405
+ border-bottom: 1px solid var(--border);
1406
+ font-size: 10px;
1407
+ color: var(--text-muted);
1408
+ text-transform: uppercase;
1409
+ letter-spacing: 0.08em;
1410
+ }
1411
+
1412
+ .omnibox-header strong {
1413
+ color: var(--accent);
1414
+ font-size: 10px;
1415
+ }
1416
+
1417
+ .omnibox-hint {
1418
+ color: var(--text-dim);
1419
+ text-transform: none;
1420
+ letter-spacing: 0;
1421
+ font-size: 11px;
1422
+ }
1423
+
1424
+ .omnibox-list {
1425
+ display: flex;
1426
+ flex-direction: column;
1427
+ max-height: 320px;
1428
+ overflow-y: auto;
1429
+ }
1430
+
1431
+ .omnibox-item {
1432
+ width: 100%;
1433
+ background: transparent;
1434
+ border: none;
1435
+ border-bottom: 1px solid var(--border);
1436
+ color: var(--text);
1437
+ display: flex;
1438
+ align-items: center;
1439
+ gap: 10px;
1440
+ padding: 12px;
1441
+ text-align: left;
1442
+ cursor: pointer;
1443
+ transition: background 0.15s ease, border-color 0.15s ease;
1444
+ }
1445
+
1446
+ .omnibox-item:last-child {
1447
+ border-bottom: none;
1448
+ }
1449
+
1450
+ .omnibox-item:hover,
1451
+ .omnibox-item.active {
1452
+ background: var(--accent-dim);
1453
+ border-color: var(--accent-glow);
1454
+ }
1455
+
1456
+ .omnibox-item-kind {
1457
+ flex-shrink: 0;
1458
+ min-width: 68px;
1459
+ font-size: 9px;
1460
+ font-weight: 700;
1461
+ text-transform: uppercase;
1462
+ letter-spacing: 0.08em;
1463
+ color: var(--accent);
1464
+ }
1465
+
1466
+ .omnibox-item-main {
1467
+ flex: 1;
1468
+ min-width: 0;
1469
+ }
1470
+
1471
+ .omnibox-item-label {
1472
+ font-size: 13px;
1473
+ font-weight: 600;
1474
+ color: var(--text);
1475
+ white-space: nowrap;
1476
+ overflow: hidden;
1477
+ text-overflow: ellipsis;
1478
+ }
1479
+
1480
+ .omnibox-item-sub {
1481
+ margin-top: 2px;
1482
+ font-size: 11px;
1483
+ color: var(--text-dim);
1484
+ white-space: nowrap;
1485
+ overflow: hidden;
1486
+ text-overflow: ellipsis;
1487
+ }
1488
+
1489
+ .omnibox-item-badge {
1490
+ flex-shrink: 0;
1491
+ font-size: 10px;
1492
+ font-weight: 700;
1493
+ color: var(--text-dim);
1494
+ background: var(--surface-3);
1495
+ border: 1px solid var(--border);
1496
+ border-radius: 999px;
1497
+ padding: 3px 8px;
1498
+ }
1499
+
1500
+ .omnibox-empty {
1501
+ padding: 16px 14px;
1502
+ font-size: 12px;
1503
+ color: var(--text-muted);
1504
+ text-align: center;
1505
+ }
1506
+
1329
1507
  /* ===== EMPTY STATE ONBOARDING ===== */
1330
1508
  .onboard-steps {
1331
1509
  text-align: left;
@@ -1848,6 +2026,7 @@
1848
2026
 
1849
2027
  @media (max-width: 768px) {
1850
2028
  .kanban { grid-template-columns: 1fr; }
2029
+ .graph-surface-wrap { height: 560px; }
1851
2030
  }
1852
2031
 
1853
2032
  /* ===== REPLAY MODE ===== */
@@ -2261,6 +2440,10 @@
2261
2440
 
2262
2441
  /* View tabs */
2263
2442
  .view-tab { padding: 7px 12px; font-size: 11px; }
2443
+
2444
+ .graph-area { padding: 12px; }
2445
+ .graph-shell { padding: 12px; }
2446
+ .graph-surface-wrap { height: 460px; }
2264
2447
  }
2265
2448
 
2266
2449
  /* ===== MOBILE: VERY SMALL PHONE (360px) ===== */
@@ -2365,6 +2548,268 @@
2365
2548
  color: var(--text);
2366
2549
  }
2367
2550
 
2551
+ .profile-popup-actions {
2552
+ display: flex;
2553
+ gap: 8px;
2554
+ margin-top: 12px;
2555
+ }
2556
+
2557
+ .profile-popup-actions .btn {
2558
+ flex: 1;
2559
+ margin-top: 0 !important;
2560
+ }
2561
+
2562
+ .agent-drawer-backdrop {
2563
+ position: fixed;
2564
+ inset: 0;
2565
+ background: rgba(0, 0, 0, 0.55);
2566
+ backdrop-filter: blur(4px);
2567
+ -webkit-backdrop-filter: blur(4px);
2568
+ opacity: 0;
2569
+ pointer-events: none;
2570
+ transition: opacity 0.2s ease;
2571
+ z-index: 320;
2572
+ }
2573
+
2574
+ .agent-drawer-backdrop.open {
2575
+ opacity: 1;
2576
+ pointer-events: auto;
2577
+ }
2578
+
2579
+ .agent-drawer {
2580
+ position: fixed;
2581
+ top: 0;
2582
+ right: 0;
2583
+ bottom: 0;
2584
+ width: min(420px, 100vw);
2585
+ background: var(--surface);
2586
+ background-image: var(--gradient-surface);
2587
+ border-left: 1px solid var(--border-light);
2588
+ box-shadow: var(--shadow-lg);
2589
+ transform: translateX(100%);
2590
+ transition: transform 0.22s ease;
2591
+ z-index: 321;
2592
+ display: flex;
2593
+ flex-direction: column;
2594
+ }
2595
+
2596
+ .agent-drawer.open {
2597
+ transform: translateX(0);
2598
+ }
2599
+
2600
+ .agent-drawer-header {
2601
+ display: flex;
2602
+ align-items: flex-start;
2603
+ justify-content: space-between;
2604
+ gap: 12px;
2605
+ padding: 20px 20px 14px;
2606
+ border-bottom: 1px solid var(--border);
2607
+ }
2608
+
2609
+ .agent-drawer-header-main {
2610
+ display: flex;
2611
+ align-items: center;
2612
+ gap: 12px;
2613
+ min-width: 0;
2614
+ }
2615
+
2616
+ .agent-drawer-avatar,
2617
+ .agent-drawer-avatar-img {
2618
+ width: 52px;
2619
+ height: 52px;
2620
+ border-radius: 16px;
2621
+ flex-shrink: 0;
2622
+ }
2623
+
2624
+ .agent-drawer-avatar {
2625
+ display: flex;
2626
+ align-items: center;
2627
+ justify-content: center;
2628
+ color: #fff;
2629
+ font-size: 18px;
2630
+ font-weight: 800;
2631
+ }
2632
+
2633
+ .agent-drawer-avatar-img {
2634
+ object-fit: cover;
2635
+ }
2636
+
2637
+ .agent-drawer-title-wrap {
2638
+ min-width: 0;
2639
+ }
2640
+
2641
+ .agent-drawer-title {
2642
+ font-size: 18px;
2643
+ font-weight: 700;
2644
+ color: var(--text);
2645
+ white-space: nowrap;
2646
+ overflow: hidden;
2647
+ text-overflow: ellipsis;
2648
+ }
2649
+
2650
+ .agent-drawer-subtitle {
2651
+ margin-top: 4px;
2652
+ font-size: 12px;
2653
+ color: var(--text-dim);
2654
+ display: flex;
2655
+ align-items: center;
2656
+ gap: 8px;
2657
+ flex-wrap: wrap;
2658
+ }
2659
+
2660
+ .agent-drawer-close {
2661
+ background: var(--surface-2);
2662
+ border: 1px solid var(--border);
2663
+ color: var(--text-dim);
2664
+ width: 32px;
2665
+ height: 32px;
2666
+ border-radius: 10px;
2667
+ cursor: pointer;
2668
+ font-size: 18px;
2669
+ line-height: 1;
2670
+ transition: all 0.15s ease;
2671
+ }
2672
+
2673
+ .agent-drawer-close:hover {
2674
+ color: var(--text);
2675
+ border-color: var(--border-light);
2676
+ background: var(--surface-3);
2677
+ }
2678
+
2679
+ .agent-drawer-body {
2680
+ flex: 1;
2681
+ overflow-y: auto;
2682
+ padding: 18px 20px 24px;
2683
+ }
2684
+
2685
+ .agent-drawer-status-row {
2686
+ display: flex;
2687
+ flex-wrap: wrap;
2688
+ gap: 8px;
2689
+ margin-bottom: 16px;
2690
+ }
2691
+
2692
+ .agent-detail-pill {
2693
+ display: inline-flex;
2694
+ align-items: center;
2695
+ gap: 6px;
2696
+ background: var(--surface-2);
2697
+ border: 1px solid var(--border);
2698
+ border-radius: 999px;
2699
+ padding: 5px 10px;
2700
+ font-size: 10px;
2701
+ font-weight: 700;
2702
+ text-transform: uppercase;
2703
+ letter-spacing: 0.06em;
2704
+ color: var(--text-dim);
2705
+ }
2706
+
2707
+ .agent-detail-pill strong {
2708
+ color: var(--text);
2709
+ }
2710
+
2711
+ .agent-detail-note {
2712
+ background: var(--surface-2);
2713
+ border: 1px solid var(--border);
2714
+ border-radius: 12px;
2715
+ padding: 12px 14px;
2716
+ margin-bottom: 16px;
2717
+ }
2718
+
2719
+ .agent-detail-note-label {
2720
+ font-size: 10px;
2721
+ font-weight: 700;
2722
+ color: var(--text-muted);
2723
+ text-transform: uppercase;
2724
+ letter-spacing: 0.08em;
2725
+ margin-bottom: 6px;
2726
+ }
2727
+
2728
+ .agent-detail-note-value {
2729
+ font-size: 12px;
2730
+ line-height: 1.6;
2731
+ color: var(--text);
2732
+ }
2733
+
2734
+ .agent-drawer-section {
2735
+ margin-bottom: 18px;
2736
+ }
2737
+
2738
+ .agent-drawer-section-title {
2739
+ font-size: 11px;
2740
+ font-weight: 700;
2741
+ color: var(--text-muted);
2742
+ text-transform: uppercase;
2743
+ letter-spacing: 0.08em;
2744
+ margin-bottom: 10px;
2745
+ }
2746
+
2747
+ .agent-detail-grid {
2748
+ display: grid;
2749
+ grid-template-columns: repeat(2, minmax(0, 1fr));
2750
+ gap: 10px;
2751
+ }
2752
+
2753
+ .agent-detail-card {
2754
+ background: var(--surface-2);
2755
+ border: 1px solid var(--border);
2756
+ border-radius: 12px;
2757
+ padding: 12px;
2758
+ min-width: 0;
2759
+ }
2760
+
2761
+ .agent-detail-label {
2762
+ font-size: 10px;
2763
+ font-weight: 700;
2764
+ color: var(--text-muted);
2765
+ text-transform: uppercase;
2766
+ letter-spacing: 0.06em;
2767
+ margin-bottom: 6px;
2768
+ }
2769
+
2770
+ .agent-detail-value {
2771
+ font-size: 13px;
2772
+ font-weight: 600;
2773
+ color: var(--text);
2774
+ line-height: 1.45;
2775
+ word-break: break-word;
2776
+ }
2777
+
2778
+ .agent-detail-value.muted {
2779
+ font-size: 12px;
2780
+ font-weight: 500;
2781
+ color: var(--text-dim);
2782
+ }
2783
+
2784
+ .agent-detail-empty {
2785
+ color: var(--text-muted);
2786
+ }
2787
+
2788
+ .agent-drawer-actions {
2789
+ display: flex;
2790
+ gap: 8px;
2791
+ margin-top: 6px;
2792
+ flex-wrap: wrap;
2793
+ }
2794
+
2795
+ .agent-drawer-actions .btn {
2796
+ flex: 1 1 140px;
2797
+ }
2798
+
2799
+ @media (max-width: 768px) {
2800
+ .agent-drawer {
2801
+ width: 100vw;
2802
+ }
2803
+
2804
+ .agent-detail-grid {
2805
+ grid-template-columns: 1fr;
2806
+ }
2807
+
2808
+ .profile-popup-actions {
2809
+ flex-direction: column;
2810
+ }
2811
+ }
2812
+
2368
2813
  /* ===== v3.0: WORKSPACES TAB ===== */
2369
2814
  .workspaces-area {
2370
2815
  flex: 1;
@@ -2459,6 +2904,91 @@
2459
2904
 
2460
2905
  .workflows-area.visible { display: block; }
2461
2906
 
2907
+ .graph-area {
2908
+ flex: 1;
2909
+ overflow-y: auto;
2910
+ padding: 16px 20px;
2911
+ display: none;
2912
+ }
2913
+
2914
+ .graph-area.visible { display: block; }
2915
+
2916
+ .graph-shell {
2917
+ background: var(--surface);
2918
+ background-image: var(--gradient-surface);
2919
+ border: 1px solid var(--border);
2920
+ border-radius: 16px;
2921
+ padding: 16px;
2922
+ box-shadow: var(--shadow-sm);
2923
+ }
2924
+
2925
+ .graph-header {
2926
+ display: flex;
2927
+ align-items: flex-start;
2928
+ justify-content: space-between;
2929
+ gap: 16px;
2930
+ flex-wrap: wrap;
2931
+ margin-bottom: 14px;
2932
+ }
2933
+
2934
+ .graph-title {
2935
+ font-size: 16px;
2936
+ font-weight: 700;
2937
+ color: var(--text);
2938
+ }
2939
+
2940
+ .graph-subtitle {
2941
+ margin-top: 4px;
2942
+ font-size: 12px;
2943
+ line-height: 1.6;
2944
+ color: var(--text-dim);
2945
+ max-width: 760px;
2946
+ }
2947
+
2948
+ .graph-meta {
2949
+ display: flex;
2950
+ align-items: center;
2951
+ gap: 8px;
2952
+ flex-wrap: wrap;
2953
+ }
2954
+
2955
+ .graph-pill {
2956
+ display: inline-flex;
2957
+ align-items: center;
2958
+ gap: 6px;
2959
+ padding: 6px 10px;
2960
+ border-radius: 999px;
2961
+ background: var(--surface-2);
2962
+ border: 1px solid var(--border);
2963
+ font-size: 10px;
2964
+ font-weight: 700;
2965
+ text-transform: uppercase;
2966
+ letter-spacing: 0.06em;
2967
+ color: var(--text-dim);
2968
+ }
2969
+
2970
+ .graph-pill strong {
2971
+ color: var(--text);
2972
+ font-size: 11px;
2973
+ }
2974
+
2975
+ .graph-surface-wrap {
2976
+ height: 680px;
2977
+ border-radius: 14px;
2978
+ overflow: hidden;
2979
+ border: 1px solid var(--border);
2980
+ background:
2981
+ radial-gradient(circle at top left, var(--accent-dim), transparent 42%),
2982
+ radial-gradient(circle at bottom right, var(--purple-dim), transparent 36%),
2983
+ var(--bg);
2984
+ }
2985
+
2986
+ .graph-surface {
2987
+ display: block;
2988
+ width: 100%;
2989
+ height: 100%;
2990
+ }
2991
+
2462
2992
  .plan-area {
2463
2993
  flex: 1;
2464
2994
  overflow-y: auto;
@@ -2683,6 +3213,107 @@
2683
3213
  .stats-area.visible { display: block; }
2684
3214
 
2685
3215
  /* ===== DOCS VIEW ===== */
3216
+ /* ===== SERVICES VIEW ===== */
3217
+ .services-area {
3218
+ flex: 1;
3219
+ overflow-y: auto;
3220
+ padding: 20px;
3221
+ display: none;
3222
+ }
3223
+ .services-area.visible { display: block; }
3224
+ .services-container { max-width: 900px; margin: 0 auto; }
3225
+ .services-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
3226
+ .services-header h2 { font-size: 20px; font-weight: 700; color: var(--text); margin: 0; }
3227
+ .services-form {
3228
+ background: var(--surface-2);
3229
+ border: 1px solid var(--border);
3230
+ border-radius: 10px;
3231
+ padding: 16px;
3232
+ margin-bottom: 20px;
3233
+ }
3234
+ .services-form label { display: block; font-size: 11px; color: var(--text-dim); margin-bottom: 4px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
3235
+ .services-form input, .services-form select {
3236
+ width: 100%;
3237
+ background: var(--surface-1);
3238
+ border: 1px solid var(--border);
3239
+ border-radius: 6px;
3240
+ padding: 8px 10px;
3241
+ font-size: 13px;
3242
+ color: var(--text);
3243
+ margin-bottom: 12px;
3244
+ box-sizing: border-box;
3245
+ }
3246
+ .services-form select option {
3247
+ background: #1e2028;
3248
+ color: #e0e0e0;
3249
+ }
3250
+ .services-form .form-row { display: flex; gap: 12px; }
3251
+ .services-form .form-row > div { flex: 1; }
3252
+ .services-form button {
3253
+ background: var(--accent);
3254
+ color: #fff;
3255
+ border: none;
3256
+ border-radius: 6px;
3257
+ padding: 8px 20px;
3258
+ font-size: 13px;
3259
+ font-weight: 600;
3260
+ cursor: pointer;
3261
+ transition: opacity 0.2s;
3262
+ }
3263
+ .services-form button:hover { opacity: 0.85; }
3264
+ .services-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin-bottom: 24px; }
3265
+ .service-card {
3266
+ background: var(--surface-2);
3267
+ border: 1px solid var(--border);
3268
+ border-radius: 10px;
3269
+ padding: 14px;
3270
+ position: relative;
3271
+ }
3272
+ .service-card .sc-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
3273
+ .service-card .sc-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
3274
+ .service-card .sc-name { font-weight: 600; font-size: 14px; color: var(--text); }
3275
+ .service-card .sc-provider { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: #fff; font-weight: 600; text-transform: uppercase; }
3276
+ .service-card .sc-stats { font-size: 11px; color: var(--text-dim); margin-bottom: 8px; line-height: 1.6; }
3277
+ .service-card .sc-controls { display: flex; gap: 6px; }
3278
+ .service-card .sc-controls button {
3279
+ flex: 1;
3280
+ padding: 5px 8px;
3281
+ border: 1px solid var(--border);
3282
+ border-radius: 5px;
3283
+ background: var(--surface-1);
3284
+ color: var(--text-muted);
3285
+ font-size: 11px;
3286
+ cursor: pointer;
3287
+ transition: all 0.2s;
3288
+ }
3289
+ .service-card .sc-controls button:hover { background: var(--surface-2); color: var(--text); }
3290
+ .service-card .sc-controls .sc-start { border-color: #22c55e44; color: #4ade80; }
3291
+ .service-card .sc-controls .sc-stop { border-color: #ef444444; color: #f87171; }
3292
+ .service-card .sc-controls .sc-delete { border-color: #ef444444; color: #f87171; }
3293
+ .media-browser { margin-top: 16px; }
3294
+ .media-browser h3 { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 12px; }
3295
+ .media-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
3296
+ .media-item {
3297
+ background: var(--surface-2);
3298
+ border: 1px solid var(--border);
3299
+ border-radius: 8px;
3300
+ overflow: hidden;
3301
+ cursor: pointer;
3302
+ transition: border-color 0.2s;
3303
+ }
3304
+ .media-item:hover { border-color: var(--accent); }
3305
+ .media-item img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
3306
+ .media-item .mi-info { padding: 6px 8px; font-size: 10px; color: var(--text-dim); }
3307
+ .media-item .mi-prompt { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
3308
+ .media-lightbox {
3309
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
3310
+ background: rgba(0,0,0,0.9); z-index: 10000;
3311
+ display: flex; align-items: center; justify-content: center;
3312
+ cursor: pointer;
3313
+ }
3314
+ .media-lightbox img { max-width: 90%; max-height: 90%; object-fit: contain; border-radius: 8px; }
3315
+ .media-lightbox .lb-info { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: #aaa; font-size: 12px; text-align: center; max-width: 600px; }
3316
+
2686
3317
  .docs-area {
2687
3318
  flex: 1;
2688
3319
  overflow-y: auto;
@@ -3715,11 +4346,15 @@
3715
4346
  <select class="project-select" id="project-select" onchange="switchProject()">
3716
4347
  <option value="">Default (local)</option>
3717
4348
  </select>
3718
- <input class="project-input" id="project-path-input" placeholder="Enter project folder path..."
3719
- onkeydown="if(event.key==='Enter'){addProject();}">
4349
+ <div class="project-input-row" id="project-input-row" style="display:none;gap:6px;align-items:center;margin-top:4px">
4350
+ <input class="project-input" id="project-path-input" placeholder="Enter project folder path..." style="flex:1;display:block"
4351
+ onkeydown="if(event.key==='Enter'){addProject();}">
4352
+ <button class="btn btn-primary" id="project-submit-btn" onclick="addProject()" title="Add the pasted path as a project">Submit</button>
4353
+ </div>
3720
4354
  <div class="project-actions">
3721
4355
  <button class="btn btn-primary" onclick="showAddProject()">+ Add</button>
3722
4356
  <button class="btn" onclick="discoverProjects()">Discover</button>
4357
+ <button class="btn" onclick="reinstallProviders()" id="reinstall-providers-btn" style="display:none" title="Re-run init for Claude/Gemini/Codex configs in this project (merge-safe — preserves your other MCP servers)">Reinstall Providers</button>
3723
4358
  <button class="btn btn-danger" onclick="removeProject()" id="remove-project-btn" style="display:none">Remove</button>
3724
4359
  </div>
3725
4360
  <div id="discover-results" style="display:none"></div>
@@ -3741,6 +4376,7 @@
3741
4376
  <option value="quality">Quality</option>
3742
4377
  <option value="monitor">Monitor</option>
3743
4378
  <option value="advisor">Advisor</option>
4379
+ <option value="api-agent">API Agent</option>
3744
4380
  </select>
3745
4381
  <select id="agent-status-filter" onchange="filterAgents()">
3746
4382
  <option value="">Status</option>
@@ -3803,10 +4439,12 @@
3803
4439
  <div class="view-tab" id="tab-tasks" onclick="switchView('tasks')">Tasks</div>
3804
4440
  <div class="view-tab" id="tab-workspaces" onclick="switchView('workspaces')">Workspaces</div>
3805
4441
  <div class="view-tab" id="tab-workflows" onclick="switchView('workflows')">Workflows</div>
4442
+ <div class="view-tab" id="tab-graph" onclick="switchView('graph')">Graph</div>
3806
4443
  <div class="view-tab" id="tab-plan" onclick="switchView('plan')">Plan</div>
3807
4444
  <div class="view-tab" id="tab-launch" onclick="switchView('launch')">Launch</div>
3808
4445
  <div class="view-tab" id="tab-rules" onclick="switchView('rules')">Rules</div>
3809
4446
  <div class="view-tab" id="tab-stats" onclick="switchView('stats')">Stats</div>
4447
+ <div class="view-tab" id="tab-services" onclick="switchView('services')">Services</div>
3810
4448
  <div class="view-tab" id="tab-docs" onclick="switchView('docs')">Docs</div>
3811
4449
  </div>
3812
4450
  <div class="branch-tabs" id="branch-tabs"></div>
@@ -3819,6 +4457,14 @@
3819
4457
  <select id="conversation-select" onchange="loadConversation()" title="Load saved conversation" style="background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:10px;cursor:pointer;color:var(--text-muted)"><option value="">Current</option></select>
3820
4458
  <button onclick="newConversation()" title="Archive current and start fresh" style="background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:4px 8px;font-size:10px;cursor:pointer;color:var(--text-muted);white-space:nowrap;transition:all 0.2s">New Conversation</button>
3821
4459
  <button onclick="clearMessages()" title="Clear all messages for this project" style="background:#4a2020;border:1px solid #6a3030;border-radius:6px;padding:4px 8px;font-size:10px;cursor:pointer;color:#ff6b6b;white-space:nowrap;transition:all 0.2s">Clear Messages</button>
4460
+ <div class="workspace-layout-controls">
4461
+ <select id="dashboard-workspace-select" class="workspace-layout-select" onchange="onDashboardWorkspaceSelection()" title="Saved dashboard layouts">
4462
+ <option value="">Current layout</option>
4463
+ </select>
4464
+ <button id="dashboard-workspace-save" class="workspace-layout-btn" onclick="saveNamedDashboardWorkspace()" title="Save the current dashboard layout as a named workspace">Save As</button>
4465
+ <button id="dashboard-workspace-load" class="workspace-layout-btn" onclick="loadNamedDashboardWorkspace()" title="Restore the selected dashboard layout" disabled>Load</button>
4466
+ </div>
4467
+ <div class="omnibox-panel" id="omnibox-panel" aria-hidden="true"></div>
3822
4468
  </div>
3823
4469
  <div class="channel-filter-bar" id="channel-filter-bar" style="display:none"></div>
3824
4470
  <div class="pinned-section" id="pinned-section">
@@ -3829,6 +4475,7 @@
3829
4475
  <div class="tasks-area" id="tasks-area"></div>
3830
4476
  <div class="workspaces-area" id="workspaces-area"></div>
3831
4477
  <div class="workflows-area" id="workflows-area"></div>
4478
+ <div class="graph-area" id="graph-area"></div>
3832
4479
  <div class="plan-area" id="plan-area"></div>
3833
4480
  <div class="plan-area" id="monitor-panel" style="display:none"></div>
3834
4481
  <div class="office-area" id="office-area">
@@ -3860,6 +4507,7 @@
3860
4507
  <div class="launch-area" id="launch-area"></div>
3861
4508
  <div class="rules-area" id="rules-area"></div>
3862
4509
  <div class="stats-area" id="stats-area"></div>
4510
+ <div class="services-area" id="services-area"></div>
3863
4511
  <div class="docs-area" id="docs-area"><div id="docs-content"></div></div>
3864
4512
  <button class="scroll-bottom" id="scroll-bottom" onclick="scrollToBottom()">&#x2193;<span class="new-count" id="new-msg-count" style="display:none">0</span></button>
3865
4513
  <div class="typing-bar" id="typing-bar"></div>
@@ -3874,8 +4522,23 @@
3874
4522
  </div>
3875
4523
  <div class="input-msg">
3876
4524
  <label>Message</label>
3877
- <textarea id="inject-content" placeholder="Type a message to inject..." rows="1"
3878
- onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();doInject();}"></textarea>
4525
+ <div style="position:relative;display:flex;align-items:flex-end;gap:6px;">
4526
+ <textarea id="inject-content" placeholder="Type a message to inject..." rows="1"
4527
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();doInject();}" style="flex:1"></textarea>
4528
+ <input type="file" id="inject-file" accept="image/*" style="display:none" onchange="onFileSelected(this)">
4529
+ <button onclick="document.getElementById('inject-file').click()" title="Attach image" style="background:none;border:1px solid var(--border);border-radius:6px;color:var(--text-dim);cursor:pointer;padding:6px 8px;font-size:14px;line-height:1;flex-shrink:0;">&#128247;</button>
4530
+ </div>
4531
+ <div id="inject-file-preview" style="display:none;margin-top:6px;position:relative;">
4532
+ <img id="inject-file-thumb" style="max-height:80px;max-width:200px;border-radius:6px;border:1px solid var(--border);">
4533
+ <button onclick="clearAttachment()" style="position:absolute;top:-6px;right:-6px;background:#ef4444;color:#fff;border:none;border-radius:50%;width:18px;height:18px;cursor:pointer;font-size:11px;line-height:18px;padding:0;">X</button>
4534
+ <div id="inject-file-name" style="font-size:9px;color:var(--text-dim);margin-top:2px;"></div>
4535
+ </div>
4536
+ <div id="assistant-private-row" style="display:none;align-items:center;gap:6px;margin-top:6px;font-size:10px;color:var(--text-muted);">
4537
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
4538
+ <input type="checkbox" id="assistant-private-optin" style="accent-color:var(--accent)">
4539
+ Send privately to Assistant only
4540
+ </label>
4541
+ </div>
3879
4542
  </div>
3880
4543
  <button class="send-btn" onclick="doInject()" id="inject-btn" disabled>Send</button>
3881
4544
  </div>
@@ -3895,9 +4558,28 @@
3895
4558
  <div id="pp-role"></div>
3896
4559
  <div class="profile-popup-bio" id="pp-bio"></div>
3897
4560
  <div class="profile-popup-stats" id="pp-stats"></div>
3898
- <button class="btn btn-primary" style="width:100%;margin-top:10px" id="pp-edit-btn" onclick="openProfileEditor()">Edit Profile</button>
4561
+ <div class="profile-popup-actions">
4562
+ <button class="btn" id="pp-details-btn" onclick="openAgentMetadataDrawerFromPopup()">Details</button>
4563
+ <button class="btn btn-primary" id="pp-edit-btn" onclick="openProfileEditor()">Edit Profile</button>
4564
+ </div>
3899
4565
  </div>
3900
4566
 
4567
+ <div class="agent-drawer-backdrop" id="agent-drawer-backdrop" onclick="closeAgentMetadataDrawer()"></div>
4568
+ <aside class="agent-drawer" id="agent-drawer" aria-hidden="true">
4569
+ <div class="agent-drawer-header">
4570
+ <div class="agent-drawer-header-main">
4571
+ <div class="agent-drawer-avatar" id="agent-drawer-avatar">?</div>
4572
+ <img class="agent-drawer-avatar-img" id="agent-drawer-avatar-img" src="data:," style="display:none">
4573
+ <div class="agent-drawer-title-wrap">
4574
+ <div class="agent-drawer-title" id="agent-drawer-title">Agent details</div>
4575
+ <div class="agent-drawer-subtitle" id="agent-drawer-subtitle"></div>
4576
+ </div>
4577
+ </div>
4578
+ <button class="agent-drawer-close" type="button" onclick="closeAgentMetadataDrawer()">&times;</button>
4579
+ </div>
4580
+ <div class="agent-drawer-body" id="agent-drawer-body"></div>
4581
+ </aside>
4582
+
3901
4583
  <!-- Character Designer Panel -->
3902
4584
  <div class="char-designer-backdrop" id="cd-backdrop" onclick="closeProfileEditor()"></div>
3903
4585
  <div class="char-designer" id="char-designer">
@@ -4278,23 +4960,448 @@ var _lttToken = (function() {
4278
4960
  try { var p = new URLSearchParams(window.location.search); var t = p.get('token'); if (t) sessionStorage.setItem('ltt-token', t); return sessionStorage.getItem('ltt-token') || ''; } catch(e) { return ''; }
4279
4961
  })();
4280
4962
 
4281
- function lttFetch(url, opts) {
4282
- opts = opts || {};
4283
- // Append LAN token to URL if present (needed for phone/LAN access)
4284
- if (_lttToken) {
4285
- var sep = url.indexOf('?') >= 0 ? '&' : '?';
4286
- url = url + sep + 'token=' + encodeURIComponent(_lttToken);
4963
+ function lttFetch(url, opts) {
4964
+ opts = opts || {};
4965
+ // Append LAN token to URL if present (needed for phone/LAN access)
4966
+ if (_lttToken) {
4967
+ var sep = url.indexOf('?') >= 0 ? '&' : '?';
4968
+ url = url + sep + 'token=' + encodeURIComponent(_lttToken);
4969
+ }
4970
+ if (opts.method && opts.method !== 'GET') {
4971
+ if (!opts.headers) opts.headers = {};
4972
+ opts.headers['X-LTT-Request'] = '1';
4973
+ }
4974
+ return fetch(url, opts);
4975
+ }
4976
+
4977
+ var DASHBOARD_WORKSPACE_STATE_KEY = 'ltt-dashboard-workspace-state';
4978
+ var DASHBOARD_WORKSPACE_STATE_VERSION = 1;
4979
+
4980
+ function cloneJsonValue(value, fallback) {
4981
+ try {
4982
+ return JSON.parse(JSON.stringify(value));
4983
+ } catch (e) {
4984
+ return fallback;
4985
+ }
4986
+ }
4987
+
4988
+ function clonePlainObject(value) {
4989
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
4990
+ return cloneJsonValue(value, {});
4991
+ }
4992
+
4993
+ function cleanString(value, fallback) {
4994
+ return typeof value === 'string' ? value : fallback;
4995
+ }
4996
+
4997
+ function buildDefaultDashboardLayoutSnapshot() {
4998
+ return {
4999
+ view: 'office',
5000
+ theme: 'dark',
5001
+ compactMode: false,
5002
+ project: '',
5003
+ branch: 'main',
5004
+ pinnedExpanded: true,
5005
+ agentFilters: {
5006
+ search: '',
5007
+ role: '',
5008
+ status: '',
5009
+ },
5010
+ collapsedRoleGroups: {},
5011
+ };
5012
+ }
5013
+
5014
+ function buildDefaultDashboardWorkspaceState() {
5015
+ return {
5016
+ version: DASHBOARD_WORKSPACE_STATE_VERSION,
5017
+ liveWorkspace: {
5018
+ name: 'Current browser layout',
5019
+ snapshot: buildDefaultDashboardLayoutSnapshot(),
5020
+ updatedAt: new Date().toISOString(),
5021
+ },
5022
+ savedWorkspaces: [],
5023
+ selectedWorkspaceId: '',
5024
+ preferences: {
5025
+ notificationsEnabled: false,
5026
+ bookmarks: {},
5027
+ pins: {},
5028
+ reactions: {},
5029
+ },
5030
+ };
5031
+ }
5032
+
5033
+ function normalizeDashboardLayoutSnapshot(rawSnapshot) {
5034
+ var defaults = buildDefaultDashboardLayoutSnapshot();
5035
+ var snapshot = rawSnapshot && typeof rawSnapshot === 'object' ? rawSnapshot : {};
5036
+ var agentFilters = snapshot.agentFilters && typeof snapshot.agentFilters === 'object' ? snapshot.agentFilters : {};
5037
+ var collapsedRoleGroups = snapshot.collapsedRoleGroups && typeof snapshot.collapsedRoleGroups === 'object' ? snapshot.collapsedRoleGroups : {};
5038
+ var allowedViews = {
5039
+ office: true,
5040
+ messages: true,
5041
+ tasks: true,
5042
+ workspaces: true,
5043
+ workflows: true,
5044
+ graph: true,
5045
+ plan: true,
5046
+ launch: true,
5047
+ rules: true,
5048
+ stats: true,
5049
+ services: true,
5050
+ docs: true,
5051
+ };
5052
+ var normalizedCollapsedGroups = {};
5053
+ for (var key in collapsedRoleGroups) {
5054
+ if (!Object.prototype.hasOwnProperty.call(collapsedRoleGroups, key)) continue;
5055
+ normalizedCollapsedGroups[key] = !!collapsedRoleGroups[key];
5056
+ }
5057
+ return {
5058
+ view: allowedViews[snapshot.view] ? snapshot.view : defaults.view,
5059
+ theme: snapshot.theme === 'light' ? 'light' : defaults.theme,
5060
+ compactMode: !!snapshot.compactMode,
5061
+ project: cleanString(snapshot.project, defaults.project),
5062
+ branch: cleanString(snapshot.branch, defaults.branch) || defaults.branch,
5063
+ pinnedExpanded: snapshot.pinnedExpanded !== false,
5064
+ agentFilters: {
5065
+ search: cleanString(agentFilters.search, defaults.agentFilters.search),
5066
+ role: cleanString(agentFilters.role, defaults.agentFilters.role),
5067
+ status: cleanString(agentFilters.status, defaults.agentFilters.status),
5068
+ },
5069
+ collapsedRoleGroups: normalizedCollapsedGroups,
5070
+ };
5071
+ }
5072
+
5073
+ function createDashboardWorkspaceId(name) {
5074
+ var base = (name || 'workspace').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
5075
+ return base + '-' + String(Date.now());
5076
+ }
5077
+
5078
+ function normalizeSavedDashboardWorkspace(entry, index) {
5079
+ var workspace = entry && typeof entry === 'object' ? entry : {};
5080
+ var workspaceName = cleanString(workspace.name, '').trim() || ('Workspace ' + (index + 1));
5081
+ return {
5082
+ id: cleanString(workspace.id, '').trim() || createDashboardWorkspaceId(workspaceName),
5083
+ name: workspaceName,
5084
+ snapshot: normalizeDashboardLayoutSnapshot(workspace.snapshot),
5085
+ updatedAt: cleanString(workspace.updatedAt, new Date().toISOString()),
5086
+ };
5087
+ }
5088
+
5089
+ function readLegacyDashboardMap(key) {
5090
+ try {
5091
+ var raw = localStorage.getItem(key);
5092
+ if (!raw) return {};
5093
+ var parsed = JSON.parse(raw);
5094
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
5095
+ } catch (e) {
5096
+ return {};
5097
+ }
5098
+ }
5099
+
5100
+ function migrateLegacyDashboardWorkspaceState() {
5101
+ var state = buildDefaultDashboardWorkspaceState();
5102
+ try {
5103
+ var legacyTheme = localStorage.getItem('ltt-theme');
5104
+ if (legacyTheme === 'light' || legacyTheme === 'dark') state.liveWorkspace.snapshot.theme = legacyTheme;
5105
+ state.liveWorkspace.snapshot.compactMode = localStorage.getItem('compactMode') === 'true';
5106
+ state.liveWorkspace.snapshot.project = localStorage.getItem('ltt_last_project') || '';
5107
+ state.preferences.notificationsEnabled = localStorage.getItem('ltt-notif') === 'true';
5108
+ state.preferences.bookmarks = readLegacyDashboardMap('ltt-bookmarks');
5109
+ state.preferences.pins = readLegacyDashboardMap('ltt-pins');
5110
+ state.preferences.reactions = readLegacyDashboardMap('ltt-reactions');
5111
+ } catch (e) {}
5112
+ return state;
5113
+ }
5114
+
5115
+ function normalizeDashboardWorkspaceState(rawState) {
5116
+ var defaults = buildDefaultDashboardWorkspaceState();
5117
+ var state = rawState && typeof rawState === 'object' ? rawState : {};
5118
+ var liveWorkspace = state.liveWorkspace && typeof state.liveWorkspace === 'object' ? state.liveWorkspace : {};
5119
+ var preferences = state.preferences && typeof state.preferences === 'object' ? state.preferences : {};
5120
+ var savedRaw = Array.isArray(state.savedWorkspaces) ? state.savedWorkspaces : [];
5121
+ var normalizedSavedWorkspaces = [];
5122
+ var seenIds = {};
5123
+ for (var i = 0; i < savedRaw.length; i++) {
5124
+ var normalizedWorkspace = normalizeSavedDashboardWorkspace(savedRaw[i], i);
5125
+ while (seenIds[normalizedWorkspace.id]) {
5126
+ normalizedWorkspace.id = createDashboardWorkspaceId(normalizedWorkspace.name);
5127
+ }
5128
+ seenIds[normalizedWorkspace.id] = true;
5129
+ normalizedSavedWorkspaces.push(normalizedWorkspace);
5130
+ }
5131
+ var selectedWorkspaceId = cleanString(state.selectedWorkspaceId, '');
5132
+ if (selectedWorkspaceId && !seenIds[selectedWorkspaceId]) selectedWorkspaceId = '';
5133
+ return {
5134
+ version: DASHBOARD_WORKSPACE_STATE_VERSION,
5135
+ liveWorkspace: {
5136
+ name: cleanString(liveWorkspace.name, defaults.liveWorkspace.name),
5137
+ snapshot: normalizeDashboardLayoutSnapshot(liveWorkspace.snapshot),
5138
+ updatedAt: cleanString(liveWorkspace.updatedAt, defaults.liveWorkspace.updatedAt),
5139
+ },
5140
+ savedWorkspaces: normalizedSavedWorkspaces,
5141
+ selectedWorkspaceId: selectedWorkspaceId,
5142
+ preferences: {
5143
+ notificationsEnabled: !!preferences.notificationsEnabled,
5144
+ bookmarks: clonePlainObject(preferences.bookmarks),
5145
+ pins: clonePlainObject(preferences.pins),
5146
+ reactions: clonePlainObject(preferences.reactions),
5147
+ },
5148
+ };
5149
+ }
5150
+
5151
+ function loadDashboardWorkspaceState() {
5152
+ try {
5153
+ var raw = localStorage.getItem(DASHBOARD_WORKSPACE_STATE_KEY);
5154
+ if (!raw) return normalizeDashboardWorkspaceState(migrateLegacyDashboardWorkspaceState());
5155
+ return normalizeDashboardWorkspaceState(JSON.parse(raw));
5156
+ } catch (e) {
5157
+ return normalizeDashboardWorkspaceState(migrateLegacyDashboardWorkspaceState());
5158
+ }
5159
+ }
5160
+
5161
+ function saveDashboardWorkspaceState() {
5162
+ try {
5163
+ localStorage.setItem(DASHBOARD_WORKSPACE_STATE_KEY, JSON.stringify(dashboardWorkspaceState));
5164
+ } catch (e) {}
5165
+ }
5166
+
5167
+ var dashboardWorkspaceState = loadDashboardWorkspaceState();
5168
+
5169
+ function getActiveDashboardBranchName() {
5170
+ return activeBranch && activeBranch !== 'main' ? activeBranch : 'main';
5171
+ }
5172
+
5173
+ function captureCurrentDashboardLayoutSnapshot() {
5174
+ return normalizeDashboardLayoutSnapshot({
5175
+ view: activeView,
5176
+ theme: currentTheme,
5177
+ compactMode: compactMode,
5178
+ project: activeProject || '',
5179
+ branch: getActiveDashboardBranchName(),
5180
+ pinnedExpanded: pinnedExpanded,
5181
+ agentFilters: {
5182
+ search: agentFilterSearch || '',
5183
+ role: agentFilterRole || '',
5184
+ status: agentFilterStatus || '',
5185
+ },
5186
+ collapsedRoleGroups: clonePlainObject(collapsedRoleGroups),
5187
+ });
5188
+ }
5189
+
5190
+ function syncDashboardWorkspacePreferences() {
5191
+ dashboardWorkspaceState.preferences.notificationsEnabled = !!notifEnabled;
5192
+ dashboardWorkspaceState.preferences.bookmarks = clonePlainObject(bookmarks);
5193
+ dashboardWorkspaceState.preferences.pins = clonePlainObject(pins);
5194
+ dashboardWorkspaceState.preferences.reactions = clonePlainObject(reactions);
5195
+ }
5196
+
5197
+ function persistLiveDashboardLayout() {
5198
+ dashboardWorkspaceState.liveWorkspace.snapshot = captureCurrentDashboardLayoutSnapshot();
5199
+ dashboardWorkspaceState.liveWorkspace.updatedAt = new Date().toISOString();
5200
+ saveDashboardWorkspaceState();
5201
+ refreshDashboardWorkspaceControls();
5202
+ }
5203
+
5204
+ function persistDashboardPreferences() {
5205
+ syncDashboardWorkspacePreferences();
5206
+ saveDashboardWorkspaceState();
5207
+ }
5208
+
5209
+ function findSavedDashboardWorkspaceById(workspaceId) {
5210
+ if (!workspaceId) return null;
5211
+ for (var i = 0; i < dashboardWorkspaceState.savedWorkspaces.length; i++) {
5212
+ if (dashboardWorkspaceState.savedWorkspaces[i].id === workspaceId) return dashboardWorkspaceState.savedWorkspaces[i];
5213
+ }
5214
+ return null;
5215
+ }
5216
+
5217
+ function applyAgentFilterInputs() {
5218
+ var searchEl = document.getElementById('agent-search');
5219
+ var roleEl = document.getElementById('agent-role-filter');
5220
+ var statusEl = document.getElementById('agent-status-filter');
5221
+ if (searchEl) searchEl.value = agentFilterSearch || '';
5222
+ if (roleEl) roleEl.value = agentFilterRole || '';
5223
+ if (statusEl) statusEl.value = agentFilterStatus || '';
5224
+ }
5225
+
5226
+ function syncProjectSelectionUI() {
5227
+ var sidebarSel = document.getElementById('project-select');
5228
+ var mobileSel = document.getElementById('mobile-project-select');
5229
+ var removeBtn = document.getElementById('remove-project-btn');
5230
+ var indicator = document.getElementById('mobile-project-name');
5231
+ var activeLabel = '';
5232
+
5233
+ if (sidebarSel) {
5234
+ sidebarSel.value = activeProject || '';
5235
+ if (sidebarSel.value !== (activeProject || '')) activeProject = sidebarSel.value || '';
5236
+ if (sidebarSel.selectedIndex >= 0 && sidebarSel.options[sidebarSel.selectedIndex]) {
5237
+ activeLabel = sidebarSel.options[sidebarSel.selectedIndex].textContent;
5238
+ }
5239
+ }
5240
+
5241
+ if (mobileSel) mobileSel.value = activeProject || '';
5242
+ if (removeBtn) removeBtn.style.display = activeProject ? '' : 'none';
5243
+ var reinstallBtn = document.getElementById('reinstall-providers-btn');
5244
+ if (reinstallBtn) reinstallBtn.style.display = activeProject ? '' : 'none';
5245
+
5246
+ if (indicator) {
5247
+ indicator.textContent = activeProject ? activeLabel : '';
5248
+ indicator.style.display = activeProject ? '' : 'none';
5249
+ }
5250
+ }
5251
+
5252
+ function applyPinnedSectionState() {
5253
+ var list = document.getElementById('pinned-list');
5254
+ var toggle = document.getElementById('pinned-toggle');
5255
+ if (list) list.style.display = pinnedExpanded ? '' : 'none';
5256
+ if (toggle) toggle.textContent = pinnedExpanded ? 'Hide' : 'Show';
5257
+ }
5258
+
5259
+ function refreshDashboardWorkspaceControls() {
5260
+ var select = document.getElementById('dashboard-workspace-select');
5261
+ var loadBtn = document.getElementById('dashboard-workspace-load');
5262
+ if (!select) return;
5263
+
5264
+ var options = ['<option value="">Current layout</option>'];
5265
+ for (var i = 0; i < dashboardWorkspaceState.savedWorkspaces.length; i++) {
5266
+ var workspace = dashboardWorkspaceState.savedWorkspaces[i];
5267
+ options.push('<option value="' + escapeHtml(workspace.id) + '">' + escapeHtml(workspace.name) + '</option>');
5268
+ }
5269
+ select.innerHTML = options.join('');
5270
+ select.value = dashboardWorkspaceState.selectedWorkspaceId || '';
5271
+ if (loadBtn) loadBtn.disabled = !dashboardWorkspaceState.selectedWorkspaceId;
5272
+ }
5273
+
5274
+ function onDashboardWorkspaceSelection() {
5275
+ var select = document.getElementById('dashboard-workspace-select');
5276
+ dashboardWorkspaceState.selectedWorkspaceId = select ? select.value : '';
5277
+ persistLiveDashboardLayout();
5278
+ }
5279
+
5280
+ function buildSuggestedDashboardWorkspaceName() {
5281
+ var projectName = activeProject ? activeProject.split(/[/\\]/).pop() : 'dashboard';
5282
+ var viewName = activeView === 'office' ? '3d-hub' : activeView;
5283
+ return (projectName + ' ' + viewName).replace(/\s+/g, ' ').trim();
5284
+ }
5285
+
5286
+ function saveNamedDashboardWorkspace() {
5287
+ var selectedWorkspace = findSavedDashboardWorkspaceById(dashboardWorkspaceState.selectedWorkspaceId);
5288
+ var suggestedName = selectedWorkspace ? selectedWorkspace.name : buildSuggestedDashboardWorkspaceName();
5289
+ var providedName = window.prompt('Save current dashboard layout as:', suggestedName);
5290
+ if (providedName === null) return;
5291
+
5292
+ var workspaceName = providedName.trim();
5293
+ if (!workspaceName) {
5294
+ showToast('Workspace name cannot be empty.');
5295
+ return;
5296
+ }
5297
+
5298
+ var normalizedName = workspaceName.toLowerCase();
5299
+ var snapshot = captureCurrentDashboardLayoutSnapshot();
5300
+ var overwriteWorkspace = null;
5301
+ for (var i = 0; i < dashboardWorkspaceState.savedWorkspaces.length; i++) {
5302
+ var existingWorkspace = dashboardWorkspaceState.savedWorkspaces[i];
5303
+ if (existingWorkspace.name.toLowerCase() !== normalizedName) continue;
5304
+ overwriteWorkspace = existingWorkspace;
5305
+ break;
5306
+ }
5307
+
5308
+ if (overwriteWorkspace && (!selectedWorkspace || overwriteWorkspace.id !== selectedWorkspace.id)) {
5309
+ if (!confirm('Overwrite the saved workspace "' + overwriteWorkspace.name + '"?')) return;
5310
+ }
5311
+
5312
+ var targetId = '';
5313
+ if (overwriteWorkspace) targetId = overwriteWorkspace.id;
5314
+ else if (selectedWorkspace && selectedWorkspace.name.toLowerCase() === normalizedName) targetId = selectedWorkspace.id;
5315
+ else targetId = createDashboardWorkspaceId(workspaceName);
5316
+
5317
+ var savedWorkspace = normalizeSavedDashboardWorkspace({
5318
+ id: targetId,
5319
+ name: workspaceName,
5320
+ snapshot: snapshot,
5321
+ updatedAt: new Date().toISOString(),
5322
+ }, dashboardWorkspaceState.savedWorkspaces.length);
5323
+
5324
+ var didUpdate = false;
5325
+ var nextSavedWorkspaces = [];
5326
+ for (var j = 0; j < dashboardWorkspaceState.savedWorkspaces.length; j++) {
5327
+ var candidate = dashboardWorkspaceState.savedWorkspaces[j];
5328
+ if (candidate.id === savedWorkspace.id) {
5329
+ nextSavedWorkspaces.push(savedWorkspace);
5330
+ didUpdate = true;
5331
+ continue;
5332
+ }
5333
+ nextSavedWorkspaces.push(candidate);
5334
+ }
5335
+ if (!didUpdate) nextSavedWorkspaces.push(savedWorkspace);
5336
+
5337
+ dashboardWorkspaceState.savedWorkspaces = nextSavedWorkspaces;
5338
+ dashboardWorkspaceState.selectedWorkspaceId = savedWorkspace.id;
5339
+ dashboardWorkspaceState.liveWorkspace.snapshot = snapshot;
5340
+ dashboardWorkspaceState.liveWorkspace.updatedAt = new Date().toISOString();
5341
+ saveDashboardWorkspaceState();
5342
+ refreshDashboardWorkspaceControls();
5343
+ showToast((didUpdate ? 'Updated' : 'Saved') + ' workspace "' + savedWorkspace.name + '"');
5344
+ }
5345
+
5346
+ function applyDashboardLayoutSnapshot(rawSnapshot, options) {
5347
+ var opts = options || {};
5348
+ var snapshot = normalizeDashboardLayoutSnapshot(rawSnapshot);
5349
+
5350
+ activeProject = snapshot.project || '';
5351
+ activeBranch = snapshot.branch || 'main';
5352
+ agentFilterSearch = snapshot.agentFilters.search;
5353
+ agentFilterRole = snapshot.agentFilters.role;
5354
+ agentFilterStatus = snapshot.agentFilters.status;
5355
+ collapsedRoleGroups = clonePlainObject(snapshot.collapsedRoleGroups);
5356
+ pinnedExpanded = snapshot.pinnedExpanded !== false;
5357
+
5358
+ applyTheme(snapshot.theme, { skipPersist: true });
5359
+ applyCompactMode(snapshot.compactMode);
5360
+ applyAgentFilterInputs();
5361
+ applyPinnedSectionState();
5362
+ syncProjectSelectionUI();
5363
+
5364
+ if (Object.prototype.hasOwnProperty.call(opts, 'selectedWorkspaceId')) {
5365
+ dashboardWorkspaceState.selectedWorkspaceId = opts.selectedWorkspaceId || '';
5366
+ }
5367
+
5368
+ if (cachedAgents && Object.keys(cachedAgents).length) renderAgents(cachedAgents);
5369
+ renderPinnedMessages();
5370
+ switchView(snapshot.view, { skipPersist: true });
5371
+
5372
+ dashboardWorkspaceState.liveWorkspace.snapshot = captureCurrentDashboardLayoutSnapshot();
5373
+ dashboardWorkspaceState.liveWorkspace.updatedAt = new Date().toISOString();
5374
+
5375
+ lastMessageCount = 0;
5376
+ lastRenderedIds = [];
5377
+ refreshDashboardWorkspaceControls();
5378
+
5379
+ if (!opts.skipPersist) saveDashboardWorkspaceState();
5380
+ if (!opts.skipDataRefresh) {
5381
+ loadConversationList();
5382
+ poll();
4287
5383
  }
4288
- if (opts.method && opts.method !== 'GET') {
4289
- if (!opts.headers) opts.headers = {};
4290
- opts.headers['X-LTT-Request'] = '1';
5384
+ }
5385
+
5386
+ function loadNamedDashboardWorkspace() {
5387
+ var selectedWorkspace = findSavedDashboardWorkspaceById(dashboardWorkspaceState.selectedWorkspaceId);
5388
+ if (!selectedWorkspace) {
5389
+ dashboardWorkspaceState.selectedWorkspaceId = '';
5390
+ saveDashboardWorkspaceState();
5391
+ refreshDashboardWorkspaceControls();
5392
+ showToast('Select a saved workspace first.');
5393
+ return;
4291
5394
  }
4292
- return fetch(url, opts);
5395
+
5396
+ applyDashboardLayoutSnapshot(selectedWorkspace.snapshot, {
5397
+ selectedWorkspaceId: selectedWorkspace.id,
5398
+ });
5399
+ showToast('Loaded workspace "' + selectedWorkspace.name + '"');
4293
5400
  }
4294
5401
 
4295
5402
  var activeThread = null;
4296
5403
  var activeChannel = null; // null = all channels
4297
- var activeProject = ''; // empty = default/local
5404
+ var activeProject = dashboardWorkspaceState.liveWorkspace.snapshot.project || ''; // empty = default/local
4298
5405
  var cachedHistory = [];
4299
5406
  var cachedAgents = {};
4300
5407
  var msgPage = 0; // 0 = latest (default poll), 1+ = paginated older messages
@@ -4533,21 +5640,23 @@ function getProviderBadge(provider) {
4533
5640
  return '<span style="color:' + c + ';font-size:9px;font-weight:600;margin-right:4px">' + escapeHtml(provider) + '</span>';
4534
5641
  }
4535
5642
 
4536
- var agentFilterSearch = '';
4537
- var agentFilterRole = '';
4538
- var agentFilterStatus = '';
4539
- var collapsedRoleGroups = {};
5643
+ var agentFilterSearch = dashboardWorkspaceState.liveWorkspace.snapshot.agentFilters.search || '';
5644
+ var agentFilterRole = dashboardWorkspaceState.liveWorkspace.snapshot.agentFilters.role || '';
5645
+ var agentFilterStatus = dashboardWorkspaceState.liveWorkspace.snapshot.agentFilters.status || '';
5646
+ var collapsedRoleGroups = clonePlainObject(dashboardWorkspaceState.liveWorkspace.snapshot.collapsedRoleGroups);
4540
5647
 
4541
5648
  function filterAgents() {
4542
5649
  agentFilterSearch = (document.getElementById('agent-search').value || '').toLowerCase();
4543
5650
  agentFilterRole = document.getElementById('agent-role-filter').value;
4544
5651
  agentFilterStatus = document.getElementById('agent-status-filter').value;
4545
5652
  renderAgents(cachedAgents);
5653
+ persistLiveDashboardLayout();
4546
5654
  }
4547
5655
 
4548
5656
  function toggleRoleGroup(role) {
4549
5657
  collapsedRoleGroups[role] = !collapsedRoleGroups[role];
4550
5658
  renderAgents(cachedAgents);
5659
+ persistLiveDashboardLayout();
4551
5660
  }
4552
5661
 
4553
5662
  function renderAgents(agents) {
@@ -4564,6 +5673,8 @@ function renderAgents(agents) {
4564
5673
  var fname = keys[f];
4565
5674
  var finfo = agents[fname];
4566
5675
  var fstate = finfo.status || (finfo.alive ? 'active' : 'dead');
5676
+ // Hide API agents unless they are actively running (user clicked Start)
5677
+ if (finfo.is_api_agent && fstate !== 'active') continue;
4567
5678
  if (agentFilterSearch && fname.toLowerCase().indexOf(agentFilterSearch) === -1 &&
4568
5679
  (!finfo.display_name || finfo.display_name.toLowerCase().indexOf(agentFilterSearch) === -1)) continue;
4569
5680
  if (agentFilterRole && finfo.role !== agentFilterRole) continue;
@@ -4577,9 +5688,9 @@ function renderAgents(agents) {
4577
5688
  }
4578
5689
 
4579
5690
  // Group by role
4580
- var ROLE_ORDER = ['lead', 'backend', 'frontend', 'quality', 'monitor', 'advisor', ''];
4581
- var ROLE_LABELS = { lead: 'Lead', backend: 'Backend', frontend: 'Frontend', quality: 'Quality', monitor: 'Monitor', advisor: 'Advisor', '': 'Unassigned' };
4582
- var ROLE_COLORS = { lead: 'var(--accent)', backend: '#f59e0b', frontend: '#8b5cf6', quality: 'var(--green)', monitor: 'var(--red,#ef4444)', advisor: 'var(--purple,#a855f7)', '': 'var(--text-muted)' };
5691
+ var ROLE_ORDER = ['lead', 'backend', 'frontend', 'quality', 'monitor', 'advisor', 'api-agent', ''];
5692
+ var ROLE_LABELS = { lead: 'Lead', backend: 'Backend', frontend: 'Frontend', quality: 'Quality', monitor: 'Monitor', advisor: 'Advisor', 'api-agent': 'API Agents', '': 'Unassigned' };
5693
+ var ROLE_COLORS = { lead: 'var(--accent)', backend: '#f59e0b', frontend: '#8b5cf6', quality: 'var(--green)', monitor: 'var(--red,#ef4444)', advisor: 'var(--purple,#a855f7)', 'api-agent': '#0ea5e9', '': 'var(--text-muted)' };
4583
5694
  var roleGroups = {};
4584
5695
  for (var g = 0; g < filtered.length; g++) {
4585
5696
  var gname = filtered[g];
@@ -4649,7 +5760,10 @@ function renderAgents(agents) {
4649
5760
  : '<div class="agent-avatar" style="background:' + color + '">' + initial(name) + '</div>';
4650
5761
  var displayName = info.display_name || name;
4651
5762
  var roleHtml = '';
4652
- if (info.role) {
5763
+ if (info.is_api_agent) {
5764
+ var botColor = info.provider_color || '#0ea5e9';
5765
+ roleHtml = '<span class="role-badge" style="background:' + botColor + '22;color:' + botColor + '">BOT</span>';
5766
+ } else if (info.role) {
4653
5767
  var roleBg = info.role === 'quality' ? 'background:rgba(34,197,94,0.15);color:var(--green)' :
4654
5768
  info.role === 'monitor' ? 'background:rgba(239,68,68,0.15);color:var(--red,#ef4444)' :
4655
5769
  info.role === 'advisor' ? 'background:rgba(168,85,247,0.15);color:var(--purple,#a855f7)' :
@@ -4720,8 +5834,127 @@ function renderAgents(agents) {
4720
5834
  alertEl.style.display = 'none';
4721
5835
  }
4722
5836
 
4723
- // Update inject target dropdown
4724
- updateInjectTargets(keys);
5837
+ // Update inject target dropdown from the visible, live branch-local agent set
5838
+ updateInjectTargets(getEligibleInjectTargets(filtered, agents));
5839
+ }
5840
+
5841
+ function getServiceAgentEntries() {
5842
+ var branchName = getActiveDashboardBranchName();
5843
+ var keys = Object.keys(cachedAgents || {}).sort();
5844
+ var entries = [];
5845
+ for (var i = 0; i < keys.length; i++) {
5846
+ var name = keys[i];
5847
+ var info = cachedAgents[name] || {};
5848
+ if (!info.is_api_agent) continue;
5849
+ var agentBranch = cleanString(info.branch, 'main') || 'main';
5850
+ if (agentBranch !== branchName) continue;
5851
+ entries.push({ name: name, info: info });
5852
+ }
5853
+ return entries;
5854
+ }
5855
+
5856
+ function renderServices() {
5857
+ var el = document.getElementById('services-area');
5858
+ if (!el) return;
5859
+
5860
+ var branchName = getActiveDashboardBranchName();
5861
+ var projectLabel = activeProject ? activeProject.split(/[/\\]/).pop() : 'default project';
5862
+ var entries = getServiceAgentEntries();
5863
+ var activeCount = 0;
5864
+ var sleepingCount = 0;
5865
+ var offlineCount = 0;
5866
+
5867
+ for (var i = 0; i < entries.length; i++) {
5868
+ var state = entries[i].info.status || (entries[i].info.alive ? 'active' : 'dead');
5869
+ if (state === 'active') activeCount++;
5870
+ else if (state === 'sleeping') sleepingCount++;
5871
+ else offlineCount++;
5872
+ }
5873
+
5874
+ var html = '<div style="padding:20px">' +
5875
+ '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:18px">' +
5876
+ '<div>' +
5877
+ '<div style="font-size:20px;font-weight:700;color:var(--text);margin-bottom:4px">Services</div>' +
5878
+ '<div style="font-size:12px;color:var(--text-dim)">API-backed agents for <strong style="color:var(--text)">' + escapeHtml(projectLabel) + '</strong> on branch <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px;font-size:11px;color:var(--orange)">' + escapeHtml(branchName) + '</code>.</div>' +
5879
+ '</div>' +
5880
+ '<div style="display:flex;gap:8px;flex-wrap:wrap">' +
5881
+ '<span class="agent-detail-pill">Services <strong>' + entries.length + '</strong></span>' +
5882
+ '<span class="agent-detail-pill">Active <strong>' + activeCount + '</strong></span>' +
5883
+ '<span class="agent-detail-pill">Sleeping <strong>' + sleepingCount + '</strong></span>' +
5884
+ '<span class="agent-detail-pill">Offline <strong>' + offlineCount + '</strong></span>' +
5885
+ '</div>' +
5886
+ '</div>';
5887
+
5888
+ if (!entries.length) {
5889
+ html += '<div class="tasks-empty" style="padding:48px 24px">No API service agents are registered for this branch yet.<br><span style="display:block;margin-top:8px;font-size:11px">Use Launch to start one in the current project or switch branches to inspect another runtime slice.</span><button class="btn" type="button" style="margin-top:16px" onclick="switchView(\'launch\')">Open Launch</button></div>';
5890
+ html += '</div>';
5891
+ el.innerHTML = html;
5892
+ return;
5893
+ }
5894
+
5895
+ html += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px">';
5896
+ for (var j = 0; j < entries.length; j++) {
5897
+ var name = entries[j].name;
5898
+ var info = entries[j].info;
5899
+ var color = info.provider_color || getColor(name);
5900
+ var state = info.status || (info.alive ? 'active' : 'dead');
5901
+ var stateLabel = state.charAt(0).toUpperCase() + state.slice(1);
5902
+ var displayName = info.display_name || name;
5903
+ var capabilities = Array.isArray(info.capabilities) ? info.capabilities : [];
5904
+ var runtimeLabel = info.runtime_type || 'api-agent';
5905
+ var modelLabel = info.model_id || info.provider_id || info.provider || 'unknown';
5906
+ var lastSeenLabel = info.last_activity ? timeAgo(info.last_activity) : 'No heartbeat';
5907
+ var capHtml = '';
5908
+ if (capabilities.length) {
5909
+ for (var c = 0; c < capabilities.length; c++) {
5910
+ capHtml += '<span class="agent-detail-pill">' + escapeHtml(capabilities[c].replace(/_/g, ' ')) + '</span>';
5911
+ }
5912
+ } else {
5913
+ capHtml = '<span class="agent-detail-pill">No capabilities declared</span>';
5914
+ }
5915
+
5916
+ html += '<div class="agent-card' + (state !== 'active' ? ' ' + state : '') + '">';
5917
+ html += '<div class="agent-top" style="cursor:pointer" onclick="openAgentMetadataDrawer(\'' + name + '\')">';
5918
+ if (info.avatar) {
5919
+ html += '<img class="agent-avatar-img" src="' + escapeHtml(info.avatar) + '" alt="' + escapeHtml(name) + '" onerror="this.style.display=\'none\'">';
5920
+ } else {
5921
+ html += '<div class="agent-avatar" style="background:' + color + '">' + initial(name) + '</div>';
5922
+ }
5923
+ html += '<div class="agent-info">' +
5924
+ '<div class="agent-name" style="color:' + color + '">' + escapeHtml(displayName) + ' <span class="agent-badge ' + state + '">' + escapeHtml(stateLabel) + '</span></div>' +
5925
+ '<div class="agent-meta"><span>' + getProviderBadge(info.provider) + escapeHtml(runtimeLabel) + '</span><span style="margin-left:8px">' + escapeHtml(modelLabel) + '</span></div>' +
5926
+ '</div>' +
5927
+ '</div>';
5928
+ html += '<div class="agent-activity"><span class="agent-activity-icon ' + state + '"></span>' + escapeHtml(lastSeenLabel) + '</div>';
5929
+ if (info.current_status) {
5930
+ html += '<div class="agent-status-intent" title="' + escapeHtml(info.current_status) + '">' + escapeHtml(info.current_status) + '</div>';
5931
+ }
5932
+ html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin:10px 0">' + capHtml + '</div>';
5933
+ html += '<div style="display:flex;gap:8px;flex-wrap:wrap">';
5934
+ if (state === 'active') {
5935
+ html += '<button class="btn btn-primary" type="button" onclick="focusInjectForAgent(\'' + name + '\')">Message</button>';
5936
+ } else {
5937
+ html += '<button class="btn" type="button" onclick="switchView(\'launch\')">Open Launch</button>';
5938
+ }
5939
+ html += '<button class="btn" type="button" onclick="openAgentMetadataDrawer(\'' + name + '\')">Details</button>' +
5940
+ '</div>' +
5941
+ '</div>';
5942
+ }
5943
+ html += '</div></div>';
5944
+ el.innerHTML = html;
5945
+ }
5946
+
5947
+ function fetchApiAgents() {
5948
+ return lttFetch(scopedApiUrl('/api/agents', null, { includeBranch: false })).then(function(r) {
5949
+ return r.json();
5950
+ }).then(function(data) {
5951
+ cachedAgents = data && typeof data === 'object' ? data : {};
5952
+ renderServices();
5953
+ refreshAgentMetadataDrawer();
5954
+ }).catch(function(e) {
5955
+ console.error('Fetch API agents failed:', e);
5956
+ renderServices();
5957
+ });
4725
5958
  }
4726
5959
 
4727
5960
  function sendNudge(agentName) {
@@ -4753,9 +5986,7 @@ function removeAgent(agentName) {
4753
5986
  // ==================== RESPAWN AGENT ====================
4754
5987
 
4755
5988
  function respawnAgent(agentName) {
4756
- var pq = projectParam();
4757
- var sep = pq ? '&' : '?';
4758
- lttFetch('/api/agents/' + encodeURIComponent(agentName) + '/respawn-prompt' + pq, {
5989
+ lttFetch(scopedApiUrl('/api/agents/' + encodeURIComponent(agentName) + '/respawn-prompt'), {
4759
5990
  method: 'GET'
4760
5991
  }).then(function(r) {
4761
5992
  if (!r.ok) throw new Error('API returned ' + r.status);
@@ -4831,61 +6062,183 @@ function copyRespawnPrompt() {
4831
6062
 
4832
6063
  var lastAgentKeys = '';
4833
6064
 
6065
+ function getEligibleInjectTargets(agentNames, agents) {
6066
+ var branchName = (typeof getActiveDashboardBranchName === 'function' ? getActiveDashboardBranchName() : (activeBranch || 'main')) || 'main';
6067
+ var eligible = [];
6068
+ for (var i = 0; i < agentNames.length; i++) {
6069
+ var name = agentNames[i];
6070
+ var info = agents[name] || {};
6071
+ var state = info.status || (info.alive ? 'active' : 'dead');
6072
+ var agentBranch = info.branch || 'main';
6073
+ if (agentBranch !== branchName) continue;
6074
+ if (state === 'dead') continue;
6075
+ eligible.push(name);
6076
+ }
6077
+ return eligible;
6078
+ }
6079
+
4834
6080
  function updateInjectTargets(keys) {
4835
- var joined = keys.join(',');
6081
+ var sel = document.getElementById('inject-target');
6082
+ if (!sel) return;
6083
+
6084
+ var normalizedKeys = [];
6085
+ var seen = {};
6086
+ for (var i = 0; i < keys.length; i++) {
6087
+ var key = keys[i];
6088
+ if (!key || seen[key]) continue;
6089
+ seen[key] = true;
6090
+ normalizedKeys.push(key);
6091
+ }
6092
+
6093
+ var joined = normalizedKeys.join(',');
4836
6094
  if (joined === lastAgentKeys) return;
4837
6095
  lastAgentKeys = joined;
4838
6096
 
4839
- var sel = document.getElementById('inject-target');
4840
6097
  var current = sel.value;
4841
6098
  sel.innerHTML = '<option value="">Select agent...</option>';
4842
- for (var i = 0; i < keys.length; i++) {
6099
+ for (var i = 0; i < normalizedKeys.length; i++) {
4843
6100
  var opt = document.createElement('option');
4844
- opt.value = keys[i];
4845
- opt.textContent = keys[i];
6101
+ opt.value = normalizedKeys[i];
6102
+ opt.textContent = normalizedKeys[i];
4846
6103
  sel.appendChild(opt);
4847
6104
  }
4848
6105
  // Broadcast option
4849
- if (keys.length > 1) {
6106
+ if (normalizedKeys.length > 1) {
4850
6107
  var allOpt = document.createElement('option');
4851
6108
  allOpt.value = '__all__';
4852
6109
  allOpt.textContent = 'All agents';
4853
6110
  sel.appendChild(allOpt);
4854
6111
  }
4855
6112
 
4856
- if (current) sel.value = current;
6113
+ if (current === '__all__' && normalizedKeys.length > 1) sel.value = '__all__';
6114
+ else if (normalizedKeys.indexOf(current) !== -1) sel.value = current;
6115
+ else sel.value = '';
4857
6116
  updateSendBtn();
4858
6117
  }
4859
6118
 
6119
+ function buildDashboardQuery(extraParams, options) {
6120
+ var opts = options || {};
6121
+ var params = new URLSearchParams();
6122
+ var includeProject = opts.includeProject !== false;
6123
+ var includeBranch = opts.includeBranch !== false;
6124
+ var branch = Object.prototype.hasOwnProperty.call(opts, 'branch') ? opts.branch : activeBranch;
6125
+
6126
+ if (includeProject && activeProject) params.set('project', activeProject);
6127
+ if (includeBranch && branch && branch !== 'main') params.set('branch', branch);
6128
+ if (opts.includeToken && _lttToken) params.set('token', _lttToken);
6129
+
6130
+ if (extraParams) {
6131
+ for (var key in extraParams) {
6132
+ if (!Object.prototype.hasOwnProperty.call(extraParams, key)) continue;
6133
+ var value = extraParams[key];
6134
+ if (value === undefined || value === null || value === '') continue;
6135
+ params.set(key, String(value));
6136
+ }
6137
+ }
6138
+
6139
+ var query = params.toString();
6140
+ return query ? '?' + query : '';
6141
+ }
6142
+
6143
+ function scopedApiUrl(path, extraParams, options) {
6144
+ return path + buildDashboardQuery(extraParams, options);
6145
+ }
6146
+
6147
+ window.scopedApiUrl = scopedApiUrl;
6148
+
4860
6149
  function updateSendBtn() {
4861
6150
  var target = document.getElementById('inject-target').value;
4862
6151
  var content = document.getElementById('inject-content').value.trim();
4863
- document.getElementById('inject-btn').disabled = !target || !content;
6152
+ updateAssistantPrivateVisibility();
6153
+ document.getElementById('inject-btn').disabled = !target || (!content && !_attachedFile);
4864
6154
  }
4865
6155
 
4866
6156
  document.getElementById('inject-target').addEventListener('change', updateSendBtn);
4867
6157
  document.getElementById('inject-content').addEventListener('input', updateSendBtn);
4868
6158
 
4869
6159
  function projectParam() {
4870
- return activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
6160
+ return buildDashboardQuery(null, { includeBranch: false });
6161
+ }
6162
+
6163
+ function isMainBranchSelected() {
6164
+ return !activeBranch || activeBranch === 'main';
6165
+ }
6166
+
6167
+ function mainBranchOnlyViewHtml(surface) {
6168
+ return '<div class="tasks-empty">' + surface + ' only supports the <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px;font-size:12px;color:var(--orange)">main</code> branch while canonical storage is still globally scoped.</div>';
6169
+ }
6170
+
6171
+ function renderMainBranchOnlyView(elementId, surface) {
6172
+ var el = document.getElementById(elementId);
6173
+ if (el) el.innerHTML = mainBranchOnlyViewHtml(surface);
6174
+ }
6175
+
6176
+ function updateAssistantPrivateVisibility() {
6177
+ var row = document.getElementById('assistant-private-row');
6178
+ var checkbox = document.getElementById('assistant-private-optin');
6179
+ var isAssistantTarget = document.getElementById('inject-target').value === 'Assistant';
6180
+ if (!row || !checkbox) return;
6181
+ row.style.display = isAssistantTarget ? 'flex' : 'none';
6182
+ if (!isAssistantTarget) checkbox.checked = false;
6183
+ }
6184
+
6185
+ // ==================== FILE ATTACHMENT ====================
6186
+ var _attachedFile = null; // { name, mimeType, base64 }
6187
+
6188
+ function onFileSelected(input) {
6189
+ if (!input.files || !input.files[0]) return;
6190
+ var file = input.files[0];
6191
+ if (file.size > 10 * 1024 * 1024) { alert('File too large (max 10MB)'); input.value = ''; return; }
6192
+ var reader = new FileReader();
6193
+ reader.onload = function(e) {
6194
+ var dataUrl = e.target.result;
6195
+ var base64 = dataUrl.split(',')[1];
6196
+ _attachedFile = { name: file.name, mimeType: file.type, base64: base64 };
6197
+ document.getElementById('inject-file-preview').style.display = 'block';
6198
+ document.getElementById('inject-file-thumb').src = dataUrl;
6199
+ document.getElementById('inject-file-name').textContent = file.name + ' (' + Math.round(file.size / 1024) + 'KB)';
6200
+ updateSendBtn();
6201
+ };
6202
+ reader.readAsDataURL(file);
6203
+ }
6204
+
6205
+ function clearAttachment() {
6206
+ _attachedFile = null;
6207
+ document.getElementById('inject-file').value = '';
6208
+ document.getElementById('inject-file-preview').style.display = 'none';
6209
+ updateSendBtn();
4871
6210
  }
4872
6211
 
4873
6212
  function doInject() {
4874
6213
  var target = document.getElementById('inject-target').value;
4875
6214
  var content = document.getElementById('inject-content').value.trim();
4876
- if (!target || !content) return;
6215
+ if (!target || (!content && !_attachedFile)) return;
4877
6216
 
4878
- // Lock project context at send time prevents race if user switches project mid-type
4879
- var lockedProject = activeProject;
4880
- var pq = lockedProject ? '?project=' + encodeURIComponent(lockedProject) : '';
4881
- var body = JSON.stringify({ to: target, content: content });
4882
- lttFetch('/api/inject' + pq, {
6217
+ // If no text but has image, default prompt
6218
+ if (!content && _attachedFile) content = 'Analyze this image';
6219
+
6220
+ var payload = { to: target, content: content };
6221
+ if (target === 'Assistant') {
6222
+ payload.assistant_private = !!document.getElementById('assistant-private-optin').checked;
6223
+ }
6224
+
6225
+ // Include attachment if present
6226
+ if (_attachedFile) {
6227
+ payload.attachments = [{
6228
+ name: _attachedFile.name,
6229
+ mimeType: _attachedFile.mimeType,
6230
+ base64: _attachedFile.base64,
6231
+ }];
6232
+ }
6233
+
6234
+ lttFetch(scopedApiUrl('/api/inject'), {
4883
6235
  method: 'POST',
4884
6236
  headers: { 'Content-Type': 'application/json' },
4885
- body: body
6237
+ body: JSON.stringify(payload)
4886
6238
  }).then(function(r) { return r.json(); }).then(function(res) {
4887
6239
  if (res.success) {
4888
6240
  document.getElementById('inject-content').value = '';
6241
+ clearAttachment();
4889
6242
  updateSendBtn();
4890
6243
  poll();
4891
6244
  }
@@ -5120,6 +6473,18 @@ function renderMessages(messages) {
5120
6473
  lastFrom = m.from;
5121
6474
  lastTo = m.to;
5122
6475
  } else {
6476
+ // Build attachment thumbnails if present
6477
+ var attachHtml = '';
6478
+ if (m.attachments && m.attachments.length > 0) {
6479
+ attachHtml = '<div style="margin:6px 0;display:flex;gap:6px;flex-wrap:wrap;">';
6480
+ for (var ati = 0; ati < m.attachments.length; ati++) {
6481
+ var att = m.attachments[ati];
6482
+ if (att.base64 && att.mimeType && att.mimeType.indexOf('image') === 0) {
6483
+ attachHtml += '<img src="data:' + att.mimeType + ';base64,' + att.base64 + '" style="max-height:120px;max-width:200px;border-radius:6px;border:1px solid var(--border);cursor:pointer;" onclick="window.open(this.src)" title="' + escapeHtml(att.name || 'image') + '">';
6484
+ }
6485
+ }
6486
+ attachHtml += '</div>';
6487
+ }
5123
6488
  html += '<div class="message' + newClass + groupClass + '">' +
5124
6489
  buildMsgActions(m.id) +
5125
6490
  msgAvatarHtml +
@@ -5131,6 +6496,7 @@ function renderMessages(messages) {
5131
6496
  '<span class="msg-time" title="' + formatTime(m.timestamp) + '">' + relativeTime(m.timestamp) + '</span>' +
5132
6497
  '<div class="msg-badges">' + badges + '</div>' +
5133
6498
  '</div>' +
6499
+ attachHtml +
5134
6500
  '<div class="msg-content">' + renderMarkdown(m.content) + '</div>' +
5135
6501
  buildReactionsHtml(m.id) +
5136
6502
  buildReadReceipts(m.id) +
@@ -5220,11 +6586,9 @@ function loadMoreMessages() {
5220
6586
  if (msgLoadingMore) return;
5221
6587
  msgLoadingMore = true;
5222
6588
  renderMessages(cachedHistory); // Re-render to show loading state
5223
- var pp = activeProject ? '&project=' + encodeURIComponent(activeProject) : '';
5224
- var bp = activeBranch && activeBranch !== 'main' ? '&branch=' + encodeURIComponent(activeBranch) : '';
5225
6589
  // Calculate next page: we want older messages
5226
6590
  var nextPage = Math.max(1, msgTotalPages - Math.floor(cachedHistory.length / 50));
5227
- lttFetch('/api/history?page=' + nextPage + '&limit=50' + pp + bp).then(function(r) {
6591
+ lttFetch(scopedApiUrl('/api/history', { page: nextPage, limit: 50 })).then(function(r) {
5228
6592
  return r.json();
5229
6593
  }).then(function(data) {
5230
6594
  msgLoadingMore = false;
@@ -5334,38 +6698,53 @@ function scrollToBottom() {
5334
6698
 
5335
6699
  // ==================== COMPACT MODE ====================
5336
6700
 
5337
- var compactMode = localStorage.getItem('compactMode') === 'true';
6701
+ var compactMode = !!dashboardWorkspaceState.liveWorkspace.snapshot.compactMode;
5338
6702
 
5339
- function initCompactMode() {
6703
+ function applyCompactMode(enabled) {
6704
+ compactMode = !!enabled;
5340
6705
  var area = document.getElementById('messages');
5341
6706
  var btn = document.getElementById('compact-toggle');
5342
- if (compactMode) {
5343
- area.classList.add('compact-mode');
5344
- btn.classList.add('active');
5345
- }
6707
+ if (area) area.classList.toggle('compact-mode', compactMode);
6708
+ if (btn) btn.classList.toggle('active', compactMode);
6709
+ }
6710
+
6711
+ function initCompactMode() {
6712
+ applyCompactMode(compactMode);
5346
6713
  }
5347
6714
 
5348
6715
  function toggleCompactMode() {
5349
- compactMode = !compactMode;
5350
- localStorage.setItem('compactMode', compactMode);
5351
- var area = document.getElementById('messages');
5352
- var btn = document.getElementById('compact-toggle');
5353
- area.classList.toggle('compact-mode', compactMode);
5354
- btn.classList.toggle('active', compactMode);
6716
+ applyCompactMode(!compactMode);
6717
+ persistLiveDashboardLayout();
5355
6718
  }
5356
6719
 
5357
6720
  // ==================== CONVERSATION MANAGEMENT ====================
5358
6721
 
5359
6722
  function clearMessages() {
5360
- if (!confirm('Clear all messages for this project? This cannot be undone.')) return;
5361
- var project = document.getElementById('project-select').value;
5362
- lttFetch('/api/clear-messages?project=' + encodeURIComponent(project), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) })
6723
+ if (!confirm('Clear all messages for the current branch? This cannot be undone.')) return;
6724
+ lttFetch(scopedApiUrl('/api/clear-messages'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) })
5363
6725
  .then(function(r) { return r.json(); })
5364
6726
  .then(function(data) {
6727
+ if (data && data.error) {
6728
+ throw new Error(data.error);
6729
+ }
5365
6730
  if (data.success) {
5366
- document.getElementById('messages').innerHTML = '';
5367
- showToast('Messages cleared');
6731
+ cachedHistory = [];
6732
+ lastRenderedIds = [];
6733
+ lastMessageCount = 0;
6734
+ msgTotalPages = 0;
6735
+ activeThread = null;
6736
+ activeChannel = null;
6737
+ renderThreads(cachedHistory);
6738
+ renderChannelBar(cachedHistory);
6739
+ renderMessages(cachedHistory);
6740
+ renderPinnedMessages();
6741
+ renderBookmarksSidebar();
6742
+ showToast('Cleared ' + (data.cleared_messages || 0) + ' message' + ((data.cleared_messages || 0) === 1 ? '' : 's') + ' from ' + ((data.branch || 'main') === 'main' ? 'main' : data.branch));
6743
+ poll();
5368
6744
  }
6745
+ }).catch(function(error) {
6746
+ console.error('Clear messages failed:', error);
6747
+ showToast('Clear messages failed: ' + error.message);
5369
6748
  });
5370
6749
  }
5371
6750
 
@@ -5436,8 +6815,281 @@ function showToast(msg) {
5436
6815
  // ==================== SEARCH ====================
5437
6816
 
5438
6817
  var searchQuery = '';
6818
+ var omniboxForcedOpen = false;
6819
+ var omniboxResults = [];
6820
+ var omniboxSelectedIndex = 0;
6821
+ var omniboxSnapshotValue = '';
6822
+
6823
+ function isOmniboxActive() {
6824
+ var input = document.getElementById('search-input');
6825
+ var raw = input ? String(input.value || '') : '';
6826
+ return !!omniboxForcedOpen || raw.trim().charAt(0) === '>';
6827
+ }
6828
+
6829
+ function getOmniboxQuery() {
6830
+ var input = document.getElementById('search-input');
6831
+ var raw = input ? String(input.value || '') : '';
6832
+ var trimmed = raw.trim();
6833
+ if (trimmed.charAt(0) === '>') trimmed = trimmed.slice(1);
6834
+ return trimmed.trim().toLowerCase();
6835
+ }
6836
+
6837
+ function closeOmniboxPanel() {
6838
+ var panel = document.getElementById('omnibox-panel');
6839
+ if (!panel) return;
6840
+ panel.classList.remove('open');
6841
+ panel.setAttribute('aria-hidden', 'true');
6842
+ panel.innerHTML = '';
6843
+ omniboxResults = [];
6844
+ omniboxSelectedIndex = 0;
6845
+ }
6846
+
6847
+ function syncSearchBarVisibility() {
6848
+ var bar = document.getElementById('search-bar');
6849
+ if (!bar) return;
6850
+ bar.style.display = (!replayActive && (activeView === 'messages' || isOmniboxActive())) ? 'flex' : 'none';
6851
+ }
6852
+
6853
+ function updateSearchInputMode() {
6854
+ var input = document.getElementById('search-input');
6855
+ if (!input) return;
6856
+ var omniboxActive = isOmniboxActive();
6857
+ input.classList.toggle('omnibox-active', omniboxActive);
6858
+ if (omniboxActive) input.placeholder = 'Command palette — switch views, branches, projects, or open an agent';
6859
+ else if (searchAllMode) input.placeholder = 'Search ALL projects...';
6860
+ else input.placeholder = 'Search messages... ( / )';
6861
+ if (!omniboxActive) closeOmniboxPanel();
6862
+ }
6863
+
6864
+ function openOmnibox(prefillQuery) {
6865
+ if (replayActive) return;
6866
+ var input = document.getElementById('search-input');
6867
+ if (!input) return;
6868
+ if (!isOmniboxActive()) omniboxSnapshotValue = input.value || '';
6869
+ omniboxForcedOpen = true;
6870
+ if (typeof prefillQuery === 'string') input.value = prefillQuery;
6871
+ syncSearchBarVisibility();
6872
+ updateSearchInputMode();
6873
+ renderOmnibox();
6874
+ input.focus();
6875
+ if (typeof input.setSelectionRange === 'function') input.setSelectionRange(input.value.length, input.value.length);
6876
+ }
6877
+
6878
+ function closeOmnibox(options) {
6879
+ var opts = options || {};
6880
+ var input = document.getElementById('search-input');
6881
+ omniboxForcedOpen = false;
6882
+ if (input) {
6883
+ if (opts.restoreSnapshot) input.value = omniboxSnapshotValue || '';
6884
+ else if (!opts.preserveInput) input.value = '';
6885
+ }
6886
+ updateSearchInputMode();
6887
+ if (activeView === 'messages' && !isOmniboxActive()) onSearch();
6888
+ else searchQuery = '';
6889
+ syncSearchBarVisibility();
6890
+ if (!opts.keepFocus && input) input.blur();
6891
+ }
6892
+
6893
+ function ensureLocalMessageSearchMode() {
6894
+ if (searchAllMode) toggleSearchAll();
6895
+ }
6896
+
6897
+ function applyOmniboxMessageSearch(query) {
6898
+ var input = document.getElementById('search-input');
6899
+ if (!input) return;
6900
+ omniboxForcedOpen = false;
6901
+ ensureLocalMessageSearchMode();
6902
+ input.value = query;
6903
+ updateSearchInputMode();
6904
+ switchView('messages');
6905
+ onSearch();
6906
+ input.focus();
6907
+ if (typeof input.setSelectionRange === 'function') input.setSelectionRange(input.value.length, input.value.length);
6908
+ }
6909
+
6910
+ function applyOmniboxDeepSearch(query) {
6911
+ var input = document.getElementById('search-input');
6912
+ if (!input) return;
6913
+ omniboxForcedOpen = false;
6914
+ ensureLocalMessageSearchMode();
6915
+ input.value = query;
6916
+ updateSearchInputMode();
6917
+ switchView('messages');
6918
+ deepSearch();
6919
+ input.focus();
6920
+ if (typeof input.setSelectionRange === 'function') input.setSelectionRange(input.value.length, input.value.length);
6921
+ }
6922
+
6923
+ function buildOmniboxCommand(kind, label, subtitle, badge, keywords, run) {
6924
+ return {
6925
+ kind: kind,
6926
+ label: label,
6927
+ subtitle: subtitle,
6928
+ badge: badge || '',
6929
+ keywords: keywords || '',
6930
+ run: run,
6931
+ };
6932
+ }
6933
+
6934
+ function collectOmniboxCommands() {
6935
+ var commands = [];
6936
+ var query = getOmniboxQuery();
6937
+ var queryLabel = query ? '“' + query + '”' : '';
6938
+ var viewCommands = [
6939
+ { view: 'office', label: 'Open 3D Hub', subtitle: 'Switch to the office world view', keywords: 'office 3d hub world' },
6940
+ { view: 'messages', label: 'Open Messages', subtitle: 'Switch to the live conversation feed', keywords: 'messages inbox chat conversation' },
6941
+ { view: 'tasks', label: 'Open Tasks', subtitle: 'Switch to the task board', keywords: 'tasks kanban board' },
6942
+ { view: 'workspaces', label: 'Open Workspaces', subtitle: 'Switch to per-agent workspaces', keywords: 'workspaces notes storage' },
6943
+ { view: 'workflows', label: 'Open Workflows', subtitle: 'Switch to workflow pipelines', keywords: 'workflows pipelines steps' },
6944
+ { view: 'graph', label: 'Open Graph', subtitle: 'Switch to the operator flow graph', keywords: 'graph operator flow channels svg' },
6945
+ { view: 'plan', label: 'Open Plan', subtitle: 'Switch to the autonomous plan view', keywords: 'plan monitor autonomous execution' },
6946
+ { view: 'launch', label: 'Open Launch', subtitle: 'Switch to the launcher panel', keywords: 'launch terminals templates' },
6947
+ { view: 'rules', label: 'Open Rules', subtitle: 'Switch to project rules', keywords: 'rules governance' },
6948
+ { view: 'stats', label: 'Open Stats', subtitle: 'Switch to team statistics', keywords: 'stats analytics leaderboard' },
6949
+ { view: 'services', label: 'Open Services', subtitle: 'Switch to API service agents', keywords: 'services api agents bots' },
6950
+ { view: 'docs', label: 'Open Docs', subtitle: 'Switch to in-dashboard documentation', keywords: 'docs help manual' },
6951
+ ];
6952
+
6953
+ if (query.length >= 2) {
6954
+ commands.push(buildOmniboxCommand('Search', 'Search loaded messages for ' + queryLabel, 'Filter the current conversation in the Messages view', 'Enter', 'search messages filter conversation', function() {
6955
+ applyOmniboxMessageSearch(query);
6956
+ }));
6957
+ commands.push(buildOmniboxCommand('Deep Search', 'Deep search history for ' + queryLabel, 'Run the existing server-side history search', 'Enter', 'deep search history full text server', function() {
6958
+ applyOmniboxDeepSearch(query);
6959
+ }));
6960
+ }
6961
+
6962
+ for (var i = 0; i < viewCommands.length; i++) {
6963
+ (function(command) {
6964
+ commands.push(buildOmniboxCommand('View', command.label, command.subtitle, activeView === command.view ? 'Current' : '', command.keywords, function() {
6965
+ closeOmnibox();
6966
+ switchView(command.view);
6967
+ }));
6968
+ })(viewCommands[i]);
6969
+ }
6970
+
6971
+ var projectSelect = document.getElementById('project-select');
6972
+ if (projectSelect) {
6973
+ for (var p = 0; p < projectSelect.options.length; p++) {
6974
+ var option = projectSelect.options[p];
6975
+ if (!option.value) continue;
6976
+ (function(projectValue, projectLabel) {
6977
+ commands.push(buildOmniboxCommand('Project', 'Switch project to ' + projectLabel, projectValue, activeProject === projectValue ? 'Current' : '', 'project workspace repo folder', function() {
6978
+ closeOmnibox();
6979
+ projectSelect.value = projectValue;
6980
+ switchProject();
6981
+ }));
6982
+ })(option.value, option.textContent || option.value);
6983
+ }
6984
+ }
6985
+
6986
+ var branchMap = (cachedBranchInfo && typeof cachedBranchInfo === 'object') ? cachedBranchInfo : {};
6987
+ if (!branchMap.main) branchMap.main = { message_count: 0 };
6988
+ var branchNames = Object.keys(branchMap);
6989
+ for (var b = 0; b < branchNames.length; b++) {
6990
+ (function(branchName, branchInfo) {
6991
+ commands.push(buildOmniboxCommand('Branch', 'Switch branch to ' + branchName, (branchInfo.message_count || 0) + ' messages', activeBranch === branchName ? 'Current' : '', 'branch conversation history', function() {
6992
+ closeOmnibox();
6993
+ switchBranch(branchName);
6994
+ }));
6995
+ })(branchNames[b], branchMap[branchNames[b]] || {});
6996
+ }
6997
+
6998
+ var agentNames = Object.keys(cachedAgents || {}).sort();
6999
+ for (var a = 0; a < agentNames.length; a++) {
7000
+ (function(agentName) {
7001
+ var agent = cachedAgents[agentName] || {};
7002
+ var state = agent.status || (agent.alive ? 'active' : 'dead');
7003
+ var parts = [];
7004
+ if (agent.role) parts.push(agent.role);
7005
+ parts.push(state);
7006
+ if (agent.provider) parts.push(agent.provider);
7007
+ commands.push(buildOmniboxCommand('Agent', 'Open details for ' + (agent.display_name || agentName), parts.join(' • '), state.charAt(0).toUpperCase() + state.slice(1), 'agent profile metadata drawer details ' + agentName + ' ' + (agent.display_name || '') + ' ' + (agent.role || '') + ' ' + (agent.provider || ''), function() {
7008
+ closeOmnibox();
7009
+ openAgentMetadataDrawer(agentName);
7010
+ }));
7011
+ })(agentNames[a]);
7012
+ }
7013
+
7014
+ commands.push(buildOmniboxCommand('Display', currentTheme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme', 'Use the existing dashboard theme toggle', currentTheme === 'dark' ? 'Dark' : 'Light', 'theme appearance display light dark', function() {
7015
+ closeOmnibox();
7016
+ toggleTheme();
7017
+ }));
7018
+ commands.push(buildOmniboxCommand('Display', compactMode ? 'Disable compact mode' : 'Enable compact mode', 'Use the existing compact message layout toggle', compactMode ? 'On' : 'Off', 'compact mode density messages layout', function() {
7019
+ closeOmnibox();
7020
+ toggleCompactMode();
7021
+ }));
7022
+ commands.push(buildOmniboxCommand('Refresh', 'Refresh dashboard data', 'Run the existing dashboard poll immediately', '', 'refresh reload sync poll', function() {
7023
+ closeOmnibox();
7024
+ poll();
7025
+ }));
7026
+ commands.push(buildOmniboxCommand('Help', 'Show keyboard shortcuts', 'Open the current shortcuts overlay', '', 'help shortcuts keyboard overlay', function() {
7027
+ closeOmnibox();
7028
+ toggleShortcutsOverlay();
7029
+ }));
7030
+
7031
+ if (!query) return commands.slice(0, 14);
7032
+
7033
+ return commands.filter(function(command) {
7034
+ var haystack = (command.kind + ' ' + command.label + ' ' + command.subtitle + ' ' + command.keywords).toLowerCase();
7035
+ return haystack.indexOf(query) !== -1;
7036
+ }).slice(0, 14);
7037
+ }
7038
+
7039
+ function renderOmnibox() {
7040
+ if (!isOmniboxActive()) {
7041
+ updateSearchInputMode();
7042
+ return;
7043
+ }
7044
+
7045
+ var panel = document.getElementById('omnibox-panel');
7046
+ if (!panel) return;
7047
+ omniboxResults = collectOmniboxCommands();
7048
+ if (omniboxSelectedIndex >= omniboxResults.length) omniboxSelectedIndex = omniboxResults.length ? 0 : 0;
7049
+ if (omniboxSelectedIndex < 0) omniboxSelectedIndex = 0;
7050
+
7051
+ var html = '<div class="omnibox-header"><strong>Command palette</strong><span class="omnibox-hint">↑ ↓ navigate • Enter run • Esc close</span></div>';
7052
+ if (!omniboxResults.length) {
7053
+ html += '<div class="omnibox-empty">No matching commands yet. Try a view, project, branch, agent, or a deep-search phrase.</div>';
7054
+ } else {
7055
+ html += '<div class="omnibox-list">';
7056
+ for (var i = 0; i < omniboxResults.length; i++) {
7057
+ var result = omniboxResults[i];
7058
+ html += '<button type="button" class="omnibox-item' + (i === omniboxSelectedIndex ? ' active' : '') + '" onclick="executeOmniboxResult(' + i + ')">' +
7059
+ '<span class="omnibox-item-kind">' + escapeHtml(result.kind) + '</span>' +
7060
+ '<span class="omnibox-item-main">' +
7061
+ '<span class="omnibox-item-label">' + escapeHtml(result.label) + '</span>' +
7062
+ '<span class="omnibox-item-sub">' + escapeHtml(result.subtitle) + '</span>' +
7063
+ '</span>' +
7064
+ (result.badge ? '<span class="omnibox-item-badge">' + escapeHtml(result.badge) + '</span>' : '') +
7065
+ '</button>';
7066
+ }
7067
+ html += '</div>';
7068
+ }
7069
+ panel.innerHTML = html;
7070
+ panel.classList.add('open');
7071
+ panel.setAttribute('aria-hidden', 'false');
7072
+ }
7073
+
7074
+ function moveOmniboxSelection(delta) {
7075
+ if (!omniboxResults.length) return;
7076
+ omniboxSelectedIndex = (omniboxSelectedIndex + delta + omniboxResults.length) % omniboxResults.length;
7077
+ renderOmnibox();
7078
+ }
7079
+
7080
+ function executeOmniboxResult(index) {
7081
+ var command = omniboxResults[index];
7082
+ if (!command || typeof command.run !== 'function') return;
7083
+ command.run();
7084
+ }
5439
7085
 
5440
7086
  function onSearch() {
7087
+ updateSearchInputMode();
7088
+ if (isOmniboxActive()) {
7089
+ renderOmnibox();
7090
+ return;
7091
+ }
7092
+ omniboxSnapshotValue = document.getElementById('search-input').value || '';
5441
7093
  searchQuery = document.getElementById('search-input').value.toLowerCase().trim();
5442
7094
  if (searchAllMode && searchQuery.length >= 2) {
5443
7095
  searchAllProjects(searchQuery);
@@ -5450,10 +7102,9 @@ function onSearch() {
5450
7102
  function deepSearch() {
5451
7103
  var query = document.getElementById('search-input').value.trim();
5452
7104
  if (query.length < 2) return;
5453
- var pq = activeProject ? '&project=' + encodeURIComponent(activeProject) : '';
5454
7105
  var countEl = document.getElementById('search-count');
5455
7106
  countEl.textContent = 'Searching...';
5456
- lttFetch('/api/search?q=' + encodeURIComponent(query) + '&limit=100' + pq).then(function(r) { return r.json(); }).then(function(data) {
7107
+ lttFetch(scopedApiUrl('/api/search', { q: query, limit: 100 })).then(function(r) { return r.json(); }).then(function(data) {
5457
7108
  if (data.error) { countEl.textContent = data.error; return; }
5458
7109
  countEl.textContent = data.results_count + ' deep result' + (data.results_count !== 1 ? 's' : '');
5459
7110
  // Convert search results to message format for renderMessages
@@ -5508,16 +7159,16 @@ var reactions = {};
5508
7159
  var pins = {};
5509
7160
 
5510
7161
  function loadReactions() {
5511
- try { var s = localStorage.getItem('ltt-reactions'); if (s) reactions = JSON.parse(s); } catch (e) {}
5512
- try { var p = localStorage.getItem('ltt-pins'); if (p) pins = JSON.parse(p); } catch (e) {}
7162
+ reactions = clonePlainObject(dashboardWorkspaceState.preferences.reactions);
7163
+ pins = clonePlainObject(dashboardWorkspaceState.preferences.pins);
5513
7164
  }
5514
7165
 
5515
7166
  function saveReactions() {
5516
- try { localStorage.setItem('ltt-reactions', JSON.stringify(reactions)); } catch (e) {}
7167
+ persistDashboardPreferences();
5517
7168
  }
5518
7169
 
5519
7170
  function savePins() {
5520
- try { localStorage.setItem('ltt-pins', JSON.stringify(pins)); } catch (e) {}
7171
+ persistDashboardPreferences();
5521
7172
  }
5522
7173
 
5523
7174
  function toggleReactPicker(msgId) {
@@ -5553,6 +7204,7 @@ function renderPinnedMessages() {
5553
7204
  var pinIds = Object.keys(pins);
5554
7205
  if (!pinIds.length) {
5555
7206
  section.classList.remove('visible');
7207
+ list.innerHTML = '';
5556
7208
  return;
5557
7209
  }
5558
7210
  section.classList.add('visible');
@@ -5569,13 +7221,14 @@ function renderPinnedMessages() {
5569
7221
  '</div></div>';
5570
7222
  }
5571
7223
  list.innerHTML = html;
7224
+ applyPinnedSectionState();
5572
7225
  }
5573
7226
 
5574
- var pinnedExpanded = true;
7227
+ var pinnedExpanded = dashboardWorkspaceState.liveWorkspace.snapshot.pinnedExpanded !== false;
5575
7228
  function togglePinnedSection() {
5576
7229
  pinnedExpanded = !pinnedExpanded;
5577
- document.getElementById('pinned-list').style.display = pinnedExpanded ? '' : 'none';
5578
- document.getElementById('pinned-toggle').textContent = pinnedExpanded ? 'Hide' : 'Show';
7230
+ applyPinnedSectionState();
7231
+ persistLiveDashboardLayout();
5579
7232
  }
5580
7233
 
5581
7234
  function buildMsgActions(msgId) {
@@ -5702,10 +7355,22 @@ loadReactions();
5702
7355
 
5703
7356
  // ==================== THEME ====================
5704
7357
 
5705
- var currentTheme = localStorage.getItem('ltt-theme') || 'dark';
7358
+ var currentTheme = dashboardWorkspaceState.liveWorkspace.snapshot.theme || 'dark';
7359
+
7360
+ function refreshThemeSensitiveDashboardViews() {
7361
+ renderAgents(cachedAgents);
7362
+ refreshAgentMetadataDrawer();
7363
+ renderAgentStats();
7364
+ renderThreads(cachedHistory);
7365
+ renderPinnedMessages();
7366
+ renderBookmarksSidebar();
7367
+ if (!replayActive) renderMessages(cachedHistory);
7368
+ if (activeView === 'graph') renderGraphView();
7369
+ }
5706
7370
 
5707
- function applyTheme(theme) {
5708
- currentTheme = theme;
7371
+ function applyTheme(theme, options) {
7372
+ var opts = options || {};
7373
+ currentTheme = theme === 'light' ? 'light' : 'dark';
5709
7374
  if (theme === 'light') {
5710
7375
  document.documentElement.setAttribute('data-theme', 'light');
5711
7376
  document.getElementById('theme-toggle').innerHTML = '&#x2600;&#xfe0f;';
@@ -5713,25 +7378,55 @@ function applyTheme(theme) {
5713
7378
  document.documentElement.removeAttribute('data-theme');
5714
7379
  document.getElementById('theme-toggle').innerHTML = '&#x1f319;';
5715
7380
  }
5716
- localStorage.setItem('ltt-theme', theme);
7381
+ if (!opts.skipRefresh) refreshThemeSensitiveDashboardViews();
7382
+ if (!opts.skipPersist) persistLiveDashboardLayout();
5717
7383
  }
5718
7384
 
5719
7385
  function toggleTheme() {
5720
7386
  applyTheme(currentTheme === 'dark' ? 'light' : 'dark');
5721
7387
  }
5722
7388
 
5723
- applyTheme(currentTheme);
7389
+ applyTheme(currentTheme, { skipPersist: true, skipRefresh: true });
5724
7390
 
5725
7391
  // ==================== KEYBOARD SHORTCUTS ====================
5726
7392
 
7393
+ document.getElementById('search-input').addEventListener('focus', function() {
7394
+ if (isOmniboxActive()) renderOmnibox();
7395
+ });
7396
+
7397
+ document.getElementById('search-input').addEventListener('keydown', function(e) {
7398
+ if (!isOmniboxActive()) return;
7399
+ if (e.key === 'ArrowDown') {
7400
+ e.preventDefault();
7401
+ moveOmniboxSelection(1);
7402
+ return;
7403
+ }
7404
+ if (e.key === 'ArrowUp') {
7405
+ e.preventDefault();
7406
+ moveOmniboxSelection(-1);
7407
+ return;
7408
+ }
7409
+ if ((e.key === 'Enter' || e.key === 'Tab') && omniboxResults.length) {
7410
+ e.preventDefault();
7411
+ executeOmniboxResult(omniboxSelectedIndex);
7412
+ }
7413
+ });
7414
+
5727
7415
  document.addEventListener('keydown', function(e) {
5728
7416
  // Don't intercept when typing in inputs
5729
7417
  var tag = document.activeElement.tagName;
5730
7418
  var inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
7419
+ var isPaletteShortcut = (e.ctrlKey || e.metaKey) && !e.shiftKey && String(e.key || '').toLowerCase() === 'k';
5731
7420
 
5732
7421
  // Escape — exit replay or clear search/filters
5733
7422
  if (e.key === 'Escape') {
5734
7423
  if (replayActive) { exitReplay(); return; }
7424
+ if (isAgentMetadataDrawerOpen()) { closeAgentMetadataDrawer(); return; }
7425
+ if (isOmniboxActive()) {
7426
+ e.preventDefault();
7427
+ closeOmnibox({ restoreSnapshot: activeView === 'messages' });
7428
+ return;
7429
+ }
5735
7430
  document.getElementById('search-input').value = '';
5736
7431
  searchQuery = '';
5737
7432
  bookmarkFilter = false;
@@ -5744,11 +7439,18 @@ document.addEventListener('keydown', function(e) {
5744
7439
  return;
5745
7440
  }
5746
7441
 
7442
+ if (isPaletteShortcut) {
7443
+ e.preventDefault();
7444
+ openOmnibox('');
7445
+ return;
7446
+ }
7447
+
5747
7448
  if (inInput) return;
5748
7449
 
5749
- // / or Ctrl+K — focus search
5750
- if (e.key === '/' || (e.ctrlKey && e.key === 'k')) {
7450
+ // / — focus search
7451
+ if (e.key === '/') {
5751
7452
  e.preventDefault();
7453
+ if (activeView !== 'messages') switchView('messages');
5752
7454
  document.getElementById('search-input').focus();
5753
7455
  return;
5754
7456
  }
@@ -5786,7 +7488,9 @@ function toggleShortcutsOverlay() {
5786
7488
  '<h3 style="font-size:16px;font-weight:700;margin-bottom:16px;color:var(--text)">Keyboard Shortcuts</h3>' +
5787
7489
  '<div style="display:grid;grid-template-columns:80px 1fr;gap:8px 16px;font-size:13px">' +
5788
7490
  '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">?</kbd><span>Show this help</span>' +
5789
- '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">/</kbd><span>Focus search</span>' +
7491
+ '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">/</kbd><span>Focus message search</span>' +
7492
+ '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">Ctrl/⌘ K</kbd><span>Open command palette</span>' +
7493
+ '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">&gt;</kbd><span>Use command mode inside search</span>' +
5790
7494
  '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">Esc</kbd><span>Clear search / exit</span>' +
5791
7495
  '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">1</kbd><span>3D Hub</span>' +
5792
7496
  '<kbd style="background:var(--surface-2);padding:2px 8px;border-radius:4px;text-align:center;font-family:monospace">2</kbd><span>Messages</span>' +
@@ -5940,14 +7644,11 @@ var bookmarks = {};
5940
7644
  var bookmarkFilter = false;
5941
7645
 
5942
7646
  function loadBookmarks() {
5943
- try {
5944
- var stored = localStorage.getItem('ltt-bookmarks');
5945
- if (stored) bookmarks = JSON.parse(stored);
5946
- } catch (e) {}
7647
+ bookmarks = clonePlainObject(dashboardWorkspaceState.preferences.bookmarks);
5947
7648
  }
5948
7649
 
5949
7650
  function saveBookmarks() {
5950
- try { localStorage.setItem('ltt-bookmarks', JSON.stringify(bookmarks)); } catch (e) {}
7651
+ persistDashboardPreferences();
5951
7652
  }
5952
7653
 
5953
7654
  function toggleBookmark(msgId) {
@@ -6031,47 +7732,53 @@ function exportConversation() {
6031
7732
 
6032
7733
  function exportJSON() {
6033
7734
  if (!cachedHistory.length) return;
6034
- var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
6035
7735
  var a = document.createElement('a');
6036
- a.href = '/api/export-json' + pq;
7736
+ a.href = scopedApiUrl('/api/export-json', null, { includeToken: true });
6037
7737
  a.download = 'conversation-' + new Date().toISOString().slice(0, 10) + '-full.json';
6038
7738
  a.click();
6039
7739
  }
6040
7740
 
6041
7741
  // ==================== VIEW SWITCHING ====================
6042
7742
 
6043
- var activeView = 'office';
7743
+ var activeView = dashboardWorkspaceState.liveWorkspace.snapshot.view || 'office';
6044
7744
 
6045
- function switchView(view) {
7745
+ function switchView(view, options) {
7746
+ var opts = options || {};
6046
7747
  activeView = view;
6047
7748
  document.getElementById('tab-messages').classList.toggle('active', view === 'messages');
6048
7749
  document.getElementById('tab-tasks').classList.toggle('active', view === 'tasks');
6049
7750
  document.getElementById('tab-workspaces').classList.toggle('active', view === 'workspaces');
6050
7751
  document.getElementById('tab-workflows').classList.toggle('active', view === 'workflows');
7752
+ document.getElementById('tab-graph').classList.toggle('active', view === 'graph');
6051
7753
  document.getElementById('tab-plan').classList.toggle('active', view === 'plan');
6052
7754
  document.getElementById('tab-office').classList.toggle('active', view === 'office');
6053
7755
  document.getElementById('tab-launch').classList.toggle('active', view === 'launch');
6054
7756
  document.getElementById('tab-rules').classList.toggle('active', view === 'rules');
6055
7757
  document.getElementById('tab-stats').classList.toggle('active', view === 'stats');
7758
+ document.getElementById('tab-services').classList.toggle('active', view === 'services');
6056
7759
  document.getElementById('tab-docs').classList.toggle('active', view === 'docs');
6057
7760
  document.getElementById('messages').style.display = view === 'messages' ? 'flex' : 'none';
6058
7761
  document.getElementById('tasks-area').classList.toggle('visible', view === 'tasks');
6059
7762
  document.getElementById('workspaces-area').classList.toggle('visible', view === 'workspaces');
6060
7763
  document.getElementById('workflows-area').classList.toggle('visible', view === 'workflows');
7764
+ document.getElementById('graph-area').classList.toggle('visible', view === 'graph');
6061
7765
  document.getElementById('plan-area').classList.toggle('visible', view === 'plan');
6062
7766
  document.getElementById('monitor-panel').style.display = view === 'plan' ? 'block' : 'none';
6063
7767
  document.getElementById('office-area').classList.toggle('visible', view === 'office');
6064
7768
  document.getElementById('launch-area').classList.toggle('visible', view === 'launch');
6065
7769
  document.getElementById('rules-area').classList.toggle('visible', view === 'rules');
6066
7770
  document.getElementById('stats-area').classList.toggle('visible', view === 'stats');
7771
+ document.getElementById('services-area').classList.toggle('visible', view === 'services');
6067
7772
  document.getElementById('docs-area').classList.toggle('visible', view === 'docs');
6068
- document.getElementById('search-bar').style.display = view === 'messages' ? 'flex' : 'none';
7773
+ syncSearchBarVisibility();
6069
7774
  document.getElementById('channel-filter-bar').style.display = view === 'messages' && activeChannel !== undefined ? '' : 'none';
6070
7775
  if (view === 'messages') renderChannelBar(cachedHistory);
6071
7776
  if (view === 'tasks') fetchTasks();
6072
7777
  if (view === 'workspaces') fetchWorkspaces();
6073
7778
  if (view === 'workflows') fetchWorkflows();
7779
+ if (view === 'graph') { renderGraphView(); ensureGraphWorkflowData(); }
6074
7780
  if (view === 'plan') { fetchPlanStatus(); fetchMonitorHealth(); }
7781
+ if (view === 'services') renderServices();
6075
7782
  if (view === 'docs') renderDocs();
6076
7783
  if (view === 'office') {
6077
7784
  if (window.office3dStart) {
@@ -6103,6 +7810,7 @@ function switchView(view) {
6103
7810
  }
6104
7811
  // Auto-close sidebar on mobile after view switch
6105
7812
  if (isMobile) closeSidebar();
7813
+ if (!opts.skipPersist) persistLiveDashboardLayout();
6106
7814
  }
6107
7815
 
6108
7816
  // ==================== TASKS ====================
@@ -6110,9 +7818,13 @@ function switchView(view) {
6110
7818
  var cachedTasks = [];
6111
7819
 
6112
7820
  function fetchTasks() {
6113
- var pq = projectParam();
6114
7821
  document.getElementById('tasks-area').innerHTML = '<div class="loading-spinner">Loading tasks...</div>';
6115
- lttFetch('/api/tasks' + pq).then(function(r) { return r.json(); }).then(function(tasks) {
7822
+ lttFetch(scopedApiUrl('/api/tasks')).then(function(r) { return r.json(); }).then(function(tasks) {
7823
+ if (tasks && tasks.error) {
7824
+ cachedTasks = [];
7825
+ document.getElementById('tasks-area').innerHTML = '<div class="tasks-empty">' + escapeHtml(tasks.error) + '</div>';
7826
+ return;
7827
+ }
6116
7828
  cachedTasks = Array.isArray(tasks) ? tasks : [];
6117
7829
  renderTasks();
6118
7830
  }).catch(function() {
@@ -6123,8 +7835,15 @@ function fetchTasks() {
6123
7835
 
6124
7836
  function renderTasks() {
6125
7837
  var el = document.getElementById('tasks-area');
7838
+ var toolbar = '<div class="tasks-toolbar" style="display:flex;justify-content:flex-end;gap:8px;padding:8px 4px;align-items:center">' +
7839
+ '<span style="font-size:11px;color:var(--text-muted);margin-right:auto">' + cachedTasks.length + ' task' + (cachedTasks.length === 1 ? '' : 's') + '</span>' +
7840
+ '<button onclick="clearTasks()" title="Clear all tasks for this branch" ' +
7841
+ 'style="background:#4a2020;border:1px solid #6a3030;border-radius:6px;padding:4px 10px;font-size:11px;cursor:pointer;color:#ff6b6b;white-space:nowrap;transition:all 0.2s"' +
7842
+ (cachedTasks.length ? '' : ' disabled') + '>Clear All Tasks</button>' +
7843
+ '</div>';
7844
+
6126
7845
  if (!cachedTasks.length) {
6127
- el.innerHTML = '<div class="kanban">' +
7846
+ el.innerHTML = toolbar + '<div class="kanban">' +
6128
7847
  '<div class="kanban-col">' +
6129
7848
  '<div class="kanban-title pending">Pending<span class="kanban-count">0</span></div>' +
6130
7849
  '<div class="task-card" style="opacity:0.3;border-style:dashed">' +
@@ -6162,7 +7881,7 @@ function renderTasks() {
6162
7881
  { key: 'blocked', label: 'Blocked' }
6163
7882
  ];
6164
7883
 
6165
- var html = '<div class="kanban">';
7884
+ var html = toolbar + '<div class="kanban">';
6166
7885
  for (var c = 0; c < cols.length; c++) {
6167
7886
  var col = cols[c];
6168
7887
  var tasks = groups[col.key] || [];
@@ -6177,6 +7896,22 @@ function renderTasks() {
6177
7896
  el.innerHTML = html;
6178
7897
  }
6179
7898
 
7899
+ function clearTasks() {
7900
+ if (!cachedTasks.length) return;
7901
+ if (!confirm('Clear all ' + cachedTasks.length + ' task' + (cachedTasks.length === 1 ? '' : 's') + ' on this branch? This cannot be undone.')) return;
7902
+ lttFetch(scopedApiUrl('/api/clear-tasks'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) })
7903
+ .then(function(r) { return r.json(); })
7904
+ .then(function(data) {
7905
+ if (data && data.error) {
7906
+ showToast('Clear tasks failed: ' + data.error);
7907
+ return;
7908
+ }
7909
+ showToast('Cleared ' + (data.cleared_tasks || 0) + ' task' + ((data.cleared_tasks || 0) === 1 ? '' : 's'));
7910
+ fetchTasks();
7911
+ })
7912
+ .catch(function(e) { showToast('Clear tasks failed: ' + (e && e.message ? e.message : e)); });
7913
+ }
7914
+
6180
7915
  function buildTaskCard(t) {
6181
7916
  var assigneeHtml = '';
6182
7917
  if (t.assignee) {
@@ -6216,11 +7951,16 @@ function buildTaskCard(t) {
6216
7951
  }
6217
7952
 
6218
7953
  function updateTaskStatus(taskId, newStatus) {
6219
- lttFetch('/api/tasks' + projectParam(), {
7954
+ lttFetch(scopedApiUrl('/api/tasks'), {
6220
7955
  method: 'POST',
6221
7956
  headers: { 'Content-Type': 'application/json' },
6222
7957
  body: JSON.stringify({ task_id: taskId, status: newStatus })
6223
- }).then(function(r) { return r.json(); }).then(function() {
7958
+ }).then(function(r) { return r.json(); }).then(function(data) {
7959
+ if (data && data.error) {
7960
+ showToast('Task update failed: ' + data.error);
7961
+ fetchTasks();
7962
+ return;
7963
+ }
6224
7964
  fetchTasks();
6225
7965
  }).catch(function(e) { console.error('Task update failed:', e); });
6226
7966
  }
@@ -6576,8 +8316,7 @@ document.addEventListener('click', function(e) {
6576
8316
 
6577
8317
  function exportShareableHTML() {
6578
8318
  if (!cachedHistory.length) return;
6579
- var pp = projectParam();
6580
- window.open('/api/export' + pp, '_blank');
8319
+ window.open(scopedApiUrl('/api/export', null, { includeToken: true }), '_blank');
6581
8320
  }
6582
8321
 
6583
8322
  // ==================== AUTO-GROW TEXTAREA ====================
@@ -6826,6 +8565,7 @@ function openProfileEditor() {
6826
8565
  if (!editingAgent || !agent) return;
6827
8566
 
6828
8567
  document.getElementById('profile-popup').classList.remove('open');
8568
+ if (isAgentMetadataDrawerOpen()) closeAgentMetadataDrawer();
6829
8569
 
6830
8570
  // Populate basic fields
6831
8571
  document.getElementById('pe-display-name').value = agent.display_name || editingAgent;
@@ -6972,16 +8712,238 @@ function saveProfile() {
6972
8712
  };
6973
8713
  if (avatar) body.avatar = avatar;
6974
8714
 
6975
- lttFetch('/api/profiles' + projectParam(), {
6976
- method: 'POST',
6977
- headers: { 'Content-Type': 'application/json' },
6978
- body: JSON.stringify(body)
6979
- }).then(function(r) { return r.json(); }).then(function(res) {
6980
- if (res.success) {
6981
- closeProfileEditor();
6982
- poll();
6983
- }
6984
- }).catch(function(e) { console.error('Save profile failed:', e); });
8715
+ lttFetch('/api/profiles' + projectParam(), {
8716
+ method: 'POST',
8717
+ headers: { 'Content-Type': 'application/json' },
8718
+ body: JSON.stringify(body)
8719
+ }).then(function(r) { return r.json(); }).then(function(res) {
8720
+ if (res.success) {
8721
+ closeProfileEditor();
8722
+ poll();
8723
+ }
8724
+ }).catch(function(e) { console.error('Save profile failed:', e); });
8725
+ }
8726
+
8727
+ // ==================== AGENT METADATA DRAWER ====================
8728
+
8729
+ var activeAgentDrawer = '';
8730
+
8731
+ function isAgentMetadataDrawerOpen() {
8732
+ var drawer = document.getElementById('agent-drawer');
8733
+ return !!(drawer && drawer.classList.contains('open'));
8734
+ }
8735
+
8736
+ function formatAgentDrawerValue(value) {
8737
+ if (value === null || value === undefined || value === '') return '<span class="agent-detail-empty">—</span>';
8738
+ return escapeHtml(String(value));
8739
+ }
8740
+
8741
+ function formatAgentDrawerTimestamp(timestamp) {
8742
+ if (!timestamp) return '<span class="agent-detail-empty">—</span>';
8743
+ return escapeHtml(timeAgo(timestamp) + ' • ' + new Date(timestamp).toLocaleString());
8744
+ }
8745
+
8746
+ function formatAgentDrawerIdle(seconds) {
8747
+ if (seconds === null || seconds === undefined || seconds === '') return '<span class="agent-detail-empty">—</span>';
8748
+ var total = Math.max(0, parseInt(seconds, 10) || 0);
8749
+ if (total < 60) return escapeHtml(total + 's');
8750
+ var mins = Math.floor(total / 60);
8751
+ var secs = total % 60;
8752
+ if (mins < 60) return escapeHtml(mins + 'm ' + secs + 's');
8753
+ var hours = Math.floor(mins / 60);
8754
+ return escapeHtml(hours + 'h ' + (mins % 60) + 'm');
8755
+ }
8756
+
8757
+ function buildAgentDetailCard(label, valueHtml, muted) {
8758
+ return '<div class="agent-detail-card">' +
8759
+ '<div class="agent-detail-label">' + escapeHtml(label) + '</div>' +
8760
+ '<div class="agent-detail-value' + (muted ? ' muted' : '') + '">' + valueHtml + '</div>' +
8761
+ '</div>';
8762
+ }
8763
+
8764
+ function buildAgentMetadataSummary(agentName) {
8765
+ var summary = {
8766
+ sent: 0,
8767
+ received: 0,
8768
+ handoffsOut: 0,
8769
+ handoffsIn: 0,
8770
+ threads: {},
8771
+ channels: {},
8772
+ lastSent: null,
8773
+ lastReceived: null,
8774
+ };
8775
+
8776
+ for (var i = 0; i < cachedHistory.length; i++) {
8777
+ var message = cachedHistory[i];
8778
+ if (message.from === agentName) {
8779
+ summary.sent++;
8780
+ if (message.type === 'handoff') summary.handoffsOut++;
8781
+ if (message.thread_id) summary.threads[message.thread_id] = true;
8782
+ summary.channels[message.channel || 'general'] = true;
8783
+ summary.lastSent = message;
8784
+ }
8785
+ if (message.to === agentName) {
8786
+ summary.received++;
8787
+ if (message.type === 'handoff') summary.handoffsIn++;
8788
+ if (message.thread_id) summary.threads[message.thread_id] = true;
8789
+ summary.channels[message.channel || 'general'] = true;
8790
+ summary.lastReceived = message;
8791
+ }
8792
+ }
8793
+
8794
+ return summary;
8795
+ }
8796
+
8797
+ function openAgentMetadataDrawerFromPopup() {
8798
+ if (!editingAgent) return;
8799
+ openAgentMetadataDrawer(editingAgent);
8800
+ }
8801
+
8802
+ function focusInjectForAgent(agentName) {
8803
+ closeAgentMetadataDrawer();
8804
+ switchView('messages');
8805
+ var target = document.getElementById('inject-target');
8806
+ var content = document.getElementById('inject-content');
8807
+ if (target) target.value = agentName;
8808
+ updateSendBtn();
8809
+ if (content) content.focus();
8810
+ }
8811
+
8812
+ function renderAgentMetadataDrawer() {
8813
+ var agent = cachedAgents[activeAgentDrawer];
8814
+ var body = document.getElementById('agent-drawer-body');
8815
+ var title = document.getElementById('agent-drawer-title');
8816
+ var subtitle = document.getElementById('agent-drawer-subtitle');
8817
+ var avatar = document.getElementById('agent-drawer-avatar');
8818
+ var avatarImg = document.getElementById('agent-drawer-avatar-img');
8819
+ if (!body || !title || !subtitle || !avatar || !avatarImg) return;
8820
+
8821
+ if (!activeAgentDrawer || !agent) {
8822
+ title.textContent = 'Agent details unavailable';
8823
+ subtitle.textContent = '';
8824
+ avatar.style.display = 'flex';
8825
+ avatarImg.style.display = 'none';
8826
+ avatar.textContent = '?';
8827
+ avatar.style.background = 'var(--surface-3)';
8828
+ body.innerHTML = '<div class="agent-detail-note"><div class="agent-detail-note-label">Unavailable</div><div class="agent-detail-note-value">This agent is not currently present in the cached dashboard data.</div></div>';
8829
+ return;
8830
+ }
8831
+
8832
+ var color = getColor(activeAgentDrawer);
8833
+ var displayName = agent.display_name || activeAgentDrawer;
8834
+ var state = agent.status || (agent.alive ? 'active' : 'dead');
8835
+ var stateLabel = state.charAt(0).toUpperCase() + state.slice(1);
8836
+ var listeningLabel = state === 'dead' ? 'Offline' : (agent.is_listening ? 'Listening' : 'Busy');
8837
+ var hasContract = !!(agent.has_explicit_contract || agent.archetype || (agent.skills && agent.skills.length) || (agent.contract && agent.contract.archetype) || (agent.contract_mode && agent.contract_mode !== 'advisory'));
8838
+ var summary = buildAgentMetadataSummary(activeAgentDrawer);
8839
+ var channelNames = Object.keys(summary.channels).sort();
8840
+ var channelSummary = channelNames.length ? channelNames.join(', ') : '—';
8841
+
8842
+ title.textContent = displayName;
8843
+ title.style.color = color;
8844
+ subtitle.textContent = '@' + activeAgentDrawer + ' • ' + (agent.provider || 'unknown') + ' • branch ' + (agent.branch || 'main');
8845
+
8846
+ if (agent.avatar && (agent.avatar.startsWith('data:image/') || agent.avatar.startsWith('/'))) {
8847
+ avatarImg.setAttribute('src', agent.avatar);
8848
+ avatarImg.style.display = '';
8849
+ avatar.style.display = 'none';
8850
+ } else {
8851
+ avatarImg.style.display = 'none';
8852
+ avatar.style.display = 'flex';
8853
+ avatar.textContent = initial(activeAgentDrawer);
8854
+ avatar.style.background = color;
8855
+ }
8856
+
8857
+ var html = '';
8858
+ html += '<div class="agent-drawer-status-row">';
8859
+ html += '<span class="agent-detail-pill"><strong>' + escapeHtml(stateLabel) + '</strong></span>';
8860
+ html += '<span class="agent-detail-pill">Listening <strong>' + escapeHtml(listeningLabel) + '</strong></span>';
8861
+ if (agent.role) html += '<span class="agent-detail-pill">Role <strong>' + escapeHtml(agent.role) + '</strong></span>';
8862
+ if (hasContract && agent.contract && agent.contract.archetype) html += '<span class="agent-detail-pill">Archetype <strong>' + escapeHtml(agent.contract.archetype) + '</strong></span>';
8863
+ if (hasContract) html += '<span class="agent-detail-pill">Contract <strong>' + escapeHtml(agent.contract_mode || 'advisory') + '</strong></span>';
8864
+ html += '<span class="agent-detail-pill">Branch <strong>' + escapeHtml(agent.branch || 'main') + '</strong></span>';
8865
+ html += '</div>';
8866
+
8867
+ if (agent.current_status) {
8868
+ html += '<div class="agent-detail-note">' +
8869
+ '<div class="agent-detail-note-label">Current status</div>' +
8870
+ '<div class="agent-detail-note-value">' + escapeHtml(agent.current_status) + '</div>' +
8871
+ '</div>';
8872
+ }
8873
+
8874
+ if (agent.bio) {
8875
+ html += '<div class="agent-detail-note">' +
8876
+ '<div class="agent-detail-note-label">Bio</div>' +
8877
+ '<div class="agent-detail-note-value">' + escapeHtml(agent.bio) + '</div>' +
8878
+ '</div>';
8879
+ }
8880
+
8881
+ html += '<div class="agent-drawer-section">';
8882
+ html += '<div class="agent-drawer-section-title">Runtime</div>';
8883
+ html += '<div class="agent-detail-grid">';
8884
+ html += buildAgentDetailCard('Provider', formatAgentDrawerValue(agent.provider || 'unknown'));
8885
+ html += buildAgentDetailCard('PID', formatAgentDrawerValue(agent.pid));
8886
+ html += buildAgentDetailCard('Status', formatAgentDrawerValue(stateLabel));
8887
+ html += buildAgentDetailCard('Idle', formatAgentDrawerIdle(agent.idle_seconds));
8888
+ html += buildAgentDetailCard('Registered', formatAgentDrawerTimestamp(agent.registered_at), true);
8889
+ html += buildAgentDetailCard('Last heartbeat', formatAgentDrawerTimestamp(agent.last_activity), true);
8890
+ html += buildAgentDetailCard('Last message', formatAgentDrawerTimestamp(agent.last_message), true);
8891
+ html += buildAgentDetailCard('Role', formatAgentDrawerValue(agent.role), true);
8892
+ if (hasContract) html += buildAgentDetailCard('Archetype', formatAgentDrawerValue(agent.archetype || (agent.contract && agent.contract.archetype)), true);
8893
+ if (hasContract) html += buildAgentDetailCard('Contract mode', formatAgentDrawerValue(agent.contract_mode || 'advisory'), true);
8894
+ if (hasContract) html += buildAgentDetailCard('Skills', formatAgentDrawerValue((agent.skills || []).join(', ')), true);
8895
+ if (hasContract) html += buildAgentDetailCard('Effective skills', formatAgentDrawerValue(agent.contract && agent.contract.effective_skills ? agent.contract.effective_skills.join(', ') : ''), true);
8896
+ if (hasContract) html += buildAgentDetailCard('Role alignment', formatAgentDrawerValue(agent.contract && agent.contract.role_alignment ? String(agent.contract.role_alignment).replace(/_/g, ' ') : ''), true);
8897
+ html += '</div>';
8898
+ html += '</div>';
8899
+
8900
+ html += '<div class="agent-drawer-section">';
8901
+ html += '<div class="agent-drawer-section-title">Conversation</div>';
8902
+ html += '<div class="agent-detail-grid">';
8903
+ html += buildAgentDetailCard('Sent', formatAgentDrawerValue(summary.sent));
8904
+ html += buildAgentDetailCard('Received', formatAgentDrawerValue(summary.received));
8905
+ html += buildAgentDetailCard('Handoffs out', formatAgentDrawerValue(summary.handoffsOut));
8906
+ html += buildAgentDetailCard('Handoffs in', formatAgentDrawerValue(summary.handoffsIn));
8907
+ html += buildAgentDetailCard('Threads', formatAgentDrawerValue(Object.keys(summary.threads).length));
8908
+ html += buildAgentDetailCard('Channels', formatAgentDrawerValue(channelSummary), true);
8909
+ html += buildAgentDetailCard('Last sent', formatAgentDrawerTimestamp(summary.lastSent && summary.lastSent.timestamp), true);
8910
+ html += buildAgentDetailCard('Last received', formatAgentDrawerTimestamp(summary.lastReceived && summary.lastReceived.timestamp), true);
8911
+ html += '</div>';
8912
+ html += '</div>';
8913
+
8914
+ html += '<div class="agent-drawer-actions">';
8915
+ html += '<button class="btn btn-primary" type="button" onclick="focusInjectForAgent(\'' + escapeHtml(activeAgentDrawer) + '\')">Message Agent</button>';
8916
+ html += '<button class="btn" type="button" onclick="openProfileEditor()">Edit Profile</button>';
8917
+ html += '</div>';
8918
+
8919
+ body.innerHTML = html;
8920
+ }
8921
+
8922
+ function openAgentMetadataDrawer(agentName) {
8923
+ if (!agentName) return;
8924
+ editingAgent = agentName;
8925
+ activeAgentDrawer = agentName;
8926
+ document.getElementById('profile-popup').classList.remove('open');
8927
+ renderAgentMetadataDrawer();
8928
+ document.getElementById('agent-drawer-backdrop').classList.add('open');
8929
+ document.getElementById('agent-drawer').classList.add('open');
8930
+ document.getElementById('agent-drawer').setAttribute('aria-hidden', 'false');
8931
+ }
8932
+
8933
+ function closeAgentMetadataDrawer() {
8934
+ activeAgentDrawer = '';
8935
+ document.getElementById('agent-drawer-backdrop').classList.remove('open');
8936
+ document.getElementById('agent-drawer').classList.remove('open');
8937
+ document.getElementById('agent-drawer').setAttribute('aria-hidden', 'true');
8938
+ }
8939
+
8940
+ function refreshAgentMetadataDrawer() {
8941
+ if (!isAgentMetadataDrawerOpen()) return;
8942
+ if (!activeAgentDrawer || !cachedAgents[activeAgentDrawer]) {
8943
+ closeAgentMetadataDrawer();
8944
+ return;
8945
+ }
8946
+ renderAgentMetadataDrawer();
6985
8947
  }
6986
8948
 
6987
8949
  // ==================== v3.0: WORKSPACES ====================
@@ -7034,15 +8996,26 @@ function renderWorkspaces(data) {
7034
8996
  // ==================== v3.0: WORKFLOWS ====================
7035
8997
 
7036
8998
  function fetchWorkflows() {
7037
- var pq = projectParam();
8999
+ if (!isMainBranchSelected()) {
9000
+ renderMainBranchOnlyView('workflows-area', 'Workflows');
9001
+ return;
9002
+ }
7038
9003
  document.getElementById('workflows-area').innerHTML = '<div class="loading-spinner">Loading workflows...</div>';
7039
- lttFetch('/api/workflows' + pq).then(function(r) { return r.json(); }).then(function(data) {
9004
+ lttFetch(scopedApiUrl('/api/workflows')).then(function(r) { return r.json(); }).then(function(data) {
9005
+ if (data && data.code === 'main_branch_only') {
9006
+ renderMainBranchOnlyView('workflows-area', 'Workflows');
9007
+ return;
9008
+ }
7040
9009
  renderWorkflows(Array.isArray(data) ? data : []);
7041
9010
  }).catch(function() {});
7042
9011
  }
7043
9012
 
7044
9013
  function renderWorkflows(workflows) {
7045
9014
  var el = document.getElementById('workflows-area');
9015
+ if (!isMainBranchSelected()) {
9016
+ el.innerHTML = mainBranchOnlyViewHtml('Workflows');
9017
+ return;
9018
+ }
7046
9019
  if (!workflows.length) {
7047
9020
  el.innerHTML = '<div class="tasks-empty">No workflows yet. Agents create workflows with <code style="background:var(--surface-2);padding:2px 6px;border-radius:4px;font-size:12px;color:var(--orange)">create_workflow(name, steps)</code></div>';
7048
9021
  return;
@@ -7106,24 +9079,687 @@ function renderWorkflows(workflows) {
7106
9079
  }
7107
9080
 
7108
9081
  function dashAdvanceWorkflow(wfId) {
7109
- lttFetch('/api/workflows' + projectParam(), {
9082
+ if (!isMainBranchSelected()) {
9083
+ showToast('Workflows only support the main branch right now.');
9084
+ return;
9085
+ }
9086
+ lttFetch(scopedApiUrl('/api/workflows'), {
7110
9087
  method: 'POST',
7111
9088
  headers: { 'Content-Type': 'application/json' },
7112
9089
  body: JSON.stringify({ action: 'advance', workflow_id: wfId })
7113
9090
  }).then(function() { fetchWorkflows(); }).catch(function() {});
7114
9091
  }
7115
9092
 
9093
+ // ==================== TASK 8C: GRAPH VIEW ====================
9094
+
9095
+ var graphWorkflowState = {
9096
+ cacheKey: '',
9097
+ status: 'idle',
9098
+ data: [],
9099
+ error: '',
9100
+ lastFetchedAt: 0,
9101
+ };
9102
+
9103
+ var GRAPH_CHANNEL_PALETTE = [
9104
+ { accent: 'var(--accent)', dim: 'var(--accent-dim)' },
9105
+ { accent: 'var(--purple)', dim: 'var(--purple-dim)' },
9106
+ { accent: 'var(--green)', dim: 'var(--green-dim)' },
9107
+ { accent: 'var(--orange)', dim: 'var(--orange-dim)' },
9108
+ { accent: 'var(--red)', dim: 'var(--red-dim)' },
9109
+ ];
9110
+
9111
+ function getGraphWorkflowCacheKey() {
9112
+ return (activeProject || '__default__') + '::main';
9113
+ }
9114
+
9115
+ function shouldRefreshGraphWorkflowData() {
9116
+ if (graphWorkflowState.cacheKey !== getGraphWorkflowCacheKey()) return true;
9117
+ if (graphWorkflowState.status === 'idle') return true;
9118
+ return (Date.now() - graphWorkflowState.lastFetchedAt) > 15000;
9119
+ }
9120
+
9121
+ function ensureGraphWorkflowData(options) {
9122
+ var opts = options || {};
9123
+ if (!opts.force && !shouldRefreshGraphWorkflowData()) return;
9124
+ if (graphWorkflowState.status === 'loading') return;
9125
+ fetchGraphWorkflowData();
9126
+ }
9127
+
9128
+ function normalizeGraphWorkflowList(rawWorkflows) {
9129
+ if (!Array.isArray(rawWorkflows)) return [];
9130
+ var workflows = [];
9131
+ for (var i = 0; i < rawWorkflows.length; i++) {
9132
+ var workflow = rawWorkflows[i];
9133
+ if (!workflow || typeof workflow !== 'object' || !Array.isArray(workflow.steps)) continue;
9134
+ var steps = [];
9135
+ for (var j = 0; j < workflow.steps.length; j++) {
9136
+ var step = workflow.steps[j];
9137
+ if (!step || typeof step !== 'object') continue;
9138
+ steps.push({
9139
+ id: step.id,
9140
+ description: cleanString(step.description, 'Step ' + (j + 1)),
9141
+ status: cleanString(step.status, 'pending') || 'pending',
9142
+ assignee: cleanString(step.assignee, ''),
9143
+ });
9144
+ }
9145
+ workflows.push({
9146
+ id: cleanString(workflow.id, 'workflow-' + (i + 1)),
9147
+ name: cleanString(workflow.name, 'Workflow ' + (i + 1)),
9148
+ status: cleanString(workflow.status, 'active') || 'active',
9149
+ steps: steps,
9150
+ autonomous: !!workflow.autonomous,
9151
+ parallel: !!workflow.parallel,
9152
+ });
9153
+ }
9154
+ return workflows;
9155
+ }
9156
+
9157
+ function fetchGraphWorkflowData() {
9158
+ var requestCacheKey = getGraphWorkflowCacheKey();
9159
+ var requestUrl = scopedApiUrl('/api/workflows', null, { branch: 'main' });
9160
+ graphWorkflowState.status = 'loading';
9161
+ graphWorkflowState.cacheKey = requestCacheKey;
9162
+ graphWorkflowState.error = '';
9163
+ graphWorkflowState.lastFetchedAt = Date.now();
9164
+ if (activeView === 'graph') renderGraphView();
9165
+ lttFetch(requestUrl).then(function(r) { return r.json(); }).then(function(data) {
9166
+ if (requestCacheKey !== getGraphWorkflowCacheKey()) {
9167
+ graphWorkflowState.status = 'idle';
9168
+ if (activeView === 'graph') ensureGraphWorkflowData({ force: true });
9169
+ return;
9170
+ }
9171
+ graphWorkflowState.cacheKey = requestCacheKey;
9172
+ graphWorkflowState.status = 'ready';
9173
+ graphWorkflowState.data = normalizeGraphWorkflowList(Array.isArray(data) ? data : []);
9174
+ graphWorkflowState.error = '';
9175
+ graphWorkflowState.lastFetchedAt = Date.now();
9176
+ if (activeView === 'graph') renderGraphView();
9177
+ }).catch(function(error) {
9178
+ if (requestCacheKey !== getGraphWorkflowCacheKey()) {
9179
+ graphWorkflowState.status = 'idle';
9180
+ if (activeView === 'graph') ensureGraphWorkflowData({ force: true });
9181
+ return;
9182
+ }
9183
+ graphWorkflowState.cacheKey = requestCacheKey;
9184
+ graphWorkflowState.status = 'error';
9185
+ graphWorkflowState.data = [];
9186
+ graphWorkflowState.error = error && error.message ? error.message : 'Workflow data unavailable';
9187
+ graphWorkflowState.lastFetchedAt = Date.now();
9188
+ if (activeView === 'graph') renderGraphView();
9189
+ });
9190
+ }
9191
+
9192
+ function normalizeGraphEndpoint(name) {
9193
+ var raw = cleanString(name, '').trim();
9194
+ if (!raw) return null;
9195
+ var lower = raw.toLowerCase();
9196
+ if (lower === '__all__' || lower === '__group__' || lower === 'all' || lower === '*' || lower === 'broadcast') {
9197
+ return { key: '__broadcast__', label: 'Broadcast', synthetic: true };
9198
+ }
9199
+ if (lower === 'system' || lower === 'dashboard' || lower === 'server') {
9200
+ return { key: '__system__', label: raw.charAt(0).toUpperCase() + raw.slice(1), synthetic: true };
9201
+ }
9202
+ return { key: raw, label: raw, synthetic: false };
9203
+ }
9204
+
9205
+ function ensureGraphNode(nodeMap, endpoint, agentsSource, branchName) {
9206
+ if (!endpoint) return null;
9207
+ if (!nodeMap[endpoint.key]) {
9208
+ var agent = !endpoint.synthetic && agentsSource && agentsSource[endpoint.key] ? agentsSource[endpoint.key] : null;
9209
+ var agentStatus = agent && cleanString(agent.status, '') ? cleanString(agent.status, '') : (agent && agent.alive ? 'active' : 'unknown');
9210
+ nodeMap[endpoint.key] = {
9211
+ key: endpoint.key,
9212
+ label: agent && cleanString(agent.display_name, '').trim() ? cleanString(agent.display_name, '').trim() : endpoint.label,
9213
+ agentName: endpoint.synthetic ? '' : endpoint.key,
9214
+ synthetic: !!endpoint.synthetic,
9215
+ color: endpoint.synthetic ? 'var(--surface-3)' : getColor(endpoint.key),
9216
+ status: endpoint.synthetic ? 'synthetic' : (agentStatus || 'unknown'),
9217
+ provider: agent ? cleanString(agent.provider, '') : '',
9218
+ role: agent ? cleanString(agent.role, '') : '',
9219
+ branch: agent ? (cleanString(agent.branch, 'main') || 'main') : branchName,
9220
+ sendCount: 0,
9221
+ receiveCount: 0,
9222
+ channelCounts: {},
9223
+ neighbors: {},
9224
+ x: 0,
9225
+ y: 0,
9226
+ laneKey: '',
9227
+ dominantChannel: '',
9228
+ };
9229
+ }
9230
+ return nodeMap[endpoint.key];
9231
+ }
9232
+
9233
+ function getGraphDominantChannel(channelCounts) {
9234
+ var keys = Object.keys(channelCounts || {});
9235
+ if (!keys.length) return '';
9236
+ keys.sort(function(a, b) {
9237
+ return (channelCounts[b] || 0) - (channelCounts[a] || 0) || a.localeCompare(b);
9238
+ });
9239
+ return keys[0] || '';
9240
+ }
9241
+
9242
+ function getGraphLanePalette(index) {
9243
+ return GRAPH_CHANNEL_PALETTE[index % GRAPH_CHANNEL_PALETTE.length];
9244
+ }
9245
+
9246
+ function getGraphStatusColor(status) {
9247
+ if (status === 'active') return 'var(--green)';
9248
+ if (status === 'sleeping' || status === 'idle') return 'var(--orange)';
9249
+ if (status === 'dead') return 'var(--red)';
9250
+ if (status === 'synthetic') return 'var(--border-light)';
9251
+ return 'var(--text-muted)';
9252
+ }
9253
+
9254
+ function truncateGraphText(text, maxLength) {
9255
+ var value = cleanString(text, '');
9256
+ if (value.length <= maxLength) return value;
9257
+ return value.slice(0, Math.max(1, maxLength - 1)) + '…';
9258
+ }
9259
+
9260
+ function buildGraphModel() {
9261
+ var branchName = getActiveDashboardBranchName();
9262
+ var agentsSource = cachedAgents && typeof cachedAgents === 'object' ? cachedAgents : {};
9263
+ var historySource = Array.isArray(cachedHistory) ? cachedHistory : [];
9264
+ var nodeMap = {};
9265
+ var edgeMap = {};
9266
+ var channelMap = {};
9267
+ var agentNames = Object.keys(agentsSource || {});
9268
+
9269
+ for (var a = 0; a < agentNames.length; a++) {
9270
+ var agentName = agentNames[a];
9271
+ var agent = agentsSource[agentName];
9272
+ var agentBranch = agent ? (cleanString(agent.branch, 'main') || 'main') : 'main';
9273
+ if (agentBranch !== branchName) continue;
9274
+ ensureGraphNode(nodeMap, { key: agentName, label: agentName, synthetic: false }, agentsSource, branchName);
9275
+ }
9276
+
9277
+ for (var i = 0; i < historySource.length; i++) {
9278
+ var message = historySource[i];
9279
+ if (!message || typeof message !== 'object') continue;
9280
+ var fromEndpoint = normalizeGraphEndpoint(message.from);
9281
+ var toEndpoint = normalizeGraphEndpoint(message.to);
9282
+ if (!fromEndpoint || !toEndpoint) continue;
9283
+ var fromNode = ensureGraphNode(nodeMap, fromEndpoint, agentsSource, branchName);
9284
+ var toNode = ensureGraphNode(nodeMap, toEndpoint, agentsSource, branchName);
9285
+ if (!fromNode || !toNode) continue;
9286
+
9287
+ var channelName = cleanString(message.channel, '').trim() || 'general';
9288
+ if (!channelMap[channelName]) {
9289
+ channelMap[channelName] = {
9290
+ name: channelName,
9291
+ count: 0,
9292
+ participants: {},
9293
+ };
9294
+ }
9295
+ channelMap[channelName].count += 1;
9296
+ channelMap[channelName].participants[fromNode.key] = true;
9297
+ channelMap[channelName].participants[toNode.key] = true;
9298
+
9299
+ fromNode.sendCount += 1;
9300
+ toNode.receiveCount += 1;
9301
+ fromNode.channelCounts[channelName] = (fromNode.channelCounts[channelName] || 0) + 1;
9302
+ toNode.channelCounts[channelName] = (toNode.channelCounts[channelName] || 0) + 1;
9303
+ fromNode.neighbors[toNode.key] = true;
9304
+ toNode.neighbors[fromNode.key] = true;
9305
+
9306
+ if (fromNode.key === toNode.key) continue;
9307
+ var edgeKey = fromNode.key + '→' + toNode.key;
9308
+ if (!edgeMap[edgeKey]) {
9309
+ edgeMap[edgeKey] = {
9310
+ key: edgeKey,
9311
+ from: fromNode.key,
9312
+ to: toNode.key,
9313
+ count: 0,
9314
+ handoffs: 0,
9315
+ channelCounts: {},
9316
+ lastTimestamp: '',
9317
+ };
9318
+ }
9319
+ edgeMap[edgeKey].count += 1;
9320
+ if (message.type === 'handoff') edgeMap[edgeKey].handoffs += 1;
9321
+ edgeMap[edgeKey].channelCounts[channelName] = (edgeMap[edgeKey].channelCounts[channelName] || 0) + 1;
9322
+ if (message.timestamp) edgeMap[edgeKey].lastTimestamp = message.timestamp;
9323
+ }
9324
+
9325
+ var channels = [];
9326
+ var channelKeys = Object.keys(channelMap);
9327
+ for (var c = 0; c < channelKeys.length; c++) {
9328
+ var key = channelKeys[c];
9329
+ channels.push({
9330
+ name: key,
9331
+ count: channelMap[key].count,
9332
+ participantCount: Object.keys(channelMap[key].participants).length,
9333
+ });
9334
+ }
9335
+ channels.sort(function(a, b) {
9336
+ return b.count - a.count || a.name.localeCompare(b.name);
9337
+ });
9338
+
9339
+ var displayedChannels = channels.slice(0, 5);
9340
+ var displayedChannelNames = {};
9341
+ var lanes = [];
9342
+ var laneLookup = {};
9343
+
9344
+ for (var dc = 0; dc < displayedChannels.length; dc++) {
9345
+ var channel = displayedChannels[dc];
9346
+ var palette = getGraphLanePalette(dc);
9347
+ var laneKey = 'channel:' + channel.name;
9348
+ displayedChannelNames[channel.name] = true;
9349
+ laneLookup[laneKey] = {
9350
+ key: laneKey,
9351
+ label: '#' + channel.name,
9352
+ type: 'channel',
9353
+ accent: palette.accent,
9354
+ dim: palette.dim,
9355
+ count: channel.count,
9356
+ participantCount: channel.participantCount,
9357
+ nodes: [],
9358
+ y: 0,
9359
+ height: 0,
9360
+ cy: 0,
9361
+ };
9362
+ lanes.push(laneLookup[laneKey]);
9363
+ }
9364
+
9365
+ var hiddenChannelCount = 0;
9366
+ var hiddenChannelMessages = 0;
9367
+ for (var hc = 5; hc < channels.length; hc++) {
9368
+ hiddenChannelCount += 1;
9369
+ hiddenChannelMessages += channels[hc].count;
9370
+ }
9371
+ if (hiddenChannelCount > 0) {
9372
+ laneLookup.__other__ = {
9373
+ key: '__other__',
9374
+ label: 'Other channels',
9375
+ type: 'other',
9376
+ accent: 'var(--text-dim)',
9377
+ dim: 'var(--surface-2)',
9378
+ count: hiddenChannelMessages,
9379
+ participantCount: 0,
9380
+ nodes: [],
9381
+ y: 0,
9382
+ height: 0,
9383
+ cy: 0,
9384
+ };
9385
+ lanes.push(laneLookup.__other__);
9386
+ }
9387
+
9388
+ var nodes = [];
9389
+ var nodeKeys = Object.keys(nodeMap);
9390
+ var hasIdleNodes = false;
9391
+ var hasSyntheticNodes = false;
9392
+ for (var nk = 0; nk < nodeKeys.length; nk++) {
9393
+ var node = nodeMap[nodeKeys[nk]];
9394
+ node.dominantChannel = getGraphDominantChannel(node.channelCounts);
9395
+ if (node.synthetic) {
9396
+ node.laneKey = '__system__';
9397
+ hasSyntheticNodes = true;
9398
+ } else if (!node.dominantChannel) {
9399
+ node.laneKey = '__idle__';
9400
+ hasIdleNodes = true;
9401
+ } else if (displayedChannelNames[node.dominantChannel]) {
9402
+ node.laneKey = 'channel:' + node.dominantChannel;
9403
+ } else {
9404
+ node.laneKey = hiddenChannelCount > 0 ? '__other__' : 'channel:' + node.dominantChannel;
9405
+ if (!laneLookup[node.laneKey]) {
9406
+ laneLookup[node.laneKey] = {
9407
+ key: node.laneKey,
9408
+ label: '#' + node.dominantChannel,
9409
+ type: 'channel',
9410
+ accent: 'var(--accent)',
9411
+ dim: 'var(--accent-dim)',
9412
+ count: node.channelCounts[node.dominantChannel] || 0,
9413
+ participantCount: 1,
9414
+ nodes: [],
9415
+ y: 0,
9416
+ height: 0,
9417
+ cy: 0,
9418
+ };
9419
+ lanes.push(laneLookup[node.laneKey]);
9420
+ }
9421
+ }
9422
+ if (laneLookup[node.laneKey]) laneLookup[node.laneKey].nodes.push(node.key);
9423
+ nodes.push(node);
9424
+ }
9425
+
9426
+ if (hasIdleNodes) {
9427
+ laneLookup.__idle__ = {
9428
+ key: '__idle__',
9429
+ label: 'Idle agents',
9430
+ type: 'idle',
9431
+ accent: 'var(--text-muted)',
9432
+ dim: 'var(--surface-2)',
9433
+ count: 0,
9434
+ participantCount: laneLookup.__idle__ && laneLookup.__idle__.nodes ? laneLookup.__idle__.nodes.length : 0,
9435
+ nodes: laneLookup.__idle__ && laneLookup.__idle__.nodes ? laneLookup.__idle__.nodes : [],
9436
+ y: 0,
9437
+ height: 0,
9438
+ cy: 0,
9439
+ };
9440
+ if (!laneLookup.__idle__.nodes.length) {
9441
+ for (var idleIndex = 0; idleIndex < nodes.length; idleIndex++) {
9442
+ if (nodes[idleIndex].laneKey === '__idle__') laneLookup.__idle__.nodes.push(nodes[idleIndex].key);
9443
+ }
9444
+ }
9445
+ lanes.push(laneLookup.__idle__);
9446
+ }
9447
+
9448
+ if (hasSyntheticNodes) {
9449
+ laneLookup.__system__ = {
9450
+ key: '__system__',
9451
+ label: 'System endpoints',
9452
+ type: 'system',
9453
+ accent: 'var(--border-light)',
9454
+ dim: 'var(--surface-2)',
9455
+ count: 0,
9456
+ participantCount: laneLookup.__system__ && laneLookup.__system__.nodes ? laneLookup.__system__.nodes.length : 0,
9457
+ nodes: laneLookup.__system__ && laneLookup.__system__.nodes ? laneLookup.__system__.nodes : [],
9458
+ y: 0,
9459
+ height: 0,
9460
+ cy: 0,
9461
+ };
9462
+ if (!laneLookup.__system__.nodes.length) {
9463
+ for (var systemIndex = 0; systemIndex < nodes.length; systemIndex++) {
9464
+ if (nodes[systemIndex].laneKey === '__system__') laneLookup.__system__.nodes.push(nodes[systemIndex].key);
9465
+ }
9466
+ }
9467
+ lanes.push(laneLookup.__system__);
9468
+ }
9469
+
9470
+ var uniqueLaneMap = {};
9471
+ var orderedLanes = [];
9472
+ for (var l = 0; l < lanes.length; l++) {
9473
+ if (uniqueLaneMap[lanes[l].key]) continue;
9474
+ uniqueLaneMap[lanes[l].key] = true;
9475
+ orderedLanes.push(lanes[l]);
9476
+ }
9477
+ lanes = orderedLanes;
9478
+
9479
+ var networkPanel = { x: 24, y: 78, w: 808, h: 718 };
9480
+ var laneAreaTop = networkPanel.y + 56;
9481
+ var laneAreaBottom = networkPanel.y + networkPanel.h - 24;
9482
+ var laneAreaHeight = laneAreaBottom - laneAreaTop;
9483
+ var laneHeight = lanes.length ? Math.max(68, Math.floor(laneAreaHeight / lanes.length)) : 0;
9484
+ var nodeXStart = networkPanel.x + 172;
9485
+ var nodeXEnd = networkPanel.x + networkPanel.w - 70;
9486
+
9487
+ for (var laneIndex = 0; laneIndex < lanes.length; laneIndex++) {
9488
+ var lane = lanes[laneIndex];
9489
+ lane.y = laneAreaTop + (laneIndex * laneHeight) + 6;
9490
+ lane.height = Math.max(58, laneHeight - 12);
9491
+ lane.cy = lane.y + (lane.height / 2);
9492
+ lane.participantCount = lane.nodes.length;
9493
+ var laneNodes = [];
9494
+ for (var ln = 0; ln < lane.nodes.length; ln++) {
9495
+ if (nodeMap[lane.nodes[ln]]) laneNodes.push(nodeMap[lane.nodes[ln]]);
9496
+ }
9497
+ laneNodes.sort(function(a, b) {
9498
+ var aCount = a.sendCount + a.receiveCount;
9499
+ var bCount = b.sendCount + b.receiveCount;
9500
+ return bCount - aCount || a.label.localeCompare(b.label);
9501
+ });
9502
+ var useTwoRows = laneNodes.length > 4;
9503
+ var columns = Math.max(1, useTwoRows ? Math.ceil(laneNodes.length / 2) : laneNodes.length);
9504
+ var stepX = columns === 1 ? 0 : (nodeXEnd - nodeXStart) / (columns - 1);
9505
+ for (var nodeIndex = 0; nodeIndex < laneNodes.length; nodeIndex++) {
9506
+ var laneNode = laneNodes[nodeIndex];
9507
+ var row = useTwoRows ? (nodeIndex % 2) : 0;
9508
+ var column = useTwoRows ? Math.floor(nodeIndex / 2) : nodeIndex;
9509
+ laneNode.x = columns === 1 ? (nodeXStart + nodeXEnd) / 2 : (nodeXStart + (column * stepX));
9510
+ laneNode.y = lane.cy + (useTwoRows ? (row === 0 ? -24 : 24) : 0);
9511
+ }
9512
+ }
9513
+
9514
+ var edges = [];
9515
+ var edgeKeys = Object.keys(edgeMap);
9516
+ for (var ek = 0; ek < edgeKeys.length; ek++) {
9517
+ var edge = edgeMap[edgeKeys[ek]];
9518
+ edge.dominantChannel = getGraphDominantChannel(edge.channelCounts);
9519
+ if (edge.dominantChannel && displayedChannelNames[edge.dominantChannel]) edge.laneKey = 'channel:' + edge.dominantChannel;
9520
+ else if (edge.dominantChannel && hiddenChannelCount > 0) edge.laneKey = '__other__';
9521
+ else edge.laneKey = '';
9522
+ edges.push(edge);
9523
+ }
9524
+ edges.sort(function(a, b) {
9525
+ return b.count - a.count || b.handoffs - a.handoffs || String(b.lastTimestamp || '').localeCompare(String(a.lastTimestamp || ''));
9526
+ });
9527
+
9528
+ return {
9529
+ branchName: branchName,
9530
+ nodes: nodes,
9531
+ nodeMap: nodeMap,
9532
+ lanes: lanes,
9533
+ channels: channels,
9534
+ edges: edges,
9535
+ visibleEdges: edges.slice(0, 18),
9536
+ };
9537
+ }
9538
+
9539
+ function buildGraphWorkflowPanelSvg(workflowState) {
9540
+ var panelX = 852;
9541
+ var panelY = 78;
9542
+ var panelW = 324;
9543
+ var panelH = 718;
9544
+ var parts = [];
9545
+ parts.push('<rect x="' + panelX + '" y="' + panelY + '" width="' + panelW + '" height="' + panelH + '" rx="22" style="fill:var(--surface);stroke:var(--border-light);"></rect>');
9546
+ parts.push('<text x="' + (panelX + 18) + '" y="' + (panelY + 28) + '" style="fill:var(--text);font-size:16px;font-weight:700;">Main branch workflows</text>');
9547
+ parts.push('<text x="' + (panelX + 18) + '" y="' + (panelY + 48) + '" style="fill:var(--text-dim);font-size:11px;">Read-only structure from the existing workflow surface</text>');
9548
+
9549
+ if (workflowState.status === 'loading' && !workflowState.data.length) {
9550
+ parts.push('<rect x="' + (panelX + 18) + '" y="' + (panelY + 72) + '" width="' + (panelW - 36) + '" height="92" rx="16" style="fill:var(--surface-2);stroke:var(--border);"></rect>');
9551
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 112) + '" style="fill:var(--text);font-size:13px;font-weight:600;">Loading workflows…</text>');
9552
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 132) + '" style="fill:var(--text-dim);font-size:11px;">The graph will add the main pipeline rail when the data returns.</text>');
9553
+ return parts.join('');
9554
+ }
9555
+
9556
+ if (workflowState.status === 'error') {
9557
+ parts.push('<rect x="' + (panelX + 18) + '" y="' + (panelY + 72) + '" width="' + (panelW - 36) + '" height="112" rx="16" style="fill:var(--surface-2);stroke:var(--red);"></rect>');
9558
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 108) + '" style="fill:var(--text);font-size:13px;font-weight:600;">Workflow data unavailable</text>');
9559
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 128) + '" style="fill:var(--text-dim);font-size:11px;">' + escapeHtml(truncateGraphText(workflowState.error || 'The existing workflow view still works.', 42)) + '</text>');
9560
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 148) + '" style="fill:var(--text-dim);font-size:11px;">This optional graph surface degrades safely instead of breaking the dashboard.</text>');
9561
+ return parts.join('');
9562
+ }
9563
+
9564
+ var workflows = Array.isArray(workflowState.data) ? workflowState.data.slice(0, 4) : [];
9565
+ if (!workflows.length) {
9566
+ parts.push('<rect x="' + (panelX + 18) + '" y="' + (panelY + 72) + '" width="' + (panelW - 36) + '" height="112" rx="16" style="fill:var(--surface-2);stroke:var(--border);"></rect>');
9567
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 110) + '" style="fill:var(--text);font-size:13px;font-weight:600;">No main-branch workflows yet</text>');
9568
+ parts.push('<text x="' + (panelX + 34) + '" y="' + (panelY + 130) + '" style="fill:var(--text-dim);font-size:11px;">The graph still renders agents, channels, and message flow from the current branch.</text>');
9569
+ return parts.join('');
9570
+ }
9571
+
9572
+ var cardHeight = 152;
9573
+ var cardGap = 12;
9574
+ for (var i = 0; i < workflows.length; i++) {
9575
+ var workflow = workflows[i];
9576
+ var steps = Array.isArray(workflow.steps) ? workflow.steps : [];
9577
+ var doneCount = 0;
9578
+ var activeStep = null;
9579
+ for (var s = 0; s < steps.length; s++) {
9580
+ if (steps[s].status === 'done') doneCount += 1;
9581
+ if (!activeStep && steps[s].status === 'in_progress') activeStep = steps[s];
9582
+ }
9583
+ if (!activeStep && steps.length) activeStep = steps[0];
9584
+ var pct = steps.length ? Math.round((doneCount / steps.length) * 100) : 0;
9585
+ var cardX = panelX + 16;
9586
+ var cardY = panelY + 68 + (i * (cardHeight + cardGap));
9587
+ var cardW = panelW - 32;
9588
+ var statusColor = workflow.status === 'completed' ? 'var(--green)' : workflow.status === 'paused' ? 'var(--orange)' : 'var(--accent)';
9589
+ var statusLabel = truncateGraphText(String(workflow.status || 'active').replace(/_/g, ' '), 12).toUpperCase();
9590
+ var summaryLabel = activeStep ? truncateGraphText(activeStep.description || 'Waiting for next step', 42) : 'Waiting for steps';
9591
+ parts.push('<rect x="' + cardX + '" y="' + cardY + '" width="' + cardW + '" height="' + cardHeight + '" rx="18" style="fill:var(--surface-2);stroke:var(--border);"></rect>');
9592
+ parts.push('<text x="' + (cardX + 16) + '" y="' + (cardY + 24) + '" style="fill:var(--text);font-size:13px;font-weight:700;">' + escapeHtml(truncateGraphText(workflow.name, 28)) + '</text>');
9593
+ parts.push('<rect x="' + (cardX + cardW - 94) + '" y="' + (cardY + 12) + '" width="78" height="20" rx="10" style="fill:' + statusColor + ';fill-opacity:0.14;stroke:' + statusColor + ';"></rect>');
9594
+ parts.push('<text x="' + (cardX + cardW - 55) + '" y="' + (cardY + 26) + '" text-anchor="middle" style="fill:' + statusColor + ';font-size:9px;font-weight:700;letter-spacing:0.08em;">' + escapeHtml(statusLabel) + '</text>');
9595
+ parts.push('<text x="' + (cardX + 16) + '" y="' + (cardY + 46) + '" style="fill:var(--text-dim);font-size:10px;">' + doneCount + '/' + steps.length + ' steps complete • ' + pct + '%</text>');
9596
+ parts.push('<rect x="' + (cardX + 16) + '" y="' + (cardY + 56) + '" width="' + (cardW - 32) + '" height="6" rx="3" style="fill:var(--surface-3);"></rect>');
9597
+ parts.push('<rect x="' + (cardX + 16) + '" y="' + (cardY + 56) + '" width="' + Math.max(0, Math.round((cardW - 32) * (pct / 100))) + '" height="6" rx="3" style="fill:' + (workflow.status === 'completed' ? 'var(--green)' : 'var(--accent)') + ';"></rect>');
9598
+
9599
+ var visibleSteps = steps.slice(0, 6);
9600
+ var stepStartX = cardX + 22;
9601
+ var stepEndX = cardX + cardW - 22;
9602
+ var stepGap = visibleSteps.length > 1 ? (stepEndX - stepStartX) / (visibleSteps.length - 1) : 0;
9603
+ var stepY = cardY + 88;
9604
+ if (visibleSteps.length > 1) {
9605
+ parts.push('<line x1="' + stepStartX + '" y1="' + stepY + '" x2="' + stepEndX + '" y2="' + stepY + '" style="stroke:var(--border-light);stroke-width:2;"></line>');
9606
+ }
9607
+ for (var stepIndex = 0; stepIndex < visibleSteps.length; stepIndex++) {
9608
+ var visibleStep = visibleSteps[stepIndex];
9609
+ var dotX = visibleSteps.length === 1 ? ((stepStartX + stepEndX) / 2) : (stepStartX + (stepIndex * stepGap));
9610
+ var dotColor = visibleStep.status === 'done' ? 'var(--green)' : visibleStep.status === 'in_progress' ? 'var(--accent)' : 'var(--text-muted)';
9611
+ parts.push('<circle cx="' + dotX + '" cy="' + stepY + '" r="7" style="fill:var(--surface);stroke:' + dotColor + ';stroke-width:3;"></circle>');
9612
+ parts.push('<text x="' + dotX + '" y="' + (stepY + 22) + '" text-anchor="middle" style="fill:var(--text-dim);font-size:9px;font-weight:700;">' + escapeHtml(String(visibleStep.id || (stepIndex + 1))) + '</text>');
9613
+ }
9614
+ if (steps.length > visibleSteps.length) {
9615
+ parts.push('<text x="' + (cardX + cardW - 18) + '" y="' + (stepY + 24) + '" text-anchor="end" style="fill:var(--text-muted);font-size:9px;font-weight:700;">+' + (steps.length - visibleSteps.length) + '</text>');
9616
+ }
9617
+
9618
+ parts.push('<text x="' + (cardX + 16) + '" y="' + (cardY + 124) + '" style="fill:var(--text);font-size:11px;font-weight:600;">' + escapeHtml(summaryLabel) + '</text>');
9619
+ parts.push('<text x="' + (cardX + 16) + '" y="' + (cardY + 142) + '" style="fill:var(--text-dim);font-size:10px;">' + escapeHtml(activeStep && activeStep.assignee ? ('Owner: ' + activeStep.assignee) : 'Owner pending') + (workflow.autonomous ? ' • autonomous' : '') + (workflow.parallel ? ' • parallel' : '') + '</text>');
9620
+ }
9621
+
9622
+ if (workflowState.data.length > workflows.length) {
9623
+ parts.push('<text x="' + (panelX + 18) + '" y="' + (panelY + panelH - 20) + '" style="fill:var(--text-dim);font-size:10px;">+' + (workflowState.data.length - workflows.length) + ' more workflows continue in the primary Workflows view.</text>');
9624
+ }
9625
+
9626
+ return parts.join('');
9627
+ }
9628
+
9629
+ function buildGraphSvg(model) {
9630
+ var networkPanel = { x: 24, y: 78, w: 808, h: 718 };
9631
+ var parts = [];
9632
+ parts.push('<svg class="graph-surface" viewBox="0 0 1200 820" role="img" aria-label="Operator graph view showing agents, message flow, channels, and main branch workflows">');
9633
+ parts.push('<defs>');
9634
+ parts.push('<marker id="graph-arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto" markerUnits="strokeWidth">');
9635
+ parts.push('<path d="M0,0 L10,5 L0,10 z" style="fill:var(--text-muted);"></path>');
9636
+ parts.push('</marker>');
9637
+ parts.push('</defs>');
9638
+ parts.push('<rect x="0" y="0" width="1200" height="820" fill="transparent"></rect>');
9639
+ parts.push('<rect x="' + networkPanel.x + '" y="' + networkPanel.y + '" width="' + networkPanel.w + '" height="' + networkPanel.h + '" rx="22" style="fill:var(--surface);stroke:var(--border-light);"></rect>');
9640
+ parts.push('<text x="' + (networkPanel.x + 18) + '" y="' + (networkPanel.y + 28) + '" style="fill:var(--text);font-size:16px;font-weight:700;">Conversation flow</text>');
9641
+ parts.push('<text x="' + (networkPanel.x + 18) + '" y="' + (networkPanel.y + 48) + '" style="fill:var(--text-dim);font-size:11px;">Branch ' + escapeHtml(model.branchName) + ' • grouped by dominant channel • top 18 directed edges</text>');
9642
+
9643
+ if (!model.nodes.length) {
9644
+ parts.push('<rect x="' + (networkPanel.x + 18) + '" y="' + (networkPanel.y + 72) + '" width="' + (networkPanel.w - 36) + '" height="120" rx="18" style="fill:var(--surface-2);stroke:var(--border);"></rect>');
9645
+ parts.push('<text x="' + (networkPanel.x + 40) + '" y="' + (networkPanel.y + 120) + '" style="fill:var(--text);font-size:14px;font-weight:600;">No graphable operator data yet</text>');
9646
+ parts.push('<text x="' + (networkPanel.x + 40) + '" y="' + (networkPanel.y + 144) + '" style="fill:var(--text-dim);font-size:11px;">Once agents register and exchange messages, this view will render channel lanes and conversation edges.</text>');
9647
+ } else {
9648
+ for (var laneIndex = 0; laneIndex < model.lanes.length; laneIndex++) {
9649
+ var lane = model.lanes[laneIndex];
9650
+ var laneWidth = networkPanel.w - 24;
9651
+ var laneX = networkPanel.x + 12;
9652
+ parts.push('<rect x="' + laneX + '" y="' + lane.y + '" width="' + laneWidth + '" height="' + lane.height + '" rx="18" style="fill:' + lane.dim + ';fill-opacity:0.55;stroke:' + lane.accent + ';stroke-opacity:0.32;"></rect>');
9653
+ parts.push('<text x="' + (laneX + 16) + '" y="' + (lane.y + 24) + '" style="fill:' + lane.accent + ';font-size:11px;font-weight:700;letter-spacing:0.05em;">' + escapeHtml(lane.label.toUpperCase()) + '</text>');
9654
+ parts.push('<text x="' + (laneX + 16) + '" y="' + (lane.y + 42) + '" style="fill:var(--text-dim);font-size:10px;">' + escapeHtml(String(lane.count || 0)) + ' msgs • ' + escapeHtml(String(lane.participantCount || 0)) + ' participants</text>');
9655
+ }
9656
+
9657
+ for (var edgeIndex = 0; edgeIndex < model.visibleEdges.length; edgeIndex++) {
9658
+ var edge = model.visibleEdges[edgeIndex];
9659
+ var fromNode = model.nodeMap[edge.from];
9660
+ var toNode = model.nodeMap[edge.to];
9661
+ if (!fromNode || !toNode || !fromNode.x || !toNode.x) continue;
9662
+ var stroke = edge.laneKey && model.lanes.filter(function(lane) { return lane.key === edge.laneKey; }).length ? model.lanes.filter(function(lane) { return lane.key === edge.laneKey; })[0].accent : 'var(--text-muted)';
9663
+ var dx = toNode.x - fromNode.x;
9664
+ var dy = toNode.y - fromNode.y;
9665
+ var sameLane = Math.abs(dy) < 8;
9666
+ var midX = (fromNode.x + toNode.x) / 2;
9667
+ var controlY = sameLane ? (fromNode.y - (44 + ((edgeIndex % 3) * 16))) : ((fromNode.y + toNode.y) / 2) - (dy > 0 ? 20 : -20);
9668
+ var path = 'M' + fromNode.x + ' ' + fromNode.y + ' Q ' + midX + ' ' + controlY + ' ' + toNode.x + ' ' + toNode.y;
9669
+ var labelX = midX;
9670
+ var labelY = sameLane ? (controlY - 10) : (controlY - 8);
9671
+ var labelText = String(edge.count) + (edge.handoffs ? '↗' : '');
9672
+ var labelWidth = 12 + (labelText.length * 6);
9673
+ parts.push('<g>');
9674
+ parts.push('<path d="' + path + '" fill="none" stroke="' + stroke + '" stroke-opacity="0.7" stroke-width="' + (1.5 + Math.min(5, edge.count * 0.8)) + '" marker-end="url(#graph-arrow)"></path>');
9675
+ parts.push('<title>' + escapeHtml(edge.from + ' → ' + edge.to + ' • ' + edge.count + ' messages' + (edge.dominantChannel ? ' • #' + edge.dominantChannel : '')) + '</title>');
9676
+ parts.push('<rect x="' + Math.round(labelX - (labelWidth / 2)) + '" y="' + Math.round(labelY - 10) + '" width="' + labelWidth + '" height="16" rx="8" style="fill:var(--surface);stroke:var(--border-light);"></rect>');
9677
+ parts.push('<text x="' + labelX + '" y="' + (labelY + 2) + '" text-anchor="middle" style="fill:var(--text);font-size:9px;font-weight:700;">' + escapeHtml(labelText) + '</text>');
9678
+ parts.push('</g>');
9679
+ }
9680
+
9681
+ for (var nodeIndex = 0; nodeIndex < model.nodes.length; nodeIndex++) {
9682
+ var node = model.nodes[nodeIndex];
9683
+ if (!node.x || !node.y) continue;
9684
+ var nodeStatusColor = getGraphStatusColor(node.status);
9685
+ var nodeInitialFill = node.synthetic ? 'var(--text)' : 'var(--surface)';
9686
+ var label = truncateGraphText(node.label, 16);
9687
+ var summary = (node.sendCount + node.receiveCount) ? (node.sendCount + '↑ ' + node.receiveCount + '↓') : (node.synthetic ? 'system' : 'idle');
9688
+ var clickAttr = node.synthetic ? '' : ' style="cursor:pointer" onclick="openAgentMetadataDrawer(' + escapeHtml(JSON.stringify(node.agentName)) + ')"';
9689
+ parts.push('<g' + clickAttr + '>');
9690
+ parts.push('<title>' + escapeHtml(node.label + ' • sent ' + node.sendCount + ' • received ' + node.receiveCount + (node.role ? ' • ' + node.role : '')) + '</title>');
9691
+ parts.push('<circle cx="' + node.x + '" cy="' + node.y + '" r="26" style="fill:var(--bg);opacity:0.48;"></circle>');
9692
+ parts.push('<circle cx="' + node.x + '" cy="' + node.y + '" r="21" style="fill:' + node.color + ';stroke:' + nodeStatusColor + ';stroke-width:2.5;opacity:' + (node.status === 'dead' ? '0.58' : '0.94') + ';"></circle>');
9693
+ parts.push('<text x="' + node.x + '" y="' + (node.y + 5) + '" text-anchor="middle" style="fill:' + nodeInitialFill + ';font-size:12px;font-weight:800;">' + escapeHtml(initial(node.label)) + '</text>');
9694
+ parts.push('<text x="' + node.x + '" y="' + (node.y + 38) + '" text-anchor="middle" style="fill:var(--text);font-size:11px;font-weight:700;">' + escapeHtml(label) + '</text>');
9695
+ parts.push('<text x="' + node.x + '" y="' + (node.y + 54) + '" text-anchor="middle" style="fill:var(--text-dim);font-size:9px;">' + escapeHtml(summary) + '</text>');
9696
+ parts.push('</g>');
9697
+ }
9698
+ }
9699
+
9700
+ parts.push('<text x="40" y="784" style="fill:var(--text-dim);font-size:10px;">Tip: click an agent node to open the existing metadata drawer. The primary Messages and Workflows views remain the main control surfaces.</text>');
9701
+ parts.push(buildGraphWorkflowPanelSvg(graphWorkflowState));
9702
+ parts.push('</svg>');
9703
+ return parts.join('');
9704
+ }
9705
+
9706
+ function renderGraphView() {
9707
+ var el = document.getElementById('graph-area');
9708
+ if (!el) return;
9709
+ var model = buildGraphModel();
9710
+ var branchLabel = activeBranch && activeBranch !== 'main' ? activeBranch : 'main';
9711
+ var workflowBadge = graphWorkflowState.status === 'loading'
9712
+ ? 'Loading…'
9713
+ : graphWorkflowState.status === 'error'
9714
+ ? 'Unavailable'
9715
+ : String(Array.isArray(graphWorkflowState.data) ? graphWorkflowState.data.length : 0);
9716
+ var subtitle = branchLabel === 'main'
9717
+ ? 'Secondary operator slice only: this graph reuses the current dashboard data already in memory, while Messages and Workflows remain the primary navigation and control views.'
9718
+ : 'Secondary operator slice only: conversation flow is scoped to branch ' + branchLabel + ', while the workflow rail intentionally stays pinned to main-branch structure.';
9719
+ var html = '';
9720
+ html += '<div class="graph-shell">';
9721
+ html += '<div class="graph-header">';
9722
+ html += '<div>';
9723
+ html += '<div class="graph-title">Graph operator view</div>';
9724
+ html += '<div class="graph-subtitle">' + escapeHtml(subtitle) + '</div>';
9725
+ html += '</div>';
9726
+ html += '<div class="graph-meta">';
9727
+ html += '<div class="graph-pill">Flow branch <strong>' + escapeHtml(branchLabel) + '</strong></div>';
9728
+ html += '<div class="graph-pill">Agents <strong>' + escapeHtml(String(model.nodes.length)) + '</strong></div>';
9729
+ html += '<div class="graph-pill">Edges <strong>' + escapeHtml(String(model.edges.length)) + '</strong></div>';
9730
+ html += '<div class="graph-pill">Channels <strong>' + escapeHtml(String(model.channels.length)) + '</strong></div>';
9731
+ html += '<div class="graph-pill">Main workflows <strong>' + escapeHtml(workflowBadge) + '</strong></div>';
9732
+ html += '</div>';
9733
+ html += '</div>';
9734
+ html += '<div class="graph-surface-wrap">' + buildGraphSvg(model) + '</div>';
9735
+ html += '</div>';
9736
+ el.innerHTML = html;
9737
+ }
9738
+
7116
9739
  // ==================== v5.0: PLAN EXECUTION VIEW ====================
7117
9740
 
7118
9741
  var planRefreshInterval = null;
7119
9742
 
7120
9743
  function fetchPlanStatus() {
7121
- var pq = projectParam();
7122
- lttFetch('/api/plan/status' + pq).then(function(r) { return r.json(); }).then(function(data) {
9744
+ if (!isMainBranchSelected()) {
9745
+ if (planRefreshInterval) { clearInterval(planRefreshInterval); planRefreshInterval = null; }
9746
+ document.getElementById('monitor-panel').innerHTML = '';
9747
+ renderMainBranchOnlyView('plan-area', 'Plan view');
9748
+ return;
9749
+ }
9750
+ lttFetch(scopedApiUrl('/api/plan/status')).then(function(r) { return r.json(); }).then(function(data) {
9751
+ if (data && data.code === 'main_branch_only') {
9752
+ renderMainBranchOnlyView('plan-area', 'Plan view');
9753
+ return;
9754
+ }
7123
9755
  renderPlanView(data);
7124
9756
  }).catch(function() {
7125
9757
  // Fallback: use workflows API if plan API not yet available
7126
- lttFetch('/api/workflows' + pq).then(function(r) { return r.json(); }).then(function(wfs) {
9758
+ lttFetch(scopedApiUrl('/api/workflows')).then(function(r) { return r.json(); }).then(function(wfs) {
9759
+ if (wfs && wfs.code === 'main_branch_only') {
9760
+ renderMainBranchOnlyView('plan-area', 'Plan view');
9761
+ return;
9762
+ }
7127
9763
  var active = (Array.isArray(wfs) ? wfs : []).filter(function(w) { return w.status === 'active'; });
7128
9764
  renderPlanView({ workflows: active, fallback: true });
7129
9765
  }).catch(function() {
@@ -7140,6 +9776,10 @@ function fetchPlanStatus() {
7140
9776
 
7141
9777
  function renderPlanView(data) {
7142
9778
  var el = document.getElementById('plan-area');
9779
+ if (!isMainBranchSelected()) {
9780
+ el.innerHTML = mainBranchOnlyViewHtml('Plan view');
9781
+ return;
9782
+ }
7143
9783
  var workflows = data.workflows || (data.fallback ? data.workflows : [data]);
7144
9784
 
7145
9785
  if (!workflows || !workflows.length) {
@@ -7296,8 +9936,11 @@ function renderPlanView(data) {
7296
9936
  }
7297
9937
 
7298
9938
  function planAction(action, stepId, wfId) {
7299
- var pq = projectParam();
7300
- var url = '/api/plan/' + action + (stepId ? '/' + stepId : '') + pq;
9939
+ if (!isMainBranchSelected()) {
9940
+ showToast('Plan controls only support the main branch right now.');
9941
+ return;
9942
+ }
9943
+ var url = scopedApiUrl('/api/plan/' + action + (stepId ? '/' + stepId : ''));
7301
9944
  lttFetch(url, {
7302
9945
  method: 'POST',
7303
9946
  headers: { 'Content-Type': 'application/json' },
@@ -7306,10 +9949,13 @@ function planAction(action, stepId, wfId) {
7306
9949
  }
7307
9950
 
7308
9951
  function planReassign(stepId, wfId) {
9952
+ if (!isMainBranchSelected()) {
9953
+ showToast('Plan controls only support the main branch right now.');
9954
+ return;
9955
+ }
7309
9956
  var newAgent = prompt('Reassign step ' + stepId + ' to which agent?');
7310
9957
  if (!newAgent) return;
7311
- var pq = projectParam();
7312
- lttFetch('/api/plan/reassign/' + stepId + pq, {
9958
+ lttFetch(scopedApiUrl('/api/plan/reassign/' + stepId), {
7313
9959
  method: 'POST',
7314
9960
  headers: { 'Content-Type': 'application/json' },
7315
9961
  body: JSON.stringify({ workflow_id: wfId, new_assignee: newAgent })
@@ -7317,10 +9963,13 @@ function planReassign(stepId, wfId) {
7317
9963
  }
7318
9964
 
7319
9965
  function planInject(wfId) {
9966
+ if (!isMainBranchSelected()) {
9967
+ showToast('Plan controls only support the main branch right now.');
9968
+ return;
9969
+ }
7320
9970
  var msg = prompt('Inject message to all agents:');
7321
9971
  if (!msg) return;
7322
- var pq = projectParam();
7323
- lttFetch('/api/plan/inject' + pq, {
9972
+ lttFetch(scopedApiUrl('/api/plan/inject'), {
7324
9973
  method: 'POST',
7325
9974
  headers: { 'Content-Type': 'application/json' },
7326
9975
  body: JSON.stringify({ workflow_id: wfId, message: msg })
@@ -7332,8 +9981,8 @@ function planInject(wfId) {
7332
9981
  var _lastKnownWorkflowStatuses = {};
7333
9982
 
7334
9983
  function checkPlanCompletion() {
7335
- var pq = projectParam();
7336
- lttFetch('/api/workflows' + pq).then(function(r) { return r.json(); }).then(function(wfs) {
9984
+ if (!isMainBranchSelected()) return;
9985
+ lttFetch(scopedApiUrl('/api/workflows')).then(function(r) { return r.json(); }).then(function(wfs) {
7337
9986
  if (!Array.isArray(wfs)) return;
7338
9987
  for (var i = 0; i < wfs.length; i++) {
7339
9988
  var wf = wfs[i];
@@ -7359,6 +10008,11 @@ function checkPlanCompletion() {
7359
10008
  // ==================== v5.0: MONITOR HEALTH PANEL ====================
7360
10009
 
7361
10010
  function fetchMonitorHealth() {
10011
+ if (!isMainBranchSelected()) {
10012
+ var hiddenEl = document.getElementById('monitor-panel');
10013
+ if (hiddenEl) hiddenEl.innerHTML = '';
10014
+ return;
10015
+ }
7362
10016
  var pq = projectParam();
7363
10017
  lttFetch('/api/monitor/health' + pq).then(function(r) { return r.json(); }).then(function(data) {
7364
10018
  renderMonitorPanel(data);
@@ -7419,12 +10073,15 @@ function renderMonitorPanel(data) {
7419
10073
 
7420
10074
  // ==================== v3.0: BRANCHES ====================
7421
10075
 
7422
- var activeBranch = '';
10076
+ var activeBranch = dashboardWorkspaceState.liveWorkspace.snapshot.branch || 'main';
10077
+ var cachedBranchInfo = { main: { message_count: 0 } };
7423
10078
 
7424
10079
  function fetchBranches() {
7425
10080
  var pq = projectParam();
7426
10081
  lttFetch('/api/branches' + pq).then(function(r) { return r.json(); }).then(function(data) {
7427
- renderBranchTabs(data);
10082
+ cachedBranchInfo = data && typeof data === 'object' ? data : { main: { message_count: 0 } };
10083
+ if (!cachedBranchInfo.main) cachedBranchInfo.main = { message_count: 0 };
10084
+ renderBranchTabs(cachedBranchInfo);
7428
10085
  }).catch(function() {});
7429
10086
  }
7430
10087
 
@@ -7452,6 +10109,8 @@ function renderBranchTabs(branches) {
7452
10109
  function switchBranch(name) {
7453
10110
  activeBranch = name;
7454
10111
  lastMessageCount = 0;
10112
+ lastRenderedIds = [];
10113
+ persistLiveDashboardLayout();
7455
10114
  poll();
7456
10115
  }
7457
10116
 
@@ -7529,11 +10188,7 @@ function toggleSearchAll() {
7529
10188
  btn.style.background = searchAllMode ? 'var(--accent-dim)' : 'var(--surface-2)';
7530
10189
  btn.style.color = searchAllMode ? 'var(--accent)' : 'var(--text-muted)';
7531
10190
  btn.style.borderColor = searchAllMode ? 'var(--accent)' : 'var(--border)';
7532
- if (searchAllMode) {
7533
- document.getElementById('search-input').placeholder = 'Search ALL projects...';
7534
- } else {
7535
- document.getElementById('search-input').placeholder = 'Search messages... ( / )';
7536
- }
10191
+ updateSearchInputMode();
7537
10192
  onSearch();
7538
10193
  }
7539
10194
 
@@ -7568,8 +10223,7 @@ function searchAllProjects(query) {
7568
10223
  // ==================== v3.5: REPLAY EXPORT ====================
7569
10224
 
7570
10225
  function exportReplay() {
7571
- var pq = projectParam();
7572
- window.location.href = '/api/export-replay' + (pq || '?') + (_lttToken ? '&token=' + encodeURIComponent(_lttToken) : '');
10226
+ window.location.href = scopedApiUrl('/api/export-replay', null, { includeToken: true });
7573
10227
  }
7574
10228
 
7575
10229
  // ==================== v3.5: PERFORMANCE SCORES ====================
@@ -7627,12 +10281,10 @@ function renderScores(data, repData) {
7627
10281
  // ==================== POLLING ====================
7628
10282
 
7629
10283
  function poll() {
7630
- var pp = activeProject ? '&project=' + encodeURIComponent(activeProject) : '';
7631
10284
  var pq = activeProject ? '?project=' + encodeURIComponent(activeProject) : '';
7632
- var bp = activeBranch && activeBranch !== 'main' ? '&branch=' + encodeURIComponent(activeBranch) : '';
7633
10285
  var pollStart = Date.now();
7634
10286
  Promise.all([
7635
- lttFetch('/api/history?limit=500' + pp + bp).then(function(r) { return r.json(); }),
10287
+ lttFetch(scopedApiUrl('/api/history', { limit: 500 })).then(function(r) { return r.json(); }),
7636
10288
  lttFetch('/api/agents' + pq).then(function(r) { return r.json(); }),
7637
10289
  lttFetch('/api/status' + pq).then(function(r) { return r.json(); }),
7638
10290
  ]).then(function(results) {
@@ -7683,6 +10335,7 @@ function poll() {
7683
10335
  : 'Let Them Talk';
7684
10336
 
7685
10337
  renderAgents(cachedAgents);
10338
+ refreshAgentMetadataDrawer();
7686
10339
  renderAgentStats();
7687
10340
  renderThreads(cachedHistory);
7688
10341
  if (!replayActive) renderMessages(cachedHistory);
@@ -7693,9 +10346,11 @@ function poll() {
7693
10346
  fetchBranches();
7694
10347
  fetchNotifications();
7695
10348
  updateTypingIndicator(cachedAgents);
10349
+ if (activeView === 'services') renderServices();
7696
10350
  if (activeView === 'tasks') fetchTasks();
7697
10351
  if (activeView === 'workspaces') fetchWorkspaces();
7698
10352
  if (activeView === 'workflows') fetchWorkflows();
10353
+ if (activeView === 'graph') { renderGraphView(); ensureGraphWorkflowData(); }
7699
10354
  if (activeView === 'plan') fetchPlanStatus();
7700
10355
  if (activeView === 'stats') fetchStats();
7701
10356
  fetchReadReceipts();
@@ -7732,10 +10387,8 @@ function loadProjects() {
7732
10387
  sel.appendChild(opt);
7733
10388
  }
7734
10389
 
7735
- // Auto-select first project if none selected
7736
10390
  if (!activeProject && projects.length > 0) {
7737
10391
  activeProject = projects[0].path;
7738
- console.log('[LTT] auto-selected first project:', activeProject);
7739
10392
  }
7740
10393
 
7741
10394
  // If project came from URL param, try fuzzy match (handles path separator differences)
@@ -7749,22 +10402,26 @@ function loadProjects() {
7749
10402
  }
7750
10403
  }
7751
10404
 
10405
+ var hasMatchingProject = !activeProject;
7752
10406
  if (activeProject) {
7753
- sel.value = activeProject;
7754
- document.getElementById('remove-project-btn').style.display = '';
7755
- // Update header project indicator for mobile
7756
- var indicator = document.getElementById('mobile-project-name');
7757
- if (indicator) {
7758
- var proj = projects.find(function(p) { return p.path === activeProject; });
7759
- indicator.textContent = proj ? proj.name : '';
7760
- indicator.style.display = proj ? '' : 'none';
10407
+ for (var k = 0; k < projects.length; k++) {
10408
+ if (projects[k].path === activeProject) {
10409
+ hasMatchingProject = true;
10410
+ break;
10411
+ }
7761
10412
  }
7762
10413
  }
10414
+ if (!hasMatchingProject) {
10415
+ activeProject = projects.length > 0 ? projects[0].path : '';
10416
+ }
10417
+
10418
+ if (activeProject) {
10419
+ sel.value = activeProject;
10420
+ }
7763
10421
 
7764
- // Show/hide remove button
7765
- document.getElementById('remove-project-btn').style.display = activeProject ? '' : 'none';
7766
- // Sync mobile header project select
7767
10422
  syncMobileProjectSelect();
10423
+ syncProjectSelectionUI();
10424
+ persistLiveDashboardLayout();
7768
10425
  }).catch(function(e) {
7769
10426
  console.error('[LTT] loadProjects failed:', e);
7770
10427
  });
@@ -7773,8 +10430,10 @@ function loadProjects() {
7773
10430
  function switchProject() {
7774
10431
  activeProject = document.getElementById('project-select').value;
7775
10432
  lastMessageCount = 0;
7776
- document.getElementById('remove-project-btn').style.display = activeProject ? '' : 'none';
10433
+ lastRenderedIds = [];
7777
10434
  syncMobileProjectSelect();
10435
+ syncProjectSelectionUI();
10436
+ persistLiveDashboardLayout();
7778
10437
  loadConversationList();
7779
10438
  poll();
7780
10439
  if (isMobile) closeSidebar();
@@ -7783,9 +10442,12 @@ function switchProject() {
7783
10442
  function mobileSelectProject(val) {
7784
10443
  activeProject = val;
7785
10444
  lastMessageCount = 0;
10445
+ lastRenderedIds = [];
7786
10446
  // Sync sidebar select
7787
10447
  document.getElementById('project-select').value = val;
7788
- document.getElementById('remove-project-btn').style.display = val ? '' : 'none';
10448
+ syncProjectSelectionUI();
10449
+ persistLiveDashboardLayout();
10450
+ loadConversationList();
7789
10451
  poll();
7790
10452
  }
7791
10453
 
@@ -7805,15 +10467,29 @@ function syncMobileProjectSelect() {
7805
10467
  }
7806
10468
 
7807
10469
  function showAddProject() {
10470
+ var row = document.getElementById('project-input-row');
7808
10471
  var input = document.getElementById('project-path-input');
7809
- if (input.classList.contains('visible')) {
7810
- input.classList.remove('visible');
10472
+ if (row.style.display === 'flex') {
10473
+ row.style.display = 'none';
7811
10474
  } else {
7812
- input.classList.add('visible');
10475
+ row.style.display = 'flex';
7813
10476
  input.focus();
7814
10477
  }
7815
10478
  }
7816
10479
 
10480
+ function projectInitSuccessMessage(res) {
10481
+ if (!res || !res.initialization) return 'Project added.';
10482
+ if (res.initialization.mode === '--all') {
10483
+ return 'Project added and initialized for Claude, Gemini, and Codex.';
10484
+ }
10485
+ return 'Project added and initialized.';
10486
+ }
10487
+
10488
+ function reportProjectAddFailure(res, fallbackMessage) {
10489
+ var message = (res && res.error) ? res.error : fallbackMessage;
10490
+ alert(message);
10491
+ }
10492
+
7817
10493
  function addProject() {
7818
10494
  var input = document.getElementById('project-path-input');
7819
10495
  var projectPath = input.value.trim();
@@ -7826,18 +10502,21 @@ function addProject() {
7826
10502
  }).then(function(r) { return r.json(); }).then(function(res) {
7827
10503
  if (res.success) {
7828
10504
  input.value = '';
7829
- input.classList.remove('visible');
10505
+ var row = document.getElementById('project-input-row');
10506
+ if (row) row.style.display = 'none';
7830
10507
  // Auto-switch to the newly added project
7831
10508
  activeProject = res.project.path;
7832
- loadProjects();
7833
- setTimeout(function() {
10509
+ return loadProjects().then(function() {
7834
10510
  document.getElementById('project-select').value = activeProject;
7835
10511
  switchProject();
7836
- }, 200);
7837
- } else {
7838
- alert(res.error || 'Failed to add project');
10512
+ showToast(projectInitSuccessMessage(res));
10513
+ });
7839
10514
  }
7840
- }).catch(function(e) { console.error('Add project failed:', e); });
10515
+ reportProjectAddFailure(res, 'Failed to add project');
10516
+ }).catch(function(e) {
10517
+ console.error('Add project failed:', e);
10518
+ reportProjectAddFailure(null, 'Add project failed: ' + e.message);
10519
+ });
7841
10520
  }
7842
10521
 
7843
10522
  function discoverProjects() {
@@ -7878,9 +10557,38 @@ function addDiscovered(projectPath, name) {
7878
10557
  }).then(function(r) { return r.json(); }).then(function(res) {
7879
10558
  if (res.success) {
7880
10559
  document.getElementById('discover-results').style.display = 'none';
7881
- loadProjects();
10560
+ return loadProjects().then(function() {
10561
+ showToast(projectInitSuccessMessage(res));
10562
+ });
7882
10563
  }
7883
- }).catch(function() {});
10564
+ reportProjectAddFailure(res, 'Failed to add discovered project');
10565
+ }).catch(function(e) {
10566
+ console.error('Add discovered project failed:', e);
10567
+ reportProjectAddFailure(null, 'Add discovered project failed: ' + e.message);
10568
+ });
10569
+ }
10570
+
10571
+ function reinstallProviders() {
10572
+ if (!activeProject) return;
10573
+ if (!confirm('Re-run init for Claude, Gemini, and Codex configs in this project?\n\nMerge-safe — preserves your other MCP servers and creates .backup files.')) return;
10574
+ var btn = document.getElementById('reinstall-providers-btn');
10575
+ if (btn) { btn.disabled = true; btn.textContent = 'Reinstalling...'; }
10576
+ lttFetch('/api/projects/reinit', {
10577
+ method: 'POST',
10578
+ headers: { 'Content-Type': 'application/json' },
10579
+ body: JSON.stringify({ path: activeProject })
10580
+ }).then(function(r) { return r.json(); }).then(function(res) {
10581
+ if (res && res.success) {
10582
+ var targets = (res.initialization && res.initialization.targets) ? res.initialization.targets.join(', ') : 'all providers';
10583
+ showToast('Reinstalled providers: ' + targets + '. Restart your CLI terminals to pick it up.');
10584
+ } else {
10585
+ showToast('Reinstall failed: ' + ((res && res.error) || 'unknown error'));
10586
+ }
10587
+ }).catch(function(e) {
10588
+ showToast('Reinstall failed: ' + (e && e.message ? e.message : e));
10589
+ }).finally(function() {
10590
+ if (btn) { btn.disabled = false; btn.textContent = 'Reinstall Providers'; }
10591
+ });
7884
10592
  }
7885
10593
 
7886
10594
  function removeProject() {
@@ -7895,6 +10603,8 @@ function removeProject() {
7895
10603
  if (res.success) {
7896
10604
  activeProject = '';
7897
10605
  document.getElementById('project-select').value = '';
10606
+ syncProjectSelectionUI();
10607
+ persistLiveDashboardLayout();
7898
10608
  loadProjects();
7899
10609
  poll();
7900
10610
  }
@@ -8003,7 +10713,7 @@ function exitReplay() {
8003
10713
  if (replayTimer) { clearTimeout(replayTimer); replayTimer = null; }
8004
10714
  document.getElementById('replay-bar').classList.remove('visible');
8005
10715
  document.getElementById('view-tabs').style.display = '';
8006
- document.getElementById('search-bar').style.display = '';
10716
+ syncSearchBarVisibility();
8007
10717
  document.getElementById('replay-header-btn').style.display = '';
8008
10718
  lastMessageCount = 0;
8009
10719
  renderMessages(cachedHistory);
@@ -8094,7 +10804,7 @@ function renderReplayMessages() {
8094
10804
 
8095
10805
  // ==================== BROWSER NOTIFICATIONS ====================
8096
10806
 
8097
- var notifEnabled = localStorage.getItem('ltt-notif') === 'true';
10807
+ var notifEnabled = !!dashboardWorkspaceState.preferences.notificationsEnabled;
8098
10808
  var notifPermission = (typeof Notification !== 'undefined') ? Notification.permission : 'denied';
8099
10809
 
8100
10810
  function toggleNotifications() {
@@ -8105,17 +10815,17 @@ function toggleNotifications() {
8105
10815
  notifPermission = perm;
8106
10816
  if (perm === 'granted') {
8107
10817
  notifEnabled = true;
8108
- localStorage.setItem('ltt-notif', 'true');
10818
+ persistDashboardPreferences();
8109
10819
  updateNotifBtn();
8110
10820
  }
8111
10821
  });
8112
10822
  } else {
8113
10823
  notifEnabled = true;
8114
- localStorage.setItem('ltt-notif', 'true');
10824
+ persistDashboardPreferences();
8115
10825
  }
8116
10826
  } else {
8117
10827
  notifEnabled = false;
8118
- localStorage.setItem('ltt-notif', 'false');
10828
+ persistDashboardPreferences();
8119
10829
  }
8120
10830
  updateNotifBtn();
8121
10831
  }
@@ -8676,6 +11386,263 @@ function copyWatcherPrompt(idx) {
8676
11386
  }).catch(function() {});
8677
11387
  }
8678
11388
 
11389
+ // ==================== SERVICES VIEW ====================
11390
+
11391
+ var cachedApiAgents = [];
11392
+ var cachedMediaItems = [];
11393
+
11394
+ function renderServices() {
11395
+ var el = document.getElementById('services-area');
11396
+ fetchApiAgents();
11397
+ fetchMediaItems();
11398
+ }
11399
+
11400
+ function fetchApiAgents() {
11401
+ var pq = projectParam();
11402
+ lttFetch('/api/api-agents' + pq).then(function(r) { return r.json(); }).then(function(agents) {
11403
+ cachedApiAgents = Array.isArray(agents) ? agents : [];
11404
+ renderServicesUI();
11405
+ }).catch(function() { cachedApiAgents = []; renderServicesUI(); });
11406
+ }
11407
+
11408
+ function fetchMediaItems() {
11409
+ var pq = projectParam();
11410
+ lttFetch('/api/media' + pq + (pq ? '&' : '?') + 'limit=50').then(function(r) { return r.json(); }).then(function(media) {
11411
+ cachedMediaItems = Array.isArray(media) ? media : [];
11412
+ renderMediaBrowser();
11413
+ }).catch(function() { cachedMediaItems = []; renderMediaBrowser(); });
11414
+ }
11415
+
11416
+ function renderServicesUI() {
11417
+ var el = document.getElementById('services-area');
11418
+ var html = '<div class="services-container">';
11419
+ html += '<div class="services-header"><h2>API Service Agents</h2></div>';
11420
+
11421
+ // Create form
11422
+ html += '<div class="services-form">';
11423
+ html += '<div class="form-row"><div>';
11424
+ html += '<label>Agent Name</label>';
11425
+ html += '<input type="text" id="svc-name" placeholder="e.g. ImageGen" maxlength="20">';
11426
+ html += '</div><div>';
11427
+ html += '<label>Provider</label>';
11428
+ html += '<select id="svc-provider" onchange="onProviderChange()">';
11429
+ html += '<option value="gemini">Gemini (Google)</option>';
11430
+ html += '<option value="zai">Z.AI / GLM (Cloud)</option>';
11431
+ html += '<option value="comfyui">ComfyUI (Local)</option>';
11432
+ html += '<option value="ollama">Ollama (Local)</option>';
11433
+ html += '<option value="dalle">DALL-E 3 (OpenAI)</option>';
11434
+ html += '<option value="replicate">Replicate (Cloud)</option>';
11435
+ html += '</select>';
11436
+ html += '</div></div>';
11437
+
11438
+ html += '<div class="form-row"><div>';
11439
+ html += '<label>Model</label>';
11440
+ html += '<input type="text" id="svc-model" placeholder="e.g. sdxl, flux, dall-e-3">';
11441
+ html += '</div><div>';
11442
+ html += '<label id="svc-endpoint-label">Endpoint URL</label>';
11443
+ html += '<input type="text" id="svc-endpoint" placeholder="http://localhost:11434">';
11444
+ html += '</div></div>';
11445
+
11446
+ html += '<div id="svc-apikey-row" style="display:none">';
11447
+ html += '<label>API Key</label>';
11448
+ html += '<input type="password" id="svc-apikey" placeholder="sk-...">';
11449
+ html += '</div>';
11450
+
11451
+ html += '<button onclick="createApiAgent()">Create Agent</button>';
11452
+ html += '</div>';
11453
+
11454
+ // Agent cards
11455
+ if (cachedApiAgents.length > 0) {
11456
+ html += '<div class="services-cards">';
11457
+ cachedApiAgents.forEach(function(agent) {
11458
+ var dotColor = agent.running ? '#4ade80' : '#666';
11459
+ var statusText = agent.running ? 'Running' : 'Stopped';
11460
+ html += '<div class="service-card">';
11461
+ html += '<div class="sc-header">';
11462
+ html += '<div class="sc-dot" style="background:' + dotColor + '"></div>';
11463
+ html += '<span class="sc-name">' + escapeHtml(agent.name) + '</span>';
11464
+ html += '<span class="sc-provider" style="background:' + (agent.color || '#666') + '">' + escapeHtml(agent.provider) + '</span>';
11465
+ html += '</div>';
11466
+ html += '<div class="sc-stats">';
11467
+ html += 'Model: ' + escapeHtml(agent.model) + '<br>';
11468
+ html += 'Status: ' + statusText + '<br>';
11469
+ html += 'Requests: ' + (agent.stats ? agent.stats.requests : 0) + ' | Completed: ' + (agent.stats ? agent.stats.completed : 0) + ' | Errors: ' + (agent.stats ? agent.stats.errors : 0);
11470
+ if (agent.stats && agent.stats.lastActivity) {
11471
+ html += '<br>Last: ' + new Date(agent.stats.lastActivity).toLocaleTimeString();
11472
+ }
11473
+ html += '</div>';
11474
+ html += '<div class="sc-controls">';
11475
+ if (agent.running) {
11476
+ html += '<button class="sc-stop" onclick="stopApiAgent(\'' + escapeHtml(agent.name) + '\')">Stop</button>';
11477
+ } else {
11478
+ html += '<button class="sc-start" onclick="startApiAgent(\'' + escapeHtml(agent.name) + '\')">Start</button>';
11479
+ }
11480
+ html += '<button class="sc-delete" onclick="deleteApiAgent(\'' + escapeHtml(agent.name) + '\')">Delete</button>';
11481
+ html += '</div>';
11482
+ html += '</div>';
11483
+ });
11484
+ html += '</div>';
11485
+ } else {
11486
+ html += '<div style="text-align:center;padding:24px;color:var(--text-dim);font-size:13px">No API agents configured. Create one above to get started.</div>';
11487
+ }
11488
+
11489
+ // Media browser placeholder
11490
+ html += '<div class="media-browser"><h3>Generated Media</h3><div id="media-grid-container" class="media-grid"></div></div>';
11491
+
11492
+ html += '</div>';
11493
+ el.innerHTML = html;
11494
+
11495
+ // Initialize provider form fields (show/hide API key, set defaults)
11496
+ if (document.getElementById('svc-provider')) onProviderChange();
11497
+
11498
+ // Render media separately (may load async)
11499
+ renderMediaBrowser();
11500
+ }
11501
+
11502
+ function renderMediaBrowser() {
11503
+ var container = document.getElementById('media-grid-container');
11504
+ if (!container) return;
11505
+
11506
+ if (cachedMediaItems.length === 0) {
11507
+ container.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:20px;color:var(--text-dim);font-size:12px">No media generated yet. Send a message to an API agent to get started.</div>';
11508
+ return;
11509
+ }
11510
+
11511
+ var pq = projectParam();
11512
+ var html = '';
11513
+ cachedMediaItems.forEach(function(item) {
11514
+ var imgSrc = '/api/media/' + item.id + '/file' + pq;
11515
+ html += '<div class="media-item" onclick="showMediaLightbox(\'' + item.id + '\')">';
11516
+ if (item.type === 'image') {
11517
+ html += '<img src="' + imgSrc + '" alt="' + escapeHtml(item.prompt || '') + '" loading="lazy">';
11518
+ } else {
11519
+ html += '<div style="width:100%;aspect-ratio:1;background:var(--surface-1);display:flex;align-items:center;justify-content:center;color:var(--text-dim);font-size:24px">' + (item.type === 'video' ? '&#x25B6;' : '&#x1F5BC;') + '</div>';
11520
+ }
11521
+ html += '<div class="mi-info">';
11522
+ html += '<div class="mi-prompt">' + escapeHtml(item.prompt || 'Untitled') + '</div>';
11523
+ html += '<div style="display:flex;justify-content:space-between;margin-top:2px"><span>' + escapeHtml(item.agent || '') + '</span><span>' + new Date(item.timestamp).toLocaleTimeString() + '</span></div>';
11524
+ html += '</div></div>';
11525
+ });
11526
+ container.innerHTML = html;
11527
+ }
11528
+
11529
+ function showMediaLightbox(id) {
11530
+ var pq = projectParam();
11531
+ var item = cachedMediaItems.find(function(m) { return m.id === id; });
11532
+ if (!item) return;
11533
+
11534
+ var overlay = document.createElement('div');
11535
+ overlay.className = 'media-lightbox';
11536
+ overlay.onclick = function() { overlay.remove(); };
11537
+
11538
+ var imgSrc = '/api/media/' + id + '/file' + pq;
11539
+ overlay.innerHTML = '<img src="' + imgSrc + '"><div class="lb-info">' + escapeHtml(item.prompt || '') + '<br><small>' + escapeHtml(item.agent || '') + ' &middot; ' + escapeHtml(item.model || '') + ' &middot; ' + new Date(item.timestamp).toLocaleString() + '</small></div>';
11540
+ document.body.appendChild(overlay);
11541
+ }
11542
+
11543
+ function onProviderChange() {
11544
+ var provider = document.getElementById('svc-provider').value;
11545
+ var keyRow = document.getElementById('svc-apikey-row');
11546
+ var endpointLabel = document.getElementById('svc-endpoint-label');
11547
+ var endpointInput = document.getElementById('svc-endpoint');
11548
+
11549
+ if (provider === 'zai') {
11550
+ keyRow.style.display = 'block';
11551
+ endpointLabel.textContent = 'Endpoint (leave default)';
11552
+ endpointInput.placeholder = 'https://api.z.ai';
11553
+ endpointInput.value = '';
11554
+ document.getElementById('svc-model').placeholder = 'glm-5';
11555
+ document.getElementById('svc-model').value = 'glm-5';
11556
+ document.getElementById('svc-apikey').placeholder = 'your-zai-api-key';
11557
+ } else if (provider === 'comfyui') {
11558
+ keyRow.style.display = 'none';
11559
+ endpointLabel.textContent = 'ComfyUI URL';
11560
+ endpointInput.placeholder = 'http://127.0.0.1:8188';
11561
+ endpointInput.value = 'http://127.0.0.1:8188';
11562
+ document.getElementById('svc-model').placeholder = 'flux_text_to_image';
11563
+ document.getElementById('svc-model').value = 'flux_text_to_image';
11564
+ } else if (provider === 'gemini') {
11565
+ keyRow.style.display = 'block';
11566
+ endpointLabel.textContent = 'Endpoint (leave default)';
11567
+ endpointInput.placeholder = 'https://generativelanguage.googleapis.com';
11568
+ endpointInput.value = '';
11569
+ document.getElementById('svc-model').placeholder = 'gemini-3-pro-image-preview';
11570
+ document.getElementById('svc-model').value = 'gemini-3-pro-image-preview';
11571
+ document.getElementById('svc-apikey').placeholder = 'AIza...';
11572
+ } else if (provider === 'ollama') {
11573
+ keyRow.style.display = 'none';
11574
+ endpointLabel.textContent = 'Endpoint URL';
11575
+ endpointInput.placeholder = 'http://localhost:11434';
11576
+ endpointInput.value = 'http://localhost:11434';
11577
+ document.getElementById('svc-model').placeholder = 'e.g. qwen3.5, llama3.2-vision';
11578
+ document.getElementById('svc-model').value = '';
11579
+ document.getElementById('svc-apikey').placeholder = 'sk-...';
11580
+ } else if (provider === 'dalle') {
11581
+ keyRow.style.display = 'block';
11582
+ endpointLabel.textContent = 'Endpoint (leave default)';
11583
+ endpointInput.placeholder = 'https://api.openai.com';
11584
+ endpointInput.value = '';
11585
+ document.getElementById('svc-model').placeholder = 'dall-e-3';
11586
+ document.getElementById('svc-model').value = 'dall-e-3';
11587
+ document.getElementById('svc-apikey').placeholder = 'sk-...';
11588
+ } else if (provider === 'replicate') {
11589
+ keyRow.style.display = 'block';
11590
+ endpointLabel.textContent = 'Endpoint (leave default)';
11591
+ endpointInput.placeholder = 'https://api.replicate.com';
11592
+ endpointInput.value = '';
11593
+ document.getElementById('svc-model').placeholder = 'stability-ai/sdxl';
11594
+ document.getElementById('svc-model').value = '';
11595
+ document.getElementById('svc-apikey').placeholder = 'r8_...';
11596
+ }
11597
+ }
11598
+
11599
+ function createApiAgent() {
11600
+ var name = document.getElementById('svc-name').value.trim();
11601
+ var provider = document.getElementById('svc-provider').value;
11602
+ var model = document.getElementById('svc-model').value.trim() || 'sdxl';
11603
+ var endpoint = document.getElementById('svc-endpoint').value.trim() || 'http://localhost:11434';
11604
+ var apiKey = (document.getElementById('svc-apikey') || {}).value || '';
11605
+
11606
+ if (!name) { alert('Enter an agent name'); return; }
11607
+
11608
+ var pq = projectParam();
11609
+ lttFetch('/api/api-agents' + pq, {
11610
+ method: 'POST',
11611
+ headers: { 'Content-Type': 'application/json', 'X-LTT-Request': '1' },
11612
+ body: JSON.stringify({ name: name, provider: provider, model: model, endpoint: endpoint, apiKey: apiKey })
11613
+ }).then(function(r) { return r.json(); }).then(function(result) {
11614
+ if (result.error) { alert(result.error); return; }
11615
+ document.getElementById('svc-name').value = '';
11616
+ // Auto-start the bot immediately after creation
11617
+ startApiAgent(name);
11618
+ }).catch(function(e) { alert('Failed: ' + e.message); });
11619
+ }
11620
+
11621
+ function startApiAgent(name) {
11622
+ var pq = projectParam();
11623
+ lttFetch('/api/api-agents/' + encodeURIComponent(name) + '/start' + pq, {
11624
+ method: 'POST',
11625
+ headers: { 'X-LTT-Request': '1' }
11626
+ }).then(function() { fetchApiAgents(); });
11627
+ }
11628
+
11629
+ function stopApiAgent(name) {
11630
+ var pq = projectParam();
11631
+ lttFetch('/api/api-agents/' + encodeURIComponent(name) + '/stop' + pq, {
11632
+ method: 'POST',
11633
+ headers: { 'X-LTT-Request': '1' }
11634
+ }).then(function() { fetchApiAgents(); });
11635
+ }
11636
+
11637
+ function deleteApiAgent(name) {
11638
+ if (!confirm('Delete API agent "' + name + '"?')) return;
11639
+ var pq = projectParam();
11640
+ lttFetch('/api/api-agents/' + encodeURIComponent(name) + pq, {
11641
+ method: 'DELETE',
11642
+ headers: { 'X-LTT-Request': '1' }
11643
+ }).then(function() { fetchApiAgents(); });
11644
+ }
11645
+
8679
11646
  // ==================== v3.6: DOCS VIEW ====================
8680
11647
 
8681
11648
  function renderDocs() {
@@ -8694,7 +11661,7 @@ function renderDocs() {
8694
11661
  '<h3>Quick Start \u2014 One Command</h3>' +
8695
11662
  '<pre><code>npx let-them-talk run "build a REST API with auth" --agents 4</code></pre>' +
8696
11663
  '<p>Spawns 4 agents, auto-assigns roles (Lead, Implementer, Quality, etc.), creates an autonomous workflow, and starts execution. Walk away, come back to finished work.</p>' +
8697
- '<p>Monitor progress: <code>npx let-them-talk dashboard</code> | Check status: <code>npx let-them-talk status</code> | Diagnose issues: <code>npx let-them-talk doctor</code></p>' +
11664
+ '<p>Monitor progress: <code>node .agent-bridge/launch.js</code> | Check status: <code>node .agent-bridge/launch.js status</code> | Diagnose issues: <code>npx let-them-talk doctor</code></p>' +
8698
11665
  '</div>' +
8699
11666
 
8700
11667
  // v5.0 Autonomy Engine
@@ -8720,7 +11687,7 @@ function renderDocs() {
8720
11687
  '<p>This auto-detects your CLI (Claude Code, Gemini CLI, or Codex CLI) and adds the agent-bridge MCP server to its config. To set up all CLIs at once:</p>' +
8721
11688
  '<pre><code>npx let-them-talk init --all</code></pre>' +
8722
11689
  '<h4>2. Open the Dashboard</h4>' +
8723
- '<pre><code>npx let-them-talk dashboard</code></pre>' +
11690
+ '<pre><code>node .agent-bridge/launch.js</code></pre>' +
8724
11691
  '<p>Opens this web dashboard at <code>http://localhost:3777</code>. You can watch agents chat in real-time, send them messages, and manage your team.</p>' +
8725
11692
  '<h4>3. Start Your Agents</h4>' +
8726
11693
  '<p>Open two or more terminal windows in your project folder. In each one, start your AI CLI (e.g. type <code>claude</code>) and tell it to register:</p>' +
@@ -8799,9 +11766,10 @@ function renderDocs() {
8799
11766
  '<div class="docs-tool-item"><code>npx let-them-talk init</code><div class="desc">Set up MCP server config for your CLI</div></div>' +
8800
11767
  '<div class="docs-tool-item"><code>npx let-them-talk init --all</code><div class="desc">Set up for all detected CLIs at once</div></div>' +
8801
11768
  '<div class="docs-tool-item"><code>npx let-them-talk init --template &lt;name&gt;</code><div class="desc">Start with a team template</div></div>' +
8802
- '<div class="docs-tool-item"><code>npx let-them-talk dashboard</code><div class="desc">Open the web dashboard</div></div>' +
11769
+ '<div class="docs-tool-item"><code>node .agent-bridge/launch.js</code><div class="desc">Open the web dashboard (no re-download)</div></div>' +
11770
+ '<div class="docs-tool-item"><code>node .agent-bridge/launch.js status</code><div class="desc">Show active agents and tasks</div></div>' +
11771
+ '<div class="docs-tool-item"><code>node .agent-bridge/launch.js reset</code><div class="desc">Clear conversation data (auto-archives first)</div></div>' +
8803
11772
  '<div class="docs-tool-item"><code>npx let-them-talk templates</code><div class="desc">List available templates</div></div>' +
8804
- '<div class="docs-tool-item"><code>npx let-them-talk reset --force</code><div class="desc">Clear conversation data (auto-archives first)</div></div>' +
8805
11773
  '</div>' +
8806
11774
  '</div>' +
8807
11775
 
@@ -9013,6 +11981,7 @@ function initSSE() {
9013
11981
  // Targeted fetch for tasks/workflows only — no full poll needed
9014
11982
  if (needTasks && activeView === 'tasks') fetchTasks();
9015
11983
  if (needWorkflows && activeView === 'workflows') fetchWorkflows();
11984
+ if (needAgents && activeView === 'services') fetchApiAgents();
9016
11985
  }
9017
11986
  };
9018
11987
  eventSource.onopen = function() {
@@ -9057,6 +12026,11 @@ function initSSE() {
9057
12026
 
9058
12027
  // Init UI preferences
9059
12028
  initCompactMode();
12029
+ applyAgentFilterInputs();
12030
+ applyPinnedSectionState();
12031
+ refreshDashboardWorkspaceControls();
12032
+ updateSearchInputMode();
12033
+ syncSearchBarVisibility();
9060
12034
 
9061
12035
  // Load projects first, then poll (so auto-select works before first data fetch)
9062
12036
  loadProjects().then(function() {
@@ -9064,12 +12038,12 @@ loadProjects().then(function() {
9064
12038
  loadConversationList();
9065
12039
  poll();
9066
12040
  initSSE();
9067
- switchView('office');
12041
+ switchView(activeView, { skipPersist: true });
9068
12042
  }).catch(function(e) {
9069
12043
  console.error('[LTT] init: loadProjects failed, polling anyway:', e);
9070
12044
  poll();
9071
12045
  initSSE();
9072
- switchView('office');
12046
+ switchView(activeView, { skipPersist: true });
9073
12047
  });
9074
12048
  // Safety-net poll at 10s (SSE handles real-time, this catches any missed updates)
9075
12049
  setInterval(poll, 10000);