cli-tunnel 1.2.0-beta.11 → 1.2.0-beta.12
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/dist/index.js +4 -4
- package/package.json +1 -1
- package/remote-ui/app.js +292 -42
- package/remote-ui/styles.css +131 -3
package/dist/index.js
CHANGED
|
@@ -338,7 +338,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
338
338
|
const baseId = t.tunnelId?.split('.')[0] || t.tunnelId;
|
|
339
339
|
const token = tokenMap.get(baseId) || tokenMap.get(t.tunnelId);
|
|
340
340
|
if (token)
|
|
341
|
-
session.
|
|
341
|
+
session.hasToken = true;
|
|
342
342
|
return session;
|
|
343
343
|
});
|
|
344
344
|
res.writeHead(200, { 'Content-Type': 'application/json', 'X-Frame-Options': 'DENY', 'X-Content-Type-Options': 'nosniff' });
|
|
@@ -434,8 +434,6 @@ const wss = new WebSocketServer({
|
|
|
434
434
|
server,
|
|
435
435
|
maxPayload: 1048576,
|
|
436
436
|
verifyClient: (info) => {
|
|
437
|
-
if (hubMode)
|
|
438
|
-
return true; // Hub mode doesn't need WS auth
|
|
439
437
|
// F-18: Session expiry
|
|
440
438
|
if (Date.now() - sessionCreatedAt > SESSION_TTL)
|
|
441
439
|
return false;
|
|
@@ -766,7 +764,9 @@ async function main() {
|
|
|
766
764
|
const DANGEROUS_VARS = new Set(['NODE_OPTIONS', 'NODE_REPL_HISTORY', 'NODE_EXTRA_CA_CERTS',
|
|
767
765
|
'NODE_PATH', 'NODE_REDIRECT_WARNINGS', 'NODE_PENDING_DEPRECATION',
|
|
768
766
|
'UV_THREADPOOL_SIZE', 'LD_PRELOAD', 'DYLD_INSERT_LIBRARIES',
|
|
769
|
-
'SSH_AUTH_SOCK', 'GPG_TTY'
|
|
767
|
+
'SSH_AUTH_SOCK', 'GPG_TTY',
|
|
768
|
+
'PYTHONPATH', 'BASH_ENV', 'BASH_FUNC', 'JAVA_TOOL_OPTIONS', 'JAVA_OPTIONS', '_JAVA_OPTIONS',
|
|
769
|
+
'PROMPT_COMMAND', 'ENV', 'ZDOTDIR', 'PERL5OPT', 'RUBYOPT']);
|
|
770
770
|
const sensitivePattern = /token|secret|key|password|credential|api_key|private_key|access_key|connection_string|auth|kubeconfig|docker_host|docker_config/i;
|
|
771
771
|
const safeEnv = {};
|
|
772
772
|
for (const [k, v] of Object.entries(process.env)) {
|
package/package.json
CHANGED
package/remote-ui/app.js
CHANGED
|
@@ -47,7 +47,10 @@
|
|
|
47
47
|
const termContainer = $('#terminal-container');
|
|
48
48
|
let currentView = 'terminal'; // 'dashboard', 'terminal', or 'grid'
|
|
49
49
|
let cachedSessions = [];
|
|
50
|
-
let gridTerminals = []; // { xterm, fitAddon, ws, session }
|
|
50
|
+
let gridTerminals = []; // { xterm, fitAddon, ws, session, panel }
|
|
51
|
+
var gridMode = 'thumbnails';
|
|
52
|
+
var focusedIndex = 0;
|
|
53
|
+
var tmuxPreset = 'equal';
|
|
51
54
|
|
|
52
55
|
// ─── xterm.js Terminal ───────────────────────────────────
|
|
53
56
|
let xterm = null;
|
|
@@ -148,17 +151,17 @@
|
|
|
148
151
|
'</div>';
|
|
149
152
|
} else {
|
|
150
153
|
html += filtered.map(s => {
|
|
151
|
-
const
|
|
154
|
+
const hasAccess = s.hasToken;
|
|
152
155
|
return `
|
|
153
|
-
<div class="session-card" ${s.online &&
|
|
156
|
+
<div class="session-card" ${s.online && hasAccess ? 'data-session-port="' + s.port + '" data-session-base-url="' + escapeHtml(s.url) + '"' : ''}>
|
|
154
157
|
<span class="status-dot ${s.online ? 'online' : 'offline'}"></span>
|
|
155
158
|
<div class="info">
|
|
156
159
|
<div class="session-name">${escapeHtml(s.name)}</div>
|
|
157
160
|
<div class="repo">📦 ${escapeHtml(s.repo)}</div>
|
|
158
161
|
<div class="branch">🌿 ${escapeHtml(s.branch)}</div>
|
|
159
|
-
<div class="machine">💻 ${escapeHtml(s.machine)}${!
|
|
162
|
+
<div class="machine">💻 ${escapeHtml(s.machine)}${!hasAccess && s.online ? ' 🔒' : ''}</div>
|
|
160
163
|
</div>
|
|
161
|
-
${s.online &&
|
|
164
|
+
${s.online && hasAccess ? '<span class="arrow">→</span>' :
|
|
162
165
|
!s.online ? '<button data-delete-id="' + escapeHtml(s.id) + '" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:14px" title="Remove">✕</button>'
|
|
163
166
|
: '<span style="color:var(--text-dim);font-size:11px">remote</span>'}
|
|
164
167
|
</div>`;
|
|
@@ -167,8 +170,25 @@
|
|
|
167
170
|
dashboard.innerHTML = html;
|
|
168
171
|
cachedSessions = sessions;
|
|
169
172
|
// Event delegation
|
|
170
|
-
dashboard.querySelectorAll('.session-card[data-session-
|
|
171
|
-
card.addEventListener('click', function() {
|
|
173
|
+
dashboard.querySelectorAll('.session-card[data-session-port]').forEach(function(card) {
|
|
174
|
+
card.addEventListener('click', function() {
|
|
175
|
+
var port = card.dataset.sessionPort;
|
|
176
|
+
var baseUrl = card.dataset.sessionBaseUrl;
|
|
177
|
+
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
178
|
+
var proxyUrl = '/api/proxy/ticket/' + port;
|
|
179
|
+
fetch(proxyUrl, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {}
|
|
182
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
183
|
+
if (data.ticket) {
|
|
184
|
+
window.location.href = baseUrl + '?ticket=' + encodeURIComponent(data.ticket);
|
|
185
|
+
} else {
|
|
186
|
+
window.location.href = baseUrl;
|
|
187
|
+
}
|
|
188
|
+
}).catch(function() {
|
|
189
|
+
window.location.href = baseUrl;
|
|
190
|
+
});
|
|
191
|
+
});
|
|
172
192
|
});
|
|
173
193
|
dashboard.querySelectorAll('[data-delete-id]').forEach(function(btn) {
|
|
174
194
|
btn.addEventListener('click', function(e) { e.stopPropagation(); deleteSession(btn.dataset.deleteId); });
|
|
@@ -207,15 +227,18 @@
|
|
|
207
227
|
loadSessions();
|
|
208
228
|
};
|
|
209
229
|
|
|
210
|
-
// ─── Grid View (
|
|
230
|
+
// ─── Grid View (multi-terminal with layout modes) ───────────
|
|
211
231
|
function showGridView(sessions) {
|
|
212
|
-
|
|
232
|
+
var connectable = sessions.filter(function(s) { return s.online && s.token; });
|
|
213
233
|
if (connectable.length === 0) return;
|
|
214
234
|
|
|
215
235
|
// Clean up previous grid
|
|
216
236
|
destroyGrid();
|
|
217
237
|
|
|
218
238
|
currentView = 'grid';
|
|
239
|
+
gridMode = 'thumbnails';
|
|
240
|
+
focusedIndex = 0;
|
|
241
|
+
tmuxPreset = 'equal';
|
|
219
242
|
dashboard.classList.add('hidden');
|
|
220
243
|
terminal.classList.add('hidden');
|
|
221
244
|
termContainer.classList.add('hidden');
|
|
@@ -230,30 +253,95 @@
|
|
|
230
253
|
gridEl.classList.remove('hidden');
|
|
231
254
|
gridEl.innerHTML = '';
|
|
232
255
|
|
|
233
|
-
//
|
|
234
|
-
var
|
|
235
|
-
|
|
256
|
+
// ── Toolbar ──
|
|
257
|
+
var toolbar = document.createElement('div');
|
|
258
|
+
toolbar.className = 'grid-toolbar';
|
|
259
|
+
|
|
260
|
+
var modes = [
|
|
261
|
+
{ id: 'thumbnails', label: '\u229E Tiles' },
|
|
262
|
+
{ id: 'tmux', label: '\u229F Tmux' },
|
|
263
|
+
{ id: 'focus', label: '\u25C9 Focus' },
|
|
264
|
+
{ id: 'fullscreen', label: '\u2A21 Full' }
|
|
265
|
+
];
|
|
266
|
+
modes.forEach(function(m) {
|
|
267
|
+
var btn = document.createElement('button');
|
|
268
|
+
btn.textContent = m.label;
|
|
269
|
+
btn.dataset.mode = m.id;
|
|
270
|
+
if (m.id === gridMode) btn.classList.add('active');
|
|
271
|
+
btn.addEventListener('click', function() { switchGridMode(m.id); });
|
|
272
|
+
toolbar.appendChild(btn);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Tmux preset buttons (visible only in tmux mode)
|
|
276
|
+
var presetGroup = document.createElement('span');
|
|
277
|
+
presetGroup.className = 'grid-toolbar-presets hidden';
|
|
278
|
+
presetGroup.id = 'tmux-presets';
|
|
279
|
+
var presets = [
|
|
280
|
+
{ id: 'equal', label: '\u2550 Equal' },
|
|
281
|
+
{ id: 'main-side', label: '\u2590 Main+Side' },
|
|
282
|
+
{ id: 'stacked', label: '\u2261 Stacked' }
|
|
283
|
+
];
|
|
284
|
+
presets.forEach(function(p) {
|
|
285
|
+
var btn = document.createElement('button');
|
|
286
|
+
btn.textContent = p.label;
|
|
287
|
+
btn.dataset.preset = p.id;
|
|
288
|
+
if (p.id === tmuxPreset) btn.classList.add('active');
|
|
289
|
+
btn.addEventListener('click', function() { switchTmuxPreset(p.id); });
|
|
290
|
+
presetGroup.appendChild(btn);
|
|
291
|
+
});
|
|
292
|
+
toolbar.appendChild(presetGroup);
|
|
293
|
+
|
|
294
|
+
var spacer = document.createElement('span');
|
|
295
|
+
spacer.className = 'spacer';
|
|
296
|
+
toolbar.appendChild(spacer);
|
|
297
|
+
|
|
298
|
+
var listBtn = document.createElement('button');
|
|
299
|
+
listBtn.textContent = '\u2190 List';
|
|
300
|
+
listBtn.addEventListener('click', function() {
|
|
301
|
+
destroyGrid();
|
|
302
|
+
currentView = 'dashboard';
|
|
303
|
+
dashboard.classList.remove('hidden');
|
|
304
|
+
if ($('#btn-sessions')) $('#btn-sessions').textContent = 'Terminal';
|
|
305
|
+
loadSessions();
|
|
306
|
+
});
|
|
307
|
+
toolbar.appendChild(listBtn);
|
|
308
|
+
gridEl.appendChild(toolbar);
|
|
309
|
+
|
|
310
|
+
// ── Content container ──
|
|
311
|
+
var contentEl = document.createElement('div');
|
|
312
|
+
contentEl.id = 'grid-content';
|
|
313
|
+
gridEl.appendChild(contentEl);
|
|
236
314
|
|
|
237
|
-
|
|
315
|
+
// ── Create panels & connect ──
|
|
316
|
+
connectable.forEach(function(s, index) {
|
|
238
317
|
var panel = document.createElement('div');
|
|
239
318
|
panel.className = 'grid-panel';
|
|
319
|
+
panel.dataset.index = index;
|
|
240
320
|
|
|
241
|
-
// Header
|
|
242
321
|
var header = document.createElement('div');
|
|
243
322
|
header.className = 'grid-panel-header';
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
323
|
+
var nameSpan = document.createElement('span');
|
|
324
|
+
nameSpan.className = 'grid-panel-name';
|
|
325
|
+
nameSpan.textContent = s.name;
|
|
326
|
+
var machineSpan = document.createElement('span');
|
|
327
|
+
machineSpan.className = 'grid-panel-machine';
|
|
328
|
+
machineSpan.textContent = s.machine;
|
|
329
|
+
var statusDot = document.createElement('span');
|
|
330
|
+
statusDot.className = 'grid-panel-status';
|
|
331
|
+
statusDot.textContent = '\u25CF';
|
|
332
|
+
header.appendChild(nameSpan);
|
|
333
|
+
header.appendChild(machineSpan);
|
|
334
|
+
header.appendChild(statusDot);
|
|
247
335
|
panel.appendChild(header);
|
|
248
336
|
|
|
249
|
-
// Terminal container
|
|
250
337
|
var termDiv = document.createElement('div');
|
|
251
338
|
termDiv.className = 'grid-panel-terminal';
|
|
252
339
|
panel.appendChild(termDiv);
|
|
253
340
|
|
|
254
|
-
|
|
341
|
+
// Append to contentEl so xterm.open has a DOM-attached container
|
|
342
|
+
contentEl.appendChild(panel);
|
|
255
343
|
|
|
256
|
-
//
|
|
344
|
+
// xterm instance
|
|
257
345
|
var panelXterm = new Terminal({
|
|
258
346
|
theme: {
|
|
259
347
|
background: '#0d1117', foreground: '#c9d1d9', cursor: '#3fb950',
|
|
@@ -268,13 +356,11 @@
|
|
|
268
356
|
panelXterm.loadAddon(panelFit);
|
|
269
357
|
panelXterm.open(termDiv);
|
|
270
358
|
|
|
271
|
-
//
|
|
272
|
-
|
|
359
|
+
// Store entry before async connect so index is stable
|
|
360
|
+
var entry = { xterm: panelXterm, fitAddon: panelFit, ws: null, session: s, panel: panel };
|
|
361
|
+
gridTerminals.push(entry);
|
|
273
362
|
|
|
274
363
|
// Connect WebSocket to this session
|
|
275
|
-
var statusDot = header.querySelector('.grid-panel-status');
|
|
276
|
-
var panelWs = null;
|
|
277
|
-
|
|
278
364
|
(function connectPanel() {
|
|
279
365
|
// Use hub's proxy endpoint to get a ticket for the session
|
|
280
366
|
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
@@ -288,7 +374,8 @@
|
|
|
288
374
|
if (!resp.ok) throw new Error('Auth failed');
|
|
289
375
|
return resp.json();
|
|
290
376
|
}).then(function(data) {
|
|
291
|
-
panelWs = new WebSocket(wsBase + '?ticket=' + encodeURIComponent(data.ticket));
|
|
377
|
+
var panelWs = new WebSocket(wsBase + '?ticket=' + encodeURIComponent(data.ticket));
|
|
378
|
+
entry.ws = panelWs;
|
|
292
379
|
|
|
293
380
|
panelWs.onopen = function() {
|
|
294
381
|
if (statusDot) { statusDot.style.color = 'var(--green)'; statusDot.title = 'Connected'; }
|
|
@@ -314,32 +401,183 @@
|
|
|
314
401
|
panelWs.send(JSON.stringify({ type: 'pty_input', data: data }));
|
|
315
402
|
}
|
|
316
403
|
});
|
|
317
|
-
|
|
318
|
-
gridTerminals.push({ xterm: panelXterm, fitAddon: panelFit, ws: panelWs, session: s });
|
|
319
404
|
}).catch(function() {
|
|
320
405
|
if (statusDot) { statusDot.style.color = 'var(--red)'; statusDot.title = 'Auth failed'; }
|
|
321
|
-
gridTerminals.push({ xterm: panelXterm, fitAddon: panelFit, ws: null, session: s });
|
|
322
406
|
});
|
|
323
407
|
})();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ── Event delegation for panel clicks ──
|
|
411
|
+
contentEl.addEventListener('click', function(e) {
|
|
412
|
+
var panel = e.target.closest('.grid-panel');
|
|
413
|
+
if (!panel) return;
|
|
414
|
+
var idx = parseInt(panel.dataset.index, 10);
|
|
415
|
+
if (isNaN(idx)) return;
|
|
416
|
+
|
|
417
|
+
if (gridMode === 'thumbnails') {
|
|
418
|
+
focusedIndex = idx;
|
|
419
|
+
switchGridMode('fullscreen');
|
|
420
|
+
} else if (gridMode === 'focus' && panel.classList.contains('focus-strip')) {
|
|
421
|
+
focusedIndex = idx;
|
|
422
|
+
applyGridLayout('focus');
|
|
423
|
+
} else if (gridMode === 'tmux') {
|
|
424
|
+
focusedIndex = idx;
|
|
425
|
+
contentEl.querySelectorAll('.grid-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
426
|
+
panel.classList.add('active');
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Apply initial layout
|
|
431
|
+
applyGridLayout(gridMode);
|
|
432
|
+
|
|
433
|
+
// Handle window resize
|
|
434
|
+
window.addEventListener('resize', fitGridPanels);
|
|
435
|
+
if ($('#btn-sessions')) { $('#btn-sessions').textContent = 'List'; }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function switchGridMode(mode) {
|
|
439
|
+
gridMode = mode;
|
|
440
|
+
if (mode === 'fullscreen') {
|
|
441
|
+
$('#input-area').classList.remove('hidden');
|
|
442
|
+
$('#input-form').classList.add('hidden');
|
|
443
|
+
} else {
|
|
444
|
+
$('#input-area').classList.add('hidden');
|
|
445
|
+
}
|
|
446
|
+
applyGridLayout(mode);
|
|
447
|
+
}
|
|
324
448
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
449
|
+
function switchTmuxPreset(preset) {
|
|
450
|
+
tmuxPreset = preset;
|
|
451
|
+
var presetGroup = document.getElementById('tmux-presets');
|
|
452
|
+
if (presetGroup) {
|
|
453
|
+
presetGroup.querySelectorAll('[data-preset]').forEach(function(btn) {
|
|
454
|
+
btn.classList.toggle('active', btn.dataset.preset === preset);
|
|
328
455
|
});
|
|
456
|
+
}
|
|
457
|
+
if (gridMode === 'tmux') applyGridLayout('tmux');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function applyGridLayout(mode) {
|
|
461
|
+
gridMode = mode;
|
|
462
|
+
var contentEl = document.getElementById('grid-content');
|
|
463
|
+
if (!contentEl || gridTerminals.length === 0) return;
|
|
329
464
|
|
|
330
|
-
|
|
465
|
+
// Clamp focusedIndex
|
|
466
|
+
if (focusedIndex >= gridTerminals.length) focusedIndex = 0;
|
|
467
|
+
|
|
468
|
+
// Update toolbar button states
|
|
469
|
+
var toolbar = contentEl.parentElement.querySelector('.grid-toolbar');
|
|
470
|
+
if (toolbar) {
|
|
471
|
+
toolbar.querySelectorAll('[data-mode]').forEach(function(btn) {
|
|
472
|
+
btn.classList.toggle('active', btn.dataset.mode === mode);
|
|
473
|
+
});
|
|
474
|
+
var presetsEl = document.getElementById('tmux-presets');
|
|
475
|
+
if (presetsEl) presetsEl.classList.toggle('hidden', mode !== 'tmux');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Detach all panels without destroying them
|
|
479
|
+
gridTerminals.forEach(function(gt, i) {
|
|
480
|
+
if (gt.panel.parentNode) gt.panel.parentNode.removeChild(gt.panel);
|
|
481
|
+
gt.panel.className = 'grid-panel';
|
|
482
|
+
gt.panel.dataset.index = i;
|
|
483
|
+
var termDiv = gt.panel.querySelector('.grid-panel-terminal');
|
|
484
|
+
if (termDiv) termDiv.style.cssText = '';
|
|
485
|
+
gt.panel.style.cssText = '';
|
|
331
486
|
});
|
|
332
487
|
|
|
333
|
-
//
|
|
334
|
-
|
|
488
|
+
// Remove leftover elements (focus-strips, back-to-grid button)
|
|
489
|
+
while (contentEl.firstChild) contentEl.removeChild(contentEl.firstChild);
|
|
490
|
+
|
|
491
|
+
// Reset content styles
|
|
492
|
+
contentEl.className = 'mode-' + mode;
|
|
493
|
+
contentEl.style.cssText = '';
|
|
494
|
+
|
|
495
|
+
switch (mode) {
|
|
496
|
+
case 'thumbnails':
|
|
497
|
+
gridTerminals.forEach(function(gt) {
|
|
498
|
+
gt.panel.classList.add('thumbnail');
|
|
499
|
+
var termDiv = gt.panel.querySelector('.grid-panel-terminal');
|
|
500
|
+
termDiv.style.width = '560px';
|
|
501
|
+
termDiv.style.height = '360px';
|
|
502
|
+
termDiv.style.transform = 'scale(0.5)';
|
|
503
|
+
termDiv.style.transformOrigin = 'top left';
|
|
504
|
+
contentEl.appendChild(gt.panel);
|
|
505
|
+
});
|
|
506
|
+
break;
|
|
335
507
|
|
|
336
|
-
|
|
337
|
-
|
|
508
|
+
case 'tmux':
|
|
509
|
+
gridTerminals.forEach(function(gt, i) {
|
|
510
|
+
if (i === focusedIndex) gt.panel.classList.add('active');
|
|
511
|
+
contentEl.appendChild(gt.panel);
|
|
512
|
+
});
|
|
513
|
+
if (tmuxPreset === 'equal') {
|
|
514
|
+
var cols = gridTerminals.length <= 2 ? gridTerminals.length : gridTerminals.length <= 4 ? 2 : 3;
|
|
515
|
+
contentEl.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
|
516
|
+
} else if (tmuxPreset === 'main-side') {
|
|
517
|
+
contentEl.style.gridTemplateColumns = '70% 30%';
|
|
518
|
+
var sideCount = Math.max(gridTerminals.length - 1, 1);
|
|
519
|
+
contentEl.style.gridTemplateRows = 'repeat(' + sideCount + ', 1fr)';
|
|
520
|
+
if (gridTerminals.length > 0) gridTerminals[0].panel.style.gridRow = '1 / -1';
|
|
521
|
+
} else if (tmuxPreset === 'stacked') {
|
|
522
|
+
contentEl.style.gridTemplateColumns = '1fr';
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
|
|
526
|
+
case 'focus':
|
|
527
|
+
var mainGt = gridTerminals[focusedIndex];
|
|
528
|
+
mainGt.panel.classList.add('focus-main');
|
|
529
|
+
contentEl.appendChild(mainGt.panel);
|
|
530
|
+
if (gridTerminals.length > 1) {
|
|
531
|
+
var stripsEl = document.createElement('div');
|
|
532
|
+
stripsEl.className = 'focus-strips';
|
|
533
|
+
gridTerminals.forEach(function(gt, i) {
|
|
534
|
+
if (i === focusedIndex) return;
|
|
535
|
+
gt.panel.classList.add('focus-strip');
|
|
536
|
+
stripsEl.appendChild(gt.panel);
|
|
537
|
+
});
|
|
538
|
+
contentEl.appendChild(stripsEl);
|
|
539
|
+
}
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case 'fullscreen':
|
|
543
|
+
var fullGt = gridTerminals[focusedIndex];
|
|
544
|
+
fullGt.panel.classList.add('fullscreen');
|
|
545
|
+
contentEl.appendChild(fullGt.panel);
|
|
546
|
+
var backBtn = document.createElement('button');
|
|
547
|
+
backBtn.className = 'back-to-grid';
|
|
548
|
+
backBtn.textContent = '\u2190 Grid';
|
|
549
|
+
backBtn.addEventListener('click', function() { switchGridMode('thumbnails'); });
|
|
550
|
+
contentEl.appendChild(backBtn);
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Fit visible terminals after DOM settles
|
|
555
|
+
setTimeout(function() {
|
|
556
|
+
gridTerminals.forEach(function(gt) {
|
|
557
|
+
if (!document.contains(gt.panel)) return;
|
|
558
|
+
if (gt.fitAddon) {
|
|
559
|
+
try {
|
|
560
|
+
gt.fitAddon.fit();
|
|
561
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN && gt.xterm) {
|
|
562
|
+
gt.ws.send(JSON.stringify({ type: 'pty_resize', cols: gt.xterm.cols, rows: gt.xterm.rows }));
|
|
563
|
+
}
|
|
564
|
+
} catch(e) {}
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}, 100);
|
|
338
568
|
}
|
|
339
569
|
|
|
340
570
|
function fitGridPanels() {
|
|
341
571
|
gridTerminals.forEach(function(gt) {
|
|
342
|
-
if (
|
|
572
|
+
if (!document.contains(gt.panel)) return;
|
|
573
|
+
if (gt.fitAddon) {
|
|
574
|
+
try {
|
|
575
|
+
gt.fitAddon.fit();
|
|
576
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN && gt.xterm) {
|
|
577
|
+
gt.ws.send(JSON.stringify({ type: 'pty_resize', cols: gt.xterm.cols, rows: gt.xterm.rows }));
|
|
578
|
+
}
|
|
579
|
+
} catch(e) {}
|
|
580
|
+
}
|
|
343
581
|
});
|
|
344
582
|
}
|
|
345
583
|
|
|
@@ -352,6 +590,10 @@
|
|
|
352
590
|
window.removeEventListener('resize', fitGridPanels);
|
|
353
591
|
var gridEl = document.getElementById('grid-view');
|
|
354
592
|
if (gridEl) { gridEl.innerHTML = ''; gridEl.classList.add('hidden'); }
|
|
593
|
+
$('#input-area').classList.add('hidden');
|
|
594
|
+
gridMode = 'thumbnails';
|
|
595
|
+
focusedIndex = 0;
|
|
596
|
+
tmuxPreset = 'equal';
|
|
355
597
|
}
|
|
356
598
|
|
|
357
599
|
window.toggleView = () => {
|
|
@@ -749,10 +991,18 @@
|
|
|
749
991
|
var btn = e.target;
|
|
750
992
|
if (btn && btn.tagName === 'BUTTON' && btn.dataset.key) {
|
|
751
993
|
var key = keyMap[btn.dataset.key] || btn.dataset.key;
|
|
752
|
-
if (
|
|
753
|
-
|
|
994
|
+
if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
|
|
995
|
+
var gt = gridTerminals[focusedIndex];
|
|
996
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN) {
|
|
997
|
+
gt.ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
998
|
+
}
|
|
999
|
+
if (gt.xterm) gt.xterm.focus();
|
|
1000
|
+
} else {
|
|
1001
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1002
|
+
ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
1003
|
+
}
|
|
1004
|
+
if (xterm) xterm.focus();
|
|
754
1005
|
}
|
|
755
|
-
if (xterm) xterm.focus();
|
|
756
1006
|
}
|
|
757
1007
|
});
|
|
758
1008
|
}
|
|
@@ -801,7 +1051,7 @@
|
|
|
801
1051
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
802
1052
|
}
|
|
803
1053
|
function escapeHtml(s) {
|
|
804
|
-
const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''');
|
|
1054
|
+
const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''').replace(/"/g, '"');
|
|
805
1055
|
}
|
|
806
1056
|
function formatText(text) {
|
|
807
1057
|
return escapeHtml(text)
|
package/remote-ui/styles.css
CHANGED
|
@@ -247,15 +247,143 @@ header {
|
|
|
247
247
|
.session-card .machine { color: var(--text-dim); font-size: 11px; }
|
|
248
248
|
.session-card .arrow { color: var(--text-dim); }
|
|
249
249
|
|
|
250
|
-
/* Grid View (
|
|
250
|
+
/* Grid View (multi-terminal with layout modes) */
|
|
251
251
|
#grid-view {
|
|
252
252
|
flex: 1;
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
overflow: hidden;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Grid toolbar */
|
|
259
|
+
.grid-toolbar {
|
|
260
|
+
display: flex;
|
|
261
|
+
gap: 4px;
|
|
262
|
+
padding: 4px 8px;
|
|
263
|
+
background: var(--bg-tool);
|
|
264
|
+
border-bottom: 1px solid var(--border);
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
}
|
|
267
|
+
.grid-toolbar button {
|
|
268
|
+
background: var(--bg);
|
|
269
|
+
border: 1px solid var(--border);
|
|
270
|
+
color: var(--text-dim);
|
|
271
|
+
font-family: var(--font);
|
|
272
|
+
font-size: 11px;
|
|
273
|
+
padding: 3px 8px;
|
|
274
|
+
border-radius: 4px;
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
}
|
|
277
|
+
.grid-toolbar button.active {
|
|
278
|
+
border-color: var(--blue);
|
|
279
|
+
color: var(--blue);
|
|
280
|
+
}
|
|
281
|
+
.grid-toolbar .spacer { flex: 1; }
|
|
282
|
+
.grid-toolbar-presets { display: flex; gap: 4px; margin-left: 8px; }
|
|
283
|
+
.grid-toolbar-presets.hidden { display: none; }
|
|
284
|
+
|
|
285
|
+
/* Grid content area */
|
|
286
|
+
#grid-content {
|
|
287
|
+
flex: 1;
|
|
288
|
+
overflow: hidden;
|
|
289
|
+
min-height: 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Thumbnail mode */
|
|
293
|
+
#grid-content.mode-thumbnails {
|
|
294
|
+
display: flex;
|
|
295
|
+
flex-wrap: wrap;
|
|
296
|
+
gap: 8px;
|
|
297
|
+
padding: 8px;
|
|
298
|
+
align-content: flex-start;
|
|
299
|
+
overflow-y: auto;
|
|
300
|
+
}
|
|
301
|
+
.grid-panel.thumbnail {
|
|
302
|
+
width: 280px;
|
|
303
|
+
height: 200px;
|
|
304
|
+
border: 1px solid var(--border);
|
|
305
|
+
border-radius: 6px;
|
|
306
|
+
cursor: pointer;
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
}
|
|
310
|
+
.grid-panel.thumbnail:hover {
|
|
311
|
+
border-color: var(--blue);
|
|
312
|
+
}
|
|
313
|
+
.grid-panel.thumbnail .grid-panel-terminal {
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Tmux mode */
|
|
318
|
+
#grid-content.mode-tmux {
|
|
253
319
|
display: grid;
|
|
254
320
|
gap: 2px;
|
|
255
|
-
padding: 2px;
|
|
256
|
-
overflow: hidden;
|
|
257
321
|
background: var(--border);
|
|
258
322
|
}
|
|
323
|
+
.grid-panel.active {
|
|
324
|
+
outline: 2px solid var(--blue);
|
|
325
|
+
outline-offset: -2px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* Focus mode */
|
|
329
|
+
#grid-content.mode-focus {
|
|
330
|
+
display: flex;
|
|
331
|
+
flex-direction: column;
|
|
332
|
+
}
|
|
333
|
+
.grid-panel.focus-main {
|
|
334
|
+
flex: 1;
|
|
335
|
+
min-height: 0;
|
|
336
|
+
}
|
|
337
|
+
.grid-panel.focus-main .grid-panel-header {
|
|
338
|
+
background: var(--blue);
|
|
339
|
+
}
|
|
340
|
+
.grid-panel.focus-main .grid-panel-name {
|
|
341
|
+
color: #fff;
|
|
342
|
+
}
|
|
343
|
+
.focus-strips {
|
|
344
|
+
display: flex;
|
|
345
|
+
flex-shrink: 0;
|
|
346
|
+
overflow-x: auto;
|
|
347
|
+
}
|
|
348
|
+
.grid-panel.focus-strip {
|
|
349
|
+
height: 80px;
|
|
350
|
+
cursor: pointer;
|
|
351
|
+
border-top: 1px solid var(--border);
|
|
352
|
+
flex: 1;
|
|
353
|
+
min-width: 120px;
|
|
354
|
+
}
|
|
355
|
+
.grid-panel.focus-strip:hover {
|
|
356
|
+
background: var(--bg-tool);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Fullscreen mode */
|
|
360
|
+
#grid-content.mode-fullscreen {
|
|
361
|
+
display: flex;
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
position: relative;
|
|
364
|
+
}
|
|
365
|
+
.grid-panel.fullscreen {
|
|
366
|
+
flex: 1;
|
|
367
|
+
min-height: 0;
|
|
368
|
+
}
|
|
369
|
+
.back-to-grid {
|
|
370
|
+
position: absolute;
|
|
371
|
+
top: 4px;
|
|
372
|
+
right: 8px;
|
|
373
|
+
background: var(--bg-tool);
|
|
374
|
+
border: 1px solid var(--border);
|
|
375
|
+
color: var(--text-dim);
|
|
376
|
+
font-family: var(--font);
|
|
377
|
+
font-size: 11px;
|
|
378
|
+
padding: 3px 8px;
|
|
379
|
+
border-radius: 4px;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
z-index: 10;
|
|
382
|
+
}
|
|
383
|
+
.back-to-grid:hover {
|
|
384
|
+
border-color: var(--blue);
|
|
385
|
+
color: var(--blue);
|
|
386
|
+
}
|
|
259
387
|
.grid-panel {
|
|
260
388
|
display: flex;
|
|
261
389
|
flex-direction: column;
|