vg-coder-cli 2.0.33 → 2.0.35

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
 
@@ -0,0 +1,148 @@
1
+ import { getById, qsa } from '../utils.js';
2
+
3
+ let activePanel = null;
4
+
5
+ /**
6
+ * Initialize Tool Window system
7
+ */
8
+ export function initToolWindow() {
9
+ const toolWindowBar = getById('tool-window-bar');
10
+ if (!toolWindowBar) {
11
+ console.warn('[ToolWindow] Tool window bar not found');
12
+ return;
13
+ }
14
+
15
+ // Attach event listeners to all tool window icons
16
+ const icons = qsa('.tool-window-icon');
17
+ icons.forEach(icon => {
18
+ icon.addEventListener('click', () => {
19
+ const panelId = icon.dataset.panel;
20
+ if (panelId) {
21
+ togglePanel(panelId);
22
+ }
23
+ });
24
+ });
25
+
26
+ console.log('[ToolWindow] Initialized');
27
+ }
28
+
29
+ /**
30
+ * Toggle a specific panel
31
+ * @param {string} panelId - Panel ID to toggle (e.g., 'project', 'git')
32
+ */
33
+ export function togglePanel(panelId) {
34
+ const panel = getById(`tool-panel-${panelId}`);
35
+ const container = getById('tool-panel-container');
36
+ const icon = qsa(`.tool-window-icon[data-panel="${panelId}"]`)[0];
37
+
38
+ if (!panel || !container) {
39
+ console.error('[ToolWindow] Panel or container not found:', panelId);
40
+ return;
41
+ }
42
+
43
+ // If clicking the same active panel, close it
44
+ if (activePanel === panelId) {
45
+ closeAllPanels();
46
+ return;
47
+ }
48
+
49
+ // Close all panels first
50
+ closeAllPanels();
51
+
52
+ // Open the new panel
53
+ setActivePanel(panelId);
54
+ container.classList.add('expanded');
55
+ panel.classList.add('active');
56
+ if (icon) icon.classList.add('active');
57
+
58
+ activePanel = panelId;
59
+
60
+ // Trigger panel-specific initialization if needed
61
+ triggerPanelInit(panelId);
62
+
63
+ console.log('[ToolWindow] Opened panel:', panelId);
64
+ }
65
+
66
+ /**
67
+ * Close all open panels
68
+ */
69
+ export function closeAllPanels() {
70
+ const container = getById('tool-panel-container');
71
+ const panels = qsa('.tool-panel');
72
+ const icons = qsa('.tool-window-icon');
73
+
74
+ if (container) {
75
+ container.classList.remove('expanded');
76
+ }
77
+
78
+ panels.forEach(panel => {
79
+ panel.classList.remove('active');
80
+ });
81
+
82
+ icons.forEach(icon => {
83
+ icon.classList.remove('active');
84
+ });
85
+
86
+ activePanel = null;
87
+
88
+ console.log('[ToolWindow] Closed all panels');
89
+ }
90
+
91
+ /**
92
+ * Set a panel as active (internal use)
93
+ * @param {string} panelId - Panel ID
94
+ */
95
+ function setActivePanel(panelId) {
96
+ const panels = qsa('.tool-panel');
97
+ panels.forEach(panel => {
98
+ if (panel.id === `tool-panel-${panelId}`) {
99
+ panel.classList.add('active');
100
+ } else {
101
+ panel.classList.remove('active');
102
+ }
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Trigger initialization for panel-specific features
108
+ * @param {string} panelId - Panel ID
109
+ */
110
+ function triggerPanelInit(panelId) {
111
+ // Dispatch custom event that panel modules can listen to
112
+ const event = new CustomEvent('tool-panel-opened', {
113
+ detail: { panelId }
114
+ });
115
+ document.dispatchEvent(event);
116
+ }
117
+
118
+ /**
119
+ * Get the currently active panel ID
120
+ * @returns {string|null} Active panel ID or null
121
+ */
122
+ export function getActivePanel() {
123
+ return activePanel;
124
+ }
125
+
126
+ /**
127
+ * Register a callback for when a panel is closed
128
+ * @param {Function} callback - Callback function
129
+ */
130
+ export function onPanelClose(callback) {
131
+ const container = getById('tool-panel-container');
132
+ if (container) {
133
+ const observer = new MutationObserver((mutations) => {
134
+ mutations.forEach((mutation) => {
135
+ if (mutation.attributeName === 'class') {
136
+ if (!container.classList.contains('expanded')) {
137
+ callback();
138
+ }
139
+ }
140
+ });
141
+ });
142
+ observer.observe(container, { attributes: true });
143
+ }
144
+ }
145
+
146
+ // Expose to window for HTML onclick if needed
147
+ window.toggleToolPanel = togglePanel;
148
+ window.closeToolPanels = closeAllPanels;
@@ -36,6 +36,17 @@ export function initEventHandlers() {
36
36
  }
37
37
  });
38
38
 
39
+ // Copy System Prompt
40
+ globalDispatcher.on(EVENT_TYPES.COPY_PROMPT, async (event) => {
41
+ console.log('[Handlers] Copy Prompt event received:', event);
42
+ try {
43
+ await navigator.clipboard.writeText(SYSTEM_PROMPT);
44
+ showToast('📋 Copied System Prompt', 'success');
45
+ } catch (err) {
46
+ showToast('Failed to copy: ' + err.message, 'error');
47
+ }
48
+ });
49
+
39
50
  console.log('[Handlers] Event handlers initialized');
40
51
  }
41
52