vg-coder-cli 2.0.12 → 2.0.15
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 +6 -19
- package/src/server/api-server.js +55 -21
- package/src/server/terminal-manager.js +82 -0
- package/src/server/views/css/terminal.css +129 -0
- package/src/server/views/dashboard.html +23 -4
- package/src/server/views/js/features/git-view.js +3 -3
- package/src/server/views/js/features/terminal.js +252 -0
- package/src/server/views/js/main.js +13 -39
- package/vg-coder-cli-2.0.14.tgz +0 -0
- package/vg-coder-cli-2.0.15.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vg-coder-cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"description": "🚀 CLI tool to analyze projects, concatenate source files, count tokens, and export HTML with syntax highlighting and copy functionality",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,19 +21,8 @@
|
|
|
21
21
|
"code-analysis",
|
|
22
22
|
"token-counter",
|
|
23
23
|
"ai-helper",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"syntax-highlighting",
|
|
27
|
-
"project-analyzer",
|
|
28
|
-
"source-code",
|
|
29
|
-
"tiktoken",
|
|
30
|
-
"spring-boot",
|
|
31
|
-
"angular",
|
|
32
|
-
"react",
|
|
33
|
-
"vue",
|
|
34
|
-
"java",
|
|
35
|
-
"javascript",
|
|
36
|
-
"typescript"
|
|
24
|
+
"terminal",
|
|
25
|
+
"web-terminal"
|
|
37
26
|
],
|
|
38
27
|
"author": "VG Coder",
|
|
39
28
|
"license": "MIT",
|
|
@@ -41,10 +30,6 @@
|
|
|
41
30
|
"type": "git",
|
|
42
31
|
"url": "https://github.com/tinhthanh/vg-coder-cli.git"
|
|
43
32
|
},
|
|
44
|
-
"bugs": {
|
|
45
|
-
"url": "https://github.com/tinhthanh/vg-coder-cli/issues"
|
|
46
|
-
},
|
|
47
|
-
"homepage": "https://github.com/tinhthanh/vg-coder-cli#readme",
|
|
48
33
|
"dependencies": {
|
|
49
34
|
"commander": "^11.1.0",
|
|
50
35
|
"directory-tree": "^3.5.1",
|
|
@@ -57,7 +42,9 @@
|
|
|
57
42
|
"ora": "^5.4.1",
|
|
58
43
|
"express": "^4.18.2",
|
|
59
44
|
"cors": "^2.8.5",
|
|
60
|
-
"body-parser": "^1.20.2"
|
|
45
|
+
"body-parser": "^1.20.2",
|
|
46
|
+
"node-pty": "^1.0.0",
|
|
47
|
+
"socket.io": "^4.7.2"
|
|
61
48
|
},
|
|
62
49
|
"devDependencies": {
|
|
63
50
|
"jest": "^29.7.0",
|
package/src/server/api-server.js
CHANGED
|
@@ -4,6 +4,8 @@ const bodyParser = require('body-parser');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
6
|
const chalk = require('chalk');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { Server } = require('socket.io');
|
|
7
9
|
const { exec } = require('child_process');
|
|
8
10
|
const util = require('util');
|
|
9
11
|
const execAsync = util.promisify(exec);
|
|
@@ -13,15 +15,24 @@ const ProjectDetector = require('../detectors/project-detector');
|
|
|
13
15
|
const FileScanner = require('../scanner/file-scanner');
|
|
14
16
|
const TokenManager = require('../tokenizer/token-manager');
|
|
15
17
|
const BashExecutor = require('../utils/bash-executor');
|
|
18
|
+
const terminalManager = require('./terminal-manager');
|
|
16
19
|
|
|
17
20
|
class ApiServer {
|
|
18
21
|
constructor(port = 6868) {
|
|
19
22
|
this.port = port;
|
|
20
23
|
this.app = express();
|
|
24
|
+
|
|
25
|
+
// Create HTTP server for Socket.IO
|
|
26
|
+
this.httpServer = http.createServer(this.app);
|
|
27
|
+
this.io = new Server(this.httpServer, {
|
|
28
|
+
cors: { origin: "*" }
|
|
29
|
+
});
|
|
30
|
+
|
|
21
31
|
this.server = null;
|
|
22
32
|
this.workingDir = process.cwd();
|
|
23
33
|
this.setupMiddleware();
|
|
24
34
|
this.setupRoutes();
|
|
35
|
+
this.setupSocketIO();
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
setupMiddleware() {
|
|
@@ -31,7 +42,6 @@ class ApiServer {
|
|
|
31
42
|
this.app.use(express.static(path.join(__dirname, 'views')));
|
|
32
43
|
|
|
33
44
|
this.app.use((req, res, next) => {
|
|
34
|
-
// Log request ngắn gọn
|
|
35
45
|
if (!req.path.includes('.')) {
|
|
36
46
|
console.log(chalk.blue(`[REQ] ${req.method} ${req.path}`));
|
|
37
47
|
}
|
|
@@ -39,6 +49,43 @@ class ApiServer {
|
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
setupSocketIO() {
|
|
53
|
+
this.io.on('connection', (socket) => {
|
|
54
|
+
// Nhận event init với termId
|
|
55
|
+
// FIX: Thêm default value = {} để tránh crash khi data undefined
|
|
56
|
+
socket.on('terminal:init', (data) => {
|
|
57
|
+
if (!data || !data.termId) {
|
|
58
|
+
// console.warn('[Socket] Ignored invalid terminal:init', data);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { termId, cols, rows } = data;
|
|
62
|
+
terminalManager.createTerminal(socket, termId, cols, rows, this.workingDir);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
socket.on('terminal:input', (data) => {
|
|
66
|
+
if (data && data.termId) {
|
|
67
|
+
terminalManager.write(data.termId, data.data);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
socket.on('terminal:resize', (data) => {
|
|
72
|
+
if (data && data.termId) {
|
|
73
|
+
terminalManager.resize(data.termId, data.cols, data.rows);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
socket.on('terminal:kill', (data) => {
|
|
78
|
+
if (data && data.termId) {
|
|
79
|
+
terminalManager.kill(data.termId);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
socket.on('disconnect', () => {
|
|
84
|
+
terminalManager.cleanupSocket(socket.id);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
42
89
|
setupRoutes() {
|
|
43
90
|
this.app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'views', 'dashboard.html')));
|
|
44
91
|
this.app.get('/health', (req, res) => res.json({ status: 'ok', version: packageJson.version }));
|
|
@@ -52,37 +99,24 @@ class ApiServer {
|
|
|
52
99
|
}
|
|
53
100
|
});
|
|
54
101
|
|
|
55
|
-
// --- DEBUG GIT DIFF ENDPOINT ---
|
|
56
102
|
this.app.get('/api/git/diff', async (req, res) => {
|
|
57
|
-
console.log(chalk.yellow('⚡ [GIT] Executing: git diff HEAD'));
|
|
58
103
|
try {
|
|
59
|
-
// Tăng maxBuffer lên 20MB đề phòng diff lớn
|
|
60
104
|
const { stdout, stderr } = await execAsync('git diff HEAD', {
|
|
61
105
|
cwd: this.workingDir,
|
|
62
106
|
maxBuffer: 20 * 1024 * 1024
|
|
63
107
|
});
|
|
64
|
-
|
|
65
|
-
console.log(chalk.green(`✅ [GIT] Success. Output length: ${stdout.length} chars`));
|
|
66
|
-
if (stderr) console.log(chalk.red(`⚠️ [GIT] Stderr: ${stderr}`));
|
|
67
|
-
|
|
68
108
|
res.json({ diff: stdout });
|
|
69
|
-
|
|
70
109
|
} catch (error) {
|
|
71
110
|
console.error(chalk.red('❌ [GIT] Error:'), error.message);
|
|
72
111
|
res.json({ diff: '', error: error.message });
|
|
73
112
|
}
|
|
74
113
|
});
|
|
75
114
|
|
|
76
|
-
|
|
77
|
-
// Để tiết kiệm không gian, tôi giữ nguyên phần logic cũ của các endpoint khác
|
|
78
|
-
// trong thực tế bạn không nên xoá chúng.
|
|
79
|
-
|
|
80
|
-
this.app.post('/api/analyze', async (req, res) => { /* Logic cũ... */
|
|
115
|
+
this.app.post('/api/analyze', async (req, res) => {
|
|
81
116
|
const { path: projectPath, options = {}, specificFiles } = req.body;
|
|
82
117
|
if (!projectPath) return res.status(400).json({ error: 'Missing path' });
|
|
83
118
|
const resolvedPath = path.resolve(projectPath);
|
|
84
119
|
if (!await fs.pathExists(resolvedPath)) return res.status(404).json({ error: 'Path not found' });
|
|
85
|
-
const detector = new ProjectDetector(resolvedPath);
|
|
86
120
|
const scanner = new FileScanner(resolvedPath, {
|
|
87
121
|
extensions: options.extensions ? options.extensions.split(',') : undefined,
|
|
88
122
|
includeHidden: options.includeHidden
|
|
@@ -94,7 +128,7 @@ class ApiServer {
|
|
|
94
128
|
res.send(content);
|
|
95
129
|
});
|
|
96
130
|
|
|
97
|
-
this.app.get('/api/info', async (req, res) => {
|
|
131
|
+
this.app.get('/api/info', async (req, res) => {
|
|
98
132
|
const projectPath = req.query.path;
|
|
99
133
|
if (!projectPath) return res.status(400).json({ error: 'Missing path' });
|
|
100
134
|
const resolvedPath = path.resolve(projectPath);
|
|
@@ -105,7 +139,7 @@ class ApiServer {
|
|
|
105
139
|
res.json({ path: resolvedPath, primaryType: projectInfo.primary, stats: { totalFiles: scanResult.files.length } });
|
|
106
140
|
});
|
|
107
141
|
|
|
108
|
-
this.app.get('/api/structure', async (req, res) => {
|
|
142
|
+
this.app.get('/api/structure', async (req, res) => {
|
|
109
143
|
const projectPath = req.query.path || '.';
|
|
110
144
|
const resolvedPath = path.resolve(projectPath);
|
|
111
145
|
const scanner = new FileScanner(resolvedPath);
|
|
@@ -115,14 +149,14 @@ class ApiServer {
|
|
|
115
149
|
res.json({ path: resolvedPath, structure: enrichedTree });
|
|
116
150
|
});
|
|
117
151
|
|
|
118
|
-
this.app.post('/api/execute', async (req, res) => {
|
|
152
|
+
this.app.post('/api/execute', async (req, res) => {
|
|
119
153
|
const { bash } = req.body;
|
|
120
154
|
const executor = new BashExecutor(this.workingDir);
|
|
121
155
|
const result = await executor.execute(bash);
|
|
122
156
|
res.status(result.success ? 200 : 400).json(result);
|
|
123
157
|
});
|
|
124
158
|
|
|
125
|
-
this.app.delete('/api/clean', async (req, res) => {
|
|
159
|
+
this.app.delete('/api/clean', async (req, res) => {
|
|
126
160
|
await fs.remove(path.resolve(req.body.output));
|
|
127
161
|
res.json({ success: true });
|
|
128
162
|
});
|
|
@@ -130,8 +164,8 @@ class ApiServer {
|
|
|
130
164
|
|
|
131
165
|
async start() {
|
|
132
166
|
return new Promise((resolve) => {
|
|
133
|
-
this.server = this.
|
|
134
|
-
console.log(chalk.green(`\n🚀 VG Coder API Server started on port ${this.port}`));
|
|
167
|
+
this.server = this.httpServer.listen(this.port, () => {
|
|
168
|
+
console.log(chalk.green(`\n🚀 VG Coder API Server & Socket.IO started on port ${this.port}`));
|
|
135
169
|
resolve();
|
|
136
170
|
});
|
|
137
171
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const pty = require('node-pty');
|
|
3
|
+
|
|
4
|
+
class TerminalManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
// Map: termId -> { process: pty, socketId: string }
|
|
7
|
+
this.sessions = new Map();
|
|
8
|
+
this.shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
createTerminal(socket, termId, cols = 80, rows = 24, cwd) {
|
|
12
|
+
try {
|
|
13
|
+
const term = pty.spawn(this.shell, [], {
|
|
14
|
+
name: 'xterm-256color',
|
|
15
|
+
cols: cols,
|
|
16
|
+
rows: rows,
|
|
17
|
+
cwd: cwd || process.cwd(),
|
|
18
|
+
env: process.env
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
this.sessions.set(termId, {
|
|
22
|
+
process: term,
|
|
23
|
+
socketId: socket.id
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Gửi dữ liệu kèm theo termId để Frontend biết của cửa sổ nào
|
|
27
|
+
term.onData((data) => {
|
|
28
|
+
socket.emit('terminal:data', { termId, data });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
term.onExit(() => {
|
|
32
|
+
if (this.sessions.has(termId)) {
|
|
33
|
+
socket.emit('terminal:exit', { termId });
|
|
34
|
+
this.sessions.delete(termId);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return term;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Failed to create terminal:', error);
|
|
41
|
+
socket.emit('terminal:error', { termId, error: 'Failed to create process' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
write(termId, data) {
|
|
46
|
+
const session = this.sessions.get(termId);
|
|
47
|
+
if (session) {
|
|
48
|
+
session.process.write(data);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resize(termId, cols, rows) {
|
|
53
|
+
const session = this.sessions.get(termId);
|
|
54
|
+
if (session) {
|
|
55
|
+
try {
|
|
56
|
+
session.process.resize(cols, rows);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
// Ignore resize errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
kill(termId) {
|
|
64
|
+
const session = this.sessions.get(termId);
|
|
65
|
+
if (session) {
|
|
66
|
+
session.process.kill();
|
|
67
|
+
this.sessions.delete(termId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Dọn dẹp tất cả terminal của một socket khi client disconnect
|
|
72
|
+
cleanupSocket(socketId) {
|
|
73
|
+
for (const [termId, session] of this.sessions.entries()) {
|
|
74
|
+
if (session.socketId === socketId) {
|
|
75
|
+
session.process.kill();
|
|
76
|
+
this.sessions.delete(termId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = new TerminalManager();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* Layer chứa các terminal trôi nổi */
|
|
2
|
+
#floating-terminals-layer {
|
|
3
|
+
position: fixed;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
width: 0;
|
|
7
|
+
height: 0;
|
|
8
|
+
z-index: 10000; /* Cao hơn mọi thứ */
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Style cho từng cửa sổ terminal */
|
|
12
|
+
.floating-terminal {
|
|
13
|
+
position: absolute;
|
|
14
|
+
width: 600px;
|
|
15
|
+
height: 400px;
|
|
16
|
+
background: #1e1e1e;
|
|
17
|
+
border-radius: 8px;
|
|
18
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
|
19
|
+
border: 1px solid #333;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
resize: both; /* Cho phép resize cửa sổ */
|
|
24
|
+
min-width: 300px;
|
|
25
|
+
min-height: 200px;
|
|
26
|
+
transition: height 0.2s ease, width 0.2s ease;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* --- TRẠNG THÁI MINIMIZED --- */
|
|
30
|
+
.floating-terminal.minimized {
|
|
31
|
+
height: 36px !important; /* Chỉ hiện header */
|
|
32
|
+
min-height: 36px !important;
|
|
33
|
+
width: 200px !important; /* Thu nhỏ chiều ngang */
|
|
34
|
+
resize: none; /* Không cho resize khi đang minimize */
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.floating-terminal.minimized .terminal-body {
|
|
39
|
+
display: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Header dùng để drag */
|
|
43
|
+
.terminal-header {
|
|
44
|
+
background: #252526;
|
|
45
|
+
padding: 8px 12px;
|
|
46
|
+
display: flex;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
align-items: center;
|
|
49
|
+
cursor: grab;
|
|
50
|
+
border-bottom: 1px solid #333;
|
|
51
|
+
user-select: none;
|
|
52
|
+
height: 36px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.terminal-header:active {
|
|
56
|
+
cursor: grabbing;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.terminal-title-group {
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 8px;
|
|
63
|
+
color: #ccc;
|
|
64
|
+
font-family: monospace;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
text-overflow: ellipsis;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.terminal-controls {
|
|
73
|
+
display: flex;
|
|
74
|
+
gap: 6px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.term-btn {
|
|
78
|
+
width: 12px;
|
|
79
|
+
height: 12px;
|
|
80
|
+
border-radius: 50%;
|
|
81
|
+
border: none;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
padding: 0;
|
|
87
|
+
font-size: 8px;
|
|
88
|
+
color: rgba(0,0,0,0.5);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.term-btn:hover {
|
|
92
|
+
color: rgba(0,0,0,0.8);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.term-btn.close {
|
|
96
|
+
background: #FF5F56;
|
|
97
|
+
}
|
|
98
|
+
.term-btn.close:hover {
|
|
99
|
+
background: #ff3b30;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.term-btn.minimize {
|
|
103
|
+
background: #FFBD2E;
|
|
104
|
+
}
|
|
105
|
+
.term-btn.minimize:hover {
|
|
106
|
+
background: #ffad08;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.term-btn.maximize {
|
|
110
|
+
background: #27C93F;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Phần nội dung chứa xterm */
|
|
114
|
+
.terminal-body {
|
|
115
|
+
flex: 1;
|
|
116
|
+
position: relative;
|
|
117
|
+
padding: 5px;
|
|
118
|
+
background: #1e1e1e;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Scrollbar customization */
|
|
123
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
124
|
+
width: 8px;
|
|
125
|
+
}
|
|
126
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
127
|
+
background: #424242;
|
|
128
|
+
border-radius: 4px;
|
|
129
|
+
}
|
|
@@ -10,11 +10,21 @@
|
|
|
10
10
|
<link rel="stylesheet" href="/css/structure.css">
|
|
11
11
|
<link rel="stylesheet" href="/css/iframe.css">
|
|
12
12
|
<link rel="stylesheet" href="/css/git-view.css">
|
|
13
|
+
<link rel="stylesheet" href="/css/terminal.css">
|
|
13
14
|
|
|
14
|
-
<!--
|
|
15
|
+
<!-- Syntax Highlighting -->
|
|
15
16
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
16
17
|
|
|
18
|
+
<!-- Git Diff -->
|
|
17
19
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
|
|
20
|
+
|
|
21
|
+
<!-- Terminal Dependencies -->
|
|
22
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
|
23
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
24
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
25
|
+
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
26
|
+
|
|
27
|
+
<!-- Other Libs -->
|
|
18
28
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
|
19
29
|
|
|
20
30
|
<script>
|
|
@@ -39,6 +49,14 @@
|
|
|
39
49
|
<span id="theme-icon">🌙</span>
|
|
40
50
|
</button>
|
|
41
51
|
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Quick Actions -->
|
|
54
|
+
<div class="endpoint-card" style="display: flex; gap: 10px; align-items: center; justify-content: space-between;">
|
|
55
|
+
<span style="font-weight: 600;">Tools:</span>
|
|
56
|
+
<button class="btn" onclick="createNewTerminal()" style="background: #252526; border: 1px solid #444;">
|
|
57
|
+
<span>🖥️</span> New Terminal
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
42
60
|
|
|
43
61
|
<!-- System Prompt Section -->
|
|
44
62
|
<div class="system-prompt-card">
|
|
@@ -104,7 +122,7 @@
|
|
|
104
122
|
</div>
|
|
105
123
|
|
|
106
124
|
<div class="endpoints">
|
|
107
|
-
<!-- Execute Bash -->
|
|
125
|
+
<!-- Execute Bash (RESTORED) -->
|
|
108
126
|
<div class="endpoint-card">
|
|
109
127
|
<div class="endpoint-header">
|
|
110
128
|
<div class="endpoint-title-group">
|
|
@@ -193,7 +211,6 @@
|
|
|
193
211
|
</select>
|
|
194
212
|
</div>
|
|
195
213
|
|
|
196
|
-
<!-- NEW: Git Toggle Button -->
|
|
197
214
|
<button id="git-view-toggle" class="git-toggle-btn">View Changes</button>
|
|
198
215
|
<button id="git-refresh-btn" class="btn-icon-head" style="display:none; margin-left:5px;" title="Refresh">↻</button>
|
|
199
216
|
|
|
@@ -213,9 +230,11 @@
|
|
|
213
230
|
</div>
|
|
214
231
|
</div>
|
|
215
232
|
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Floating Terminals Container (Fixed on top of everything) -->
|
|
235
|
+
<div id="floating-terminals-layer"></div>
|
|
216
236
|
|
|
217
237
|
<div class="toast" id="toast"></div>
|
|
218
238
|
<script type="module" src="/js/main.js"></script>
|
|
219
239
|
</body>
|
|
220
|
-
|
|
221
240
|
</html>
|
|
@@ -31,9 +31,8 @@ async function toggleGitMode() {
|
|
|
31
31
|
const rect = gitContainer.getBoundingClientRect();
|
|
32
32
|
console.log('[GitView] Container Size:', rect.width, 'x', rect.height);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
34
|
+
// FIX: Always load changes when opening, removing the empty check
|
|
35
|
+
await loadGitChanges();
|
|
37
36
|
} else {
|
|
38
37
|
gitContainer.classList.remove('active');
|
|
39
38
|
toggleBtn.classList.remove('active');
|
|
@@ -53,6 +52,7 @@ async function loadGitChanges() {
|
|
|
53
52
|
refreshBtn.disabled = true;
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
// Only show loading if we are replacing content or it's empty
|
|
56
56
|
gitContainer.innerHTML = '<div class="git-loading-msg">Loading git changes... (Check Console if stuck)</div>';
|
|
57
57
|
|
|
58
58
|
try {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Terminal Logic: Multi-instance & Floating
|
|
2
|
+
|
|
3
|
+
let socket;
|
|
4
|
+
const activeTerminals = new Map(); // Map<termId, { term, fitAddon, element, prevSize }>
|
|
5
|
+
|
|
6
|
+
// Z-Index Management
|
|
7
|
+
let maxZIndex = 10001;
|
|
8
|
+
|
|
9
|
+
export function initTerminal() {
|
|
10
|
+
if (typeof io === 'undefined' || typeof Terminal === 'undefined') {
|
|
11
|
+
console.error('Libraries missing for Terminal');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 1. Init Socket
|
|
16
|
+
socket = io();
|
|
17
|
+
|
|
18
|
+
// 2. Global Event Listeners
|
|
19
|
+
socket.on('terminal:data', ({ termId, data }) => {
|
|
20
|
+
const session = activeTerminals.get(termId);
|
|
21
|
+
if (session) {
|
|
22
|
+
session.term.write(data);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
socket.on('terminal:exit', ({ termId }) => {
|
|
27
|
+
closeTerminalUI(termId);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tạo một cửa sổ terminal mới
|
|
33
|
+
*/
|
|
34
|
+
export function createNewTerminal() {
|
|
35
|
+
const termId = 'term_' + Date.now();
|
|
36
|
+
const layer = document.getElementById('floating-terminals-layer');
|
|
37
|
+
|
|
38
|
+
// 1. Create DOM Elements
|
|
39
|
+
const wrapper = document.createElement('div');
|
|
40
|
+
wrapper.className = 'floating-terminal';
|
|
41
|
+
wrapper.id = `wrapper-${termId}`;
|
|
42
|
+
|
|
43
|
+
// Offset vị trí một chút nếu mở nhiều cái
|
|
44
|
+
const offset = (activeTerminals.size % 10) * 30;
|
|
45
|
+
wrapper.style.top = `${100 + offset}px`;
|
|
46
|
+
wrapper.style.left = `${400 + offset}px`;
|
|
47
|
+
wrapper.style.zIndex = ++maxZIndex;
|
|
48
|
+
|
|
49
|
+
// HTML Template - Đã thêm sự kiện onclick cho nút Minimize
|
|
50
|
+
wrapper.innerHTML = `
|
|
51
|
+
<div class="terminal-header" id="header-${termId}" ondblclick="window.toggleMinimize('${termId}')">
|
|
52
|
+
<div class="terminal-title-group">
|
|
53
|
+
<span>>_</span> Terminal (${activeTerminals.size + 1})
|
|
54
|
+
</div>
|
|
55
|
+
<div class="terminal-controls">
|
|
56
|
+
<button class="term-btn minimize" onclick="window.toggleMinimize('${termId}')" title="Minimize/Restore">-</button>
|
|
57
|
+
<button class="term-btn maximize" onclick="window.toggleMaximize('${termId}')" title="Maximize">+</button>
|
|
58
|
+
<button class="term-btn close" onclick="window.closeTerminal('${termId}')" title="Close">x</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="terminal-body" id="body-${termId}"></div>
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
layer.appendChild(wrapper);
|
|
65
|
+
|
|
66
|
+
// 2. Init xterm.js
|
|
67
|
+
const term = new Terminal({
|
|
68
|
+
cursorBlink: true,
|
|
69
|
+
fontSize: 13,
|
|
70
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
71
|
+
theme: {
|
|
72
|
+
background: '#1e1e1e',
|
|
73
|
+
foreground: '#f0f0f0'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
78
|
+
term.loadAddon(fitAddon);
|
|
79
|
+
|
|
80
|
+
term.open(document.getElementById(`body-${termId}`));
|
|
81
|
+
fitAddon.fit();
|
|
82
|
+
|
|
83
|
+
// 3. Register Events
|
|
84
|
+
|
|
85
|
+
// Bring to front on click
|
|
86
|
+
wrapper.addEventListener('mousedown', () => {
|
|
87
|
+
wrapper.style.zIndex = ++maxZIndex;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Send input
|
|
91
|
+
term.onData(data => {
|
|
92
|
+
socket.emit('terminal:input', { termId, data });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Resize Observer to refit terminal when window is resized
|
|
96
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
97
|
+
// Chỉ fit lại nếu không bị minimized
|
|
98
|
+
if (!wrapper.classList.contains('minimized')) {
|
|
99
|
+
try {
|
|
100
|
+
fitAddon.fit();
|
|
101
|
+
socket.emit('terminal:resize', {
|
|
102
|
+
termId,
|
|
103
|
+
cols: term.cols,
|
|
104
|
+
rows: term.rows
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
resizeObserver.observe(document.getElementById(`body-${termId}`));
|
|
110
|
+
|
|
111
|
+
// 4. Make Draggable
|
|
112
|
+
makeDraggable(wrapper, document.getElementById(`header-${termId}`));
|
|
113
|
+
|
|
114
|
+
// 5. Store Session
|
|
115
|
+
activeTerminals.set(termId, { term, fitAddon, element: wrapper });
|
|
116
|
+
|
|
117
|
+
// 6. Init Backend Process
|
|
118
|
+
socket.emit('terminal:init', {
|
|
119
|
+
termId,
|
|
120
|
+
cols: term.cols,
|
|
121
|
+
rows: term.rows
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Đóng terminal UI & Process
|
|
127
|
+
*/
|
|
128
|
+
export function closeTerminalUI(termId) {
|
|
129
|
+
const session = activeTerminals.get(termId);
|
|
130
|
+
if (session) {
|
|
131
|
+
// Remove from DOM
|
|
132
|
+
session.element.remove();
|
|
133
|
+
// Clean map
|
|
134
|
+
activeTerminals.delete(termId);
|
|
135
|
+
// Tell server to kill
|
|
136
|
+
socket.emit('terminal:kill', { termId });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Toggle Minimize/Restore
|
|
142
|
+
*/
|
|
143
|
+
export function toggleMinimize(termId) {
|
|
144
|
+
const session = activeTerminals.get(termId);
|
|
145
|
+
if (!session) return;
|
|
146
|
+
|
|
147
|
+
const el = session.element;
|
|
148
|
+
const isMinimized = el.classList.contains('minimized');
|
|
149
|
+
|
|
150
|
+
if (isMinimized) {
|
|
151
|
+
// Restore
|
|
152
|
+
el.classList.remove('minimized');
|
|
153
|
+
|
|
154
|
+
// Restore size logic if needed, but CSS transition handles visualization
|
|
155
|
+
// Need to refit xterm after transition
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
try { session.fitAddon.fit(); } catch(e){}
|
|
158
|
+
}, 250);
|
|
159
|
+
} else {
|
|
160
|
+
// Minimize
|
|
161
|
+
// Store current width/height if we want to restore exact pixel values later
|
|
162
|
+
// (CSS handles the visual hiding)
|
|
163
|
+
el.classList.add('minimized');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Toggle Maximize (Simple Fullscreen simulation)
|
|
169
|
+
*/
|
|
170
|
+
export function toggleMaximize(termId) {
|
|
171
|
+
const session = activeTerminals.get(termId);
|
|
172
|
+
if (!session) return;
|
|
173
|
+
|
|
174
|
+
const el = session.element;
|
|
175
|
+
const isMaximized = el.classList.contains('maximized');
|
|
176
|
+
|
|
177
|
+
if (isMaximized) {
|
|
178
|
+
// Restore normal size
|
|
179
|
+
el.classList.remove('maximized');
|
|
180
|
+
el.style.width = session.prevSize?.width || '600px';
|
|
181
|
+
el.style.height = session.prevSize?.height || '400px';
|
|
182
|
+
el.style.top = session.prevSize?.top || '100px';
|
|
183
|
+
el.style.left = session.prevSize?.left || '400px';
|
|
184
|
+
} else {
|
|
185
|
+
// Save current state
|
|
186
|
+
session.prevSize = {
|
|
187
|
+
width: el.style.width,
|
|
188
|
+
height: el.style.height,
|
|
189
|
+
top: el.style.top,
|
|
190
|
+
left: el.style.left
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Go full "floating" screen
|
|
194
|
+
el.classList.add('maximized');
|
|
195
|
+
el.style.width = '90vw';
|
|
196
|
+
el.style.height = '80vh';
|
|
197
|
+
el.style.top = '10vh';
|
|
198
|
+
el.style.left = '5vw';
|
|
199
|
+
el.classList.remove('minimized'); // Ensure not minimized
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
try { session.fitAddon.fit(); } catch(e){}
|
|
204
|
+
}, 250);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Logic Drag & Drop
|
|
210
|
+
*/
|
|
211
|
+
function makeDraggable(element, handle) {
|
|
212
|
+
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
|
213
|
+
|
|
214
|
+
handle.onmousedown = dragMouseDown;
|
|
215
|
+
|
|
216
|
+
function dragMouseDown(e) {
|
|
217
|
+
// Don't drag if clicking buttons
|
|
218
|
+
if(e.target.tagName === 'BUTTON') return;
|
|
219
|
+
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
// Get mouse cursor position at startup
|
|
222
|
+
pos3 = e.clientX;
|
|
223
|
+
pos4 = e.clientY;
|
|
224
|
+
document.onmouseup = closeDragElement;
|
|
225
|
+
// Call function whenever cursor moves
|
|
226
|
+
document.onmousemove = elementDrag;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function elementDrag(e) {
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
// Calculate new cursor position
|
|
232
|
+
pos1 = pos3 - e.clientX;
|
|
233
|
+
pos2 = pos4 - e.clientY;
|
|
234
|
+
pos3 = e.clientX;
|
|
235
|
+
pos4 = e.clientY;
|
|
236
|
+
// Set element's new position
|
|
237
|
+
element.style.top = (element.offsetTop - pos2) + "px";
|
|
238
|
+
element.style.left = (element.offsetLeft - pos1) + "px";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function closeDragElement() {
|
|
242
|
+
// Stop moving when mouse button is released
|
|
243
|
+
document.onmouseup = null;
|
|
244
|
+
document.onmousemove = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Global Exports for HTML onclick
|
|
249
|
+
window.createNewTerminal = createNewTerminal;
|
|
250
|
+
window.closeTerminal = closeTerminalUI;
|
|
251
|
+
window.toggleMinimize = toggleMinimize;
|
|
252
|
+
window.toggleMaximize = toggleMaximize;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// Main entry point - Initialize application
|
|
2
2
|
import { SYSTEM_PROMPT } from './config.js';
|
|
3
3
|
import { checkHealth } from './api.js';
|
|
4
|
-
import './handlers.js';
|
|
4
|
+
import './handlers.js';
|
|
5
5
|
import { showToast, showCopiedState } from './utils.js';
|
|
6
6
|
import { initIframeManager } from './features/iframe-manager.js';
|
|
7
7
|
import { initGitView } from './features/git-view.js';
|
|
8
|
+
import { initTerminal, createNewTerminal } from './features/terminal.js';
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
* Initialize application on DOM ready
|
|
11
|
-
*/
|
|
12
10
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
13
11
|
// Load system prompt text
|
|
14
12
|
document.getElementById('prompt-text').textContent = SYSTEM_PROMPT;
|
|
@@ -27,11 +25,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
27
25
|
|
|
28
26
|
// Initialize Git View
|
|
29
27
|
initGitView();
|
|
28
|
+
|
|
29
|
+
// Initialize Terminal System
|
|
30
|
+
initTerminal();
|
|
31
|
+
|
|
32
|
+
// Auto open one terminal on start (Optional)
|
|
33
|
+
// setTimeout(() => createNewTerminal(), 500);
|
|
30
34
|
});
|
|
31
35
|
|
|
32
|
-
/**
|
|
33
|
-
* Check and update server status
|
|
34
|
-
*/
|
|
35
36
|
async function checkServerStatus() {
|
|
36
37
|
const statusEl = document.getElementById('status');
|
|
37
38
|
const isHealthy = await checkHealth();
|
|
@@ -47,28 +48,15 @@ async function checkServerStatus() {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
* Initialize Theme Logic
|
|
52
|
-
*/
|
|
53
51
|
function initTheme() {
|
|
54
52
|
const themeBtn = document.getElementById('theme-toggle');
|
|
55
|
-
const themeIcon = document.getElementById('theme-icon');
|
|
56
|
-
|
|
57
|
-
// Get current theme from DOM (set by inline script) or localStorage
|
|
58
53
|
let currentTheme = localStorage.getItem('theme') || 'light';
|
|
59
|
-
|
|
60
|
-
// Update icon initially
|
|
61
54
|
updateThemeIcon(currentTheme);
|
|
62
55
|
|
|
63
56
|
themeBtn.addEventListener('click', () => {
|
|
64
|
-
// Toggle theme
|
|
65
57
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
|
66
|
-
|
|
67
|
-
// Update DOM
|
|
68
58
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
69
59
|
localStorage.setItem('theme', newTheme);
|
|
70
|
-
|
|
71
|
-
// Update local state
|
|
72
60
|
currentTheme = newTheme;
|
|
73
61
|
updateThemeIcon(newTheme);
|
|
74
62
|
});
|
|
@@ -83,25 +71,19 @@ function updateThemeIcon(theme) {
|
|
|
83
71
|
}
|
|
84
72
|
}
|
|
85
73
|
|
|
86
|
-
// Extension Helpers
|
|
87
74
|
async function loadExtensionPath() {
|
|
88
75
|
try {
|
|
89
76
|
const res = await fetch('/api/extension-path');
|
|
90
77
|
const data = await res.json();
|
|
91
78
|
const input = document.getElementById('extension-path-input');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
input.value =
|
|
95
|
-
} else {
|
|
96
|
-
input.value = "Error: Extension folder not found. Run 'npm run build' first.";
|
|
79
|
+
if (data.exists) input.value = data.path;
|
|
80
|
+
else {
|
|
81
|
+
input.value = "Error: Extension folder not found.";
|
|
97
82
|
input.style.color = "var(--ios-red)";
|
|
98
83
|
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error('Failed to load extension path', err);
|
|
101
|
-
}
|
|
84
|
+
} catch (err) {}
|
|
102
85
|
}
|
|
103
86
|
|
|
104
|
-
// Expose extension handlers to window for onclick events
|
|
105
87
|
window.toggleExtensionGuide = function() {
|
|
106
88
|
const content = document.getElementById('extension-content');
|
|
107
89
|
const icon = document.getElementById('ext-toggle-icon');
|
|
@@ -114,12 +96,9 @@ window.copyExtensionPath = function(event) {
|
|
|
114
96
|
const btn = event.currentTarget;
|
|
115
97
|
const icon = document.getElementById('ext-copy-icon');
|
|
116
98
|
const text = document.getElementById('ext-copy-text');
|
|
117
|
-
|
|
118
99
|
navigator.clipboard.writeText(input.value).then(() => {
|
|
119
100
|
showCopiedState(btn, icon, text, '📋', 'Copy Path');
|
|
120
101
|
showToast('Đã copy đường dẫn extension', 'success');
|
|
121
|
-
}).catch(err => {
|
|
122
|
-
showToast('Lỗi copy: ' + err.message, 'error');
|
|
123
102
|
});
|
|
124
103
|
}
|
|
125
104
|
|
|
@@ -127,14 +106,9 @@ window.copyChromeUrl = function(event) {
|
|
|
127
106
|
const input = document.getElementById('chrome-url-input');
|
|
128
107
|
const btn = event.currentTarget;
|
|
129
108
|
const originalText = btn.innerHTML;
|
|
130
|
-
|
|
131
109
|
navigator.clipboard.writeText(input.value).then(() => {
|
|
132
110
|
btn.innerHTML = '✓';
|
|
133
111
|
showToast('Đã copy URL', 'success');
|
|
134
|
-
setTimeout(() =>
|
|
135
|
-
btn.innerHTML = originalText;
|
|
136
|
-
}, 1500);
|
|
137
|
-
}).catch(err => {
|
|
138
|
-
showToast('Lỗi copy: ' + err.message, 'error');
|
|
112
|
+
setTimeout(() => btn.innerHTML = originalText, 1500);
|
|
139
113
|
});
|
|
140
114
|
}
|
|
Binary file
|
|
Binary file
|