@cluesmith/codev 1.2.3 → 1.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 (80) hide show
  1. package/bin/generate-image.js +7 -0
  2. package/dist/agent-farm/cli.d.ts.map +1 -1
  3. package/dist/agent-farm/cli.js +2 -37
  4. package/dist/agent-farm/cli.js.map +1 -1
  5. package/dist/agent-farm/commands/cleanup.js +12 -51
  6. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  7. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  8. package/dist/agent-farm/commands/spawn.js +13 -1
  9. package/dist/agent-farm/commands/spawn.js.map +1 -1
  10. package/dist/agent-farm/commands/start.js +32 -0
  11. package/dist/agent-farm/commands/start.js.map +1 -1
  12. package/dist/agent-farm/servers/dashboard-server.d.ts +0 -2
  13. package/dist/agent-farm/servers/dashboard-server.d.ts.map +1 -1
  14. package/dist/agent-farm/servers/dashboard-server.js +196 -9
  15. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  16. package/dist/agent-farm/servers/open-server.d.ts +0 -2
  17. package/dist/agent-farm/servers/open-server.d.ts.map +1 -1
  18. package/dist/agent-farm/servers/open-server.js +12 -7
  19. package/dist/agent-farm/servers/open-server.js.map +1 -1
  20. package/dist/agent-farm/servers/tower-server.d.ts +0 -2
  21. package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
  22. package/dist/agent-farm/servers/tower-server.js +16 -4
  23. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  24. package/dist/agent-farm/types.d.ts +1 -0
  25. package/dist/agent-farm/types.d.ts.map +1 -1
  26. package/dist/agent-farm/utils/shell.d.ts +2 -1
  27. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  28. package/dist/agent-farm/utils/shell.js +35 -2
  29. package/dist/agent-farm/utils/shell.js.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +26 -17
  32. package/dist/cli.js.map +1 -1
  33. package/dist/commands/adopt.d.ts.map +1 -1
  34. package/dist/commands/adopt.js +27 -2
  35. package/dist/commands/adopt.js.map +1 -1
  36. package/dist/commands/consult/index.d.ts.map +1 -1
  37. package/dist/commands/consult/index.js +23 -7
  38. package/dist/commands/consult/index.js.map +1 -1
  39. package/dist/commands/doctor.d.ts.map +1 -1
  40. package/dist/commands/doctor.js +51 -0
  41. package/dist/commands/doctor.js.map +1 -1
  42. package/dist/commands/generate-image.d.ts +13 -0
  43. package/dist/commands/generate-image.d.ts.map +1 -0
  44. package/dist/commands/generate-image.js +155 -0
  45. package/dist/commands/generate-image.js.map +1 -0
  46. package/dist/commands/init.d.ts.map +1 -1
  47. package/dist/commands/init.js +23 -2
  48. package/dist/commands/init.js.map +1 -1
  49. package/dist/version.d.ts +3 -0
  50. package/dist/version.d.ts.map +1 -0
  51. package/dist/version.js +23 -0
  52. package/dist/version.js.map +1 -0
  53. package/package.json +5 -3
  54. package/skeleton/DEPENDENCIES.md +3 -3
  55. package/skeleton/protocols/maintain/protocol.md +2 -2
  56. package/skeleton/{docs → resources}/commands/codev.md +0 -39
  57. package/skeleton/{docs → resources}/commands/consult.md +12 -2
  58. package/skeleton/{docs → resources}/commands/overview.md +0 -1
  59. package/skeleton/roles/architect.md +22 -0
  60. package/skeleton/roles/builder.md +22 -0
  61. package/skeleton/templates/arch.md +56 -0
  62. package/skeleton/templates/pr-overview.md +73 -0
  63. package/templates/dashboard-split.html +781 -39
  64. package/templates/open.html +278 -0
  65. package/templates/tower.html +71 -12
  66. package/dist/agent-farm/index.d.ts +0 -7
  67. package/dist/agent-farm/index.d.ts.map +0 -1
  68. package/dist/agent-farm/index.js +0 -373
  69. package/dist/agent-farm/index.js.map +0 -1
  70. package/skeleton/bin/agent-farm +0 -7
  71. package/skeleton/bin/codev-doctor +0 -335
  72. package/skeleton/resources/lessons-learned.md +0 -30
  73. /package/skeleton/{roles/review-types → consult-types}/impl-review.md +0 -0
  74. /package/skeleton/{roles/review-types → consult-types}/integration-review.md +0 -0
  75. /package/skeleton/{roles/review-types → consult-types}/plan-review.md +0 -0
  76. /package/skeleton/{roles/review-types → consult-types}/pr-ready.md +0 -0
  77. /package/skeleton/{roles/review-types → consult-types}/spec-review.md +0 -0
  78. /package/skeleton/{docs → resources}/commands/agent-farm.md +0 -0
  79. /package/skeleton/{AGENTS.md.template → templates/AGENTS.md} +0 -0
  80. /package/skeleton/{CLAUDE.md.template → templates/CLAUDE.md} +0 -0
