@cluesmith/codev 1.5.9 → 1.5.11

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 (45) hide show
  1. package/dist/agent-farm/commands/architect.d.ts.map +1 -1
  2. package/dist/agent-farm/commands/architect.js +5 -20
  3. package/dist/agent-farm/commands/architect.js.map +1 -1
  4. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  5. package/dist/agent-farm/commands/spawn.js +35 -22
  6. package/dist/agent-farm/commands/spawn.js.map +1 -1
  7. package/dist/agent-farm/commands/start.d.ts.map +1 -1
  8. package/dist/agent-farm/commands/start.js +35 -21
  9. package/dist/agent-farm/commands/start.js.map +1 -1
  10. package/dist/agent-farm/servers/dashboard-server.js +5 -53
  11. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  12. package/dist/agent-farm/servers/open-server.js +14 -22
  13. package/dist/agent-farm/servers/open-server.js.map +1 -1
  14. package/dist/agent-farm/servers/tower-server.js +2 -54
  15. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  16. package/dist/agent-farm/utils/roles.d.ts +32 -0
  17. package/dist/agent-farm/utils/roles.d.ts.map +1 -0
  18. package/dist/agent-farm/utils/roles.js +43 -0
  19. package/dist/agent-farm/utils/roles.js.map +1 -0
  20. package/dist/agent-farm/utils/server-utils.d.ts +24 -0
  21. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -0
  22. package/dist/agent-farm/utils/server-utils.js +66 -0
  23. package/dist/agent-farm/utils/server-utils.js.map +1 -0
  24. package/dist/commands/adopt.d.ts.map +1 -1
  25. package/dist/commands/adopt.js +37 -157
  26. package/dist/commands/adopt.js.map +1 -1
  27. package/dist/commands/init.d.ts.map +1 -1
  28. package/dist/commands/init.js +26 -138
  29. package/dist/commands/init.js.map +1 -1
  30. package/dist/lib/cli-prompts.d.ts +20 -0
  31. package/dist/lib/cli-prompts.d.ts.map +1 -0
  32. package/dist/lib/cli-prompts.js +51 -0
  33. package/dist/lib/cli-prompts.js.map +1 -0
  34. package/dist/lib/scaffold.d.ts +81 -0
  35. package/dist/lib/scaffold.d.ts.map +1 -0
  36. package/dist/lib/scaffold.js +189 -0
  37. package/dist/lib/scaffold.js.map +1 -0
  38. package/package.json +1 -1
  39. package/templates/dashboard/js/activity.js +4 -130
  40. package/templates/dashboard/js/dialogs.js +4 -49
  41. package/templates/dashboard/js/files.js +2 -29
  42. package/templates/dashboard/js/main.js +2 -31
  43. package/templates/dashboard/js/tabs.js +6 -30
  44. package/templates/dashboard/js/utils.js +199 -0
  45. package/templates/open.html +8 -8
@@ -51,143 +51,17 @@ async function renderActivityTab() {
51
51
  }
52
52
 
53
53
  // Render activity tab content
