vg-coder-cli 2.0.25 → 2.0.26

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.
@@ -60,20 +60,39 @@
60
60
  <!-- CỘT TRÁI: Giao diện VG Coder -->
61
61
  <div class="left-panel">
62
62
  <div class="container">
63
+ <!-- Header Layout Redesign -->
63
64
  <div class="header">
64
- <div class="header-content">
65
- <!-- Status Dot -->
66
- <span class="status" id="status" style="font-size: 14px;">●</span>
67
-
68
- <!-- NEW: Project Info Section -->
69
- <div class="project-info">
70
- <div class="project-name" id="project-name">Loading...</div>
71
- <div class="project-meta" id="project-meta">...</div>
65
+ <!-- Top Row: Status, Switcher (Title), Actions -->
66
+ <div class="header-top-row">
67
+ <div class="header-left-group">
68
+ <span class="status" id="status" style="font-size: 14px;">●</span>
69
+
70
+ <!-- Project Switcher takes primary spot -->
71
+ <div class="project-switcher">
72
+ <select id="project-selector" class="project-selector" onchange="window.switchProject(this.value)" title="Switch Project">
73
+ <option value="">Loading...</option>
74
+ </select>
75
+ <span class="project-count" id="project-count">1 project</span>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Right Actions -->
80
+ <div class="header-actions">
81
+ <button class="stop-server-btn" id="stop-server-btn" onclick="window.stopServer()" title="Stop Server">
82
+ 🛑
83
+ </button>
84
+ <button class="theme-toggle" id="theme-toggle" title="Toggle Dark Mode">
85
+ <span id="theme-icon">🌙</span>
86
+ </button>
72
87
  </div>
73
88
  </div>
74
- <button class="theme-toggle" id="theme-toggle" title="Toggle Dark Mode">
75
- <span id="theme-icon">🌙</span>
76
- </button>
89
+
90
+ <!-- Bottom Row: Meta Info (Path/Type) -->
91
+ <div class="header-bottom-row">
92
+ <div class="project-meta" id="project-meta">...</div>
93
+ <!-- Hidden element to keep JS happy -->
94
+ <div id="project-name" style="display: none;"></div>
95
+ </div>
77
96
  </div>
78
97
 
79
98
  <!-- Saved Commands Panel -->
@@ -324,6 +343,70 @@
324
343
  </div>
325
344
  </div>
326
345
 
346
+ <!-- VG Coder Parent Context Detector -->
347
+ <script>
348
+ // Listener for nested iframe detection
349
+ // When extension's controller.ts sends VG_CODER_PING, we reply with VG_CODER_PARENT
350
+ window.addEventListener('message', (event) => {
351
+ if (event.data?.type === 'VG_CODER_PING') {
352
+ // Confirm this is VG Coder parent context
353
+ event.source.postMessage({ type: 'VG_CODER_PARENT' }, '*');
354
+ console.log('📡 Responded to VG_CODER_PING from iframe:', event.origin);
355
+ }
356
+ });
357
+ </script>
358
+
359
+ <!-- Embedded Mode Detection -->
360
+ <script>
361
+ // When dashboard is loaded with ?embedded=true (inside AI chat page),
362
+ // hide the AI tab and iframe to prevent nested structure
363
+ (function() {
364
+ const urlParams = new URLSearchParams(window.location.search);
365
+ const isEmbedded = urlParams.has('embedded');
366
+
367
+ if (isEmbedded) {
368
+ console.log('🎯 Embedded mode detected - hiding AI iframe tab');
369
+
370
+ // Wait for DOM to be ready
371
+ if (document.readyState === 'loading') {
372
+ document.addEventListener('DOMContentLoaded', hideAITab);
373
+ } else {
374
+ hideAITab();
375
+ }
376
+
377
+ function hideAITab() {
378
+ // Hide AI tab
379
+ const aiTab = document.getElementById('ai-tab');
380
+ if (aiTab) {
381
+ aiTab.style.display = 'none';
382
+ console.log('✅ AI tab hidden');
383
+ }
384
+
385
+ // Hide AI iframe container
386
+ const aiContainer = document.querySelector('.ai-iframe-container');
387
+ if (aiContainer) {
388
+ aiContainer.style.display = 'none';
389
+ console.log('✅ AI iframe container hidden');
390
+ }
391
+
392
+ // Switch to first editor tab if exists, or show monaco by default
393
+ const fileTabsContainer = document.getElementById('file-tabs-container');
394
+ if (fileTabsContainer && fileTabsContainer.children.length > 0) {
395
+ // Click first file tab
396
+ const firstTab = fileTabsContainer.children[0];
397
+ if (firstTab) firstTab.click();
398
+ } else {
399
+ // Show monaco editor as default
400
+ const monacoContainer = document.getElementById('monaco-container');
401
+ if (monacoContainer) {
402
+ monacoContainer.classList.remove('view-mode-hidden');
403
+ }
404
+ }
405
+ }
406
+ }
407
+ })();
408
+ </script>
409
+
327
410
  <!-- Smart Log Copy Utilities -->
