@cluesmith/codev 1.2.2 → 1.3.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 (32) 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 +0 -36
  4. package/dist/agent-farm/cli.js.map +1 -1
  5. package/dist/agent-farm/commands/start.js +32 -0
  6. package/dist/agent-farm/commands/start.js.map +1 -1
  7. package/dist/agent-farm/servers/dashboard-server.d.ts +0 -2
  8. package/dist/agent-farm/servers/dashboard-server.d.ts.map +1 -1
  9. package/dist/agent-farm/servers/dashboard-server.js +89 -4
  10. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  11. package/dist/agent-farm/servers/open-server.d.ts +0 -2
  12. package/dist/agent-farm/servers/open-server.d.ts.map +1 -1
  13. package/dist/agent-farm/servers/open-server.js +51 -7
  14. package/dist/agent-farm/servers/open-server.js.map +1 -1
  15. package/dist/agent-farm/servers/tower-server.d.ts +0 -2
  16. package/dist/agent-farm/servers/tower-server.d.ts.map +1 -1
  17. package/dist/agent-farm/servers/tower-server.js +16 -4
  18. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  19. package/dist/agent-farm/utils/shell.d.ts +2 -1
  20. package/dist/agent-farm/utils/shell.d.ts.map +1 -1
  21. package/dist/agent-farm/utils/shell.js +35 -2
  22. package/dist/agent-farm/utils/shell.js.map +1 -1
  23. package/dist/cli.d.ts.map +1 -1
  24. package/dist/cli.js +24 -0
  25. package/dist/cli.js.map +1 -1
  26. package/dist/commands/generate-image.d.ts +13 -0
  27. package/dist/commands/generate-image.d.ts.map +1 -0
  28. package/dist/commands/generate-image.js +155 -0
  29. package/dist/commands/generate-image.js.map +1 -0
  30. package/package.json +5 -3
  31. package/templates/dashboard-split.html +378 -3
  32. package/templates/open.html +48 -1
@@ -1156,6 +1156,138 @@
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
+ }
1159
1291
  </style>
1160
1292
  </head>
1161
1293
  <body>
@@ -1183,7 +1315,6 @@
1183
1315
  </button>
1184
1316
  <div class="overflow-menu hidden" id="overflow-menu" role="menu"></div>
1185
1317
  <div class="add-buttons">
1186
- <button class="add-btn" onclick="showFileDialog()" title="Open file">+ 📄</button>
1187
1318
  <button class="add-btn" onclick="spawnBuilder()" title="Spawn worktree builder">+ 🔨</button>
1188
1319
  <button class="add-btn" onclick="spawnShell()" title="New shell">+ >_</button>
1189
1320
  </div>
@@ -1374,6 +1505,12 @@
1374
1505
  let projectlistError = null;
1375
1506
  let projectlistDebounce = null;
1376
1507
 
1508
+ // Files tab state (Spec 0055)
1509
+ let filesTreeData = [];
1510
+ let filesTreeExpanded = new Set(); // Set of expanded folder paths
1511
+ let filesTreeError = null;
1512
+ let filesTreeLoaded = false;
1513
+
1377
1514
  // Build tabs from initial state
1378
1515
  function buildTabsFromState() {
1379
1516
  const previousTabIds = new Set(tabs.map(t => t.id));
@@ -1387,6 +1524,14 @@
1387
1524
  closeable: false
1388
1525
  });
1389
1526
 
1527
+ // Files tab is second and uncloseable (Spec 0055)
1528
+ tabs.push({
1529
+ id: 'files',
1530
+ type: 'files',
1531
+ name: 'Files',
1532
+ closeable: false
1533
+ });
1534
+
1390
1535
  // Add file tabs from annotations
