@cluesmith/codev 1.4.1 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agent-farm/servers/dashboard-server.js +487 -9
  2. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  3. package/dist/agent-farm/servers/tower-server.js +141 -40
  4. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  5. package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
  6. package/dist/agent-farm/utils/port-registry.js +19 -5
  7. package/dist/agent-farm/utils/port-registry.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +2 -0
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/adopt.d.ts.map +1 -1
  12. package/dist/commands/adopt.js +10 -0
  13. package/dist/commands/adopt.js.map +1 -1
  14. package/dist/commands/consult/index.d.ts +1 -0
  15. package/dist/commands/consult/index.d.ts.map +1 -1
  16. package/dist/commands/consult/index.js +56 -8
  17. package/dist/commands/consult/index.js.map +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +8 -0
  20. package/dist/commands/init.js.map +1 -1
  21. package/package.json +1 -1
  22. package/skeleton/resources/commands/consult.md +50 -0
  23. package/skeleton/templates/projectlist-archive.md +21 -0
  24. package/skeleton/templates/projectlist.md +17 -0
  25. package/templates/dashboard/css/activity.css +151 -0
  26. package/templates/dashboard/css/dialogs.css +149 -0
  27. package/templates/dashboard/css/files.css +530 -0
  28. package/templates/dashboard/css/layout.css +124 -0
  29. package/templates/dashboard/css/projects.css +501 -0
  30. package/templates/dashboard/css/statusbar.css +23 -0
  31. package/templates/dashboard/css/tabs.css +314 -0
  32. package/templates/dashboard/css/utilities.css +50 -0
  33. package/templates/dashboard/css/variables.css +45 -0
  34. package/templates/dashboard/index.html +158 -0
  35. package/templates/dashboard/js/activity.js +238 -0
  36. package/templates/dashboard/js/dialogs.js +328 -0
  37. package/templates/dashboard/js/files.js +436 -0
  38. package/templates/dashboard/js/main.js +487 -0
  39. package/templates/dashboard/js/projects.js +544 -0
  40. package/templates/dashboard/js/state.js +91 -0
  41. package/templates/dashboard/js/tabs.js +500 -0
  42. package/templates/dashboard/js/utils.js +57 -0
  43. package/templates/tower.html +172 -4
  44. package/dist/commands/eject.d.ts +0 -18
  45. package/dist/commands/eject.d.ts.map +0 -1
  46. package/dist/commands/eject.js +0 -149
  47. package/dist/commands/eject.js.map +0 -1
  48. package/templates/dashboard-split.html +0 -3721
  49. package/templates/dashboard.html +0 -149
