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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "2.0.19",
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 port = parseInt(options.port);
376
- const server = new ApiServer(port);
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
 
@@ -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 (NEW)
206
+ // Git Discard
162
207
  this.app.post('/api/git/discard', async (req, res) => {
163
208
  try {
164
- const { files } = req.body; // array or '*'
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
- // --- GIT API END ---
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
- this.server = this.httpServer.listen(this.port, () => {
303
- console.log(chalk.green(`\n🚀 VG Coder API Server & Socket.IO started on port ${this.port}`));
304
- resolve();
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: 50px;
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 > .git-tree-content > .git-arrow {
211
+ .git-tree-node.collapsed>.git-tree-content>.git-arrow {
212
212
  transform: rotate(-90deg);
213
213
  }
214
214
 
215
- .git-tree-node.collapsed > ul {
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 { color: #d29922; }
230
- .git-status-A, .git-status-U { color: #3fb950; }
231
- .git-status-D { color: #f85149; text-decoration: line-through; }
232
- .git-status-R { color: #d29922; }
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 * { box-sizing: border-box; }
308
- .d2h-file-list-wrapper { display: none; }
309
- .d2h-wrapper { margin: 0; }
310
- .d2h-file-header { display: none; }
311
- .d2h-code-line-ctn, .d2h-code-line, .hljs {
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
- .d2h-ins { background-color: rgba(46, 160, 67, 0.15) !important; }
323
- .d2h-del { background-color: rgba(248, 81, 73, 0.15) !important; }
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 h1 {
82
- font-size: 20px;
83
- font-weight: 700;
84
- margin: 0;
82
+ .header-content {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 12px;
85
86
  }
86
87
 
87
88
  .status {
88
- font-size: 11px;
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" type="text/css"
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
- <!-- Other Libs -->
30
- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></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
- <span class="status" id="status">● Server Starting...</span>
48
- <div style="height: 5px;"></div>
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
- <div class="ai-header">
169
- <div class="ai-select-group">
170
- <span style="font-size: 16px;">🤖</span>
171
- <select id="ai-provider-select" class="ai-select">
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
- <!-- NEW: Toggle Guide Button -->
177
- <button id="guide-toggle-btn" class="btn-icon-head" style="margin-left:5px;"
178
- title="Show Installation Guide">🧩</button>
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
- <button id="git-view-toggle" class="git-toggle-btn">View Changes</button>
181
- <button id="git-refresh-btn" class="btn-icon-head" style="display:none; margin-left:5px;"
182
- title="Refresh">↻</button>
207
+ <!-- Dynamic File Tabs -->
208
+ <div id="file-tabs-container" style="display:flex; height:100%;"></div>
209
+ </div>
183
210
 
184
- <a id="ai-external-link" href="#" target="_blank" class="btn-icon-head" title="Open in new tab">↗</a>
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
- <!-- AI Iframe Container -->
191
- <div class="ai-iframe-container">
192
- <!-- Center Extension Guide (Z-Index 10, covers iframe) -->
193
- <div id="iframe-placeholder" class="iframe-placeholder hidden">
194
- <div class="extension-guide-center">
195
- <button class="guide-close-btn" id="guide-close-btn" title="Close Guide">×</button>
196
- <span class="guide-icon">🧩</span>
197
- <h3 class="guide-title">Cài đặt VG Coder Extension</h3>
198
- <p class="guide-desc">
199
- Không thấy trang web? AI Provider chặn hiển thị trong Iframe.<br>
200
- Hãy cài đặt Extension để bỏ qua các giới hạn này.
201
- </p>
202
- <ol class="guide-steps">
203
- <li class="guide-step">
204
- <span class="step-number">1</span>
205
- Copy link và dán vào tab mới:
206
- <div class="url-box">
207
- <input type="text" id="chrome-url-input-center" class="guide-input" readonly
208
- value="chrome://extensions" onclick="this.select()">
209
- <button class="guide-btn-copy" onclick="copyChromeUrl(event)">Copy</button>
210
- </div>
211
- </li>
212
- <li class="guide-step">
213
- <span class="step-number">2</span>
214
- Bật <b>Developer mode</b> (Góc phải trên) &rarr; <b>Load unpacked</b>
215
- </li>
216
- <li class="guide-step">
217
- <span class="step-number">3</span>
218
- Chọn thư mục bên dưới:
219
- <div class="path-box">
220
- <input type="text" id="extension-path-input-center" class="guide-input" readonly
221
- value="Loading..." onclick="this.select()">
222
- <button class="guide-btn-copy" onclick="copyExtensionPath(event)">Copy</button>
223
- </div>
224
- </li>
225
- </ol>
226
- <div class="guide-footer">
227
- <a id="ai-placeholder-link" href="#" target="_blank" class="link-fallback">Mở tab mới ↗</a>
228
- <button id="guide-done-btn" class="btn-primary-guide">Đã xong / Tải lại</button>
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 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) &rarr; <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
- <iframe id="ai-iframe" src="" title="AI Integration"></iframe>
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 Diff2HtmlUI(viewer, diff, {
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
- const externalLink = document.getElementById('ai-external-link');
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
- externalLink.href = provider.url;
46
- placeholderLink.href = provider.url;
47
- placeholderLink.textContent = `Mở ${provider.name} tab mới ↗`;
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" ${clickAttr}>
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 = '● Online';
39
- statusEl.style.background = 'rgba(52, 199, 89, 0.15)';
52
+ statusEl.textContent = '●';
53
+ statusEl.style.background = 'transparent';
40
54
  statusEl.style.color = 'var(--ios-green)';
41
55
  } else {
42
- statusEl.textContent = '● Offline';
43
- statusEl.style.background = 'rgba(255, 59, 48, 0.15)';
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
Binary file
Binary file