@cluesmith/codev 1.4.0 → 1.4.2

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 (35) hide show
  1. package/bin/af.js +0 -0
  2. package/bin/codev.js +0 -0
  3. package/bin/consult.js +0 -0
  4. package/bin/generate-image.js +0 -0
  5. package/dist/agent-farm/servers/dashboard-server.js +487 -5
  6. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  7. package/dist/commands/adopt.d.ts.map +1 -1
  8. package/dist/commands/adopt.js +10 -0
  9. package/dist/commands/adopt.js.map +1 -1
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +8 -0
  12. package/dist/commands/init.js.map +1 -1
  13. package/package.json +1 -1
  14. package/skeleton/templates/projectlist-archive.md +21 -0
  15. package/skeleton/templates/projectlist.md +17 -0
  16. package/templates/dashboard/css/activity.css +151 -0
  17. package/templates/dashboard/css/dialogs.css +149 -0
  18. package/templates/dashboard/css/files.css +530 -0
  19. package/templates/dashboard/css/layout.css +124 -0
  20. package/templates/dashboard/css/projects.css +501 -0
  21. package/templates/dashboard/css/statusbar.css +23 -0
  22. package/templates/dashboard/css/tabs.css +314 -0
  23. package/templates/dashboard/css/utilities.css +50 -0
  24. package/templates/dashboard/css/variables.css +45 -0
  25. package/templates/dashboard/index.html +158 -0
  26. package/templates/dashboard/js/activity.js +238 -0
  27. package/templates/dashboard/js/dialogs.js +328 -0
  28. package/templates/dashboard/js/files.js +436 -0
  29. package/templates/dashboard/js/main.js +487 -0
  30. package/templates/dashboard/js/projects.js +544 -0
  31. package/templates/dashboard/js/state.js +91 -0
  32. package/templates/dashboard/js/tabs.js +500 -0
  33. package/templates/dashboard/js/utils.js +57 -0
  34. package/templates/dashboard-split.html +1186 -171
  35. package/templates/open.html +7 -2
@@ -651,6 +651,156 @@
651
651
  color: #ef4444;
652
652
  }
653
653
 
654
+ /* Activity Summary Modal (Spec 0059) */
655
+ .activity-dialog {
656
+ width: 600px;
657
+ max-width: 90vw;
658
+ max-height: 80vh;
659
+ display: flex;
660
+ flex-direction: column;
661
+ }
662
+
663
+ .activity-dialog-header {
664
+ display: flex;
665
+ justify-content: space-between;
666
+ align-items: center;
667
+ margin-bottom: 16px;
668
+ }
669
+
670
+ .activity-dialog-header h3 {
671
+ margin: 0;
672
+ }
673
+
674
+ .activity-close-btn {
675
+ background: none;
676
+ border: none;
677
+ font-size: 24px;
678
+ color: var(--text-muted);
679
+ cursor: pointer;
680
+ padding: 0 8px;
681
+ line-height: 1;
682
+ }
683
+
684
+ .activity-close-btn:hover {
685
+ color: var(--text-primary);
686
+ }
687
+
688
+ .activity-dialog-content {
689
+ flex: 1;
690
+ overflow-y: auto;
691
+ max-height: 50vh;
692
+ margin-bottom: 16px;
693
+ }
694
+
695
+ .activity-loading {
696
+ display: flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ gap: 12px;
700
+ padding: 40px 20px;
701
+ color: var(--text-muted);
702
+ }
703
+
704
+ .activity-spinner {
705
+ width: 20px;
706
+ height: 20px;
707
+ border: 2px solid var(--border);
708
+ border-top-color: var(--accent);
709
+ border-radius: 50%;
710
+ animation: spin 1s linear infinite;
711
+ }
712
+
713
+ @keyframes spin {
714
+ to { transform: rotate(360deg); }
715
+ }
716
+
717
+ .activity-empty {
718
+ text-align: center;
719
+ padding: 40px 20px;
720
+ color: var(--text-muted);
721
+ }
722
+
723
+ .activity-error {
724
+ text-align: center;
725
+ padding: 40px 20px;
726
+ color: #ef4444;
727
+ }
728
+
729
+ .activity-summary {
730
+ line-height: 1.6;
731
+ }
732
+
733
+ .activity-ai-summary {
734
+ background: var(--bg-tertiary);
735
+ border-left: 3px solid var(--accent);
736
+ padding: 12px 16px;
737
+ margin-bottom: 20px;
738
+ font-style: italic;
739
+ color: var(--text-secondary);
740
+ }
741
+
742
+ .activity-section {
743
+ margin-bottom: 16px;
744
+ }
745
+
746
+ .activity-section h4 {
747
+ font-size: 13px;
748
+ text-transform: uppercase;
749
+ color: var(--text-muted);
750
+ margin: 0 0 8px 0;
751
+ letter-spacing: 0.5px;
752
+ }
753
+
754
+ .activity-section ul {
755
+ margin: 0;
756
+ padding-left: 20px;
757
+ color: var(--text-secondary);
758
+ }
759
+
760
+ .activity-section li {
761
+ margin-bottom: 4px;
762
+ }
763
+
764
+ .activity-section p {
765
+ margin: 4px 0;
766
+ color: var(--text-secondary);
767
+ }
768
+
769
+ .activity-time-value {
770
+ font-size: 18px;
771
+ font-weight: 500;
772
+ color: var(--text-primary);
773
+ }
774
+
775
+ .activity-dialog-footer {
776
+ display: flex;
777
+ justify-content: flex-end;
778
+ gap: 8px;
779
+ padding-top: 12px;
780
+ border-top: 1px solid var(--border);
781
+ }
782
+
783
+ /* Activity Tab Styles (Spec 0059) */
784
+ .activity-tab-container {
785
+ padding: 24px;
786
+ max-width: 700px;
787
+ margin: 0 auto;
788
+ }
789
+
790
+ .activity-tab-container .activity-summary {
791
+ background: var(--bg-secondary);
792
+ border-radius: 8px;
793
+ padding: 20px;
794
+ }
795
+
796
+ .activity-tab-container .activity-actions {
797
+ margin-top: 20px;
798
+ padding-top: 16px;
799
+ border-top: 1px solid var(--border);
800
+ display: flex;
801
+ justify-content: flex-end;
802
+ }
803
+
654
804
  /* Projects Tab Styles (Spec 0045) */
655
805
  .projects-container {
656
806
  flex: 1;
@@ -1157,56 +1307,7 @@
1157
1307
  display: none;
1158
1308
  }
1159
1309
 
1160
- /* Files Tab Styles (Spec 0055) */
1161
- .files-container {
1162
- flex: 1;
1163
- overflow-y: auto;
1164
- display: flex;
1165
- flex-direction: column;
1166
- }
1167
-
1168
- .files-header {
1169
- display: flex;
1170
- justify-content: space-between;
1171
- align-items: center;
1172
- padding: 8px 12px;
1173
- background: var(--bg-secondary);
1174
- border-bottom: 1px solid var(--border);
1175
- }
1176
-
1177
- .files-header-title {
1178
- font-size: 12px;
1179
- color: var(--text-muted);
1180
- text-transform: uppercase;
1181
- letter-spacing: 0.5px;
1182
- }
1183
-
1184
- .files-header-actions {
1185
- display: flex;
1186
- gap: 4px;
1187
- }
1188
-
1189
- .files-header-actions button {
1190
- padding: 4px 8px;
1191
- border-radius: 4px;
1192
- border: 1px solid var(--border);
1193
- background: var(--bg-tertiary);
1194
- color: var(--text-secondary);
1195
- cursor: pointer;
1196
- font-size: 11px;
1197
- }
1198
-
1199
- .files-header-actions button:hover {
1200
- background: var(--tab-hover);
1201
- color: var(--text-primary);
1202
- }
1203
-
1204
- .files-tree {
1205
- flex: 1;
1206
- overflow-y: auto;
1207
- padding: 8px 0;
1208
- }
1209
-
1310
+ /* Tree Styles (used by dashboard file browser) */
1210
1311
  .tree-item {
1211
1312
  display: flex;
1212
1313
  align-items: center;
@@ -1270,25 +1371,6 @@
1270
1371
  display: none;
1271
1372
  }