54
+ // Uses shared renderActivityContentHtml from utils.js (Maintenance Run 0004)
54
55
  function renderActivityTabContent(data) {
55
56
  const content = document.getElementById('tab-content');
56
-
57
- if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
58
- content.innerHTML = `
59
- <div class="activity-tab-container">
60
- <div class="activity-empty">
61
- <p>No activity recorded today</p>
62
- <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
63
- </div>
64
- </div>
65
- `;
66
- return;
67
- }
68
-
69
- const hours = Math.floor(data.timeTracking.activeMinutes / 60);
70
- const mins = data.timeTracking.activeMinutes % 60;
71
- const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
72
- const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
73
-
74
- const formatTime = (isoString) => {
75
- if (!isoString) return '--';
76
- const date = new Date(isoString);
77
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
78
- };
79
-
80
- let html = '<div class="activity-tab-container"><div class="activity-summary">';
81
-
82
- if (data.aiSummary) {
83
- html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
84
- }
85
-
86
- html += `
87
- <div class="activity-section">
88
- <h4>Activity</h4>
89
- <ul>
90
- <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
91
- <li>${data.files.length} files modified</li>
92
- <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
93
- </ul>
94
- </div>
95
- `;
96
-
97
- if (data.projectChanges && data.projectChanges.length > 0) {
98
- html += `
99
- <div class="activity-section">
100
- <h4>Projects Touched</h4>
101
- <ul>
102
- ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
103
- </ul>
104
- </div>
105
- `;
106
- }
107
-
108
- html += `
109
- <div class="activity-section">
110
- <h4>Time</h4>
111
- <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
112
- <p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
113
- <p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
114
- </div>
115
- `;
116
-
117
- html += `
118
- <div class="activity-actions">
119
- <button class="btn" onclick="copyActivityToClipboard()">Copy to Clipboard</button>
120
- </div>
121
- `;
122
-
123
- html += '</div></div>';
124
- content.innerHTML = html;
57
+ content.innerHTML = renderActivityContentHtml(data, { isTab: true });
125
58
  }
126
59
 
127
60
  // Render activity summary content (for modal)
61
+ // Uses shared renderActivityContentHtml from utils.js (Maintenance Run 0004)
128
62
  function renderActivitySummary(data) {
129
63
  const content = document.getElementById('activity-content');
130
-
131
- if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
132
- content.innerHTML = `
133
- <div class="activity-empty">
134
- <p>No activity recorded today</p>
135
- <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
136
- </div>
137
- `;
138
- return;
139
- }
140
-
141
- const hours = Math.floor(data.timeTracking.activeMinutes / 60);
142
- const mins = data.timeTracking.activeMinutes % 60;
143
- const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
144
- const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
145
-
146
- const formatTime = (isoString) => {
147
- if (!isoString) return '--';
148
- const date = new Date(isoString);
149
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
150
- };
151
-
152
- let html = '<div class="activity-summary">';
153
-
154
- if (data.aiSummary) {
155
- html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
156
- }
157
-
158
- html += `
159
- <div class="activity-section">
160
- <h4>Activity</h4>
161
- <ul>
162
- <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
163
- <li>${data.files.length} files modified</li>
164
- <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
165
- </ul>
166
- </div>
167
- `;
168
-
169
- if (data.projectChanges && data.projectChanges.length > 0) {
170
- html += `
171
- <div class="activity-section">
172
- <h4>Projects Touched</h4>
173
- <ul>
174
- ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
175
- </ul>
176
- </div>
177
- `;
178
- }
179
-
180
- html += `
181
- <div class="activity-section">
182
- <h4>Time</h4>
183
- <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
184
- <p>First activity: ${formatTime(data.timeTracking.firstActivity)}</p>
185
- <p>Last activity: ${formatTime(data.timeTracking.lastActivity)}</p>
186
- </div>
187
- `;
188
-
189
- html += '</div>';
190
- content.innerHTML = html;
64
+ content.innerHTML = renderActivityContentHtml(data, { isTab: false });
191
65
  }
192
66
 
193
67
  // Close activity modal
@@ -108,38 +108,9 @@ function hideContextMenu() {
108
108
  }
109
109
 
110
110
  // Handle keyboard navigation in context menu
111
+ // Uses shared handleMenuKeydown from utils.js (Maintenance Run 0004)
111
112
  function handleContextMenuKeydown(event) {
112
- const menu = document.getElementById('context-menu');
113
- const items = Array.from(menu.querySelectorAll('.context-menu-item'));
114
- const currentIndex = items.findIndex(item => item === document.activeElement);
115
-
116
- switch (event.key) {
117
- case 'ArrowDown':
118
- event.preventDefault();
119
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
120
- items[nextIndex].focus();
121
- break;
122
- case 'ArrowUp':
123
- event.preventDefault();
124
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
125
- items[prevIndex].focus();
126
- break;
127
- case 'Enter':
128
- case ' ':
129
- event.preventDefault();
130
- const actionName = event.target.dataset.action;
131
- if (actionName && typeof window[actionName] === 'function') {
132
- window[actionName]();
133
- }
134
- break;
135
- case 'Escape':
136
- event.preventDefault();
137
- hideContextMenu();
138
- break;
139
- case 'Tab':
140
- hideContextMenu();
141
- break;
142
- }
113
+ handleMenuKeydown(event, 'context-menu', 'context-menu-item', hideContextMenu);
143
114
  }
