vg-coder-cli 2.0.23 → 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 +109 -1
- 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.24.tgz +0 -0
- package/vg-coder-cli-2.0.25.tgz +0 -0
package/package.json
CHANGED
package/src/server/api-server.js
CHANGED
|
@@ -274,6 +274,82 @@ class ApiServer {
|
|
|
274
274
|
}
|
|
275
275
|
});
|
|
276
276
|
|
|
277
|
+
// --- TERMINAL LOG API ---
|
|
278
|
+
|
|
279
|
+
// Get terminal logs
|
|
280
|
+
this.app.get('/api/terminal/:termId/logs', (req, res) => {
|
|
281
|
+
try {
|
|
282
|
+
const { termId } = req.params;
|
|
283
|
+
const logs = terminalManager.getLogBuffer(termId);
|
|
284
|
+
|
|
285
|
+
res.json({
|
|
286
|
+
termId,
|
|
287
|
+
logs,
|
|
288
|
+
totalLines: logs.length
|
|
289
|
+
});
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error(chalk.red('❌ [TERMINAL LOGS] Error:'), error.message);
|
|
292
|
+
res.status(500).json({ error: error.message });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Analyze terminal logs
|
|
297
|
+
this.app.post('/api/terminal/:termId/analyze', (req, res) => {
|
|
298
|
+
try {
|
|
299
|
+
const { termId } = req.params;
|
|
300
|
+
const analysis = terminalManager.analyzeLogBuffer(termId);
|
|
301
|
+
|
|
302
|
+
res.json({
|
|
303
|
+
termId,
|
|
304
|
+
...analysis
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error(chalk.red('❌ [TERMINAL ANALYZE] Error:'), error.message);
|
|
308
|
+
res.status(500).json({ error: error.message });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// --- SAVED COMMANDS API ---
|
|
313
|
+
|
|
314
|
+
// Load saved commands
|
|
315
|
+
this.app.get('/api/commands/load', async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const commandsFile = path.join(this.workingDir, '.vg', 'commands.json');
|
|
318
|
+
|
|
319
|
+
if (!await fs.pathExists(commandsFile)) {
|
|
320
|
+
return res.json({ commands: [] });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const data = await fs.readJson(commandsFile);
|
|
324
|
+
res.json({ commands: data.commands || [] });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error(chalk.red('❌ [COMMANDS LOAD] Error:'), error.message);
|
|
327
|
+
res.json({ commands: [] });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Save commands
|
|
332
|
+
this.app.post('/api/commands/save', async (req, res) => {
|
|
333
|
+
try {
|
|
334
|
+
const { commands } = req.body;
|
|
335
|
+
if (!Array.isArray(commands)) {
|
|
336
|
+
return res.status(400).json({ error: 'commands must be an array' });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const vgDir = path.join(this.workingDir, '.vg');
|
|
340
|
+
await fs.ensureDir(vgDir);
|
|
341
|
+
|
|
342
|
+
const commandsFile = path.join(vgDir, 'commands.json');
|
|
343
|
+
await fs.writeJson(commandsFile, { commands }, { spaces: 2 });
|
|
344
|
+
|
|
345
|
+
console.log(chalk.green(`✓ Saved ${commands.length} commands`));
|
|
346
|
+
res.json({ success: true, count: commands.length });
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(chalk.red('❌ [COMMANDS SAVE] Error:'), error.message);
|
|
349
|
+
res.status(500).json({ error: error.message });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
277
353
|
// --- TREE STATE API ---
|
|
278
354
|
|
|
279
355
|
// Save tree state (excluded paths)
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
2
|
const pty = require('node-pty');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { stripAnsiCodes, classifyLogLine, extractErrors } = require(path.join(__dirname, 'views/js/utils/log-utils'));
|
|
3
5
|
|
|
4
6
|
class TerminalManager {
|
|
5
7
|
constructor() {
|
|
6
8
|
// Map: termId -> { process: pty, socketId: string }
|
|
7
9
|
this.sessions = new Map();
|
|
8
|
-
|
|
10
|
+
// Map: termId -> Array of log lines (circular buffer, max 10000)
|
|
11
|
+
this.logBuffers = new Map();
|
|
12
|
+
this.MAX_LOG_LINES = 10000;
|
|
13
|
+
// Use full path to shell to avoid posix_spawnp errors
|
|
14
|
+
if (os.platform() === 'win32') {
|
|
15
|
+
this.shell = 'powershell.exe';
|
|
16
|
+
} else if (os.platform() === 'darwin') {
|
|
17
|
+
// macOS - use zsh (default shell since Catalina)
|
|
18
|
+
this.shell = process.env.SHELL || '/bin/zsh';
|
|
19
|
+
} else {
|
|
20
|
+
// Linux and others
|
|
21
|
+
this.shell = process.env.SHELL || '/bin/bash';
|
|
22
|
+
}
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
createTerminal(socket, termId, cols = 80, rows = 24, cwd) {
|
|
@@ -23,8 +37,15 @@ class TerminalManager {
|
|
|
23
37
|
socketId: socket.id
|
|
24
38
|
});
|
|
25
39
|
|
|
40
|
+
// Initialize log buffer for this terminal
|
|
41
|
+
this.logBuffers.set(termId, []);
|
|
42
|
+
|
|
26
43
|
// Gửi dữ liệu kèm theo termId để Frontend biết của cửa sổ nào
|
|
27
44
|
term.onData((data) => {
|
|
45
|
+
// Store in log buffer (strip ANSI for storage)
|
|
46
|
+
this.addToLogBuffer(termId, data);
|
|
47
|
+
|
|
48
|
+
// Send raw data to frontend (keep ANSI for display)
|
|
28
49
|
socket.emit('terminal:data', { termId, data });
|
|
29
50
|
});
|
|
30
51
|
|
|
@@ -32,6 +53,8 @@ class TerminalManager {
|
|
|
32
53
|
if (this.sessions.has(termId)) {
|
|
33
54
|
socket.emit('terminal:exit', { termId });
|
|
34
55
|
this.sessions.delete(termId);
|
|
56
|
+
// Clean up log buffer
|
|
57
|
+
this.logBuffers.delete(termId);
|
|
35
58
|
}
|
|
36
59
|
});
|
|
37
60
|
|
|
@@ -74,9 +97,94 @@ class TerminalManager {
|
|
|
74
97
|
if (session.socketId === socketId) {
|
|
75
98
|
session.process.kill();
|
|
76
99
|
this.sessions.delete(termId);
|
|
100
|
+
this.logBuffers.delete(termId);
|
|
77
101
|
}
|
|
78
102
|
}
|
|
79
103
|
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Add data to log buffer (circular buffer with max 10000 lines)
|
|
107
|
+
* @param {string} termId - Terminal ID
|
|
108
|
+
* @param {string} data - Raw terminal data (may contain ANSI codes)
|
|
109
|
+
*/
|
|
110
|
+
addToLogBuffer(termId, data) {
|
|
111
|
+
if (!this.logBuffers.has(termId)) {
|
|
112
|
+
this.logBuffers.set(termId, []);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const buffer = this.logBuffers.get(termId);
|
|
116
|
+
|
|
117
|
+
// Strip ANSI codes before storing
|
|
118
|
+
const cleanData = stripAnsiCodes(data);
|
|
119
|
+
|
|
120
|
+
// Split by newlines and add to buffer
|
|
121
|
+
const lines = cleanData.split(/\r?\n/);
|
|
122
|
+
|
|
123
|
+
lines.forEach(line => {
|
|
124
|
+
// Only add non-empty lines
|
|
125
|
+
if (line.trim().length > 0) {
|
|
126
|
+
buffer.push(line);
|
|
127
|
+
|
|
128
|
+
// Maintain circular buffer - remove oldest if exceeds limit
|
|
129
|
+
if (buffer.length > this.MAX_LOG_LINES) {
|
|
130
|
+
buffer.shift();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get log buffer for a terminal
|
|
138
|
+
* @param {string} termId - Terminal ID
|
|
139
|
+
* @returns {string[]} Array of log lines
|
|
140
|
+
*/
|
|
141
|
+
getLogBuffer(termId) {
|
|
142
|
+
return this.logBuffers.get(termId) || [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Analyze log buffer and return statistics
|
|
147
|
+
* @param {string} termId - Terminal ID
|
|
148
|
+
* @returns {Object} Analysis with error counts, line counts, etc.
|
|
149
|
+
*/
|
|
150
|
+
analyzeLogBuffer(termId) {
|
|
151
|
+
const lines = this.getLogBuffer(termId);
|
|
152
|
+
|
|
153
|
+
if (lines.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
totalLines: 0,
|
|
156
|
+
errorLines: 0,
|
|
157
|
+
warningLines: 0,
|
|
158
|
+
normalLines: 0,
|
|
159
|
+
errors: []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let errorCount = 0;
|
|
164
|
+
let warningCount = 0;
|
|
165
|
+
let normalCount = 0;
|
|
166
|
+
|
|
167
|
+
lines.forEach(line => {
|
|
168
|
+
const type = classifyLogLine(line);
|
|
169
|
+
if (type === 'ERROR') errorCount++;
|
|
170
|
+
else if (type === 'WARNING') warningCount++;
|
|
171
|
+
else normalCount++;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const errors = extractErrors(lines, 2);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
totalLines: lines.length,
|
|
178
|
+
errorLines: errorCount,
|
|
179
|
+
warningLines: warningCount,
|
|
180
|
+
normalLines: normalCount,
|
|
181
|
+
errors: errors.map(e => ({
|
|
182
|
+
line: e.line,
|
|
183
|
+
type: e.type,
|
|
184
|
+
lineIndex: e.lineIndex
|
|
185
|
+
}))
|
|
186
|
+
};
|
|
187
|
+
}
|
|
80
188
|
}
|
|
81
189
|
|
|
82
190
|
module.exports = new TerminalManager();
|
|
@@ -69,6 +69,94 @@
|
|
|
69
69
|
text-overflow: ellipsis;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/* Copy Button Group */
|
|
73
|
+
.terminal-copy-group {
|
|
74
|
+
display: flex;
|
|
75
|
+
gap: 4px;
|
|
76
|
+
margin-left: auto;
|
|
77
|
+
margin-right: 8px;
|
|
78
|
+
align-items: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.copy-btn {
|
|
82
|
+
padding: 3px 6px;
|
|
83
|
+
font-size: 10px;
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
background: #2d2d30;
|
|
86
|
+
border: 1px solid #3e3e42;
|
|
87
|
+
color: #ccc;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition: all 0.2s;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 3px;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.copy-btn:hover {
|
|
98
|
+
background: #3e3e42;
|
|
99
|
+
border-color: #555;
|
|
100
|
+
transform: translateY(-1px);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.copy-btn:active {
|
|
104
|
+
transform: translateY(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.token-badge {
|
|
108
|
+
font-size: 9px;
|
|
109
|
+
color: #888;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
min-width: 20px;
|
|
112
|
+
text-align: right;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Specific button colors */
|
|
116
|
+
.copy-smart:hover {
|
|
117
|
+
border-color: #4a9eff;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.copy-errors:hover {
|
|
121
|
+
border-color: #f48771;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.copy-recent:hover {
|
|
125
|
+
border-color: #89d185;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.copy-all:hover {
|
|
129
|
+
border-color: #dcdcaa;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Clear Button */
|
|
133
|
+
.term-btn-clear {
|
|
134
|
+
width: 24px;
|
|
135
|
+
height: 24px;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
background: #2d2d30;
|
|
138
|
+
border: 1px solid #3e3e42;
|
|
139
|
+
color: #ccc;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
margin-left: 4px;
|
|
146
|
+
margin-right: 8px;
|
|
147
|
+
transition: all 0.2s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.term-btn-clear:hover {
|
|
151
|
+
background: #3e3e42;
|
|
152
|
+
border-color: #f48771;
|
|
153
|
+
transform: translateY(-1px);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.term-btn-clear:active {
|
|
157
|
+
transform: translateY(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
72
160
|
.terminal-controls {
|
|
73
161
|
display: flex;
|
|
74
162
|
gap: 6px;
|
|
@@ -348,3 +348,270 @@ body.resizing {
|
|
|
348
348
|
border-bottom: 1px solid var(--ios-separator);
|
|
349
349
|
}
|
|
350
350
|
}
|
|
351
|
+
|
|
352
|
+
/* --- SAVED COMMANDS STYLES --- */
|
|
353
|
+
.saved-commands-panel {
|
|
354
|
+
margin-bottom: 12px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.saved-commands-header {
|
|
358
|
+
display: flex;
|
|
359
|
+
justify-content: space-between;
|
|
360
|
+
align-items: center;
|
|
361
|
+
margin-bottom: 10px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.saved-commands-title {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 8px;
|
|
368
|
+
font-weight: 600;
|
|
369
|
+
font-size: 13px;
|
|
370
|
+
color: var(--text-primary);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.command-icon-header {
|
|
374
|
+
font-size: 16px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.saved-commands-actions {
|
|
378
|
+
display: flex;
|
|
379
|
+
gap: 6px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.btn-new-terminal {
|
|
383
|
+
width: 28px;
|
|
384
|
+
height: 28px;
|
|
385
|
+
border-radius: 6px;
|
|
386
|
+
background: var(--ios-gray-light);
|
|
387
|
+
border: 1px solid var(--ios-separator);
|
|
388
|
+
color: var(--text-primary);
|
|
389
|
+
cursor: pointer;
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
justify-content: center;
|
|
393
|
+
font-size: 14px;
|
|
394
|
+
transition: all 0.2s;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.btn-new-terminal:hover {
|
|
398
|
+
background: var(--ios-separator);
|
|
399
|
+
transform: scale(1.05);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.btn-add-command {
|
|
403
|
+
width: 28px;
|
|
404
|
+
height: 28px;
|
|
405
|
+
border-radius: 6px;
|
|
406
|
+
background: var(--ios-blue);
|
|
407
|
+
border: none;
|
|
408
|
+
color: white;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
justify-content: center;
|
|
413
|
+
font-size: 14px;
|
|
414
|
+
transition: all 0.2s;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.btn-add-command:hover {
|
|
418
|
+
opacity: 0.8;
|
|
419
|
+
transform: scale(1.05);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.saved-commands-content {
|
|
423
|
+
margin-top: 0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.commands-list {
|
|
427
|
+
display: flex;
|
|
428
|
+
flex-direction: column;
|
|
429
|
+
gap: 8px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.command-card {
|
|
433
|
+
display: flex;
|
|
434
|
+
justify-content: space-between;
|
|
435
|
+
align-items: center;
|
|
436
|
+
padding: 8px 10px;
|
|
437
|
+
background: var(--ios-input-bg);
|
|
438
|
+
border-radius: 6px;
|
|
439
|
+
cursor: pointer;
|
|
440
|
+
transition: all 0.2s;
|
|
441
|
+
border: 1px solid transparent;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.command-card:hover {
|
|
445
|
+
background: var(--ios-gray-light);
|
|
446
|
+
border-color: var(--ios-blue);
|
|
447
|
+
transform: translateX(2px);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.command-card-main {
|
|
451
|
+
display: flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
gap: 10px;
|
|
454
|
+
flex: 1;
|
|
455
|
+
min-width: 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.command-icon {
|
|
459
|
+
font-size: 20px;
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.command-info {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
gap: 2px;
|
|
467
|
+
min-width: 0;
|
|
468
|
+
flex: 1;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.command-name {
|
|
472
|
+
font-weight: 600;
|
|
473
|
+
font-size: 13px;
|
|
474
|
+
color: var(--text-primary);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.command-text {
|
|
478
|
+
font-family: monospace;
|
|
479
|
+
font-size: 11px;
|
|
480
|
+
color: var(--text-secondary);
|
|
481
|
+
white-space: nowrap;
|
|
482
|
+
overflow: hidden;
|
|
483
|
+
text-overflow: ellipsis;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.command-card-actions {
|
|
487
|
+
display: flex;
|
|
488
|
+
gap: 4px;
|
|
489
|
+
opacity: 0;
|
|
490
|
+
transition: opacity 0.2s;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.command-card:hover .command-card-actions {
|
|
494
|
+
opacity: 1;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.command-action-btn {
|
|
498
|
+
width: 24px;
|
|
499
|
+
height: 24px;
|
|
500
|
+
border: none;
|
|
501
|
+
background: rgba(0, 0, 0, 0.05);
|
|
502
|
+
border-radius: 4px;
|
|
503
|
+
cursor: pointer;
|
|
504
|
+
display: flex;
|
|
505
|
+
align-items: center;
|
|
506
|
+
justify-content: center;
|
|
507
|
+
font-size: 12px;
|
|
508
|
+
transition: all 0.2s;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.command-action-btn:hover {
|
|
512
|
+
background: rgba(0, 0, 0, 0.1);
|
|
513
|
+
transform: scale(1.1);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* Modal Styles */
|
|
517
|
+
.modal {
|
|
518
|
+
display: none;
|
|
519
|
+
position: fixed;
|
|
520
|
+
top: 0;
|
|
521
|
+
left: 0;
|
|
522
|
+
width: 100%;
|
|
523
|
+
height: 100%;
|
|
524
|
+
background: rgba(0, 0, 0, 0.5);
|
|
525
|
+
z-index: 10000;
|
|
526
|
+
align-items: center;
|
|
527
|
+
justify-content: center;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.modal-content {
|
|
531
|
+
background: var(--ios-card);
|
|
532
|
+
border-radius: 12px;
|
|
533
|
+
padding: 20px;
|
|
534
|
+
width: 90%;
|
|
535
|
+
max-width: 400px;
|
|
536
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.modal-content h3 {
|
|
540
|
+
margin: 0 0 15px 0;
|
|
541
|
+
font-size: 16px;
|
|
542
|
+
color: var(--text-primary);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.modal-content .form-group {
|
|
546
|
+
margin-bottom: 15px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.modal-content .form-group label {
|
|
550
|
+
display: block;
|
|
551
|
+
margin-bottom: 5px;
|
|
552
|
+
font-size: 12px;
|
|
553
|
+
font-weight: 600;
|
|
554
|
+
color: var(--text-secondary);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.modal-content .form-group input {
|
|
558
|
+
width: 100%;
|
|
559
|
+
padding: 8px 12px;
|
|
560
|
+
background: var(--ios-input-bg);
|
|
561
|
+
border: 1px solid var(--ios-separator);
|
|
562
|
+
border-radius: 8px;
|
|
563
|
+
font-size: 13px;
|
|
564
|
+
color: var(--text-primary);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.modal-content .form-group input:focus {
|
|
568
|
+
outline: none;
|
|
569
|
+
border-color: var(--ios-blue);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.modal-actions {
|
|
573
|
+
display: flex;
|
|
574
|
+
gap: 10px;
|
|
575
|
+
margin-top: 20px;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.modal-actions .btn {
|
|
579
|
+
flex: 1;
|
|
580
|
+
padding: 10px;
|
|
581
|
+
border: none;
|
|
582
|
+
border-radius: 8px;
|
|
583
|
+
font-size: 13px;
|
|
584
|
+
font-weight: 600;
|
|
585
|
+
cursor: pointer;
|
|
586
|
+
transition: all 0.2s;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.modal-actions .btn:first-child {
|
|
590
|
+
background: var(--ios-gray-light);
|
|
591
|
+
color: var(--text-primary);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
.modal-actions .btn:first-child:hover {
|
|
595
|
+
background: var(--ios-separator);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.modal-actions .btn-primary {
|
|
599
|
+
background: var(--ios-blue);
|
|
600
|
+
color: white;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.modal-actions .btn-primary:hover {
|
|
604
|
+
opacity: 0.9;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.endpoint-title-group {
|
|
608
|
+
display: flex;
|
|
609
|
+
align-items: center;
|
|
610
|
+
gap: 8px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.toggle-icon {
|
|
614
|
+
font-size: 12px;
|
|
615
|
+
color: var(--text-secondary);
|
|
616
|
+
transition: transform 0.2s;
|
|
617
|
+
}
|
|
@@ -76,14 +76,32 @@
|
|
|
76
76
|
</button>
|
|
77
77
|
</div>
|
|
78
78
|
|
|
79
|
-
<!--
|
|
80
|
-
<div class="endpoint-card"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
<!-- Saved Commands Panel -->
|
|
80
|
+
<div class="endpoint-card saved-commands-panel">
|
|
81
|
+
<div class="saved-commands-header">
|
|
82
|
+
<div class="saved-commands-title">
|
|
83
|
+
<span class="command-icon-header">💾</span>
|
|
84
|
+
<span>Saved Commands</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="saved-commands-actions">
|
|
87
|
+
<button class="btn-new-terminal" onclick="createNewTerminal()" title="New Terminal">
|
|
88
|
+
🖥️
|
|
89
|
+
</button>
|
|
90
|
+
<button class="btn-add-command" onclick="openAddCommandModal()" title="Add Command">
|
|
91
|
+
➕
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="saved-commands-content" id="saved-commands-content">
|
|
96
|
+
<div id="commands-list" class="commands-list">
|
|
97
|
+
<!-- Commands will be rendered here -->
|
|
98
|
+
</div>
|
|
99
|
+
<div class="empty-state" id="commands-empty-state" style="display: none;">
|
|
100
|
+
<p style="color: #888; text-align: center; padding: 15px 10px; font-size: 12px; margin: 0;">
|
|
101
|
+
Click ➕ to add a command
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
87
105
|
</div>
|
|
88
106
|
|
|
89
107
|
<!-- System Prompt Section -->
|
|
@@ -280,6 +298,36 @@
|
|
|
280
298
|
|
|
281
299
|
<div id="floating-terminals-layer"></div>
|
|
282
300
|
<div class="toast" id="toast"></div>
|
|
301
|
+
|
|
302
|
+
<!-- Add/Edit Command Modal -->
|
|
303
|
+
<div id="command-modal" class="modal" style="display: none;">
|
|
304
|
+
<div class="modal-content">
|
|
305
|
+
<h3 id="modal-title">Add Command</h3>
|
|
306
|
+
<form id="command-form">
|
|
307
|
+
<div class="form-group">
|
|
308
|
+
<label>Icon (emoji)</label>
|
|
309
|
+
<input type="text" id="command-icon" placeholder="🚀" maxlength="2" required>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="form-group">
|
|
312
|
+
<label>Name</label>
|
|
313
|
+
<input type="text" id="command-name" placeholder="Run Dev Server" required>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="form-group">
|
|
316
|
+
<label>Command</label>
|
|
317
|
+
<input type="text" id="command-text" placeholder="npm run dev" required>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="modal-actions">
|
|
320
|
+
<button type="button" class="btn" onclick="closeCommandModal()">Cancel</button>
|
|
321
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
|
322
|
+
</div>
|
|
323
|
+
</form>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<!-- Smart Log Copy Utilities -->
|
|
328
|
+
<script src="/js/utils/log-utils.js"></script>
|
|
329
|
+
<script src="/js/utils/smart-copy-engine.js"></script>
|
|
330
|
+
|
|
283
331
|
<script type="module" src="/js/main.js"></script>
|
|
284
332
|
</body>
|
|
285
333
|
|