@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,487 @@
1
+ // Dashboard Main - Initialization, Polling, Keyboard Shortcuts
2
+
3
+ // Initialize the dashboard
4
+ function init() {
5
+ buildTabsFromState();
6
+ renderArchitect();
7
+ renderTabs();
8
+ renderTabContent();
9
+ updateStatusBar();
10
+ startPolling();
11
+ setupBroadcastChannel();
12
+ setupOverflowDetection();
13
+ setupKeyboardShortcuts();
14
+ setupActivityModalListeners();
15
+ }
16
+
17
+ // Set up BroadcastChannel for cross-tab communication
18
+ function setupBroadcastChannel() {
19
+ const channel = new BroadcastChannel('agent-farm');
20
+ channel.onmessage = async (event) => {
21
+ const { type, path, line } = event.data;
22
+ if (type === 'openFile' && path) {
23
+ await openFileFromMessage(path, line);
24
+ }
25
+ };
26
+ }
27
+
28
+ // Open a file from a BroadcastChannel message
29
+ async function openFileFromMessage(filePath, lineNumber) {
30
+ try {
31
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
32
+ if (existingTab) {
33
+ selectTab(existingTab.id);
34
+ refreshFileTab(existingTab.id);
35
+ showToast(`Switched to ${getFileName(filePath)}`, 'success');
36
+ return;
37
+ }
38
+
39
+ const response = await fetch('/api/tabs/file', {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/json' },
42
+ body: JSON.stringify({ path: filePath })
43
+ });
44
+
45
+ if (!response.ok) {
46
+ throw new Error(await response.text());
47
+ }
48
+
49
+ const result = await response.json();
50
+ await refresh();
51
+
52
+ const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
53
+ if (newTab) {
54
+ selectTab(newTab.id);
55
+ }
56
+
57
+ showToast(`Opened ${getFileName(filePath)}${lineNumber ? ':' + lineNumber : ''}`, 'success');
58
+ } catch (err) {
59
+ showToast('Failed to open file: ' + err.message, 'error');
60
+ }
61
+ }
62
+
63
+ // Refresh state from API
64
+ async function refresh() {
65
+ try {
66
+ const response = await fetch('/api/state');
67
+ if (!response.ok) throw new Error('Failed to fetch state');
68
+
69
+ const newState = await response.json();
70
+ Object.assign(state, newState);
71
+
72
+ buildTabsFromState();
73
+ renderArchitect();
74
+ renderTabs();
75
+ renderTabContent();
76
+ updateStatusBar();
77
+ } catch (err) {
78
+ console.error('Refresh error:', err);
79
+ }
80
+ }
81
+
82
+ // Polling for state updates
83
+ function startPolling() {
84
+ pollInterval = setInterval(refresh, 1000);
85
+ }
86
+
87
+ function stopPolling() {
88
+ if (pollInterval) {
89
+ clearInterval(pollInterval);
90
+ pollInterval = null;
91
+ }
92
+ }
93
+
94
+ // Poll for projectlist.md creation when in starter mode
95
+ async function pollForProjectlistCreation() {
96
+ try {
97
+ const response = await fetch('/api/projectlist-exists');
98
+ if (!response.ok) return;
99
+
100
+ const { exists } = await response.json();
101
+ if (exists) {
102
+ if (starterModePollingInterval) {
103
+ clearInterval(starterModePollingInterval);
104
+ starterModePollingInterval = null;
105
+ }
106
+ window.location.reload();
107
+ }
108
+ } catch (err) {
109
+ // Silently ignore polling errors
110
+ }
111
+ }
112
+
113
+ // Check if we should start starter mode polling
114
+ function checkStarterMode() {
115
+ const isStarterMode = projectsData.length === 0 && !projectlistError && projectlistHash === null;
116
+
117
+ if (isStarterMode && !starterModePollingInterval) {
118
+ starterModePollingInterval = setInterval(pollForProjectlistCreation, 15000);
119
+ } else if (!isStarterMode && starterModePollingInterval) {
120
+ clearInterval(starterModePollingInterval);
121
+ starterModePollingInterval = null;
122
+ }
123
+ }
124
+
125
+ // Render the info header
126
+ function renderInfoHeader() {
127
+ return `
128
+ <div class="projects-info">
129
+ <h1 style="font-size: 20px; margin-bottom: 12px; color: var(--text-primary);">Agent Farm Dashboard</h1>
130
+ <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>
131
+ <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>
132
+ </div>
133
+ `;
134
+ }
135
+
136
+ // Render the dashboard tab content
137
+ function renderDashboardTabContent() {
138
+ const content = document.getElementById('tab-content');
139
+
140
+ content.innerHTML = `
141
+ <div class="dashboard-container">
142
+ ${renderInfoHeader()}
143
+ <div class="dashboard-header">
144
+ <!-- Tabs Section -->
145
+ <div class="dashboard-section section-tabs ${sectionState.tabs ? '' : 'collapsed'}">
146
+ <div class="dashboard-section-header" onclick="toggleSection('tabs')">
147
+ <h3><span class="collapse-icon">▼</span> Tabs</h3>
148
+ <div class="header-actions" onclick="event.stopPropagation()">
149
+ <button onclick="spawnBuilder()" title="New Worktree">+ Worktree</button>
150
+ <button onclick="spawnShell()" title="New Shell">+ Shell</button>
151
+ </div>
152
+ </div>
153
+ <div class="dashboard-section-content">
154
+ <div class="dashboard-tabs-list" id="dashboard-tabs-list">
155
+ ${renderDashboardTabsList()}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ <!-- Files Section -->
160
+ <div class="dashboard-section section-files ${sectionState.files ? '' : 'collapsed'}">
161
+ <div class="dashboard-section-header" onclick="toggleSection('files')">
162
+ <h3><span class="collapse-icon">▼</span> Files</h3>
163
+ <div class="header-actions" onclick="event.stopPropagation()">
164
+ <button onclick="refreshFilesTree()" title="Refresh">↻</button>
165
+ <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
166
+ <button onclick="expandAllFolders()" title="Expand All">⊞</button>
167
+ </div>
168
+ </div>
169
+ <div class="dashboard-section-content">
170
+ <div class="files-search-container" onclick="event.stopPropagation()">
171
+ <input type="text"
172
+ id="files-search-input"
173
+ class="files-search-input"
174
+ placeholder="Search files by name..."
175
+ oninput="onFilesSearchInput(this.value)"
176
+ onkeydown="onFilesSearchKeydown(event)"
177
+ value="${escapeHtml(filesSearchQuery)}" />
178
+ <button class="files-search-clear ${filesSearchQuery ? '' : 'hidden'}"
179
+ onclick="clearFilesSearch()"
180
+ title="Clear search">×</button>
181
+ </div>
182
+ <div id="dashboard-files-content">
183
+ ${filesSearchQuery ? renderFilesSearchResults() : renderDashboardFilesBrowserWithWrapper()}
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <!-- Projects Section -->
189
+ <div class="dashboard-section section-projects ${sectionState.projects ? '' : 'collapsed'}">
190
+ <div class="dashboard-section-header" onclick="toggleSection('projects')">
191
+ <h3><span class="collapse-icon">▼</span> Projects</h3>
192
+ </div>
193
+ <div class="dashboard-section-content" id="dashboard-projects">
194
+ ${renderDashboardProjectsSection()}
195
+ </div>
196
+ </div>
197
+ </div>
198
+ `;
199
+ }
200
+
201
+ // Render the tabs list for dashboard
202
+ function renderDashboardTabsList() {
203
+ const terminalTabs = tabs.filter(t => t.type !== 'dashboard' && t.type !== 'files');
204
+
205
+ if (terminalTabs.length === 0) {
206
+ return '<div class="dashboard-empty-state">No tabs open</div>';
207
+ }
208
+
209
+ return terminalTabs.map(tab => {
210
+ const isActive = tab.id === activeTabId;
211
+ const icon = getTabIcon(tab.type);
212
+ const statusIndicator = getDashboardStatusIndicator(tab);
213
+
214
+ return `
215
+ <div class="dashboard-tab-item ${isActive ? 'active' : ''}" onclick="selectTab('${tab.id}')">
216
+ ${statusIndicator}
217
+ <span class="tab-icon">${icon}</span>
218
+ <span class="tab-name">${escapeHtml(tab.name)}</span>
219
+ </div>
220
+ `;
221
+ }).join('');
222
+ }
223
+
224
+ // Get status indicator for dashboard tab list
225
+ function getDashboardStatusIndicator(tab) {
226
+ if (tab.type !== 'builder') return '';
227
+
228
+ const builderState = (state.builders || []).find(b => `builder-${b.id}` === tab.id);
229
+ if (!builderState) return '';
230
+
231
+ const status = builderState.status;
232
+ if (['spawning', 'implementing'].includes(status)) {
233
+ return '<span class="dashboard-status-indicator dashboard-status-working" title="Working"></span>';
234
+ }
235
+ if (status === 'blocked') {
236
+ return '<span class="dashboard-status-indicator dashboard-status-blocked" title="Blocked"></span>';
237
+ }
238
+ if (['pr-ready', 'complete'].includes(status)) {
239
+ return '<span class="dashboard-status-indicator dashboard-status-idle" title="Idle"></span>';
240
+ }
241
+ return '';
242
+ }
243
+
244
+ // Render the dashboard tab (entry point)
245
+ async function renderDashboardTab() {
246
+ const content = document.getElementById('tab-content');
247
+ content.innerHTML = '<div class="dashboard-container"><p style="color: var(--text-muted); padding: 16px;">Loading dashboard...</p></div>';
248
+
249
+ await Promise.all([
250
+ loadProjectlist(),
251
+ loadFilesTreeIfNeeded()
252
+ ]);
253
+
254
+ renderDashboardTabContent();
255
+ checkStarterMode();
256
+ }
257
+
258
+ // Set up keyboard shortcuts
259
+ function setupKeyboardShortcuts() {
260
+ document.addEventListener('keydown', (e) => {
261
+ // Escape to close dialogs and menus
262
+ if (e.key === 'Escape') {
263
+ hideFileDialog();
264
+ hideCloseDialog();
265
+ hideContextMenu();
266
+ hideOverflowMenu();
267
+ const activityModal = document.getElementById('activity-modal');
268
+ if (activityModal && !activityModal.classList.contains('hidden')) {
269
+ closeActivityModal();
270
+ }
271
+ if (paletteOpen) {
272
+ closePalette();
273
+ }
274
+ }
275
+
276
+ // Enter in dialogs
277
+ if (e.key === 'Enter') {
278
+ if (!document.getElementById('file-dialog').classList.contains('hidden')) {
279
+ openFile();
280
+ }
281
+ }
282
+
283
+ // Ctrl+Tab / Ctrl+Shift+Tab to switch tabs
284
+ if (e.ctrlKey && e.key === 'Tab') {
285
+ e.preventDefault();
286
+ if (tabs.length < 2) return;
287
+
288
+ const currentIndex = tabs.findIndex(t => t.id === activeTabId);
289
+ let newIndex;
290
+
291
+ if (e.shiftKey) {
292
+ newIndex = currentIndex <= 0 ? tabs.length - 1 : currentIndex - 1;
293
+ } else {
294
+ newIndex = currentIndex >= tabs.length - 1 ? 0 : currentIndex + 1;
295
+ }
296
+
297
+ selectTab(tabs[newIndex].id);
298
+ }
299
+
300
+ // Ctrl+W to close current tab
301
+ if (e.ctrlKey && e.key === 'w') {
302
+ e.preventDefault();
303
+ if (activeTabId) {
304
+ closeTab(activeTabId, e);
305
+ }
306
+ }
307
+
308
+ // Cmd+P (macOS) or Ctrl+P (Windows/Linux) for file search palette
309
+ if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
310
+ const active = document.activeElement;
311
+ const isOurInput = active?.id === 'palette-input' || active?.id === 'files-search-input';
312
+ const isEditable = active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA' || active?.isContentEditable;
313
+
314
+ if (!isOurInput && isEditable) return;
315
+
316
+ e.preventDefault();
317
+ if (paletteOpen) {
318
+ closePalette();
319
+ } else {
320
+ openPalette();
321
+ }
322
+ }
323
+ });
324
+ }
325
+
326
+ // Set up activity modal event listeners
327
+ function setupActivityModalListeners() {
328
+ const activityModal = document.getElementById('activity-modal');
329
+ if (activityModal) {
330
+ activityModal.addEventListener('click', (e) => {
331
+ if (e.target.id === 'activity-modal') {
332
+ closeActivityModal();
333
+ }
334
+ });
335
+ }
336
+ }
337
+
338
+ // Start projectlist polling (separate from main state polling)
339
+ setInterval(pollProjectlist, 5000);
340
+
341
+ // ========================================
342
+ // Hot Reload Functions (Spec 0060)
343
+ // ========================================
344
+
345
+ // Hot reload CSS by replacing stylesheet link with cache-busted version
346
+ function hotReloadCSS(filename) {
347
+ const links = document.querySelectorAll(`link[href^="/dashboard/css/${filename}"]`);
348
+ links.forEach(link => {
349
+ const newHref = `/dashboard/css/${filename}?t=${Date.now()}`;
350
+ link.href = newHref;
351
+ });
352
+ console.log(`[Hot Reload] CSS updated: ${filename}`);
353
+ }
354
+
355
+ // Hot reload JS by saving state and reloading page
356
+ function hotReloadJS(filename) {
357
+ // Save current UI state to sessionStorage for restoration after reload
358
+ try {
359
+ const uiState = {
360
+ activeTabId,
361
+ sectionState,
362
+ filesTreeExpanded: Array.from(filesTreeExpanded),
363
+ expandedProjectId,
364
+ filesSearchQuery,
365
+ paletteOpen
366
+ };
367
+ sessionStorage.setItem('codev-hot-reload-state', JSON.stringify(uiState));
368
+ } catch (e) {
369
+ console.warn('[Hot Reload] Could not save state:', e);
370
+ }
371
+
372
+ console.log(`[Hot Reload] JS changed: ${filename} - reloading page...`);
373
+ showToast(`Reloading for ${filename} changes...`, 'info');
374
+
375
+ // Small delay to show toast before reload
376
+ setTimeout(() => {
377
+ window.location.reload();
378
+ }, 300);
379
+ }
380
+
381
+ // Restore UI state after hot reload
382
+ function restoreHotReloadState() {
383
+ try {
384
+ const saved = sessionStorage.getItem('codev-hot-reload-state');
385
+ if (!saved) return;
386
+
387
+ const uiState = JSON.parse(saved);
388
+ sessionStorage.removeItem('codev-hot-reload-state');
389
+
390
+ // Restore section state
391
+ if (uiState.sectionState) {
392
+ sectionState = uiState.sectionState;
393
+ saveSectionState();
394
+ }
395
+
396
+ // Restore files tree expansion
397
+ if (uiState.filesTreeExpanded) {
398
+ filesTreeExpanded = new Set(uiState.filesTreeExpanded);
399
+ }
400
+
401
+ // Restore expanded project
402
+ if (uiState.expandedProjectId) {
403
+ expandedProjectId = uiState.expandedProjectId;
404
+ }
405
+
406
+ // Restore active tab (will be applied after tabs are built)
407
+ if (uiState.activeTabId) {
408
+ // Store for later application
409
+ window._hotReloadActiveTabId = uiState.activeTabId;
410
+ }
411
+
412
+ console.log('[Hot Reload] State restored from previous session');
413
+ } catch (e) {
414
+ console.warn('[Hot Reload] Could not restore state:', e);
415
+ }
416
+ }
417
+
418
+ // Apply restored active tab after tabs are built
419
+ function applyRestoredActiveTab() {
420
+ if (window._hotReloadActiveTabId) {
421
+ const restoredTab = tabs.find(t => t.id === window._hotReloadActiveTabId);
422
+ if (restoredTab) {
423
+ activeTabId = window._hotReloadActiveTabId;
424
+ }
425
+ delete window._hotReloadActiveTabId;
426
+ }
427
+ }
428
+
429
+ // Poll for file changes
430
+ async function pollHotReload() {
431
+ if (!hotReloadEnabled) return;
432
+
433
+ try {
434
+ const response = await fetch('/api/hot-reload');
435
+ if (!response.ok) return;
436
+
437
+ const data = await response.json();
438
+ const newMtimes = data.mtimes || {};
439
+
440
+ // Check for changes
441
+ for (const [file, mtime] of Object.entries(newMtimes)) {
442
+ const oldMtime = hotReloadMtimes[file];
443
+
444
+ if (oldMtime !== undefined && oldMtime !== mtime) {
445
+ // File changed!
446
+ const filename = file.split('/').pop();
447
+
448
+ if (file.startsWith('css/')) {
449
+ hotReloadCSS(filename);
450
+ } else if (file.startsWith('js/')) {
451
+ hotReloadJS(filename);
452
+ return; // Stop polling, page will reload
453
+ }
454
+ }
455
+ }
456
+
457
+ // Update tracked mtimes
458
+ hotReloadMtimes = newMtimes;
459
+ } catch (err) {
460
+ // Silently ignore polling errors
461
+ }
462
+ }
463
+
464
+ // Start hot reload polling
465
+ function startHotReload() {
466
+ if (hotReloadInterval) return;
467
+
468
+ // Initial fetch to populate mtimes
469
+ pollHotReload();
470
+
471
+ // Poll every 2 seconds
472
+ hotReloadInterval = setInterval(pollHotReload, 2000);
473
+ }
474
+
475
+ // Stop hot reload polling
476
+ function stopHotReload() {
477
+ if (hotReloadInterval) {
478
+ clearInterval(hotReloadInterval);
479
+ hotReloadInterval = null;
480
+ }
481
+ }
482
+
483
+ // Initialize on load
484
+ restoreHotReloadState();
485
+ init();
486
+ applyRestoredActiveTab();
487
+ startHotReload();