144
115
 
145
116
  function closeActiveTab() {
@@ -186,27 +157,11 @@ function setFilePath(path) {
186
157
  document.getElementById('file-path-input').focus();
187
158
  }
188
159
 
160
+ // Uses shared openFileTab from utils.js (Maintenance Run 0004)
189
161
  async function openFile() {
190
162
  const path = document.getElementById('file-path-input').value.trim();
191
163
  if (!path) return;
192
-
193
- try {
194
- const response = await fetch('/api/tabs/file', {
195
- method: 'POST',
196
- headers: { 'Content-Type': 'application/json' },
197
- body: JSON.stringify({ path })
198
- });
199
-
200
- if (!response.ok) {
201
- throw new Error(await response.text());
202
- }
203
-
204
- hideFileDialog();
205
- await refresh();
206
- showToast(`Opened ${path}`, 'success');
207
- } catch (err) {
208
- showToast('Failed to open file: ' + err.message, 'error');
209
- }
164
+ await openFileTab(path, { onSuccess: hideFileDialog });
210
165
  }
211
166
 
212
167
  // Spawn worktree builder
@@ -181,36 +181,9 @@ async function refreshFilesTree() {
181
181
  }
182
182
 
183
183
  // Open file from tree click
184
+ // Uses shared openFileTab from utils.js (Maintenance Run 0004)
184
185
  async function openFileFromTree(filePath) {
185
- try {
186
- const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
187
- if (existingTab) {
188
- selectTab(existingTab.id);
189
- refreshFileTab(existingTab.id);
190
- return;
191
- }
192
-
193
- const response = await fetch('/api/tabs/file', {
194
- method: 'POST',
195
- headers: { 'Content-Type': 'application/json' },
196
- body: JSON.stringify({ path: filePath })
197
- });
198
-
199
- if (!response.ok) {
200
- throw new Error(await response.text());
201
- }
202
-
203
- await refresh();
204
-
205
- const newTab = tabs.find(t => t.type === 'file' && t.path === filePath);
206
- if (newTab) {
207
- selectTab(newTab.id);
208
- }
209
-
210
- showToast(`Opened ${getFileName(filePath)}`, 'success');
211
- } catch (err) {
212
- showToast('Failed to open file: ' + err.message, 'error');
213
- }
186
+ await openFileTab(filePath, { showSwitchToast: false });
214
187
  }
215
188
 
216
189
  // ========================================
@@ -26,38 +26,9 @@ function setupBroadcastChannel() {
26
26
  }
27
27
 
28
28
  // Open a file from a BroadcastChannel message
29
+ // Uses shared openFileTab from utils.js (Maintenance Run 0004)
29
30
  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
- }
31
+ await openFileTab(filePath, { lineNumber });
61
32
  }
62
33
 
63
34
  // Refresh state from API