328
411
  <script src="/js/utils/log-utils.js"></script>
329
412
  <script src="/js/utils/smart-copy-engine.js"></script>
@@ -22,6 +22,7 @@ async function loadSavedCommands() {
22
22
  const response = await fetch('/api/commands/load');
23
23
  const data = await response.json();
24
24
  savedCommands = data.commands || [];
25
+ renderCommands(); // Re-render after loading
25
26
  } catch (error) {
26
27
  console.error('Failed to load commands:', error);
27
28
  savedCommands = [];
@@ -214,6 +215,7 @@ window.editCommand = editCommand;
214
215
  window.closeCommandModal = closeCommandModal;
215
216
  window.deleteCommand = deleteCommand;
216
217
  window.runSavedCommand = runSavedCommand;
218
+ window.loadSavedCommands = loadSavedCommands; // Export for project switching
217
219
 
218
220
  // Setup form submit handler
219
221
  if (typeof document !== 'undefined') {
@@ -225,4 +227,4 @@ if (typeof document !== 'undefined') {
225
227
  });
226
228
  }
227
229
 
228
- export { openAddCommandModal, editCommand, deleteCommand, runSavedCommand };
230
+ export { openAddCommandModal, editCommand, deleteCommand, runSavedCommand, loadSavedCommands };
@@ -37,13 +37,17 @@ export function initIframeManager() {
37
37
  const updateProvider = (providerId) => {
38
38
  const provider = AI_PROVIDERS.find(p => p.id === providerId) || AI_PROVIDERS[0];
39
39
 
40
+ // Add VG Coder context parameter to prevent nested iframe injection
41
+ const urlWithParam = new URL(provider.url);
42
+ urlWithParam.searchParams.set('vg_coder_context', 'true');
43
+
40
44
  // Reset iframe source to trigger reload
41
45
  iframe.src = 'about:blank';
42
46
  setTimeout(() => {
43
- iframe.src = provider.url;
47
+ iframe.src = urlWithParam.toString();
44
48
  }, 50);
45
49
 
46
- // Update placeholder link only (external link removed from UI)
50
+ // Update placeholder link (without the parameter for direct access)
47
51
  if (placeholderLink) {
48
52
  placeholderLink.href = provider.url;
49
53
  placeholderLink.textContent = `Mở ${provider.name} tab mới ↗`;
@@ -0,0 +1,153 @@
1
+ // Project Switcher - Multi-project management UI
2
+
3
+ /**
4
+ * Initialize project switcher
5
+ */
6
+ export async function initProjectSwitcher() {
7
+ // Load initial project list
8
+ await loadProjects();
9
+
10
+ // Poll for project updates every 5 seconds
11
+ setInterval(loadProjects, 5000);
12
+
13
+ // Listen for socket events
14
+ setupSocketListeners();
15
+ }
16
+
17
+ /**
18
+ * Load and display all projects
19
+ */
20
+ async function loadProjects() {
21
+ try {
22
+ const response = await fetch('/api/projects');
23
+ const data = await response.json();
24
+
25
+ updateProjectSelector(data.projects, data.activeProjectId);
26
+ updateProjectCount(data.totalProjects);
27
+
28
+ } catch (error) {
29
+ console.error('Failed to load projects:', error);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Update project selector dropdown
35
+ */
36
+ function updateProjectSelector(projects, activeProjectId) {
37
+ const selector = document.getElementById('project-selector');
38
+ if (!selector) return;
39
+
40
+ // Clear existing options
41
+ selector.innerHTML = '';
42
+
43
+ // Add projects
44
+ projects.forEach(project => {
45
+ const option = document.createElement('option');
46
+ option.value = project.id;
47
+ option.textContent = project.name;
48
+ option.selected = project.id === activeProjectId;
49
+ selector.appendChild(option);
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Update project count badge
55
+ */
56
+ function updateProjectCount(count) {
57
+ const badge = document.getElementById('project-count');
58
+ if (!badge) return;
59
+
60
+ badge.textContent = `${count} project${count !== 1 ? 's' : ''}`;
61
+ }
62
+
63
+ /**
64
+ * Switch to a different project
65
+ */
66
+ export async function switchProject(projectId) {
67
+ try {
68
+ const response = await fetch('/api/projects/switch', {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ projectId })
72
+ });
73
+
74
+ const data = await response.json();
75
+
76
+ if (data.success) {
77
+ // Emit custom event for other components to react
78
+ const event = new CustomEvent('project-switched', {
79
+ detail: {
80
+ projectId,
81
+ projectName: data.project.name,
82
+ project: data.project
83
+ }
84
+ });
85
+ window.dispatchEvent(event);
86
+
87
+ // Show toast
88
+ showToast(`Switched to: ${data.project.name}`, 'success');
89
+ } else {
90
+ showToast('Failed to switch project', 'error');
91
+ }
92
+ } catch (error) {
93
+ console.error('Failed to switch project:', error);
94
+ showToast('Error switching project', 'error');
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Setup socket listeners for real-time updates
100
+ */
101
+ function setupSocketListeners() {
102
+ if (typeof io === 'undefined') return;
103
+
104
+ const socket = io();
105
+
106
+ // New project registered
107
+ socket.on('project:registered', (data) => {
108
+ console.log('New project registered:', data);
109
+ loadProjects();
110
+ showToast(`New project joined: ${data.name}`, 'info');
111
+ });
112
+
113
+ // Project switched
114
+ socket.on('project:switched', (data) => {
115
+ console.log('Project switched:', data);
116
+ loadProjects();
117
+ });
118
+
119
+ // Project removed
120
+ socket.on('project:removed', (data) => {
121
+ console.log('Project removed:', data);
122
+ loadProjects();
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Show toast notification
128
+ */
129
+ function showToast(message, type = 'info') {
130
+ const toast = document.getElementById('toast');
131
+ if (!toast) return;
132
+
133
+ toast.textContent = message;
134
+ toast.className = 'toast show';
135
+
136
+ if (type === 'success') {
137
+ toast.style.background = '#28a745';
138
+ } else if (type === 'error') {
139
+ toast.style.background = '#dc3545';
140
+ } else if (type === 'warning') {
141
+ toast.style.background = '#ffc107';
142
+ toast.style.color = '#000';
143
+ } else {
144
+ toast.style.background = '#007bff';
145
+ }
146
+
147
+ setTimeout(() => {
148
+ toast.classList.remove('show');
149
+ }, 3000);
150
+ }
151
+
152
+ // Export for global access
153
+ window.switchProject = switchProject;
@@ -184,12 +184,33 @@ export function createNewTerminal() {
184
184
  }
185
185
  });
186
186
 
187
- // 7. Init Backend Process
188
- socket.emit('terminal:init', {
189
- termId,
190
- cols: term.cols,
191
- rows: term.rows
192
- });
187
+ // 7. Get current project ID from API
188
+ let currentProjectId = null;
189
+ fetch('/api/projects')
190
+ .then(res => res.json())
191
+ .then(data => {
192
+ currentProjectId = data.activeProjectId;
193
+
194
+ // Store in session
195
+ activeTerminals.get(termId).projectId = currentProjectId;
196
+
197
+ // Init Backend Process with projectId
198
+ socket.emit('terminal:init', {
199
+ termId,
200
+ cols: term.cols,
201
+ rows: term.rows,
202
+ projectId: currentProjectId
203
+ });
204
+ })
205
+ .catch(err => {
206
+ console.error('Failed to get project info:', err);
207
+ // Fallback without projectId
208
+ socket.emit('terminal:init', {
209
+ termId,
210
+ cols: term.cols,
211
+ rows: term.rows
212
+ });
213
+ });
193
214
 
194
215
  // 8. Initial token count update
195
216
  setTimeout(() => updateTokenCounts(termId), 100);
@@ -477,6 +498,22 @@ function clearTerminal(termId) {
477
498
  showToast('🗑️ Terminal cleared', 'info');
478
499
  }
479
500
 
501
+ /**
502
+ * Update terminal visibility based on active project
503
+ * @param {string} activeProjectId - Active project ID
504
+ */
505
+ function updateTerminalVisibility(activeProjectId) {
506
+ activeTerminals.forEach((session, termId) => {
507
+ const shouldShow = !session.projectId || session.projectId === activeProjectId;
508
+ session.element.style.display = shouldShow ? 'block' : 'none';
509
+ });
510
+
511
+ const visibleCount = Array.from(activeTerminals.values())
512
+ .filter(s => s.element.style.display !== 'none').length;
513
+
514
+ console.log(`Updated terminal visibility: ${visibleCount} visible for project ${activeProjectId}`);
515
+ }
516
+
480
517
  // Global Exports for HTML onclick
481
518
  window.createNewTerminal = createNewTerminal;
482
519
  window.closeTerminal = closeTerminalUI;
@@ -484,3 +521,4 @@ window.toggleMinimize = toggleMinimize;
484
521
  window.toggleMaximize = toggleMaximize;
485
522
  window.copyTerminalLog = copyTerminalLog;
486
523
  window.clearTerminal = clearTerminal;
524
+ window.updateTerminalVisibility = updateTerminalVisibility;
@@ -10,6 +10,7 @@ import { initEditorTabs } from './features/editor-tabs.js';
10
10
  import { initMonaco, updateMonacoTheme } from './features/monaco-manager.js';
11
11
  import { initResizeHandler } from './features/resize.js';
12
12
  import { initSavedCommands } from './features/commands.js';
13
+ import { initProjectSwitcher } from './features/project-switcher.js';
13
14
 
14
15
  document.addEventListener('DOMContentLoaded', async () => {
15
16
  // Load system prompt text
@@ -47,12 +48,49 @@ document.addEventListener('DOMContentLoaded', async () => {
47
48
  // Initialize Saved Commands
48
49
  initSavedCommands();
49
50
 
51
+ // Initialize Project Switcher
52
+ await initProjectSwitcher();
53
+
50
54
  // Set default tab to AI Assistant
51
55
  if (window.switchTab) {
52
56
  window.switchTab('ai-assistant');
53
57
  }
54
58
  });
55
59
 
60
+ // Global event handler for project switches
61
+ window.addEventListener('project-switched', async (event) => {
62
+ const { projectId, projectName, project } = event.detail;
63
+ console.log(`Project switched to: ${projectName}`);
64
+
65
+ // Reload project info
66
+ await loadProjectInfo();
67
+
68
+ // Filter terminal visibility (show only terminals for active project)
69
+ if (window.updateTerminalVisibility) {
70
+ window.updateTerminalVisibility(projectId);
71
+ }
72
+
73
+ // Reload saved commands for new project
74
+ if (window.loadSavedCommands) {
75
+ await window.loadSavedCommands();
76
+ }
77
+
78
+ // Reset tree view (hide it)
79
+ const treeContainer = document.getElementById('structure-tree');
80
+ if (treeContainer) {
81
+ treeContainer.style.display = 'none';
82
+ }
83
+
84
+ // Clear tree content
85
+ const treeContent = document.getElementById('tree-content');
86
+ if (treeContent) {
87
+ treeContent.innerHTML = '';
88
+ }
89
+
90
+ // TODO: Refresh other context-dependent components
91
+ // - Git view refresh
92
+ });
93
+
56
94
  async function checkServerStatus() {
57
95
  const statusEl = document.getElementById('status');
58
96
  const isHealthy = await checkHealth();
@@ -173,3 +211,26 @@ window.copyChromeUrl = function(event) {
173
211
  }, 2000);
174
212
  });
175
213
  }
214
+
215
+ window.stopServer = async function() {
216
+ if (!confirm('Are you sure you want to stop the server?')) {
217
+ return;
218
+ }
219
+
220
+ try {
221
+ await fetch('/api/shutdown', { method: 'POST' });
222
+ showToast('Server stopped successfully', 'success');
223
+
224
+ // Show a message that server is stopped
225
+ setTimeout(() => {
226
+ document.body.innerHTML = `
227
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh; flex-direction: column; gap: 20px;">
228
+ <h2>🛑 Server Stopped</h2>
229
+ <p>You can close this tab now.</p>
230
+ </div>
231
+ `;
232
+ }, 1000);
233
+ } catch (error) {
234
+ console.error('Failed to stop server:', error);
235
+ }
236
+ }