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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "2.0.12",
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
- "gitignore",
25
- "html-export",
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",
@@ -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
- // ... (Các endpoint cũ giữ nguyên: analyze, info, structure, clean, execute) ...
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) => { /* Logic cũ... */
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) => { /* Logic cũ... */
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) => { /* Logic cũ... */
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) => { /* Logic cũ... */
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.app.listen(this.port, () => {
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
- <!-- QUAN TRỌNG: Đã đổi sang github-dark.min.css để chữ có màu sáng -->
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
- if (!gitContainer.innerHTML.trim()) {
35
- await loadGitChanges();
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'; // Import to register global functions
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
- if (data.exists) {
94
- input.value = data.path;
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