1272
1373
 
1273
- .files-loading {
1274
- display: flex;
1275
- align-items: center;
1276
- justify-content: center;
1277
- padding: 24px;
1278
- color: var(--text-muted);
1279
- font-size: 13px;
1280
- }
1281
-
1282
- .files-error {
1283
- padding: 16px;
1284
- margin: 8px;
1285
- background: rgba(239, 68, 68, 0.1);
1286
- border: 1px solid var(--status-error);
1287
- border-radius: 6px;
1288
- color: var(--text-secondary);
1289
- font-size: 13px;
1290
- }
1291
-
1292
1374
  /* Dashboard Tab Styles (Spec 0057) */
1293
1375
  .dashboard-container {
1294
1376
  flex: 1;
@@ -1298,8 +1380,7 @@
1298
1380
  }
1299
1381
 
1300
1382
  .dashboard-header {
1301
- display: grid;
1302
- grid-template-columns: 1fr 1fr;
1383
+ display: flex;
1303
1384
  gap: 16px;
1304
1385
  padding: 16px;
1305
1386
  flex-shrink: 0;
@@ -1307,10 +1388,118 @@
1307
1388
 
1308
1389
  @media (max-width: 900px) {
1309
1390
  .dashboard-header {
1310
- grid-template-columns: 1fr;
1391
+ flex-direction: column;
1311
1392
  }
1312
1393
  }
1313
1394
 
1395
+ /* Collapsible section styles */
1396
+ .dashboard-section {
1397
+ background: var(--bg-secondary);
1398
+ border: 1px solid var(--border);
1399
+ border-radius: 8px;
1400
+ overflow: hidden;
1401
+ display: flex;
1402
+ flex-direction: column;
1403
+ }
1404
+
1405
+ .dashboard-section.section-tabs,
1406
+ .dashboard-section.section-files {
1407
+ flex: 1;
1408
+ max-height: 280px;
1409
+ }
1410
+
1411
+ .dashboard-section.section-projects {
1412
+ flex: 0 0 auto;
1413
+ margin: 0 16px 16px 16px;
1414
+ max-height: 50%;
1415
+ overflow-y: auto;
1416
+ }
1417
+
1418
+ .dashboard-section.section-projects .dashboard-section-content {
1419
+ flex: 0 0 auto;
1420
+ }
1421
+
1422
+ /* Tabs/Files expand to fill remaining space above Projects */
1423
+ .dashboard-header {
1424
+ flex: 1;
1425
+ min-height: 0;
1426
+ }
1427
+
1428
+ .dashboard-section.section-tabs,
1429
+ .dashboard-section.section-files {
1430
+ max-height: none;
1431
+ }
1432
+
1433
+ .dashboard-section-header {
1434
+ display: flex;
1435
+ justify-content: space-between;
1436
+ align-items: center;
1437
+ padding: 8px 12px;
1438
+ cursor: pointer;
1439
+ user-select: none;
1440
+ flex-shrink: 0;
1441
+ border-bottom: 1px solid var(--border);
1442
+ }
1443
+
1444
+ .dashboard-section-header:hover {
1445
+ background: var(--bg-tertiary);
1446
+ }
1447
+
1448
+ .dashboard-section-header h3 {
1449
+ font-size: 12px;
1450
+ text-transform: uppercase;
1451
+ color: var(--text-muted);
1452
+ letter-spacing: 0.5px;
1453
+ margin: 0;
1454
+ display: flex;
1455
+ align-items: center;
1456
+ gap: 6px;
1457
+ }
1458
+
1459
+ .dashboard-section-header .collapse-icon {
1460
+ font-size: 10px;
1461
+ transition: transform 0.2s;
1462
+ }
1463
+
1464
+ .dashboard-section.collapsed .collapse-icon {
1465
+ transform: rotate(-90deg);
1466
+ }
1467
+
1468
+ .dashboard-section.collapsed .dashboard-section-header {
1469
+ border-bottom: none;
1470
+ }
1471
+
1472
+ .dashboard-section-header .header-actions {
1473
+ display: flex;
1474
+ gap: 4px;
1475
+ }
1476
+
1477
+ .dashboard-section-header .header-actions button {
1478
+ padding: 4px 8px;
1479
+ border-radius: 4px;
1480
+ border: 1px solid var(--border);
1481
+ background: var(--bg-tertiary);
1482
+ color: var(--text-secondary);
1483
+ cursor: pointer;
1484
+ font-size: 11px;
1485
+ }
1486
+
1487
+ .dashboard-section-header .header-actions button:hover {
1488
+ background: var(--tab-hover);
1489
+ color: var(--text-primary);
1490
+ }
1491
+
1492
+ .dashboard-section-content {
1493
+ flex: 1;
1494
+ overflow-y: auto;
1495
+ padding: 8px 12px;
1496
+ }
1497
+
1498
+ .dashboard-section.collapsed .dashboard-section-content {
1499
+ display: none;
1500
+ }
1501
+
1502
+ /* Legacy support */
1314
1503
  .dashboard-column {
1315
1504
  background: var(--bg-secondary);
1316
1505
  border: 1px solid var(--border);
@@ -1437,17 +1626,183 @@
1437
1626
  font-size: 12px;
1438
1627
  }
1439
1628
 
1440
- .dashboard-empty-state {
1629
+ /* File search styles (Spec 0058) */
1630
+ .files-search-container {
1631
+ display: flex;
1632
+ align-items: center;
1633
+ padding: 6px 8px;
1634
+ gap: 6px;
1635
+ border-bottom: 1px solid var(--border);
1636
+ }
1637
+
1638
+ .files-search-input {
1639
+ flex: 1;
1640
+ background: var(--bg-tertiary);
1641
+ border: 1px solid var(--border);
1642
+ border-radius: 4px;
1643
+ padding: 6px 10px;
1644
+ font-size: 12px;
1645
+ color: var(--text-primary);
1646
+ outline: none;
1647
+ }
1648
+
1649
+ .files-search-input:focus {
1650
+ border-color: var(--accent);
1651
+ }
1652
+
1653
+ .files-search-input::placeholder {
1654
+ color: var(--text-muted);
1655
+ }
1656
+
1657
+ .files-search-clear {
1658
+ background: transparent;
1659
+ border: none;
1660
+ color: var(--text-muted);
1661
+ cursor: pointer;
1662
+ font-size: 14px;
1663
+ padding: 2px 6px;
1664
+ border-radius: 4px;
1665
+ line-height: 1;
1666
+ }
1667
+
1668
+ .files-search-clear:hover {
1669
+ color: var(--text-primary);
1670
+ background: var(--bg-tertiary);
1671
+ }
1672
+
1673
+ .files-search-clear.hidden {
1674
+ display: none;
1675
+ }
1676
+
1677
+ .files-search-results {
1678
+ flex: 1;
1679
+ overflow-y: auto;
1680
+ }
1681
+
1682
+ .files-search-result {
1683
+ padding: 6px 12px;
1684
+ cursor: pointer;
1685
+ display: flex;
1686
+ flex-direction: column;
1687
+ gap: 2px;
1688
+ }
1689
+
1690
+ .files-search-result:hover,
1691
+ .files-search-result.selected {
1692
+ background: var(--bg-tertiary);
1693
+ }
1694
+
1695
+ .files-search-result-name {
1696
+ font-size: 12px;
1697
+ color: var(--text-primary);
1698
+ }
1699
+
1700
+ .files-search-result-path {
1701
+ font-size: 11px;
1702
+ color: var(--text-muted);
1703
+ overflow: hidden;
1704
+ text-overflow: ellipsis;
1705
+ white-space: nowrap;
1706
+ }
1707
+
1708
+ .files-search-highlight {
1709
+ color: var(--accent);
1710
+ font-weight: 500;
1711
+ }
1712
+
1713
+ /* Cmd+P Palette styles (Spec 0058) */
1714
+ .file-palette {
1715
+ position: fixed;
1716
+ inset: 0;
1717
+ z-index: 1000;
1718
+ display: flex;
1719
+ justify-content: center;
1720
+ padding-top: 80px;
1721
+ }
1722
+
1723
+ .file-palette.hidden {
1724
+ display: none;
1725
+ }
1726
+
1727
+ .file-palette-backdrop {
1728
+ position: absolute;
1729
+ inset: 0;
1730
+ background: rgba(0, 0, 0, 0.5);
1731
+ }
1732
+
1733
+ .file-palette-container {
1734
+ position: relative;
1735
+ width: 500px;
1736
+ max-width: 90vw;
1737
+ max-height: 450px;
1738
+ background: var(--bg-secondary);
1739
+ border: 1px solid var(--border);
1740
+ border-radius: 8px;
1741
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
1742
+ display: flex;
1743
+ flex-direction: column;
1744
+ overflow: hidden;
1745
+ }
1746
+
1747
+ .file-palette-input {
1748
+ width: 100%;
1749
+ padding: 14px 16px;
1750
+ background: var(--bg-tertiary);
1751
+ border: none;
1752
+ border-bottom: 1px solid var(--border);
1753
+ font-size: 14px;
1754
+ color: var(--text-primary);
1755
+ outline: none;
1756
+ }
1757
+
1758
+ .file-palette-input::placeholder {
1441
1759
  color: var(--text-muted);
1442
- font-size: 13px;
1443
- padding: 12px;
1444
- text-align: center;
1445
1760
  }
1446
1761
 
1447
- .dashboard-projects {
1762
+ .file-palette-results {
1448
1763
  flex: 1;
1449
1764
  overflow-y: auto;
1450
- padding: 0 16px 16px 16px;
1765
+ max-height: 380px;
1766
+ }
1767
+
1768
+ .file-palette-result {
1769
+ padding: 10px 16px;
1770
+ cursor: pointer;
1771
+ display: flex;
1772
+ flex-direction: column;
1773
+ gap: 2px;
1774
+ }
1775
+
1776
+ .file-palette-result:hover,
1777
+ .file-palette-result.selected {
1778
+ background: var(--bg-tertiary);
1779
+ }
1780
+
1781
+ .file-palette-result-name {
1782
+ font-size: 13px;
1783
+ color: var(--text-primary);
1784
+ }
1785
+
1786
+ .file-palette-result-path {
1787
+ font-size: 12px;
1788
+ color: var(--text-muted);
1789
+ overflow: hidden;
1790
+ text-overflow: ellipsis;
1791
+ white-space: nowrap;
1792
+ }
1793
+
1794
+ .file-palette-empty {
1795
+ padding: 16px;
1796
+ text-align: center;
1797
+ color: var(--text-muted);
1798
+ font-size: 13px;
1799
+ }
1800
+
1801
+ .dashboard-empty-state {
1802
+ color: var(--text-muted);
1803
+ font-size: 13px;
1804
+ padding: 12px;
1805
+ text-align: center;
1451
1806
  }
1452
1807
 
1453
1808
  /* Status indicators in dashboard tab list */
@@ -1485,6 +1840,11 @@
1485
1840
  <body>
1486
1841
  <header class="header">
1487
1842
  <h1>Agent Farm - {{PROJECT_NAME}}</h1>
1843
+ <div class="header-actions">
1844
+ <button class="btn activity-summary-btn" onclick="showActivitySummary()" title="What did I do today?">
1845
+ 🕐 Today
1846
+ </button>
1847
+ </div>
1488
1848
  </header>
1489
1849
 
1490
1850
  <main class="main">
@@ -1506,10 +1866,6 @@
1506
1866
  <span class="overflow-count" id="overflow-count">+0</span>
1507
1867
  </button>
1508
1868
  <div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
1509
- <div class="add-buttons">
1510
- <button class="add-btn" onclick="spawnBuilder()" title="Spawn worktree builder">+ 🔨</button>
1511
- <button class="add-btn" onclick="spawnShell()" title="New shell">+ >_</button>
1512
- </div>
1513
1869
  </div>
1514
1870
  <div class="tab-content" id="tab-content"></div>
1515
1871
  </div>
@@ -1563,6 +1919,7 @@
1563
1919
  <!-- Context menu -->
1564
1920
  <div class="context-menu hidden" id="context-menu" role="menu">
1565
1921
  <div class="context-menu-item" role="menuitem" tabindex="0" data-action="openContextTab" onclick="openContextTab()" onkeydown="handleContextMenuKeydown(event)">Open in New Tab</div>
1922
+ <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="reloadContextTab" id="context-reload" onclick="reloadContextTab()" onkeydown="handleContextMenuKeydown(event)">Reload</div>
1566
1923
  <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeActiveTab" onclick="closeActiveTab()" onkeydown="handleContextMenuKeydown(event)">Close</div>
1567
1924
  <div class="context-menu-item" role="menuitem" tabindex="-1" data-action="closeOtherTabs" onclick="closeOtherTabs()" onkeydown="handleContextMenuKeydown(event)">Close Others</div>
1568
1925
  <div class="context-menu-item danger" role="menuitem" tabindex="-1" data-action="closeAllTabs" onclick="closeAllTabs()" onkeydown="handleContextMenuKeydown(event)">Close All</div>
@@ -1571,6 +1928,40 @@
1571
1928
  <!-- Toast container -->
1572
1929
  <div class="toast-container" id="toast-container"></div>
1573
1930
 
1931
+ <!-- Activity Summary Modal (Spec 0059) -->
1932
+ <div class="dialog-overlay hidden" id="activity-modal">
1933
+ <div class="dialog activity-dialog">
1934
+ <div class="activity-dialog-header">
1935
+ <h3>Today's Summary</h3>
1936
+ <button class="activity-close-btn" onclick="closeActivityModal()" title="Close (Esc)">×</button>
1937
+ </div>
1938
+ <div class="activity-dialog-content" id="activity-content">
1939
+ <div class="activity-loading">
1940
+ <span class="activity-spinner"></span>
1941
+ Loading activity...
1942
+ </div>
1943
+ </div>
1944
+ <div class="activity-dialog-footer">
1945
+ <button class="btn" onclick="copyActivitySummary()">📋 Copy to Clipboard</button>
1946
+ <button class="btn" onclick="closeActivityModal()">Close</button>
1947
+ </div>
1948
+ </div>
1949
+ </div>
1950
+
1951
+ <!-- File search palette (Cmd+P) - Spec 0058 -->
1952
+ <div id="file-palette" class="file-palette hidden">
1953
+ <div class="file-palette-backdrop" onclick="closePalette()"></div>
1954
+ <div class="file-palette-container">
1955
+ <input type="text"
1956
+ id="palette-input"
1957
+ class="file-palette-input"
1958
+ placeholder="Search files by name..."
1959
+ oninput="onPaletteInput(this.value)"
1960
+ onkeydown="onPaletteKeydown(event)" />
1961
+ <div id="palette-results" class="file-palette-results"></div>
1962
+ </div>
1963
+ </div>
1964
+
1574
1965
  <script>
1575
1966
  // STATE_INJECTION_POINT
1576
1967
 
@@ -1588,6 +1979,30 @@
1588
1979
  let pendingCloseTabId = null;
1589
1980
  let contextMenuTabId = null;
1590
1981
 
1982
+ // Collapsible section state (persisted to localStorage)
1983
+ const SECTION_STATE_KEY = 'codev-dashboard-sections';
1984
+ let sectionState = loadSectionState();
1985
+
1986
+ function loadSectionState() {
1987
+ try {
1988
+ const saved = localStorage.getItem(SECTION_STATE_KEY);
1989
+ if (saved) return JSON.parse(saved);
1990
+ } catch (e) { /* ignore */ }
1991
+ return { tabs: true, files: true, projects: true };
1992
+ }
1993
+
1994
+ function saveSectionState() {
1995
+ try {
1996
+ localStorage.setItem(SECTION_STATE_KEY, JSON.stringify(sectionState));
1997
+ } catch (e) { /* ignore */ }
1998
+ }
1999
+
2000
+ function toggleSection(section) {
2001
+ sectionState[section] = !sectionState[section];
2002
+ saveSectionState();
2003
+ renderDashboardTabContent();
2004
+ }
2005
+
1591
2006
  // Initialize
1592
2007
  function init() {
1593
2008
  buildTabsFromState();
@@ -1652,8 +2067,9 @@
1652
2067
  // Check if file is already open
1653
2068
  const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
1654
2069
  if (existingTab) {
1655
- // Just switch to the existing tab
2070
+ // Switch to the existing tab and refresh content
1656
2071
  selectTab(existingTab.id);
2072
+ refreshFileTab(existingTab.id); // Refresh content in case file changed
1657
2073
  showToast(`Switched to ${getFileName(filePath)}`, 'success');
1658
2074
  // TODO: scroll to line if lineNumber provided
1659
2075
  return;
@@ -1703,9 +2119,25 @@
1703
2119
  let filesTreeError = null;
1704
2120
  let filesTreeLoaded = false;
1705
2121
 
2122
+ // File search state (Spec 0058)
2123
+ let filesTreeFlat = []; // Flattened array of {name, path} objects for searching
2124
+ let filesSearchQuery = '';
2125
+ let filesSearchResults = [];
2126
+ let filesSearchIndex = 0;
2127
+ let filesSearchDebounceTimer = null;
2128
+
2129
+ // Cmd+P palette state (Spec 0058)
2130
+ let paletteOpen = false;
2131
+ let paletteQuery = '';
2132
+ let paletteResults = [];
2133
+ let paletteIndex = 0;
2134
+ let paletteDebounceTimer = null;
2135
+
1706
2136
  // Build tabs from initial state
1707
2137
  function buildTabsFromState() {
1708
2138
  const previousTabIds = new Set(tabs.map(t => t.id));
2139
+ // Preserve client-side-only tabs (like activity)
2140
+ const clientSideTabs = tabs.filter(t => t.type === 'activity');
1709
2141
  tabs = [];
1710
2142
 
1711
2143
  // Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
@@ -1716,14 +2148,6 @@
1716
2148
  closeable: false
1717
2149
  });
1718
2150
 
1719
- // Files tab is second and uncloseable (Spec 0055)
1720
- tabs.push({
1721
- id: 'files',
1722
- type: 'files',
1723
- name: 'Files',
1724
- closeable: false
1725
- });
1726
-
1727
2151
  // Add file tabs from annotations
1728
2152
  for (const annotation of state.annotations || []) {
1729
2153
  tabs.push({
@@ -1759,6 +2183,11 @@
1759
2183
  });
1760
2184
  }
1761
2185
 
2186
+ // Re-add preserved client-side tabs
2187
+ for (const tab of clientSideTabs) {
2188
+ tabs.push(tab);
2189
+ }
2190
+
1762
2191
  // Detect new tabs and auto-switch to them (skip projects tab)
1763
2192
  for (const tab of tabs) {
1764
2193
  if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
@@ -1777,10 +2206,13 @@
1777
2206
  }
1778
2207
  }
1779
2208
 
1780
- // Get filename from path
2209
+ // Get filename from path (includes parent dir for context)
1781
2210
  function getFileName(path) {
1782
- const parts = path.split('/');
1783
- return parts[parts.length - 1];
2211
+ const parts = path.split('/').filter(p => p);
2212
+ if (parts.length >= 2) {
2213
+ return parts.slice(-2).join('/');
2214
+ }
2215
+ return parts[parts.length - 1] || path;
1784
2216
  }
1785
2217
 
1786
2218
  // Track current architect port to avoid re-rendering iframe unnecessarily
@@ -1855,7 +2287,7 @@
1855
2287
  // Get tab icon
1856
2288
  function getTabIcon(type) {
1857
2289
  switch (type) {
1858
- case 'dashboard': return '📋';
2290
+ case 'dashboard': return '🏠';
1859
2291
  case 'files': return '📁';
1860
2292
  case 'file': return '📄';
1861
2293
  case 'builder': return '🔨';
@@ -1964,12 +2396,12 @@
1964
2396
  return;
1965
2397
  }
1966
2398
 
1967
- // Handle files tab specially (no iframe, inline content)
1968
- if (tab.type === 'files') {
1969
- if (currentTabType !== 'files') {
1970
- currentTabType = 'files';
2399
+ // Handle activity tab specially (no iframe, inline content)
2400
+ if (tab.type === 'activity') {
2401
+ if (currentTabType !== 'activity') {
2402
+ currentTabType = 'activity';
1971
2403
  currentTabPort = null;
1972
- renderFilesTab();
2404
+ renderActivityTab();
1973
2405
  }
1974
2406
  return;
1975
2407
  }
@@ -1982,6 +2414,22 @@
1982
2414
  }
1983
2415
  }
1984
2416
 
2417
+ // Force refresh the iframe for a file tab (reloads content from server)
2418
+ function refreshFileTab(tabId) {
2419
+ const tab = tabs.find(t => t.id === tabId);
2420
+ if (!tab || tab.type !== 'file' || !tab.port) return;
2421
+
2422
+ // If this tab is currently active, force iframe reload
2423
+ if (activeTabId === tabId) {
2424
+ const content = document.getElementById('tab-content');
2425
+ const iframe = content.querySelector('iframe');
2426
+ if (iframe) {
2427
+ // Add cache-busting query param to force reload
2428
+ iframe.src = `http://localhost:${tab.port}?t=${Date.now()}`;
2429
+ }
2430
+ }
2431
+ }
2432
+
1985
2433
  // Update status bar
1986
2434
  function updateStatusBar() {
1987
2435
  // Architect status
@@ -2260,6 +2708,13 @@
2260
2708
  menu.style.top = event.clientY + 'px';
2261
2709
  menu.classList.remove('hidden');
2262
2710
 
2711
+ // Show/hide reload option based on tab type
2712
+ const tab = tabs.find(t => t.id === tabId);
2713
+ const reloadItem = document.getElementById('context-reload');
2714
+ if (reloadItem) {
2715
+ reloadItem.style.display = (tab && tab.type === 'file') ? 'block' : 'none';
2716
+ }
2717
+
2263
2718
  // Focus first item for keyboard navigation
2264
2719
  const firstItem = menu.querySelector('.context-menu-item');
2265
2720
  if (firstItem) firstItem.focus();
@@ -2270,6 +2725,15 @@
2270
2725
  }, 0);
2271
2726
  }
2272
2727
 
2728
+ // Reload file tab content
2729
+ function reloadContextTab() {
2730
+ if (contextMenuTabId) {
2731
+ refreshFileTab(contextMenuTabId);
2732
+ showToast('Reloaded', 'success');
2733
+ }
2734
+ hideContextMenu();
2735
+ }
2736
+
2273
2737
  function hideContextMenu() {
2274
2738
  document.getElementById('context-menu').classList.add('hidden');
2275
2739
  contextMenuTabId = null;
@@ -2534,6 +2998,11 @@
2534
2998
  hideCloseDialog();
2535
2999
  hideContextMenu();
2536
3000
  hideOverflowMenu();
3001
+ // Activity modal (Spec 0059)
3002
+ const activityModal = document.getElementById('activity-modal');
3003
+ if (activityModal && !activityModal.classList.contains('hidden')) {
3004
+ closeActivityModal();
3005
+ }
2537
3006
  }
2538
3007
 
2539
3008
  // Enter in dialogs
@@ -3172,58 +3641,61 @@
3172
3641
  filesTreeData = await response.json();
3173
3642
  filesTreeError = null;
3174
3643
  filesTreeLoaded = true;
3644
+ // Flatten tree for search (Spec 0058)
3645
+ filesTreeFlat = flattenFilesTree(filesTreeData);
3175
3646
  } catch (err) {
3176
3647
  console.error('Failed to load files tree:', err);
3177
3648
  filesTreeError = 'Could not load file tree: ' + err.message;
3178
3649
  filesTreeData = [];
3650
+ filesTreeFlat = [];
3179
3651
  }
3180
3652
  }
