vg-coder-cli 2.0.24 → 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.
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Saved Commands Feature
3
+ * Manages saved terminal commands that can be quickly executed
4
+ */
5
+
6
+ let savedCommands = [];
7
+ let editingCommandId = null;
8
+
9
+ /**
10
+ * Initialize saved commands on page load
11
+ */
12
+ export async function initSavedCommands() {
13
+ await loadSavedCommands();
14
+ renderCommands();
15
+ }
16
+
17
+ /**
18
+ * Load saved commands from backend
19
+ */
20
+ async function loadSavedCommands() {
21
+ try {
22
+ const response = await fetch('/api/commands/load');
23
+ const data = await response.json();
24
+ savedCommands = data.commands || [];
25
+ renderCommands(); // Re-render after loading
26
+ } catch (error) {
27
+ console.error('Failed to load commands:', error);
28
+ savedCommands = [];
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Save commands to backend (debounced)
34
+ */
35
+ let saveTimeout = null;
36
+ async function saveCommands() {
37
+ clearTimeout(saveTimeout);
38
+ saveTimeout = setTimeout(async () => {
39
+ try {
40
+ const response = await fetch('/api/commands/save', {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ commands: savedCommands })
44
+ });
45
+ const data = await response.json();
46
+ if (data.success) {
47
+ console.log(`✓ Saved ${data.count} commands`);
48
+ }
49
+ } catch (error) {
50
+ console.error('Failed to save commands:', error);
51
+ }
52
+ }, 500);
53
+ }
54
+
55
+ /**
56
+ * Render commands in UI
57
+ */
58
+ function renderCommands() {
59
+ const container = document.getElementById('commands-list');
60
+ const emptyState = document.getElementById('commands-empty-state');
61
+
62
+ if (!container) return;
63
+
64
+ if (savedCommands.length === 0) {
65
+ container.innerHTML = '';
66
+ if (emptyState) emptyState.style.display = 'block';
67
+ return;
68
+ }
69
+
70
+ if (emptyState) emptyState.style.display = 'none';
71
+
72
+ container.innerHTML = savedCommands.map(cmd => `
73
+ <div class="command-card" onclick="window.runSavedCommand('${cmd.id}')">
74
+ <div class="command-card-main">
75
+ <span class="command-icon">${cmd.icon}</span>
76
+ <div class="command-info">
77
+ <div class="command-name">${escapeHtml(cmd.name)}</div>
78
+ <div class="command-text">${escapeHtml(cmd.command)}</div>
79
+ </div>
80
+ </div>
81
+ <div class="command-card-actions">
82
+ <button class="command-action-btn" onclick="window.editCommand('${cmd.id}'); event.stopPropagation();" title="Edit">
83
+ ✏️
84
+ </button>
85
+ <button class="command-action-btn" onclick="window.deleteCommand('${cmd.id}'); event.stopPropagation();" title="Delete">
86
+ 🗑️
87
+ </button>
88
+ </div>
89
+ </div>
90
+ `).join('');
91
+ }
92
+
93
+ /**
94
+ * Open add command modal
95
+ */
96
+ function openAddCommandModal() {
97
+ editingCommandId = null;
98
+ document.getElementById('modal-title').textContent = 'Add Command';
99
+ document.getElementById('command-icon').value = '🚀';
100
+ document.getElementById('command-name').value = '';
101
+ document.getElementById('command-text').value = '';
102
+ document.getElementById('command-modal').style.display = 'flex';
103
+ document.getElementById('command-name').focus();
104
+ }
105
+
106
+ /**
107
+ * Open edit command modal
108
+ */
109
+ function editCommand(id) {
110
+ const command = savedCommands.find(c => c.id === id);
111
+ if (!command) return;
112
+
113
+ editingCommandId = id;
114
+ document.getElementById('modal-title').textContent = 'Edit Command';
115
+ document.getElementById('command-icon').value = command.icon;
116
+ document.getElementById('command-name').value = command.name;
117
+ document.getElementById('command-text').value = command.command;
118
+ document.getElementById('command-modal').style.display = 'flex';
119
+ document.getElementById('command-name').focus();
120
+ }
121
+
122
+ /**
123
+ * Close command modal
124
+ */
125
+ function closeCommandModal() {
126
+ document.getElementById('command-modal').style.display = 'none';
127
+ editingCommandId = null;
128
+ }
129
+
130
+ /**
131
+ * Handle command form submit
132
+ */
133
+ function handleCommandFormSubmit(event) {
134
+ event.preventDefault();
135
+
136
+ const icon = document.getElementById('command-icon').value.trim();
137
+ const name = document.getElementById('command-name').value.trim();
138
+ const command = document.getElementById('command-text').value.trim();
139
+
140
+ if (!icon || !name || !command) return;
141
+
142
+ if (editingCommandId) {
143
+ // Edit existing
144
+ const index = savedCommands.findIndex(c => c.id === editingCommandId);
145
+ if (index !== -1) {
146
+ savedCommands[index] = { ...savedCommands[index], icon, name, command };
147
+ }
148
+ } else {
149
+ // Add new
150
+ savedCommands.push({
151
+ id: 'cmd_' + Date.now(),
152
+ icon,
153
+ name,
154
+ command
155
+ });
156
+ }
157
+
158
+ saveCommands();
159
+ renderCommands();
160
+ closeCommandModal();
161
+ }
162
+
163
+ /**
164
+ * Delete command with confirmation
165
+ */
166
+ function deleteCommand(id) {
167
+ const command = savedCommands.find(c => c.id === id);
168
+ if (!command) return;
169
+
170
+ if (confirm(`Delete command "${command.name}"?`)) {
171
+ savedCommands = savedCommands.filter(c => c.id !== id);
172
+ saveCommands();
173
+ renderCommands();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Run saved command - open terminal and copy command to clipboard
179
+ */
180
+ function runSavedCommand(id) {
181
+ const command = savedCommands.find(c => c.id === id);
182
+ if (!command) return;
183
+
184
+ // Create new terminal
185
+ if (typeof window.createNewTerminal === 'function') {
186
+ window.createNewTerminal();
187
+ }
188
+
189
+ // Copy command to clipboard
190
+ navigator.clipboard.writeText(command.command).then(() => {
191
+ // Show toast notification
192
+ if (typeof window.showToast === 'function') {
193
+ window.showToast(`📋 Copied: ${command.command}`, 'success');
194
+ }
195
+ }).catch(err => {
196
+ console.error('Failed to copy command:', err);
197
+ if (typeof window.showToast === 'function') {
198
+ window.showToast('Failed to copy command', 'error');
199
+ }
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Escape HTML to prevent XSS
205
+ */
206
+ function escapeHtml(text) {
207
+ const div = document.createElement('div');
208
+ div.textContent = text;
209
+ return div.innerHTML;
210
+ }
211
+
212
+ // Global exports for HTML onclick
213
+ window.openAddCommandModal = openAddCommandModal;
214
+ window.editCommand = editCommand;
215
+ window.closeCommandModal = closeCommandModal;
216
+ window.deleteCommand = deleteCommand;
217
+ window.runSavedCommand = runSavedCommand;
218
+ window.loadSavedCommands = loadSavedCommands; // Export for project switching
219
+
220
+ // Setup form submit handler
221
+ if (typeof document !== 'undefined') {
222
+ document.addEventListener('DOMContentLoaded', () => {
223
+ const form = document.getElementById('command-form');
224
+ if (form) {
225
+ form.addEventListener('submit', handleCommandFormSubmit);
226
+ }
227
+ });
228
+ }
229
+
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;