vg-coder-cli 2.0.24 → 2.0.25
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.
- package/package.json +1 -1
- package/src/server/api-server.js +76 -0
- package/src/server/terminal-manager.js +99 -0
- package/src/server/views/css/terminal.css +88 -0
- package/src/server/views/dashboard.css +267 -0
- package/src/server/views/dashboard.html +56 -8
- package/src/server/views/js/features/commands.js +228 -0
- package/src/server/views/js/features/terminal.js +238 -4
- package/src/server/views/js/main.js +4 -0
- package/src/server/views/js/utils/log-utils.js +164 -0
- package/src/server/views/js/utils/smart-copy-engine.js +283 -0
- package/vg-coder-cli-2.0.25.tgz +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
} catch (error) {
|
|
26
|
+
console.error('Failed to load commands:', error);
|
|
27
|
+
savedCommands = [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save commands to backend (debounced)
|
|
33
|
+
*/
|
|
34
|
+
let saveTimeout = null;
|
|
35
|
+
async function saveCommands() {
|
|
36
|
+
clearTimeout(saveTimeout);
|
|
37
|
+
saveTimeout = setTimeout(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch('/api/commands/save', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ commands: savedCommands })
|
|
43
|
+
});
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
if (data.success) {
|
|
46
|
+
console.log(`✓ Saved ${data.count} commands`);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Failed to save commands:', error);
|
|
50
|
+
}
|
|
51
|
+
}, 500);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render commands in UI
|
|
56
|
+
*/
|
|
57
|
+
function renderCommands() {
|
|
58
|
+
const container = document.getElementById('commands-list');
|
|
59
|
+
const emptyState = document.getElementById('commands-empty-state');
|
|
60
|
+
|
|
61
|
+
if (!container) return;
|
|
62
|
+
|
|
63
|
+
if (savedCommands.length === 0) {
|
|
64
|
+
container.innerHTML = '';
|
|
65
|
+
if (emptyState) emptyState.style.display = 'block';
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (emptyState) emptyState.style.display = 'none';
|
|
70
|
+
|
|
71
|
+
container.innerHTML = savedCommands.map(cmd => `
|
|
72
|
+
<div class="command-card" onclick="window.runSavedCommand('${cmd.id}')">
|
|
73
|
+
<div class="command-card-main">
|
|
74
|
+
<span class="command-icon">${cmd.icon}</span>
|
|
75
|
+
<div class="command-info">
|
|
76
|
+
<div class="command-name">${escapeHtml(cmd.name)}</div>
|
|
77
|
+
<div class="command-text">${escapeHtml(cmd.command)}</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="command-card-actions">
|
|
81
|
+
<button class="command-action-btn" onclick="window.editCommand('${cmd.id}'); event.stopPropagation();" title="Edit">
|
|
82
|
+
✏️
|
|
83
|
+
</button>
|
|
84
|
+
<button class="command-action-btn" onclick="window.deleteCommand('${cmd.id}'); event.stopPropagation();" title="Delete">
|
|
85
|
+
🗑️
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
`).join('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Open add command modal
|
|
94
|
+
*/
|
|
95
|
+
function openAddCommandModal() {
|
|
96
|
+
editingCommandId = null;
|
|
97
|
+
document.getElementById('modal-title').textContent = 'Add Command';
|
|
98
|
+
document.getElementById('command-icon').value = '🚀';
|
|
99
|
+
document.getElementById('command-name').value = '';
|
|
100
|
+
document.getElementById('command-text').value = '';
|
|
101
|
+
document.getElementById('command-modal').style.display = 'flex';
|
|
102
|
+
document.getElementById('command-name').focus();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Open edit command modal
|
|
107
|
+
*/
|
|
108
|
+
function editCommand(id) {
|
|
109
|
+
const command = savedCommands.find(c => c.id === id);
|
|
110
|
+
if (!command) return;
|
|
111
|
+
|
|
112
|
+
editingCommandId = id;
|
|
113
|
+
document.getElementById('modal-title').textContent = 'Edit Command';
|
|
114
|
+
document.getElementById('command-icon').value = command.icon;
|
|
115
|
+
document.getElementById('command-name').value = command.name;
|
|
116
|
+
document.getElementById('command-text').value = command.command;
|
|
117
|
+
document.getElementById('command-modal').style.display = 'flex';
|
|
118
|
+
document.getElementById('command-name').focus();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Close command modal
|
|
123
|
+
*/
|
|
124
|
+
function closeCommandModal() {
|
|
125
|
+
document.getElementById('command-modal').style.display = 'none';
|
|
126
|
+
editingCommandId = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Handle command form submit
|
|
131
|
+
*/
|
|
132
|
+
function handleCommandFormSubmit(event) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
|
|
135
|
+
const icon = document.getElementById('command-icon').value.trim();
|
|
136
|
+
const name = document.getElementById('command-name').value.trim();
|
|
137
|
+
const command = document.getElementById('command-text').value.trim();
|
|
138
|
+
|
|
139
|
+
if (!icon || !name || !command) return;
|
|
140
|
+
|
|
141
|
+
if (editingCommandId) {
|
|
142
|
+
// Edit existing
|
|
143
|
+
const index = savedCommands.findIndex(c => c.id === editingCommandId);
|
|
144
|
+
if (index !== -1) {
|
|
145
|
+
savedCommands[index] = { ...savedCommands[index], icon, name, command };
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Add new
|
|
149
|
+
savedCommands.push({
|
|
150
|
+
id: 'cmd_' + Date.now(),
|
|
151
|
+
icon,
|
|
152
|
+
name,
|
|
153
|
+
command
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
saveCommands();
|
|
158
|
+
renderCommands();
|
|
159
|
+
closeCommandModal();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete command with confirmation
|
|
164
|
+
*/
|
|
165
|
+
function deleteCommand(id) {
|
|
166
|
+
const command = savedCommands.find(c => c.id === id);
|
|
167
|
+
if (!command) return;
|
|
168
|
+
|
|
169
|
+
if (confirm(`Delete command "${command.name}"?`)) {
|
|
170
|
+
savedCommands = savedCommands.filter(c => c.id !== id);
|
|
171
|
+
saveCommands();
|
|
172
|
+
renderCommands();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run saved command - open terminal and copy command to clipboard
|
|
178
|
+
*/
|
|
179
|
+
function runSavedCommand(id) {
|
|
180
|
+
const command = savedCommands.find(c => c.id === id);
|
|
181
|
+
if (!command) return;
|
|
182
|
+
|
|
183
|
+
// Create new terminal
|
|
184
|
+
if (typeof window.createNewTerminal === 'function') {
|
|
185
|
+
window.createNewTerminal();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Copy command to clipboard
|
|
189
|
+
navigator.clipboard.writeText(command.command).then(() => {
|
|
190
|
+
// Show toast notification
|
|
191
|
+
if (typeof window.showToast === 'function') {
|
|
192
|
+
window.showToast(`📋 Copied: ${command.command}`, 'success');
|
|
193
|
+
}
|
|
194
|
+
}).catch(err => {
|
|
195
|
+
console.error('Failed to copy command:', err);
|
|
196
|
+
if (typeof window.showToast === 'function') {
|
|
197
|
+
window.showToast('Failed to copy command', 'error');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Escape HTML to prevent XSS
|
|
204
|
+
*/
|
|
205
|
+
function escapeHtml(text) {
|
|
206
|
+
const div = document.createElement('div');
|
|
207
|
+
div.textContent = text;
|
|
208
|
+
return div.innerHTML;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Global exports for HTML onclick
|
|
212
|
+
window.openAddCommandModal = openAddCommandModal;
|
|
213
|
+
window.editCommand = editCommand;
|
|
214
|
+
window.closeCommandModal = closeCommandModal;
|
|
215
|
+
window.deleteCommand = deleteCommand;
|
|
216
|
+
window.runSavedCommand = runSavedCommand;
|
|
217
|
+
|
|
218
|
+
// Setup form submit handler
|
|
219
|
+
if (typeof document !== 'undefined') {
|
|
220
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
221
|
+
const form = document.getElementById('command-form');
|
|
222
|
+
if (form) {
|
|
223
|
+
form.addEventListener('submit', handleCommandFormSubmit);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { openAddCommandModal, editCommand, deleteCommand, runSavedCommand };
|
|
@@ -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
|
|
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,69 @@ 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, {
|
|
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;
|
|
153
|
+
|
|
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
|
+
}
|
|
185
|
+
});
|
|
116
186
|
|
|
117
|
-
//
|
|
187
|
+
// 7. Init Backend Process
|
|
118
188
|
socket.emit('terminal:init', {
|
|
119
189
|
termId,
|
|
120
190
|
cols: term.cols,
|
|
121
191
|
rows: term.rows
|
|
122
192
|
});
|
|
193
|
+
|
|
194
|
+
// 8. Initial token count update
|
|
195
|
+
setTimeout(() => updateTokenCounts(termId), 100);
|
|
196
|
+
|
|
197
|
+
// Return termId so caller can use it
|
|
198
|
+
return termId;
|
|
123
199
|
}
|
|
124
200
|
|
|
125
201
|
/**
|
|
@@ -245,8 +321,166 @@ function makeDraggable(element, handle) {
|
|
|
245
321
|
}
|
|
246
322
|
}
|
|
247
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Copy terminal log with specified strategy
|
|
326
|
+
* @param {string} termId - Terminal ID
|
|
327
|
+
* @param {string} strategy - 'smart' | 'errors' | 'recent' | 'all'
|
|
328
|
+
*/
|
|
329
|
+
async function copyTerminalLog(termId, strategy) {
|
|
330
|
+
const session = activeTerminals.get(termId);
|
|
331
|
+
if (!session || !session.logBuffer || session.logBuffer.length === 0) {
|
|
332
|
+
showToast('⚠️ No logs to copy', 'warning');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
// Use SmartCopyEngine to generate content
|
|
338
|
+
let result;
|
|
339
|
+
const lines = session.logBuffer;
|
|
340
|
+
|
|
341
|
+
switch (strategy) {
|
|
342
|
+
case 'smart':
|
|
343
|
+
result = window.SmartCopyEngine.generateSmartCopy(lines, 3000);
|
|
344
|
+
break;
|
|
345
|
+
case 'errors':
|
|
346
|
+
result = window.SmartCopyEngine.generateErrorsOnly(lines);
|
|
347
|
+
break;
|
|
348
|
+
case 'recent':
|
|
349
|
+
result = window.SmartCopyEngine.generateRecent(lines, 200);
|
|
350
|
+
break;
|
|
351
|
+
case 'all':
|
|
352
|
+
result = window.SmartCopyEngine.generateCopyAll(lines);
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
result = window.SmartCopyEngine.generateSmartCopy(lines, 3000);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Copy to clipboard
|
|
359
|
+
await navigator.clipboard.writeText(result.content);
|
|
360
|
+
|
|
361
|
+
// Show success toast with details
|
|
362
|
+
const strategyNames = {
|
|
363
|
+
smart: '🧠 Smart',
|
|
364
|
+
errors: '⚠️ Errors',
|
|
365
|
+
recent: '📄 Recent',
|
|
366
|
+
all: '📦 All'
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
let message = `✅ Copied ${result.tokens} tokens (${strategyNames[strategy]})`;
|
|
370
|
+
|
|
371
|
+
if (result.stats && result.stats.message) {
|
|
372
|
+
message += `\n${result.stats.message}`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (result.warning) {
|
|
376
|
+
message += `\n${result.warning}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
showToast(message, 'success');
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('Copy failed:', error);
|
|
383
|
+
showToast('❌ Failed to copy logs', 'error');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Update token count badges for a terminal
|
|
389
|
+
* Debounced to avoid excessive updates
|
|
390
|
+
*/
|
|
391
|
+
let updateTokenCountsTimeout = null;
|
|
392
|
+
function updateTokenCounts(termId) {
|
|
393
|
+
// Debounce updates
|
|
394
|
+
clearTimeout(updateTokenCountsTimeout);
|
|
395
|
+
updateTokenCountsTimeout = setTimeout(() => {
|
|
396
|
+
const session = activeTerminals.get(termId);
|
|
397
|
+
if (!session || !session.logBuffer) return;
|
|
398
|
+
|
|
399
|
+
const analysis = window.SmartCopyEngine.analyzeLogBuffer(session.logBuffer);
|
|
400
|
+
|
|
401
|
+
// Update badges
|
|
402
|
+
updateBadge(`badge-smart-${termId}`, analysis.smart.tokens);
|
|
403
|
+
updateBadge(`badge-errors-${termId}`, analysis.errors.tokens);
|
|
404
|
+
updateBadge(`badge-recent-${termId}`, analysis.recent.tokens);
|
|
405
|
+
updateBadge(`badge-all-${termId}`, analysis.all.tokens);
|
|
406
|
+
}, 500); // 500ms debounce
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Update a single badge element
|
|
411
|
+
*/
|
|
412
|
+
function updateBadge(badgeId, tokens) {
|
|
413
|
+
const badge = document.getElementById(badgeId);
|
|
414
|
+
if (badge) {
|
|
415
|
+
badge.textContent = formatTokenCount(tokens);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Format token count for display
|
|
421
|
+
*/
|
|
422
|
+
function formatTokenCount(tokens) {
|
|
423
|
+
if (tokens === 0) return '0';
|
|
424
|
+
if (tokens < 1000) return tokens.toString();
|
|
425
|
+
if (tokens < 10000) return (tokens / 1000).toFixed(1) + 'k';
|
|
426
|
+
return Math.floor(tokens / 1000) + 'k';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Show toast notification
|
|
431
|
+
*/
|
|
432
|
+
function showToast(message, type = 'info') {
|
|
433
|
+
const toast = document.getElementById('toast');
|
|
434
|
+
if (!toast) return;
|
|
435
|
+
|
|
436
|
+
toast.textContent = message;
|
|
437
|
+
toast.className = 'toast show';
|
|
438
|
+
|
|
439
|
+
if (type === 'success') {
|
|
440
|
+
toast.style.background = '#28a745';
|
|
441
|
+
} else if (type === 'error') {
|
|
442
|
+
toast.style.background = '#dc3545';
|
|
443
|
+
} else if (type === 'warning') {
|
|
444
|
+
toast.style.background = '#ffc107';
|
|
445
|
+
toast.style.color = '#000';
|
|
446
|
+
} else {
|
|
447
|
+
toast.style.background = '#007bff';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
setTimeout(() => {
|
|
451
|
+
toast.classList.remove('show');
|
|
452
|
+
}, 3000);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Clear terminal display and log buffer
|
|
457
|
+
* @param {string} termId - Terminal ID
|
|
458
|
+
*/
|
|
459
|
+
function clearTerminal(termId) {
|
|
460
|
+
const session = activeTerminals.get(termId);
|
|
461
|
+
if (!session) return;
|
|
462
|
+
|
|
463
|
+
// Clear the xterm display
|
|
464
|
+
session.term.clear();
|
|
465
|
+
|
|
466
|
+
// Clear the log buffer
|
|
467
|
+
session.logBuffer = [];
|
|
468
|
+
session.partialLine = '';
|
|
469
|
+
|
|
470
|
+
// Reset token counts to 0
|
|
471
|
+
updateBadge(`badge-smart-${termId}`, 0);
|
|
472
|
+
updateBadge(`badge-errors-${termId}`, 0);
|
|
473
|
+
updateBadge(`badge-recent-${termId}`, 0);
|
|
474
|
+
updateBadge(`badge-all-${termId}`, 0);
|
|
475
|
+
|
|
476
|
+
// Show toast notification
|
|
477
|
+
showToast('🗑️ Terminal cleared', 'info');
|
|
478
|
+
}
|
|
479
|
+
|
|
248
480
|
// Global Exports for HTML onclick
|
|
249
481
|
window.createNewTerminal = createNewTerminal;
|
|
250
482
|
window.closeTerminal = closeTerminalUI;
|
|
251
483
|
window.toggleMinimize = toggleMinimize;
|
|
252
484
|
window.toggleMaximize = toggleMaximize;
|
|
485
|
+
window.copyTerminalLog = copyTerminalLog;
|
|
486
|
+
window.clearTerminal = clearTerminal;
|
|
@@ -9,6 +9,7 @@ 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';
|
|
12
13
|
|
|
13
14
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
14
15
|
// Load system prompt text
|
|
@@ -43,6 +44,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
43
44
|
// Initialize Resize Handler
|
|
44
45
|
initResizeHandler();
|
|
45
46
|
|
|
47
|
+
// Initialize Saved Commands
|
|
48
|
+
initSavedCommands();
|
|
49
|
+
|
|
46
50
|
// Set default tab to AI Assistant
|
|
47
51
|
if (window.switchTab) {
|
|
48
52
|
window.switchTab('ai-assistant');
|
|
@@ -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
|
+
}
|