@@ -31,7 +31,7 @@ function getTerminalUrl(tab) {
31
31
 
32
32
  // File/annotation tabs - use the annotation ID for proxy routing
33
33
  if (tab.type === 'file' && tab.annotationId) {
34
- return `/annotation/${tab.annotationId}`;
34
+ return `/annotation/${tab.annotationId}/`;
35
35
  }
36
36
 
37
37
  // Fallback for backward compatibility
@@ -494,36 +494,12 @@ function openInNewTabFromMenu(tabId) {
494
494
  }
495
495
 
496
496
  // Handle keyboard navigation in overflow menu
497
+ // Uses shared handleMenuKeydown from utils.js (Maintenance Run 0004)
497
498
  function handleOverflowMenuKeydown(event, tabId) {
498
- const menu = document.getElementById('overflow-menu');
499
- const items = Array.from(menu.querySelectorAll('.overflow-menu-item'));
500
- const currentIndex = items.findIndex(item => item === document.activeElement);
501
-
502
- switch (event.key) {
503
- case 'ArrowDown':
504
- event.preventDefault();
505
- const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
506
- items[nextIndex].focus();
507
- break;
508
- case 'ArrowUp':
509
- event.preventDefault();
510
- const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
511
- items[prevIndex].focus();
512
- break;
513
- case 'Enter':
514
- case ' ':
515
- event.preventDefault();
516
- selectTabFromMenu(tabId);
517
- break;
518
- case 'Escape':
519
- event.preventDefault();
520
- hideOverflowMenu();
521
- document.getElementById('overflow-btn').focus();
522
- break;
523
- case 'Tab':
524
- hideOverflowMenu();
525
- break;
526
- }
499
+ handleMenuKeydown(event, 'overflow-menu', 'overflow-menu-item', hideOverflowMenu, {
500
+ onEnter: () => selectTabFromMenu(tabId),
501
+ focusOnEscape: 'overflow-btn'
502
+ });
527
503
  }
528
504
 
529
505
  // Open tab content in a new browser tab
@@ -55,3 +55,202 @@ function showToast(message, type = 'info') {
55
55
  toast.remove();
56
56
  }, 3000);
57
57
  }
