cli-tunnel 1.2.0-beta.9 → 1.2.1-beta.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 +98 -40
- package/dist/index.js +172 -60
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +26 -0
- package/package.json +7 -5
- package/remote-ui/app.js +388 -77
- package/remote-ui/index.html +10 -10
- package/remote-ui/styles.css +131 -3
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;
|
|
@@ -90,15 +93,24 @@
|
|
|
90
93
|
fitAddon.fit();
|
|
91
94
|
|
|
92
95
|
// Send terminal size to PTY so copilot renders correctly
|
|
96
|
+
var lastCols = 0, lastRows = 0;
|
|
97
|
+
var resizeTimer = null;
|
|
93
98
|
function sendResize() {
|
|
94
99
|
if (ws && ws.readyState === WebSocket.OPEN && xterm) {
|
|
95
|
-
|
|
100
|
+
if (xterm.cols !== lastCols || xterm.rows !== lastRows) {
|
|
101
|
+
lastCols = xterm.cols;
|
|
102
|
+
lastRows = xterm.rows;
|
|
103
|
+
ws.send(JSON.stringify({ type: 'pty_resize', cols: xterm.cols, rows: xterm.rows }));
|
|
104
|
+
}
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
|
|
99
|
-
// Handle resize
|
|
108
|
+
// Handle resize — debounced to avoid rapid PTY resizes (mobile keyboard, URL bar, etc.)
|
|
100
109
|
window.addEventListener('resize', () => {
|
|
101
|
-
if (
|
|
110
|
+
if (resizeTimer) clearTimeout(resizeTimer);
|
|
111
|
+
resizeTimer = setTimeout(() => {
|
|
112
|
+
if (fitAddon) { fitAddon.fit(); sendResize(); }
|
|
113
|
+
}, 150);
|
|
102
114
|
});
|
|
103
115
|
|
|
104
116
|
// Send initial size
|
|
@@ -117,7 +129,9 @@
|
|
|
117
129
|
|
|
118
130
|
async function loadSessions() {
|
|
119
131
|
try {
|
|
120
|
-
const
|
|
132
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
133
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
134
|
+
const resp = await fetch('/api/sessions', { headers });
|
|
121
135
|
const data = await resp.json();
|
|
122
136
|
renderDashboard(data.sessions || []);
|
|
123
137
|
} catch (err) {
|
|
@@ -146,17 +160,17 @@
|
|
|
146
160
|
'</div>';
|
|
147
161
|
} else {
|
|
148
162
|
html += filtered.map(s => {
|
|
149
|
-
const
|
|
163
|
+
const hasAccess = s.hasToken;
|
|
150
164
|
return `
|
|
151
|
-
<div class="session-card" ${s.online &&
|
|
165
|
+
<div class="session-card" ${s.online && hasAccess ? 'data-session-port="' + s.port + '" data-session-base-url="' + escapeHtml(s.url) + '"' : ''}>
|
|
152
166
|
<span class="status-dot ${s.online ? 'online' : 'offline'}"></span>
|
|
153
167
|
<div class="info">
|
|
154
168
|
<div class="session-name">${escapeHtml(s.name)}</div>
|
|
155
169
|
<div class="repo">📦 ${escapeHtml(s.repo)}</div>
|
|
156
170
|
<div class="branch">🌿 ${escapeHtml(s.branch)}</div>
|
|
157
|
-
<div class="machine">💻 ${escapeHtml(s.machine)}${!
|
|
171
|
+
<div class="machine">💻 ${escapeHtml(s.machine)}${!hasAccess && s.online ? ' 🔒' : ''}</div>
|
|
158
172
|
</div>
|
|
159
|
-
${s.online &&
|
|
173
|
+
${s.online && hasAccess ? '<span class="arrow">→</span>' :
|
|
160
174
|
!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>'
|
|
161
175
|
: '<span style="color:var(--text-dim);font-size:11px">remote</span>'}
|
|
162
176
|
</div>`;
|
|
@@ -165,8 +179,25 @@
|
|
|
165
179
|
dashboard.innerHTML = html;
|
|
166
180
|
cachedSessions = sessions;
|
|
167
181
|
// Event delegation
|
|
168
|
-
dashboard.querySelectorAll('.session-card[data-session-
|
|
169
|
-
card.addEventListener('click', function() {
|
|
182
|
+
dashboard.querySelectorAll('.session-card[data-session-port]').forEach(function(card) {
|
|
183
|
+
card.addEventListener('click', function() {
|
|
184
|
+
var port = card.dataset.sessionPort;
|
|
185
|
+
var baseUrl = card.dataset.sessionBaseUrl;
|
|
186
|
+
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
187
|
+
var proxyUrl = '/api/proxy/ticket/' + port;
|
|
188
|
+
fetch(proxyUrl, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {}
|
|
191
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
192
|
+
if (data.ticket) {
|
|
193
|
+
window.location.href = baseUrl + '?ticket=' + encodeURIComponent(data.ticket);
|
|
194
|
+
} else {
|
|
195
|
+
window.location.href = baseUrl;
|
|
196
|
+
}
|
|
197
|
+
}).catch(function() {
|
|
198
|
+
window.location.href = baseUrl;
|
|
199
|
+
});
|
|
200
|
+
});
|
|
170
201
|
});
|
|
171
202
|
dashboard.querySelectorAll('[data-delete-id]').forEach(function(btn) {
|
|
172
203
|
btn.addEventListener('click', function(e) { e.stopPropagation(); deleteSession(btn.dataset.deleteId); });
|
|
@@ -187,29 +218,36 @@
|
|
|
187
218
|
};
|
|
188
219
|
|
|
189
220
|
window.cleanOffline = async () => {
|
|
190
|
-
const
|
|
221
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
222
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
223
|
+
const resp = await fetch('/api/sessions', { headers });
|
|
191
224
|
const data = await resp.json();
|
|
192
225
|
const offline = (data.sessions || []).filter(s => !s.online);
|
|
193
226
|
for (const s of offline) {
|
|
194
|
-
await fetch('/api/sessions/' + s.id, { method: 'DELETE' });
|
|
227
|
+
await fetch('/api/sessions/' + s.id, { method: 'DELETE', headers });
|
|
195
228
|
}
|
|
196
229
|
loadSessions();
|
|
197
230
|
};
|
|
198
231
|
|
|
199
232
|
window.deleteSession = async (id) => {
|
|
200
|
-
|
|
233
|
+
const tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
234
|
+
const headers = tokenParam ? { 'Authorization': 'Bearer ' + tokenParam } : {};
|
|
235
|
+
await fetch('/api/sessions/' + id, { method: 'DELETE', headers });
|
|
201
236
|
loadSessions();
|
|
202
237
|
};
|
|
203
238
|
|
|
204
|
-
// ─── Grid View (
|
|
239
|
+
// ─── Grid View (multi-terminal with layout modes) ───────────
|
|
205
240
|
function showGridView(sessions) {
|
|
206
|
-
|
|
241
|
+
var connectable = sessions.filter(function(s) { return s.online && s.token; });
|
|
207
242
|
if (connectable.length === 0) return;
|
|
208
243
|
|
|
209
244
|
// Clean up previous grid
|
|
210
245
|
destroyGrid();
|
|
211
246
|
|
|
212
247
|
currentView = 'grid';
|
|
248
|
+
gridMode = 'thumbnails';
|
|
249
|
+
focusedIndex = 0;
|
|
250
|
+
tmuxPreset = 'equal';
|
|
213
251
|
dashboard.classList.add('hidden');
|
|
214
252
|
terminal.classList.add('hidden');
|
|
215
253
|
termContainer.classList.add('hidden');
|
|
@@ -224,30 +262,95 @@
|
|
|
224
262
|
gridEl.classList.remove('hidden');
|
|
225
263
|
gridEl.innerHTML = '';
|
|
226
264
|
|
|
227
|
-
//
|
|
228
|
-
var
|
|
229
|
-
|
|
265
|
+
// ── Toolbar ──
|
|
266
|
+
var toolbar = document.createElement('div');
|
|
267
|
+
toolbar.className = 'grid-toolbar';
|
|
268
|
+
|
|
269
|
+
var modes = [
|
|
270
|
+
{ id: 'thumbnails', label: '\u229E Tiles' },
|
|
271
|
+
{ id: 'tmux', label: '\u229F Tmux' },
|
|
272
|
+
{ id: 'focus', label: '\u25C9 Focus' },
|
|
273
|
+
{ id: 'fullscreen', label: '\u2A21 Full' }
|
|
274
|
+
];
|
|
275
|
+
modes.forEach(function(m) {
|
|
276
|
+
var btn = document.createElement('button');
|
|
277
|
+
btn.textContent = m.label;
|
|
278
|
+
btn.dataset.mode = m.id;
|
|
279
|
+
if (m.id === gridMode) btn.classList.add('active');
|
|
280
|
+
btn.addEventListener('click', function() { switchGridMode(m.id); });
|
|
281
|
+
toolbar.appendChild(btn);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Tmux preset buttons (visible only in tmux mode)
|
|
285
|
+
var presetGroup = document.createElement('span');
|
|
286
|
+
presetGroup.className = 'grid-toolbar-presets hidden';
|
|
287
|
+
presetGroup.id = 'tmux-presets';
|
|
288
|
+
var presets = [
|
|
289
|
+
{ id: 'equal', label: '\u2550 Equal' },
|
|
290
|
+
{ id: 'main-side', label: '\u2590 Main+Side' },
|
|
291
|
+
{ id: 'stacked', label: '\u2261 Stacked' }
|
|
292
|
+
];
|
|
293
|
+
presets.forEach(function(p) {
|
|
294
|
+
var btn = document.createElement('button');
|
|
295
|
+
btn.textContent = p.label;
|
|
296
|
+
btn.dataset.preset = p.id;
|
|
297
|
+
if (p.id === tmuxPreset) btn.classList.add('active');
|
|
298
|
+
btn.addEventListener('click', function() { switchTmuxPreset(p.id); });
|
|
299
|
+
presetGroup.appendChild(btn);
|
|
300
|
+
});
|
|
301
|
+
toolbar.appendChild(presetGroup);
|
|
302
|
+
|
|
303
|
+
var spacer = document.createElement('span');
|
|
304
|
+
spacer.className = 'spacer';
|
|
305
|
+
toolbar.appendChild(spacer);
|
|
306
|
+
|
|
307
|
+
var listBtn = document.createElement('button');
|
|
308
|
+
listBtn.textContent = '\u2190 List';
|
|
309
|
+
listBtn.addEventListener('click', function() {
|
|
310
|
+
destroyGrid();
|
|
311
|
+
currentView = 'dashboard';
|
|
312
|
+
dashboard.classList.remove('hidden');
|
|
313
|
+
if ($('#btn-sessions')) $('#btn-sessions').textContent = 'Terminal';
|
|
314
|
+
loadSessions();
|
|
315
|
+
});
|
|
316
|
+
toolbar.appendChild(listBtn);
|
|
317
|
+
gridEl.appendChild(toolbar);
|
|
318
|
+
|
|
319
|
+
// ── Content container ──
|
|
320
|
+
var contentEl = document.createElement('div');
|
|
321
|
+
contentEl.id = 'grid-content';
|
|
322
|
+
gridEl.appendChild(contentEl);
|
|
230
323
|
|
|
231
|
-
|
|
324
|
+
// ── Create panels & connect ──
|
|
325
|
+
connectable.forEach(function(s, index) {
|
|
232
326
|
var panel = document.createElement('div');
|
|
233
327
|
panel.className = 'grid-panel';
|
|
328
|
+
panel.dataset.index = index;
|
|
234
329
|
|
|
235
|
-
// Header
|
|
236
330
|
var header = document.createElement('div');
|
|
237
331
|
header.className = 'grid-panel-header';
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
332
|
+
var nameSpan = document.createElement('span');
|
|
333
|
+
nameSpan.className = 'grid-panel-name';
|
|
334
|
+
nameSpan.textContent = s.name;
|
|
335
|
+
var machineSpan = document.createElement('span');
|
|
336
|
+
machineSpan.className = 'grid-panel-machine';
|
|
337
|
+
machineSpan.textContent = s.machine;
|
|
338
|
+
var statusDot = document.createElement('span');
|
|
339
|
+
statusDot.className = 'grid-panel-status';
|
|
340
|
+
statusDot.textContent = '\u25CF';
|
|
341
|
+
header.appendChild(nameSpan);
|
|
342
|
+
header.appendChild(machineSpan);
|
|
343
|
+
header.appendChild(statusDot);
|
|
241
344
|
panel.appendChild(header);
|
|
242
345
|
|
|
243
|
-
// Terminal container
|
|
244
346
|
var termDiv = document.createElement('div');
|
|
245
347
|
termDiv.className = 'grid-panel-terminal';
|
|
246
348
|
panel.appendChild(termDiv);
|
|
247
349
|
|
|
248
|
-
|
|
350
|
+
// Append to contentEl so xterm.open has a DOM-attached container
|
|
351
|
+
contentEl.appendChild(panel);
|
|
249
352
|
|
|
250
|
-
//
|
|
353
|
+
// xterm instance
|
|
251
354
|
var panelXterm = new Terminal({
|
|
252
355
|
theme: {
|
|
253
356
|
background: '#0d1117', foreground: '#c9d1d9', cursor: '#3fb950',
|
|
@@ -262,61 +365,237 @@
|
|
|
262
365
|
panelXterm.loadAddon(panelFit);
|
|
263
366
|
panelXterm.open(termDiv);
|
|
264
367
|
|
|
265
|
-
//
|
|
266
|
-
|
|
368
|
+
// Store entry before async connect so index is stable
|
|
369
|
+
var entry = { xterm: panelXterm, fitAddon: panelFit, ws: null, session: s, panel: panel };
|
|
370
|
+
gridTerminals.push(entry);
|
|
267
371
|
|
|
268
372
|
// Connect WebSocket to this session
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
373
|
+
(function connectPanel() {
|
|
374
|
+
// Use hub's proxy endpoint to get a ticket for the session
|
|
375
|
+
var tokenParam = new URLSearchParams(window.location.search).get('token');
|
|
376
|
+
var proxyUrl = '/api/proxy/ticket/' + s.port;
|
|
377
|
+
var wsBase = s.isLocal ? 'ws://127.0.0.1:' + s.port : s.url.replace('https://', 'wss://');
|
|
378
|
+
|
|
379
|
+
fetch(proxyUrl, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: { 'Authorization': 'Bearer ' + tokenParam }
|
|
382
|
+
}).then(function(resp) {
|
|
383
|
+
if (!resp.ok) throw new Error('Auth failed');
|
|
384
|
+
return resp.json();
|
|
385
|
+
}).then(function(data) {
|
|
386
|
+
var panelWs = new WebSocket(wsBase + '?ticket=' + encodeURIComponent(data.ticket));
|
|
387
|
+
entry.ws = panelWs;
|
|
388
|
+
|
|
389
|
+
panelWs.onopen = function() {
|
|
390
|
+
if (statusDot) { statusDot.style.color = 'var(--green)'; statusDot.title = 'Connected'; }
|
|
391
|
+
panelWs.send(JSON.stringify({ type: 'pty_resize', cols: panelXterm.cols, rows: panelXterm.rows }));
|
|
392
|
+
};
|
|
393
|
+
panelWs.onclose = function() {
|
|
394
|
+
if (statusDot) { statusDot.style.color = 'var(--red)'; statusDot.title = 'Disconnected'; }
|
|
395
|
+
};
|
|
396
|
+
panelWs.onerror = function() {
|
|
397
|
+
if (statusDot) { statusDot.style.color = 'var(--red)'; }
|
|
398
|
+
};
|
|
399
|
+
panelWs.onmessage = function(e) {
|
|
400
|
+
try {
|
|
401
|
+
var msg = JSON.parse(e.data);
|
|
402
|
+
if (msg.type === 'pty') {
|
|
403
|
+
panelXterm.write(msg.data);
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
panelXterm.onData(function(data) {
|
|
409
|
+
if (panelWs && panelWs.readyState === WebSocket.OPEN) {
|
|
410
|
+
panelWs.send(JSON.stringify({ type: 'pty_input', data: data }));
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}).catch(function() {
|
|
414
|
+
if (statusDot) { statusDot.style.color = 'var(--red)'; statusDot.title = 'Auth failed'; }
|
|
415
|
+
});
|
|
416
|
+
})();
|
|
417
|
+
});
|
|
293
418
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
419
|
+
// ── Event delegation for panel clicks ──
|
|
420
|
+
contentEl.addEventListener('click', function(e) {
|
|
421
|
+
var panel = e.target.closest('.grid-panel');
|
|
422
|
+
if (!panel) return;
|
|
423
|
+
var idx = parseInt(panel.dataset.index, 10);
|
|
424
|
+
if (isNaN(idx)) return;
|
|
425
|
+
|
|
426
|
+
if (gridMode === 'thumbnails') {
|
|
427
|
+
focusedIndex = idx;
|
|
428
|
+
switchGridMode('fullscreen');
|
|
429
|
+
} else if (gridMode === 'focus' && panel.classList.contains('focus-strip')) {
|
|
430
|
+
focusedIndex = idx;
|
|
431
|
+
applyGridLayout('focus');
|
|
432
|
+
} else if (gridMode === 'tmux') {
|
|
433
|
+
focusedIndex = idx;
|
|
434
|
+
contentEl.querySelectorAll('.grid-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
435
|
+
panel.classList.add('active');
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Apply initial layout
|
|
440
|
+
applyGridLayout(gridMode);
|
|
441
|
+
|
|
442
|
+
// Handle window resize
|
|
443
|
+
window.removeEventListener('resize', fitGridPanels);
|
|
444
|
+
window.addEventListener('resize', fitGridPanels);
|
|
445
|
+
if ($('#btn-sessions')) { $('#btn-sessions').textContent = 'List'; }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function switchGridMode(mode) {
|
|
449
|
+
gridMode = mode;
|
|
450
|
+
if (mode === 'fullscreen') {
|
|
451
|
+
$('#input-area').classList.remove('hidden');
|
|
452
|
+
$('#input-form').classList.add('hidden');
|
|
453
|
+
} else {
|
|
454
|
+
$('#input-area').classList.add('hidden');
|
|
455
|
+
}
|
|
456
|
+
applyGridLayout(mode);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function switchTmuxPreset(preset) {
|
|
460
|
+
tmuxPreset = preset;
|
|
461
|
+
var presetGroup = document.getElementById('tmux-presets');
|
|
462
|
+
if (presetGroup) {
|
|
463
|
+
presetGroup.querySelectorAll('[data-preset]').forEach(function(btn) {
|
|
464
|
+
btn.classList.toggle('active', btn.dataset.preset === preset);
|
|
299
465
|
});
|
|
466
|
+
}
|
|
467
|
+
if (gridMode === 'tmux') applyGridLayout('tmux');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function applyGridLayout(mode) {
|
|
471
|
+
gridMode = mode;
|
|
472
|
+
var contentEl = document.getElementById('grid-content');
|
|
473
|
+
if (!contentEl || gridTerminals.length === 0) return;
|
|
474
|
+
|
|
475
|
+
// Clamp focusedIndex
|
|
476
|
+
if (focusedIndex >= gridTerminals.length) focusedIndex = 0;
|
|
300
477
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
478
|
+
// Update toolbar button states
|
|
479
|
+
var toolbar = contentEl.parentElement.querySelector('.grid-toolbar');
|
|
480
|
+
if (toolbar) {
|
|
481
|
+
toolbar.querySelectorAll('[data-mode]').forEach(function(btn) {
|
|
482
|
+
btn.classList.toggle('active', btn.dataset.mode === mode);
|
|
304
483
|
});
|
|
484
|
+
var presetsEl = document.getElementById('tmux-presets');
|
|
485
|
+
if (presetsEl) presetsEl.classList.toggle('hidden', mode !== 'tmux');
|
|
486
|
+
}
|
|
305
487
|
|
|
306
|
-
|
|
488
|
+
// Detach all panels without destroying them
|
|
489
|
+
gridTerminals.forEach(function(gt, i) {
|
|
490
|
+
if (gt.panel.parentNode) gt.panel.parentNode.removeChild(gt.panel);
|
|
491
|
+
gt.panel.className = 'grid-panel';
|
|
492
|
+
gt.panel.dataset.index = i;
|
|
493
|
+
var termDiv = gt.panel.querySelector('.grid-panel-terminal');
|
|
494
|
+
if (termDiv) termDiv.style.cssText = '';
|
|
495
|
+
gt.panel.style.cssText = '';
|
|
307
496
|
});
|
|
308
497
|
|
|
309
|
-
//
|
|
310
|
-
|
|
498
|
+
// Remove leftover elements (focus-strips, back-to-grid button)
|
|
499
|
+
while (contentEl.firstChild) contentEl.removeChild(contentEl.firstChild);
|
|
500
|
+
|
|
501
|
+
// Reset content styles
|
|
502
|
+
contentEl.className = 'mode-' + mode;
|
|
503
|
+
contentEl.style.cssText = '';
|
|
504
|
+
|
|
505
|
+
switch (mode) {
|
|
506
|
+
case 'thumbnails':
|
|
507
|
+
gridTerminals.forEach(function(gt) {
|
|
508
|
+
gt.panel.classList.add('thumbnail');
|
|
509
|
+
var termDiv = gt.panel.querySelector('.grid-panel-terminal');
|
|
510
|
+
termDiv.style.width = '560px';
|
|
511
|
+
termDiv.style.height = '360px';
|
|
512
|
+
termDiv.style.transform = 'scale(0.5)';
|
|
513
|
+
termDiv.style.transformOrigin = 'top left';
|
|
514
|
+
contentEl.appendChild(gt.panel);
|
|
515
|
+
});
|
|
516
|
+
break;
|
|
517
|
+
|
|
518
|
+
case 'tmux':
|
|
519
|
+
gridTerminals.forEach(function(gt, i) {
|
|
520
|
+
if (i === focusedIndex) gt.panel.classList.add('active');
|
|
521
|
+
contentEl.appendChild(gt.panel);
|
|
522
|
+
});
|
|
523
|
+
if (tmuxPreset === 'equal') {
|
|
524
|
+
var cols = gridTerminals.length <= 2 ? gridTerminals.length : gridTerminals.length <= 4 ? 2 : 3;
|
|
525
|
+
contentEl.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
|
526
|
+
} else if (tmuxPreset === 'main-side') {
|
|
527
|
+
contentEl.style.gridTemplateColumns = '70% 30%';
|
|
528
|
+
var sideCount = Math.max(gridTerminals.length - 1, 1);
|
|
529
|
+
contentEl.style.gridTemplateRows = 'repeat(' + sideCount + ', 1fr)';
|
|
530
|
+
if (gridTerminals.length > 0) gridTerminals[0].panel.style.gridRow = '1 / -1';
|
|
531
|
+
} else if (tmuxPreset === 'stacked') {
|
|
532
|
+
contentEl.style.gridTemplateColumns = '1fr';
|
|
533
|
+
}
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case 'focus':
|
|
537
|
+
var mainGt = gridTerminals[focusedIndex];
|
|
538
|
+
mainGt.panel.classList.add('focus-main');
|
|
539
|
+
contentEl.appendChild(mainGt.panel);
|
|
540
|
+
if (gridTerminals.length > 1) {
|
|
541
|
+
var stripsEl = document.createElement('div');
|
|
542
|
+
stripsEl.className = 'focus-strips';
|
|
543
|
+
gridTerminals.forEach(function(gt, i) {
|
|
544
|
+
if (i === focusedIndex) return;
|
|
545
|
+
gt.panel.classList.add('focus-strip');
|
|
546
|
+
stripsEl.appendChild(gt.panel);
|
|
547
|
+
});
|
|
548
|
+
contentEl.appendChild(stripsEl);
|
|
549
|
+
}
|
|
550
|
+
break;
|
|
551
|
+
|
|
552
|
+
case 'fullscreen':
|
|
553
|
+
var fullGt = gridTerminals[focusedIndex];
|
|
554
|
+
fullGt.panel.classList.add('fullscreen');
|
|
555
|
+
contentEl.appendChild(fullGt.panel);
|
|
556
|
+
var backBtn = document.createElement('button');
|
|
557
|
+
backBtn.className = 'back-to-grid';
|
|
558
|
+
backBtn.textContent = '\u2190 Grid';
|
|
559
|
+
backBtn.addEventListener('click', function() { switchGridMode('thumbnails'); });
|
|
560
|
+
contentEl.appendChild(backBtn);
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
311
563
|
|
|
312
|
-
//
|
|
313
|
-
|
|
564
|
+
// Fit visible terminals after DOM settles
|
|
565
|
+
setTimeout(function() {
|
|
566
|
+
gridTerminals.forEach(function(gt) {
|
|
567
|
+
if (!document.contains(gt.panel)) return;
|
|
568
|
+
if (gt.fitAddon) {
|
|
569
|
+
try {
|
|
570
|
+
gt.fitAddon.fit();
|
|
571
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN && gt.xterm) {
|
|
572
|
+
gt.ws.send(JSON.stringify({ type: 'pty_resize', cols: gt.xterm.cols, rows: gt.xterm.rows }));
|
|
573
|
+
}
|
|
574
|
+
} catch(e) {}
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}, 100);
|
|
314
578
|
}
|
|
315
579
|
|
|
580
|
+
var gridResizeTimer = null;
|
|
316
581
|
function fitGridPanels() {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
582
|
+
if (gridResizeTimer) clearTimeout(gridResizeTimer);
|
|
583
|
+
gridResizeTimer = setTimeout(function() {
|
|
584
|
+
gridTerminals.forEach(function(gt) {
|
|
585
|
+
if (!document.contains(gt.panel)) return;
|
|
586
|
+
if (gt.fitAddon) {
|
|
587
|
+
try {
|
|
588
|
+
var prevCols = gt.xterm ? gt.xterm.cols : 0;
|
|
589
|
+
var prevRows = gt.xterm ? gt.xterm.rows : 0;
|
|
590
|
+
gt.fitAddon.fit();
|
|
591
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN && gt.xterm &&
|
|
592
|
+
(gt.xterm.cols !== prevCols || gt.xterm.rows !== prevRows)) {
|
|
593
|
+
gt.ws.send(JSON.stringify({ type: 'pty_resize', cols: gt.xterm.cols, rows: gt.xterm.rows }));
|
|
594
|
+
}
|
|
595
|
+
} catch(e) {}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}, 150);
|
|
320
599
|
}
|
|
321
600
|
|
|
322
601
|
function destroyGrid() {
|
|
@@ -328,6 +607,10 @@
|
|
|
328
607
|
window.removeEventListener('resize', fitGridPanels);
|
|
329
608
|
var gridEl = document.getElementById('grid-view');
|
|
330
609
|
if (gridEl) { gridEl.innerHTML = ''; gridEl.classList.add('hidden'); }
|
|
610
|
+
$('#input-area').classList.add('hidden');
|
|
611
|
+
gridMode = 'thumbnails';
|
|
612
|
+
focusedIndex = 0;
|
|
613
|
+
tmuxPreset = 'equal';
|
|
331
614
|
}
|
|
332
615
|
|
|
333
616
|
window.toggleView = () => {
|
|
@@ -519,7 +802,7 @@
|
|
|
519
802
|
}
|
|
520
803
|
|
|
521
804
|
// ─── Detect hub mode (no token in URL) ────────────────────
|
|
522
|
-
const isHubMode =
|
|
805
|
+
const isHubMode = new URLSearchParams(window.location.search).get('hub') === '1';
|
|
523
806
|
|
|
524
807
|
// ─── WebSocket ───────────────────────────────────────────
|
|
525
808
|
let reconnectAttempt = 0;
|
|
@@ -544,7 +827,7 @@
|
|
|
544
827
|
|
|
545
828
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
546
829
|
|
|
547
|
-
// F-02:
|
|
830
|
+
// F-02: Ticket-based auth (required)
|
|
548
831
|
try {
|
|
549
832
|
const resp = await fetch('/api/auth/ticket', {
|
|
550
833
|
method: 'POST',
|
|
@@ -554,19 +837,20 @@
|
|
|
554
837
|
const { ticket } = await resp.json();
|
|
555
838
|
ws = new WebSocket(`${proto}//${location.host}?ticket=${encodeURIComponent(ticket)}`);
|
|
556
839
|
} else {
|
|
557
|
-
|
|
558
|
-
|
|
840
|
+
setStatus('offline', 'Auth failed');
|
|
841
|
+
return;
|
|
559
842
|
}
|
|
560
843
|
} catch {
|
|
561
|
-
|
|
562
|
-
|
|
844
|
+
setStatus('offline', 'Auth failed');
|
|
845
|
+
return;
|
|
563
846
|
}
|
|
564
847
|
setStatus('connecting', 'Connecting...');
|
|
565
848
|
|
|
566
849
|
ws.onopen = () => {
|
|
567
850
|
connected = true;
|
|
568
851
|
reconnectAttempt = 0;
|
|
569
|
-
|
|
852
|
+
setStatus('online', 'Connected');
|
|
853
|
+
// PTY mode: server sends pty data immediately, no ACP handshake needed
|
|
570
854
|
};
|
|
571
855
|
ws.onclose = () => {
|
|
572
856
|
connected = false; acpReady = false; sessionId = null;
|
|
@@ -714,6 +998,33 @@
|
|
|
714
998
|
};
|
|
715
999
|
|
|
716
1000
|
// ─── Mobile Key Bar ───────────────────────────────────────
|
|
1001
|
+
// F-5: Event delegation for key-bar buttons (no inline onclick)
|
|
1002
|
+
const keyBar = document.getElementById('key-bar');
|
|
1003
|
+
if (keyBar) {
|
|
1004
|
+
var keyMap = {
|
|
1005
|
+
'\\x1b[A': '\x1b[A', '\\x1b[B': '\x1b[B', '\\x1b[C': '\x1b[C', '\\x1b[D': '\x1b[D',
|
|
1006
|
+
'\\t': '\t', '\\r': '\r', '\\x1b': '\x1b', '\\x03': '\x03', ' ': ' ', '\\x7f': '\x7f',
|
|
1007
|
+
};
|
|
1008
|
+
keyBar.addEventListener('click', function(e) {
|
|
1009
|
+
var btn = e.target;
|
|
1010
|
+
if (btn && btn.tagName === 'BUTTON' && btn.dataset.key) {
|
|
1011
|
+
var key = keyMap[btn.dataset.key] || btn.dataset.key;
|
|
1012
|
+
if (currentView === 'grid' && gridMode === 'fullscreen' && gridTerminals[focusedIndex]) {
|
|
1013
|
+
var gt = gridTerminals[focusedIndex];
|
|
1014
|
+
if (gt.ws && gt.ws.readyState === WebSocket.OPEN) {
|
|
1015
|
+
gt.ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
1016
|
+
}
|
|
1017
|
+
if (gt.xterm) gt.xterm.focus();
|
|
1018
|
+
} else {
|
|
1019
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1020
|
+
ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
1021
|
+
}
|
|
1022
|
+
if (xterm) xterm.focus();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
|
|
717
1028
|
window.sendKey = (key) => {
|
|
718
1029
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
719
1030
|
ws.send(JSON.stringify({ type: 'pty_input', data: key }));
|
|
@@ -758,7 +1069,7 @@
|
|
|
758
1069
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
759
1070
|
}
|
|
760
1071
|
function escapeHtml(s) {
|
|
761
|
-
const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''');
|
|
1072
|
+
const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML.replace(/'/g, ''').replace(/"/g, '"');
|
|
762
1073
|
}
|
|
763
1074
|
function formatText(text) {
|
|
764
1075
|
return escapeHtml(text)
|
package/remote-ui/index.html
CHANGED
|
@@ -32,16 +32,16 @@
|
|
|
32
32
|
|
|
33
33
|
<footer id="input-area">
|
|
34
34
|
<div id="key-bar">
|
|
35
|
-
<button
|
|
36
|
-
<button
|
|
37
|
-
<button
|
|
38
|
-
<button
|
|
39
|
-
<button
|
|
40
|
-
<button
|
|
41
|
-
<button
|
|
42
|
-
<button
|
|
43
|
-
<button
|
|
44
|
-
<button
|
|
35
|
+
<button data-key="\x1b[A">↑</button>
|
|
36
|
+
<button data-key="\x1b[B">↓</button>
|
|
37
|
+
<button data-key="\x1b[C">→</button>
|
|
38
|
+
<button data-key="\x1b[D">←</button>
|
|
39
|
+
<button data-key="\t">Tab</button>
|
|
40
|
+
<button data-key="\r">Enter</button>
|
|
41
|
+
<button data-key="\x1b">Esc</button>
|
|
42
|
+
<button data-key="\x03">Ctrl+C</button>
|
|
43
|
+
<button data-key=" ">Space</button>
|
|
44
|
+
<button data-key="\x7f">⌫</button>
|
|
45
45
|
</div>
|
|
46
46
|
<form id="input-form">
|
|
47
47
|
<span class="prompt">></span>
|