1391
1536
  for (const annotation of state.annotations || []) {
1392
1537
  tabs.push({
@@ -1424,7 +1569,7 @@
1424
1569
 
1425
1570
  // Detect new tabs and auto-switch to them (skip projects tab)
1426
1571
  for (const tab of tabs) {
1427
- if (tab.id !== 'projects' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1572
+ if (tab.id !== 'projects' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1428
1573
  // This is a new tab - switch to it
1429
1574
  activeTabId = tab.id;
1430
1575
  break;
@@ -1519,6 +1664,7 @@
1519
1664
  function getTabIcon(type) {
1520
1665
  switch (type) {
1521
1666
  case 'projects': return '📋';
1667
+ case 'files': return '📁';
1522
1668
  case 'file': return '📄';
1523
1669
  case 'builder': return '🔨';
1524
1670
  case 'shell': return '>_';
@@ -1626,6 +1772,16 @@
1626
1772
  return;
1627
1773
  }
1628
1774
 
1775
+ // Handle files tab specially (no iframe, inline content)
1776
+ if (tab.type === 'files') {
1777
+ if (currentTabType !== 'files') {
1778
+ currentTabType = 'files';
1779
+ currentTabPort = null;
1780
+ renderFilesTab();
1781
+ }
1782
+ return;
1783
+ }
1784
+
1629
1785
  // For other tabs, only update iframe if port changed (avoid flashing on poll)
1630
1786
  if (currentTabPort !== tab.port || currentTabType !== tab.type) {
1631
1787
  currentTabPort = tab.port;
@@ -2810,6 +2966,225 @@
2810
2966
  `;
2811
2967
  }
2812
2968
 
2969
+ // ========================================
2970
+ // Files Tab Functions (Spec 0055)
2971
+ // ========================================
2972
+
2973
+ // Load the file tree from the API
2974
+ async function loadFilesTree() {
2975
+ try {
2976
+ const response = await fetch('/api/files');
2977
+ if (!response.ok) {
2978
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2979
+ }
2980
+ filesTreeData = await response.json();
2981
+ filesTreeError = null;
2982
+ filesTreeLoaded = true;
2983
+ } catch (err) {
2984
+ console.error('Failed to load files tree:', err);
2985
+ filesTreeError = 'Could not load file tree: ' + err.message;
2986
+ filesTreeData = [];
2987
+ }
2988
+ }
2989
+
2990
+ // Render the files tab (entry point)
2991
+ // Only fetches on first load; use refreshFilesTree() to force reload
2992
+ async function renderFilesTab() {
2993
+ const content = document.getElementById('tab-content');
2994
+
2995
+ // If already loaded, just render cached data (no network request)
2996
+ if (filesTreeLoaded) {
2997
+ renderFilesTabContent();
2998
+ return;
2999
+ }
3000
+
3001
+ // First load - show loading state and fetch
3002
+ content.innerHTML = '<div class="files-loading">Loading files...</div>';
3003
+ await loadFilesTree();
3004
+ renderFilesTabContent();
3005
+ }
3006
+
3007
+ // Render the files tab content (internal - called after data is loaded)
3008
+ function renderFilesTabContent() {
3009
+ const content = document.getElementById('tab-content');
3010
+
3011
+ if (filesTreeError) {
3012
+ content.innerHTML = `
3013
+ <div class="files-container">
3014
+ <div class="files-error">${escapeHtml(filesTreeError)}</div>
3015
+ </div>
3016
+ `;
3017
+ return;
3018
+ }
3019
+
3020
+ content.innerHTML = `
3021
+ <div class="files-container">
3022
+ <div class="files-header">
3023
+ <span class="files-header-title">Explorer</span>
3024
+ <div class="files-header-actions">
3025
+ <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3026
+ <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3027
+ <button onclick="refreshFilesTree()" title="Refresh">↻</button>
3028
+ </div>
3029
+ </div>
3030
+ <div class="files-tree" id="files-tree">
3031
+ ${renderTreeNodes(filesTreeData, 0)}
3032
+ </div>
3033
+ </div>
3034
+ `;
3035
+ }
3036
+
3037
+ // Escape a string for use inside a JavaScript string literal in onclick handlers
3038
+ // This handles quotes, backslashes, and other special characters
3039
+ function escapeJsString(str) {
3040
+ return str
3041
+ .replace(/\\/g, '\\\\')
3042
+ .replace(/'/g, "\\'")
3043
+ .replace(/"/g, '\\"')
3044
+ .replace(/\n/g, '\\n')
3045
+ .replace(/\r/g, '\\r');
3046
+ }
3047
+
3048
+ // Render tree nodes recursively
3049
+ function renderTreeNodes(nodes, depth) {
3050
+ if (!nodes || nodes.length === 0) return '';
3051
+
3052
+ return nodes.map(node => {
3053
+ const indent = depth * 16;
3054
+ const isExpanded = filesTreeExpanded.has(node.path);
3055
+ // Use escapeJsString for onclick handlers (handles quotes correctly)
3056
+ // Use escapeHtml for data attributes and display text (handles XSS)
3057
+ const jsPath = escapeJsString(node.path);
3058
+
3059
+ if (node.type === 'dir') {
3060
+ const icon = isExpanded ? '▼' : '▶';
3061
+ const childrenHtml = node.children && node.children.length > 0
3062
+ ? `<div class="tree-children ${isExpanded ? '' : 'collapsed'}" data-path="${escapeHtml(node.path)}">${renderTreeNodes(node.children, depth + 1)}</div>`
3063
+ : '';
3064
+
3065
+ return `
3066
+ <div class="tree-item" data-type="dir" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="toggleFolder('${jsPath}')">
3067
+ <span class="tree-item-icon folder-toggle">${icon}</span>
3068
+ <span class="tree-item-name">${escapeHtml(node.name)}</span>
3069
+ </div>
3070
+ ${childrenHtml}
3071
+ `;
3072
+ } else {
3073
+ return `
3074
+ <div class="tree-item" data-type="file" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="openFileFromTree('${jsPath}')">
3075
+ <span class="tree-item-icon">${getFileIcon(node.name)}</span>
3076
+ <span class="tree-item-name">${escapeHtml(node.name)}</span>
3077
+ </div>
3078
+ `;
3079
+ }
3080
+ }).join('');
3081
+ }
3082
+
3083
+ // Get file icon based on extension
3084
+ function getFileIcon(filename) {
3085
+ const ext = filename.split('.').pop().toLowerCase();
3086
+ const iconMap = {
3087
+ 'js': '📜',
3088
+ 'ts': '📜',
3089
+ 'jsx': '⚛️',
3090
+ 'tsx': '⚛️',
3091
+ 'json': '{}',
3092
+ 'md': '📝',
3093
+ 'html': '🌐',
3094
+ 'css': '🎨',
3095
+ 'py': '🐍',
3096
+ 'sh': '⚙️',
3097
+ 'bash': '⚙️',
3098
+ 'yml': '⚙️',
3099
+ 'yaml': '⚙️',
3100
+ 'png': '🖼️',
3101
+ 'jpg': '🖼️',
3102
+ 'jpeg': '🖼️',
3103
+ 'gif': '🖼️',
3104
+ 'svg': '🖼️',
3105
+ };
3106
+ return iconMap[ext] || '📄';
3107
+ }
3108
+
3109
+ // Toggle folder expanded/collapsed state
3110
+ function toggleFolder(path) {
3111
+ if (filesTreeExpanded.has(path)) {
3112
+ filesTreeExpanded.delete(path);
3113
+ } else {
3114
+ filesTreeExpanded.add(path);
3115
+ }
3116
+ renderFilesTabContent();
3117
+ }
3118
+
3119
+ // Collapse all folders
3120
+ function collapseAllFolders() {
3121
+ filesTreeExpanded.clear();
3122
+ renderFilesTabContent();
3123
+ }
3124
+
3125
+ // Expand all folders
3126
+ function expandAllFolders() {
3127
+ function collectPaths(nodes) {
3128
+ for (const node of nodes) {
3129
+ if (node.type === 'dir') {
3130
+ filesTreeExpanded.add(node.path);
3131
+ if (node.children) {
3132
+ collectPaths(node.children);
3133
+ }
3134
+ }
3135
+ }
3136
+ }
3137
+ collectPaths(filesTreeData);
3138
+ renderFilesTabContent();
3139
+ }
3140
+
3141
+ // Refresh files tree
3142
+ async function refreshFilesTree() {
3143
+ await loadFilesTree();
3144
+ renderFilesTabContent();
3145
+ showToast('Files refreshed', 'success');
3146
+ }
3147
+
3148
+ // Open file from tree click
3149
+ async function openFileFromTree(filePath) {
3150
+ try {
3151
+ // Check if file is already open
3152
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3153
+ if (existingTab) {
3154
+ selectTab(existingTab.id);
3155
+ return;
3156
+ }
3157
+
3158
+ // Open the file via API
3159
+ const response = await fetch('/api/tabs/file', {
3160
+ method: 'POST',
3161
+ headers: { 'Content-Type': 'application/json' },
3162
+ body: JSON.stringify({ path: filePath })
3163
+ });
3164
+
3165
+ if (!response.ok) {
3166
+ throw new Error(await response.text());
3167
+ }
3168
+
3169
+ // Refresh state and switch to the new tab
3170
+ await refresh();
3171
+
3172
+ // Find and select the new file tab
3173
+ const newTab = tabs.find(t => t.type === 'file' && t.path === filePath);
3174
+ if (newTab) {
3175
+ selectTab(newTab.id);
3176
+ }
3177
+
3178
+ showToast(`Opened ${getFileName(filePath)}`, 'success');
3179
+ } catch (err) {
3180
+ showToast('Failed to open file: ' + err.message, 'error');
3181
+ }
3182
+ }
3183
+
3184
+ // ========================================
3185
+ // Projects Tab Functions (Spec 0045)
3186
+ // ========================================
3187
+
2813
3188
  // Render the info header with helpful links
2814
3189
  function renderInfoHeader() {
2815
3190
  return `
@@ -2817,7 +3192,7 @@
2817
3192
  <h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Codev: Project View</h1>
2818
3193
  <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
3194
  <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>
3195
+ <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
3196
  </div>
2822
3197
  `;
2823
3198
  }
@@ -348,7 +348,7 @@
348
348
  }
349
349
  </style>
350
350
  </head>
351
- <body data-builder="{{BUILDER_ID}}" data-file="{{FILE_PATH}}" data-lang="{{LANG}}" data-is-image="{{IS_IMAGE}}" data-file-size="{{FILE_SIZE}}">
351
+ <body data-builder="{{BUILDER_ID}}" data-file="{{FILE_PATH}}" data-lang="{{LANG}}" data-is-image="{{IS_IMAGE}}" data-is-video="{{IS_VIDEO}}" data-file-size="{{FILE_SIZE}}">
352
352
  <div class="header">
353
353
  <h1 class="path">{{FILE_PATH}}</h1>
354
354
  <div class="subtitle" id="subtitle">Click on a line number to leave an annotation.</div>
@@ -388,6 +388,16 @@
388
388
  </div>
389
389
  </div>
390
390
 
391
+ <!-- Video viewer mode -->
392
+ <div id="video-viewer" style="display: none; padding: 15px; height: calc(100vh - 80px);">
393
+ <div style="display: flex; align-items: center; justify-content: center; height: 100%; background: #222; border-radius: 4px;">
394
+ <video id="video-display" controls style="max-width: 100%; max-height: 100%;">
395
+ Your browser does not support the video tag.
396
+ </video>
397
+ <div id="video-error" style="display: none; color: #ef4444; text-align: center; padding: 40px;"></div>
398
+ </div>
399
+ </div>
400
+
391
401
  <!-- Editor mode -->
392
402
  <textarea id="editor" spellcheck="false"></textarea>
393
403
 
@@ -448,6 +458,9 @@
448
458
  let imageNaturalWidth = 0;
449
459
  let imageNaturalHeight = 0;
450
460
 
461
+ // Video viewer state
462
+ const isVideoFile = {{IS_VIDEO}};
463
+
451
464
  // Comment patterns by file extension
452
465
  const COMMENT_PATTERNS = {
453
466
  js: { prefix: '// REVIEW', regex: /^(\s*)\/\/\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
@@ -525,6 +538,40 @@
525
538
  img.src = '/api/image?t=' + Date.now();
526
539
  }
527
540
 
541
+ // Initialize video viewer
542
+ function initVideo(fileSize) {
543
+ // Hide code view elements
544
+ document.getElementById('viewMode').style.display = 'none';
545
+ document.getElementById('editBtn').style.display = 'none';
546
+ document.getElementById('togglePreviewBtn').style.display = 'none';
547
+
548
+ // Update subtitle
549
+ const sizeStr = formatFileSize(fileSize);
550
+ document.querySelector('.subtitle').textContent = 'Video player · ' + sizeStr;
551
+
552
+ // Show video viewer
553
+ const videoViewer = document.getElementById('video-viewer');
554
+ videoViewer.style.display = 'block';
555
+
556
+ // Load video
557
+ const video = document.getElementById('video-display');
558
+ const videoError = document.getElementById('video-error');
559
+
560
+ video.onloadedmetadata = function() {
561
+ videoError.style.display = 'none';
562
+ video.style.display = 'block';
563
+ };
564
+
565
+ video.onerror = function() {
566
+ videoError.textContent = 'Failed to load video. The file may be corrupted or in an unsupported format.';
567
+ videoError.style.display = 'block';
568
+ video.style.display = 'none';
569
+ };
570
+
571
+ // Add cache-busting query param to allow reload
572
+ video.src = '/api/video?t=' + Date.now();
573
+ }
574
+
528
575
  // Update image info display (in both header and controls)
529
576
  function updateImageInfo(fileSize) {
530
577
  const sizeStr = formatFileSize(fileSize);