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.
@@ -46,12 +46,34 @@ export function createNewTerminal() {
46
46
  wrapper.style.left = `${400 + offset}px`;
47
47
  wrapper.style.zIndex = ++maxZIndex;
48
48
 
49
- // HTML Template - Đã thêm sự kiện onclick cho nút Minimize
49
+ // HTML Template - Đã thêm Copy Buttons
50
50
  wrapper.innerHTML = `
51
51
  <div class="terminal-header" id="header-${termId}" ondblclick="window.toggleMinimize('${termId}')">
52
52
  <div class="terminal-title-group">
53
53
  <span>>_</span> Terminal (${activeTerminals.size + 1})
54
54
  </div>
55
+
56
+ <!-- Copy Button Group -->
57
+ <div class="terminal-copy-group" id="copy-group-${termId}">
58
+ <button class="copy-btn copy-smart" onclick="window.copyTerminalLog('${termId}', 'smart')" title="Smart Copy (optimized for 3000 tokens)">
59
+ 🧠 <span class="token-badge" id="badge-smart-${termId}">0</span>
60
+ </button>
61
+ <button class="copy-btn copy-errors" onclick="window.copyTerminalLog('${termId}', 'errors')" title="Errors Only (with context)">
62
+ ⚠️ <span class="token-badge" id="badge-errors-${termId}">0</span>
63
+ </button>
64
+ <button class="copy-btn copy-recent" onclick="window.copyTerminalLog('${termId}', 'recent')" title="Recent 200 lines">
65
+ 📄 <span class="token-badge" id="badge-recent-${termId}">0</span>
66
+ </button>
67
+ <button class="copy-btn copy-all" onclick="window.copyTerminalLog('${termId}', 'all')" title="Copy All">
68
+ 📦 <span class="token-badge" id="badge-all-${termId}">0</span>
69
+ </button>
70
+ </div>
71
+
72
+ <!-- Clear Button -->
73
+ <button class="term-btn-clear" onclick="window.clearTerminal('${termId}')" title="Clear Terminal">
74
+ 🗑️
75
+ </button>
76
+
55
77
  <div class="terminal-controls">
56
78
  <button class="term-btn minimize" onclick="window.toggleMinimize('${termId}')" title="Minimize/Restore">-</button>
57
79
  <button class="term-btn maximize" onclick="window.toggleMaximize('${termId}')" title="Maximize">+</button>
@@ -111,15 +133,90 @@ export function createNewTerminal() {
111
133
  // 4. Make Draggable
112
134
  makeDraggable(wrapper, document.getElementById(`header-${termId}`));
113
135
 
114
- // 5. Store Session
115
- activeTerminals.set(termId, { term, fitAddon, element: wrapper });
136
+ // 5. Store Session with log buffer
137
+ activeTerminals.set(termId, {
138
+ term,
139
+ fitAddon,
140
+ element: wrapper,
141
+ logBuffer: [], // Local buffer for quick access
142
+ partialLine: '' // Buffer for incomplete lines
143
+ });
144
+
145
+ // 6. Setup log buffer capture and token count updates
146
+ // Listen to terminal's onData event (this fires for user input)
147
+ // We need to listen to the actual output from the backend
148
+ socket.on('terminal:data', ({ termId: dataTermId, data }) => {
149
+ if (dataTermId !== termId) return;
150
+
151
+ const session = activeTerminals.get(termId);
152
+ if (!session) return;
116
153
 
117
- // 6. Init Backend Process
118
- socket.emit('terminal:init', {
119
- termId,
120
- cols: term.cols,
121
- rows: term.rows
154
+ // Strip ANSI codes
155
+ const cleanData = data.replace(
156
+ // eslint-disable-next-line no-control-regex
157
+ /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b\][^\x1b]*\x1b\\/g,
158
+ ''
159
+ );
160
+
161
+ // Accumulate partial line
162
+ session.partialLine += cleanData;
163
+
164
+ // Split by newlines and process complete lines
165
+ const lines = session.partialLine.split(/\r?\n/);
166
+
167
+ // Keep the last incomplete line in partialLine
168
+ session.partialLine = lines.pop() || '';
169
+
170
+ // Add complete lines to buffer
171
+ lines.forEach(line => {
172
+ if (line.trim().length > 0) {
173
+ session.logBuffer.push(line);
174
+ // Maintain max 10000 lines
175
+ if (session.logBuffer.length > 10000) {
176
+ session.logBuffer.shift();
177
+ }
178
+ }
179
+ });
180
+
181
+ // Update token counts (debounced)
182
+ if (lines.length > 0) {
183
+ updateTokenCounts(termId);
184
+ }
122
185
  });
186
+
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
+ });
214
+
215
+ // 8. Initial token count update
216
+ setTimeout(() => updateTokenCounts(termId), 100);
217
+
218
+ // Return termId so caller can use it
219
+ return termId;
123
220
  }
124
221
 
125
222
  /**
@@ -245,8 +342,183 @@ function makeDraggable(element, handle) {
245
342
  }
246
343
  }
247
344
 
345
+ /**
346
+ * Copy terminal log with specified strategy
347
+ * @param {string} termId - Terminal ID
348
+ * @param {string} strategy - 'smart' | 'errors' | 'recent' | 'all'
349
+ */
350
+ async function copyTerminalLog(termId, strategy) {
351
+ const session = activeTerminals.get(termId);
352
+ if (!session || !session.logBuffer || session.logBuffer.length === 0) {
353
+ showToast('⚠️ No logs to copy', 'warning');
354
+ return;
355
+ }
356
+
357
+ try {
358
+ // Use SmartCopyEngine to generate content
359
+ let result;
360
+ const lines = session.logBuffer;
361
+
362
+ switch (strategy) {
363
+ case 'smart':
364
+ result = window.SmartCopyEngine.generateSmartCopy(lines, 3000);
365
+ break;
366
+ case 'errors':
367
+ result = window.SmartCopyEngine.generateErrorsOnly(lines);
368
+ break;
369
+ case 'recent':
370
+ result = window.SmartCopyEngine.generateRecent(lines, 200);
371
+ break;
372
+ case 'all':
373
+ result = window.SmartCopyEngine.generateCopyAll(lines);
374
+ break;
375
+ default:
376
+ result = window.SmartCopyEngine.generateSmartCopy(lines, 3000);
377
+ }
378
+
379
+ // Copy to clipboard
380
+ await navigator.clipboard.writeText(result.content);
381
+
382
+ // Show success toast with details
383
+ const strategyNames = {
384
+ smart: '🧠 Smart',
385
+ errors: '⚠️ Errors',
386
+ recent: '📄 Recent',
387
+ all: '📦 All'
388
+ };
389
+
390
+ let message = `✅ Copied ${result.tokens} tokens (${strategyNames[strategy]})`;
391
+
392
+ if (result.stats && result.stats.message) {
393
+ message += `\n${result.stats.message}`;
394
+ }
395
+
396
+ if (result.warning) {
397
+ message += `\n${result.warning}`;
398
+ }
399
+
400
+ showToast(message, 'success');
401
+
402
+ } catch (error) {
403
+ console.error('Copy failed:', error);
404
+ showToast('❌ Failed to copy logs', 'error');
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Update token count badges for a terminal
410
+ * Debounced to avoid excessive updates
411
+ */
412
+ let updateTokenCountsTimeout = null;
413
+ function updateTokenCounts(termId) {
414
+ // Debounce updates
415
+ clearTimeout(updateTokenCountsTimeout);
416
+ updateTokenCountsTimeout = setTimeout(() => {
417
+ const session = activeTerminals.get(termId);
418
+ if (!session || !session.logBuffer) return;
419
+
420
+ const analysis = window.SmartCopyEngine.analyzeLogBuffer(session.logBuffer);
421
+
422
+ // Update badges
423
+ updateBadge(`badge-smart-${termId}`, analysis.smart.tokens);
424
+ updateBadge(`badge-errors-${termId}`, analysis.errors.tokens);
425
+ updateBadge(`badge-recent-${termId}`, analysis.recent.tokens);
426
+ updateBadge(`badge-all-${termId}`, analysis.all.tokens);
427
+ }, 500); // 500ms debounce
428
+ }
429
+
430
+ /**
431
+ * Update a single badge element
432
+ */
433
+ function updateBadge(badgeId, tokens) {
434
+ const badge = document.getElementById(badgeId);
435
+ if (badge) {
436
+ badge.textContent = formatTokenCount(tokens);
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Format token count for display
442
+ */
443
+ function formatTokenCount(tokens) {
444
+ if (tokens === 0) return '0';
445
+ if (tokens < 1000) return tokens.toString();
446
+ if (tokens < 10000) return (tokens / 1000).toFixed(1) + 'k';
447
+ return Math.floor(tokens / 1000) + 'k';
448
+ }
449
+
450
+ /**
451
+ * Show toast notification
452
+ */
453
+ function showToast(message, type = 'info') {
454
+ const toast = document.getElementById('toast');
455
+ if (!toast) return;
456
+
457
+ toast.textContent = message;
458
+ toast.className = 'toast show';
459
+
460
+ if (type === 'success') {
461
+ toast.style.background = '#28a745';
462
+ } else if (type === 'error') {
463
+ toast.style.background = '#dc3545';
464
+ } else if (type === 'warning') {
465
+ toast.style.background = '#ffc107';
466
+ toast.style.color = '#000';
467
+ } else {
468
+ toast.style.background = '#007bff';
469
+ }
470
+
471
+ setTimeout(() => {
472
+ toast.classList.remove('show');
473
+ }, 3000);
474
+ }
475
+
476
+ /**
477
+ * Clear terminal display and log buffer
478
+ * @param {string} termId - Terminal ID
479
+ */
480
+ function clearTerminal(termId) {
481
+ const session = activeTerminals.get(termId);
482
+ if (!session) return;
483
+
484
+ // Clear the xterm display
485
+ session.term.clear();
486
+
487
+ // Clear the log buffer
488
+ session.logBuffer = [];
489
+ session.partialLine = '';
490
+
491
+ // Reset token counts to 0
492
+ updateBadge(`badge-smart-${termId}`, 0);
493
+ updateBadge(`badge-errors-${termId}`, 0);
494
+ updateBadge(`badge-recent-${termId}`, 0);
495
+ updateBadge(`badge-all-${termId}`, 0);
496
+
497
+ // Show toast notification
498
+ showToast('🗑️ Terminal cleared', 'info');
499
+ }
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
+
248
517
  // Global Exports for HTML onclick
249
518
  window.createNewTerminal = createNewTerminal;
250
519
  window.closeTerminal = closeTerminalUI;
251
520
  window.toggleMinimize = toggleMinimize;
252
521
  window.toggleMaximize = toggleMaximize;
522
+ window.copyTerminalLog = copyTerminalLog;
523
+ window.clearTerminal = clearTerminal;
524
+ window.updateTerminalVisibility = updateTerminalVisibility;
@@ -9,6 +9,8 @@ import { initTerminal, createNewTerminal } from './features/terminal.js';
9
9
  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
+ import { initSavedCommands } from './features/commands.js';
13
+ import { initProjectSwitcher } from './features/project-switcher.js';
12
14
 
13
15
  document.addEventListener('DOMContentLoaded', async () => {
14
16
  // Load system prompt text
@@ -43,12 +45,52 @@ document.addEventListener('DOMContentLoaded', async () => {
43
45
  // Initialize Resize Handler
44
46
  initResizeHandler();
45
47
 
48
+ // Initialize Saved Commands
49
+ initSavedCommands();
50
+
51
+ // Initialize Project Switcher
52
+ await initProjectSwitcher();
53
+
46
54
  // Set default tab to AI Assistant
47
55
  if (window.switchTab) {
48
56
  window.switchTab('ai-assistant');
49
57
  }
50
58
  });
51
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
+
52
94
  async function checkServerStatus() {
53
95
  const statusEl = document.getElementById('status');
54
96
  const isHealthy = await checkHealth();
@@ -169,3 +211,26 @@ window.copyChromeUrl = function(event) {
169
211
  }, 2000);
170
212
  });
171
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
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Log Utilities for Smart Log Copy Feature
3
+ * Provides ANSI stripping, token estimation, and log classification
4
+ */
5
+
6
+ /**
7
+ * Strip ANSI escape codes from text
8
+ * Removes color codes, cursor movements, and other terminal control sequences
9
+ * @param {string} text - Text with potential ANSI codes
10
+ * @returns {string} Clean text without ANSI codes
11
+ */
12
+ function stripAnsiCodes(text) {
13
+ if (!text) return '';
14
+
15
+ // Remove ANSI escape sequences
16
+ // Pattern matches: ESC [ ... m (colors, styles)
17
+ // ESC [ ... H/J/K (cursor movements, clear)
18
+ // ESC ] ... BEL/ST (operating system commands)
19
+ return text.replace(
20
+ // eslint-disable-next-line no-control-regex
21
+ /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b\][^\x1b]*\x1b\\/g,
22
+ ''
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Estimate token count from text
28
+ * Uses approximation: 1 token ≈ 4 characters (for English/code)
29
+ * This is a simplified estimation and may vary ±20% from actual tokenization
30
+ * @param {string} text - Text to estimate
31
+ * @returns {number} Estimated token count
32
+ */
33
+ function estimateTokens(text) {
34
+ if (!text) return 0;
35
+
36
+ const chars = text.length;
37
+ const words = text.split(/\s+/).filter(w => w.length > 0).length;
38
+
39
+ // Use the maximum of two methods for better accuracy:
40
+ // Method 1: 1 token ≈ 0.75 words
41
+ // Method 2: 1 token ≈ 4 characters
42
+ const tokensFromWords = Math.ceil(words / 0.75);
43
+ const tokensFromChars = Math.ceil(chars / 4);
44
+
45
+ return Math.max(tokensFromWords, tokensFromChars);
46
+ }
47
+
48
+ /**
49
+ * Classify a log line by severity
50
+ * @param {string} line - Single log line
51
+ * @returns {'ERROR'|'WARNING'|'NORMAL'} Classification
52
+ */
53
+ function classifyLogLine(line) {
54
+ if (!line) return 'NORMAL';
55
+
56
+ const lowerLine = line.toLowerCase();
57
+
58
+ // Error patterns
59
+ const errorPatterns = [
60
+ 'error', 'err:', 'exception', 'fatal', 'failed', 'failure',
61
+ 'cannot', 'could not', 'unable to', 'not found', '404',
62
+ 'stack trace', 'traceback', 'segmentation fault', 'core dumped'
63
+ ];
64
+
65
+ // Warning patterns
66
+ const warningPatterns = [
67
+ 'warn', 'warning', 'deprecated', 'deprecation',
68
+ 'caution', 'attention', 'notice'
69
+ ];
70
+
71
+ // Check for errors first (higher priority)
72
+ if (errorPatterns.some(pattern => lowerLine.includes(pattern))) {
73
+ return 'ERROR';
74
+ }
75
+
76
+ // Then check for warnings
77
+ if (warningPatterns.some(pattern => lowerLine.includes(pattern))) {
78
+ return 'WARNING';
79
+ }
80
+
81
+ return 'NORMAL';
82
+ }
83
+
84
+ /**
85
+ * Extract error and warning lines with context
86
+ * @param {string[]} lines - Array of log lines
87
+ * @param {number} contextLines - Number of lines before/after to include (default: 2)
88
+ * @returns {Object[]} Array of {lineIndex, line, type, context}
89
+ */
90
+ function extractErrors(lines, contextLines = 2) {
91
+ const errors = [];
92
+ const includedIndices = new Set();
93
+
94
+ // First pass: identify error/warning lines
95
+ lines.forEach((line, index) => {
96
+ const type = classifyLogLine(line);
97
+ if (type === 'ERROR' || type === 'WARNING') {
98
+ // Include the error line and context
99
+ const startIndex = Math.max(0, index - contextLines);
100
+ const endIndex = Math.min(lines.length - 1, index + contextLines);
101
+
102
+ for (let i = startIndex; i <= endIndex; i++) {
103
+ includedIndices.add(i);
104
+ }
105
+
106
+ errors.push({
107
+ lineIndex: index,
108
+ line,
109
+ type,
110
+ contextStart: startIndex,
111
+ contextEnd: endIndex
112
+ });
113
+ }
114
+ });
115
+
116
+ // Second pass: build result with context
117
+ const result = [];
118
+ const sortedIndices = Array.from(includedIndices).sort((a, b) => a - b);
119
+
120
+ sortedIndices.forEach(index => {
121
+ result.push({
122
+ lineIndex: index,
123
+ line: lines[index],
124
+ type: classifyLogLine(lines[index])
125
+ });
126
+ });
127
+
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Format file size in human-readable format
133
+ * @param {number} bytes - Size in bytes
134
+ * @returns {string} Formatted size
135
+ */
136
+ function formatBytes(bytes) {
137
+ if (bytes === 0) return '0 B';
138
+ const k = 1024;
139
+ const sizes = ['B', 'KB', 'MB', 'GB'];
140
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
141
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
142
+ }
143
+
144
+ // Export for Node.js
145
+ if (typeof module !== 'undefined' && module.exports) {
146
+ module.exports = {
147
+ stripAnsiCodes,
148
+ estimateTokens,
149
+ classifyLogLine,
150
+ extractErrors,
151
+ formatBytes
152
+ };
153
+ }
154
+
155
+ // Export for browser
156
+ if (typeof window !== 'undefined') {
157
+ window.LogUtils = {
158
+ stripAnsiCodes,
159
+ estimateTokens,
160
+ classifyLogLine,
161
+ extractErrors,
162
+ formatBytes
163
+ };
164
+ }