@@ -1156,6 +1156,330 @@
1156
1156
  .tab.tab-uncloseable .close {
1157
1157
  display: none;
1158
1158
  }
1159
+
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
+
1210
+ .tree-item {
1211
+ display: flex;
1212
+ align-items: center;
1213
+ padding: 4px 8px;
1214
+ cursor: pointer;
1215
+ user-select: none;
1216
+ }
1217
+
1218
+ .tree-item:hover {
1219
+ background: var(--bg-secondary);
1220
+ }
1221
+
1222
+ .tree-item.selected {
1223
+ background: var(--tab-active);
1224
+ }
1225
+
1226
+ .tree-item-icon {
1227
+ width: 16px;
1228
+ height: 16px;
1229
+ margin-right: 4px;
1230
+ display: flex;
1231
+ align-items: center;
1232
+ justify-content: center;
1233
+ font-size: 10px;
1234
+ color: var(--text-muted);
1235
+ }
1236
+
1237
+ .tree-item-icon.folder-toggle {
1238
+ cursor: pointer;
1239
+ }
1240
+
1241
+ .tree-item-icon.folder-toggle:hover {
1242
+ color: var(--text-secondary);
1243
+ }
1244
+
1245
+ .tree-item-name {
1246
+ font-size: 13px;
1247
+ color: var(--text-secondary);
1248
+ overflow: hidden;
1249
+ text-overflow: ellipsis;
1250
+ white-space: nowrap;
1251
+ }
1252
+
1253
+ .tree-item:hover .tree-item-name {
1254
+ color: var(--text-primary);
1255
+ }
1256
+
1257
+ .tree-item[data-type="dir"] .tree-item-name {
1258
+ color: var(--text-primary);
1259
+ }
1260
+
1261
+ .tree-item[data-type="file"]:hover .tree-item-name {
1262
+ color: var(--accent);
1263
+ }
1264
+
1265
+ .tree-children {
1266
+ overflow: hidden;
1267
+ }
1268
+
1269
+ .tree-children.collapsed {
1270
+ display: none;
1271
+ }
1272
+
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
+ /* Dashboard Tab Styles (Spec 0057) */
1293
+ .dashboard-container {
1294
+ flex: 1;
1295
+ overflow-y: auto;
1296
+ display: flex;
1297
+ flex-direction: column;
1298
+ }
1299
+
1300
+ .dashboard-header {
1301
+ display: grid;
1302
+ grid-template-columns: 1fr 1fr;
1303
+ gap: 16px;
1304
+ padding: 16px;
1305
+ flex-shrink: 0;
1306
+ }
1307
+
1308
+ @media (max-width: 900px) {
1309
+ .dashboard-header {
1310
+ grid-template-columns: 1fr;
1311
+ }
1312
+ }
1313
+
1314
+ .dashboard-column {
1315
+ background: var(--bg-secondary);
1316
+ border: 1px solid var(--border);
1317
+ border-radius: 8px;
1318
+ padding: 12px;
1319
+ overflow: hidden;
1320
+ display: flex;
1321
+ flex-direction: column;
1322
+ max-height: 280px;
1323
+ }
1324
+
1325
+ .dashboard-column-header {
1326
+ display: flex;
1327
+ justify-content: space-between;
1328
+ align-items: center;
1329
+ margin-bottom: 8px;
1330
+ flex-shrink: 0;
1331
+ }
1332
+
1333
+ .dashboard-column-header h3 {
1334
+ font-size: 12px;
1335
+ text-transform: uppercase;
1336
+ color: var(--text-muted);
1337
+ letter-spacing: 0.5px;
1338
+ margin: 0;
1339
+ }
1340
+
1341
+ .dashboard-column-header .header-actions {
1342
+ display: flex;
1343
+ gap: 4px;
1344
+ }
1345
+
1346
+ .dashboard-column-header .header-actions button {
1347
+ padding: 4px 8px;
1348
+ border-radius: 4px;
1349
+ border: 1px solid var(--border);
1350
+ background: var(--bg-tertiary);
1351
+ color: var(--text-secondary);
1352
+ cursor: pointer;
1353
+ font-size: 11px;
1354
+ }
1355
+
1356
+ .dashboard-column-header .header-actions button:hover {
1357
+ background: var(--tab-hover);
1358
+ color: var(--text-primary);
1359
+ }
1360
+
1361
+ .dashboard-tabs-list {
1362
+ flex: 1;
1363
+ overflow-y: auto;
1364
+ margin-bottom: 8px;
1365
+ }
1366
+
1367
+ .dashboard-tab-item {
1368
+ display: flex;
1369
+ align-items: center;
1370
+ gap: 8px;
1371
+ padding: 6px 8px;
1372
+ border-radius: 4px;
1373
+ cursor: pointer;
1374
+ font-size: 13px;
1375
+ color: var(--text-secondary);
1376
+ }
1377
+
1378
+ .dashboard-tab-item:hover {
1379
+ background: var(--bg-tertiary);
1380
+ }
1381
+
1382
+ .dashboard-tab-item.active {
1383
+ background: var(--accent);
1384
+ color: white;
1385
+ }
1386
+
1387
+ .dashboard-tab-item .tab-icon {
1388
+ font-size: 14px;
1389
+ flex-shrink: 0;
1390
+ }
1391
+
1392
+ .dashboard-tab-item .tab-name {
1393
+ flex: 1;
1394
+ overflow: hidden;
1395
+ text-overflow: ellipsis;
1396
+ white-space: nowrap;
1397
+ }
1398
+
1399
+ .dashboard-actions {
1400
+ flex-shrink: 0;
1401
+ display: flex;
1402
+ gap: 8px;
1403
+ }
1404
+
1405
+ .dashboard-actions .btn-action {
1406
+ flex: 1;
1407
+ padding: 8px 12px;
1408
+ border-radius: 4px;
1409
+ border: 1px dashed var(--border);
1410
+ background: transparent;
1411
+ color: var(--text-muted);
1412
+ cursor: pointer;
1413
+ font-size: 12px;
1414
+ display: flex;
1415
+ align-items: center;
1416
+ justify-content: center;
1417
+ gap: 4px;
1418
+ }
1419
+
1420
+ .dashboard-actions .btn-action:hover {
1421
+ border-style: solid;
1422
+ color: var(--text-secondary);
1423
+ background: var(--bg-tertiary);
1424
+ }
1425
+
1426
+ .dashboard-files-list {
1427
+ flex: 1;
1428
+ overflow-y: auto;
1429
+ }
1430
+
1431
+ .dashboard-files-list .tree-item {
1432
+ padding: 3px 6px;
1433
+ font-size: 12px;
1434
+ }
1435
+
1436
+ .dashboard-files-list .tree-item-name {
1437
+ font-size: 12px;
1438
+ }
1439
+
1440
+ .dashboard-empty-state {
1441
+ color: var(--text-muted);
1442
+ font-size: 13px;
1443
+ padding: 12px;
1444
+ text-align: center;
1445
+ }
1446
+
1447
+ .dashboard-projects {
1448
+ flex: 1;
1449
+ overflow-y: auto;
1450
+ padding: 0 16px 16px 16px;
1451
+ }
1452
+
1453
+ /* Status indicators in dashboard tab list */
1454
+ .dashboard-status-indicator {
1455
+ width: 8px;
1456
+ height: 8px;
1457
+ border-radius: 50%;
1458
+ flex-shrink: 0;
1459
+ }
1460
+
1461
+ .dashboard-status-working {
1462
+ background: var(--status-active);
1463
+ animation: status-pulse 2s ease-in-out infinite;
1464
+ }
1465
+
1466
+ .dashboard-status-idle {
1467
+ background: var(--status-waiting);
1468
+ animation: status-blink-slow 3s ease-in-out infinite;
1469
+ }
1470
+
1471
+ .dashboard-status-blocked {
1472
+ background: var(--status-error);
1473
+ animation: status-blink-fast 0.8s ease-in-out infinite;
1474
+ }
1475
+
1476
+ @media (prefers-reduced-motion: reduce) {
1477
+ .dashboard-status-working,
1478
+ .dashboard-status-idle,
1479
+ .dashboard-status-blocked {
1480
+ animation: none;
1481
+ }
1482
+ }
1159
1483
  </style>
