@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.
- package/dist/agent-farm/commands/architect.d.ts.map +1 -1
- package/dist/agent-farm/commands/architect.js +5 -20
- package/dist/agent-farm/commands/architect.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +35 -22
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/start.d.ts.map +1 -1
- package/dist/agent-farm/commands/start.js +35 -21
- package/dist/agent-farm/commands/start.js.map +1 -1
- package/dist/agent-farm/servers/dashboard-server.js +5 -53
- package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
- package/dist/agent-farm/servers/open-server.js +14 -22
- package/dist/agent-farm/servers/open-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +2 -54
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/utils/roles.d.ts +32 -0
- package/dist/agent-farm/utils/roles.d.ts.map +1 -0
- package/dist/agent-farm/utils/roles.js +43 -0
- package/dist/agent-farm/utils/roles.js.map +1 -0
- package/dist/agent-farm/utils/server-utils.d.ts +24 -0
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -0
- package/dist/agent-farm/utils/server-utils.js +66 -0
- package/dist/agent-farm/utils/server-utils.js.map +1 -0
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +37 -157
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -138
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/cli-prompts.d.ts +20 -0
- package/dist/lib/cli-prompts.d.ts.map +1 -0
- package/dist/lib/cli-prompts.js +51 -0
- package/dist/lib/cli-prompts.js.map +1 -0
- package/dist/lib/scaffold.d.ts +81 -0
- package/dist/lib/scaffold.d.ts.map +1 -0
- package/dist/lib/scaffold.js +189 -0
- package/dist/lib/scaffold.js.map +1 -0
- package/package.json +1 -1
- package/templates/dashboard/js/activity.js +4 -130
- package/templates/dashboard/js/dialogs.js +4 -49
- package/templates/dashboard/js/files.js +2 -29
- package/templates/dashboard/js/main.js +2 -31
- package/templates/dashboard/js/tabs.js +6 -30
- package/templates/dashboard/js/utils.js +199 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
package/templates/open.html
CHANGED
|
@@ -622,7 +622,7 @@
|
|
|
622
622
|
};
|
|
623
623
|
|
|
624
624
|
// Add cache-busting query param to allow reload
|
|
625
|
-
img.src = '
|
|
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 = '
|
|
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('
|
|
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('
|
|
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('
|
|
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 = '
|
|
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 = '
|
|
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('
|
|
1706
|
+
const res = await fetch('file');
|
|
1707
1707
|
if (res.ok) {
|
|
1708
1708
|
const content = await res.text();
|
|
1709
1709
|
currentContent = content;
|