58
+
59
+ // ========================================
60
+ // Shared Utilities (Maintenance Run 0004)
61
+ // ========================================
62
+
63
+ /**
64
+ * Open a file in a new tab (or switch to existing)
65
+ * Consolidates duplicate code from main.js, files.js, dialogs.js
66
+ *
67
+ * @param {string} filePath - Path to the file to open
68
+ * @param {Object} options - Optional settings
69
+ * @param {number} options.lineNumber - Line number to show in toast
70
+ * @param {boolean} options.showSwitchToast - Show toast when switching to existing tab
71
+ * @param {Function} options.onSuccess - Callback after successful open
72
+ */
73
+ async function openFileTab(filePath, options = {}) {
74
+ const { lineNumber, showSwitchToast = true, onSuccess } = options;
75
+
76
+ try {
77
+ // Check for existing tab
78
+ const existingTab = tabs.find(t => t.type === 'file' && t.path === filePath);
79
+ if (existingTab) {
80
+ selectTab(existingTab.id);
81
+ refreshFileTab(existingTab.id);
82
+ if (showSwitchToast) {
83
+ showToast(`Switched to ${getFileName(filePath)}`, 'success');
84
+ }
85
+ if (onSuccess) onSuccess();
86
+ return;
87
+ }
88
+
89
+ // Create new tab
90
+ const response = await fetch('/api/tabs/file', {
91
+ method: 'POST',
92
+ headers: { 'Content-Type': 'application/json' },
93
+ body: JSON.stringify({ path: filePath })
94
+ });
95
+
96
+ if (!response.ok) {
97
+ throw new Error(await response.text());
98
+ }
99
+
100
+ const result = await response.json();
101
+ await refresh();
102
+
103
+ // Select the new tab
104
+ const newTab = tabs.find(t => t.type === 'file' && (t.path === filePath || t.annotationId === result.id));
105
+ if (newTab) {
106
+ selectTab(newTab.id);
107
+ }
108
+
109
+ const lineInfo = lineNumber ? `:${lineNumber}` : '';
110
+ showToast(`Opened ${getFileName(filePath)}${lineInfo}`, 'success');
111
+ if (onSuccess) onSuccess();
112
+ } catch (err) {
113
+ showToast('Failed to open file: ' + err.message, 'error');
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Handle keyboard navigation in dropdown menus
119
+ * Consolidates duplicate code from dialogs.js and tabs.js
120
+ *
121
+ * @param {KeyboardEvent} event - The keyboard event
122
+ * @param {string} menuId - ID of the menu element
123
+ * @param {string} itemClass - CSS class of menu items
124
+ * @param {Function} hideFunction - Function to hide the menu
125
+ * @param {Object} options - Optional settings
126
+ * @param {Function} options.onEnter - Custom handler for Enter/Space (default: call data-action)
127
+ * @param {string} options.focusOnEscape - Element ID to focus after Escape
128
+ */
129
+ function handleMenuKeydown(event, menuId, itemClass, hideFunction, options = {}) {
130
+ const { onEnter, focusOnEscape } = options;
131
+ const menu = document.getElementById(menuId);
132
+ const items = Array.from(menu.querySelectorAll(`.${itemClass}`));
133
+ const currentIndex = items.findIndex(item => item === document.activeElement);
134
+
135
+ switch (event.key) {
136
+ case 'ArrowDown':
137
+ event.preventDefault();
138
+ const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
139
+ items[nextIndex].focus();
140
+ break;
141
+ case 'ArrowUp':
142
+ event.preventDefault();
143
+ const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
144
+ items[prevIndex].focus();
145
+ break;
146
+ case 'Enter':
147
+ case ' ':
148
+ event.preventDefault();
149
+ if (onEnter) {
150
+ onEnter(event);
151
+ } else {
152
+ const actionName = event.target.dataset.action;
153
+ if (actionName && typeof window[actionName] === 'function') {
154
+ window[actionName]();
155
+ }
156
+ }
157
+ break;
158
+ case 'Escape':
159
+ event.preventDefault();
160
+ hideFunction();
161
+ if (focusOnEscape) {
162
+ document.getElementById(focusOnEscape).focus();
163
+ }
164
+ break;
165
+ case 'Tab':
166
+ hideFunction();
167
+ break;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Format ISO time string for display
173
+ * Used by activity rendering
174
+ */
175
+ function formatActivityTime(isoString) {
176
+ if (!isoString) return '--';
177
+ const date = new Date(isoString);
178
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
179
+ }
180
+
181
+ /**
182
+ * Render activity summary content
183
+ * Consolidates duplicate code from renderActivityTabContent and renderActivitySummary
184
+ *
185
+ * @param {Object} data - Activity data
186
+ * @param {Object} options - Render options
187
+ * @param {boolean} options.isTab - Whether rendering for tab (includes wrapper and copy button)
188
+ * @returns {string} HTML content
189
+ */
190
+ function renderActivityContentHtml(data, options = {}) {
191
+ const { isTab = false } = options;
192
+
193
+ if (data.commits.length === 0 && data.prs.length === 0 && data.builders.length === 0) {
194
+ return `
195
+ <div class="activity-empty">
196
+ <p>No activity recorded today</p>
197
+ <p style="font-size: 12px; margin-top: 8px;">Make some commits or create PRs to see your daily summary!</p>
198
+ </div>
199
+ `;
200
+ }
201
+
202
+ const hours = Math.floor(data.timeTracking.activeMinutes / 60);
203
+ const mins = data.timeTracking.activeMinutes % 60;
204
+ const uniqueBranches = new Set(data.commits.map(c => c.branch)).size;
205
+ const mergedPrs = data.prs.filter(p => p.state === 'MERGED').length;
206
+
207
+ let html = isTab ? '<div class="activity-tab-container"><div class="activity-summary">' : '<div class="activity-summary">';
208
+
209
+ if (data.aiSummary) {
210
+ html += `<div class="activity-ai-summary">${escapeHtml(data.aiSummary)}</div>`;
211
+ }
212
+
213
+ html += `
214
+ <div class="activity-section">
215
+ <h4>Activity</h4>
216
+ <ul>
217
+ <li>${data.commits.length} commits across ${uniqueBranches} branch${uniqueBranches !== 1 ? 'es' : ''}</li>
218
+ <li>${data.files.length} files modified</li>
219
+ <li>${data.prs.length} PR${data.prs.length !== 1 ? 's' : ''} created${mergedPrs > 0 ? `, ${mergedPrs} merged` : ''}</li>
220
+ </ul>
221
+ </div>
222
+ `;
223
+
224
+ if (data.projectChanges && data.projectChanges.length > 0) {
225
+ html += `
226
+ <div class="activity-section">
227
+ <h4>Projects Touched</h4>
228
+ <ul>
229
+ ${data.projectChanges.map(p => `<li>${escapeHtml(p.id)}: ${escapeHtml(p.title)} (${escapeHtml(p.oldStatus)} → ${escapeHtml(p.newStatus)})</li>`).join('')}
230
+ </ul>
231
+ </div>
232
+ `;
233
+ }
234
+
235
+ html += `
236
+ <div class="activity-section">
237
+ <h4>Time</h4>
238
+ <p><span class="activity-time-value">~${hours}h ${mins}m</span> active time</p>
239
+ <p>First activity: ${formatActivityTime(data.timeTracking.firstActivity)}</p>
240
+ <p>Last activity: ${formatActivityTime(data.timeTracking.lastActivity)}</p>
241
+ </div>
242
+ `;
243
+
244
+ if (isTab) {
245
+ html += `
246
+ <div class="activity-actions">
247
+ <button class="btn" onclick="copyActivityToClipboard()">Copy to Clipboard</button>
248
+ </div>
249
+ `;
250
+ html += '</div></div>';
251
+ } else {
252
+ html += '</div>';
253
+ }
254
+
255
+ return html;
256
+ }
@@ -622,7 +622,7 @@
622
622
  };
623
623
 
624
624
  // Add cache-busting query param to allow reload
625
- img.src = '/api/image?t=' + Date.now();
625
+ img.src = 'api/image?t=' + Date.now();
626
626
  }
627
627
 
628
628
  // Initialize video viewer
@@ -656,7 +656,7 @@
656
656
  };
657
657
 
658
658
  // Add cache-busting query param to allow reload
659
- video.src = '/api/video?t=' + Date.now();
659
+ video.src = 'api/video?t=' + Date.now();
660
660
  }
