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.
@@ -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 leftPanel = qs('.left-panel');
4
+ const toolPanelContainer = getById('tool-panel-container');
5
5
  const handle = getById('resize-handler');
6
6
 
7
- if (!leftPanel || !handle) {
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 = leftPanel.getBoundingClientRect().width;
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
- const maxWidth = window.innerWidth - 300;
32
-
33
- if (newWidth < maxWidth) {
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"><span>>_</span> Terminal (${activeTerminals.size + 1})</div>
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;