@@ -0,0 +1,436 @@
1
+ // File Tree Browser and Search (Spec 0055, 0058)
2
+
3
+ // Load the file tree from the API
4
+ async function loadFilesTree() {
5
+ try {
6
+ const response = await fetch('/api/files');
7
+ if (!response.ok) {
8
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
9
+ }
10
+ filesTreeData = await response.json();
11
+ filesTreeError = null;
12
+ filesTreeLoaded = true;
13
+ filesTreeFlat = flattenFilesTree(filesTreeData);
14
+ } catch (err) {
15
+ console.error('Failed to load files tree:', err);
16
+ filesTreeError = 'Could not load file tree: ' + err.message;
17
+ filesTreeData = [];
18
+ filesTreeFlat = [];
19
+ }
20
+ }
21
+
22
+ // Load files tree if not already loaded
23
+ async function loadFilesTreeIfNeeded() {
24
+ if (!filesTreeLoaded) {
25
+ await loadFilesTree();
26
+ }
27
+ }
28
+
29
+ // Flatten the file tree into a searchable array
30
+ function flattenFilesTree(nodes, result = []) {
31
+ for (const node of nodes) {
32
+ if (node.type === 'file') {
33
+ result.push({ name: node.name, path: node.path });
34
+ } else if (node.children) {
35
+ flattenFilesTree(node.children, result);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ // Search files with relevance sorting
42
+ function searchFiles(query) {
43
+ if (!query) return [];
44
+ const q = query.toLowerCase();
45
+
46
+ const matches = filesTreeFlat.filter(f =>
47
+ f.path.toLowerCase().includes(q)
48
+ );
49
+
50
+ matches.sort((a, b) => {
51
+ const aName = a.name.toLowerCase();
52
+ const bName = b.name.toLowerCase();
53
+ const aPath = a.path.toLowerCase();
54
+ const bPath = b.path.toLowerCase();
55
+
56
+ if (aName === q && bName !== q) return -1;
57
+ if (bName === q && aName !== q) return 1;
58
+ if (aName.startsWith(q) && !bName.startsWith(q)) return -1;
59
+ if (bName.startsWith(q) && !aName.startsWith(q)) return 1;
60
+ if (aName.includes(q) && !bName.includes(q)) return -1;
61
+ if (bName.includes(q) && !aName.includes(q)) return 1;
62
+ return aPath.localeCompare(bPath);
63
+ });
64
+
65
+ return matches.slice(0, 15);
66
+ }
67
+
68
+ // Get file icon based on extension
69
+ function getFileIcon(filename) {
70
+ const ext = filename.split('.').pop().toLowerCase();
71
+ const iconMap = {
72
+ 'js': '📜', 'ts': '📜', 'jsx': '⚛️', 'tsx': '⚛️',
73
+ 'json': '{}', 'md': '📝', 'html': '🌐', 'css': '🎨',
74
+ 'py': '🐍', 'sh': '⚙️', 'bash': '⚙️', 'yml': '⚙️', 'yaml': '⚙️',
75
+ 'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
76
+ };
77
+ return iconMap[ext] || '📄';
78
+ }
79
+
80
+ // Render tree nodes recursively
81
+ function renderTreeNodes(nodes, depth) {
82
+ if (!nodes || nodes.length === 0) return '';
83
+
84
+ return nodes.map(node => {
85
+ const indent = depth * 16;
86
+ const isExpanded = filesTreeExpanded.has(node.path);
87
+ const jsPath = escapeJsString(node.path);
88
+
89
+ if (node.type === 'dir') {
90
+ const icon = isExpanded ? '▼' : '▶';
91
+ const childrenHtml = node.children && node.children.length > 0
92
+ ? `<div class="tree-children ${isExpanded ? '' : 'collapsed'}" data-path="${escapeHtml(node.path)}">${renderTreeNodes(node.children, depth + 1)}</div>`
93
+ : '';
94
+
95
+ return `
96
+ <div class="tree-item" data-type="dir" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="toggleFolder('${jsPath}')">
97
+ <span class="tree-item-icon folder-toggle">${icon}</span>
98
+ <span class="tree-item-name">${escapeHtml(node.name)}</span>
99
+ </div>
100
+ ${childrenHtml}
101
+ `;
102
+ } else {
103
+ return `
104
+ <div class="tree-item" data-type="file" data-path="${escapeHtml(node.path)}" style="padding-left: ${indent + 8}px;" onclick="openFileFromTree('${jsPath}')">
105
+ <span class="tree-item-icon">${getFileIcon(node.name)}</span>
106
+ <span class="tree-item-name">${escapeHtml(node.name)}</span>
107
+ </div>
108
+ `;
109
+ }
110
+ }).join('');
111
+ }
112
+
113
+ // Toggle folder expanded/collapsed state
114
+ function toggleFolder(path) {
115
+ if (filesTreeExpanded.has(path)) {
116
+ filesTreeExpanded.delete(path);
117
+ } else {
118
+ filesTreeExpanded.add(path);
119
+ }
120
+ rerenderFilesBrowser();
121
+ }
122
+
123
+ // Re-render file browser in current context
124
+ function rerenderFilesBrowser() {
125
+ if (activeTabId === 'dashboard') {
126
+ const filesContentEl = document.getElementById('dashboard-files-content');
127
+ if (filesContentEl) {
128
+ filesContentEl.innerHTML = filesSearchQuery
129
+ ? renderFilesSearchResults()
130
+ : renderDashboardFilesBrowserWithWrapper();
131
+ }
132
+ }
133
+ }
134
+
135
+ // Wrapper for file browser
136
+ function renderDashboardFilesBrowserWithWrapper() {
137
+ return `<div class="dashboard-files-list" id="dashboard-files-list">${renderDashboardFilesBrowser()}</div>`;
138
+ }
139
+
140
+ // Render compact file browser for dashboard
141
+ function renderDashboardFilesBrowser() {
142
+ if (filesTreeError) {
143
+ return `<div class="dashboard-empty-state">${escapeHtml(filesTreeError)}</div>`;
144
+ }
145
+
146
+ if (!filesTreeLoaded || filesTreeData.length === 0) {
147
+ return '<div class="dashboard-empty-state">Loading files...</div>';
148
+ }
149
+
150
+ return renderTreeNodes(filesTreeData, 0);
151
+ }
152
+
153
+ // Collapse all folders
154
+ function collapseAllFolders() {
155
+ filesTreeExpanded.clear();
156
+ rerenderFilesBrowser();
157
+ }
158
+
159
+ // Expand all folders
160
+ function expandAllFolders() {
161
+ function collectPaths(nodes) {
162
+ for (const node of nodes) {
163
+ if (node.type === 'dir') {
164
+ filesTreeExpanded.add(node.path);
165
+ if (node.children) {
166
+ collectPaths(node.children);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ collectPaths(filesTreeData);
172
+ rerenderFilesBrowser();
173
+ }
174
+
175
+ // Refresh files tree
176
+ async function refreshFilesTree() {
177
+ await loadFilesTree();
178
+ rerenderFilesBrowser();
179
+ showToast('Files refreshed', 'success');
180
+ }
181
+
182
+ // Open file from tree click
183
+ async function openFileFromTree(filePath) {
184
+ try {
185
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
186
+ if (existingTab) {
187
+ selectTab(existingTab.id);
188
+ refreshFileTab(existingTab.id);
189
+ return;
190
+ }
191
+
192
+ const response = await fetch('/api/tabs/file', {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/json' },
195
+ body: JSON.stringify({ path: filePath })
196
+ });
197
+
198
+ if (!response.ok) {
199
+ throw new Error(await response.text());
200
+ }
201
+
202
+ await refresh();
203
+
204
+ const newTab = tabs.find(t => t.type === 'file' && t.path === filePath);
205
+ if (newTab) {
206
+ selectTab(newTab.id);
207
+ }
208
+
209
+ showToast(`Opened ${getFileName(filePath)}`, 'success');
210
+ } catch (err) {
211
+ showToast('Failed to open file: ' + err.message, 'error');
212
+ }
213
+ }
214
+
215
+ // ========================================
216
+ // File Search Functions (Spec 0058)
217
+ // ========================================
218
+
219
+ // Debounced search input handler for Files column
220
+ function onFilesSearchInput(value) {
221
+ clearTimeout(filesSearchDebounceTimer);
222
+ filesSearchDebounceTimer = setTimeout(() => {
223
+ filesSearchQuery = value;
224
+ filesSearchResults = searchFiles(value);
225
+ filesSearchIndex = 0;
226
+ rerenderFilesSearch();
227
+ }, 100);
228
+ }
229
+
230
+ // Clear files search and restore tree view
231
+ function clearFilesSearch() {
232
+ filesSearchQuery = '';
233
+ filesSearchResults = [];
234
+ filesSearchIndex = 0;
235
+ const input = document.getElementById('files-search-input');
236
+ if (input) {
237
+ input.value = '';
238
+ }
239
+ rerenderFilesSearch();
240
+ }
241
+
242
+ // Re-render the files search area
243
+ function rerenderFilesSearch() {
244
+ const filesContentEl = document.getElementById('dashboard-files-content');
245
+ if (filesContentEl) {
246
+ filesContentEl.innerHTML = filesSearchQuery
247
+ ? renderFilesSearchResults()
248
+ : renderDashboardFilesBrowserWithWrapper();
249
+ }
250
+ const clearBtn = document.querySelector('.files-search-clear');
251
+ if (clearBtn) {
252
+ clearBtn.classList.toggle('hidden', !filesSearchQuery);
253
+ }
254
+ }
255
+
256
+ // Render search results for Files column
257
+ function renderFilesSearchResults() {
258
+ if (!filesSearchResults.length) {
259
+ return '<div class="dashboard-empty-state">No files found</div>';
260
+ }
261
+
262
+ return `<div class="files-search-results">${filesSearchResults.map((file, index) =>
263
+ renderSearchResult(file, index, index === filesSearchIndex, filesSearchQuery, 'files')
264
+ ).join('')}</div>`;
265
+ }
266
+
267
+ // Highlight matching text in search results
268
+ function highlightMatch(text, query) {
269
+ if (!query) return escapeHtml(text);
270
+ const q = query.toLowerCase();
271
+ const t = text.toLowerCase();
272
+ const idx = t.indexOf(q);
273
+ if (idx === -1) return escapeHtml(text);
274
+
275
+ return escapeHtml(text.substring(0, idx)) +
276
+ '<span class="files-search-highlight">' + escapeHtml(text.substring(idx, idx + query.length)) + '</span>' +
277
+ escapeHtml(text.substring(idx + query.length));
278
+ }
279
+
280
+ // Render a single search result
281
+ function renderSearchResult(file, index, isSelected, query, context) {
282
+ const classPrefix = context === 'palette' ? 'file-palette' : 'files-search';
283
+ const jsPath = escapeJsString(file.path);
284
+
285
+ return `
286
+ <div class="${classPrefix}-result ${isSelected ? 'selected' : ''}"
287
+ data-index="${index}"
288
+ onclick="openFileFromSearch('${jsPath}', '${context}')">
289
+ <div class="${classPrefix}-result-name">${highlightMatch(file.name, query)}</div>
290
+ <div class="${classPrefix}-result-path">${highlightMatch(file.path, query)}</div>
291
+ </div>
292
+ `;
293
+ }
294
+
295
+ // Keyboard handler for Files search input
296
+ function onFilesSearchKeydown(event) {
297
+ if (!filesSearchResults.length) {
298
+ if (event.key === 'Escape') {
299
+ clearFilesSearch();
300
+ event.target.blur();
301
+ }
302
+ return;
303
+ }
304
+
305
+ if (event.key === 'ArrowDown') {
306
+ event.preventDefault();
307
+ filesSearchIndex = Math.min(filesSearchIndex + 1, filesSearchResults.length - 1);
308
+ rerenderFilesSearch();
309
+ scrollSelectedIntoView('files');
310
+ } else if (event.key === 'ArrowUp') {
311
+ event.preventDefault();
312
+ filesSearchIndex = Math.max(filesSearchIndex - 1, 0);
313
+ rerenderFilesSearch();
314
+ scrollSelectedIntoView('files');
315
+ } else if (event.key === 'Enter') {
316
+ event.preventDefault();
317
+ if (filesSearchResults[filesSearchIndex]) {
318
+ openFileFromSearch(filesSearchResults[filesSearchIndex].path, 'files');
319
+ }
320
+ } else if (event.key === 'Escape') {
321
+ clearFilesSearch();
322
+ event.target.blur();
323
+ }
324
+ }
325
+
326
+ // Scroll selected result into view
327
+ function scrollSelectedIntoView(context) {
328
+ const selector = context === 'palette'
329
+ ? '.file-palette-result.selected'
330
+ : '.files-search-result.selected';
331
+ const selected = document.querySelector(selector);
332
+ if (selected) {
333
+ selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
334
+ }
335
+ }
336
+
337
+ // Open file from search result
338
+ function openFileFromSearch(filePath, context) {
339
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
340
+ if (existingTab) {
341
+ selectTab(existingTab.id);
342
+ refreshFileTab(existingTab.id);
343
+ } else {
344
+ openFileFromTree(filePath);
345
+ }
346
+
347
+ if (context === 'palette') {
348
+ closePalette();
349
+ } else {
350
+ clearFilesSearch();
351
+ }
352
+ }
353
+
354
+ // ========================================
355
+ // Cmd+P Palette Functions (Spec 0058)
356
+ // ========================================
357
+
358
+ // Open the file search palette
359
+ function openPalette() {
360
+ paletteOpen = true;
361
+ paletteQuery = '';
362
+ paletteResults = [];
363
+ paletteIndex = 0;
364
+ document.getElementById('file-palette').classList.remove('hidden');
365
+ const input = document.getElementById('palette-input');
366
+ input.value = '';
367
+ input.focus();
368
+ rerenderPaletteResults();
369
+ }
370
+
371
+ // Close the file search palette
372
+ function closePalette() {
373
+ paletteOpen = false;
374
+ paletteQuery = '';
375
+ paletteResults = [];
376
+ paletteIndex = 0;
377
+ document.getElementById('file-palette').classList.add('hidden');
378
+ }
379
+
380
+ // Debounced palette input handler
381
+ function onPaletteInput(value) {
382
+ clearTimeout(paletteDebounceTimer);
383
+ paletteDebounceTimer = setTimeout(() => {
384
+ paletteQuery = value;
385
+ paletteResults = searchFiles(value);
386
+ paletteIndex = 0;
387
+ rerenderPaletteResults();
388
+ }, 100);
389
+ }
390
+
391
+ // Re-render palette results
392
+ function rerenderPaletteResults() {
393
+ const resultsEl = document.getElementById('palette-results');
394
+ if (!resultsEl) return;
395
+
396
+ if (!paletteQuery) {
397
+ resultsEl.innerHTML = '<div class="file-palette-empty">Type to search files...</div>';
398
+ return;
399
+ }
400
+
401
+ if (!paletteResults.length) {
402
+ resultsEl.innerHTML = '<div class="file-palette-empty">No files found</div>';
403
+ return;
404
+ }
405
+
406
+ resultsEl.innerHTML = paletteResults.map((file, index) =>
407
+ renderSearchResult(file, index, index === paletteIndex, paletteQuery, 'palette')
408
+ ).join('');
409
+ }
410
+
411
+ // Keyboard handler for palette input
412
+ function onPaletteKeydown(event) {
413
+ if (event.key === 'Escape') {
414
+ closePalette();
415
+ return;
416
+ }
417
+
418
+ if (!paletteResults.length) return;
419
+
420
+ if (event.key === 'ArrowDown') {
421
+ event.preventDefault();
422
+ paletteIndex = Math.min(paletteIndex + 1, paletteResults.length - 1);
423
+ rerenderPaletteResults();
424
+ scrollSelectedIntoView('palette');
425
+ } else if (event.key === 'ArrowUp') {
426
+ event.preventDefault();
427
+ paletteIndex = Math.max(paletteIndex - 1, 0);
428
+ rerenderPaletteResults();
429
+ scrollSelectedIntoView('palette');
430
+ } else if (event.key === 'Enter') {
431
+ event.preventDefault();
432
+ if (paletteResults[paletteIndex]) {
433
+ openFileFromSearch(paletteResults[paletteIndex].path, 'palette');
434
+ }
435
+ }
436
+ }