@wangyaoshen/remux 0.3.10-dev.19fb76c → 0.3.10-dev.574c4d2
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 +1 -1
- package/server.js +138 -629
- package/services/discovery/local-server.js +98 -0
- package/services/discovery/worker.js +125 -0
- package/services/discovery/wrangler.toml +7 -0
- package/src/server.ts +144 -634
- package/tests/e2e/app.spec.js +100 -315
- package/tests/server.test.js +2 -2
package/server.js
CHANGED
|
@@ -5013,8 +5013,6 @@ var init_server = __esm({
|
|
|
5013
5013
|
--compose-bg: #3a3a3a;
|
|
5014
5014
|
--compose-border: #555;
|
|
5015
5015
|
--tab-hover: #383838;
|
|
5016
|
-
--view-switch-bg: #1a1a1a;
|
|
5017
|
-
--inspect-meta-border: #333;
|
|
5018
5016
|
}
|
|
5019
5017
|
|
|
5020
5018
|
[data-theme="light"] {
|
|
@@ -5037,8 +5035,6 @@ var init_server = __esm({
|
|
|
5037
5035
|
--compose-bg: #e8e8e8;
|
|
5038
5036
|
--compose-border: #c0c0c0;
|
|
5039
5037
|
--tab-hover: #d6d6d6;
|
|
5040
|
-
--view-switch-bg: #d4d4d4;
|
|
5041
|
-
--inspect-meta-border: #d4d4d4;
|
|
5042
5038
|
}
|
|
5043
5039
|
|
|
5044
5040
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
@@ -5085,20 +5081,13 @@ var init_server = __esm({
|
|
|
5085
5081
|
.sidebar-footer { padding: 8px 12px; border-top: 1px solid var(--border);
|
|
5086
5082
|
display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
5087
5083
|
.sidebar-footer .version { font-size: 10px; color: var(--text-dim); }
|
|
5088
|
-
|
|
5084
|
+
|
|
5089
5085
|
.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--text-muted); flex-shrink: 0; }
|
|
5090
5086
|
.status-dot.connected { background: var(--dot-ok); }
|
|
5091
5087
|
.status-dot.disconnected { background: var(--dot-err); }
|
|
5092
5088
|
.status-dot.connecting { background: var(--dot-warn); animation: pulse 1s infinite; }
|
|
5093
5089
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
|
|
5094
5090
|
|
|
5095
|
-
.role-indicator { font-size: 11px; color: var(--text-muted); display: flex; align-items: center; gap: 4px; }
|
|
5096
|
-
.role-indicator.active { color: var(--dot-ok); }
|
|
5097
|
-
.role-indicator.observer { color: var(--dot-warn); }
|
|
5098
|
-
.role-btn { background: none; border: 1px solid var(--border); border-radius: 4px;
|
|
5099
|
-
color: var(--text-muted); font-size: 10px; padding: 2px 8px; cursor: pointer; font-family: inherit; }
|
|
5100
|
-
.role-btn:hover { color: var(--text-bright); border-color: var(--text-muted); }
|
|
5101
|
-
|
|
5102
5091
|
/* -- Theme toggle -- */
|
|
5103
5092
|
.theme-toggle { background: none; border: none; cursor: pointer; font-size: 16px;
|
|
5104
5093
|
color: var(--text-muted); padding: 4px 8px; border-radius: 4px; }
|
|
@@ -5110,9 +5099,9 @@ var init_server = __esm({
|
|
|
5110
5099
|
border-bottom: 1px solid var(--border); background: var(--bg); min-height: 40px; }
|
|
5111
5100
|
.main-toolbar .toolbar-spacer { flex: 1; }
|
|
5112
5101
|
|
|
5113
|
-
/* -- Tab bar
|
|
5102
|
+
/* -- Tab bar -- */
|
|
5114
5103
|
.tab-bar { background: var(--bg-tab-bar); display: flex; align-items: flex-end; flex-shrink: 0;
|
|
5115
|
-
min-height: 36px;
|
|
5104
|
+
min-height: 36px; position: relative; z-index: 101; }
|
|
5116
5105
|
.tab-toggle { padding: 8px 10px; background: none; border: none; color: var(--text-muted);
|
|
5117
5106
|
cursor: pointer; font-size: 16px; flex-shrink: 0; align-self: center; }
|
|
5118
5107
|
.tab-toggle:hover { color: var(--text-bright); }
|
|
@@ -5145,56 +5134,39 @@ var init_server = __esm({
|
|
|
5145
5134
|
flex-shrink: 0; align-self: center; }
|
|
5146
5135
|
.tab-new:hover { color: var(--text); background: var(--compose-bg); }
|
|
5147
5136
|
|
|
5148
|
-
/* -- Terminal -- */
|
|
5137
|
+
/* -- Terminal (read-only) -- */
|
|
5149
5138
|
#terminal { flex: 1; background: var(--bg); overflow: hidden; position: relative; }
|
|
5150
5139
|
#terminal canvas { display: block; position: absolute; top: 0; left: 0; }
|
|
5151
|
-
#terminal.hidden { display: none; }
|
|
5152
|
-
|
|
5153
|
-
/* -- View switcher -- */
|
|
5154
|
-
.view-switch { display: flex; gap: 1px; margin-left: auto; margin-right: 8px;
|
|
5155
|
-
align-self: center; background: var(--view-switch-bg); border-radius: 4px; overflow: hidden; }
|
|
5156
|
-
.view-switch button { padding: 4px 10px; font-size: 11px; font-family: inherit;
|
|
5157
|
-
color: var(--text-muted); background: var(--bg-tab-bar); border: none; cursor: pointer; }
|
|
5158
|
-
.view-switch button:hover { color: var(--text); }
|
|
5159
|
-
.view-switch button.active { color: var(--text-on-active); background: var(--accent); }
|
|
5160
|
-
|
|
5161
|
-
/* -- Inspect -- */
|
|
5162
|
-
#inspect { flex: 1; background: var(--bg); overflow: auto; display: none;
|
|
5163
|
-
padding: 12px 16px; -webkit-overflow-scrolling: touch; }
|
|
5164
|
-
#inspect.visible { display: block; }
|
|
5165
5140
|
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
#inspect-search .match-count { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
|
5141
|
+
/* -- Compose input -- */
|
|
5142
|
+
.compose-input { display: flex; background: var(--bg-sidebar); border-top: 1px solid var(--border);
|
|
5143
|
+
padding: 8px; gap: 6px; flex-shrink: 0; align-items: center; }
|
|
5144
|
+
.compose-input input { flex: 1; min-width: 0; padding: 8px 12px; font-size: 14px;
|
|
5145
|
+
font-family: 'Menlo','Monaco','Courier New',monospace;
|
|
5146
|
+
background: var(--compose-bg); border: 1px solid var(--compose-border); border-radius: 6px;
|
|
5147
|
+
color: var(--text-bright); outline: none; }
|
|
5148
|
+
.compose-input input:focus { border-color: var(--accent); }
|
|
5149
|
+
.compose-input .send-btn { padding: 8px 16px; font-size: 13px; font-family: inherit;
|
|
5150
|
+
background: var(--accent); color: #fff; border: none; border-radius: 6px;
|
|
5151
|
+
cursor: pointer; white-space: nowrap; flex-shrink: 0; }
|
|
5152
|
+
.compose-input .send-btn:hover { opacity: 0.9; }
|
|
5153
|
+
.compose-input .send-btn:active { opacity: 0.8; }
|
|
5154
|
+
.compose-input .preset-toggle { padding: 8px; font-size: 16px; background: none;
|
|
5155
|
+
border: 1px solid var(--compose-border); border-radius: 6px; color: var(--text-muted);
|
|
5156
|
+
cursor: pointer; flex-shrink: 0; line-height: 1; }
|
|
5157
|
+
.compose-input .preset-toggle:hover { color: var(--text-bright); background: var(--compose-bg); }
|
|
5158
|
+
.compose-input .preset-toggle.active { color: var(--accent); border-color: var(--accent); }
|
|
5185
5159
|
|
|
5186
|
-
/* --
|
|
5187
|
-
.
|
|
5188
|
-
padding:
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
.compose-bar button { padding: 8px 12px; font-size: 14px;
|
|
5160
|
+
/* -- Preset commands -- */
|
|
5161
|
+
.preset-bar { display: none; background: var(--bg-sidebar); border-top: 1px solid var(--border);
|
|
5162
|
+
padding: 6px 8px; gap: 5px; flex-shrink: 0; flex-wrap: wrap; }
|
|
5163
|
+
.preset-bar.visible { display: flex; }
|
|
5164
|
+
.preset-bar button { padding: 6px 12px; font-size: 12px;
|
|
5192
5165
|
font-family: 'Menlo','Monaco',monospace; color: var(--text-bright); background: var(--compose-bg);
|
|
5193
5166
|
border: 1px solid var(--compose-border); border-radius: 5px; cursor: pointer; white-space: nowrap;
|
|
5194
5167
|
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
|
|
5195
|
-
min-width:
|
|
5196
|
-
.
|
|
5197
|
-
.compose-bar button.active { background: #4a6a9a; border-color: #6a9ade; }
|
|
5168
|
+
min-width: 36px; text-align: center; user-select: none; }
|
|
5169
|
+
.preset-bar button:active { background: var(--compose-border); }
|
|
5198
5170
|
|
|
5199
5171
|
/* -- Mobile -- */
|
|
5200
5172
|
@media (max-width: 768px) {
|
|
@@ -5207,9 +5179,10 @@ var init_server = __esm({
|
|
|
5207
5179
|
.sidebar-overlay.visible { display: block; pointer-events: auto; }
|
|
5208
5180
|
.main { margin-left: 0 !important; width: 100vw; min-width: 0; }
|
|
5209
5181
|
.tab-bar { overflow-x: auto; }
|
|
5210
|
-
.session-item { min-height: 44px; }
|
|
5182
|
+
.session-item { min-height: 44px; }
|
|
5211
5183
|
.tab { min-height: 36px; }
|
|
5212
5184
|
.main-toolbar { padding-left: 8px; padding-right: 8px; }
|
|
5185
|
+
.compose-input input { font-size: 16px; } /* prevent iOS zoom on focus */
|
|
5213
5186
|
}
|
|
5214
5187
|
|
|
5215
5188
|
@media (hover: none), (pointer: coarse) {
|
|
@@ -5235,7 +5208,7 @@ var init_server = __esm({
|
|
|
5235
5208
|
<div class="session-list" id="session-list"></div>
|
|
5236
5209
|
|
|
5237
5210
|
<div class="sidebar-footer">
|
|
5238
|
-
<div class="version">v
|
|
5211
|
+
<div class="version">v\${VERSION}</div>
|
|
5239
5212
|
</div>
|
|
5240
5213
|
</aside>
|
|
5241
5214
|
<div class="main">
|
|
@@ -5243,53 +5216,30 @@ var init_server = __esm({
|
|
|
5243
5216
|
<button class="tab-toggle" id="btn-sidebar" title="Toggle sidebar">☰</button>
|
|
5244
5217
|
<div class="tab-list" id="tab-list"></div>
|
|
5245
5218
|
<button class="tab-new" id="btn-new-tab" title="New tab">+</button>
|
|
5246
|
-
<div class="view-switch">
|
|
5247
|
-
<button id="btn-live" class="active">Live</button>
|
|
5248
|
-
<button id="btn-inspect">Inspect</button>
|
|
5249
|
-
</div>
|
|
5250
5219
|
</div>
|
|
5251
5220
|
<div class="main-toolbar">
|
|
5252
5221
|
<div class="status">
|
|
5253
5222
|
<div class="status-dot connecting" id="status-dot"></div>
|
|
5254
5223
|
<span id="status-text">Connecting...</span>
|
|
5255
5224
|
</div>
|
|
5256
|
-
<div class="role-indicator" id="role-indicator">
|
|
5257
|
-
<span id="role-dot"></span>
|
|
5258
|
-
<span id="role-text"></span>
|
|
5259
|
-
</div>
|
|
5260
|
-
<button class="role-btn" id="btn-role" style="display:none"></button>
|
|
5261
5225
|
<div class="toolbar-spacer"></div>
|
|
5262
5226
|
<button id="btn-theme" class="theme-toggle" title="Toggle theme">☀</button>
|
|
5263
5227
|
</div>
|
|
5264
5228
|
<div id="terminal"></div>
|
|
5265
|
-
<div id="
|
|
5266
|
-
<
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
<span class="match-count" id="inspect-match-count"></span>
|
|
5271
|
-
</div>
|
|
5272
|
-
</div>
|
|
5273
|
-
<pre id="inspect-content"></pre>
|
|
5274
|
-
</div>
|
|
5275
|
-
<div class="compose-bar" id="compose-bar">
|
|
5276
|
-
<button data-seq="esc">Esc</button>
|
|
5277
|
-
<button data-seq="tab">Tab</button>
|
|
5278
|
-
<button data-mod="ctrl" id="btn-ctrl">Ctrl</button>
|
|
5229
|
+
<div class="preset-bar" id="preset-bar">
|
|
5230
|
+
<button data-seq="ctrl-c">Ctrl+C</button>
|
|
5231
|
+
<button data-seq="ctrl-d">Ctrl+D</button>
|
|
5232
|
+
<button data-seq="ctrl-z">Ctrl+Z</button>
|
|
5233
|
+
<button data-seq="ctrl-l">Ctrl+L</button>
|
|
5279
5234
|
<button data-seq="up">↑</button>
|
|
5280
5235
|
<button data-seq="down">↓</button>
|
|
5281
|
-
<button data-seq="
|
|
5282
|
-
<button data-seq="
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
<button
|
|
5286
|
-
<
|
|
5287
|
-
<button
|
|
5288
|
-
<button data-seq="ctrl-z">C-z</button>
|
|
5289
|
-
<button data-seq="pgup">PgUp</button>
|
|
5290
|
-
<button data-seq="pgdn">PgDn</button>
|
|
5291
|
-
<button data-seq="home">Home</button>
|
|
5292
|
-
<button data-seq="end">End</button>
|
|
5236
|
+
<button data-seq="tab">Tab</button>
|
|
5237
|
+
<button data-seq="esc">Esc</button>
|
|
5238
|
+
</div>
|
|
5239
|
+
<div class="compose-input" id="compose-input">
|
|
5240
|
+
<button class="preset-toggle" id="btn-preset-toggle" title="Quick keys">⚙</button>
|
|
5241
|
+
<input type="text" id="cmd-input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
|
5242
|
+
<button class="send-btn" id="btn-send">Send</button>
|
|
5293
5243
|
</div>
|
|
5294
5244
|
</div>
|
|
5295
5245
|
|
|
@@ -5297,10 +5247,8 @@ var init_server = __esm({
|
|
|
5297
5247
|
import { init, Terminal, FitAddon } from '/dist/ghostty-web.js';
|
|
5298
5248
|
await init();
|
|
5299
5249
|
|
|
5300
|
-
// -- Terminal color themes (ghostty-web ITheme) --
|
|
5301
5250
|
const THEMES = {
|
|
5302
5251
|
dark: {
|
|
5303
|
-
// ghostty-web default dark
|
|
5304
5252
|
background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#ffffff',
|
|
5305
5253
|
cursorAccent: '#1e1e1e', selectionBackground: '#264f78', selectionForeground: '#ffffff',
|
|
5306
5254
|
black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
|
|
@@ -5310,7 +5258,6 @@ var init_server = __esm({
|
|
|
5310
5258
|
brightCyan: '#29b8db', brightWhite: '#ffffff',
|
|
5311
5259
|
},
|
|
5312
5260
|
light: {
|
|
5313
|
-
// Ghostty-style light
|
|
5314
5261
|
background: '#ffffff', foreground: '#1d1f21', cursor: '#1d1f21',
|
|
5315
5262
|
cursorAccent: '#ffffff', selectionBackground: '#b4d5fe', selectionForeground: '#1d1f21',
|
|
5316
5263
|
black: '#1d1f21', red: '#c82829', green: '#718c00', yellow: '#eab700',
|
|
@@ -5332,125 +5279,43 @@ var init_server = __esm({
|
|
|
5332
5279
|
if (term) { term.dispose(); container.innerHTML = ''; }
|
|
5333
5280
|
term = window._remuxTerm = new Terminal({ cols: 80, rows: 24,
|
|
5334
5281
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
5335
|
-
fontSize: 14, cursorBlink:
|
|
5282
|
+
fontSize: 14, cursorBlink: false,
|
|
5336
5283
|
theme: THEMES[themeMode] || THEMES.dark,
|
|
5337
|
-
scrollback: 10000
|
|
5284
|
+
scrollback: 10000,
|
|
5285
|
+
disableStdin: true });
|
|
5338
5286
|
fitAddon = new FitAddon();
|
|
5339
5287
|
term.loadAddon(fitAddon);
|
|
5340
5288
|
term.open(container);
|
|
5341
5289
|
fitObserver = new ResizeObserver(() => safeFit());
|
|
5342
5290
|
fitObserver.observe(container);
|
|
5343
5291
|
safeFit();
|
|
5344
|
-
fitSettleTimer = setTimeout(() => {
|
|
5345
|
-
fitSettleTimer = null;
|
|
5346
|
-
safeFit();
|
|
5347
|
-
}, 250);
|
|
5292
|
+
fitSettleTimer = setTimeout(() => { fitSettleTimer = null; safeFit(); }, 250);
|
|
5348
5293
|
if (document.fonts && document.fonts.ready) {
|
|
5349
5294
|
document.fonts.ready.then(() => safeFit()).catch(() => {});
|
|
5350
5295
|
}
|
|
5351
5296
|
return term;
|
|
5352
5297
|
}
|
|
5353
5298
|
|
|
5354
|
-
// -- IME composition guard --
|
|
5355
|
-
// Defer fit()/resize during active IME composition to avoid layout thrash.
|
|
5356
|
-
// Note: ghostty-web binds composition listeners on the container, and
|
|
5357
|
-
// browser-native composition events bubble from textarea to container,
|
|
5358
|
-
// so no forwarding patch is needed. Just guard fit() during composition.
|
|
5359
|
-
let _isComposing = false;
|
|
5360
|
-
let _pendingFit = false;
|
|
5361
5299
|
let fitDebounceTimer = null;
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
if (
|
|
5366
|
-
|
|
5300
|
+
function safeFit() { if (fitAddon) fitAddon.fit(); }
|
|
5301
|
+
function stabilizeFit() {
|
|
5302
|
+
safeFit();
|
|
5303
|
+
if (fitSettleTimer) clearTimeout(fitSettleTimer);
|
|
5304
|
+
fitSettleTimer = setTimeout(() => { fitSettleTimer = null; safeFit(); }, 250);
|
|
5367
5305
|
}
|
|
5368
5306
|
function syncTouchViewportHeight() {
|
|
5369
|
-
if (!window.visualViewport
|
|
5307
|
+
if (!window.visualViewport) return;
|
|
5370
5308
|
const vh = window.visualViewport.height;
|
|
5371
5309
|
if (vh > 0) document.body.style.height = vh + 'px';
|
|
5372
5310
|
clearTimeout(fitDebounceTimer);
|
|
5373
5311
|
fitDebounceTimer = setTimeout(safeFit, 100);
|
|
5374
5312
|
}
|
|
5375
|
-
function stabilizeFit() {
|
|
5376
|
-
safeFit();
|
|
5377
|
-
if (fitSettleTimer) clearTimeout(fitSettleTimer);
|
|
5378
|
-
fitSettleTimer = setTimeout(() => {
|
|
5379
|
-
fitSettleTimer = null;
|
|
5380
|
-
safeFit();
|
|
5381
|
-
}, 250);
|
|
5382
|
-
}
|
|
5383
5313
|
window.addEventListener('resize', safeFit);
|
|
5384
|
-
const _termContainer = document.getElementById('terminal');
|
|
5385
|
-
_termContainer.addEventListener('compositionstart', () => { _isComposing = true; });
|
|
5386
|
-
_termContainer.addEventListener('compositionend', () => {
|
|
5387
|
-
_isComposing = false;
|
|
5388
|
-
if (_pendingFit) { _pendingFit = false; stabilizeFit(); }
|
|
5389
|
-
});
|
|
5390
5314
|
createTerminal(initTheme);
|
|
5391
5315
|
|
|
5392
|
-
let sessions = [], currentSession = null, currentTabId = null, ws = null
|
|
5393
|
-
let myClientId = null,
|
|
5316
|
+
let sessions = [], currentSession = null, currentTabId = null, ws = null;
|
|
5317
|
+
let myClientId = null, clientsList = [];
|
|
5394
5318
|
|
|
5395
|
-
// -- Predictive echo via DOM overlay (see #80 Phase 2) --
|
|
5396
|
-
// Shows predicted characters as transparent HTML spans over the canvas.
|
|
5397
|
-
// Does NOT inject any ANSI escape sequences into the terminal \u2014 the overlay
|
|
5398
|
-
// is purely visual and the terminal data path is never modified.
|
|
5399
|
-
// Adapted from VS Code TypeAheadAddon concept, using DOM overlay instead of
|
|
5400
|
-
// xterm.js decorations (which ghostty-web lacks).
|
|
5401
|
-
const _peOverlay = document.createElement('div');
|
|
5402
|
-
_peOverlay.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:1;overflow:hidden;';
|
|
5403
|
-
_termContainer.appendChild(_peOverlay);
|
|
5404
|
-
const _pePreds = [];
|
|
5405
|
-
function _peCellSize() {
|
|
5406
|
-
// Use ghostty-web renderer's exact font metrics when available
|
|
5407
|
-
if (term.renderer) return { w: term.renderer.charWidth, h: term.renderer.charHeight };
|
|
5408
|
-
const cvs = _termContainer.querySelector('canvas');
|
|
5409
|
-
if (!cvs) return { w: 8, h: 16 };
|
|
5410
|
-
return { w: cvs.offsetWidth / term.cols, h: cvs.offsetHeight / term.rows };
|
|
5411
|
-
}
|
|
5412
|
-
function peOnInput(data) {
|
|
5413
|
-
if (_isComposing) return;
|
|
5414
|
-
if (term.buffer && term.buffer.active && term.buffer.active.type === 'alternate') return;
|
|
5415
|
-
if (myRole && myRole !== 'active') return;
|
|
5416
|
-
for (let i = 0; i < data.length; i++) {
|
|
5417
|
-
const c = data.charCodeAt(i);
|
|
5418
|
-
if (c >= 0x20 && c <= 0x7e && _pePreds.length < 32) {
|
|
5419
|
-
const buf = term.buffer && term.buffer.active;
|
|
5420
|
-
const cx = (buf ? buf.cursorX : 0) + _pePreds.length;
|
|
5421
|
-
const cy = buf ? buf.cursorY : 0;
|
|
5422
|
-
const cell = _peCellSize();
|
|
5423
|
-
const span = document.createElement('span');
|
|
5424
|
-
span.textContent = data[i];
|
|
5425
|
-
span.style.cssText = 'position:absolute;display:inline-block;color:var(--text,#d4d4d4);opacity:0.6;'
|
|
5426
|
-
+ 'font-family:Menlo,Monaco,Courier New,monospace;'
|
|
5427
|
-
+ 'left:' + (cx * cell.w) + 'px;top:' + (cy * cell.h) + 'px;'
|
|
5428
|
-
+ 'width:' + cell.w + 'px;height:' + cell.h + 'px;'
|
|
5429
|
-
+ 'font-size:14px;line-height:' + cell.h + 'px;text-align:center;';
|
|
5430
|
-
_peOverlay.appendChild(span);
|
|
5431
|
-
_pePreds.push({ ch: data[i], span, ts: Date.now() });
|
|
5432
|
-
} else if (c < 0x20 || c === 0x7f) {
|
|
5433
|
-
peClearAll();
|
|
5434
|
-
}
|
|
5435
|
-
// Non-ASCII: skip prediction, don't clear
|
|
5436
|
-
}
|
|
5437
|
-
}
|
|
5438
|
-
function peOnServerData(data) {
|
|
5439
|
-
// Match predictions against server echo, remove confirmed overlay spans.
|
|
5440
|
-
// NEVER modify or consume data \u2014 always pass full data to term.write().
|
|
5441
|
-
if (_pePreds.length === 0) return;
|
|
5442
|
-
for (let i = 0; i < data.length && _pePreds.length > 0; i++) {
|
|
5443
|
-
if (data.charCodeAt(i) === 0x1b) { peClearAll(); return; }
|
|
5444
|
-
if (data[i] === _pePreds[0].ch) {
|
|
5445
|
-
const p = _pePreds.shift();
|
|
5446
|
-
p.span.remove();
|
|
5447
|
-
} else { peClearAll(); return; }
|
|
5448
|
-
}
|
|
5449
|
-
}
|
|
5450
|
-
function peClearAll() {
|
|
5451
|
-
for (const p of _pePreds) p.span.remove();
|
|
5452
|
-
_pePreds.length = 0;
|
|
5453
|
-
}
|
|
5454
5319
|
const $ = id => document.getElementById(id);
|
|
5455
5320
|
const esc = t => (t || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
5456
5321
|
const setStatus = (s, t) => { $('status-dot').className = 'status-dot ' + s; $('status-text').textContent = t; };
|
|
@@ -5460,27 +5325,12 @@ var init_server = __esm({
|
|
|
5460
5325
|
document.documentElement.setAttribute('data-theme', mode);
|
|
5461
5326
|
localStorage.setItem('remux-theme', mode);
|
|
5462
5327
|
$('btn-theme').innerHTML = mode === 'dark' ? '☀' : '☾';
|
|
5463
|
-
// Recreate terminal with new theme (ghostty-web doesn't support runtime theme change)
|
|
5464
5328
|
createTerminal(mode);
|
|
5465
|
-
peClearAll();
|
|
5466
|
-
// Rebind terminal I/O
|
|
5467
|
-
term.onData(data => {
|
|
5468
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
5469
|
-
if (ctrlActive) {
|
|
5470
|
-
ctrlActive = false; $('btn-ctrl').classList.remove('active');
|
|
5471
|
-
const ch = data.toLowerCase().charCodeAt(0);
|
|
5472
|
-
if (ch >= 0x61 && ch <= 0x7a) { sendTermData(String.fromCharCode(ch - 0x60)); return; }
|
|
5473
|
-
}
|
|
5474
|
-
peOnInput(data);
|
|
5475
|
-
sendTermData(data);
|
|
5476
|
-
});
|
|
5477
5329
|
term.onResize(({ cols, rows }) => sendCtrl({ type: 'resize', cols, rows }));
|
|
5478
|
-
// Re-attach to current tab to get snapshot
|
|
5479
5330
|
if (currentTabId != null) {
|
|
5480
5331
|
sendCtrl({ type: 'attach_tab', tabId: currentTabId, cols: term.cols, rows: term.rows });
|
|
5481
5332
|
}
|
|
5482
5333
|
}
|
|
5483
|
-
// Apply initial theme CSS (terminal already created with correct theme)
|
|
5484
5334
|
document.documentElement.setAttribute('data-theme', initTheme);
|
|
5485
5335
|
$('btn-theme').innerHTML = initTheme === 'dark' ? '☀' : '☾';
|
|
5486
5336
|
$('btn-theme').addEventListener('click', () => {
|
|
@@ -5515,21 +5365,15 @@ var init_server = __esm({
|
|
|
5515
5365
|
const live = s.tabs.filter(t => !t.ended).length;
|
|
5516
5366
|
el.innerHTML = '<span class="dot"></span><span class="name">' + esc(s.name)
|
|
5517
5367
|
+ '</span><span class="count">' + live + '</span>'
|
|
5518
|
-
+ '<button class="del" data-del="' + esc(s.name) + '"
|
|
5368
|
+
+ '<button class="del" data-del="' + esc(s.name) + '">\\u00d7</button>';
|
|
5519
5369
|
el.addEventListener('pointerdown', e => {
|
|
5520
5370
|
if (e.target.dataset.del) {
|
|
5521
5371
|
e.stopPropagation(); e.preventDefault();
|
|
5522
5372
|
sendCtrl({ type: 'delete_session', name: e.target.dataset.del });
|
|
5523
|
-
// if deleting current, switch to another or create fresh
|
|
5524
5373
|
if (e.target.dataset.del === currentSession) {
|
|
5525
5374
|
const other = sessions.find(x => x.name !== currentSession);
|
|
5526
|
-
if (other) {
|
|
5527
|
-
|
|
5528
|
-
} else {
|
|
5529
|
-
// Last session deleted \u2014 re-bootstrap via attach_first
|
|
5530
|
-
currentSession = null;
|
|
5531
|
-
sendCtrl({ type: 'attach_first', cols: term.cols, rows: term.rows });
|
|
5532
|
-
}
|
|
5375
|
+
if (other) { selectSession(other.name); }
|
|
5376
|
+
else { currentSession = null; sendCtrl({ type: 'attach_first', cols: term.cols, rows: term.rows }); }
|
|
5533
5377
|
}
|
|
5534
5378
|
return;
|
|
5535
5379
|
}
|
|
@@ -5538,17 +5382,13 @@ var init_server = __esm({
|
|
|
5538
5382
|
closeSidebarMobile();
|
|
5539
5383
|
});
|
|
5540
5384
|
el.addEventListener('keydown', e => {
|
|
5541
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
5542
|
-
e.preventDefault();
|
|
5543
|
-
selectSession(s.name);
|
|
5544
|
-
closeSidebarMobile();
|
|
5545
|
-
}
|
|
5385
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectSession(s.name); closeSidebarMobile(); }
|
|
5546
5386
|
});
|
|
5547
5387
|
list.appendChild(el);
|
|
5548
5388
|
});
|
|
5549
5389
|
}
|
|
5550
5390
|
|
|
5551
|
-
// -- Render tabs
|
|
5391
|
+
// -- Render tabs --
|
|
5552
5392
|
function renderTabs() {
|
|
5553
5393
|
const list = $('tab-list'); list.innerHTML = '';
|
|
5554
5394
|
const sess = sessions.find(s => s.name === currentSession);
|
|
@@ -5561,54 +5401,20 @@ var init_server = __esm({
|
|
|
5561
5401
|
const clientCount = t.clients || 0;
|
|
5562
5402
|
const countBadge = clientCount > 1 ? '<span class="client-count">' + clientCount + '</span>' : '';
|
|
5563
5403
|
el.innerHTML = '<span class="title">' + esc(t.title) + '</span>' + countBadge
|
|
5564
|
-
+ '<button class="close" data-close="' + t.id + '"
|
|
5404
|
+
+ '<button class="close" data-close="' + t.id + '">\\u00d7</button>';
|
|
5565
5405
|
el.addEventListener('pointerdown', e => {
|
|
5566
5406
|
const closeId = e.target.dataset.close ?? e.target.closest('[data-close]')?.dataset.close;
|
|
5567
|
-
if (closeId != null) {
|
|
5568
|
-
e.stopPropagation(); e.preventDefault();
|
|
5569
|
-
closeTab(Number(closeId));
|
|
5570
|
-
return;
|
|
5571
|
-
}
|
|
5407
|
+
if (closeId != null) { e.stopPropagation(); e.preventDefault(); closeTab(Number(closeId)); return; }
|
|
5572
5408
|
e.preventDefault();
|
|
5573
5409
|
if (t.id !== currentTabId) attachTab(t.id);
|
|
5574
5410
|
});
|
|
5575
5411
|
el.addEventListener('keydown', e => {
|
|
5576
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
5577
|
-
e.preventDefault();
|
|
5578
|
-
if (t.id !== currentTabId) attachTab(t.id);
|
|
5579
|
-
}
|
|
5412
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (t.id !== currentTabId) attachTab(t.id); }
|
|
5580
5413
|
});
|
|
5581
5414
|
list.appendChild(el);
|
|
5582
5415
|
});
|
|
5583
5416
|
}
|
|
5584
5417
|
|
|
5585
|
-
// -- Render role indicator --
|
|
5586
|
-
function renderRole() {
|
|
5587
|
-
const indicator = $('role-indicator');
|
|
5588
|
-
const dot = $('role-dot');
|
|
5589
|
-
const text = $('role-text');
|
|
5590
|
-
const btn = $('btn-role');
|
|
5591
|
-
if (!indicator || !myRole) return;
|
|
5592
|
-
indicator.className = 'role-indicator ' + myRole;
|
|
5593
|
-
if (myRole === 'active') {
|
|
5594
|
-
dot.textContent = '\u25CF';
|
|
5595
|
-
text.textContent = 'Active';
|
|
5596
|
-
btn.textContent = 'Release';
|
|
5597
|
-
btn.style.display = 'inline-block';
|
|
5598
|
-
// Auto-focus terminal when becoming active so keystrokes reach xterm
|
|
5599
|
-
if (currentView === 'live') setTimeout(() => term.focus(), 50);
|
|
5600
|
-
} else {
|
|
5601
|
-
dot.textContent = '\u25CB';
|
|
5602
|
-
text.textContent = 'Observer';
|
|
5603
|
-
btn.textContent = 'Take control';
|
|
5604
|
-
btn.style.display = 'inline-block';
|
|
5605
|
-
}
|
|
5606
|
-
}
|
|
5607
|
-
$('btn-role').addEventListener('click', () => {
|
|
5608
|
-
if (myRole === 'active') sendCtrl({ type: 'release_control' });
|
|
5609
|
-
else sendCtrl({ type: 'request_control' });
|
|
5610
|
-
});
|
|
5611
|
-
|
|
5612
5418
|
function selectSession(name) {
|
|
5613
5419
|
currentSession = name;
|
|
5614
5420
|
const sess = sessions.find(s => s.name === name);
|
|
@@ -5618,7 +5424,7 @@ var init_server = __esm({
|
|
|
5618
5424
|
|
|
5619
5425
|
function attachTab(tabId) {
|
|
5620
5426
|
currentTabId = tabId;
|
|
5621
|
-
term.reset();
|
|
5427
|
+
term.reset();
|
|
5622
5428
|
sendCtrl({ type: 'attach_tab', tabId, cols: term.cols, rows: term.rows });
|
|
5623
5429
|
stabilizeFit();
|
|
5624
5430
|
renderTabs(); renderSessions();
|
|
@@ -5627,7 +5433,6 @@ var init_server = __esm({
|
|
|
5627
5433
|
function closeTab(tabId) {
|
|
5628
5434
|
const sess = sessions.find(s => s.name === currentSession);
|
|
5629
5435
|
if (!sess) return;
|
|
5630
|
-
// if closing active tab, switch to neighbor first
|
|
5631
5436
|
if (tabId === currentTabId) {
|
|
5632
5437
|
const idx = sess.tabs.findIndex(t => t.id === tabId);
|
|
5633
5438
|
const next = sess.tabs[idx + 1] || sess.tabs[idx - 1];
|
|
@@ -5649,10 +5454,7 @@ var init_server = __esm({
|
|
|
5649
5454
|
$('session-composer').classList.remove('visible');
|
|
5650
5455
|
$('new-session-input').value = '';
|
|
5651
5456
|
}
|
|
5652
|
-
$('btn-new-session').addEventListener('pointerdown', e => {
|
|
5653
|
-
e.preventDefault();
|
|
5654
|
-
openSessionComposer();
|
|
5655
|
-
});
|
|
5457
|
+
$('btn-new-session').addEventListener('pointerdown', e => { e.preventDefault(); openSessionComposer(); });
|
|
5656
5458
|
$('btn-create-session').addEventListener('click', () => {
|
|
5657
5459
|
const name = $('new-session-input').value.trim();
|
|
5658
5460
|
if (!name) return;
|
|
@@ -5661,109 +5463,58 @@ var init_server = __esm({
|
|
|
5661
5463
|
});
|
|
5662
5464
|
$('btn-cancel-session').addEventListener('click', closeSessionComposer);
|
|
5663
5465
|
$('new-session-input').addEventListener('keydown', e => {
|
|
5664
|
-
if (e.key === 'Enter') {
|
|
5665
|
-
|
|
5666
|
-
$('btn-create-session').click();
|
|
5667
|
-
} else if (e.key === 'Escape') {
|
|
5668
|
-
e.preventDefault();
|
|
5669
|
-
closeSessionComposer();
|
|
5670
|
-
}
|
|
5466
|
+
if (e.key === 'Enter') { e.preventDefault(); $('btn-create-session').click(); }
|
|
5467
|
+
else if (e.key === 'Escape') { e.preventDefault(); closeSessionComposer(); }
|
|
5671
5468
|
});
|
|
5672
5469
|
|
|
5673
5470
|
// -- E2EE client (Web Crypto API) --
|
|
5674
|
-
// Adapted from Signal Protocol X25519+AES-GCM pattern
|
|
5675
5471
|
const e2ee = {
|
|
5676
|
-
established: false,
|
|
5677
|
-
|
|
5678
|
-
recvCounter: -1n,
|
|
5679
|
-
localKeyPair: null, // { publicKey: CryptoKey, privateKey: CryptoKey, rawPublic: Uint8Array }
|
|
5680
|
-
sharedKey: null, // CryptoKey (AES-GCM)
|
|
5472
|
+
established: false, sendCounter: 0n, recvCounter: -1n,
|
|
5473
|
+
localKeyPair: null, sharedKey: null,
|
|
5681
5474
|
available: !!(crypto && crypto.subtle),
|
|
5682
|
-
|
|
5683
5475
|
async init() {
|
|
5684
5476
|
if (!this.available) return;
|
|
5685
5477
|
try {
|
|
5686
5478
|
const kp = await crypto.subtle.generateKey('X25519', false, ['deriveBits']);
|
|
5687
5479
|
const rawPub = new Uint8Array(await crypto.subtle.exportKey('raw', kp.publicKey));
|
|
5688
5480
|
this.localKeyPair = { publicKey: kp.publicKey, privateKey: kp.privateKey, rawPublic: rawPub };
|
|
5689
|
-
} catch (e) {
|
|
5690
|
-
console.warn('[e2ee] X25519 not available:', e);
|
|
5691
|
-
this.available = false;
|
|
5692
|
-
}
|
|
5481
|
+
} catch (e) { this.available = false; }
|
|
5693
5482
|
},
|
|
5694
|
-
|
|
5695
5483
|
getPublicKeyB64() {
|
|
5696
5484
|
if (!this.localKeyPair) return null;
|
|
5697
5485
|
return btoa(String.fromCharCode(...this.localKeyPair.rawPublic));
|
|
5698
5486
|
},
|
|
5699
|
-
|
|
5700
5487
|
async completeHandshake(peerPubKeyB64) {
|
|
5701
5488
|
if (!this.localKeyPair) return;
|
|
5702
5489
|
try {
|
|
5703
5490
|
const peerRaw = Uint8Array.from(atob(peerPubKeyB64), c => c.charCodeAt(0));
|
|
5704
5491
|
const peerKey = await crypto.subtle.importKey('raw', peerRaw, 'X25519', false, []);
|
|
5705
|
-
|
|
5706
|
-
const rawBits = await crypto.subtle.deriveBits(
|
|
5707
|
-
{ name: 'X25519', public: peerKey },
|
|
5708
|
-
this.localKeyPair.privateKey,
|
|
5709
|
-
256
|
|
5710
|
-
);
|
|
5711
|
-
// HKDF-SHA256 to derive AES-256-GCM key
|
|
5492
|
+
const rawBits = await crypto.subtle.deriveBits({ name: 'X25519', public: peerKey }, this.localKeyPair.privateKey, 256);
|
|
5712
5493
|
const hkdfKey = await crypto.subtle.importKey('raw', rawBits, 'HKDF', false, ['deriveBits']);
|
|
5713
|
-
const salt = new TextEncoder().encode('remux-e2ee-v1');
|
|
5714
|
-
const info = new TextEncoder().encode('aes-256-gcm');
|
|
5715
5494
|
const derived = await crypto.subtle.deriveBits(
|
|
5716
|
-
{ name: 'HKDF', hash: 'SHA-256', salt, info },
|
|
5717
|
-
hkdfKey,
|
|
5718
|
-
256
|
|
5719
|
-
);
|
|
5495
|
+
{ name: 'HKDF', hash: 'SHA-256', salt: new TextEncoder().encode('remux-e2ee-v1'), info: new TextEncoder().encode('aes-256-gcm') }, hkdfKey, 256);
|
|
5720
5496
|
this.sharedKey = await crypto.subtle.importKey('raw', derived, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
5721
|
-
this.established = true;
|
|
5722
|
-
|
|
5723
|
-
this.recvCounter = -1n;
|
|
5724
|
-
console.log('[e2ee] handshake complete');
|
|
5725
|
-
} catch (e) {
|
|
5726
|
-
console.error('[e2ee] handshake failed:', e);
|
|
5727
|
-
this.available = false;
|
|
5728
|
-
}
|
|
5497
|
+
this.established = true; this.sendCounter = 0n; this.recvCounter = -1n;
|
|
5498
|
+
} catch (e) { this.available = false; }
|
|
5729
5499
|
},
|
|
5730
|
-
|
|
5731
5500
|
async encryptMessage(plaintext) {
|
|
5732
5501
|
if (!this.sharedKey) throw new Error('E2EE not established');
|
|
5733
|
-
const
|
|
5734
|
-
|
|
5735
|
-
const iv = new Uint8Array(12);
|
|
5736
|
-
crypto.getRandomValues(iv.subarray(0, 4));
|
|
5737
|
-
const counterView = new DataView(iv.buffer, iv.byteOffset + 4, 8);
|
|
5738
|
-
counterView.setBigUint64(0, this.sendCounter, false);
|
|
5502
|
+
const iv = new Uint8Array(12); crypto.getRandomValues(iv.subarray(0, 4));
|
|
5503
|
+
new DataView(iv.buffer, iv.byteOffset + 4, 8).setBigUint64(0, this.sendCounter, false);
|
|
5739
5504
|
this.sendCounter++;
|
|
5740
|
-
const encrypted = await crypto.subtle.encrypt(
|
|
5741
|
-
{ name: 'AES-GCM', iv, tagLength: 128 },
|
|
5742
|
-
this.sharedKey,
|
|
5743
|
-
plaintextBuf
|
|
5744
|
-
);
|
|
5745
|
-
// AES-GCM returns ciphertext + tag concatenated
|
|
5505
|
+
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, this.sharedKey, new TextEncoder().encode(plaintext));
|
|
5746
5506
|
const result = new Uint8Array(12 + encrypted.byteLength);
|
|
5747
|
-
result.set(iv, 0);
|
|
5748
|
-
result.set(new Uint8Array(encrypted), 12);
|
|
5507
|
+
result.set(iv, 0); result.set(new Uint8Array(encrypted), 12);
|
|
5749
5508
|
return btoa(String.fromCharCode(...result));
|
|
5750
5509
|
},
|
|
5751
|
-
|
|
5752
5510
|
async decryptMessage(encryptedB64) {
|
|
5753
5511
|
if (!this.sharedKey) throw new Error('E2EE not established');
|
|
5754
5512
|
const packed = Uint8Array.from(atob(encryptedB64), c => c.charCodeAt(0));
|
|
5755
|
-
if (packed.length < 28) throw new Error('E2EE message too short');
|
|
5513
|
+
if (packed.length < 28) throw new Error('E2EE message too short');
|
|
5756
5514
|
const iv = packed.subarray(0, 12);
|
|
5757
|
-
const
|
|
5758
|
-
// Anti-replay: check counter is monotonically increasing
|
|
5759
|
-
const counterView = new DataView(iv.buffer, iv.byteOffset + 4, 8);
|
|
5760
|
-
const counter = counterView.getBigUint64(0, false);
|
|
5515
|
+
const counter = new DataView(iv.buffer, iv.byteOffset + 4, 8).getBigUint64(0, false);
|
|
5761
5516
|
if (counter <= this.recvCounter) throw new Error('E2EE replay detected');
|
|
5762
|
-
const decrypted = await crypto.subtle.decrypt(
|
|
5763
|
-
{ name: 'AES-GCM', iv, tagLength: 128 },
|
|
5764
|
-
this.sharedKey,
|
|
5765
|
-
ciphertextWithTag
|
|
5766
|
-
);
|
|
5517
|
+
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv, tagLength: 128 }, this.sharedKey, packed.subarray(12));
|
|
5767
5518
|
this.recvCounter = counter;
|
|
5768
5519
|
return new TextDecoder().decode(decrypted);
|
|
5769
5520
|
}
|
|
@@ -5772,18 +5523,12 @@ var init_server = __esm({
|
|
|
5772
5523
|
// -- WebSocket with exponential backoff + heartbeat --
|
|
5773
5524
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
5774
5525
|
const urlToken = new URLSearchParams(location.search).get('token');
|
|
5775
|
-
|
|
5776
|
-
// Exponential backoff: 1s -> 2s -> 4s -> 8s -> 16s -> 30s max
|
|
5777
5526
|
let backoffMs = 1000;
|
|
5778
5527
|
const BACKOFF_MAX = 30000;
|
|
5779
5528
|
let reconnectTimer = null;
|
|
5780
|
-
|
|
5781
|
-
// Heartbeat: if no message received for 45s, consider connection dead
|
|
5782
5529
|
const HEARTBEAT_TIMEOUT = 45000;
|
|
5783
5530
|
let lastMessageAt = Date.now();
|
|
5784
5531
|
let heartbeatChecker = null;
|
|
5785
|
-
|
|
5786
|
-
// Track last received message timestamp for session recovery
|
|
5787
5532
|
let lastReceivedTimestamp = 0;
|
|
5788
5533
|
let isResuming = false;
|
|
5789
5534
|
|
|
@@ -5791,29 +5536,19 @@ var init_server = __esm({
|
|
|
5791
5536
|
lastMessageAt = Date.now();
|
|
5792
5537
|
if (heartbeatChecker) clearInterval(heartbeatChecker);
|
|
5793
5538
|
heartbeatChecker = setInterval(() => {
|
|
5794
|
-
if (Date.now() - lastMessageAt > HEARTBEAT_TIMEOUT) {
|
|
5795
|
-
console.log('[remux] heartbeat timeout, reconnecting');
|
|
5796
|
-
if (ws) ws.close();
|
|
5797
|
-
}
|
|
5539
|
+
if (Date.now() - lastMessageAt > HEARTBEAT_TIMEOUT) { if (ws) ws.close(); }
|
|
5798
5540
|
}, 5000);
|
|
5799
5541
|
}
|
|
5800
|
-
|
|
5801
|
-
function stopHeartbeat() {
|
|
5802
|
-
if (heartbeatChecker) { clearInterval(heartbeatChecker); heartbeatChecker = null; }
|
|
5803
|
-
}
|
|
5542
|
+
function stopHeartbeat() { if (heartbeatChecker) { clearInterval(heartbeatChecker); heartbeatChecker = null; } }
|
|
5804
5543
|
|
|
5805
5544
|
function scheduleReconnect() {
|
|
5806
5545
|
if (reconnectTimer) return;
|
|
5807
5546
|
const delay = backoffMs;
|
|
5808
5547
|
let remaining = Math.ceil(delay / 1000);
|
|
5809
5548
|
setStatus('disconnected', 'Reconnecting in ' + remaining + 's...');
|
|
5810
|
-
const countdown = setInterval(() => {
|
|
5811
|
-
remaining--;
|
|
5812
|
-
if (remaining > 0) setStatus('disconnected', 'Reconnecting in ' + remaining + 's...');
|
|
5813
|
-
}, 1000);
|
|
5549
|
+
const countdown = setInterval(() => { remaining--; if (remaining > 0) setStatus('disconnected', 'Reconnecting in ' + remaining + 's...'); }, 1000);
|
|
5814
5550
|
reconnectTimer = setTimeout(() => {
|
|
5815
|
-
clearInterval(countdown);
|
|
5816
|
-
reconnectTimer = null;
|
|
5551
|
+
clearInterval(countdown); reconnectTimer = null;
|
|
5817
5552
|
backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX);
|
|
5818
5553
|
connect();
|
|
5819
5554
|
}, delay);
|
|
@@ -5821,36 +5556,28 @@ var init_server = __esm({
|
|
|
5821
5556
|
|
|
5822
5557
|
function connect() {
|
|
5823
5558
|
setStatus('connecting', 'Connecting...');
|
|
5824
|
-
e2ee.established = false;
|
|
5825
|
-
e2ee.sharedKey = null;
|
|
5559
|
+
e2ee.established = false; e2ee.sharedKey = null;
|
|
5826
5560
|
ws = new WebSocket(proto + '//' + location.host + '/ws');
|
|
5827
5561
|
ws.onopen = async () => {
|
|
5828
|
-
backoffMs = 1000;
|
|
5562
|
+
backoffMs = 1000;
|
|
5829
5563
|
startHeartbeat();
|
|
5830
5564
|
if (urlToken) {
|
|
5831
|
-
// Use persistent device ID from localStorage so each browser context
|
|
5832
|
-
// is a distinct device even with identical User-Agent
|
|
5833
5565
|
if (!localStorage.getItem('remux-device-id')) {
|
|
5834
5566
|
localStorage.setItem('remux-device-id', Math.random().toString(36).slice(2, 10) + Date.now().toString(36));
|
|
5835
5567
|
}
|
|
5836
5568
|
ws.send(JSON.stringify({ type: 'auth', token: urlToken, deviceId: localStorage.getItem('remux-device-id') }));
|
|
5837
5569
|
}
|
|
5838
|
-
// Initiate E2EE handshake if Web Crypto API is available
|
|
5839
5570
|
if (e2ee.available) {
|
|
5840
5571
|
await e2ee.init();
|
|
5841
5572
|
const pubKey = e2ee.getPublicKeyB64();
|
|
5842
|
-
if (pubKey) {
|
|
5843
|
-
ws.send(JSON.stringify({ v: 1, type: 'e2ee_init', payload: { publicKey: pubKey } }));
|
|
5844
|
-
}
|
|
5573
|
+
if (pubKey) ws.send(JSON.stringify({ v: 1, type: 'e2ee_init', payload: { publicKey: pubKey } }));
|
|
5845
5574
|
}
|
|
5846
|
-
// Session recovery: if we have a previous timestamp, request buffered messages
|
|
5847
5575
|
const deviceId = localStorage.getItem('remux-device-id');
|
|
5848
5576
|
if (lastReceivedTimestamp > 0 && deviceId) {
|
|
5849
5577
|
isResuming = true;
|
|
5850
5578
|
setStatus('connecting', 'Resuming session...');
|
|
5851
|
-
sendCtrl({ type: 'resume', deviceId
|
|
5579
|
+
sendCtrl({ type: 'resume', deviceId, lastTimestamp: lastReceivedTimestamp });
|
|
5852
5580
|
}
|
|
5853
|
-
// Let server pick the session if we have none (bootstrap flow)
|
|
5854
5581
|
sendCtrl({ type: 'attach_first', session: currentSession || undefined, cols: term.cols, rows: term.rows });
|
|
5855
5582
|
};
|
|
5856
5583
|
ws.onmessage = e => {
|
|
@@ -5858,240 +5585,108 @@ var init_server = __esm({
|
|
|
5858
5585
|
if (typeof e.data === 'string' && e.data[0] === '{') {
|
|
5859
5586
|
try {
|
|
5860
5587
|
const parsed = JSON.parse(e.data);
|
|
5861
|
-
// Handle both envelope (v:1) and legacy messages
|
|
5862
|
-
// Unwrap envelope: spread payload first, then override type with the
|
|
5863
|
-
// envelope's type to prevent payload.type (e.g. artifact type "snapshot")
|
|
5864
|
-
// from colliding with the message type (e.g. "snapshot_captured")
|
|
5865
5588
|
const msg = parsed.v === 1 ? { ...(parsed.payload || {}), type: parsed.type } : parsed;
|
|
5866
|
-
// Server heartbeat \u2014 just keep connection alive (lastMessageAt already updated)
|
|
5867
5589
|
if (msg.type === 'ping') return;
|
|
5868
|
-
|
|
5869
|
-
if (msg.type === '
|
|
5870
|
-
if (msg.publicKey && e2ee.available && e2ee.localKeyPair) {
|
|
5871
|
-
e2ee.completeHandshake(msg.publicKey);
|
|
5872
|
-
}
|
|
5873
|
-
return;
|
|
5874
|
-
}
|
|
5875
|
-
if (msg.type === 'e2ee_ready') {
|
|
5876
|
-
console.log('[e2ee] server confirmed E2EE established');
|
|
5877
|
-
return;
|
|
5878
|
-
}
|
|
5879
|
-
// E2EE encrypted message (terminal output from server)
|
|
5590
|
+
if (msg.type === 'e2ee_init') { if (msg.publicKey && e2ee.available && e2ee.localKeyPair) e2ee.completeHandshake(msg.publicKey); return; }
|
|
5591
|
+
if (msg.type === 'e2ee_ready') return;
|
|
5880
5592
|
if (msg.type === 'e2ee_msg') {
|
|
5881
|
-
if (e2ee.established && msg.data) {
|
|
5882
|
-
e2ee.decryptMessage(msg.data).then(decrypted => {
|
|
5883
|
-
term.write(decrypted);
|
|
5884
|
-
}).catch(err => console.error('[e2ee] decrypt failed:', err));
|
|
5885
|
-
}
|
|
5593
|
+
if (e2ee.established && msg.data) e2ee.decryptMessage(msg.data).then(d => term.write(d)).catch(() => {});
|
|
5886
5594
|
return;
|
|
5887
5595
|
}
|
|
5888
|
-
|
|
5889
|
-
if (msg.type === 'resume_complete') {
|
|
5890
|
-
isResuming = false;
|
|
5891
|
-
if (msg.replayed > 0) {
|
|
5892
|
-
console.log('[remux] session recovered: ' + msg.replayed + ' buffered messages replayed');
|
|
5893
|
-
}
|
|
5894
|
-
return;
|
|
5895
|
-
}
|
|
5896
|
-
// Track timestamp for session recovery on reconnect
|
|
5596
|
+
if (msg.type === 'resume_complete') { isResuming = false; return; }
|
|
5897
5597
|
lastReceivedTimestamp = Date.now();
|
|
5898
|
-
if (msg.type === 'auth_ok')
|
|
5899
|
-
|
|
5900
|
-
}
|
|
5901
|
-
if (msg.type === 'bootstrap') {
|
|
5902
|
-
sessions = msg.sessions || [];
|
|
5903
|
-
clientsList = msg.clients || [];
|
|
5904
|
-
renderSessions(); renderTabs(); renderRole(); stabilizeFit();
|
|
5905
|
-
return;
|
|
5906
|
-
}
|
|
5598
|
+
if (msg.type === 'auth_ok') return;
|
|
5599
|
+
if (msg.type === 'bootstrap') { sessions = msg.sessions || []; clientsList = msg.clients || []; renderSessions(); renderTabs(); stabilizeFit(); return; }
|
|
5907
5600
|
if (msg.type === 'auth_error') { setStatus('disconnected', 'Auth failed'); ws.close(); return; }
|
|
5908
|
-
|
|
5909
|
-
if (msg.type === '
|
|
5910
|
-
console.warn('[remux] server error:', msg.reason || 'unknown');
|
|
5911
|
-
alert('Error: ' + (msg.reason || 'unknown error'));
|
|
5912
|
-
return;
|
|
5913
|
-
}
|
|
5914
|
-
if (msg.type === 'state') {
|
|
5915
|
-
sessions = msg.sessions || [];
|
|
5916
|
-
clientsList = msg.clients || [];
|
|
5917
|
-
// Re-derive own role from authoritative server state
|
|
5918
|
-
if (myClientId) {
|
|
5919
|
-
const me = clientsList.find(c => c.clientId === myClientId);
|
|
5920
|
-
if (me) myRole = me.role;
|
|
5921
|
-
}
|
|
5922
|
-
renderSessions(); renderTabs(); renderRole(); stabilizeFit(); return;
|
|
5923
|
-
}
|
|
5601
|
+
if (msg.type === 'error') { alert('Error: ' + (msg.reason || 'unknown error')); return; }
|
|
5602
|
+
if (msg.type === 'state') { sessions = msg.sessions || []; clientsList = msg.clients || []; renderSessions(); renderTabs(); stabilizeFit(); return; }
|
|
5924
5603
|
if (msg.type === 'attached') {
|
|
5925
5604
|
currentTabId = msg.tabId; currentSession = msg.session;
|
|
5926
5605
|
if (msg.clientId) myClientId = msg.clientId;
|
|
5927
|
-
|
|
5928
|
-
setStatus('connected', 'Connected'); renderSessions(); renderTabs(); renderRole(); stabilizeFit(); return;
|
|
5929
|
-
}
|
|
5930
|
-
if (msg.type === 'role_changed') {
|
|
5931
|
-
if (msg.clientId === myClientId) myRole = msg.role;
|
|
5932
|
-
renderRole(); return;
|
|
5606
|
+
setStatus('connected', 'Connected'); renderSessions(); renderTabs(); stabilizeFit(); return;
|
|
5933
5607
|
}
|
|
5934
|
-
if (msg.type === 'inspect_result')
|
|
5935
|
-
|
|
5936
|
-
const m = msg.meta || {};
|
|
5937
|
-
$('inspect-meta').innerHTML =
|
|
5938
|
-
'<span>' + esc(m.session) + ' / ' + esc(m.tabTitle || 'Tab ' + m.tabId) + '</span>' +
|
|
5939
|
-
'<span>' + (m.cols || '?') + 'x' + (m.rows || '?') + '</span>' +
|
|
5940
|
-
'<span>' + new Date(m.timestamp || Date.now()).toLocaleTimeString() + '</span>' +
|
|
5941
|
-
'<button class="inspect-btn" id="btn-copy-inspect">Copy</button>';
|
|
5942
|
-
$('btn-copy-inspect').addEventListener('click', () => {
|
|
5943
|
-
navigator.clipboard.writeText(window._inspectText).then(() => {
|
|
5944
|
-
$('btn-copy-inspect').textContent = 'Copied!';
|
|
5945
|
-
setTimeout(() => { const el = $('btn-copy-inspect'); if (el) el.textContent = 'Copy'; }, 1500);
|
|
5946
|
-
});
|
|
5947
|
-
});
|
|
5948
|
-
// Apply search highlight if active
|
|
5949
|
-
applyInspectSearch();
|
|
5950
|
-
return;
|
|
5951
|
-
}
|
|
5952
|
-
// Unrecognized enveloped control message \u2014 discard, never write to terminal
|
|
5953
|
-
if (parsed.v === 1) {
|
|
5954
|
-
console.warn('[remux] unhandled message type:', msg.type);
|
|
5955
|
-
return;
|
|
5956
|
-
}
|
|
5957
|
-
// Non-enveloped JSON (e.g. PTY output that looks like JSON) \u2014 fall through to term.write
|
|
5608
|
+
if (msg.type === 'role_changed' || msg.type === 'inspect_result') return;
|
|
5609
|
+
if (parsed.v === 1) return;
|
|
5958
5610
|
} catch {}
|
|
5959
5611
|
}
|
|
5960
|
-
peOnServerData(e.data);
|
|
5961
5612
|
term.write(e.data);
|
|
5962
5613
|
};
|
|
5963
|
-
ws.onclose = () => { stopHeartbeat();
|
|
5614
|
+
ws.onclose = () => { stopHeartbeat(); scheduleReconnect(); };
|
|
5964
5615
|
ws.onerror = () => setStatus('disconnected', 'Error');
|
|
5965
5616
|
}
|
|
5966
5617
|
connect();
|
|
5967
5618
|
function sendCtrl(msg) { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(msg)); }
|
|
5968
|
-
// Send terminal data, encrypting if E2EE is established
|
|
5969
5619
|
function sendTermData(data) {
|
|
5970
5620
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
5971
5621
|
if (e2ee.established) {
|
|
5972
5622
|
e2ee.encryptMessage(data).then(encrypted => {
|
|
5973
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
}).catch(err => {
|
|
5977
|
-
console.error('[e2ee] encrypt failed, sending plaintext:', err);
|
|
5978
|
-
ws.send(data);
|
|
5979
|
-
});
|
|
5980
|
-
} else {
|
|
5981
|
-
ws.send(data);
|
|
5982
|
-
}
|
|
5623
|
+
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ v: 1, type: 'e2ee_msg', payload: { data: encrypted } }));
|
|
5624
|
+
}).catch(() => { ws.send(data); });
|
|
5625
|
+
} else { ws.send(data); }
|
|
5983
5626
|
}
|
|
5984
5627
|
|
|
5985
|
-
// -- Terminal
|
|
5986
|
-
term.onData(data => {
|
|
5987
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
5988
|
-
if (ctrlActive) {
|
|
5989
|
-
ctrlActive = false; $('btn-ctrl').classList.remove('active');
|
|
5990
|
-
const ch = data.toLowerCase().charCodeAt(0);
|
|
5991
|
-
if (ch >= 0x61 && ch <= 0x7a) { sendTermData(String.fromCharCode(ch - 0x60)); return; }
|
|
5992
|
-
}
|
|
5993
|
-
peOnInput(data);
|
|
5994
|
-
sendTermData(data);
|
|
5995
|
-
});
|
|
5628
|
+
// -- Terminal resize (read-only, no keyboard input) --
|
|
5996
5629
|
term.onResize(({ cols, rows }) => sendCtrl({ type: 'resize', cols, rows }));
|
|
5997
5630
|
|
|
5998
|
-
// -- Compose
|
|
5631
|
+
// -- Compose input: text field + send --
|
|
5632
|
+
const cmdInput = $('cmd-input');
|
|
5633
|
+
function sendCommand() {
|
|
5634
|
+
const cmd = cmdInput.value;
|
|
5635
|
+
if (!cmd) return;
|
|
5636
|
+
sendTermData(cmd + '\\n');
|
|
5637
|
+
cmdInput.value = '';
|
|
5638
|
+
cmdInput.focus();
|
|
5639
|
+
}
|
|
5640
|
+
$('btn-send').addEventListener('click', sendCommand);
|
|
5641
|
+
cmdInput.addEventListener('keydown', e => {
|
|
5642
|
+
if (e.key === 'Enter') { e.preventDefault(); sendCommand(); }
|
|
5643
|
+
});
|
|
5644
|
+
|
|
5645
|
+
// -- Preset quick keys --
|
|
5999
5646
|
const SEQ = {
|
|
6000
5647
|
esc: '\\x1b', tab: '\\t',
|
|
6001
|
-
up: '\\x1b[A', down: '\\x1b[B',
|
|
6002
|
-
'ctrl-c': '\\x03', 'ctrl-d': '\\x04', 'ctrl-z': '\\x1a',
|
|
6003
|
-
pgup: '\\x1b[5~', pgdn: '\\x1b[6~', home: '\\x1b[H', end: '\\x1b[F',
|
|
5648
|
+
up: '\\x1b[A', down: '\\x1b[B',
|
|
5649
|
+
'ctrl-c': '\\x03', 'ctrl-d': '\\x04', 'ctrl-z': '\\x1a', 'ctrl-l': '\\x0c',
|
|
6004
5650
|
};
|
|
6005
|
-
$('
|
|
5651
|
+
$('btn-preset-toggle').addEventListener('click', () => {
|
|
5652
|
+
const bar = $('preset-bar');
|
|
5653
|
+
const btn = $('btn-preset-toggle');
|
|
5654
|
+
bar.classList.toggle('visible');
|
|
5655
|
+
btn.classList.toggle('active', bar.classList.contains('visible'));
|
|
5656
|
+
setTimeout(stabilizeFit, 50);
|
|
5657
|
+
});
|
|
5658
|
+
$('preset-bar').addEventListener('pointerdown', e => {
|
|
6006
5659
|
const btn = e.target.closest('button'); if (!btn) return;
|
|
6007
5660
|
e.preventDefault();
|
|
6008
|
-
|
|
6009
|
-
const d = SEQ[btn.dataset.seq] || btn.dataset.ch;
|
|
5661
|
+
const d = SEQ[btn.dataset.seq];
|
|
6010
5662
|
if (d) sendTermData(d);
|
|
6011
|
-
|
|
5663
|
+
cmdInput.focus();
|
|
6012
5664
|
});
|
|
6013
5665
|
|
|
6014
|
-
// -- Inspect view --
|
|
6015
|
-
let currentView = 'live', inspectTimer = null;
|
|
6016
|
-
function syncComposeBar() {
|
|
6017
|
-
$('compose-bar').classList.toggle('visible', isTouchDevice && currentView === 'live');
|
|
6018
|
-
}
|
|
6019
|
-
function setView(mode) {
|
|
6020
|
-
currentView = mode;
|
|
6021
|
-
$('btn-live').classList.toggle('active', mode === 'live');
|
|
6022
|
-
$('btn-inspect').classList.toggle('active', mode === 'inspect');
|
|
6023
|
-
$('terminal').classList.toggle('hidden', mode !== 'live');
|
|
6024
|
-
$('inspect').classList.toggle('visible', mode === 'inspect');
|
|
6025
|
-
if (inspectTimer) { clearInterval(inspectTimer); inspectTimer = null; }
|
|
6026
|
-
if (mode === 'inspect') {
|
|
6027
|
-
sendCtrl({ type: 'inspect' });
|
|
6028
|
-
inspectTimer = setInterval(() => sendCtrl({ type: 'inspect' }), 3000);
|
|
6029
|
-
}
|
|
6030
|
-
syncComposeBar();
|
|
6031
|
-
if (mode === 'live') { term.focus(); stabilizeFit(); }
|
|
6032
|
-
}
|
|
6033
|
-
$('btn-live').addEventListener('pointerdown', e => { e.preventDefault(); closeSidebarMobile(); setView('live'); });
|
|
6034
|
-
$('btn-inspect').addEventListener('pointerdown', e => { e.preventDefault(); closeSidebarMobile(); setView('inspect'); });
|
|
6035
|
-
|
|
6036
|
-
// -- Inspect search --
|
|
6037
|
-
function applyInspectSearch() {
|
|
6038
|
-
const query = ($('inspect-search-input') || {}).value || '';
|
|
6039
|
-
const text = window._inspectText || '';
|
|
6040
|
-
if (!query) {
|
|
6041
|
-
$('inspect-content').textContent = text;
|
|
6042
|
-
$('inspect-match-count').textContent = '';
|
|
6043
|
-
return;
|
|
6044
|
-
}
|
|
6045
|
-
const esc = t => t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6046
|
-
const q = query.toLowerCase();
|
|
6047
|
-
const lower = text.toLowerCase();
|
|
6048
|
-
let result = '', count = 0, pos = 0;
|
|
6049
|
-
while (pos < text.length) {
|
|
6050
|
-
const idx = lower.indexOf(q, pos);
|
|
6051
|
-
if (idx === -1) { result += esc(text.slice(pos)); break; }
|
|
6052
|
-
result += esc(text.slice(pos, idx)) + '<mark>' + esc(text.slice(idx, idx + q.length)) + '</mark>';
|
|
6053
|
-
count++; pos = idx + q.length;
|
|
6054
|
-
}
|
|
6055
|
-
$('inspect-content').innerHTML = result;
|
|
6056
|
-
$('inspect-match-count').textContent = count > 0 ? count + ' match' + (count !== 1 ? 'es' : '') : 'No matches';
|
|
6057
|
-
}
|
|
6058
|
-
$('inspect-search-input').addEventListener('input', applyInspectSearch);
|
|
6059
|
-
syncComposeBar();
|
|
6060
|
-
|
|
6061
5666
|
// -- Tab rename (double-click) --
|
|
6062
5667
|
$('tab-list').addEventListener('dblclick', e => {
|
|
6063
5668
|
const tabEl = e.target.closest('.tab');
|
|
6064
5669
|
if (!tabEl) return;
|
|
6065
5670
|
const titleSpan = tabEl.querySelector('.title');
|
|
6066
5671
|
if (!titleSpan) return;
|
|
6067
|
-
// Find tab id from close button
|
|
6068
5672
|
const closeBtn = tabEl.querySelector('.close');
|
|
6069
5673
|
if (!closeBtn) return;
|
|
6070
5674
|
const tabId = Number(closeBtn.dataset.close);
|
|
6071
5675
|
const oldTitle = titleSpan.textContent;
|
|
6072
|
-
|
|
6073
5676
|
const input = document.createElement('input');
|
|
6074
5677
|
input.className = 'rename-input';
|
|
6075
5678
|
input.value = oldTitle;
|
|
6076
5679
|
input.setAttribute('maxlength', '32');
|
|
6077
5680
|
titleSpan.replaceWith(input);
|
|
6078
|
-
input.focus();
|
|
6079
|
-
input.select();
|
|
6080
|
-
|
|
5681
|
+
input.focus(); input.select();
|
|
6081
5682
|
function commit() {
|
|
6082
5683
|
const newTitle = input.value.trim() || oldTitle;
|
|
6083
|
-
// Send rename to server
|
|
6084
5684
|
if (newTitle !== oldTitle) sendCtrl({ type: 'rename_tab', tabId, title: newTitle });
|
|
6085
|
-
|
|
6086
|
-
const span = document.createElement('span');
|
|
6087
|
-
span.className = 'title';
|
|
6088
|
-
span.textContent = newTitle;
|
|
5685
|
+
const span = document.createElement('span'); span.className = 'title'; span.textContent = newTitle;
|
|
6089
5686
|
input.replaceWith(span);
|
|
6090
5687
|
}
|
|
6091
5688
|
function cancel() {
|
|
6092
|
-
const span = document.createElement('span');
|
|
6093
|
-
span.className = 'title';
|
|
6094
|
-
span.textContent = oldTitle;
|
|
5689
|
+
const span = document.createElement('span'); span.className = 'title'; span.textContent = oldTitle;
|
|
6095
5690
|
input.replaceWith(span);
|
|
6096
5691
|
}
|
|
6097
5692
|
input.addEventListener('keydown', ev => {
|
|
@@ -6101,97 +5696,11 @@ var init_server = __esm({
|
|
|
6101
5696
|
input.addEventListener('blur', commit);
|
|
6102
5697
|
});
|
|
6103
5698
|
|
|
6104
|
-
// -- Mobile
|
|
6105
|
-
|
|
6106
|
-
// Ignore viewport sync during IME composition so candidate UI can't
|
|
6107
|
-
// temporarily collapse the terminal area.
|
|
6108
|
-
if (window.visualViewport && isTouchDevice) {
|
|
5699
|
+
// -- Mobile viewport handling --
|
|
5700
|
+
if (window.visualViewport) {
|
|
6109
5701
|
window.visualViewport.addEventListener('resize', syncTouchViewportHeight);
|
|
6110
5702
|
window.visualViewport.addEventListener('scroll', () => window.scrollTo(0, 0));
|
|
6111
5703
|
}
|
|
6112
|
-
// iOS Safari: touching terminal area focuses hidden textarea for input
|
|
6113
|
-
document.getElementById('terminal').addEventListener('touchend', () => { if (currentView === 'live') term.focus(); });
|
|
6114
|
-
|
|
6115
|
-
// -- IME diagnostic (active when ?debug=1) --
|
|
6116
|
-
if (new URLSearchParams(location.search).has('debug')) {
|
|
6117
|
-
const _d = window._imeDiag = { events: [], t0: Date.now() };
|
|
6118
|
-
function _ilog(type, detail) {
|
|
6119
|
-
const e = { t: Date.now() - _d.t0, type, ...detail };
|
|
6120
|
-
_d.events.push(e);
|
|
6121
|
-
if (_d.events.length > 300) _d.events.shift();
|
|
6122
|
-
try { localStorage.setItem('_imeDiag', JSON.stringify(_d.events.slice(-50))); } catch {}
|
|
6123
|
-
}
|
|
6124
|
-
const _ta = document.querySelector('textarea');
|
|
6125
|
-
if (_ta) {
|
|
6126
|
-
['compositionstart','compositionupdate','compositionend'].forEach(n =>
|
|
6127
|
-
_ta.addEventListener(n, e => _ilog(n, { data: e.data }), true));
|
|
6128
|
-
_ta.addEventListener('input', e => _ilog('input', {
|
|
6129
|
-
data: e.data?.substring(0,20), inputType: e.inputType, isComposing: e.isComposing
|
|
6130
|
-
}), true);
|
|
6131
|
-
_ta.addEventListener('keydown', e => {
|
|
6132
|
-
if (e.isComposing || e.keyCode === 229)
|
|
6133
|
-
_ilog('keydown-ime', { key: e.key, code: e.code, kc: e.keyCode });
|
|
6134
|
-
}, true);
|
|
6135
|
-
}
|
|
6136
|
-
new ResizeObserver(entries => entries.forEach(e => {
|
|
6137
|
-
const n = e.target.id || e.target.tagName;
|
|
6138
|
-
_ilog('resize', { el: n, h: Math.round(e.contentRect.height) });
|
|
6139
|
-
})).observe(document.body);
|
|
6140
|
-
if (window.visualViewport) window.visualViewport.addEventListener('resize', () =>
|
|
6141
|
-
_ilog('vv-resize', { vh: Math.round(window.visualViewport.height), bh: document.body.offsetHeight }));
|
|
6142
|
-
new MutationObserver(() => _ilog('body-style', {
|
|
6143
|
-
h: document.body.style.height, oh: document.body.offsetHeight
|
|
6144
|
-
})).observe(document.body, { attributes: true, attributeFilter: ['style'] });
|
|
6145
|
-
window.addEventListener('error', e => _ilog('js-error', { msg: e.message, line: e.lineno }));
|
|
6146
|
-
// Track textarea style/size changes (IME may resize it)
|
|
6147
|
-
if (_ta) {
|
|
6148
|
-
new ResizeObserver(() => {
|
|
6149
|
-
_ilog('ta-resize', { w: _ta.offsetWidth, h: _ta.offsetHeight, vis: getComputedStyle(_ta).visibility, op: getComputedStyle(_ta).opacity });
|
|
6150
|
-
}).observe(_ta);
|
|
6151
|
-
}
|
|
6152
|
-
// Track canvas visibility
|
|
6153
|
-
const _cvs = document.querySelector('#terminal canvas');
|
|
6154
|
-
if (_cvs) {
|
|
6155
|
-
new ResizeObserver(() => {
|
|
6156
|
-
_ilog('canvas-resize', { w: _cvs.width, h: _cvs.height, display: getComputedStyle(_cvs).display });
|
|
6157
|
-
}).observe(_cvs);
|
|
6158
|
-
}
|
|
6159
|
-
// Floating debug overlay \u2014 stays visible even when page goes "blank"
|
|
6160
|
-
const _dbg = document.createElement('div');
|
|
6161
|
-
_dbg.id = 'ime-debug';
|
|
6162
|
-
_dbg.style.cssText = 'position:fixed;bottom:0;right:0;z-index:999999;background:rgba(0,0,0,0.85);color:#0f0;font:10px monospace;padding:4px 8px;max-width:60vw;max-height:40vh;overflow:auto;pointer-events:none;white-space:pre-wrap;';
|
|
6163
|
-
document.documentElement.appendChild(_dbg);
|
|
6164
|
-
|
|
6165
|
-
// Continuous polling \u2014 captures state even when no events fire
|
|
6166
|
-
let _prevState = '';
|
|
6167
|
-
setInterval(() => {
|
|
6168
|
-
const _ta2 = document.querySelector('textarea');
|
|
6169
|
-
const _cvs2 = document.querySelector('#terminal canvas');
|
|
6170
|
-
const _term2 = document.getElementById('terminal');
|
|
6171
|
-
const _main2 = document.querySelector('.main');
|
|
6172
|
-
const _sidebar2 = document.querySelector('.sidebar');
|
|
6173
|
-
const state = JSON.stringify({
|
|
6174
|
-
body: { h: document.body.offsetHeight, w: document.body.offsetWidth, styleH: document.body.style.height, vis: document.body.style.visibility, disp: document.body.style.display },
|
|
6175
|
-
main: _main2 ? { h: _main2.offsetHeight, vis: getComputedStyle(_main2).visibility, op: getComputedStyle(_main2).opacity, disp: getComputedStyle(_main2).display } : null,
|
|
6176
|
-
term: _term2 ? { h: _term2.offsetHeight, w: _term2.offsetWidth, vis: getComputedStyle(_term2).visibility, disp: getComputedStyle(_term2).display, op: getComputedStyle(_term2).opacity } : null,
|
|
6177
|
-
cvs: _cvs2 ? { w: _cvs2.width, h: _cvs2.height, styleW: _cvs2.style.width, styleH: _cvs2.style.height, vis: getComputedStyle(_cvs2).visibility, disp: getComputedStyle(_cvs2).display } : null,
|
|
6178
|
-
ta: _ta2 ? { w: _ta2.offsetWidth, h: _ta2.offsetHeight, styleW: _ta2.style.width, styleH: _ta2.style.height, pos: getComputedStyle(_ta2).position, vis: getComputedStyle(_ta2).visibility, op: getComputedStyle(_ta2).opacity, zIdx: getComputedStyle(_ta2).zIndex, bg: getComputedStyle(_ta2).background?.substring(0,40) } : null,
|
|
6179
|
-
sidebar: _sidebar2 ? { h: _sidebar2.offsetHeight, vis: getComputedStyle(_sidebar2).visibility } : null,
|
|
6180
|
-
vv: window.visualViewport ? { h: Math.round(window.visualViewport.height), w: Math.round(window.visualViewport.width) } : null
|
|
6181
|
-
});
|
|
6182
|
-
if (state !== _prevState) {
|
|
6183
|
-
_prevState = state;
|
|
6184
|
-
_ilog('poll', JSON.parse(state));
|
|
6185
|
-
}
|
|
6186
|
-
// Always update overlay with latest events
|
|
6187
|
-
const last = _d.events.slice(-12);
|
|
6188
|
-
_dbg.textContent = last.map(e => {
|
|
6189
|
-
const {t, type, ...r} = e;
|
|
6190
|
-
return t + 'ms ' + type + ': ' + JSON.stringify(r).substring(0, 120);
|
|
6191
|
-
}).join('\\n');
|
|
6192
|
-
}, 200);
|
|
6193
|
-
console.log('[remux] IME diagnostic v2 active \u2014 polling every 200ms');
|
|
6194
|
-
}
|
|
6195
5704
|
</script>
|
|
6196
5705
|
</body>
|
|
6197
5706
|
</html>`;
|