1160
1484
  </head>
1161
1485
  <body>
@@ -1183,7 +1507,6 @@
1183
1507
  </button>
1184
1508
  <div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
1185
1509
  <div class="add-buttons">
1186
- <button class="add-btn" onclick="showFileDialog()" title="Open file">+ 📄</button>
1187
1510
  <button class="add-btn" onclick="spawnBuilder()" title="Spawn worktree builder">+ 🔨</button>
1188
1511
  <button class="add-btn" onclick="spawnShell()" title="New shell">+ >_</button>
1189
1512
  </div>
@@ -1374,16 +1697,30 @@
1374
1697
  let projectlistError = null;
1375
1698
  let projectlistDebounce = null;
1376
1699
 
1700
+ // Files tab state (Spec 0055)
1701
+ let filesTreeData = [];
1702
+ let filesTreeExpanded = new Set(); // Set of expanded folder paths
1703
+ let filesTreeError = null;
1704
+ let filesTreeLoaded = false;
1705
+
1377
1706
  // Build tabs from initial state
1378
1707
  function buildTabsFromState() {
1379
1708
  const previousTabIds = new Set(tabs.map(t => t.id));
1380
1709
  tabs = [];
1381
1710
 
1382
- // Projects tab is ALWAYS first and uncloseable (Spec 0045)
1711
+ // Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
1383
1712
  tabs.push({
1384
- id: 'projects',
1385
- type: 'projects',
1386
- name: 'Projects',
1713
+ id: 'dashboard',
1714
+ type: 'dashboard',
1715
+ name: 'Dashboard',
1716
+ closeable: false
1717
+ });
1718
+
1719
+ // Files tab is second and uncloseable (Spec 0055)
1720
+ tabs.push({
1721
+ id: 'files',
1722
+ type: 'files',
1723
+ name: 'Files',
1387
1724
  closeable: false
1388
1725
  });
1389
1726
 
@@ -1424,7 +1761,7 @@
1424
1761
 
1425
1762
  // Detect new tabs and auto-switch to them (skip projects tab)
1426
1763
  for (const tab of tabs) {
1427
- if (tab.id !== 'projects' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1764
+ if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1428
1765
  // This is a new tab - switch to it
1429
1766
  activeTabId = tab.id;
1430
1767
  break;
@@ -1434,9 +1771,9 @@
1434
1771
  // Update known tab IDs
1435
1772
  knownTabIds = new Set(tabs.map(t => t.id));
1436
1773
 
1437
- // Set active tab to Projects on first load if none selected
1774
+ // Set active tab to Dashboard on first load if none selected
1438
1775
  if (!activeTabId) {
1439
- activeTabId = 'projects';
1776
+ activeTabId = 'dashboard';
1440
1777
  }
1441
1778
  }
1442
1779
 
@@ -1518,7 +1855,8 @@
1518
1855
  // Get tab icon
1519
1856
  function getTabIcon(type) {
1520
1857
  switch (type) {
1521
- case 'projects': return '📋';
1858
+ case 'dashboard': return '📋';
1859
+ case 'files': return '📁';
1522
1860
  case 'file': return '📄';
1523
1861
  case 'builder': return '🔨';
1524
1862
  case 'shell': return '>_';
@@ -1616,12 +1954,22 @@
1616
1954
  return;
1617
1955
  }
1618
1956
 
1619
- // Handle projects tab specially (no iframe, inline content)
1620
- if (tab.type === 'projects') {
1621
- if (currentTabType !== 'projects') {
1622
- currentTabType = 'projects';
1957
+ // Handle dashboard tab specially (no iframe, inline content)
1958
+ if (tab.type === 'dashboard') {
1959
+ if (currentTabType !== 'dashboard') {
1960
+ currentTabType = 'dashboard';
1623
1961
  currentTabPort = null;
1624
- renderProjectsTab();
1962
+ renderDashboardTab();
1963
+ }
1964
+ return;
1965
+ }
1966
+
1967
+ // Handle files tab specially (no iframe, inline content)
1968
+ if (tab.type === 'files') {
1969
+ if (currentTabType !== 'files') {
1970
+ currentTabType = 'files';
1971
+ currentTabPort = null;
1972
+ renderFilesTab();
1625
1973
  }
1626
1974
  return;
1627
1975
  }
@@ -2810,6 +3158,238 @@
2810
3158
  `;
2811
3159
  }
2812
3160
 
3161
+ // ========================================
3162
+ // Files Tab Functions (Spec 0055)
3163
+ // ========================================
3164
+
3165
+ // Load the file tree from the API
3166
+ async function loadFilesTree() {
3167
+ try {
3168
+ const response = await fetch('/api/files');
3169
+ if (!response.ok) {
3170
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
3171
+ }
3172
+ filesTreeData = await response.json();
3173
+ filesTreeError = null;
3174
+ filesTreeLoaded = true;
3175
+ } catch (err) {
3176
+ console.error('Failed to load files tree:', err);
3177
+ filesTreeError = 'Could not load file tree: ' + err.message;
3178
+ filesTreeData = [];
3179
+ }
3180
+ }
3181
+
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;
3191
+ }
3192
+
3193
+ // First load - show loading state and fetch
3194
+ content.innerHTML = '<div class="files-loading">Loading files...</div>';
3195
+ await loadFilesTree();
3196
+ renderFilesTabContent();
3197
+ }
3198
+
3199
+ // Render the files tab content (internal - called after data is loaded)
3200
+ function renderFilesTabContent() {
3201
+ const content = document.getElementById('tab-content');
3202
+
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
+ }
3211
+
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
+ `;
3227
+ }
3228
+
3229
+ // Escape a string for use inside a JavaScript string literal in onclick handlers
3230
+ // This handles quotes, backslashes, and other special characters
3231
+ function escapeJsString(str) {
3232
+ return str
3233
+ .replace(/\\/g, '\\\\')
3234
+ .replace(/'/g, "\\'")
3235
+ .replace(/"/g, '\\"')
3236
+ .replace(/\n/g, '\\n')
3237
+ .replace(/\r/g, '\\r');
3238
+ }
3239
+
3240
+ // Render tree nodes recursively
3241
+ function renderTreeNodes(nodes, depth) {
3242
+ if (!nodes || nodes.length === 0) return '';
3243
+
3244
+ return nodes.map(node => {
3245
+ const indent = depth * 16;
3246
+ const isExpanded = filesTreeExpanded.has(node.path);
3247
+ // Use escapeJsString for onclick handlers (handles quotes correctly)
3248
+ // Use escapeHtml for data attributes and display text (handles XSS)
3249
+ const jsPath = escapeJsString(node.path);
3250
+
3251
+ if (node.type === 'dir') {
3252
+ const icon = isExpanded ? '▼' : '▶';
3253
+ const childrenHtml = node.children && node.children.length > 0
3254
+ ? `<div class="tree-children ${isExpanded ? '' : 'collapsed'}" data-path="${escapeHtml(node.path)}">${renderTreeNodes(node.children, depth + 1)}</div>`
3255
+ : '';
3256
+
3257
+ return `
3258
+ <div class="tree-item" data-type="dir" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="toggleFolder('${jsPath}')">
3259
+ <span class="tree-item-icon folder-toggle">${icon}</span>
3260
+ <span class="tree-item-name">${escapeHtml(node.name)}</span>
3261
+ </div>
3262
+ ${childrenHtml}
3263
+ `;
3264
+ } else {
3265
+ return `
3266
+ <div class="tree-item" data-type="file" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="openFileFromTree('${jsPath}')">
3267
+ <span class="tree-item-icon">${getFileIcon(node.name)}</span>
3268
+ <span class="tree-item-name">${escapeHtml(node.name)}</span>
3269
+ </div>
3270
+ `;
3271
+ }
3272
+ }).join('');
3273
+ }
3274
+
3275
+ // Get file icon based on extension
3276
+ function getFileIcon(filename) {
3277
+ const ext = filename.split('.').pop().toLowerCase();
3278
+ const iconMap = {
3279
+ 'js': '📜',
3280
+ 'ts': '📜',
3281
+ 'jsx': '⚛️',
3282
+ 'tsx': '⚛️',
3283
+ 'json': '{}',
3284
+ 'md': '📝',
3285
+ 'html': '🌐',
3286
+ 'css': '🎨',
3287
+ 'py': '🐍',
3288
+ 'sh': '⚙️',
3289
+ 'bash': '⚙️',
3290
+ 'yml': '⚙️',
3291
+ 'yaml': '⚙️',
3292
+ 'png': '🖼️',
3293
+ 'jpg': '🖼️',
3294
+ 'jpeg': '🖼️',
3295
+ 'gif': '🖼️',
3296
+ 'svg': '🖼️',
3297
+ };
3298
+ return iconMap[ext] || '📄';
3299
+ }
3300
+
3301
+ // Toggle folder expanded/collapsed state
3302
+ function toggleFolder(path) {
3303
+ if (filesTreeExpanded.has(path)) {
3304
+ filesTreeExpanded.delete(path);
3305
+ } else {
3306
+ filesTreeExpanded.add(path);
3307
+ }
3308
+ rerenderFilesBrowser();
3309
+ }
3310
+
3311
+ // Re-render file browser in current context (dashboard or files tab)
3312
+ function rerenderFilesBrowser() {
3313
+ 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();
3318
+ }
3319
+ } else if (activeTabId === 'files') {
3320
+ renderFilesTabContent();
3321
+ }
3322
+ }
3323
+
3324
+ // Collapse all folders
3325
+ function collapseAllFolders() {
3326
+ filesTreeExpanded.clear();
3327
+ rerenderFilesBrowser();
3328
+ }
3329
+
3330
+ // Expand all folders
3331
+ function expandAllFolders() {
3332
+ function collectPaths(nodes) {
3333
+ for (const node of nodes) {
3334
+ if (node.type === 'dir') {
3335
+ filesTreeExpanded.add(node.path);
3336
+ if (node.children) {
3337
+ collectPaths(node.children);
3338
+ }
3339
+ }
3340
+ }
3341
+ }
3342
+ collectPaths(filesTreeData);
3343
+ rerenderFilesBrowser();
3344
+ }
3345
+
3346
+ // Refresh files tree
3347
+ async function refreshFilesTree() {
3348
+ await loadFilesTree();
3349
+ rerenderFilesBrowser();
3350
+ showToast('Files refreshed', 'success');
3351
+ }
3352
+
3353
+ // Open file from tree click
3354
+ async function openFileFromTree(filePath) {
3355
+ try {
3356
+ // Check if file is already open
3357
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3358
+ if (existingTab) {
3359
+ selectTab(existingTab.id);
3360
+ return;
3361
+ }
3362
+
3363
+ // Open the file via API
3364
+ const response = await fetch('/api/tabs/file', {
3365
+ method: 'POST',
3366
+ headers: { 'Content-Type': 'application/json' },
3367
+ body: JSON.stringify({ path: filePath })
3368
+ });
3369
+
3370
+ if (!response.ok) {
3371
+ throw new Error(await response.text());
3372
+ }
3373
+
3374
+ // Refresh state and switch to the new tab
3375
+ await refresh();
3376
+
3377
+ // Find and select the new file tab
3378
+ const newTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3379
+ if (newTab) {
3380
+ selectTab(newTab.id);
3381
+ }
3382
+
3383
+ showToast(`Opened ${getFileName(filePath)}`, 'success');
3384
+ } catch (err) {
3385
+ showToast('Failed to open file: ' + err.message, 'error');
3386
+ }
3387
+ }
3388
+
3389
+ // ========================================
3390
+ // Projects Tab Functions (Spec 0045)
3391
+ // ========================================
3392
+
2813
3393
  // Render the info header with helpful links
2814
3394
  function renderInfoHeader() {
2815
3395
  return `
@@ -2817,52 +3397,214 @@
2817
3397
  <h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Codev: Project View</h1>
2818
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>
2819
3399
  <p>To add projects, update status, or approve stages, use the <strong>Architect</strong> terminal on the left.</p>
2820
- <p>Docs: <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>
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>
2821
3401
  </div>
2822
3402
  `;
2823
3403
  }
2824
3404
 
2825
- // Render the projects tab content (internal - called after data is loaded)
2826
- function renderProjectsTabContent() {
3405
+ // Render the dashboard tab content (internal - called after data is loaded)
3406
+ function renderDashboardTabContent() {
2827
3407
  const content = document.getElementById('tab-content');
2828
3408
 
2829
- if (projectlistError) {
2830
- content.innerHTML = `
2831
- <div class="projects-container">
2832
- ${renderErrorBanner(projectlistError)}
3409
+ content.innerHTML = `
3410
+ <div class="dashboard-container">
3411
+ <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()}
3419
+ </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>
3423
+ </div>
3424
+ </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">
3430
+ <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3431
+ <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3432
+ </div>
3433
+ </div>
3434
+ <div class="dashboard-files-list" id="dashboard-files-list">
3435
+ ${renderDashboardFilesBrowser()}
3436
+ </div>
3437
+ </div>
3438
+ </div>
3439
+ <div class="dashboard-projects" id="dashboard-projects">
3440
+ ${renderDashboardProjectsSection()}
3441
+ </div>
3442
+ </div>
3443
+ `;
3444
+ }
3445
+
3446
+ // Render the tabs list for dashboard
3447
+ function renderDashboardTabsList() {
3448
+ // Filter to show terminal tabs only (not Dashboard/Files tabs)
3449
+ const terminalTabs = tabs.filter(t => t.type !== 'dashboard' && t.type !== 'files');
3450
+
3451
+ if (terminalTabs.length === 0) {
3452
+ return '<div class="dashboard-empty-state">No tabs open</div>';
3453
+ }
3454
+
3455
+ return terminalTabs.map(tab => {
3456
+ const isActive = tab.id === activeTabId;
3457
+ const icon = getTabIcon(tab.type);
3458
+ const statusIndicator = getDashboardStatusIndicator(tab);
3459
+
3460
+ return `
3461
+ <div class="dashboard-tab-item ${isActive ? 'active' : ''}" onclick="selectTab('${tab.id}')">
3462
+ ${statusIndicator}
3463
+ <span class="tab-icon">${icon}</span>
3464
+ <span class="tab-name">${escapeHtml(tab.name)}</span>
2833
3465
  </div>
2834
3466
  `;
2835
- return;
3467
+ }).join('');
3468
+ }
3469
+
3470
+ // Get status indicator for dashboard tab list
3471
+ function getDashboardStatusIndicator(tab) {
3472
+ if (tab.type !== 'builder') return '';
3473
+
3474
+ // Use builder status from state
3475
+ const builderState = (state.builders || []).find(b => `builder-${b.id}` === tab.id);
3476
+ if (!builderState) return '';
3477
+
3478
+ const status = builderState.status;
3479
+ if (['spawning', 'implementing'].includes(status)) {
3480
+ return '<span class="dashboard-status-indicator dashboard-status-working" title="Working"></span>';
3481
+ }
3482
+ if (status === 'blocked') {
3483
+ return '<span class="dashboard-status-indicator dashboard-status-blocked" title="Blocked"></span>';
3484
+ }
3485
+ if (['pr-ready', 'complete'].includes(status)) {
3486
+ return '<span class="dashboard-status-indicator dashboard-status-idle" title="Idle"></span>';
3487
+ }
3488
+ return '';
3489
+ }
3490
+
3491
+ // Render compact file browser for dashboard
3492
+ function renderDashboardFilesBrowser() {
3493
+ if (filesTreeError) {
3494
+ return `<div class="dashboard-empty-state">${escapeHtml(filesTreeError)}</div>`;
3495
+ }
3496
+
3497
+ if (!filesTreeLoaded || filesTreeData.length === 0) {
3498
+ return '<div class="dashboard-empty-state">Loading files...</div>';
3499
+ }
3500
+
3501
+ return renderTreeNodes(filesTreeData, 0);
3502
+ }
3503
+
3504
+ // Render the projects section for dashboard
3505
+ function renderDashboardProjectsSection() {
3506
+ if (projectlistError) {
3507
+ return renderErrorBanner(projectlistError);
2836
3508
  }
2837
3509
 
2838
3510
  if (projectsData.length === 0) {
2839
- content.innerHTML = `
2840
- <div class="projects-container">
2841
- ${renderWelcomeScreen()}
3511
+ // No welcome screen - just a helpful message
3512
+ return `
3513
+ <div class="dashboard-empty-state" style="padding: 24px;">
3514
+ No projects yet. Ask the Architect to create your first project.
2842
3515
  </div>
2843
3516
  `;
2844
- return;
2845
3517
  }
2846
3518
 
2847
- content.innerHTML = `
2848
- <div class="projects-container">
2849
- ${renderInfoHeader()}
2850
- ${renderKanbanGrid(projectsData)}
2851
- ${renderTerminalProjects(projectsData)}
2852
- </div>
3519
+ // Render the existing project view (unchanged)
3520
+ return `
3521
+ ${renderInfoHeader()}
3522
+ ${renderKanbanGrid(projectsData)}
3523
+ ${renderTerminalProjects(projectsData)}
2853
3524
  `;
2854
3525
  }
2855
3526
 
2856
- // Render the projects tab (entry point - loads data first)
2857
- async function renderProjectsTab() {
3527
+ // Create new utility shell (quick action button)
3528
+ async function createNewShell() {
3529
+ try {
3530
+ const response = await fetch('/api/tabs/shell', { method: 'POST' });
3531
+ const data = await response.json();
3532
+ if (!data.success && data.error) {
3533
+ showToast(data.error || 'Failed to create shell', 'error');
3534
+ return;
3535
+ }
3536
+ await refresh();
3537
+ if (data.id) {
3538
+ selectTab(`shell-${data.id}`);
3539
+ }
3540
+ showToast('Shell created', 'success');
3541
+ } catch (err) {
3542
+ showToast('Network error: ' + err.message, 'error');
3543
+ }
3544
+ }
3545
+
3546
+ // Create new worktree shell (quick action button)
3547
+ async function createNewWorktreeShell() {
3548
+ const branch = prompt('Branch name (leave empty for temp worktree):');
3549
+ if (branch === null) return; // User cancelled
3550
+
3551
+ try {
3552
+ const response = await fetch('/api/tabs/shell', {
3553
+ method: 'POST',
3554
+ headers: { 'Content-Type': 'application/json' },
3555
+ body: JSON.stringify({ worktree: true, branch: branch || undefined })
3556
+ });
3557
+ const data = await response.json();
3558
+ if (!data.success && data.error) {
3559
+ showToast(data.error || 'Failed to create worktree shell', 'error');
3560
+ return;
3561
+ }
3562
+ await refresh();
3563
+ // Auto-select the newly created tab (consistent with createNewShell behavior)
3564
+ if (data.id) {
3565
+ selectTab(`shell-${data.id}`);
3566
+ }
3567
+ showToast('Worktree shell created', 'success');
3568
+ } catch (err) {
3569
+ showToast('Network error: ' + err.message, 'error');
3570
+ }
3571
+ }
3572
+
3573
+ // Render the dashboard tab (entry point - loads data first)
3574
+ async function renderDashboardTab() {
2858
3575
  const content = document.getElementById('tab-content');
2859
- content.innerHTML = '<div class="projects-container"><p style="color: var(--text-muted);">Loading projects...</p></div>';
3576
+ content.innerHTML = '<div class="dashboard-container"><p style="color: var(--text-muted); padding: 16px;">Loading dashboard...</p></div>';
2860
3577
 
2861
- await loadProjectlist();
2862
- renderProjectsTabContent();
3578
+ // Load both projectlist and files tree in parallel
3579
+ await Promise.all([
3580
+ loadProjectlist(),
3581
+ loadFilesTreeIfNeeded()
3582
+ ]);
3583
+
3584
+ renderDashboardTabContent();
2863
3585
  checkStarterMode(); // Update polling state after initial load
2864
3586
  }
2865
3587
 
3588
+ // Load files tree if not already loaded
3589
+ async function loadFilesTreeIfNeeded() {
3590
+ if (!filesTreeLoaded) {
3591
+ await loadFilesTree();
3592
+ }
3593
+ }
3594
+
3595
+ // Legacy function for backward compatibility (still used by polling)
3596
+ function renderProjectsTabContent() {
3597
+ // If dashboard tab is active, re-render dashboard instead
3598
+ if (activeTabId === 'dashboard') {
3599
+ renderDashboardTabContent();
3600
+ }
3601
+ }
3602
+
3603
+ // Legacy function alias
3604
+ async function renderProjectsTab() {
3605
+ await renderDashboardTab();
3606
+ }
3607
+
2866
3608
  // Load projectlist.md from disk
2867
3609
  async function loadProjectlist() {
2868
3610
  try {
@@ -2907,8 +3649,8 @@
2907
3649
 
2908
3650
  // Poll projectlist for changes (every 5 seconds)
2909
3651
  async function pollProjectlist() {
2910
- // Only poll if projects tab is active
2911
- if (activeTabId !== 'projects') return;
3652
+ // Only poll if dashboard tab is active
3653
+ if (activeTabId !== 'dashboard') return;
2912
3654
 
2913
3655
  try {
2914
3656
  const response = await fetch('/file?path=codev/projectlist.md');