vg-coder-cli 2.0.19 → 2.0.22
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 +3 -2
- package/src/index.js +4 -4
- package/src/server/api-server.js +88 -27
- package/src/server/views/css/editor.css +161 -0
- package/src/server/views/css/git-view.css +50 -15
- package/src/server/views/css/monaco.css +15 -0
- package/src/server/views/dashboard.css +27 -5
- package/src/server/views/dashboard.html +105 -63
- package/src/server/views/js/features/editor-tabs.js +123 -0
- package/src/server/views/js/features/git-view.js +10 -1
- package/src/server/views/js/features/iframe-manager.js +10 -7
- package/src/server/views/js/features/monaco-manager.js +160 -0
- package/src/server/views/js/features/structure.js +20 -6
- package/src/server/views/js/main.js +43 -4
- package/vg-coder-cli-2.0.21.tgz +0 -0
- package/vg-coder-cli-2.0.22.tgz +0 -0
- package/change.sh +0 -0
- package/vg-coder-cli-2.0.17.tgz +0 -0
- package/vg-coder-cli-2.0.19.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.22",
|
|
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": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"ora": "^5.4.1",
|
|
45
45
|
"path": "^0.12.7",
|
|
46
46
|
"socket.io": "^4.7.2",
|
|
47
|
+
"socket.io-client": "^4.7.2",
|
|
47
48
|
"tiktoken": "^1.0.10",
|
|
48
49
|
"vg-coder-cli": "^2.0.15"
|
|
49
50
|
},
|
|
@@ -56,4 +57,4 @@
|
|
|
56
57
|
"engines": {
|
|
57
58
|
"node": ">=16.0.0"
|
|
58
59
|
}
|
|
59
|
-
}
|
|
60
|
+
}
|
package/src/index.js
CHANGED
|
@@ -372,13 +372,13 @@ class VGCoderCLI {
|
|
|
372
372
|
*/
|
|
373
373
|
async handleStart(options) {
|
|
374
374
|
try {
|
|
375
|
-
const
|
|
376
|
-
const server = new ApiServer(
|
|
375
|
+
const initialPort = parseInt(options.port);
|
|
376
|
+
const server = new ApiServer(initialPort);
|
|
377
377
|
|
|
378
378
|
await server.start();
|
|
379
379
|
|
|
380
|
-
// Auto-open browser to dashboard
|
|
381
|
-
const dashboardUrl = `http://localhost:${port}`;
|
|
380
|
+
// Auto-open browser to dashboard using actual port from server
|
|
381
|
+
const dashboardUrl = `http://localhost:${server.port}`;
|
|
382
382
|
const { exec } = require('child_process');
|
|
383
383
|
const platform = process.platform;
|
|
384
384
|
|
package/src/server/api-server.js
CHANGED
|
@@ -94,12 +94,57 @@ class ApiServer {
|
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
// --- FILE OPERATIONS (NEW) ---
|
|
98
|
+
|
|
99
|
+
// Read raw file content
|
|
100
|
+
this.app.get('/api/read-file', async (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const filePath = req.query.path;
|
|
103
|
+
if (!filePath) return res.status(400).json({ error: 'Missing path' });
|
|
104
|
+
|
|
105
|
+
// Prevent directory traversal (basic check)
|
|
106
|
+
const resolvedPath = path.resolve(this.workingDir, filePath);
|
|
107
|
+
if (!resolvedPath.startsWith(this.workingDir)) {
|
|
108
|
+
// Allow reading but log warning - in dev tool we might want flexibility
|
|
109
|
+
// For strict mode: return res.status(403).json({ error: 'Access denied' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!await fs.pathExists(resolvedPath)) {
|
|
113
|
+
return res.status(404).json({ error: 'File not found' });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const content = await fs.readFile(resolvedPath, 'utf8');
|
|
117
|
+
res.json({ content, path: filePath });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
res.status(500).json({ error: error.message });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Save file content
|
|
124
|
+
this.app.post('/api/save-file', async (req, res) => {
|
|
125
|
+
try {
|
|
126
|
+
const { path: filePath, content } = req.body;
|
|
127
|
+
if (!filePath || content === undefined) return res.status(400).json({ error: 'Missing data' });
|
|
128
|
+
|
|
129
|
+
const resolvedPath = path.resolve(this.workingDir, filePath);
|
|
130
|
+
|
|
131
|
+
// Security check
|
|
132
|
+
if (!resolvedPath.startsWith(this.workingDir)) {
|
|
133
|
+
// For strict mode: return res.status(403).json({ error: 'Access denied' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await fs.writeFile(resolvedPath, content, 'utf8');
|
|
137
|
+
res.json({ success: true });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
res.status(500).json({ error: error.message });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
97
143
|
// --- GIT API START ---
|
|
98
144
|
|
|
99
145
|
// Get Git Status
|
|
100
146
|
this.app.get('/api/git/status', async (req, res) => {
|
|
101
147
|
try {
|
|
102
|
-
// FIX: Added -u flag to show individual files in untracked directories
|
|
103
148
|
const { stdout } = await execAsync('git status --porcelain -u', { cwd: this.workingDir });
|
|
104
149
|
|
|
105
150
|
const staged = [];
|
|
@@ -158,23 +203,16 @@ class ApiServer {
|
|
|
158
203
|
}
|
|
159
204
|
});
|
|
160
205
|
|
|
161
|
-
// Git Discard
|
|
206
|
+
// Git Discard
|
|
162
207
|
this.app.post('/api/git/discard', async (req, res) => {
|
|
163
208
|
try {
|
|
164
|
-
const { files } = req.body;
|
|
165
|
-
|
|
166
|
-
// Discard All
|
|
209
|
+
const { files } = req.body;
|
|
167
210
|
if (files.includes('*')) {
|
|
168
|
-
// Restore tracked files
|
|
169
211
|
try { await execAsync('git restore .', { cwd: this.workingDir }); } catch (e) {}
|
|
170
|
-
// Clean untracked files
|
|
171
212
|
try { await execAsync('git clean -fd', { cwd: this.workingDir }); } catch (e) {}
|
|
172
213
|
} else {
|
|
173
|
-
// Discard specific files
|
|
174
214
|
for (const file of files) {
|
|
175
|
-
// Try restore (for tracked modified/deleted)
|
|
176
215
|
try { await execAsync(`git restore "${file}"`, { cwd: this.workingDir }); } catch (e) {}
|
|
177
|
-
// Try clean (for untracked)
|
|
178
216
|
try { await execAsync(`git clean -f "${file}"`, { cwd: this.workingDir }); } catch (e) {}
|
|
179
217
|
}
|
|
180
218
|
}
|
|
@@ -205,25 +243,17 @@ class ApiServer {
|
|
|
205
243
|
const type = req.query.type || 'working';
|
|
206
244
|
|
|
207
245
|
let cmd = '';
|
|
208
|
-
|
|
209
246
|
if (type === 'staged') {
|
|
210
247
|
cmd = file ? `git diff --cached -- "${file}"` : `git diff --cached`;
|
|
211
248
|
} else {
|
|
212
249
|
if (file) {
|
|
213
|
-
// Check if directory to avoid EISDIR (Safety Check)
|
|
214
250
|
try {
|
|
215
251
|
const filePath = path.join(this.workingDir, file);
|
|
216
252
|
if (await fs.pathExists(filePath)) {
|
|
217
253
|
const stat = await fs.stat(filePath);
|
|
218
|
-
if (stat.isDirectory()) {
|
|
219
|
-
return res.json({ diff: '' });
|
|
220
|
-
}
|
|
254
|
+
if (stat.isDirectory()) return res.json({ diff: '' });
|
|
221
255
|
}
|
|
222
|
-
} catch (e) {
|
|
223
|
-
// Ignore stat errors
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Check untracked
|
|
256
|
+
} catch (e) {}
|
|
227
257
|
const { stdout: isUntracked } = await execAsync(`git ls-files --others --exclude-standard "${file}"`, { cwd: this.workingDir });
|
|
228
258
|
if (isUntracked.trim()) {
|
|
229
259
|
const content = await fs.readFile(path.join(this.workingDir, file), 'utf8');
|
|
@@ -236,7 +266,6 @@ class ApiServer {
|
|
|
236
266
|
cmd = `git diff`;
|
|
237
267
|
}
|
|
238
268
|
}
|
|
239
|
-
|
|
240
269
|
const { stdout } = await execAsync(cmd, { cwd: this.workingDir, maxBuffer: 20 * 1024 * 1024 });
|
|
241
270
|
res.json({ diff: stdout });
|
|
242
271
|
} catch (error) {
|
|
@@ -245,7 +274,7 @@ class ApiServer {
|
|
|
245
274
|
}
|
|
246
275
|
});
|
|
247
276
|
|
|
248
|
-
// ---
|
|
277
|
+
// --- GENERAL API ---
|
|
249
278
|
|
|
250
279
|
this.app.post('/api/analyze', async (req, res) => {
|
|
251
280
|
const { path: projectPath, options = {}, specificFiles } = req.body;
|
|
@@ -298,11 +327,43 @@ class ApiServer {
|
|
|
298
327
|
}
|
|
299
328
|
|
|
300
329
|
async start() {
|
|
301
|
-
return new Promise((resolve) => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const tryPort = (port) => {
|
|
332
|
+
const onError = (e) => {
|
|
333
|
+
if (e.code === 'EADDRINUSE') {
|
|
334
|
+
console.log(chalk.yellow(`⚠️ Port ${port} is busy, trying ${port + 1}...`));
|
|
335
|
+
this.httpServer.close();
|
|
336
|
+
tryPort(port + 1);
|
|
337
|
+
} else {
|
|
338
|
+
this.httpServer.removeListener('error', onError);
|
|
339
|
+
reject(e);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
this.httpServer.once('error', onError);
|
|
344
|
+
|
|
345
|
+
this.server = this.httpServer.listen(port, () => {
|
|
346
|
+
this.httpServer.removeListener('error', onError);
|
|
347
|
+
|
|
348
|
+
// Update actual port
|
|
349
|
+
this.port = this.server.address().port;
|
|
350
|
+
|
|
351
|
+
const projectName = path.basename(this.workingDir);
|
|
352
|
+
const startTime = new Date().toLocaleString();
|
|
353
|
+
|
|
354
|
+
console.log(chalk.green('\n──────────────────────────────────────────────────'));
|
|
355
|
+
console.log(`🚀 ${chalk.bold('VG Coder Server')} ${chalk.green('● Online')}`);
|
|
356
|
+
console.log(chalk.gray('──────────────────────────────────────────────────'));
|
|
357
|
+
console.log(`📁 Project: ${chalk.cyan(projectName)}`);
|
|
358
|
+
console.log(`⏰ Started: ${chalk.yellow(startTime)}`);
|
|
359
|
+
console.log(`📡 URL: ${chalk.blue(`http://localhost:${this.port}`)}`);
|
|
360
|
+
console.log(chalk.green('──────────────────────────────────────────────────\n'));
|
|
361
|
+
|
|
362
|
+
resolve();
|
|
363
|
+
});
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
tryPort(this.port);
|
|
306
367
|
});
|
|
307
368
|
}
|
|
308
369
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* Container chính cho khu vực Editor */
|
|
2
|
+
.editor-container {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
height: 100%;
|
|
6
|
+
background: var(--ios-bg);
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Thanh Tabs - Layout Flexbox mới */
|
|
11
|
+
.tabs-header {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: space-between; /* Tabs trái, Actions phải */
|
|
15
|
+
background: #252526;
|
|
16
|
+
height: 35px;
|
|
17
|
+
border-bottom: 1px solid #1e1e1e;
|
|
18
|
+
user-select: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Khu vực chứa các Tabs (Scrollable) */
|
|
22
|
+
.tabs-scroll-area {
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
overflow-x: auto;
|
|
26
|
+
flex: 1; /* Chiếm phần lớn không gian */
|
|
27
|
+
height: 100%;
|
|
28
|
+
}
|
|
29
|
+
.tabs-scroll-area::-webkit-scrollbar {
|
|
30
|
+
height: 0px; /* Ẩn scrollbar cho gọn */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Khu vực Actions bên phải */
|
|
34
|
+
.tabs-actions {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
padding: 0 5px;
|
|
38
|
+
background: #252526;
|
|
39
|
+
height: 100%;
|
|
40
|
+
border-left: 1px solid #1e1e1e;
|
|
41
|
+
box-shadow: -5px 0 10px rgba(0,0,0,0.2); /* Tạo bóng đổ nhẹ ngăn cách */
|
|
42
|
+
z-index: 5;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Style cho Tab Item */
|
|
46
|
+
.tab-item {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
padding: 0 10px;
|
|
50
|
+
min-width: fit-content;
|
|
51
|
+
max-width: 200px;
|
|
52
|
+
height: 100%;
|
|
53
|
+
background: #2d2d2d;
|
|
54
|
+
color: #969696;
|
|
55
|
+
border-right: 1px solid #252526;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
transition: background 0.2s;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.tab-item:hover {
|
|
62
|
+
background: #383838;
|
|
63
|
+
color: #e0e0e0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.tab-item.active {
|
|
67
|
+
background: #1e1e1e;
|
|
68
|
+
color: #ffffff;
|
|
69
|
+
border-top: 1px solid var(--ios-blue);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.tab-icon {
|
|
73
|
+
margin-right: 6px;
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.tab-name {
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
max-width: 150px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.tab-close {
|
|
85
|
+
margin-left: 8px;
|
|
86
|
+
border-radius: 3px;
|
|
87
|
+
width: 16px;
|
|
88
|
+
height: 16px;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
opacity: 0;
|
|
93
|
+
font-size: 11px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.tab-item:hover .tab-close,
|
|
97
|
+
.tab-item.active .tab-close {
|
|
98
|
+
opacity: 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.tab-close:hover {
|
|
102
|
+
background: rgba(255, 255, 255, 0.2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Style đặc biệt cho Select Box trong Tab */
|
|
106
|
+
.ai-provider-select {
|
|
107
|
+
background: transparent;
|
|
108
|
+
border: none;
|
|
109
|
+
color: inherit;
|
|
110
|
+
font-family: inherit;
|
|
111
|
+
font-size: 13px;
|
|
112
|
+
font-weight: 500;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
outline: none;
|
|
115
|
+
padding: 0;
|
|
116
|
+
margin: 0;
|
|
117
|
+
max-width: 120px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ai-provider-select option {
|
|
121
|
+
background: #252526;
|
|
122
|
+
color: white;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Action Buttons ở góc phải */
|
|
126
|
+
.action-btn-mini {
|
|
127
|
+
background: transparent;
|
|
128
|
+
border: none;
|
|
129
|
+
color: #cccccc;
|
|
130
|
+
width: 28px;
|
|
131
|
+
height: 28px;
|
|
132
|
+
border-radius: 4px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
font-size: 14px;
|
|
138
|
+
margin-left: 2px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.action-btn-mini:hover {
|
|
142
|
+
background: rgba(255,255,255,0.1);
|
|
143
|
+
color: white;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.action-btn-mini.active {
|
|
147
|
+
background: rgba(0, 122, 255, 0.2);
|
|
148
|
+
color: var(--ios-blue);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Khu vực nội dung Code */
|
|
152
|
+
.editor-body {
|
|
153
|
+
flex: 1;
|
|
154
|
+
position: relative;
|
|
155
|
+
background: #1e1e1e;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.view-mode-hidden {
|
|
160
|
+
display: none !important;
|
|
161
|
+
}
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
.git-view-container {
|
|
46
46
|
position: absolute;
|
|
47
|
-
top:
|
|
47
|
+
top: 35px;
|
|
48
48
|
left: 0;
|
|
49
49
|
right: 0;
|
|
50
50
|
bottom: 0;
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
|
|
194
194
|
.git-indent-guide {
|
|
195
195
|
display: inline-block;
|
|
196
|
-
width: 16px;
|
|
196
|
+
width: 16px;
|
|
197
197
|
height: 100%;
|
|
198
198
|
flex-shrink: 0;
|
|
199
199
|
}
|
|
@@ -208,11 +208,11 @@
|
|
|
208
208
|
flex-shrink: 0;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
.git-tree-node.collapsed
|
|
211
|
+
.git-tree-node.collapsed>.git-tree-content>.git-arrow {
|
|
212
212
|
transform: rotate(-90deg);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
.git-tree-node.collapsed
|
|
215
|
+
.git-tree-node.collapsed>ul {
|
|
216
216
|
display: none;
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -226,10 +226,23 @@
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/* Status Colors */
|
|
229
|
-
.git-status-M {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
.git-status-M {
|
|
230
|
+
color: #d29922;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.git-status-A,
|
|
234
|
+
.git-status-U {
|
|
235
|
+
color: #3fb950;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.git-status-D {
|
|
239
|
+
color: #f85149;
|
|
240
|
+
text-decoration: line-through;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.git-status-R {
|
|
244
|
+
color: #d29922;
|
|
245
|
+
}
|
|
233
246
|
|
|
234
247
|
.git-label {
|
|
235
248
|
flex: 1;
|
|
@@ -245,6 +258,7 @@
|
|
|
245
258
|
.git-file-label {
|
|
246
259
|
color: #8b949e;
|
|
247
260
|
}
|
|
261
|
+
|
|
248
262
|
.git-tree-content:hover .git-file-label,
|
|
249
263
|
.git-tree-content.selected .git-file-label {
|
|
250
264
|
color: #e6edf3;
|
|
@@ -304,20 +318,41 @@
|
|
|
304
318
|
}
|
|
305
319
|
|
|
306
320
|
/* Diff2Html Overrides */
|
|
307
|
-
.d2h-wrapper * {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
.d2h-
|
|
321
|
+
.d2h-wrapper * {
|
|
322
|
+
box-sizing: border-box;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.d2h-file-list-wrapper {
|
|
326
|
+
display: none;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.d2h-wrapper {
|
|
330
|
+
margin: 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.d2h-file-header {
|
|
334
|
+
display: none;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.d2h-code-line-ctn,
|
|
338
|
+
.d2h-code-line,
|
|
339
|
+
.hljs {
|
|
312
340
|
color: #e6edf3 !important;
|
|
313
341
|
background: transparent !important;
|
|
314
342
|
font-family: Menlo, Monaco, Consolas, monospace;
|
|
315
343
|
font-size: 12px;
|
|
316
344
|
}
|
|
345
|
+
|
|
317
346
|
.d2h-code-side-linenumber {
|
|
318
347
|
background: #0d1117 !important;
|
|
319
348
|
border-color: #30363d !important;
|
|
320
349
|
color: #6e7681 !important;
|
|
321
350
|
}
|
|
322
|
-
|
|
323
|
-
.d2h-
|
|
351
|
+
|
|
352
|
+
.d2h-ins {
|
|
353
|
+
background-color: rgba(46, 160, 67, 0.15) !important;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.d2h-del {
|
|
357
|
+
background-color: rgba(248, 81, 73, 0.15) !important;
|
|
358
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.monaco-container {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
background: #1e1e1e; /* Màu nền mặc định của VS Code Dark */
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* Ẩn hiện container */
|
|
8
|
+
.view-mode-hidden {
|
|
9
|
+
display: none !important;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Fix z-index để context menu của Monaco đè lên được các thành phần khác */
|
|
13
|
+
.monaco-editor .context-view.widget {
|
|
14
|
+
z-index: 10000 !important;
|
|
15
|
+
}
|
|
@@ -75,18 +75,40 @@ body {
|
|
|
75
75
|
.header {
|
|
76
76
|
display: flex;
|
|
77
77
|
justify-content: space-between;
|
|
78
|
+
align-items: center; /* Align items vertically center */
|
|
78
79
|
margin-bottom: 15px;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
.header
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
.header-content {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 12px;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
.status {
|
|
88
|
-
font-size:
|
|
89
|
+
font-size: 14px;
|
|
89
90
|
color: var(--ios-green);
|
|
91
|
+
line-height: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* NEW: Project Info Styles */
|
|
95
|
+
.project-info {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
line-height: 1.2;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.project-name {
|
|
103
|
+
font-weight: 700;
|
|
104
|
+
font-size: 16px;
|
|
105
|
+
color: var(--text-primary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.project-meta {
|
|
109
|
+
font-size: 11px;
|
|
110
|
+
color: var(--text-secondary);
|
|
111
|
+
font-family: monospace;
|
|
90
112
|
}
|
|
91
113
|
|
|
92
114
|
.theme-toggle {
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
<link rel="stylesheet" href="/css/iframe.css">
|
|
12
12
|
<link rel="stylesheet" href="/css/git-view.css">
|
|
13
13
|
<link rel="stylesheet" href="/css/terminal.css">
|
|
14
|
+
<link rel="stylesheet" href="/css/editor.css">
|
|
15
|
+
<link rel="stylesheet" href="/css/monaco.css">
|
|
14
16
|
|
|
15
17
|
<!-- Syntax Highlighting -->
|
|
16
18
|
<link rel="stylesheet"
|
|
17
19
|
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
18
20
|
|
|
19
|
-
<!-- Git Diff -->
|
|
20
|
-
<link rel="stylesheet"
|
|
21
|
-
href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
|
|
21
|
+
<!-- Git Diff CSS (JSDelivr Bundle) -->
|
|
22
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/css/diff2html.min.css" />
|
|
22
23
|
|
|
23
24
|
<!-- Terminal Dependencies -->
|
|
24
25
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
|
@@ -26,8 +27,25 @@
|
|
|
26
27
|
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
|
|
27
28
|
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
|
|
28
29
|
|
|
29
|
-
<!--
|
|
30
|
-
<script
|
|
30
|
+
<!-- Monaco Editor Loader -->
|
|
31
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
|
|
32
|
+
|
|
33
|
+
<!-- FIX: AMD Loader Conflict Patch -->
|
|
34
|
+
<script>
|
|
35
|
+
var __amdDefine = window.define;
|
|
36
|
+
var __amdRequire = window.require;
|
|
37
|
+
window.define = undefined;
|
|
38
|
+
window.require = undefined;
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<!-- Git Diff JS (JSDelivr Bundle) -->
|
|
42
|
+
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.47/bundles/js/diff2html-ui.min.js"></script>
|
|
43
|
+
|
|
44
|
+
<!-- FIX: Restore AMD Loader -->
|
|
45
|
+
<script>
|
|
46
|
+
window.define = __amdDefine;
|
|
47
|
+
window.require = __amdRequire;
|
|
48
|
+
</script>
|
|
31
49
|
|
|
32
50
|
<script>
|
|
33
51
|
(function () {
|
|
@@ -44,8 +62,14 @@
|
|
|
44
62
|
<div class="container">
|
|
45
63
|
<div class="header">
|
|
46
64
|
<div class="header-content">
|
|
47
|
-
|
|
48
|
-
<
|
|
65
|
+
<!-- Status Dot -->
|
|
66
|
+
<span class="status" id="status" style="font-size: 14px;">●</span>
|
|
67
|
+
|
|
68
|
+
<!-- NEW: Project Info Section -->
|
|
69
|
+
<div class="project-info">
|
|
70
|
+
<div class="project-name" id="project-name">Loading...</div>
|
|
71
|
+
<div class="project-meta" id="project-meta">...</div>
|
|
72
|
+
</div>
|
|
49
73
|
</div>
|
|
50
74
|
<button class="theme-toggle" id="theme-toggle" title="Toggle Dark Mode">
|
|
51
75
|
<span id="theme-icon">🌙</span>
|
|
@@ -163,73 +187,91 @@
|
|
|
163
187
|
</div>
|
|
164
188
|
</div>
|
|
165
189
|
|
|
166
|
-
<!-- CỘT PHẢI: AI Iframe -->
|
|
190
|
+
<!-- CỘT PHẢI: AI Iframe & Editor -->
|
|
167
191
|
<div class="right-panel">
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<!-- Options filled by JS -->
|
|
173
|
-
</select>
|
|
174
|
-
</div>
|
|
192
|
+
<!-- HEADER MỚI: Tích hợp Tabs & Actions -->
|
|
193
|
+
<div id="tabs-header" class="tabs-header">
|
|
194
|
+
<!-- Vùng Tabs Scrollable -->
|
|
195
|
+
<div class="tabs-scroll-area">
|
|
175
196
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
<!-- AI Tab (STATIC) -->
|
|
198
|
+
<div class="tab-item active" id="ai-tab" onclick="window.switchTab('ai-assistant')"
|
|
199
|
+
data-path="ai-assistant">
|
|
200
|
+
<span class="tab-icon">🤖</span>
|
|
201
|
+
<!-- Select Box nằm ngay trong tên Tab -->
|
|
202
|
+
<select id="ai-provider-select" class="ai-provider-select" onclick="event.stopPropagation()">
|
|
203
|
+
<!-- Options filled by JS -->
|
|
204
|
+
</select>
|
|
205
|
+
</div>
|
|
179
206
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
207
|
+
<!-- Dynamic File Tabs -->
|
|
208
|
+
<div id="file-tabs-container" style="display:flex; height:100%;"></div>
|
|
209
|
+
</div>
|
|
183
210
|
|
|
184
|
-
|
|
211
|
+
<!-- Global Actions -->
|
|
212
|
+
<div class="tabs-actions">
|
|
213
|
+
<button id="guide-toggle-btn" class="action-btn-mini" title="Show Installation Guide">🧩</button>
|
|
214
|
+
<button id="git-refresh-btn" class="action-btn-mini" style="display:none;"
|
|
215
|
+
title="Refresh Git">↻</button>
|
|
216
|
+
<button id="git-view-toggle" class="action-btn-mini" title="Toggle Git View">
|
|
217
|
+
<span style="font-size:12px">Git</span>
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
185
220
|
</div>
|
|
186
221
|
|
|
187
222
|
<!-- Git View Container -->
|
|
188
223
|
<div id="git-view-container" class="git-view-container"></div>
|
|
189
224
|
|
|
190
|
-
<!--
|
|
191
|
-
<div class="
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
<
|
|
225
|
+
<!-- Editor Container -->
|
|
226
|
+
<div class="editor-container" style="flex: 1; position: relative; overflow: hidden;">
|
|
227
|
+
|
|
228
|
+
<!-- AI Iframe -->
|
|
229
|
+
<div class="ai-iframe-container">
|
|
230
|
+
<div id="iframe-placeholder" class="iframe-placeholder hidden">
|
|
231
|
+
<div class="extension-guide-center">
|
|
232
|
+
<button class="guide-close-btn" id="guide-close-btn" title="Close Guide">×</button>
|
|
233
|
+
<span class="guide-icon">🧩</span>
|
|
234
|
+
<h3 class="guide-title">Cài đặt VG Coder Extension</h3>
|
|
235
|
+
<p class="guide-desc">
|
|
236
|
+
Không thấy trang web? AI Provider chặn hiển thị trong Iframe.<br>
|
|
237
|
+
Hãy cài đặt Extension để bỏ qua các giới hạn này.
|
|
238
|
+
</p>
|
|
239
|
+
<ol class="guide-steps">
|
|
240
|
+
<li class="guide-step">
|
|
241
|
+
<span class="step-number">1</span>
|
|
242
|
+
Copy link và dán vào tab mới:
|
|
243
|
+
<div class="url-box">
|
|
244
|
+
<input type="text" id="chrome-url-input-center" class="guide-input" readonly
|
|
245
|
+
value="chrome://extensions" onclick="this.select()">
|
|
246
|
+
<button class="guide-btn-copy" onclick="copyChromeUrl(event)">Copy</button>
|
|
247
|
+
</div>
|
|
248
|
+
</li>
|
|
249
|
+
<li class="guide-step">
|
|
250
|
+
<span class="step-number">2</span>
|
|
251
|
+
Bật <b>Developer mode</b> (Góc phải trên) → <b>Load unpacked</b>
|
|
252
|
+
</li>
|
|
253
|
+
<li class="guide-step">
|
|
254
|
+
<span class="step-number">3</span>
|
|
255
|
+
Chọn thư mục bên dưới:
|
|
256
|
+
<div class="path-box">
|
|
257
|
+
<input type="text" id="extension-path-input-center" class="guide-input" readonly
|
|
258
|
+
value="Loading..." onclick="this.select()">
|
|
259
|
+
<button class="guide-btn-copy" onclick="copyExtensionPath(event)">Copy</button>
|
|
260
|
+
</div>
|
|
261
|
+
</li>
|
|
262
|
+
</ol>
|
|
263
|
+
<div class="guide-footer">
|
|
264
|
+
<a id="ai-placeholder-link" href="#" target="_blank" class="link-fallback">Mở tab mới
|
|
265
|
+
↗</a>
|
|
266
|
+
<button id="guide-done-btn" class="btn-primary-guide">Đã xong / Tải lại</button>
|
|
267
|
+
</div>
|
|
229
268
|
</div>
|
|
230
269
|
</div>
|
|
270
|
+
<iframe id="ai-iframe" src="" title="AI Integration"></iframe>
|
|
231
271
|
</div>
|
|
232
|
-
|
|
272
|
+
|
|
273
|
+
<!-- Monaco Editor Container -->
|
|
274
|
+
<div id="monaco-container" class="monaco-container view-mode-hidden"></div>
|
|
233
275
|
</div>
|
|
234
276
|
</div>
|
|
235
277
|
</div>
|
|
@@ -239,4 +281,4 @@
|
|
|
239
281
|
<script type="module" src="/js/main.js"></script>
|
|
240
282
|
</body>
|
|
241
283
|
|
|
242
|
-
</html>
|
|
284
|
+
</html>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { openFileInMonaco, saveViewState, disposeModel } from './monaco-manager.js';
|
|
2
|
+
|
|
3
|
+
let activeTabs = []; // Array of { path, name, icon }
|
|
4
|
+
let currentPath = 'ai-assistant'; // Default
|
|
5
|
+
|
|
6
|
+
export function initEditorTabs() {
|
|
7
|
+
renderTabs();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function openFileTab(path, name, icon = '📄') {
|
|
11
|
+
// 1. Ẩn AI, hiện Monaco
|
|
12
|
+
toggleViewMode('code');
|
|
13
|
+
|
|
14
|
+
// 2. Lưu state tab cũ
|
|
15
|
+
if (currentPath && currentPath !== 'ai-assistant' && currentPath !== path) {
|
|
16
|
+
saveViewState(currentPath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 3. Logic Tabs Array
|
|
20
|
+
const existingTab = activeTabs.find(t => t.path === path);
|
|
21
|
+
if (!existingTab) {
|
|
22
|
+
activeTabs.push({ path, name, icon });
|
|
23
|
+
renderTabs();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 4. Update UI
|
|
27
|
+
currentPath = path;
|
|
28
|
+
updateTabUI();
|
|
29
|
+
|
|
30
|
+
// 5. Open in Monaco
|
|
31
|
+
await openFileInMonaco(path);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function switchTab(path) {
|
|
35
|
+
if (currentPath && currentPath !== 'ai-assistant') {
|
|
36
|
+
saveViewState(currentPath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
currentPath = path;
|
|
40
|
+
updateTabUI();
|
|
41
|
+
|
|
42
|
+
if (path === 'ai-assistant') {
|
|
43
|
+
toggleViewMode('ai');
|
|
44
|
+
} else {
|
|
45
|
+
toggleViewMode('code');
|
|
46
|
+
openFileInMonaco(path);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function closeTab(event, path) {
|
|
51
|
+
event.stopPropagation();
|
|
52
|
+
|
|
53
|
+
const index = activeTabs.findIndex(t => t.path === path);
|
|
54
|
+
if (index === -1) return;
|
|
55
|
+
|
|
56
|
+
activeTabs.splice(index, 1);
|
|
57
|
+
disposeModel(path);
|
|
58
|
+
|
|
59
|
+
if (currentPath === path) {
|
|
60
|
+
if (activeTabs.length > 0) {
|
|
61
|
+
const nextTab = activeTabs[activeTabs.length - 1];
|
|
62
|
+
switchTab(nextTab.path);
|
|
63
|
+
} else {
|
|
64
|
+
switchTab('ai-assistant');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
renderTabs();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderTabs() {
|
|
71
|
+
// Chỉ render các file tabs vào container con
|
|
72
|
+
const container = document.getElementById('file-tabs-container');
|
|
73
|
+
if (!container) return;
|
|
74
|
+
|
|
75
|
+
let html = '';
|
|
76
|
+
activeTabs.forEach(tab => {
|
|
77
|
+
html += `
|
|
78
|
+
<div class="tab-item"
|
|
79
|
+
onclick="window.switchTab('${tab.path}')"
|
|
80
|
+
data-path="${tab.path}"
|
|
81
|
+
title="${tab.path}">
|
|
82
|
+
<span class="tab-icon">${tab.icon}</span>
|
|
83
|
+
<span class="tab-name">${tab.name}</span>
|
|
84
|
+
<span class="tab-close" onclick="window.closeTab(event, '${tab.path}')">×</span>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
container.innerHTML = html;
|
|
90
|
+
updateTabUI();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function updateTabUI() {
|
|
94
|
+
// 1. Update Static AI Tab
|
|
95
|
+
const aiTab = document.getElementById('ai-tab');
|
|
96
|
+
if (currentPath === 'ai-assistant') aiTab.classList.add('active');
|
|
97
|
+
else aiTab.classList.remove('active');
|
|
98
|
+
|
|
99
|
+
// 2. Update Dynamic Tabs
|
|
100
|
+
const fileTabs = document.querySelectorAll('#file-tabs-container .tab-item');
|
|
101
|
+
fileTabs.forEach(el => {
|
|
102
|
+
if (el.dataset.path === currentPath) el.classList.add('active');
|
|
103
|
+
else el.classList.remove('active');
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toggleViewMode(mode) {
|
|
108
|
+
const aiContainer = document.querySelector('.ai-iframe-container');
|
|
109
|
+
const monacoContainer = document.getElementById('monaco-container');
|
|
110
|
+
|
|
111
|
+
if (mode === 'code') {
|
|
112
|
+
aiContainer.classList.add('view-mode-hidden');
|
|
113
|
+
monacoContainer.classList.remove('view-mode-hidden');
|
|
114
|
+
} else {
|
|
115
|
+
aiContainer.classList.remove('view-mode-hidden');
|
|
116
|
+
monacoContainer.classList.add('view-mode-hidden');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Export global helpers
|
|
121
|
+
window.switchTab = switchTab;
|
|
122
|
+
window.closeTab = closeTab;
|
|
123
|
+
window.openFileTab = openFileTab;
|
|
@@ -344,6 +344,15 @@ async function loadDiffView(filePath, type) {
|
|
|
344
344
|
const viewer = document.getElementById('git-diff-viewer');
|
|
345
345
|
viewer.innerHTML = '<div class="git-empty-state">Loading diff...</div>';
|
|
346
346
|
|
|
347
|
+
// SAFE CHECK FOR UI LIBRARY
|
|
348
|
+
// We check window.Diff2HtmlUI first (standard for bundles)
|
|
349
|
+
const UIConstructor = window.Diff2HtmlUI;
|
|
350
|
+
|
|
351
|
+
if (!UIConstructor) {
|
|
352
|
+
viewer.innerHTML = '<div class="git-empty-state" style="color:#f85149">Error: Diff2HtmlUI library not loaded correctly.<br>Please check your internet connection or CDN availability.</div>';
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
347
356
|
try {
|
|
348
357
|
const diff = await getGitDiff(filePath, type === 'staged' ? 'staged' : 'working');
|
|
349
358
|
|
|
@@ -353,7 +362,7 @@ async function loadDiffView(filePath, type) {
|
|
|
353
362
|
}
|
|
354
363
|
|
|
355
364
|
viewer.innerHTML = '';
|
|
356
|
-
const ui = new
|
|
365
|
+
const ui = new UIConstructor(viewer, diff, {
|
|
357
366
|
drawFileList: false,
|
|
358
367
|
matching: 'lines',
|
|
359
368
|
outputFormat: 'side-by-side',
|
|
@@ -3,13 +3,14 @@ const AI_PROVIDERS = [
|
|
|
3
3
|
{ id: 'kimi', name: 'Kimi AI', url: 'https://www.kimi.com' },
|
|
4
4
|
{ id: 'deepseek', name: 'DeepSeek', url: 'https://chat.deepseek.com' },
|
|
5
5
|
{ id: 'gemini', name: 'Google Gemini', url: 'https://gemini.google.com/app' },
|
|
6
|
-
{ id: 'aistudio', name: 'Google AI Studio', url: 'https://aistudio.google.com/prompts/new_chat' }
|
|
6
|
+
{ id: 'aistudio', name: 'Google AI Studio', url: 'https://aistudio.google.com/prompts/new_chat' },
|
|
7
|
+
{ id: 'gork', name: 'Gork', url: 'https://grok.com' },
|
|
7
8
|
];
|
|
8
9
|
|
|
9
10
|
export function initIframeManager() {
|
|
10
11
|
const select = document.getElementById('ai-provider-select');
|
|
11
12
|
const iframe = document.getElementById('ai-iframe');
|
|
12
|
-
|
|
13
|
+
// Removed externalLink reference
|
|
13
14
|
const placeholderLink = document.getElementById('ai-placeholder-link');
|
|
14
15
|
|
|
15
16
|
// Guide elements
|
|
@@ -42,9 +43,11 @@ export function initIframeManager() {
|
|
|
42
43
|
iframe.src = provider.url;
|
|
43
44
|
}, 50);
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
placeholderLink
|
|
47
|
-
|
|
46
|
+
// Update placeholder link only (external link removed from UI)
|
|
47
|
+
if (placeholderLink) {
|
|
48
|
+
placeholderLink.href = provider.url;
|
|
49
|
+
placeholderLink.textContent = `Mở ${provider.name} tab mới ↗`;
|
|
50
|
+
}
|
|
48
51
|
|
|
49
52
|
localStorage.setItem('ai_provider', providerId);
|
|
50
53
|
};
|
|
@@ -60,11 +63,11 @@ export function initIframeManager() {
|
|
|
60
63
|
// --- GUIDE TOGGLE LOGIC ---
|
|
61
64
|
|
|
62
65
|
const showGuide = () => {
|
|
63
|
-
guideContainer.classList.remove('hidden');
|
|
66
|
+
if (guideContainer) guideContainer.classList.remove('hidden');
|
|
64
67
|
};
|
|
65
68
|
|
|
66
69
|
const hideGuide = () => {
|
|
67
|
-
guideContainer.classList.add('hidden');
|
|
70
|
+
if (guideContainer) guideContainer.classList.add('hidden');
|
|
68
71
|
};
|
|
69
72
|
|
|
70
73
|
const reloadIframe = () => {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { API_BASE } from '../config.js';
|
|
2
|
+
import { showToast } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
let editor = null;
|
|
5
|
+
let models = new Map(); // Map<filePath, { model: ITextModel, viewState: object }>
|
|
6
|
+
let isMonacoLoaded = false;
|
|
7
|
+
|
|
8
|
+
// Cấu hình đường dẫn cho AMD Loader của Monaco
|
|
9
|
+
export function initMonaco() {
|
|
10
|
+
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Đảm bảo Editor đã được khởi tạo
|
|
15
|
+
*/
|
|
16
|
+
async function ensureEditor() {
|
|
17
|
+
if (editor) return editor;
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
require(['vs/editor/editor.main'], function () {
|
|
21
|
+
const container = document.getElementById('monaco-container');
|
|
22
|
+
|
|
23
|
+
// Xác định theme dựa trên theme hiện tại của web
|
|
24
|
+
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs';
|
|
25
|
+
|
|
26
|
+
editor = monaco.editor.create(container, {
|
|
27
|
+
value: '',
|
|
28
|
+
language: 'plaintext',
|
|
29
|
+
theme: currentTheme,
|
|
30
|
+
automaticLayout: true, // Tự động resize
|
|
31
|
+
minimap: { enabled: true },
|
|
32
|
+
fontSize: 13,
|
|
33
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
34
|
+
scrollBeyondLastLine: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Lắng nghe phím tắt Ctrl+S / Cmd+S để lưu
|
|
38
|
+
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
|
|
39
|
+
saveCurrentFile();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
isMonacoLoaded = true;
|
|
43
|
+
resolve(editor);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mở file vào Editor
|
|
50
|
+
*/
|
|
51
|
+
export async function openFileInMonaco(path) {
|
|
52
|
+
const editorInstance = await ensureEditor();
|
|
53
|
+
|
|
54
|
+
// 1. Nếu Model đã tồn tại trong bộ nhớ -> Dùng lại
|
|
55
|
+
if (models.has(path)) {
|
|
56
|
+
const stored = models.get(path);
|
|
57
|
+
editorInstance.setModel(stored.model);
|
|
58
|
+
if (stored.viewState) {
|
|
59
|
+
editorInstance.restoreViewState(stored.viewState);
|
|
60
|
+
}
|
|
61
|
+
editorInstance.focus();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Nếu chưa -> Fetch nội dung từ Server
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${API_BASE}/api/read-file?path=${encodeURIComponent(path)}`);
|
|
68
|
+
const data = await res.json();
|
|
69
|
+
|
|
70
|
+
if (res.ok) {
|
|
71
|
+
// Xác định ngôn ngữ từ extension
|
|
72
|
+
const language = getLanguageFromPath(path);
|
|
73
|
+
|
|
74
|
+
// Tạo Model mới
|
|
75
|
+
const newModel = monaco.editor.createModel(data.content, language, monaco.Uri.file(path));
|
|
76
|
+
|
|
77
|
+
// Lưu vào cache
|
|
78
|
+
models.set(path, { model: newModel, viewState: null });
|
|
79
|
+
|
|
80
|
+
// Gán vào Editor
|
|
81
|
+
editorInstance.setModel(newModel);
|
|
82
|
+
} else {
|
|
83
|
+
showToast(`Error opening file: ${data.error}`, 'error');
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
showToast(`Failed to load file: ${err.message}`, 'error');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Lưu trạng thái View (Scroll, Cursor) trước khi chuyển tab
|
|
92
|
+
*/
|
|
93
|
+
export function saveViewState(path) {
|
|
94
|
+
if (editor && models.has(path)) {
|
|
95
|
+
const viewState = editor.saveViewState();
|
|
96
|
+
const stored = models.get(path);
|
|
97
|
+
stored.viewState = viewState;
|
|
98
|
+
models.set(path, stored);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Giải phóng Model khi đóng Tab
|
|
104
|
+
*/
|
|
105
|
+
export function disposeModel(path) {
|
|
106
|
+
if (models.has(path)) {
|
|
107
|
+
const stored = models.get(path);
|
|
108
|
+
stored.model.dispose(); // Quan trọng: Tránh memory leak
|
|
109
|
+
models.delete(path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Cập nhật Theme cho Monaco khi web đổi theme
|
|
115
|
+
*/
|
|
116
|
+
export function updateMonacoTheme(theme) {
|
|
117
|
+
if (editor) {
|
|
118
|
+
monaco.editor.setTheme(theme === 'dark' ? 'vs-dark' : 'vs');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Lưu file hiện tại
|
|
124
|
+
*/
|
|
125
|
+
async function saveCurrentFile() {
|
|
126
|
+
const model = editor.getModel();
|
|
127
|
+
if (!model) return;
|
|
128
|
+
|
|
129
|
+
const content = model.getValue();
|
|
130
|
+
const filePath = model.uri.fsPath; // Lấy path từ URI
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(`${API_BASE}/api/save-file`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ path: filePath, content })
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (res.ok) {
|
|
140
|
+
showToast('File saved successfully!', 'success');
|
|
141
|
+
} else {
|
|
142
|
+
showToast('Failed to save file', 'error');
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
showToast('Error saving file: ' + err.message, 'error');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Helper: Map extension to Monaco Language
|
|
150
|
+
function getLanguageFromPath(path) {
|
|
151
|
+
const ext = path.split('.').pop().toLowerCase();
|
|
152
|
+
const map = {
|
|
153
|
+
js: 'javascript', ts: 'typescript', html: 'html', css: 'css',
|
|
154
|
+
json: 'json', md: 'markdown', py: 'python', java: 'java',
|
|
155
|
+
sh: 'shell', xml: 'xml', sql: 'sql'
|
|
156
|
+
};
|
|
157
|
+
return map[ext] || 'plaintext';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { editor };
|
|
@@ -183,15 +183,29 @@ function generateTreeHtml(node) {
|
|
|
183
183
|
const arrow = hasChildren ? '▼' : '';
|
|
184
184
|
const liClass = `tree-li ${hasChildren ? 'has-children' : ''}`;
|
|
185
185
|
|
|
186
|
+
// Actions
|
|
187
|
+
// On click: Toggle if folder, Open Tab if file
|
|
188
|
+
let clickAction = '';
|
|
189
|
+
let cursorStyle = '';
|
|
190
|
+
|
|
191
|
+
if (isDir) {
|
|
192
|
+
if (hasChildren) {
|
|
193
|
+
clickAction = 'onclick="toggleFolder(event)"';
|
|
194
|
+
cursorStyle = 'cursor: pointer;';
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// File click -> Open in Editor
|
|
198
|
+
// Escape backslashes for Windows paths
|
|
199
|
+
const safePath = (node.relativePath || node.path).replace(/\\/g, '\\\\');
|
|
200
|
+
clickAction = `onclick="window.openFileTab('${safePath}', '${node.name}')"`;
|
|
201
|
+
cursorStyle = 'cursor: pointer; color: var(--text-primary);';
|
|
202
|
+
}
|
|
203
|
+
|
|
186
204
|
// Build HTML
|
|
187
205
|
let html = `<li class="${liClass}">`;
|
|
188
206
|
|
|
189
|
-
const clickAttr = hasChildren ? 'onclick="toggleFolder(event)"' : '';
|
|
190
|
-
|
|
191
|
-
// Add data-tokens and data-type for client-side calculation
|
|
192
|
-
|
|
193
207
|
html += `
|
|
194
|
-
<div class="tree-item-row" ${
|
|
208
|
+
<div class="tree-item-row" ${isDir ? clickAction : ''}>
|
|
195
209
|
<span class="arrow">${arrow}</span>
|
|
196
210
|
<input type="checkbox" class="tree-checkbox"
|
|
197
211
|
data-path="${node.relativePath || node.path}"
|
|
@@ -200,7 +214,7 @@ function generateTreeHtml(node) {
|
|
|
200
214
|
checked
|
|
201
215
|
onclick="handleCheckboxChange(event)">
|
|
202
216
|
<span class="tree-icon">${icon}</span>
|
|
203
|
-
<span class="tree-name">${node.name}</span>
|
|
217
|
+
<span class="tree-name" style="${cursorStyle}" ${!isDir ? clickAction : ''}>${node.name}</span>
|
|
204
218
|
<span class="token-badge ${tokenClass}">${formatNumber(tokens)}</span>
|
|
205
219
|
</div>
|
|
206
220
|
`;
|
|
@@ -6,6 +6,8 @@ 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
8
|
import { initTerminal, createNewTerminal } from './features/terminal.js';
|
|
9
|
+
import { initEditorTabs } from './features/editor-tabs.js';
|
|
10
|
+
import { initMonaco, updateMonacoTheme } from './features/monaco-manager.js';
|
|
9
11
|
|
|
10
12
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
11
13
|
// Load system prompt text
|
|
@@ -14,6 +16,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
14
16
|
// Check server status
|
|
15
17
|
await checkServerStatus();
|
|
16
18
|
|
|
19
|
+
// Load Project Info
|
|
20
|
+
await loadProjectInfo();
|
|
21
|
+
|
|
17
22
|
// Initialize Theme
|
|
18
23
|
initTheme();
|
|
19
24
|
|
|
@@ -28,6 +33,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
28
33
|
|
|
29
34
|
// Initialize Terminal System
|
|
30
35
|
initTerminal();
|
|
36
|
+
|
|
37
|
+
// Initialize Monaco & Tabs
|
|
38
|
+
initMonaco();
|
|
39
|
+
initEditorTabs();
|
|
40
|
+
|
|
41
|
+
// Set default tab to AI Assistant
|
|
42
|
+
if (window.switchTab) {
|
|
43
|
+
window.switchTab('ai-assistant');
|
|
44
|
+
}
|
|
31
45
|
});
|
|
32
46
|
|
|
33
47
|
async function checkServerStatus() {
|
|
@@ -35,16 +49,40 @@ async function checkServerStatus() {
|
|
|
35
49
|
const isHealthy = await checkHealth();
|
|
36
50
|
|
|
37
51
|
if (isHealthy) {
|
|
38
|
-
statusEl.textContent = '●
|
|
39
|
-
statusEl.style.background = '
|
|
52
|
+
statusEl.textContent = '●';
|
|
53
|
+
statusEl.style.background = 'transparent';
|
|
40
54
|
statusEl.style.color = 'var(--ios-green)';
|
|
41
55
|
} else {
|
|
42
|
-
statusEl.textContent = '●
|
|
43
|
-
statusEl.style.background = '
|
|
56
|
+
statusEl.textContent = '●';
|
|
57
|
+
statusEl.style.background = 'transparent';
|
|
44
58
|
statusEl.style.color = 'var(--ios-red)';
|
|
45
59
|
}
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
async function loadProjectInfo() {
|
|
63
|
+
try {
|
|
64
|
+
// Fetch info for current directory (.)
|
|
65
|
+
const res = await fetch('/api/info?path=.');
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
|
|
68
|
+
const projectNameEl = document.getElementById('project-name');
|
|
69
|
+
const projectMetaEl = document.getElementById('project-meta');
|
|
70
|
+
|
|
71
|
+
// Extract folder name from path
|
|
72
|
+
const fullPath = data.path;
|
|
73
|
+
// Handle both Windows (\) and Unix (/) paths
|
|
74
|
+
const folderName = fullPath.split(/[\\/]/).pop();
|
|
75
|
+
|
|
76
|
+
projectNameEl.textContent = folderName;
|
|
77
|
+
projectMetaEl.textContent = `${data.primaryType} • ${fullPath}`;
|
|
78
|
+
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('Failed to load project info:', err);
|
|
81
|
+
document.getElementById('project-name').textContent = 'Unknown Project';
|
|
82
|
+
document.getElementById('project-meta').textContent = 'Error loading info';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
function initTheme() {
|
|
49
87
|
const themeBtn = document.getElementById('theme-toggle');
|
|
50
88
|
let currentTheme = localStorage.getItem('theme') || 'light';
|
|
@@ -56,6 +94,7 @@ function initTheme() {
|
|
|
56
94
|
localStorage.setItem('theme', newTheme);
|
|
57
95
|
currentTheme = newTheme;
|
|
58
96
|
updateThemeIcon(newTheme);
|
|
97
|
+
updateMonacoTheme(newTheme);
|
|
59
98
|
});
|
|
60
99
|
}
|
|
61
100
|
|
|
Binary file
|
|
Binary file
|
package/change.sh
DELETED
|
File without changes
|
package/vg-coder-cli-2.0.17.tgz
DELETED
|
Binary file
|
package/vg-coder-cli-2.0.19.tgz
DELETED
|
Binary file
|