claude-relay 2.1.2 → 2.2.0
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/README.md +31 -6
- package/bin/cli.js +7 -3
- package/lib/pages.js +3 -2
- package/lib/project.js +130 -39
- package/lib/public/app.js +381 -10
- package/lib/public/css/base.css +7 -0
- package/lib/public/css/filebrowser.css +149 -2
- package/lib/public/css/input.css +83 -10
- package/lib/public/css/menus.css +281 -1
- package/lib/public/css/messages.css +191 -0
- package/lib/public/css/sidebar.css +93 -1
- package/lib/public/index.html +90 -9
- package/lib/public/modules/filebrowser.js +33 -2
- package/lib/public/modules/input.js +98 -1
- package/lib/public/modules/notifications.js +19 -1
- package/lib/public/modules/qrcode.js +7 -1
- package/lib/public/modules/sidebar.js +233 -3
- package/lib/public/modules/terminal.js +484 -74
- package/lib/public/modules/tools.js +346 -2
- package/lib/public/sw.js +2 -5
- package/lib/push.js +16 -0
- package/lib/sdk-bridge.js +56 -6
- package/lib/server.js +4 -1
- package/lib/sessions.js +34 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +3 -3
- package/lib/usage.js +90 -0
- package/package.json +1 -1
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import { refreshIcons } from './icons.js';
|
|
1
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
2
|
import { closeSidebar } from './sidebar.js';
|
|
3
|
+
import { closeFileViewer } from './filebrowser.js';
|
|
3
4
|
|
|
4
5
|
var ctx;
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var resizeObserver = null;
|
|
6
|
+
var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
|
|
7
|
+
var activeTabId = null;
|
|
8
8
|
var isOpen = false;
|
|
9
|
+
var ctrlActive = false;
|
|
10
|
+
var isTouchDevice = "ontouchstart" in window;
|
|
11
|
+
var viewportHandler = null;
|
|
12
|
+
var resizeObserver = null;
|
|
13
|
+
var toolbarBound = false;
|
|
9
14
|
|
|
15
|
+
// --- Init ---
|
|
10
16
|
export function initTerminal(_ctx) {
|
|
11
17
|
ctx = _ctx;
|
|
12
18
|
|
|
13
|
-
// Close button
|
|
19
|
+
// Close panel button
|
|
14
20
|
document.getElementById("terminal-close").addEventListener("click", function () {
|
|
15
21
|
closeTerminal();
|
|
16
22
|
});
|
|
@@ -35,32 +41,157 @@ export function initTerminal(_ctx) {
|
|
|
35
41
|
openTerminal();
|
|
36
42
|
});
|
|
37
43
|
}
|
|
44
|
+
|
|
45
|
+
// New tab button
|
|
46
|
+
var newTabBtn = document.getElementById("terminal-new-tab");
|
|
47
|
+
if (newTabBtn) {
|
|
48
|
+
newTabBtn.addEventListener("click", function () {
|
|
49
|
+
createNewTab();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
// --- Open terminal panel ---
|
|
40
55
|
export function openTerminal() {
|
|
41
56
|
var container = ctx.terminalContainerEl;
|
|
42
|
-
var body = ctx.terminalBodyEl;
|
|
43
57
|
|
|
44
|
-
// Hide file viewer if open
|
|
45
|
-
|
|
58
|
+
// Hide file viewer if open (also unwatches)
|
|
59
|
+
closeFileViewer();
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
container.classList.remove("hidden");
|
|
62
|
+
isOpen = true;
|
|
63
|
+
|
|
64
|
+
// If no tabs exist, create one
|
|
65
|
+
if (tabs.size === 0) {
|
|
66
|
+
createNewTab();
|
|
67
|
+
return; // createNewTab will handle the rest via term_created
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
70
|
+
// Attach to active tab (or first available)
|
|
71
|
+
if (!activeTabId || !tabs.has(activeTabId)) {
|
|
72
|
+
activeTabId = tabs.keys().next().value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
activateTab(activeTabId);
|
|
76
|
+
|
|
77
|
+
// Mobile: close sidebar
|
|
78
|
+
if (window.innerWidth <= 768) {
|
|
79
|
+
closeSidebar();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
refreshIcons();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- Close terminal panel (hide, detach, but keep PTYs alive) ---
|
|
86
|
+
export function closeTerminal() {
|
|
87
|
+
var container = ctx.terminalContainerEl;
|
|
88
|
+
container.classList.add("hidden");
|
|
89
|
+
|
|
90
|
+
// Detach from active tab
|
|
91
|
+
if (activeTabId && ctx.ws && ctx.connected) {
|
|
92
|
+
ctx.ws.send(JSON.stringify({ type: "term_detach", id: activeTabId }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cleanupListeners();
|
|
96
|
+
|
|
97
|
+
// Hide toolbar
|
|
98
|
+
var toolbar = document.getElementById("terminal-toolbar");
|
|
99
|
+
if (toolbar) {
|
|
100
|
+
toolbar.classList.add("hidden");
|
|
101
|
+
var ctrlBtn = toolbar.querySelector("[data-key='ctrl']");
|
|
102
|
+
if (ctrlBtn) ctrlBtn.classList.remove("active");
|
|
103
|
+
}
|
|
104
|
+
ctrlActive = false;
|
|
105
|
+
|
|
106
|
+
isOpen = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Create new tab ---
|
|
110
|
+
function createNewTab() {
|
|
111
|
+
if (!ctx.ws || !ctx.connected) return;
|
|
112
|
+
|
|
113
|
+
// Get current terminal body dimensions for cols/rows
|
|
114
|
+
var cols = 80;
|
|
115
|
+
var rows = 24;
|
|
116
|
+
if (activeTabId && tabs.has(activeTabId)) {
|
|
117
|
+
var activeTab = tabs.get(activeTabId);
|
|
118
|
+
if (activeTab.xterm) {
|
|
119
|
+
cols = activeTab.xterm.cols || 80;
|
|
120
|
+
rows = activeTab.xterm.rows || 24;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ctx.ws.send(JSON.stringify({ type: "term_create", cols: cols, rows: rows }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Close a tab (kill PTY) ---
|
|
128
|
+
function closeTab(termId) {
|
|
129
|
+
if (!ctx.ws || !ctx.connected) return;
|
|
130
|
+
ctx.ws.send(JSON.stringify({ type: "term_close", id: termId }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- Activate a tab (show xterm, attach) ---
|
|
134
|
+
function activateTab(termId) {
|
|
135
|
+
var tab = tabs.get(termId);
|
|
136
|
+
if (!tab) return;
|
|
137
|
+
|
|
138
|
+
// Detach from old active
|
|
139
|
+
if (activeTabId && activeTabId !== termId && ctx.ws && ctx.connected) {
|
|
140
|
+
ctx.ws.send(JSON.stringify({ type: "term_detach", id: activeTabId }));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Hide all tab bodies
|
|
144
|
+
for (var t of tabs.values()) {
|
|
145
|
+
if (t.bodyEl) t.bodyEl.style.display = "none";
|
|
146
|
+
}
|
|
56
147
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
148
|
+
activeTabId = termId;
|
|
149
|
+
|
|
150
|
+
// Lazy-create xterm instance
|
|
151
|
+
if (!tab.xterm) {
|
|
152
|
+
createXtermForTab(tab);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Show this tab's body
|
|
156
|
+
if (tab.bodyEl) tab.bodyEl.style.display = "";
|
|
157
|
+
|
|
158
|
+
// Attach to server
|
|
159
|
+
if (ctx.ws && ctx.connected) {
|
|
160
|
+
ctx.ws.send(JSON.stringify({ type: "term_attach", id: termId }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fit and focus
|
|
164
|
+
setupListeners();
|
|
165
|
+
fitTerminal();
|
|
166
|
+
|
|
167
|
+
if (tab.xterm) {
|
|
168
|
+
tab.xterm.focus();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Show toolbar on touch devices
|
|
172
|
+
var toolbar = document.getElementById("terminal-toolbar");
|
|
173
|
+
if (toolbar && isTouchDevice) {
|
|
174
|
+
toolbar.classList.remove("hidden");
|
|
175
|
+
initToolbar(toolbar);
|
|
61
176
|
}
|
|
62
177
|
|
|
63
|
-
|
|
178
|
+
// Mobile viewport handling
|
|
179
|
+
if (window.visualViewport && !viewportHandler) {
|
|
180
|
+
viewportHandler = function () {
|
|
181
|
+
ctx.terminalContainerEl.style.height = window.visualViewport.height + "px";
|
|
182
|
+
fitTerminal();
|
|
183
|
+
};
|
|
184
|
+
window.visualViewport.addEventListener("resize", viewportHandler);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
renderTabBar();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --- Create xterm.js instance for a tab ---
|
|
191
|
+
function createXtermForTab(tab) {
|
|
192
|
+
if (typeof Terminal === "undefined") return;
|
|
193
|
+
|
|
194
|
+
var xterm = new Terminal({
|
|
64
195
|
cursorBlink: true,
|
|
65
196
|
fontSize: 13,
|
|
66
197
|
fontFamily: "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
|
|
@@ -88,100 +219,379 @@ export function openTerminal() {
|
|
|
88
219
|
},
|
|
89
220
|
});
|
|
90
221
|
|
|
222
|
+
var fitAddon = null;
|
|
91
223
|
if (typeof FitAddon !== "undefined") {
|
|
92
224
|
fitAddon = new FitAddon.FitAddon();
|
|
93
|
-
|
|
225
|
+
xterm.loadAddon(fitAddon);
|
|
94
226
|
}
|
|
95
227
|
|
|
96
|
-
|
|
228
|
+
// Create a container div for this tab's terminal
|
|
229
|
+
var bodyEl = document.createElement("div");
|
|
230
|
+
bodyEl.className = "terminal-tab-body";
|
|
231
|
+
ctx.terminalBodyEl.appendChild(bodyEl);
|
|
97
232
|
|
|
98
|
-
|
|
99
|
-
|
|
233
|
+
xterm.open(bodyEl);
|
|
234
|
+
|
|
235
|
+
// Route input to server
|
|
236
|
+
xterm.onData(function (data) {
|
|
100
237
|
if (ctx.ws && ctx.connected) {
|
|
101
|
-
ctx.ws.send(JSON.stringify({ type: "term_input", data: data }));
|
|
238
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: data }));
|
|
102
239
|
}
|
|
103
240
|
});
|
|
104
241
|
|
|
105
|
-
|
|
106
|
-
|
|
242
|
+
tab.xterm = xterm;
|
|
243
|
+
tab.fitAddon = fitAddon;
|
|
244
|
+
tab.bodyEl = bodyEl;
|
|
245
|
+
}
|
|
107
246
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
247
|
+
// --- Fit active terminal ---
|
|
248
|
+
function fitTerminal() {
|
|
249
|
+
if (!activeTabId) return;
|
|
250
|
+
var tab = tabs.get(activeTabId);
|
|
251
|
+
if (!tab || !tab.fitAddon || !tab.xterm) return;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
tab.fitAddon.fit();
|
|
255
|
+
if (ctx.ws && ctx.connected) {
|
|
256
|
+
ctx.ws.send(JSON.stringify({
|
|
257
|
+
type: "term_resize",
|
|
258
|
+
id: activeTabId,
|
|
259
|
+
cols: tab.xterm.cols,
|
|
260
|
+
rows: tab.xterm.rows,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// --- Setup/cleanup resize listeners ---
|
|
267
|
+
function setupListeners() {
|
|
268
|
+
cleanupListeners();
|
|
112
269
|
|
|
113
|
-
// Auto-fit on resize
|
|
114
270
|
window.addEventListener("resize", fitTerminal);
|
|
115
271
|
|
|
116
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
272
|
+
if (typeof ResizeObserver !== "undefined" && ctx.terminalBodyEl) {
|
|
117
273
|
resizeObserver = new ResizeObserver(function () {
|
|
118
274
|
fitTerminal();
|
|
119
275
|
});
|
|
120
|
-
resizeObserver.observe(
|
|
276
|
+
resizeObserver.observe(ctx.terminalBodyEl);
|
|
121
277
|
}
|
|
278
|
+
}
|
|
122
279
|
|
|
123
|
-
|
|
124
|
-
|
|
280
|
+
function cleanupListeners() {
|
|
281
|
+
window.removeEventListener("resize", fitTerminal);
|
|
125
282
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
283
|
+
if (resizeObserver) {
|
|
284
|
+
resizeObserver.disconnect();
|
|
285
|
+
resizeObserver = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (viewportHandler && window.visualViewport) {
|
|
289
|
+
window.visualViewport.removeEventListener("resize", viewportHandler);
|
|
290
|
+
viewportHandler = null;
|
|
291
|
+
}
|
|
292
|
+
ctx.terminalContainerEl.style.height = "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Render tab bar ---
|
|
296
|
+
function renderTabBar() {
|
|
297
|
+
var tabsEl = document.getElementById("terminal-tabs");
|
|
298
|
+
if (!tabsEl) return;
|
|
299
|
+
|
|
300
|
+
tabsEl.innerHTML = "";
|
|
301
|
+
|
|
302
|
+
for (var tab of tabs.values()) {
|
|
303
|
+
(function (t) {
|
|
304
|
+
var el = document.createElement("div");
|
|
305
|
+
el.className = "terminal-tab";
|
|
306
|
+
if (t.id === activeTabId) el.classList.add("active");
|
|
307
|
+
if (t.exited) el.classList.add("exited");
|
|
308
|
+
|
|
309
|
+
var label = document.createElement("span");
|
|
310
|
+
label.className = "terminal-tab-label";
|
|
311
|
+
label.textContent = t.title;
|
|
312
|
+
el.appendChild(label);
|
|
313
|
+
|
|
314
|
+
// Double-click label to rename
|
|
315
|
+
label.addEventListener("dblclick", function (e) {
|
|
316
|
+
e.stopPropagation();
|
|
317
|
+
startRenameTab(t, label);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
var closeBtn = document.createElement("button");
|
|
321
|
+
closeBtn.className = "terminal-tab-close";
|
|
322
|
+
closeBtn.innerHTML = '<i data-lucide="x" style="width:12px;height:12px"></i>';
|
|
323
|
+
closeBtn.addEventListener("click", function (e) {
|
|
324
|
+
e.stopPropagation();
|
|
325
|
+
closeTab(t.id);
|
|
326
|
+
});
|
|
327
|
+
el.appendChild(closeBtn);
|
|
328
|
+
|
|
329
|
+
el.addEventListener("click", function () {
|
|
330
|
+
if (t.id !== activeTabId) {
|
|
331
|
+
activateTab(t.id);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
tabsEl.appendChild(el);
|
|
336
|
+
})(tab);
|
|
129
337
|
}
|
|
130
338
|
|
|
339
|
+
updateTerminalBadge();
|
|
131
340
|
refreshIcons();
|
|
132
341
|
}
|
|
133
342
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
343
|
+
// --- Rename tab inline ---
|
|
344
|
+
function startRenameTab(tab, labelEl) {
|
|
345
|
+
var input = document.createElement("input");
|
|
346
|
+
input.className = "terminal-tab-rename";
|
|
347
|
+
input.value = tab.title;
|
|
348
|
+
input.maxLength = 50;
|
|
349
|
+
|
|
350
|
+
labelEl.replaceWith(input);
|
|
351
|
+
input.focus();
|
|
352
|
+
input.select();
|
|
353
|
+
|
|
354
|
+
function commit() {
|
|
355
|
+
var newTitle = input.value.trim();
|
|
356
|
+
if (newTitle && newTitle !== tab.title) {
|
|
357
|
+
tab.title = newTitle;
|
|
358
|
+
if (ctx.ws && ctx.connected) {
|
|
359
|
+
ctx.ws.send(JSON.stringify({ type: "term_rename", id: tab.id, title: newTitle }));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
renderTabBar();
|
|
137
363
|
}
|
|
364
|
+
|
|
365
|
+
input.addEventListener("blur", commit);
|
|
366
|
+
input.addEventListener("keydown", function (e) {
|
|
367
|
+
if (e.key === "Enter") { input.blur(); }
|
|
368
|
+
if (e.key === "Escape") {
|
|
369
|
+
input.value = tab.title; // revert
|
|
370
|
+
input.blur();
|
|
371
|
+
}
|
|
372
|
+
e.stopPropagation();
|
|
373
|
+
});
|
|
138
374
|
}
|
|
139
375
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
376
|
+
// --- Update terminal count badge ---
|
|
377
|
+
function updateTerminalBadge() {
|
|
378
|
+
var countEl = document.getElementById("terminal-count");
|
|
379
|
+
if (!countEl) return;
|
|
380
|
+
|
|
381
|
+
var count = 0;
|
|
382
|
+
for (var t of tabs.values()) {
|
|
383
|
+
if (!t.exited) count++;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (count > 0) {
|
|
387
|
+
countEl.textContent = count;
|
|
388
|
+
countEl.classList.remove("hidden");
|
|
389
|
+
} else {
|
|
390
|
+
countEl.classList.add("hidden");
|
|
143
391
|
}
|
|
144
392
|
}
|
|
145
393
|
|
|
146
|
-
|
|
147
|
-
var container = ctx.terminalContainerEl;
|
|
148
|
-
container.classList.add("hidden");
|
|
394
|
+
// --- Handle server messages ---
|
|
149
395
|
|
|
150
|
-
|
|
151
|
-
|
|
396
|
+
export function handleTermList(msg) {
|
|
397
|
+
var serverTerminals = msg.terminals || [];
|
|
398
|
+
var serverIds = new Set();
|
|
399
|
+
|
|
400
|
+
// Add/update tabs from server list
|
|
401
|
+
for (var i = 0; i < serverTerminals.length; i++) {
|
|
402
|
+
var st = serverTerminals[i];
|
|
403
|
+
serverIds.add(st.id);
|
|
404
|
+
|
|
405
|
+
if (tabs.has(st.id)) {
|
|
406
|
+
var existing = tabs.get(st.id);
|
|
407
|
+
existing.title = st.title;
|
|
408
|
+
existing.exited = st.exited;
|
|
409
|
+
} else {
|
|
410
|
+
tabs.set(st.id, {
|
|
411
|
+
id: st.id,
|
|
412
|
+
title: st.title,
|
|
413
|
+
exited: st.exited,
|
|
414
|
+
xterm: null,
|
|
415
|
+
fitAddon: null,
|
|
416
|
+
bodyEl: null,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
152
419
|
}
|
|
153
420
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
421
|
+
// Remove tabs no longer on server
|
|
422
|
+
for (var id of tabs.keys()) {
|
|
423
|
+
if (!serverIds.has(id)) {
|
|
424
|
+
var removed = tabs.get(id);
|
|
425
|
+
if (removed.xterm) {
|
|
426
|
+
removed.xterm.dispose();
|
|
427
|
+
}
|
|
428
|
+
if (removed.bodyEl && removed.bodyEl.parentNode) {
|
|
429
|
+
removed.bodyEl.parentNode.removeChild(removed.bodyEl);
|
|
430
|
+
}
|
|
431
|
+
tabs.delete(id);
|
|
432
|
+
}
|
|
157
433
|
}
|
|
158
434
|
|
|
159
|
-
|
|
435
|
+
// If active tab was removed, switch to first available
|
|
436
|
+
if (activeTabId && !tabs.has(activeTabId)) {
|
|
437
|
+
activeTabId = null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
renderTabBar();
|
|
160
441
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
442
|
+
// If panel is open and we have tabs, re-attach
|
|
443
|
+
if (isOpen && tabs.size > 0) {
|
|
444
|
+
if (!activeTabId) {
|
|
445
|
+
activeTabId = tabs.keys().next().value;
|
|
446
|
+
}
|
|
447
|
+
activateTab(activeTabId);
|
|
165
448
|
}
|
|
166
449
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
450
|
+
// If panel is open and all tabs are gone, close panel
|
|
451
|
+
if (isOpen && tabs.size === 0) {
|
|
452
|
+
closeTerminal();
|
|
453
|
+
}
|
|
170
454
|
}
|
|
171
455
|
|
|
172
|
-
function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
456
|
+
export function handleTermCreated(msg) {
|
|
457
|
+
// Switch to the newly created tab
|
|
458
|
+
if (msg.id && tabs.has(msg.id)) {
|
|
459
|
+
activateTab(msg.id);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function handleTermOutput(msg) {
|
|
464
|
+
if (!msg.id) return;
|
|
465
|
+
var tab = tabs.get(msg.id);
|
|
466
|
+
if (tab && tab.xterm && msg.data) {
|
|
467
|
+
tab.xterm.write(msg.data);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function handleTermExited(msg) {
|
|
472
|
+
if (!msg.id) return;
|
|
473
|
+
var tab = tabs.get(msg.id);
|
|
474
|
+
if (tab) {
|
|
475
|
+
tab.exited = true;
|
|
476
|
+
if (tab.xterm) {
|
|
477
|
+
tab.xterm.write("\r\n\x1b[90m[Process exited]\x1b[0m\r\n");
|
|
478
|
+
}
|
|
479
|
+
renderTabBar();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function handleTermClosed(msg) {
|
|
484
|
+
if (!msg.id) return;
|
|
485
|
+
var tab = tabs.get(msg.id);
|
|
486
|
+
if (tab) {
|
|
487
|
+
if (tab.xterm) tab.xterm.dispose();
|
|
488
|
+
if (tab.bodyEl && tab.bodyEl.parentNode) {
|
|
489
|
+
tab.bodyEl.parentNode.removeChild(tab.bodyEl);
|
|
490
|
+
}
|
|
491
|
+
tabs.delete(msg.id);
|
|
492
|
+
|
|
493
|
+
if (activeTabId === msg.id) {
|
|
494
|
+
activeTabId = null;
|
|
495
|
+
if (tabs.size > 0) {
|
|
496
|
+
activeTabId = tabs.keys().next().value;
|
|
497
|
+
activateTab(activeTabId);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
renderTabBar();
|
|
502
|
+
|
|
503
|
+
// Close panel if no tabs left
|
|
504
|
+
if (isOpen && tabs.size === 0) {
|
|
505
|
+
closeTerminal();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// --- Reset on reconnect ---
|
|
511
|
+
export function resetTerminals() {
|
|
512
|
+
// Dispose all xterm instances (server state survives, client re-syncs via term_list)
|
|
513
|
+
for (var tab of tabs.values()) {
|
|
514
|
+
if (tab.xterm) {
|
|
515
|
+
tab.xterm.dispose();
|
|
516
|
+
tab.xterm = null;
|
|
517
|
+
tab.fitAddon = null;
|
|
518
|
+
}
|
|
519
|
+
if (tab.bodyEl && tab.bodyEl.parentNode) {
|
|
520
|
+
tab.bodyEl.parentNode.removeChild(tab.bodyEl);
|
|
521
|
+
tab.bodyEl = null;
|
|
183
522
|
}
|
|
184
|
-
}
|
|
185
|
-
|
|
523
|
+
}
|
|
524
|
+
tabs.clear();
|
|
525
|
+
activeTabId = null;
|
|
526
|
+
cleanupListeners();
|
|
527
|
+
renderTabBar();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// --- Mobile toolbar ---
|
|
531
|
+
var KEY_MAP = {
|
|
532
|
+
tab: "\t",
|
|
533
|
+
esc: "\x1b",
|
|
534
|
+
up: "\x1b[A",
|
|
535
|
+
down: "\x1b[B",
|
|
536
|
+
right: "\x1b[C",
|
|
537
|
+
left: "\x1b[D",
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
function initToolbar(toolbar) {
|
|
541
|
+
if (!toolbarBound) {
|
|
542
|
+
toolbarBound = true;
|
|
543
|
+
|
|
544
|
+
toolbar.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
|
545
|
+
|
|
546
|
+
toolbar.addEventListener("click", function (e) {
|
|
547
|
+
var btn = e.target.closest(".term-key");
|
|
548
|
+
if (!btn) return;
|
|
549
|
+
|
|
550
|
+
var tab = activeTabId ? tabs.get(activeTabId) : null;
|
|
551
|
+
if (!tab || !tab.xterm) return;
|
|
552
|
+
|
|
553
|
+
var key = btn.dataset.key;
|
|
554
|
+
if (!key) return;
|
|
555
|
+
|
|
556
|
+
if (key === "ctrl") {
|
|
557
|
+
ctrlActive = !ctrlActive;
|
|
558
|
+
btn.classList.toggle("active", ctrlActive);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
var seq = KEY_MAP[key];
|
|
563
|
+
if (!seq) return;
|
|
564
|
+
|
|
565
|
+
if (ctx.ws && ctx.connected) {
|
|
566
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: seq }));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (ctrlActive) {
|
|
570
|
+
ctrlActive = false;
|
|
571
|
+
var ctrlBtn = toolbar.querySelector("[data-key='ctrl']");
|
|
572
|
+
if (ctrlBtn) ctrlBtn.classList.remove("active");
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Attach Ctrl handler to active terminal
|
|
578
|
+
var tab = activeTabId ? tabs.get(activeTabId) : null;
|
|
579
|
+
if (tab && tab.xterm) {
|
|
580
|
+
tab.xterm.attachCustomKeyEventHandler(function (ev) {
|
|
581
|
+
if (ctrlActive && ev.type === "keydown" && ev.key.length === 1) {
|
|
582
|
+
var charCode = ev.key.toUpperCase().charCodeAt(0);
|
|
583
|
+
if (charCode >= 65 && charCode <= 90) {
|
|
584
|
+
var ctrlChar = String.fromCharCode(charCode - 64);
|
|
585
|
+
if (ctx.ws && ctx.connected) {
|
|
586
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: ctrlChar }));
|
|
587
|
+
}
|
|
588
|
+
ctrlActive = false;
|
|
589
|
+
var ctrlBtn = document.querySelector("#terminal-toolbar [data-key='ctrl']");
|
|
590
|
+
if (ctrlBtn) ctrlBtn.classList.remove("active");
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
});
|
|
186
596
|
}
|
|
187
597
|
}
|