vg-coder-cli 2.0.34 → 2.0.36
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/README.md +1 -0
- package/dist/vg-coder-bundle.js +3 -3
- package/package.json +1 -2
- package/src/server/api-server.js +130 -0
- package/src/server/views/css/commands-panel.css +75 -0
- package/src/server/views/css/editor.css +1 -1
- package/src/server/views/css/git-panel.css +361 -0
- package/src/server/views/css/git-view.css +1 -77
- package/src/server/views/css/project-panel.css +242 -0
- package/src/server/views/css/shortcuts-help.css +165 -0
- package/src/server/views/css/tool-window.css +267 -0
- package/src/server/views/dashboard.css +1 -1
- package/src/server/views/dashboard.html +78 -102
- package/src/server/views/js/api.js +26 -0
- package/src/server/views/js/event-protocol.js +1 -0
- package/src/server/views/js/features/bubble-features/copy-prompt-feature.js +14 -0
- package/src/server/views/js/features/bubble-features/index.js +6 -1
- package/src/server/views/js/features/commands-panel.js +63 -0
- package/src/server/views/js/features/git-panel.js +481 -0
- package/src/server/views/js/features/git-view.js +79 -307
- package/src/server/views/js/features/keyboard-shortcuts.js +333 -0
- package/src/server/views/js/features/project-panel.js +452 -0
- package/src/server/views/js/features/resize.js +7 -10
- package/src/server/views/js/features/terminal.js +74 -1
- package/src/server/views/js/features/tool-window.js +154 -0
- package/src/server/views/js/handlers.js +32 -5
- package/src/server/views/js/main.js +17 -31
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { getStructure, analyzeProject, copyToClipboard, saveTreeState as apiSaveTreeState, loadTreeState as apiLoadTreeState } from '../api.js';
|
|
2
|
+
import { showToast, formatNumber, getById, qsa } from '../utils.js';
|
|
3
|
+
import { API_BASE } from '../config.js';
|
|
4
|
+
import { switchProject } from './project-switcher.js';
|
|
5
|
+
|
|
6
|
+
let currentStructureData = null;
|
|
7
|
+
let excludedPaths = new Set();
|
|
8
|
+
let saveStateTimeout = null;
|
|
9
|
+
let isInitialized = false;
|
|
10
|
+
|
|
11
|
+
// Expose switchProject to window for selector onChange
|
|
12
|
+
window.switchProject = switchProject;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Initialize Project Panel
|
|
16
|
+
*/
|
|
17
|
+
export function initProjectPanel() {
|
|
18
|
+
// Listen for panel open events
|
|
19
|
+
document.addEventListener('tool-panel-opened', (event) => {
|
|
20
|
+
if (event.detail.panelId === 'project') {
|
|
21
|
+
if (!isInitialized) {
|
|
22
|
+
renderProjectPanel();
|
|
23
|
+
loadProjectTree();
|
|
24
|
+
isInitialized = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Listen for project changes to update selector AND reload tree
|
|
30
|
+
window.addEventListener('project-switched', (e) => {
|
|
31
|
+
console.log('[ProjectPanel] 🔄 Project switched event detected:', e.detail);
|
|
32
|
+
|
|
33
|
+
// Update selector to reflect new project
|
|
34
|
+
loadProjectsIntoSelector();
|
|
35
|
+
|
|
36
|
+
// Reload project tree for new project
|
|
37
|
+
if (isInitialized) {
|
|
38
|
+
console.log('[ProjectPanel] Reloading project tree for new project...');
|
|
39
|
+
loadProjectTree();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Poll for project updates every 5 seconds (since socket.io may not be available in shadow DOM)
|
|
44
|
+
setInterval(() => {
|
|
45
|
+
if (getById('project-panel-selector')) {
|
|
46
|
+
loadProjectsIntoSelector();
|
|
47
|
+
}
|
|
48
|
+
}, 5000);
|
|
49
|
+
|
|
50
|
+
console.log('[ProjectPanel] Initialized with polling (Socket.IO not available in shadow DOM)');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Render the Project Panel UI structure
|
|
55
|
+
*/
|
|
56
|
+
function renderProjectPanel() {
|
|
57
|
+
const container = getById('project-panel-content');
|
|
58
|
+
if (!container) return;
|
|
59
|
+
|
|
60
|
+
// Get current project from main selector (if exists)
|
|
61
|
+
const mainProjectSelector = getById('project-selector');
|
|
62
|
+
const currentProject = mainProjectSelector ? mainProjectSelector.value : '';
|
|
63
|
+
|
|
64
|
+
container.innerHTML = `
|
|
65
|
+
<div class="project-panel-selector-wrapper">
|
|
66
|
+
<select id="project-panel-selector" class="project-panel-selector" title="Switch Project">
|
|
67
|
+
<option value="">Loading projects...</option>
|
|
68
|
+
</select>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="project-panel-actions-bar">
|
|
71
|
+
<button class="project-action-btn" id="project-refresh-btn" title="Refresh Project Tree">
|
|
72
|
+
<span>🔄</span>
|
|
73
|
+
<span>Refresh</span>
|
|
74
|
+
</button>
|
|
75
|
+
<button class="project-action-btn" id="project-copy-selected-btn" title="Copy Selected Files">
|
|
76
|
+
<span>📋</span>
|
|
77
|
+
<span>Copy</span>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="project-token-summary" id="project-token-summary">
|
|
81
|
+
<span>Selected:</span>
|
|
82
|
+
<span class="project-token-count" id="project-token-count">0 tokens</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="project-tree-wrapper" id="project-tree-wrapper">
|
|
85
|
+
<div class="project-loading">Loading project structure...</div>
|
|
86
|
+
</div>
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
// Load projects into selector
|
|
90
|
+
loadProjectsIntoSelector();
|
|
91
|
+
|
|
92
|
+
// Attach event listeners
|
|
93
|
+
const refreshBtn = getById('project-refresh-btn');
|
|
94
|
+
const copyBtn = getById('project-copy-selected-btn');
|
|
95
|
+
const projectSelector = getById('project-panel-selector');
|
|
96
|
+
|
|
97
|
+
if (refreshBtn) {
|
|
98
|
+
refreshBtn.addEventListener('click', loadProjectTree);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (copyBtn) {
|
|
102
|
+
copyBtn.addEventListener('click', handleCopySelected);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (projectSelector) {
|
|
106
|
+
console.log('[ProjectPanel] Attaching change listener to project selector');
|
|
107
|
+
projectSelector.addEventListener('change', (e) => {
|
|
108
|
+
const projectId = e.target.value;
|
|
109
|
+
console.log('[ProjectPanel] Project selector changed to:', projectId);
|
|
110
|
+
// Sync with main project selector
|
|
111
|
+
if (window.switchProject) {
|
|
112
|
+
window.switchProject(projectId);
|
|
113
|
+
} else {
|
|
114
|
+
console.warn('[ProjectPanel] window.switchProject not available');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Load projects into the panel selector
|
|
122
|
+
*/
|
|
123
|
+
async function loadProjectsIntoSelector() {
|
|
124
|
+
const selector = getById('project-panel-selector');
|
|
125
|
+
console.log('[ProjectPanel] loadProjectsIntoSelector called, selector exists:', !!selector);
|
|
126
|
+
|
|
127
|
+
if (!selector) {
|
|
128
|
+
console.warn('[ProjectPanel] Project selector not found in DOM');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Save current selection to preserve it
|
|
133
|
+
const currentValue = selector.value;
|
|
134
|
+
console.log('[ProjectPanel] Current selected project:', currentValue);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const apiBase = window.API_BASE || 'http://localhost:6868';
|
|
138
|
+
console.log('[ProjectPanel] Fetching projects from:', `${apiBase}/api/projects`);
|
|
139
|
+
|
|
140
|
+
const response = await fetch(`${apiBase}/api/projects`);
|
|
141
|
+
const data = await response.json();
|
|
142
|
+
|
|
143
|
+
console.log('[ProjectPanel] Received projects:', data);
|
|
144
|
+
|
|
145
|
+
if (data.projects && data.projects.length > 0) {
|
|
146
|
+
const options = data.projects.map(p =>
|
|
147
|
+
`<option value="${p.id}" ${p.isActive ? 'selected' : ''}>${p.name}</option>`
|
|
148
|
+
).join('');
|
|
149
|
+
|
|
150
|
+
console.log('[ProjectPanel] Updating selector with', data.projects.length, 'projects');
|
|
151
|
+
selector.innerHTML = options;
|
|
152
|
+
|
|
153
|
+
// IMPORTANT: Restore previous selection if it still exists
|
|
154
|
+
// This prevents auto-switching when new projects join
|
|
155
|
+
if (currentValue && data.projects.some(p => p.id === currentValue)) {
|
|
156
|
+
selector.value = currentValue;
|
|
157
|
+
console.log('[ProjectPanel] ✅ Restored previous selection:', currentValue);
|
|
158
|
+
} else {
|
|
159
|
+
console.log('[ProjectPanel] ℹ️ Using active project from API:', data.activeProjectId);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.warn('[ProjectPanel] No projects returned from API');
|
|
163
|
+
selector.innerHTML = '<option value="">No projects</option>';
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error('[ProjectPanel] Failed to load projects:', err);
|
|
167
|
+
selector.innerHTML = '<option value="">Error loading projects</option>';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Load and render project tree
|
|
175
|
+
*/
|
|
176
|
+
async function loadProjectTree() {
|
|
177
|
+
const wrapper = getById('project-tree-wrapper');
|
|
178
|
+
if (!wrapper) return;
|
|
179
|
+
|
|
180
|
+
wrapper.innerHTML = '<div class="project-loading">Loading project structure...</div>';
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// Get the current project path from the structure-path input (if exists) or use default
|
|
184
|
+
const pathInput = getById('structure-path');
|
|
185
|
+
const path = pathInput ? pathInput.value : '.';
|
|
186
|
+
|
|
187
|
+
const data = await getStructure(path);
|
|
188
|
+
currentStructureData = data.structure;
|
|
189
|
+
|
|
190
|
+
// Load saved state
|
|
191
|
+
await loadTreeState();
|
|
192
|
+
|
|
193
|
+
// Render tree
|
|
194
|
+
const treeHtml = generateProjectTree(currentStructureData);
|
|
195
|
+
wrapper.innerHTML = `<ul class="project-tree-ul">${treeHtml}</ul>`;
|
|
196
|
+
|
|
197
|
+
// IMPORTANT: Re-attach event listeners after rendering
|
|
198
|
+
attachProjectTreeListeners();
|
|
199
|
+
|
|
200
|
+
// Update token count
|
|
201
|
+
updateTokenCount();
|
|
202
|
+
|
|
203
|
+
showToast('Project tree loaded', 'success');
|
|
204
|
+
} catch (err) {
|
|
205
|
+
wrapper.innerHTML = `<div class="project-empty-state">Error loading project: ${err.message}</div>`;
|
|
206
|
+
showToast('Error loading project: ' + err.message, 'error');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Generate project tree HTML
|
|
212
|
+
*/
|
|
213
|
+
function generateProjectTree(node, depth = 0) {
|
|
214
|
+
if (!node) return '';
|
|
215
|
+
|
|
216
|
+
// Handle compact folders (merge single-child directories)
|
|
217
|
+
let currentNode = node;
|
|
218
|
+
let displayName = node.name;
|
|
219
|
+
|
|
220
|
+
if (currentNode.type === 'directory') {
|
|
221
|
+
while (
|
|
222
|
+
currentNode.children &&
|
|
223
|
+
currentNode.children.length === 1 &&
|
|
224
|
+
currentNode.children[0].type === 'directory'
|
|
225
|
+
) {
|
|
226
|
+
const child = currentNode.children[0];
|
|
227
|
+
displayName += '/' + child.name;
|
|
228
|
+
currentNode = child;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const isDir = currentNode.type === 'directory';
|
|
233
|
+
const hasChildren = isDir && currentNode.children && currentNode.children.length > 0;
|
|
234
|
+
const tokens = currentNode.tokens || 0;
|
|
235
|
+
|
|
236
|
+
let tokenClass = 'project-token-low';
|
|
237
|
+
if (tokens > 5000) tokenClass = 'project-token-high';
|
|
238
|
+
else if (tokens > 2000) tokenClass = 'project-token-med';
|
|
239
|
+
|
|
240
|
+
const icon = isDir ? '📁' : '📄';
|
|
241
|
+
const arrow = hasChildren ? '▼' : '';
|
|
242
|
+
const liClass = `project-tree-li ${hasChildren ? 'has-children' : ''}`;
|
|
243
|
+
|
|
244
|
+
const nodePath = currentNode.relativePath || currentNode.path;
|
|
245
|
+
const isExcluded = excludedPaths.has(nodePath);
|
|
246
|
+
|
|
247
|
+
let html = `<li class="${liClass}">`;
|
|
248
|
+
|
|
249
|
+
html += `
|
|
250
|
+
<div class="project-tree-row" data-path="${nodePath}" data-type="${currentNode.type}">
|
|
251
|
+
<span class="project-arrow">${arrow}</span>
|
|
252
|
+
<input type="checkbox" class="project-checkbox"
|
|
253
|
+
data-path="${nodePath}"
|
|
254
|
+
data-tokens="${tokens}"
|
|
255
|
+
data-type="${currentNode.type}"
|
|
256
|
+
${isExcluded ? '' : 'checked'}>
|
|
257
|
+
<span class="project-icon">${icon}</span>
|
|
258
|
+
<span class="project-name" title="${displayName}">${displayName}</span>
|
|
259
|
+
<span class="project-token-badge ${tokenClass}">${formatNumber(tokens)}</span>
|
|
260
|
+
</div>
|
|
261
|
+
`;
|
|
262
|
+
|
|
263
|
+
if (hasChildren) {
|
|
264
|
+
html += '<ul class="project-tree-ul">';
|
|
265
|
+
currentNode.children.forEach(child => {
|
|
266
|
+
html += generateProjectTree(child, depth + 1);
|
|
267
|
+
});
|
|
268
|
+
html += '</ul>';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
html += '</li>';
|
|
272
|
+
return html;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle tree interactions after render
|
|
277
|
+
*/
|
|
278
|
+
export function attachProjectTreeListeners() {
|
|
279
|
+
const wrapper = getById('project-tree-wrapper');
|
|
280
|
+
if (!wrapper) return;
|
|
281
|
+
|
|
282
|
+
// Delegate events
|
|
283
|
+
wrapper.addEventListener('click', (e) => {
|
|
284
|
+
const row = e.target.closest('.project-tree-row');
|
|
285
|
+
if (!row) return;
|
|
286
|
+
|
|
287
|
+
// If clicking checkbox, don't toggle folder
|
|
288
|
+
if (e.target.classList.contains('project-checkbox')) {
|
|
289
|
+
handleCheckboxChange(e);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const type = row.dataset.type;
|
|
294
|
+
const path = row.dataset.path;
|
|
295
|
+
|
|
296
|
+
if (type === 'directory') {
|
|
297
|
+
console.log('[ProjectPanel] Toggling directory:', path);
|
|
298
|
+
// Toggle folder
|
|
299
|
+
const li = row.closest('.project-tree-li');
|
|
300
|
+
if (li && li.classList.contains('has-children')) {
|
|
301
|
+
li.classList.toggle('collapsed');
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
console.log('[ProjectPanel] 🔥 FILE CLICKED:', path);
|
|
305
|
+
// Open file in editor
|
|
306
|
+
const fileName = row.querySelector('.project-name').textContent;
|
|
307
|
+
console.log('[ProjectPanel] File name:', fileName);
|
|
308
|
+
console.log('[ProjectPanel] window.openFileTab exists:', typeof window.openFileTab !== 'undefined');
|
|
309
|
+
|
|
310
|
+
if (window.openFileTab) {
|
|
311
|
+
console.log('[ProjectPanel] Opening file tab:', path, fileName);
|
|
312
|
+
window.openFileTab(path, fileName);
|
|
313
|
+
} else {
|
|
314
|
+
console.error('[ProjectPanel] window.openFileTab is not defined!');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Highlight selected row
|
|
318
|
+
qsa('.project-tree-row').forEach(r => r.classList.remove('selected'));
|
|
319
|
+
row.classList.add('selected');
|
|
320
|
+
console.log('[ProjectPanel] Row highlighted');
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Checkbox changes
|
|
325
|
+
wrapper.addEventListener('change', (e) => {
|
|
326
|
+
if (e.target.classList.contains('project-checkbox')) {
|
|
327
|
+
handleCheckboxChange(e);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Handle checkbox state change
|
|
334
|
+
*/
|
|
335
|
+
function handleCheckboxChange(event) {
|
|
336
|
+
const checkbox = event.target;
|
|
337
|
+
const isChecked = checkbox.checked;
|
|
338
|
+
const path = checkbox.dataset.path;
|
|
339
|
+
|
|
340
|
+
if (isChecked) {
|
|
341
|
+
excludedPaths.delete(path);
|
|
342
|
+
} else {
|
|
343
|
+
excludedPaths.add(path);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update children
|
|
347
|
+
const li = checkbox.closest('.project-tree-li');
|
|
348
|
+
if (li) {
|
|
349
|
+
const childCheckboxes = li.querySelectorAll('.project-checkbox');
|
|
350
|
+
childCheckboxes.forEach(child => {
|
|
351
|
+
child.checked = isChecked;
|
|
352
|
+
const childPath = child.dataset.path;
|
|
353
|
+
if (isChecked) {
|
|
354
|
+
excludedPaths.delete(childPath);
|
|
355
|
+
} else {
|
|
356
|
+
excludedPaths.add(childPath);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
updateTokenCount();
|
|
362
|
+
debouncedSaveState();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Update token count summary
|
|
367
|
+
*/
|
|
368
|
+
function updateTokenCount() {
|
|
369
|
+
const checkedFiles = qsa('.project-checkbox[data-type="file"]:checked');
|
|
370
|
+
let total = 0;
|
|
371
|
+
checkedFiles.forEach(box => {
|
|
372
|
+
const tokens = parseInt(box.dataset.tokens || '0');
|
|
373
|
+
total += tokens;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const countEl = getById('project-token-count');
|
|
377
|
+
if (countEl) {
|
|
378
|
+
countEl.textContent = `${formatNumber(total)} tokens`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Handle copy selected files
|
|
384
|
+
*/
|
|
385
|
+
async function handleCopySelected() {
|
|
386
|
+
const checkedBoxes = qsa('.project-checkbox[data-type="file"]:checked');
|
|
387
|
+
const checkedPaths = [];
|
|
388
|
+
|
|
389
|
+
checkedBoxes.forEach(box => {
|
|
390
|
+
if (box.dataset.path) checkedPaths.push(box.dataset.path);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (checkedPaths.length === 0) {
|
|
394
|
+
showToast('No files selected', 'error');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const btn = getById('project-copy-selected-btn');
|
|
399
|
+
if (btn) btn.disabled = true;
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const pathInput = getById('structure-path');
|
|
403
|
+
const path = pathInput ? pathInput.value : '.';
|
|
404
|
+
|
|
405
|
+
const content = await analyzeProject(path, checkedPaths);
|
|
406
|
+
await copyToClipboard(content);
|
|
407
|
+
|
|
408
|
+
showToast(`Copied ${checkedPaths.length} files to clipboard!`, 'success');
|
|
409
|
+
} catch (err) {
|
|
410
|
+
showToast('Copy error: ' + err.message, 'error');
|
|
411
|
+
} finally {
|
|
412
|
+
if (btn) btn.disabled = false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Load saved tree state
|
|
418
|
+
*/
|
|
419
|
+
async function loadTreeState() {
|
|
420
|
+
try {
|
|
421
|
+
const data = await apiLoadTreeState();
|
|
422
|
+
excludedPaths = new Set(data.excludedPaths || []);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.error('[ProjectPanel] Failed to load tree state:', err);
|
|
425
|
+
excludedPaths = new Set();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Debounced save state
|
|
431
|
+
*/
|
|
432
|
+
function debouncedSaveState() {
|
|
433
|
+
if (saveStateTimeout) clearTimeout(saveStateTimeout);
|
|
434
|
+
saveStateTimeout = setTimeout(() => {
|
|
435
|
+
saveTreeState();
|
|
436
|
+
}, 500);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Save tree state
|
|
441
|
+
*/
|
|
442
|
+
async function saveTreeState() {
|
|
443
|
+
try {
|
|
444
|
+
const excludedArray = Array.from(excludedPaths);
|
|
445
|
+
await apiSaveTreeState(excludedArray);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.error('[ProjectPanel] Failed to save tree state:', err);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Export loadProjectTree wrapped with listener attachment
|
|
452
|
+
export { loadProjectTree };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { qs, getById } from '../utils.js';
|
|
2
2
|
|
|
3
3
|
export function initResizeHandler() {
|
|
4
|
-
const
|
|
4
|
+
const toolPanelContainer = getById('tool-panel-container');
|
|
5
5
|
const handle = getById('resize-handler');
|
|
6
6
|
|
|
7
|
-
if (!
|
|
7
|
+
if (!toolPanelContainer || !handle) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ export function initResizeHandler() {
|
|
|
15
15
|
handle.addEventListener('mousedown', (e) => {
|
|
16
16
|
isResizing = true;
|
|
17
17
|
startX = e.clientX;
|
|
18
|
-
startWidth =
|
|
18
|
+
startWidth = toolPanelContainer.getBoundingClientRect().width;
|
|
19
19
|
|
|
20
20
|
document.body.classList.add('resizing');
|
|
21
21
|
e.preventDefault();
|
|
@@ -27,13 +27,10 @@ export function initResizeHandler() {
|
|
|
27
27
|
requestAnimationFrame(() => {
|
|
28
28
|
const currentX = e.clientX;
|
|
29
29
|
const diffX = currentX - startX;
|
|
30
|
-
const newWidth = Math.max(250, startWidth + diffX);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
leftPanel.style.flex = `0 0 ${newWidth}px`;
|
|
35
|
-
leftPanel.style.width = `${newWidth}px`;
|
|
36
|
-
}
|
|
30
|
+
const newWidth = Math.max(250, Math.min(600, startWidth + diffX)); // Min 250px, Max 600px
|
|
31
|
+
|
|
32
|
+
// Update width of tool-panel-container
|
|
33
|
+
toolPanelContainer.style.width = `${newWidth}px`;
|
|
37
34
|
});
|
|
38
35
|
});
|
|
39
36
|
|
|
@@ -2,6 +2,7 @@ import { io } from 'socket.io-client';
|
|
|
2
2
|
import { Terminal } from 'xterm';
|
|
3
3
|
import { FitAddon } from 'xterm-addon-fit';
|
|
4
4
|
import { getById, showToast } from '../utils.js';
|
|
5
|
+
import { countTokens as countTokensAPI } from '../api.js';
|
|
5
6
|
|
|
6
7
|
let socket;
|
|
7
8
|
const activeTerminals = new Map();
|
|
@@ -43,8 +44,12 @@ export function createNewTerminal() {
|
|
|
43
44
|
// --- HTML STRUCTURE: Header + Body + Separate Input ---
|
|
44
45
|
wrapper.innerHTML = `
|
|
45
46
|
<div class="terminal-header" id="header-${termId}">
|
|
46
|
-
<div class="terminal-title-group"
|
|
47
|
+
<div class="terminal-title-group">
|
|
48
|
+
<span>>_</span> Terminal (${activeTerminals.size + 1})
|
|
49
|
+
<span class="terminal-token-count" id="token-count-${termId}">0 tokens</span>
|
|
50
|
+
</div>
|
|
47
51
|
<div class="terminal-controls">
|
|
52
|
+
<button class="term-btn copy-logs" onclick="window.copyTerminalLogs('${termId}')" title="Copy Logs">📋</button>
|
|
48
53
|
<button class="term-btn minimize" onclick="window.toggleMinimize('${termId}')">-</button>
|
|
49
54
|
<button class="term-btn maximize" onclick="window.toggleMaximize('${termId}')">+</button>
|
|
50
55
|
<button class="term-btn close" onclick="window.closeTerminal('${termId}')">x</button>
|
|
@@ -173,6 +178,73 @@ export function toggleMaximize(termId) {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Copy terminal logs to clipboard with token count
|
|
183
|
+
*/
|
|
184
|
+
export async function copyTerminalLogs(termId) {
|
|
185
|
+
const session = activeTerminals.get(termId);
|
|
186
|
+
if (!session) {
|
|
187
|
+
showToast('Terminal not found', 'error');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
// Get terminal buffer content
|
|
193
|
+
const term = session.term;
|
|
194
|
+
const buffer = term.buffer.active;
|
|
195
|
+
const lines = [];
|
|
196
|
+
|
|
197
|
+
// Extract all lines from buffer
|
|
198
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
199
|
+
const line = buffer.getLine(i);
|
|
200
|
+
if (line) {
|
|
201
|
+
lines.push(line.translateToString(true));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const logsText = lines.join('\n').trim();
|
|
206
|
+
|
|
207
|
+
if (!logsText) {
|
|
208
|
+
showToast('No logs to copy', 'info');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Copy to clipboard first (fast)
|
|
213
|
+
await navigator.clipboard.writeText(logsText);
|
|
214
|
+
|
|
215
|
+
// Then count tokens via API (accurate)
|
|
216
|
+
try {
|
|
217
|
+
const tokens = await countTokensAPI(logsText);
|
|
218
|
+
|
|
219
|
+
// Update token count display
|
|
220
|
+
updateTokenCount(termId, tokens);
|
|
221
|
+
|
|
222
|
+
// Show toast
|
|
223
|
+
showToast(`📋 Copied ${tokens.toLocaleString()} tokens`, 'success');
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.error('[Terminal] Token counting error:', err);
|
|
226
|
+
// Still copied to clipboard, just show basic message
|
|
227
|
+
showToast('📋 Copied to clipboard', 'success');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error('[Terminal] Copy logs error:', err);
|
|
232
|
+
showToast('Error copying logs', 'error');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Update token count display in terminal header
|
|
238
|
+
*/
|
|
239
|
+
function updateTokenCount(termId, tokens) {
|
|
240
|
+
const tokenEl = getById(`token-count-${termId}`);
|
|
241
|
+
if (tokenEl) {
|
|
242
|
+
tokenEl.textContent = `${tokens.toLocaleString()} tokens`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Remove old simple countTokens function - no longer needed
|
|
247
|
+
|
|
176
248
|
function makeDraggable(element, handle) {
|
|
177
249
|
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
|
178
250
|
handle.onmousedown = dragMouseDown;
|
|
@@ -206,3 +278,4 @@ window.createNewTerminal = createNewTerminal;
|
|
|
206
278
|
window.closeTerminal = closeTerminalUI;
|
|
207
279
|
window.toggleMinimize = toggleMinimize;
|
|
208
280
|
window.toggleMaximize = toggleMaximize;
|
|
281
|
+
window.copyTerminalLogs = copyTerminalLogs;
|