@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.
- package/dist/agent-farm/servers/dashboard-server.js +487 -9
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +141 -40
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/utils/port-registry.d.ts.map +1 -1
- package/dist/agent-farm/utils/port-registry.js +19 -5
- package/dist/agent-farm/utils/port-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +10 -0
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/index.d.ts +1 -0
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +56 -8
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -0
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/skeleton/resources/commands/consult.md +50 -0
- package/skeleton/templates/projectlist-archive.md +21 -0
- package/skeleton/templates/projectlist.md +17 -0
- package/templates/dashboard/css/activity.css +151 -0
- package/templates/dashboard/css/dialogs.css +149 -0
- package/templates/dashboard/css/files.css +530 -0
- package/templates/dashboard/css/layout.css +124 -0
- package/templates/dashboard/css/projects.css +501 -0
- package/templates/dashboard/css/statusbar.css +23 -0
- package/templates/dashboard/css/tabs.css +314 -0
- package/templates/dashboard/css/utilities.css +50 -0
- package/templates/dashboard/css/variables.css +45 -0
- package/templates/dashboard/index.html +158 -0
- package/templates/dashboard/js/activity.js +238 -0
- package/templates/dashboard/js/dialogs.js +328 -0
- package/templates/dashboard/js/files.js +436 -0
- package/templates/dashboard/js/main.js +487 -0
- package/templates/dashboard/js/projects.js +544 -0
- package/templates/dashboard/js/state.js +91 -0
- package/templates/dashboard/js/tabs.js +500 -0
- package/templates/dashboard/js/utils.js +57 -0
- package/templates/tower.html +172 -4
- package/dist/commands/eject.d.ts +0 -18
- package/dist/commands/eject.d.ts.map +0 -1
- package/dist/commands/eject.js +0 -149
- package/dist/commands/eject.js.map +0 -1
- package/templates/dashboard-split.html +0 -3721
- 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();
|