661
661
 
662
662
  // Update image info display (in both header and controls)
@@ -1245,7 +1245,7 @@
1245
1245
 
1246
1246
  async function saveFile() {
1247
1247
  try {
1248
- const response = await fetch('/save', {
1248
+ const response = await fetch('save', {
1249
1249
  method: 'POST',
1250
1250
  headers: { 'Content-Type': 'application/json' },
1251
1251
  body: JSON.stringify({
@@ -1348,7 +1348,7 @@
1348
1348
  const content = editor.value;
1349
1349
 
1350
1350
  try {
1351
- const response = await fetch('/save', {
1351
+ const response = await fetch('save', {
1352
1352
  method: 'POST',
1353
1353
  headers: { 'Content-Type': 'application/json' },
1354
1354
  body: JSON.stringify({ file: filePath, content })
@@ -1670,7 +1670,7 @@
1670
1670
  if (hasUnsavedChanges || editMode) return;
1671
1671
 
1672
1672
  try {
1673
- const res = await fetch('/api/mtime');
1673
+ const res = await fetch('api/mtime');
1674
1674
  if (res.ok) {
1675
1675
  const data = await res.json();
1676
1676
  if (lastMtime === null) {
@@ -1689,21 +1689,21 @@
1689
1689
  // For images and videos, just reload the source
1690
1690
  if (isImageFile) {
1691
1691
  const img = document.getElementById('image-display');
1692
- img.src = '/api/image?t=' + Date.now();
1692
+ img.src = 'api/image?t=' + Date.now();
1693
1693
  showNotification('Image reloaded');
1694
1694
  return;
1695
1695
  }
1696
1696
 
1697
1697
  if (isVideoFile) {
1698
1698
  const video = document.getElementById('video-display');
1699
- video.src = '/api/video?t=' + Date.now();
1699
+ video.src = 'api/video?t=' + Date.now();
1700
1700
  showNotification('Video reloaded');
1701
1701
  return;
1702
1702
  }
1703
1703
 
1704
1704
  // For text files, fetch new content
1705
1705
  try {
1706
- const res = await fetch('/file');
1706
+ const res = await fetch('file');
1707
1707
  if (res.ok) {
1708
1708
  const content = await res.text();
1709
1709
  currentContent = content;