@cluesmith/codev 1.4.1 → 1.4.2

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.
@@ -0,0 +1,238 @@
1
+ // Activity Summary Functions (Spec 0059)
2
+
3
+ // Show activity summary - creates tab if needed
4
+ async function showActivitySummary() {
5
+ let activityTab = tabs.find(t => t.type === 'activity');
6
+
7
+ if (!activityTab) {
8
+ activityTab = {
9
+ id: 'activity-today',
10
+ type: 'activity',
11
+ name: 'Today'
12
+ };
13
+ tabs.push(activityTab);
14
+ }
15
+
16
+ activeTabId = activityTab.id;
17
+ currentTabType = null;
18
+ renderTabs();
19
+ renderTabContent();
20
+ }
21
+
22
+ // Render the activity tab content
23
+ async function renderActivityTab() {
24
+ const content = document.getElementById('tab-content');
25
+
26
+ content.innerHTML = `
27
+ <div class="activity-tab-container">
28
+ <div class="activity-loading">
29
+ <span class="activity-spinner"></span>
30
+ Loading activity...
31
+ </div>
32
+ </div>
33
+ `;
34
+
35
+ try {
36
+ const response = await fetch('/api/activity-summary');
37
+ if (!response.ok) {
38
+ throw new Error(await response.text());
39
+ }
40
+ activityData = await response.json();
41
+ renderActivityTabContent(activityData);
42
+ } catch (err) {
43
+ content.innerHTML = `
44
+ <div class="activity-tab-container">
45
+ <div class="activity-error">
46
+ Failed to load activity: ${escapeHtml(err.message)}
47
+ </div>
48
+ </div>
49
+ `;
50
+ }
51
+ }
52
+
53
+ // Render activity tab content
54
+ function renderActivityTabContent(data) {
55
+ 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;
125
+ }
126
+
127
+ // Render activity summary content (for modal)
128
+ function renderActivitySummary(data) {
129
+ 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;
191
+ }
192
+
193
+ // Close activity modal
194
+ function closeActivityModal() {
195
+ document.getElementById('activity-modal').classList.add('hidden');
196
+ }
197
+
198
+ // Copy activity summary to clipboard (shared by tab and modal)
199
+ function copyActivityToClipboard() {
200
+ copyActivitySummary();
201
+ }
202
+
203
+ function copyActivitySummary() {
204
+ if (!activityData) return;
205
+
206
+ const hours = Math.floor(activityData.timeTracking.activeMinutes / 60);
207
+ const mins = activityData.timeTracking.activeMinutes % 60;
208
+ const uniqueBranches = new Set(activityData.commits.map(c => c.branch)).size;
209
+ const mergedPrs = activityData.prs.filter(p => p.state === 'MERGED').length;
210
+
211
+ let markdown = `## Today's Summary\n\n`;
212
+
213
+ if (activityData.aiSummary) {
214
+ markdown += `${activityData.aiSummary}\n\n`;
215
+ }
216
+
217
+ markdown += `### Activity\n`;
218
+ markdown += `- ${activityData.commits.length} commits across ${uniqueBranches} branches\n`;
219
+ markdown += `- ${activityData.files.length} files modified\n`;
220
+ markdown += `- ${activityData.prs.length} PRs${mergedPrs > 0 ? ` (${mergedPrs} merged)` : ''}\n\n`;
221
+
222
+ if (activityData.projectChanges && activityData.projectChanges.length > 0) {
223
+ markdown += `### Projects Touched\n`;
224
+ activityData.projectChanges.forEach(p => {
225
+ markdown += `- ${p.id}: ${p.title} (${p.oldStatus} → ${p.newStatus})\n`;
226
+ });
227
+ markdown += '\n';
228
+ }
229
+
230
+ markdown += `### Time\n`;
231
+ markdown += `Active time: ~${hours}h ${mins}m\n`;
232
+
233
+ navigator.clipboard.writeText(markdown).then(() => {
234
+ showToast('Copied to clipboard', 'success');
235
+ }).catch(() => {
236
+ showToast('Failed to copy', 'error');
237
+ });
238
+ }
@@ -0,0 +1,328 @@
1
+ // Dialog, Context Menu, and Tab Close Functions
2
+
3
+ // Close tab
4
+ function closeTab(tabId, event) {
5
+ const tab = tabs.find(t => t.id === tabId);
6
+ if (!tab) return;
7
+
8
+ // Shift+click bypasses confirmation
9
+ if (event && event.shiftKey) {
10
+ doCloseTab(tabId);
11
+ return;
12
+ }
13
+
14
+ // Files don't need confirmation
15
+ if (tab.type === 'file') {
16
+ doCloseTab(tabId);
17
+ return;
18
+ }
19
+
20
+ // Show confirmation for builders and shells
21
+ pendingCloseTabId = tabId;
22
+ const dialog = document.getElementById('close-dialog');
23
+ const title = document.getElementById('close-dialog-title');
24
+ const message = document.getElementById('close-dialog-message');
25
+
26
+ if (tab.type === 'builder') {
27
+ title.textContent = `Stop builder ${tab.name}?`;
28
+ message.textContent = 'This will terminate the builder process.';
29
+ } else {
30
+ title.textContent = `Close shell ${tab.name}?`;
31
+ message.textContent = 'This will terminate the shell process.';
32
+ }
33
+
34
+ dialog.classList.remove('hidden');
35
+ }
36
+
37
+ // Actually close the tab
38
+ async function doCloseTab(tabId) {
39
+ const tab = tabs.find(t => t.id === tabId);
40
+ if (!tab) return;
41
+
42
+ try {
43
+ await fetch(`/api/tabs/${encodeURIComponent(tabId)}`, { method: 'DELETE' });
44
+
45
+ tabs = tabs.filter(t => t.id !== tabId);
46
+
47
+ if (activeTabId === tabId) {
48
+ activeTabId = tabs.length > 0 ? tabs[tabs.length - 1].id : null;
49
+ }
50
+
51
+ renderTabs();
52
+ renderTabContent();
53
+ showToast('Tab closed', 'success');
54
+ } catch (err) {
55
+ showToast('Failed to close tab: ' + err.message, 'error');
56
+ }
57
+ }
58
+
59
+ // Confirm close from dialog
60
+ function confirmClose() {
61
+ if (pendingCloseTabId) {
62
+ doCloseTab(pendingCloseTabId);
63
+ hideCloseDialog();
64
+ }
65
+ }
66
+
67
+ function hideCloseDialog() {
68
+ document.getElementById('close-dialog').classList.add('hidden');
69
+ pendingCloseTabId = null;
70
+ }
71
+
72
+ // Context menu
73
+ function showContextMenu(event, tabId) {
74
+ event.preventDefault();
75
+ contextMenuTabId = tabId;
76
+
77
+ const menu = document.getElementById('context-menu');
78
+ menu.style.left = event.clientX + 'px';
79
+ menu.style.top = event.clientY + 'px';
80
+ menu.classList.remove('hidden');
81
+
82
+ const tab = tabs.find(t => t.id === tabId);
83
+ const reloadItem = document.getElementById('context-reload');
84
+ if (reloadItem) {
85
+ reloadItem.style.display = (tab && tab.type === 'file') ? 'block' : 'none';
86
+ }
87
+
88
+ const firstItem = menu.querySelector('.context-menu-item');
89
+ if (firstItem) firstItem.focus();
90
+
91
+ setTimeout(() => {
92
+ document.addEventListener('click', hideContextMenu, { once: true });
93
+ }, 0);
94
+ }
95
+
96
+ // Reload file tab content
97
+ function reloadContextTab() {
98
+ if (contextMenuTabId) {
99
+ refreshFileTab(contextMenuTabId);
100
+ showToast('Reloaded', 'success');
101
+ }
102
+ hideContextMenu();
103
+ }
104
+
105
+ function hideContextMenu() {
106
+ document.getElementById('context-menu').classList.add('hidden');
107
+ contextMenuTabId = null;
108
+ }
109
+
110
+ // Handle keyboard navigation in context menu
111
+ 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
+ }
143
+ }
144
+
145
+ function closeActiveTab() {
146
+ if (contextMenuTabId) {
147
+ closeTab(contextMenuTabId);
148
+ }
149
+ hideContextMenu();
150
+ }
151
+
152
+ function closeOtherTabs() {
153
+ if (contextMenuTabId) {
154
+ const otherTabs = tabs.filter(t => t.id !== contextMenuTabId && t.closeable !== false);
155
+ otherTabs.forEach(t => doCloseTab(t.id));
156
+ }
157
+ hideContextMenu();
158
+ }
159
+
160
+ function closeAllTabs() {
161
+ tabs.filter(t => t.closeable !== false).forEach(t => doCloseTab(t.id));
162
+ hideContextMenu();
163
+ }
164
+
165
+ // Open context menu tab in new tab
166
+ function openContextTab() {
167
+ if (contextMenuTabId) {
168
+ openInNewTab(contextMenuTabId);
169
+ }
170
+ hideContextMenu();
171
+ }
172
+
173
+ // File dialog
174
+ function showFileDialog() {
175
+ document.getElementById('file-dialog').classList.remove('hidden');
176
+ document.getElementById('file-path-input').focus();
177
+ }
178
+
179
+ function hideFileDialog() {
180
+ document.getElementById('file-dialog').classList.add('hidden');
181
+ document.getElementById('file-path-input').value = '';
182
+ }
183
+
184
+ function setFilePath(path) {
185
+ document.getElementById('file-path-input').value = path;
186
+ document.getElementById('file-path-input').focus();
187
+ }
188
+
189
+ async function openFile() {
190
+ const path = document.getElementById('file-path-input').value.trim();
191
+ 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
+ }
210
+ }
211
+
212
+ // Spawn worktree builder
213
+ async function spawnBuilder() {
214
+ try {
215
+ const response = await fetch('/api/tabs/builder', {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json' },
218
+ body: JSON.stringify({})
219
+ });
220
+
221
+ if (!response.ok) {
222
+ throw new Error(await response.text());
223
+ }
224
+
225
+ const result = await response.json();
226
+
227
+ const newTab = {
228
+ id: `builder-${result.id}`,
229
+ type: 'builder',
230
+ name: result.name,
231
+ port: result.port
232
+ };
233
+ tabs.push(newTab);
234
+ activeTabId = newTab.id;
235
+ renderTabs();
236
+ renderTabContent();
237
+ showToast(`Builder ${result.name} spawned`, 'success');
238
+ } catch (err) {
239
+ showToast('Failed to spawn builder: ' + err.message, 'error');
240
+ }
241
+ }
242
+
243
+ // Spawn shell
244
+ async function spawnShell() {
245
+ try {
246
+ const response = await fetch('/api/tabs/shell', {
247
+ method: 'POST',
248
+ headers: { 'Content-Type': 'application/json' },
249
+ body: JSON.stringify({})
250
+ });
251
+
252
+ if (!response.ok) {
253
+ throw new Error(await response.text());
254
+ }
255
+
256
+ const result = await response.json();
257
+
258
+ const newTab = {
259
+ id: `shell-${result.id}`,
260
+ type: 'shell',
261
+ name: result.name,
262
+ port: result.port,
263
+ utilId: result.id,
264
+ pendingLoad: true
265
+ };
266
+ tabs.push(newTab);
267
+ activeTabId = newTab.id;
268
+ renderTabs();
269
+
270
+ const content = document.getElementById('tab-content');
271
+ content.innerHTML = '<div class="empty-state"><p>Starting shell...</p></div>';
272
+
273
+ setTimeout(() => {
274
+ delete newTab.pendingLoad;
275
+ currentTabPort = null;
276
+ renderTabContent();
277
+ }, 800);
278
+
279
+ showToast('Shell spawned', 'success');
280
+ } catch (err) {
281
+ showToast('Failed to spawn shell: ' + err.message, 'error');
282
+ }
283
+ }
284
+
285
+ // Create new utility shell (quick action button)
286
+ async function createNewShell() {
287
+ try {
288
+ const response = await fetch('/api/tabs/shell', { method: 'POST' });
289
+ const data = await response.json();
290
+ if (!data.success && data.error) {
291
+ showToast(data.error || 'Failed to create shell', 'error');
292
+ return;
293
+ }
294
+ await refresh();
295
+ if (data.id) {
296
+ selectTab(`shell-${data.id}`);
297
+ }
298
+ showToast('Shell created', 'success');
299
+ } catch (err) {
300
+ showToast('Network error: ' + err.message, 'error');
301
+ }
302
+ }
303
+
304
+ // Create new worktree shell (quick action button)
305
+ async function createNewWorktreeShell() {
306
+ const branch = prompt('Branch name (leave empty for temp worktree):');
307
+ if (branch === null) return;
308
+
309
+ try {
310
+ const response = await fetch('/api/tabs/shell', {
311
+ method: 'POST',
312
+ headers: { 'Content-Type': 'application/json' },
313
+ body: JSON.stringify({ worktree: true, branch: branch || undefined })
314
+ });
315
+ const data = await response.json();
316
+ if (!data.success && data.error) {
317
+ showToast(data.error || 'Failed to create worktree shell', 'error');
318
+ return;
319
+ }
320
+ await refresh();
321
+ if (data.id) {
322
+ selectTab(`shell-${data.id}`);
323
+ }
324
+ showToast('Worktree shell created', 'success');
325
+ } catch (err) {
326
+ showToast('Network error: ' + err.message, 'error');
327
+ }
328
+ }