vg-coder-cli 2.0.11 → 2.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -19
- package/src/server/api-server.js +93 -297
- package/src/server/terminal-manager.js +82 -0
- package/src/server/views/css/git-view.css +155 -0
- package/src/server/views/css/terminal.css +98 -0
- package/src/server/views/dashboard.html +39 -3
- package/src/server/views/js/api.js +15 -0
- package/src/server/views/js/features/git-view.js +117 -0
- package/src/server/views/js/features/terminal.js +174 -0
- package/src/server/views/js/main.js +18 -40
- package/vg-coder-cli-2.0.12.tgz +0 -0
- package/vg-coder-cli-2.0.14.tgz +0 -0
- package/vg-coder-cli-2.0.11.tgz +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* --- LAYOUT HEADER --- */
|
|
2
|
+
.git-toggle-group {
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
margin-left: 10px;
|
|
7
|
+
padding-left: 10px;
|
|
8
|
+
border-left: 1px solid var(--ios-separator);
|
|
9
|
+
flex-shrink: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.git-toggle-btn {
|
|
13
|
+
padding: 0 12px;
|
|
14
|
+
background: transparent;
|
|
15
|
+
border: 1px solid var(--ios-separator);
|
|
16
|
+
border-radius: 6px;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
font-size: 13px;
|
|
19
|
+
font-weight: 500;
|
|
20
|
+
color: var(--text-primary);
|
|
21
|
+
height: 32px;
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.git-toggle-btn.active {
|
|
27
|
+
background: var(--ios-blue);
|
|
28
|
+
color: #fff;
|
|
29
|
+
border-color: var(--ios-blue);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.git-refresh-btn {
|
|
33
|
+
width: 32px;
|
|
34
|
+
height: 32px;
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
border: 1px solid var(--ios-separator);
|
|
37
|
+
background: var(--ios-card);
|
|
38
|
+
color: var(--text-primary);
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* --- CONTAINER CHÍNH --- */
|
|
46
|
+
.right-panel {
|
|
47
|
+
position: relative;
|
|
48
|
+
/* Quan trọng */
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.d2h-file-list {
|
|
52
|
+
position: fixed;
|
|
53
|
+
top: 0;
|
|
54
|
+
left: 0;
|
|
55
|
+
z-index: 99;
|
|
56
|
+
background-color: #0d1117;
|
|
57
|
+
overflow: scroll;
|
|
58
|
+
height: calc(100vh - 46px);
|
|
59
|
+
width: 446px;
|
|
60
|
+
top: 46px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.git-view-container {
|
|
64
|
+
position: absolute;
|
|
65
|
+
top: 50px;
|
|
66
|
+
left: 0;
|
|
67
|
+
right: 0;
|
|
68
|
+
bottom: 0;
|
|
69
|
+
z-index: 9999;
|
|
70
|
+
/* Z-index cao nhất */
|
|
71
|
+
background: #0d1117;
|
|
72
|
+
/* Nền đen GitHub */
|
|
73
|
+
display: none;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
overflow: scroll;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.git-view-container.active {
|
|
79
|
+
display: flex;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* --- DEBUG / SAFETY NET CSS --- */
|
|
83
|
+
/* Ép toàn bộ text trong vùng code phải có màu sáng */
|
|
84
|
+
.d2h-wrapper * {
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Force text color */
|
|
89
|
+
.d2h-code-line-ctn,
|
|
90
|
+
.d2h-code-line,
|
|
91
|
+
.hljs {
|
|
92
|
+
color: #e6edf3 !important;
|
|
93
|
+
/* Trắng xám */
|
|
94
|
+
background: transparent !important;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Force line number color */
|
|
98
|
+
.d2h-code-side-linenumber {
|
|
99
|
+
color: #6e7681 !important;
|
|
100
|
+
background: #0d1117 !important;
|
|
101
|
+
border-color: #30363d !important;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Fix background diff */
|
|
105
|
+
.d2h-ins {
|
|
106
|
+
background-color: rgba(46, 160, 67, 0.15) !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.d2h-del {
|
|
110
|
+
background-color: rgba(248, 81, 73, 0.15) !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Layout */
|
|
114
|
+
.d2h-files-wrapper {
|
|
115
|
+
flex: 1;
|
|
116
|
+
overflow: auto;
|
|
117
|
+
background: #0d1117;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.d2h-file-list-wrapper {
|
|
121
|
+
width: 250px;
|
|
122
|
+
flex-shrink: 0;
|
|
123
|
+
background: #0d1117;
|
|
124
|
+
border-right: 1px solid #30363d;
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.d2h-file-list-header,
|
|
130
|
+
.d2h-file-header {
|
|
131
|
+
background: #161b22 !important;
|
|
132
|
+
border-bottom: 1px solid #30363d !important;
|
|
133
|
+
color: #e6edf3 !important;
|
|
134
|
+
position: sticky;
|
|
135
|
+
top: 0;
|
|
136
|
+
z-index: 10;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.d2h-file-list a {
|
|
140
|
+
color: #8b949e !important;
|
|
141
|
+
text-decoration: none;
|
|
142
|
+
display: block;
|
|
143
|
+
padding: 5px 10px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.d2h-file-list a:hover {
|
|
147
|
+
color: #58a6ff !important;
|
|
148
|
+
background: #161b22;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Ẩn rác */
|
|
152
|
+
.d2h-file-switch,
|
|
153
|
+
.d2h-tag {
|
|
154
|
+
display: none !important;
|
|
155
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Header dùng để drag */
|
|
29
|
+
.terminal-header {
|
|
30
|
+
background: #252526;
|
|
31
|
+
padding: 8px 12px;
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
align-items: center;
|
|
35
|
+
cursor: grab;
|
|
36
|
+
border-bottom: 1px solid #333;
|
|
37
|
+
user-select: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.terminal-header:active {
|
|
41
|
+
cursor: grabbing;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.terminal-title-group {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
color: #ccc;
|
|
49
|
+
font-family: monospace;
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.terminal-controls {
|
|
55
|
+
display: flex;
|
|
56
|
+
gap: 6px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.term-btn {
|
|
60
|
+
width: 12px;
|
|
61
|
+
height: 12px;
|
|
62
|
+
border-radius: 50%;
|
|
63
|
+
border: none;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.term-btn.close {
|
|
68
|
+
background: #FF5F56;
|
|
69
|
+
}
|
|
70
|
+
.term-btn.close:hover {
|
|
71
|
+
background: #ff3b30;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.term-btn.minimize {
|
|
75
|
+
background: #FFBD2E;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.term-btn.maximize {
|
|
79
|
+
background: #27C93F;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Phần nội dung chứa xterm */
|
|
83
|
+
.terminal-body {
|
|
84
|
+
flex: 1;
|
|
85
|
+
position: relative;
|
|
86
|
+
padding: 5px;
|
|
87
|
+
background: #1e1e1e;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Scrollbar customization */
|
|
92
|
+
.xterm-viewport::-webkit-scrollbar {
|
|
93
|
+
width: 8px;
|
|
94
|
+
}
|
|
95
|
+
.xterm-viewport::-webkit-scrollbar-thumb {
|
|
96
|
+
background: #424242;
|
|
97
|
+
border-radius: 4px;
|
|
98
|
+
}
|
|
@@ -9,6 +9,24 @@
|
|
|
9
9
|
<link rel="stylesheet" href="/dashboard.css">
|
|
10
10
|
<link rel="stylesheet" href="/css/structure.css">
|
|
11
11
|
<link rel="stylesheet" href="/css/iframe.css">
|
|
12
|
+
<link rel="stylesheet" href="/css/git-view.css">
|
|
13
|
+
<link rel="stylesheet" href="/css/terminal.css">
|
|
14
|
+
|
|
15
|
+
<!-- Syntax Highlighting -->
|
|
16
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" />
|
|
17
|
+
|
|
18
|
+
<!-- Git Diff -->
|
|
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 -->
|
|
28
|
+
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
|
29
|
+
|
|
12
30
|
<script>
|
|
13
31
|
(function () {
|
|
14
32
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
@@ -31,6 +49,14 @@
|
|
|
31
49
|
<span id="theme-icon">🌙</span>
|
|
32
50
|
</button>
|
|
33
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>
|
|
34
60
|
|
|
35
61
|
<!-- System Prompt Section -->
|
|
36
62
|
<div class="system-prompt-card">
|
|
@@ -96,7 +122,7 @@
|
|
|
96
122
|
</div>
|
|
97
123
|
|
|
98
124
|
<div class="endpoints">
|
|
99
|
-
<!-- Execute Bash -->
|
|
125
|
+
<!-- Execute Bash (RESTORED) -->
|
|
100
126
|
<div class="endpoint-card">
|
|
101
127
|
<div class="endpoint-header">
|
|
102
128
|
<div class="endpoint-title-group">
|
|
@@ -175,7 +201,7 @@
|
|
|
175
201
|
</div>
|
|
176
202
|
</div>
|
|
177
203
|
|
|
178
|
-
<!-- CỘT PHẢI: AI Iframe with Selector -->
|
|
204
|
+
<!-- CỘT PHẢI: AI Iframe with Selector & Git View -->
|
|
179
205
|
<div class="right-panel">
|
|
180
206
|
<div class="ai-header">
|
|
181
207
|
<div class="ai-select-group">
|
|
@@ -184,9 +210,17 @@
|
|
|
184
210
|
<!-- Options filled by JS -->
|
|
185
211
|
</select>
|
|
186
212
|
</div>
|
|
213
|
+
|
|
214
|
+
<button id="git-view-toggle" class="git-toggle-btn">View Changes</button>
|
|
215
|
+
<button id="git-refresh-btn" class="btn-icon-head" style="display:none; margin-left:5px;" title="Refresh">↻</button>
|
|
216
|
+
|
|
187
217
|
<a id="ai-external-link" href="#" target="_blank" class="btn-icon-head" title="Open in new tab">↗</a>
|
|
188
218
|
</div>
|
|
189
219
|
|
|
220
|
+
<!-- Git View Container (Hidden by default) -->
|
|
221
|
+
<div id="git-view-container" class="git-view-container"></div>
|
|
222
|
+
|
|
223
|
+
<!-- AI Iframe Container -->
|
|
190
224
|
<div class="ai-iframe-container">
|
|
191
225
|
<div class="iframe-placeholder">
|
|
192
226
|
<p>⚠️ Nếu trang trắng, hãy cài Extension <b>"Ignore X-Frame-Options"</b></p>
|
|
@@ -196,9 +230,11 @@
|
|
|
196
230
|
</div>
|
|
197
231
|
</div>
|
|
198
232
|
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Floating Terminals Container (Fixed on top of everything) -->
|
|
235
|
+
<div id="floating-terminals-layer"></div>
|
|
199
236
|
|
|
200
237
|
<div class="toast" id="toast"></div>
|
|
201
238
|
<script type="module" src="/js/main.js"></script>
|
|
202
239
|
</body>
|
|
203
|
-
|
|
204
240
|
</html>
|
|
@@ -72,6 +72,21 @@ export async function getStructure(path) {
|
|
|
72
72
|
return await res.json();
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Get Git Diff
|
|
77
|
+
* @returns {Promise<string>} - Unified diff string
|
|
78
|
+
*/
|
|
79
|
+
export async function getGitDiff() {
|
|
80
|
+
const res = await fetch(`${API_BASE}/api/git/diff`);
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
throw new Error(data.error || 'Failed to get git diff');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return data.diff;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
/**
|
|
76
91
|
* Copy content as file to clipboard
|
|
77
92
|
* @param {string} filename - Filename for the clipboard item
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getGitDiff } from '../api.js';
|
|
2
|
+
import { showToast, showLoading } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
export function initGitView() {
|
|
5
|
+
console.log('[GitView] Initializing...');
|
|
6
|
+
const toggleBtn = document.getElementById('git-view-toggle');
|
|
7
|
+
const refreshBtn = document.getElementById('git-refresh-btn');
|
|
8
|
+
|
|
9
|
+
if (toggleBtn) toggleBtn.addEventListener('click', toggleGitMode);
|
|
10
|
+
if (refreshBtn) refreshBtn.addEventListener('click', loadGitChanges);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let isGitMode = false;
|
|
14
|
+
|
|
15
|
+
async function toggleGitMode() {
|
|
16
|
+
const gitContainer = document.getElementById('git-view-container');
|
|
17
|
+
const toggleBtn = document.getElementById('git-view-toggle');
|
|
18
|
+
const refreshBtn = document.getElementById('git-refresh-btn');
|
|
19
|
+
const toggleText = document.getElementById('git-toggle-text');
|
|
20
|
+
|
|
21
|
+
isGitMode = !isGitMode;
|
|
22
|
+
console.log('[GitView] Toggle Mode:', isGitMode);
|
|
23
|
+
|
|
24
|
+
if (isGitMode) {
|
|
25
|
+
gitContainer.classList.add('active');
|
|
26
|
+
toggleBtn.classList.add('active');
|
|
27
|
+
if (toggleText) toggleText.textContent = 'Close Changes';
|
|
28
|
+
if (refreshBtn) refreshBtn.style.display = 'flex';
|
|
29
|
+
|
|
30
|
+
// Debug kích thước container
|
|
31
|
+
const rect = gitContainer.getBoundingClientRect();
|
|
32
|
+
console.log('[GitView] Container Size:', rect.width, 'x', rect.height);
|
|
33
|
+
|
|
34
|
+
if (!gitContainer.innerHTML.trim()) {
|
|
35
|
+
await loadGitChanges();
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
gitContainer.classList.remove('active');
|
|
39
|
+
toggleBtn.classList.remove('active');
|
|
40
|
+
if (toggleText) toggleText.textContent = 'View Changes';
|
|
41
|
+
if (refreshBtn) refreshBtn.style.display = 'none';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function loadGitChanges() {
|
|
46
|
+
const gitContainer = document.getElementById('git-view-container');
|
|
47
|
+
const refreshBtn = document.getElementById('git-refresh-btn');
|
|
48
|
+
|
|
49
|
+
console.log('[GitView] Loading changes...');
|
|
50
|
+
|
|
51
|
+
if (refreshBtn) {
|
|
52
|
+
refreshBtn.style.opacity = '0.5';
|
|
53
|
+
refreshBtn.disabled = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
gitContainer.innerHTML = '<div class="git-loading-msg">Loading git changes... (Check Console if stuck)</div>';
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Kiểm tra thư viện
|
|
60
|
+
if (typeof Diff2HtmlUI === 'undefined') {
|
|
61
|
+
throw new Error('Thư viện Diff2HtmlUI chưa được load!');
|
|
62
|
+
}
|
|
63
|
+
if (typeof hljs === 'undefined') {
|
|
64
|
+
console.warn('⚠️ Highlight.js chưa được load!');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const diffString = await getGitDiff();
|
|
68
|
+
console.log('[GitView] Diff received, length:', diffString?.length);
|
|
69
|
+
|
|
70
|
+
if (!diffString || !diffString.trim()) {
|
|
71
|
+
gitContainer.innerHTML = `
|
|
72
|
+
<div class="git-loading-msg">
|
|
73
|
+
<div style="font-size:30px;">✨</div>
|
|
74
|
+
<p>No changes detected</p>
|
|
75
|
+
</div>`;
|
|
76
|
+
} else {
|
|
77
|
+
console.log('[GitView] Rendering diff...');
|
|
78
|
+
|
|
79
|
+
// Cấu hình Diff2Html
|
|
80
|
+
const configuration = {
|
|
81
|
+
drawFileList: true,
|
|
82
|
+
fileListToggle: false,
|
|
83
|
+
fileListStartVisible: true,
|
|
84
|
+
fileContentToggle: false,
|
|
85
|
+
matching: 'lines',
|
|
86
|
+
outputFormat: 'side-by-side',
|
|
87
|
+
synchronisedScroll: true,
|
|
88
|
+
highlight: true,
|
|
89
|
+
renderNothingWhenEmpty: true,
|
|
90
|
+
colorScheme: 'dark' // Ép buộc Dark Mode từ thư viện
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
gitContainer.innerHTML = ''; // Clear loading
|
|
94
|
+
|
|
95
|
+
const diff2htmlUi = new Diff2HtmlUI(gitContainer, diffString, configuration);
|
|
96
|
+
diff2htmlUi.draw();
|
|
97
|
+
diff2htmlUi.highlightCode();
|
|
98
|
+
|
|
99
|
+
console.log('[GitView] Render complete.');
|
|
100
|
+
}
|
|
101
|
+
showToast('Git changes loaded', 'success');
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('[GitView] Error:', err);
|
|
105
|
+
gitContainer.innerHTML = `
|
|
106
|
+
<div class="git-loading-msg" style="color:var(--ios-red);">
|
|
107
|
+
<div>⚠️ Error</div>
|
|
108
|
+
<div style="font-size:12px;">${err.message}</div>
|
|
109
|
+
<div style="font-size:10px; margin-top:5px;">Check F12 Console for details</div>
|
|
110
|
+
</div>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (refreshBtn) {
|
|
114
|
+
refreshBtn.style.opacity = '1';
|
|
115
|
+
refreshBtn.disabled = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Terminal Logic: Multi-instance & Floating
|
|
2
|
+
|
|
3
|
+
let socket;
|
|
4
|
+
const activeTerminals = new Map(); // Map<termId, { term, fitAddon, element }>
|
|
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
|
+
wrapper.style.top = '100px';
|
|
43
|
+
wrapper.style.left = '100px';
|
|
44
|
+
// Offset vị trí một chút nếu mở nhiều cái
|
|
45
|
+
const offset = activeTerminals.size * 30;
|
|
46
|
+
wrapper.style.top = `${100 + offset}px`;
|
|
47
|
+
wrapper.style.left = `${400 + offset}px`;
|
|
48
|
+
wrapper.style.zIndex = ++maxZIndex;
|
|
49
|
+
|
|
50
|
+
wrapper.innerHTML = `
|
|
51
|
+
<div class="terminal-header" id="header-${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 close" onclick="window.closeTerminal('${termId}')" title="Close"></button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="terminal-body" id="body-${termId}"></div>
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
layer.appendChild(wrapper);
|
|
63
|
+
|
|
64
|
+
// 2. Init xterm.js
|
|
65
|
+
const term = new Terminal({
|
|
66
|
+
cursorBlink: true,
|
|
67
|
+
fontSize: 13,
|
|
68
|
+
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
69
|
+
theme: {
|
|
70
|
+
background: '#1e1e1e',
|
|
71
|
+
foreground: '#f0f0f0'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
76
|
+
term.loadAddon(fitAddon);
|
|
77
|
+
|
|
78
|
+
term.open(document.getElementById(`body-${termId}`));
|
|
79
|
+
fitAddon.fit();
|
|
80
|
+
|
|
81
|
+
// 3. Register Events
|
|
82
|
+
|
|
83
|
+
// Bring to front on click
|
|
84
|
+
wrapper.addEventListener('mousedown', () => {
|
|
85
|
+
wrapper.style.zIndex = ++maxZIndex;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Send input
|
|
89
|
+
term.onData(data => {
|
|
90
|
+
socket.emit('terminal:input', { termId, data });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Resize Observer to refit terminal when window is resized
|
|
94
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
95
|
+
try {
|
|
96
|
+
fitAddon.fit();
|
|
97
|
+
socket.emit('terminal:resize', {
|
|
98
|
+
termId,
|
|
99
|
+
cols: term.cols,
|
|
100
|
+
rows: term.rows
|
|
101
|
+
});
|
|
102
|
+
} catch (e) {}
|
|
103
|
+
});
|
|
104
|
+
resizeObserver.observe(document.getElementById(`body-${termId}`));
|
|
105
|
+
|
|
106
|
+
// 4. Make Draggable
|
|
107
|
+
makeDraggable(wrapper, document.getElementById(`header-${termId}`));
|
|
108
|
+
|
|
109
|
+
// 5. Store Session
|
|
110
|
+
activeTerminals.set(termId, { term, fitAddon, element: wrapper });
|
|
111
|
+
|
|
112
|
+
// 6. Init Backend Process
|
|
113
|
+
socket.emit('terminal:init', {
|
|
114
|
+
termId,
|
|
115
|
+
cols: term.cols,
|
|
116
|
+
rows: term.rows
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Đóng terminal UI & Process
|
|
122
|
+
*/
|
|
123
|
+
export function closeTerminalUI(termId) {
|
|
124
|
+
const session = activeTerminals.get(termId);
|
|
125
|
+
if (session) {
|
|
126
|
+
// Remove from DOM
|
|
127
|
+
session.element.remove();
|
|
128
|
+
// Clean map
|
|
129
|
+
activeTerminals.delete(termId);
|
|
130
|
+
// Tell server to kill
|
|
131
|
+
socket.emit('terminal:kill', { termId });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Logic Drag & Drop
|
|
137
|
+
*/
|
|
138
|
+
function makeDraggable(element, handle) {
|
|
139
|
+
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
|
|
140
|
+
|
|
141
|
+
handle.onmousedown = dragMouseDown;
|
|
142
|
+
|
|
143
|
+
function dragMouseDown(e) {
|
|
144
|
+
e.preventDefault();
|
|
145
|
+
// Get mouse cursor position at startup
|
|
146
|
+
pos3 = e.clientX;
|
|
147
|
+
pos4 = e.clientY;
|
|
148
|
+
document.onmouseup = closeDragElement;
|
|
149
|
+
// Call function whenever cursor moves
|
|
150
|
+
document.onmousemove = elementDrag;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function elementDrag(e) {
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
// Calculate new cursor position
|
|
156
|
+
pos1 = pos3 - e.clientX;
|
|
157
|
+
pos2 = pos4 - e.clientY;
|
|
158
|
+
pos3 = e.clientX;
|
|
159
|
+
pos4 = e.clientY;
|
|
160
|
+
// Set element's new position
|
|
161
|
+
element.style.top = (element.offsetTop - pos2) + "px";
|
|
162
|
+
element.style.left = (element.offsetLeft - pos1) + "px";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function closeDragElement() {
|
|
166
|
+
// Stop moving when mouse button is released
|
|
167
|
+
document.onmouseup = null;
|
|
168
|
+
document.onmousemove = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Global Exports for HTML onclick
|
|
173
|
+
window.createNewTerminal = createNewTerminal;
|
|
174
|
+
window.closeTerminal = closeTerminalUI;
|