3181
3653
 
3182
- // Render the files tab (entry point)
3183
- // Only fetches on first load; use refreshFilesTree() to force reload
3184
- async function renderFilesTab() {
3185
- const content = document.getElementById('tab-content');
3186
-
3187
- // If already loaded, just render cached data (no network request)
3188
- if (filesTreeLoaded) {
3189
- renderFilesTabContent();
3190
- return;
3654
+ // Flatten the file tree into a searchable array (Spec 0058)
3655
+ function flattenFilesTree(nodes, result = []) {
3656
+ for (const node of nodes) {
3657
+ if (node.type === 'file') {
3658
+ result.push({ name: node.name, path: node.path });
3659
+ } else if (node.children) {
3660
+ flattenFilesTree(node.children, result);
3661
+ }
3191
3662
  }
3192
-
3193
- // First load - show loading state and fetch
3194
- content.innerHTML = '<div class="files-loading">Loading files...</div>';
3195
- await loadFilesTree();
3196
- renderFilesTabContent();
3663
+ return result;
3197
3664
  }
3198
3665
 
3199
- // Render the files tab content (internal - called after data is loaded)
3200
- function renderFilesTabContent() {
3201
- const content = document.getElementById('tab-content');
3666
+ // Search files with relevance sorting (Spec 0058)
3667
+ function searchFiles(query) {
3668
+ if (!query) return [];
3669
+ const q = query.toLowerCase();
3202
3670
 
3203
- if (filesTreeError) {
3204
- content.innerHTML = `
3205
- <div class="files-container">
3206
- <div class="files-error">${escapeHtml(filesTreeError)}</div>
3207
- </div>
3208
- `;
3209
- return;
3210
- }
3671
+ const matches = filesTreeFlat.filter(f =>
3672
+ f.path.toLowerCase().includes(q)
3673
+ );
3211
3674
 
3212
- content.innerHTML = `
3213
- <div class="files-container">
3214
- <div class="files-header">
3215
- <span class="files-header-title">Explorer</span>
3216
- <div class="files-header-actions">
3217
- <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3218
- <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3219
- <button onclick="refreshFilesTree()" title="Refresh">↻</button>
3220
- </div>
3221
- </div>
3222
- <div class="files-tree" id="files-tree">
3223
- ${renderTreeNodes(filesTreeData, 0)}
3224
- </div>
3225
- </div>
3226
- `;
3675
+ // Sort by relevance: exact filename > filename prefix > filename contains > path
3676
+ matches.sort((a, b) => {
3677
+ const aName = a.name.toLowerCase();
3678
+ const bName = b.name.toLowerCase();
3679
+ const aPath = a.path.toLowerCase();
3680
+ const bPath = b.path.toLowerCase();
3681
+
3682
+ // Exact filename match first
3683
+ if (aName === q && bName !== q) return -1;
3684
+ if (bName === q && aName !== q) return 1;
3685
+
3686
+ // Filename starts with query
3687
+ if (aName.startsWith(q) && !bName.startsWith(q)) return -1;
3688
+ if (bName.startsWith(q) && !aName.startsWith(q)) return 1;
3689
+
3690
+ // Filename contains query
3691
+ if (aName.includes(q) && !bName.includes(q)) return -1;
3692
+ if (bName.includes(q) && !aName.includes(q)) return 1;
3693
+
3694
+ // Alphabetical by path
3695
+ return aPath.localeCompare(bPath);
3696
+ });
3697
+
3698
+ return matches.slice(0, 15);
3227
3699
  }
3228
3700
 
3229
3701
  // Escape a string for use inside a JavaScript string literal in onclick handlers
@@ -3311,16 +3783,273 @@
3311
3783
  // Re-render file browser in current context (dashboard or files tab)
3312
3784
  function rerenderFilesBrowser() {
3313
3785
  if (activeTabId === 'dashboard') {
3314
- // Re-render just the files column in dashboard
3315
- const filesListEl = document.getElementById('dashboard-files-list');
3316
- if (filesListEl) {
3317
- filesListEl.innerHTML = renderDashboardFilesBrowser();
3786
+ // Re-render just the files content in dashboard
3787
+ const filesContentEl = document.getElementById('dashboard-files-content');
3788
+ if (filesContentEl) {
3789
+ filesContentEl.innerHTML = filesSearchQuery
3790
+ ? renderFilesSearchResults()
3791
+ : renderDashboardFilesBrowserWithWrapper();
3792
+ }
3793
+ }
3794
+ }
3795
+
3796
+ // Wrapper for file browser that includes the list element ID (Spec 0058)
3797
+ function renderDashboardFilesBrowserWithWrapper() {
3798
+ return `<div class="dashboard-files-list" id="dashboard-files-list">${renderDashboardFilesBrowser()}</div>`;
3799
+ }
3800
+
3801
+ // ========================================
3802
+ // File Search Functions (Spec 0058)
3803
+ // ========================================
3804
+
3805
+ // Debounced search input handler for Files column
3806
+ function onFilesSearchInput(value) {
3807
+ clearTimeout(filesSearchDebounceTimer);
3808
+ filesSearchDebounceTimer = setTimeout(() => {
3809
+ filesSearchQuery = value;
3810
+ filesSearchResults = searchFiles(value);
3811
+ filesSearchIndex = 0;
3812
+ rerenderFilesSearch();
3813
+ }, 100);
3814
+ }
3815
+
3816
+ // Clear files search and restore tree view
3817
+ function clearFilesSearch() {
3818
+ filesSearchQuery = '';
3819
+ filesSearchResults = [];
3820
+ filesSearchIndex = 0;
3821
+ const input = document.getElementById('files-search-input');
3822
+ if (input) {
3823
+ input.value = '';
3824
+ }
3825
+ rerenderFilesSearch();
3826
+ }
3827
+
3828
+ // Re-render the files search area (results or tree)
3829
+ function rerenderFilesSearch() {
3830
+ const filesContentEl = document.getElementById('dashboard-files-content');
3831
+ if (filesContentEl) {
3832
+ filesContentEl.innerHTML = filesSearchQuery
3833
+ ? renderFilesSearchResults()
3834
+ : renderDashboardFilesBrowserWithWrapper();
3835
+ }
3836
+ // Update clear button visibility
3837
+ const clearBtn = document.querySelector('.files-search-clear');
3838
+ if (clearBtn) {
3839
+ clearBtn.classList.toggle('hidden', !filesSearchQuery);
3840
+ }
3841
+ }
3842
+
3843
+ // Render search results for Files column
3844
+ function renderFilesSearchResults() {
3845
+ if (!filesSearchResults.length) {
3846
+ return '<div class="dashboard-empty-state">No files found</div>';
3847
+ }
3848
+
3849
+ return `<div class="files-search-results">${filesSearchResults.map((file, index) =>
3850
+ renderSearchResult(file, index, index === filesSearchIndex, filesSearchQuery, 'files')
3851
+ ).join('')}</div>`;
3852
+ }
3853
+
3854
+ // Highlight matching text in search results
3855
+ function highlightMatch(text, query) {
3856
+ if (!query) return escapeHtml(text);
3857
+ const q = query.toLowerCase();
3858
+ const t = text.toLowerCase();
3859
+ const idx = t.indexOf(q);
3860
+ if (idx === -1) return escapeHtml(text);
3861
+
3862
+ return escapeHtml(text.substring(0, idx)) +
3863
+ '<span class="files-search-highlight">' + escapeHtml(text.substring(idx, idx + query.length)) + '</span>' +
3864
+ escapeHtml(text.substring(idx + query.length));
3865
+ }
3866
+
3867
+ // Render a single search result (shared by Files column and palette)
3868
+ function renderSearchResult(file, index, isSelected, query, context) {
3869
+ const classPrefix = context === 'palette' ? 'file-palette' : 'files-search';
3870
+ const jsPath = escapeJsString(file.path);
3871
+
3872
+ return `
3873
+ <div class="${classPrefix}-result ${isSelected ? 'selected' : ''}"
3874
+ data-index="${index}"
3875
+ onclick="openFileFromSearch('${jsPath}', '${context}')">
3876
+ <div class="${classPrefix}-result-name">${highlightMatch(file.name, query)}</div>
3877
+ <div class="${classPrefix}-result-path">${highlightMatch(file.path, query)}</div>
3878
+ </div>
3879
+ `;
3880
+ }
3881
+
3882
+ // Keyboard handler for Files search input
3883
+ function onFilesSearchKeydown(event) {
3884
+ if (!filesSearchResults.length) {
3885
+ if (event.key === 'Escape') {
3886
+ clearFilesSearch();
3887
+ event.target.blur();
3888
+ }
3889
+ return;
3890
+ }
3891
+
3892
+ if (event.key === 'ArrowDown') {
3893
+ event.preventDefault();
3894
+ filesSearchIndex = Math.min(filesSearchIndex + 1, filesSearchResults.length - 1);
3895
+ rerenderFilesSearch();
3896
+ scrollSelectedIntoView('files');
3897
+ } else if (event.key === 'ArrowUp') {
3898
+ event.preventDefault();
3899
+ filesSearchIndex = Math.max(filesSearchIndex - 1, 0);
3900
+ rerenderFilesSearch();
3901
+ scrollSelectedIntoView('files');
3902
+ } else if (event.key === 'Enter') {
3903
+ event.preventDefault();
3904
+ if (filesSearchResults[filesSearchIndex]) {
3905
+ openFileFromSearch(filesSearchResults[filesSearchIndex].path, 'files');
3906
+ }
3907
+ } else if (event.key === 'Escape') {
3908
+ clearFilesSearch();
3909
+ event.target.blur();
3910
+ }
3911
+ }
3912
+
3913
+ // Scroll selected result into view
3914
+ function scrollSelectedIntoView(context) {
3915
+ const selector = context === 'palette'
3916
+ ? '.file-palette-result.selected'
3917
+ : '.files-search-result.selected';
3918
+ const selected = document.querySelector(selector);
3919
+ if (selected) {
3920
+ selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
3921
+ }
3922
+ }
3923
+
3924
+ // Open file from search result (shared by Files column and palette)
3925
+ function openFileFromSearch(filePath, context) {
3926
+ // Check if file is already open
3927
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3928
+ if (existingTab) {
3929
+ selectTab(existingTab.id);
3930
+ refreshFileTab(existingTab.id); // Refresh content in case file changed
3931
+ } else {
3932
+ openFileFromTree(filePath);
3933
+ }
3934
+
3935
+ // Clear search / close palette
3936
+ if (context === 'palette') {
3937
+ closePalette();
3938
+ } else {
3939
+ clearFilesSearch();
3940
+ }
3941
+ }
3942
+
3943
+ // ========================================
3944
+ // Cmd+P Palette Functions (Spec 0058)
3945
+ // ========================================
3946
+
3947
+ // Open the file search palette
3948
+ function openPalette() {
3949
+ paletteOpen = true;
3950
+ paletteQuery = '';
3951
+ paletteResults = [];
3952
+ paletteIndex = 0;
3953
+ document.getElementById('file-palette').classList.remove('hidden');
3954
+ const input = document.getElementById('palette-input');
3955
+ input.value = '';
3956
+ input.focus();
3957
+ rerenderPaletteResults();
3958
+ }
3959
+
3960
+ // Close the file search palette
3961
+ function closePalette() {
3962
+ paletteOpen = false;
3963
+ paletteQuery = '';
3964
+ paletteResults = [];
3965
+ paletteIndex = 0;
3966
+ document.getElementById('file-palette').classList.add('hidden');
3967
+ }
3968
+
3969
+ // Debounced palette input handler
3970
+ function onPaletteInput(value) {
3971
+ clearTimeout(paletteDebounceTimer);
3972
+ paletteDebounceTimer = setTimeout(() => {
3973
+ paletteQuery = value;
3974
+ paletteResults = searchFiles(value);
3975
+ paletteIndex = 0;
3976
+ rerenderPaletteResults();
3977
+ }, 100);
3978
+ }
3979
+
3980
+ // Re-render palette results
3981
+ function rerenderPaletteResults() {
3982
+ const resultsEl = document.getElementById('palette-results');
3983
+ if (!resultsEl) return;
3984
+
3985
+ if (!paletteQuery) {
3986
+ resultsEl.innerHTML = '<div class="file-palette-empty">Type to search files...</div>';
3987
+ return;
3988
+ }
3989
+
3990
+ if (!paletteResults.length) {
3991
+ resultsEl.innerHTML = '<div class="file-palette-empty">No files found</div>';
3992
+ return;
3993
+ }
3994
+
3995
+ resultsEl.innerHTML = paletteResults.map((file, index) =>
3996
+ renderSearchResult(file, index, index === paletteIndex, paletteQuery, 'palette')
3997
+ ).join('');
3998
+ }
3999
+
4000
+ // Keyboard handler for palette input
4001
+ function onPaletteKeydown(event) {
4002
+ if (event.key === 'Escape') {
4003
+ closePalette();
4004
+ return;
4005
+ }
4006
+
4007
+ if (!paletteResults.length) return;
4008
+
4009
+ if (event.key === 'ArrowDown') {
4010
+ event.preventDefault();
4011
+ paletteIndex = Math.min(paletteIndex + 1, paletteResults.length - 1);
4012
+ rerenderPaletteResults();
4013
+ scrollSelectedIntoView('palette');
4014
+ } else if (event.key === 'ArrowUp') {
4015
+ event.preventDefault();
4016
+ paletteIndex = Math.max(paletteIndex - 1, 0);
4017
+ rerenderPaletteResults();
4018
+ scrollSelectedIntoView('palette');
4019
+ } else if (event.key === 'Enter') {
4020
+ event.preventDefault();
4021
+ if (paletteResults[paletteIndex]) {
4022
+ openFileFromSearch(paletteResults[paletteIndex].path, 'palette');
3318
4023
  }
3319
- } else if (activeTabId === 'files') {
3320
- renderFilesTabContent();
3321
4024
  }
3322
4025
  }
3323
4026
 
4027
+ // Global keyboard handler for Cmd+P / Ctrl+P and Escape
4028
+ document.addEventListener('keydown', (e) => {
4029
+ // Global Escape handler for palette (works even if input loses focus)
4030
+ if (e.key === 'Escape' && paletteOpen) {
4031
+ closePalette();
4032
+ return;
4033
+ }
4034
+
4035
+ // Cmd+P (macOS) or Ctrl+P (Windows/Linux)
4036
+ if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
4037
+ // Skip if user is typing in an input/textarea (except our search inputs)
4038
+ const active = document.activeElement;
4039
+ const isOurInput = active?.id === 'palette-input' || active?.id === 'files-search-input';
4040
+ const isEditable = active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA' || active?.isContentEditable;
4041
+
4042
+ if (!isOurInput && isEditable) return; // Let native behavior happen
4043
+
4044
+ e.preventDefault(); // Prevent browser Print dialog
4045
+ if (paletteOpen) {
4046
+ closePalette();
4047
+ } else {
4048
+ openPalette();
4049
+ }
4050
+ }
4051
+ });
4052
+
3324
4053
  // Collapse all folders
3325
4054
  function collapseAllFolders() {
3326
4055
  filesTreeExpanded.clear();
@@ -3357,6 +4086,7 @@
3357
4086
  const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3358
4087
  if (existingTab) {
3359
4088
  selectTab(existingTab.id);
4089
+ refreshFileTab(existingTab.id); // Refresh content in case file changed
3360
4090
  return;
3361
4091
  }
3362
4092
 
@@ -3394,10 +4124,9 @@
3394
4124
  function renderInfoHeader() {
3395
4125
  return `
3396
4126
  <div class="projects-info">
3397
- <h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Codev: Project View</h1>
3398
- <p>This shows the state of all projects. Our goal is to move each project through all the stages until it reaches INTGR'D (integrated). Hover over column headers to learn about each stage.</p>
3399
- <p>To add projects, update status, or approve stages, use the <strong>Architect</strong> terminal on the left.</p>
3400
- <p>Docs: <a href="#" onclick="openProjectFile('codev/resources/cheatsheet.md'); return false;">Cheatsheet</a> · <a href="#" onclick="openProjectFile('codev/docs/lifecycle.md'); return false;">Lifecycle</a> · <a href="#" onclick="openProjectFile('codev/docs/commands/overview.md'); return false;">CLI Reference</a> · <a href="#" onclick="openProjectFile('codev/protocols/spider/protocol.md'); return false;">SPIDER Protocol</a> · <a href="https://github.com/cluesmith/codev#readme" target="_blank">README</a></p>
4127
+ <h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Agent Farm Dashboard</h1>
4128
+ <p>Coordinate AI builders working on your codebase. The left panel shows the Architect terminal tell it what you want to build. <strong>Tabs</strong> shows open terminals (Architect, Builders, utility shells). <strong>Files</strong> lets you browse and open project files. <strong>Projects</strong> tracks work as it moves from conception to integration.</p>
4129
+ <p>Docs: <a href="#" onclick="openProjectFile('codev/resources/cheatsheet.md'); return false;">Cheatsheet</a> · <a href="#" onclick="openProjectFile('codev/resources/lifecycle.md'); return false;">Lifecycle</a> · <a href="#" onclick="openProjectFile('codev/resources/commands/overview.md'); return false;">CLI Reference</a> · <a href="#" onclick="openProjectFile('codev/protocols/spider/protocol.md'); return false;">SPIDER Protocol</a> · <a href="https://github.com/cluesmith/codev#readme" target="_blank">README</a> · <a href="https://discord.gg/mJ92DhDa6n" target="_blank">Discord</a></p>
3401
4130
  </div>
3402
4131
  `;
3403
4132
  }
@@ -3408,36 +4137,60 @@
3408
4137
 
3409
4138
  content.innerHTML = `
3410
4139
  <div class="dashboard-container">
4140
+ ${renderInfoHeader()}
3411
4141
  <div class="dashboard-header">
3412
- <!-- Left Column: Tabs -->
3413
- <div class="dashboard-column">
3414
- <div class="dashboard-column-header">
3415
- <h3>Tabs</h3>
3416
- </div>
3417
- <div class="dashboard-tabs-list" id="dashboard-tabs-list">
3418
- ${renderDashboardTabsList()}
4142
+ <!-- Tabs Section -->
4143
+ <div class="dashboard-section section-tabs ${sectionState.tabs ? '' : 'collapsed'}">
4144
+ <div class="dashboard-section-header" onclick="toggleSection('tabs')">
4145
+ <h3><span class="collapse-icon">▼</span> Tabs</h3>
4146
+ <div class="header-actions" onclick="event.stopPropagation()">
4147
+ <button onclick="spawnBuilder()" title="New Worktree">+ Worktree</button>
4148
+ <button onclick="spawnShell()" title="New Shell">+ Shell</button>
4149
+ </div>
3419
4150
  </div>
3420
- <div class="dashboard-actions">
3421
- <button class="btn-action" onclick="createNewShell()" title="New utility shell">+ New Shell</button>
3422
- <button class="btn-action" onclick="createNewWorktreeShell()" title="New worktree shell">+ New Worktree</button>
4151
+ <div class="dashboard-section-content">
4152
+ <div class="dashboard-tabs-list" id="dashboard-tabs-list">
4153
+ ${renderDashboardTabsList()}
4154
+ </div>
3423
4155
  </div>
3424
4156
  </div>
3425
- <!-- Right Column: Files -->
3426
- <div class="dashboard-column">
3427
- <div class="dashboard-column-header">
3428
- <h3>Files</h3>
3429
- <div class="header-actions">
4157
+ <!-- Files Section -->
4158
+ <div class="dashboard-section section-files ${sectionState.files ? '' : 'collapsed'}">
4159
+ <div class="dashboard-section-header" onclick="toggleSection('files')">
4160
+ <h3><span class="collapse-icon">▼</span> Files</h3>
4161
+ <div class="header-actions" onclick="event.stopPropagation()">
4162
+ <button onclick="refreshFilesTree()" title="Refresh">↻</button>
3430
4163
  <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3431
4164
  <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3432
4165
  </div>
3433
4166
  </div>
3434
- <div class="dashboard-files-list" id="dashboard-files-list">
3435
- ${renderDashboardFilesBrowser()}
4167
+ <div class="dashboard-section-content">
4168
+ <div class="files-search-container" onclick="event.stopPropagation()">
4169
+ <input type="text"
4170
+ id="files-search-input"
4171
+ class="files-search-input"
4172
+ placeholder="Search files by name..."
4173
+ oninput="onFilesSearchInput(this.value)"
4174
+ onkeydown="onFilesSearchKeydown(event)"
4175
+ value="${escapeHtml(filesSearchQuery)}" />
4176
+ <button class="files-search-clear ${filesSearchQuery ? '' : 'hidden'}"
4177
+ onclick="clearFilesSearch()"
4178
+ title="Clear search">×</button>
4179
+ </div>
4180
+ <div id="dashboard-files-content">
4181
+ ${filesSearchQuery ? renderFilesSearchResults() : renderDashboardFilesBrowserWithWrapper()}
4182
+ </div>
3436
4183
  </div>
3437
4184
  </div>
3438
4185
  </div>
3439
- <div class="dashboard-projects" id="dashboard-projects">
3440
- ${renderDashboardProjectsSection()}
4186
+ <!-- Projects Section -->
4187
+ <div class="dashboard-section section-projects ${sectionState.projects ? '' : 'collapsed'}">
4188
+ <div class="dashboard-section-header" onclick="toggleSection('projects')">
4189
+ <h3><span class="collapse-icon">▼</span> Projects</h3>
4190
+ </div>
4191
+ <div class="dashboard-section-content" id="dashboard-projects">
4192
+ ${renderDashboardProjectsSection()}
4193
+ </div>
3441
4194
  </div>
3442
4195
  </div>
3443
4196
  `;
@@ -3516,9 +4269,8 @@
3516
4269
  `;
3517
4270
  }
3518
4271
 
3519
- // Render the existing project view (unchanged)
4272
+ // Render the existing project view
3520
4273
  return `
3521
- ${renderInfoHeader()}
3522
4274
  ${renderKanbanGrid(projectsData)}
3523
4275
  ${renderTerminalProjects(projectsData)}
3524
4276
  `;
@@ -3719,6 +4471,269 @@
3719
4471
  // Start projectlist polling (separate from main state polling)
3720
4472
  setInterval(pollProjectlist, 5000);
3721
4473
 
4474
+ // ========================================
4475
+ // Activity Summary (Spec 0059)
4476
+ // ========================================
4477
+
4478
+ let activityData = null;
4479
+
4480
+ // Show activity summary modal
4481
+ async function showActivitySummary() {
4482
+ // Check if activity tab already exists
4483
+ let activityTab = tabs.find(t => t.type === 'activity');
4484
+
4485
+ if (!activityTab) {
4486
+ // Create new activity tab
4487
+ activityTab = {
4488
+ id: 'activity-today',
4489
+ type: 'activity',
4490
+ name: 'Today'
4491
+ };
4492
+ tabs.push(activityTab);
4493
+ }
4494
+
4495
+ // Switch to activity tab
4496
+ activeTabId = activityTab.id;
4497
+ currentTabType = null; // Force re-render
4498
+ renderTabs();
4499
+ renderTabContent();
4500
+ }
4501
+
4502
+ // Render the activity tab content
4503
+ async function renderActivityTab() {
4504
+ const content = document.getElementById('tab-content');
4505
+
4506
+ // Show loading state
4507
+ content.innerHTML = `
4508
+ <div class="activity-tab-container">
4509
+ <div class="activity-loading">
4510
+ <span class="activity-spinner"></span>
4511
+ Loading activity...
4512
+ </div>
4513
+ </div>
4514
+ `;
4515
+
4516
+ try {
4517
+ const response = await fetch('/api/activity-summary');
4518
+ if (!response.ok) {
4519
+ throw new Error(await response.text());
4520
+ }
4521
+ activityData = await response.json();
4522
+ renderActivityTabContent(activityData);
4523
+ } catch (err) {
4524
+ content.innerHTML = `
4525
+ <div class="activity-tab-container">
4526
+ <div class="activity-error">
4527
+ Failed to load activity: ${escapeHtml(err.message)}
4528
+ </div>
4529
+ </div>
4530
+ `;
4531
+ }
4532
+ }
4533
+
4534
+ // Render activity tab content (similar to modal but in tab)
4535
+ function renderActivityTabContent(data) {
4536
+ const content = document.getElementById('tab-content');
4537
+
4538
+ // Check for zero activity
4539
+ if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
4540
+ content.innerHTML = `
4541
+ <div class="activity-tab-container">
4542
+ <div class="activity-empty">
4543
+ <p>No activity recorded today</p>
4544
+ <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
4545
+ </div>
4546
+ </div>
4547
+ `;
4548
+ return;
4549
+ }
4550
+
4551
+ const hours = Math.floor(data.timeTracking.activeMinutes / 60);
4552
+ const mins = data.timeTracking.activeMinutes % 60;
4553
+ const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
4554
+ const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
4555
+
4556
+ // Format time strings
4557
+ const formatTime = (isoString) => {
4558
+ if (!isoString) return '--';
4559
+ const date = new Date(isoString);
4560
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
4561
+ };
4562
+
4563
+ let html = '<div class="activity-tab-container"><div class="activity-summary">';
4564
+
4565
+ // AI Summary (if available)
4566
+ if (data.aiSummary) {
4567
+ html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
4568
+ }
4569
+
4570
+ // Activity section
4571
+ html += `
4572
+ <div class="activity-section">
4573
+ <h4>Activity</h4>
4574
+ <ul>
4575
+ <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
4576
+ <li>${data.files.length} files modified</li>
4577
+ <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
4578
+ </ul>
4579
+ </div>
4580
+ `;
4581
+
4582
+ // Projects section (if any status changes)
4583
+ if (data.projectChanges && data.projectChanges.length > 0) {
4584
+ html += `
4585
+ <div class="activity-section">
4586
+ <h4>Projects Touched</h4>
4587
+ <ul>
4588
+ ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
4589
+ </ul>
4590
+ </div>
4591
+ `;
4592
+ }
4593
+
4594
+ // Time section
4595
+ html += `
4596
+ <div class="activity-section">
4597
+ <h4>Time</h4>
4598
+ <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
4599
+ <p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
4600
+ <p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
4601
+ </div>
4602
+ `;
4603
+
4604
+ // Copy button
4605
+ html += `
4606
+ <div class="activity-actions">
4607
+ <button class="btn" onclick="copyActivityToClipboard()">Copy to Clipboard</button>
4608
+ </div>
4609
+ `;
4610
+
4611
+ html += '</div></div>';
4612
+ content.innerHTML = html;
4613
+ }
4614
+
4615
+ // Render activity summary content
4616
+ function renderActivitySummary(data) {
4617
+ const content = document.getElementById('activity-content');
4618
+
4619
+ // Check for zero activity
4620
+ if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
4621
+ content.innerHTML = `
4622
+ <div class="activity-empty">
4623
+ <p>No activity recorded today</p>
4624
+ <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
4625
+ </div>
4626
+ `;
4627
+ return;
4628
+ }
4629
+
4630
+ const hours = Math.floor(data.timeTracking.activeMinutes / 60);
4631
+ const mins = data.timeTracking.activeMinutes % 60;
4632
+ const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
4633
+ const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
4634
+
4635
+ // Format time strings
4636
+ const formatTime = (isoString) => {
4637
+ if (!isoString) return '--';
4638
+ const date = new Date(isoString);
4639
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
4640
+ };
4641
+
4642
+ let html = '<div class="activity-summary">';
4643
+
4644
+ // AI Summary (if available)
4645
+ if (data.aiSummary) {
4646
+ html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
4647
+ }
4648
+
4649
+ // Activity section
4650
+ html += `
4651
+ <div class="activity-section">
4652
+ <h4>Activity</h4>
4653
+ <ul>
4654
+ <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
4655
+ <li>${data.files.length} files modified</li>
4656
+ <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
4657
+ </ul>
4658
+ </div>
4659
+ `;
4660
+
4661
+ // Projects section (if any status changes)
4662
+ if (data.projectChanges && data.projectChanges.length > 0) {
4663
+ html += `
4664
+ <div class="activity-section">
4665
+ <h4>Projects Touched</h4>
4666
+ <ul>
4667
+ ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
4668
+ </ul>
4669
+ </div>
4670
+ `;
4671
+ }
4672
+
4673
+ // Time section
4674
+ html += `
4675
+ <div class="activity-section">
4676
+ <h4>Time</h4>
4677
+ <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
4678
+ <p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
4679
+ <p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
4680
+ </div>
4681
+ `;
4682
+
4683
+ html += '</div>';
4684
+ content.innerHTML = html;
4685
+ }
4686
+
4687
+ // Close activity modal
4688
+ function closeActivityModal() {
4689
+ document.getElementById('activity-modal').classList.add('hidden');
4690
+ }
4691
+
4692
+ // Copy activity summary to clipboard
4693
+ function copyActivitySummary() {
4694
+ if (!activityData) return;
4695
+
4696
+ const hours = Math.floor(activityData.timeTracking.activeMinutes / 60);
4697
+ const mins = activityData.timeTracking.activeMinutes % 60;
4698
+ const uniqueBranches = new Set(activityData.commits.map(c => c.branch)).size;
4699
+ const mergedPrs = activityData.prs.filter(p => p.state === 'MERGED').length;
4700
+
4701
+ let markdown = `## Today's Summary\n\n`;
4702
+
4703
+ if (activityData.aiSummary) {
4704
+ markdown += `${activityData.aiSummary}\n\n`;
4705
+ }
4706
+
4707
+ markdown += `### Activity\n`;
4708
+ markdown += `- ${activityData.commits.length} commits across ${uniqueBranches} branches\n`;
4709
+ markdown += `- ${activityData.files.length} files modified\n`;
4710
+ markdown += `- ${activityData.prs.length} PRs${mergedPrs > 0 ? ` (${mergedPrs} merged)` : ''}\n\n`;
4711
+
4712
+ if (activityData.projectChanges && activityData.projectChanges.length > 0) {
4713
+ markdown += `### Projects Touched\n`;
4714
+ activityData.projectChanges.forEach(p => {
4715
+ markdown += `- ${p.id}: ${p.title} (${p.oldStatus} → ${p.newStatus})\n`;
4716
+ });
4717
+ markdown += '\n';
4718
+ }
4719
+
4720
+ markdown += `### Time\n`;
4721
+ markdown += `Active time: ~${hours}h ${mins}m\n`;
4722
+
4723
+ navigator.clipboard.writeText(markdown).then(() => {
4724
+ showToast('Copied to clipboard', 'success');
4725
+ }).catch(() => {
4726
+ showToast('Failed to copy', 'error');
4727
+ });
4728
+ }
4729
+
4730
+ // Close activity modal when clicking backdrop
4731
+ document.getElementById('activity-modal').addEventListener('click', (e) => {
4732
+ if (e.target.id === 'activity-modal') {
4733
+ closeActivityModal();
4734
+ }
4735
+ });
4736
+
3722
4737
  // Initialize on load
3723
4738
  init();
3724
4739
  </script>