ai-or-die 0.1.94 → 0.1.95
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/src/public/app.js +56 -26
- package/src/public/index.html +23 -9
- package/src/public/service-worker.js +8 -2
- package/src/public/splits.js +82 -19
- package/src/public/terminal-wheel.js +188 -0
- package/src/public/vendor/xterm/addon-fit.js +2 -0
- package/src/public/vendor/xterm/addon-search.js +2 -0
- package/src/public/vendor/xterm/addon-serialize.js +2 -0
- package/src/public/vendor/xterm/addon-unicode11.js +2 -0
- package/src/public/vendor/xterm/addon-web-links.js +2 -0
- package/src/public/vendor/xterm/addon-webgl.js +2 -0
- package/src/public/vendor/xterm/xterm.css +285 -0
- package/src/public/vendor/xterm/xterm.js +2 -0
- package/src/public/ws-reconnect.js +33 -0
- package/src/server.js +29 -3
- package/src/usage-reader.js +11 -1
package/package.json
CHANGED
package/src/public/app.js
CHANGED
|
@@ -692,7 +692,7 @@ class ClaudeCodeWebInterface {
|
|
|
692
692
|
foreground: '#f0f6fc',
|
|
693
693
|
cursor: '#ff6b00',
|
|
694
694
|
cursorAccent: '#0d1117',
|
|
695
|
-
|
|
695
|
+
selectionBackground: 'rgba(255, 107, 0, 0.2)',
|
|
696
696
|
black: '#484f58',
|
|
697
697
|
red: '#ff7b72',
|
|
698
698
|
green: '#7ee787',
|
|
@@ -741,6 +741,20 @@ class ClaudeCodeWebInterface {
|
|
|
741
741
|
|
|
742
742
|
this.terminal.open(document.getElementById('terminal'));
|
|
743
743
|
|
|
744
|
+
// Trackpad/mouse-wheel policy: preempt xterm's alt-buffer wheel->arrow
|
|
745
|
+
// translation so scrolling doesn't hijack the Claude Code TUI. Reads the
|
|
746
|
+
// live setting via the cached `_wheelScrollMode` (default 'dontHijack').
|
|
747
|
+
if (typeof window.attachTerminalWheel === 'function') {
|
|
748
|
+
if (this._wheelHandler && this._wheelHandler.dispose) {
|
|
749
|
+
try { this._wheelHandler.dispose(); } catch (_) {}
|
|
750
|
+
}
|
|
751
|
+
this._wheelHandler = window.attachTerminalWheel(
|
|
752
|
+
this.terminal,
|
|
753
|
+
document.getElementById('terminal'),
|
|
754
|
+
() => this._wheelScrollMode || 'dontHijack'
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
744
758
|
// Faithful per-tab snapshot cache for instant tab-switch repaint.
|
|
745
759
|
// Guarded: if the serialize addon or the cache class failed to load
|
|
746
760
|
// (e.g. CDN blocked), snapshotCache stays null and every call site is
|
|
@@ -2293,7 +2307,17 @@ class ClaudeCodeWebInterface {
|
|
|
2293
2307
|
if (restartGen !== this._socketGeneration) return;
|
|
2294
2308
|
this.reconnect();
|
|
2295
2309
|
}, restartBackoff);
|
|
2296
|
-
|
|
2310
|
+
// Reconnect decision extracted to WsReconnect.isReconnectableClose
|
|
2311
|
+
// (unit-tested): abnormal close, our own heartbeat pong-timeout
|
|
2312
|
+
// (code 4000 — a clean close that MUST still reconnect, or a single
|
|
2313
|
+
// transient pong-timeout strands the client on "Disconnected"), or a
|
|
2314
|
+
// server frame-rejection (1009/1003). Inline fallback keeps reconnect
|
|
2315
|
+
// working even if the tiny module failed to load.
|
|
2316
|
+
} else if (
|
|
2317
|
+
((window.WsReconnect && window.WsReconnect.isReconnectableClose)
|
|
2318
|
+
? window.WsReconnect.isReconnectableClose(event, voiceClose.rejected)
|
|
2319
|
+
: (!event.wasClean || event.code === 4000 || voiceClose.rejected))
|
|
2320
|
+
&& this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
2297
2321
|
this.updateStatus('Reconnecting (' + (this.reconnectAttempts + 1) + '/' + this.maxReconnectAttempts + ')...');
|
|
2298
2322
|
// First attempt is fast (250ms covers a server-process restart window);
|
|
2299
2323
|
// subsequent attempts use exponential backoff with jitter.
|
|
@@ -2315,7 +2339,11 @@ class ClaudeCodeWebInterface {
|
|
|
2315
2339
|
this.reconnectAttempts++;
|
|
2316
2340
|
} else {
|
|
2317
2341
|
this.updateStatus('Disconnected');
|
|
2318
|
-
|
|
2342
|
+
const n = this.reconnectAttempts;
|
|
2343
|
+
const lead = n > 0
|
|
2344
|
+
? `Connection lost after ${n} reconnect attempt${n === 1 ? '' : 's'}.`
|
|
2345
|
+
: 'Disconnected from the server.';
|
|
2346
|
+
this.showError(`${lead}\n\nYour session data is preserved on the server.\n\u2022 Check your network connection\n\u2022 The server may have restarted \u2014 try refreshing the page`);
|
|
2319
2347
|
}
|
|
2320
2348
|
};
|
|
2321
2349
|
|
|
@@ -3573,40 +3601,30 @@ class ClaudeCodeWebInterface {
|
|
|
3573
3601
|
}
|
|
3574
3602
|
|
|
3575
3603
|
_loadGpuRenderer() {
|
|
3604
|
+
// The Canvas renderer was removed in xterm 6.0. Mobile keeps using the
|
|
3605
|
+
// reliable default DOM renderer (WebGL was historically avoided there for
|
|
3606
|
+
// context-loss/memory reasons); desktop uses WebGL and falls back to the
|
|
3607
|
+
// DOM renderer on failure or context loss.
|
|
3576
3608
|
if (this.isMobile) {
|
|
3577
|
-
console.log('[Renderer] Mobile detected, using
|
|
3578
|
-
this._loadCanvasAddon();
|
|
3609
|
+
console.log('[Renderer] Mobile detected, using DOM renderer for reliability');
|
|
3579
3610
|
return;
|
|
3580
3611
|
}
|
|
3581
3612
|
if (typeof WebglAddon !== 'undefined') {
|
|
3582
3613
|
try {
|
|
3583
3614
|
this.webglAddon = new WebglAddon.WebglAddon();
|
|
3584
3615
|
this.webglAddon.onContextLoss(() => {
|
|
3585
|
-
this.webglAddon
|
|
3616
|
+
const addon = this.webglAddon;
|
|
3586
3617
|
this.webglAddon = null;
|
|
3587
|
-
|
|
3618
|
+
try { if (addon) addon.dispose(); } catch (_) {}
|
|
3619
|
+
console.log('[Renderer] WebGL context lost, using DOM renderer');
|
|
3588
3620
|
});
|
|
3589
3621
|
this.terminal.loadAddon(this.webglAddon);
|
|
3590
3622
|
} catch (e) {
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
}
|
|
3594
|
-
} else {
|
|
3595
|
-
console.log('[Renderer] WebGL unavailable, using Canvas renderer');
|
|
3596
|
-
this._loadCanvasAddon();
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
|
|
3600
|
-
_loadCanvasAddon() {
|
|
3601
|
-
if (typeof CanvasAddon !== 'undefined') {
|
|
3602
|
-
try {
|
|
3603
|
-
this.canvasAddon = new CanvasAddon.CanvasAddon();
|
|
3604
|
-
this.terminal.loadAddon(this.canvasAddon);
|
|
3605
|
-
} catch (e) {
|
|
3606
|
-
console.log('[Renderer] Canvas unavailable, using DOM renderer (slower)');
|
|
3623
|
+
this.webglAddon = null;
|
|
3624
|
+
console.log('[Renderer] WebGL unavailable, using DOM renderer');
|
|
3607
3625
|
}
|
|
3608
3626
|
} else {
|
|
3609
|
-
console.log('[Renderer]
|
|
3627
|
+
console.log('[Renderer] WebGL unavailable, using DOM renderer');
|
|
3610
3628
|
}
|
|
3611
3629
|
}
|
|
3612
3630
|
|
|
@@ -4259,6 +4277,9 @@ class ClaudeCodeWebInterface {
|
|
|
4259
4277
|
const enableSessionStickyNotes = document.getElementById('enableSessionStickyNotes');
|
|
4260
4278
|
if (enableSessionStickyNotes) enableSessionStickyNotes.checked = settings.enableSessionStickyNotes ?? true;
|
|
4261
4279
|
|
|
4280
|
+
const wheelScrollMode = document.getElementById('wheelScrollMode');
|
|
4281
|
+
if (wheelScrollMode) wheelScrollMode.value = settings.wheelScrollMode || 'dontHijack';
|
|
4282
|
+
|
|
4262
4283
|
// Update install section
|
|
4263
4284
|
this._updateInstallSection();
|
|
4264
4285
|
}
|
|
@@ -4559,7 +4580,11 @@ class ClaudeCodeWebInterface {
|
|
|
4559
4580
|
notifVolume: 30,
|
|
4560
4581
|
notifDesktop: true,
|
|
4561
4582
|
enableSessionStickyNotes: true,
|
|
4562
|
-
tabSnapshotLines: 500
|
|
4583
|
+
tabSnapshotLines: 500,
|
|
4584
|
+
// Trackpad/mouse-wheel behaviour inside full-screen (alt-buffer) apps:
|
|
4585
|
+
// 'dontHijack' - wheel does nothing there (no menu hijack in the Claude Code TUI)
|
|
4586
|
+
// 'altScroll' - wheel sends Up/Down arrows (pagers like `less` scroll)
|
|
4587
|
+
wheelScrollMode: 'dontHijack'
|
|
4563
4588
|
};
|
|
4564
4589
|
}
|
|
4565
4590
|
|
|
@@ -4602,7 +4627,8 @@ class ClaudeCodeWebInterface {
|
|
|
4602
4627
|
notifVolume: parseInt(document.getElementById('notifVolume')?.value || '30'),
|
|
4603
4628
|
notifDesktop: document.getElementById('notifDesktop')?.checked ?? true,
|
|
4604
4629
|
enableSessionStickyNotes: document.getElementById('enableSessionStickyNotes')?.checked ?? true,
|
|
4605
|
-
tabSnapshotLines: parseInt(document.getElementById('tabSnapshotLines')?.value || '500', 10)
|
|
4630
|
+
tabSnapshotLines: parseInt(document.getElementById('tabSnapshotLines')?.value || '500', 10),
|
|
4631
|
+
wheelScrollMode: document.getElementById('wheelScrollMode')?.value || 'dontHijack'
|
|
4606
4632
|
};
|
|
4607
4633
|
|
|
4608
4634
|
try {
|
|
@@ -4651,6 +4677,10 @@ class ClaudeCodeWebInterface {
|
|
|
4651
4677
|
this.terminal.options.cursorBlink = settings.cursorBlink ?? true;
|
|
4652
4678
|
if (settings.scrollback) this.terminal.options.scrollback = settings.scrollback;
|
|
4653
4679
|
|
|
4680
|
+
// Cache the wheel-scroll policy for the capture-phase wheel handler
|
|
4681
|
+
// (read live per wheel event without touching localStorage each notch).
|
|
4682
|
+
this._wheelScrollMode = settings.wheelScrollMode || 'dontHijack';
|
|
4683
|
+
|
|
4654
4684
|
// Push the instant-snapshot line cap to the cache (0 disables capture+paint).
|
|
4655
4685
|
if (this.snapshotCache && settings.tabSnapshotLines !== undefined) {
|
|
4656
4686
|
this.snapshotCache.setMaxLines(settings.tabSnapshotLines);
|
package/src/public/index.html
CHANGED
|
@@ -106,15 +106,19 @@
|
|
|
106
106
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
107
107
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
|
108
108
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<script src="
|
|
115
|
-
<script src="
|
|
116
|
-
<script src="
|
|
117
|
-
<
|
|
109
|
+
<!-- xterm.js self-hosted under vendor/xterm/ (not a CDN) for deterministic,
|
|
110
|
+
offline-capable, fast loads — same rationale as the self-hosted fonts.
|
|
111
|
+
Versions: xterm 6.0.0, addon-fit 0.11.0, addon-web-links 0.12.0,
|
|
112
|
+
addon-search 0.16.0, addon-unicode11 0.9.0, addon-webgl 0.19.0,
|
|
113
|
+
addon-serialize 0.14.0. -->
|
|
114
|
+
<script src="/vendor/xterm/xterm.js"></script>
|
|
115
|
+
<script src="/vendor/xterm/addon-fit.js"></script>
|
|
116
|
+
<script src="/vendor/xterm/addon-web-links.js"></script>
|
|
117
|
+
<script src="/vendor/xterm/addon-search.js"></script>
|
|
118
|
+
<script src="/vendor/xterm/addon-unicode11.js"></script>
|
|
119
|
+
<script src="/vendor/xterm/addon-webgl.js"></script>
|
|
120
|
+
<script src="/vendor/xterm/addon-serialize.js"></script>
|
|
121
|
+
<link rel="stylesheet" href="/vendor/xterm/xterm.css" />
|
|
118
122
|
<!-- Monaco editor is loaded lazily on first file preview/editor open via
|
|
119
123
|
file-viewer-monaco.js (see ADR-0016). Preconnect above warms the
|
|
120
124
|
CDN handshake; we don't preload loader.js to avoid charging users
|
|
@@ -493,6 +497,14 @@
|
|
|
493
497
|
<span class="range-value" id="terminalPaddingValue">8px</span>
|
|
494
498
|
</span>
|
|
495
499
|
</div>
|
|
500
|
+
<div class="setting-group">
|
|
501
|
+
<label for="wheelScrollMode">Trackpad scroll in full-screen apps</label>
|
|
502
|
+
<select id="wheelScrollMode">
|
|
503
|
+
<option value="dontHijack">Don't hijack menus</option>
|
|
504
|
+
<option value="altScroll">Scroll (send arrows)</option>
|
|
505
|
+
</select>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="setting-hint">Inside full-screen apps like the Claude Code TUI, the wheel can't both scroll and drive menus. “Don't hijack” stops it from jumping menus; “Scroll” makes it scroll pagers like <code>less</code>. Normal shell scrolling is unaffected.</div>
|
|
496
508
|
</section>
|
|
497
509
|
|
|
498
510
|
<section class="settings-pane" id="settingsPane-voice" role="tabpanel" aria-labelledby="settingsTab-voice" tabindex="0" hidden>
|
|
@@ -841,6 +853,7 @@
|
|
|
841
853
|
<script src="clipboard-handler.js"></script>
|
|
842
854
|
<script src="image-handler.js"></script>
|
|
843
855
|
<script src="generic-drop-handler.js"></script>
|
|
856
|
+
<script src="terminal-wheel.js"></script>
|
|
844
857
|
<script src="auth.js"></script>
|
|
845
858
|
<script src="feedback-manager.js"></script>
|
|
846
859
|
<!-- marked.min.js and purify.min.js lazy-loaded on first plan viewer open -->
|
|
@@ -849,6 +862,7 @@
|
|
|
849
862
|
<script src="sticky-note-card.js"></script>
|
|
850
863
|
<script src="artifact-panel.js"></script>
|
|
851
864
|
<script src="heartbeat-watchdog.js"></script>
|
|
865
|
+
<script src="ws-reconnect.js"></script>
|
|
852
866
|
<script src="splits.js"></script>
|
|
853
867
|
<script src="icons.js"></script>
|
|
854
868
|
<script src="file-viewer-monaco.js"></script>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Bump this version when urlsToCache entries are added or removed.
|
|
2
2
|
// Content changes to existing files are handled by the network-first fetch strategy.
|
|
3
|
-
const CACHE_NAME = 'ai-or-die-
|
|
3
|
+
const CACHE_NAME = 'ai-or-die-v13';
|
|
4
4
|
const urlsToCache = [
|
|
5
5
|
'/',
|
|
6
6
|
'/index.html',
|
|
@@ -49,7 +49,13 @@ const urlsToCache = [
|
|
|
49
49
|
'/voice-handler.js',
|
|
50
50
|
'/image-handler.js',
|
|
51
51
|
'/input-overlay.js',
|
|
52
|
-
'/feedback-manager.js'
|
|
52
|
+
'/feedback-manager.js',
|
|
53
|
+
'/terminal-wheel.js'
|
|
54
|
+
// xterm.js is self-hosted under /vendor/xterm/ (served locally, fast) but is
|
|
55
|
+
// intentionally NOT precached on install: ~900KB of addons would bloat the
|
|
56
|
+
// install step (it churns on every fresh page load, and is pathologically slow
|
|
57
|
+
// under the WebKit-on-Windows CI runner). The runtime fetch handler caches it
|
|
58
|
+
// on first load, so offline still works after the first online visit.
|
|
53
59
|
];
|
|
54
60
|
|
|
55
61
|
// Install event - cache resources
|
package/src/public/splits.js
CHANGED
|
@@ -69,34 +69,34 @@ class Split {
|
|
|
69
69
|
|
|
70
70
|
this.terminal.open(terminalDiv);
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Trackpad/mouse-wheel policy (same as the main terminal): preempt
|
|
73
|
+
// xterm's alt-buffer wheel->arrow translation so scrolling doesn't
|
|
74
|
+
// hijack the Claude Code TUI. Reads the app-wide setting live.
|
|
75
|
+
if (typeof window.attachTerminalWheel === 'function') {
|
|
76
|
+
this._wheelHandler = window.attachTerminalWheel(
|
|
77
|
+
this.terminal,
|
|
78
|
+
terminalDiv,
|
|
79
|
+
() => (this.app && this.app._wheelScrollMode) || 'dontHijack'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// WebGL renderer with DOM-renderer fallback. The Canvas renderer was
|
|
84
|
+
// removed in xterm 6.0, so on WebGL failure/context-loss we fall back to
|
|
85
|
+
// xterm's default DOM renderer (loading no addon).
|
|
73
86
|
if (typeof WebglAddon !== 'undefined') {
|
|
74
87
|
try {
|
|
75
88
|
this.webglAddon = new WebglAddon.WebglAddon();
|
|
76
89
|
this.webglAddon.onContextLoss(() => {
|
|
77
|
-
this.webglAddon
|
|
90
|
+
const addon = this.webglAddon;
|
|
78
91
|
this.webglAddon = null;
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
this.canvasAddon = new CanvasAddon.CanvasAddon();
|
|
82
|
-
this.terminal.loadAddon(this.canvasAddon);
|
|
83
|
-
} catch (_) {}
|
|
84
|
-
}
|
|
92
|
+
try { if (addon) addon.dispose(); } catch (_) {}
|
|
93
|
+
// No addon loaded -> xterm reverts to the DOM renderer.
|
|
85
94
|
});
|
|
86
95
|
this.terminal.loadAddon(this.webglAddon);
|
|
87
96
|
} catch (e) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.canvasAddon = new CanvasAddon.CanvasAddon();
|
|
91
|
-
this.terminal.loadAddon(this.canvasAddon);
|
|
92
|
-
} catch (_) {}
|
|
93
|
-
}
|
|
97
|
+
this.webglAddon = null;
|
|
98
|
+
// Falls back to the default DOM renderer.
|
|
94
99
|
}
|
|
95
|
-
} else if (typeof CanvasAddon !== 'undefined') {
|
|
96
|
-
try {
|
|
97
|
-
this.canvasAddon = new CanvasAddon.CanvasAddon();
|
|
98
|
-
this.terminal.loadAddon(this.canvasAddon);
|
|
99
|
-
} catch (_) {}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// Re-render split terminal when fonts finish loading
|
|
@@ -152,11 +152,69 @@ class Split {
|
|
|
152
152
|
caption: imageData.caption || ''
|
|
153
153
|
}));
|
|
154
154
|
}
|
|
155
|
+
},
|
|
156
|
+
// Non-image files pasted from a file manager route through the
|
|
157
|
+
// generic pipeline — same bridge as the main terminal
|
|
158
|
+
// (app.js). Without this, non-image PASTE is silently dropped
|
|
159
|
+
// in a split pane. Fires only after the image branch declines.
|
|
160
|
+
onFilesPaste: (files) => {
|
|
161
|
+
if (this._genericDropHandler
|
|
162
|
+
&& typeof this._genericDropHandler.dispatchFiles === 'function') {
|
|
163
|
+
this._genericDropHandler.dispatchFiles(files);
|
|
164
|
+
}
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
);
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
// Generic (non-image) file drop — mirror of the main terminal wiring
|
|
171
|
+
// in app.js. WITHOUT this, a split pane attaches only the image
|
|
172
|
+
// handler, whose drop listener preventDefaults every drop and then
|
|
173
|
+
// silently returns for anything that isn't an accepted image — so
|
|
174
|
+
// dropping a PDF (or any non-image file) on a split does nothing.
|
|
175
|
+
// Runs in capture phase so it preempts xterm's own drop handling;
|
|
176
|
+
// image MIMEs delegate to the image preview via onImageDrop, everything
|
|
177
|
+
// else uploads to <session cwd>/.claude-attachments/ and injects
|
|
178
|
+
// `@<absolute-path>` as bracketed paste.
|
|
179
|
+
//
|
|
180
|
+
// Session scoping is load-bearing: use THIS split's sessionId-scoped
|
|
181
|
+
// working dir + THIS split's socket, never the foregrounded session's
|
|
182
|
+
// (same wrong-session hazard the link-provider comment above warns of).
|
|
183
|
+
if (window.genericDropHandler) {
|
|
184
|
+
this._genericDropHandler = window.genericDropHandler.attachGenericDropHandler({
|
|
185
|
+
containerEl: terminalContainer,
|
|
186
|
+
getWorkingDir: () => (this.app && typeof this.app.getSessionWorkingDir === 'function'
|
|
187
|
+
? this.app.getSessionWorkingDir(this.sessionId) : null),
|
|
188
|
+
getAuthToken: () => (window.authManager && window.authManager.getToken
|
|
189
|
+
? window.authManager.getToken() : null),
|
|
190
|
+
onImageDrop: () => {
|
|
191
|
+
// Intentionally a no-op: for an image-only drop the generic
|
|
192
|
+
// handler returns WITHOUT stopPropagation (see the "defer to
|
|
193
|
+
// image-handler.js entirely" branch in generic-drop-handler.js),
|
|
194
|
+
// so this split's own image handler onDrop still fires and
|
|
195
|
+
// shows the preview. Calling showImagePreview here too would
|
|
196
|
+
// stack a SECOND identical modal. (Mixed image+non-image drops
|
|
197
|
+
// do stopPropagation, so the image partition of a mixed drop is
|
|
198
|
+
// not previewed — a rare edge; drop images on their own.)
|
|
199
|
+
},
|
|
200
|
+
injectAtPath: (atPath) => {
|
|
201
|
+
if (!atPath) return;
|
|
202
|
+
let normalized = attachClipboardHandler.normalizeLineEndings(atPath + ' ');
|
|
203
|
+
if (this.terminal && this.terminal.modes && this.terminal.modes.bracketedPasteMode) {
|
|
204
|
+
normalized = attachClipboardHandler.wrapBracketedPaste(normalized);
|
|
205
|
+
}
|
|
206
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
207
|
+
this.socket.send(JSON.stringify({ type: 'input', data: normalized }));
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
onError: (basename, msg) => {
|
|
211
|
+
if (window.feedback && typeof window.feedback.error === 'function') {
|
|
212
|
+
window.feedback.error(basename + ': ' + msg);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
160
218
|
// Setup terminal input handler
|
|
161
219
|
this.terminal.onData((data) => {
|
|
162
220
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
@@ -424,6 +482,11 @@ class Split {
|
|
|
424
482
|
|
|
425
483
|
destroy() {
|
|
426
484
|
this.disconnect();
|
|
485
|
+
// Tear down drop/paste handlers (listeners + any in-flight uploads)
|
|
486
|
+
// before disposing the terminal so nothing fires against a dead pane.
|
|
487
|
+
try { if (this._genericDropHandler && this._genericDropHandler.destroy) this._genericDropHandler.destroy(); } catch (_) {}
|
|
488
|
+
try { if (this._imageHandler && this._imageHandler.destroy) this._imageHandler.destroy(); } catch (_) {}
|
|
489
|
+
try { if (this._wheelHandler && this._wheelHandler.dispose) this._wheelHandler.dispose(); } catch (_) {}
|
|
427
490
|
if (this.terminal) {
|
|
428
491
|
this.terminal.dispose();
|
|
429
492
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminal-wheel.js — trackpad / mouse-wheel policy for xterm terminals.
|
|
3
|
+
*
|
|
4
|
+
* Problem it fixes: when a full-screen app is on the ALTERNATE screen buffer
|
|
5
|
+
* (the Claude Code TUI, `less`, `vim`, ...), xterm.js unconditionally converts
|
|
6
|
+
* every wheel notch into cursor Up/Down key bytes (ESC O A / ESC O B, or the
|
|
7
|
+
* CSI form) and cancels native scroll. Inside the Claude Code TUI those arrows
|
|
8
|
+
* jump menus/history instead of scrolling — so the trackpad "presses the
|
|
9
|
+
* up/down buttons" rather than scrolling.
|
|
10
|
+
*
|
|
11
|
+
* There is no xterm option to disable this. On xterm >= 5.4 we use the official
|
|
12
|
+
* `attachCustomWheelEventHandler` (returning false cancels xterm's own wheel
|
|
13
|
+
* processing — both the scrollback scroll and the alt-buffer arrow translation).
|
|
14
|
+
* On older xterm we fall back to a CAPTURE-PHASE wheel listener installed on an
|
|
15
|
+
* ANCESTOR of `.xterm`, which fires BEFORE xterm's own wheel listener so
|
|
16
|
+
* stopImmediatePropagation() there preempts the translation. Either way the
|
|
17
|
+
* decision is identical (decideWheelAction).
|
|
18
|
+
*
|
|
19
|
+
* Behaviour (see decideWheelAction):
|
|
20
|
+
* - Normal buffer -> passthrough (xterm scrolls scrollback natively)
|
|
21
|
+
* - Alt buffer + mouse mode -> passthrough (xterm reports wheel as mouse events)
|
|
22
|
+
* - Alt buffer, no mouse -> governed by the user's `wheelScrollMode`:
|
|
23
|
+
* 'dontHijack' (default): suppress -> no arrows, no menu hijack
|
|
24
|
+
* 'altScroll' : passthrough -> xterm sends arrows (pagers scroll)
|
|
25
|
+
* - An app that explicitly sets DEC private mode 1007 (alternate scroll mode)
|
|
26
|
+
* overrides the default for that app: 1007h -> passthrough, 1007l -> suppress.
|
|
27
|
+
*
|
|
28
|
+
* Honest note: in the alt buffer the Claude Code TUI and a pager are otherwise
|
|
29
|
+
* indistinguishable (both alt-screen, no mouse, no 1007), so the toggle is the
|
|
30
|
+
* real control; the default is tuned for the Claude-Code-primary workflow.
|
|
31
|
+
*/
|
|
32
|
+
(function () {
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Pure decision function (exported for unit tests).
|
|
37
|
+
* @param {object} terminal xterm Terminal (needs .buffer.active.type and .modes)
|
|
38
|
+
* @param {string} mode 'dontHijack' | 'altScroll'
|
|
39
|
+
* @param {('on'|'off'|null)} force1007 explicit DEC 1007 state, overrides `mode`
|
|
40
|
+
* @returns {'passthrough'|'suppress'}
|
|
41
|
+
*/
|
|
42
|
+
function decideWheelAction(terminal, mode, force1007) {
|
|
43
|
+
try {
|
|
44
|
+
var active = terminal && terminal.buffer && terminal.buffer.active;
|
|
45
|
+
var isAlt = !!(active && active.type === 'alternate');
|
|
46
|
+
// Normal buffer: never interfere — xterm scrolls the scrollback natively.
|
|
47
|
+
if (!isAlt) return 'passthrough';
|
|
48
|
+
|
|
49
|
+
// Alt buffer but the app enabled mouse tracking: let xterm report the wheel
|
|
50
|
+
// as mouse events so mouse-aware apps keep scrolling.
|
|
51
|
+
var modes = terminal.modes || {};
|
|
52
|
+
if (modes.mouseTrackingMode && modes.mouseTrackingMode !== 'none') {
|
|
53
|
+
return 'passthrough';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// An app's explicit DEC 1007 wins over the user's wheelScrollMode:
|
|
57
|
+
// 1007h forces passthrough even under dontHijack; 1007l forces suppress
|
|
58
|
+
// even under altScroll (the app explicitly opted out of alternate scroll).
|
|
59
|
+
if (force1007 === 'on') return 'passthrough';
|
|
60
|
+
if (force1007 === 'off') return 'suppress';
|
|
61
|
+
|
|
62
|
+
// Ambiguous alt-buffer case: user setting decides.
|
|
63
|
+
if (mode === 'altScroll') return 'passthrough';
|
|
64
|
+
return 'suppress'; // 'dontHijack' default
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// Never break scrolling on an unexpected terminal shape.
|
|
67
|
+
return 'passthrough';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Attach the wheel policy to a terminal.
|
|
73
|
+
* @param {object} terminal xterm Terminal instance
|
|
74
|
+
* @param {HTMLElement} containerEl an ANCESTOR of the `.xterm` element (the element passed to terminal.open())
|
|
75
|
+
* @param {function(): string} getMode returns the current 'wheelScrollMode'
|
|
76
|
+
* @returns {{dispose: function}} handle whose dispose() removes the listener + parser hooks
|
|
77
|
+
*/
|
|
78
|
+
function attachTerminalWheel(terminal, containerEl, getMode) {
|
|
79
|
+
if (!terminal) {
|
|
80
|
+
return { dispose: function () {} };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Track explicit DEC private mode 1007 (alternate scroll mode). xterm does
|
|
84
|
+
// not expose 1007, so we observe the control sequences via the parser. We
|
|
85
|
+
// ALWAYS return false so the sequence still flows to xterm's own handlers
|
|
86
|
+
// (xterm ignores 1007, but returning true would swallow every other
|
|
87
|
+
// DEC-private set/reset — cursor, alt buffer, mouse, bracketed paste).
|
|
88
|
+
//
|
|
89
|
+
// CRITICAL: the flag is per-alt-screen-app, not per-session. Entering OR
|
|
90
|
+
// leaving the alt buffer (1049/1047/47) and a full reset (RIS, ESC c) clear
|
|
91
|
+
// it, so a stale 1007 from a previous app (e.g. tmux) can't hijack the next
|
|
92
|
+
// one (e.g. the Claude Code TUI).
|
|
93
|
+
var alt1007 = null; // null = the current alt-screen app never set it
|
|
94
|
+
var disposers = [];
|
|
95
|
+
try {
|
|
96
|
+
var parser = terminal.parser;
|
|
97
|
+
if (parser && typeof parser.registerCsiHandler === 'function') {
|
|
98
|
+
var scan = function (value) {
|
|
99
|
+
return function (params) {
|
|
100
|
+
try {
|
|
101
|
+
var toggleAlt = false, set1007 = false;
|
|
102
|
+
for (var i = 0; i < params.length; i++) {
|
|
103
|
+
var p = params[i];
|
|
104
|
+
if (p === 1007) set1007 = true;
|
|
105
|
+
else if (p === 1049 || p === 1047 || p === 47) toggleAlt = true;
|
|
106
|
+
}
|
|
107
|
+
if (toggleAlt) alt1007 = null; // alt-buffer enter/exit resets the per-app preference
|
|
108
|
+
if (set1007) alt1007 = value; // explicit 1007 wins
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
return false; // never consume
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
disposers.push(parser.registerCsiHandler({ prefix: '?', final: 'h' }, scan('on')));
|
|
114
|
+
disposers.push(parser.registerCsiHandler({ prefix: '?', final: 'l' }, scan('off')));
|
|
115
|
+
}
|
|
116
|
+
if (parser && typeof parser.registerEscHandler === 'function') {
|
|
117
|
+
// RIS (ESC c) — full terminal reset clears the 1007 preference too.
|
|
118
|
+
disposers.push(parser.registerEscHandler({ final: 'c' }, function () { alt1007 = null; return false; }));
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {
|
|
121
|
+
// Parser API differences: degrade gracefully to setting-only behaviour.
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var decide = function () {
|
|
125
|
+
var mode = 'dontHijack';
|
|
126
|
+
try {
|
|
127
|
+
var m = typeof getMode === 'function' ? getMode() : null;
|
|
128
|
+
if (m) mode = m;
|
|
129
|
+
} catch (_) {}
|
|
130
|
+
return decideWheelAction(terminal, mode, alt1007);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
var disposeParser = function () {
|
|
134
|
+
for (var i = 0; i < disposers.length; i++) {
|
|
135
|
+
try { if (disposers[i] && disposers[i].dispose) disposers[i].dispose(); } catch (_) {}
|
|
136
|
+
}
|
|
137
|
+
disposers = [];
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Preferred (xterm >= 5.4): the official custom wheel handler. Returning
|
|
141
|
+
// false cancels xterm's own wheel processing. Re-attaching replaces the
|
|
142
|
+
// handler (naturally idempotent), and terminal.dispose() tears it down.
|
|
143
|
+
if (typeof terminal.attachCustomWheelEventHandler === 'function') {
|
|
144
|
+
terminal.attachCustomWheelEventHandler(function () {
|
|
145
|
+
return decide() !== 'suppress';
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
dispose: function () {
|
|
149
|
+
try { terminal.attachCustomWheelEventHandler(function () { return true; }); } catch (_) {}
|
|
150
|
+
disposeParser();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fallback (older xterm without the API): capture-phase listener on an
|
|
156
|
+
// ancestor of `.xterm` preempts xterm's own wheel listener.
|
|
157
|
+
if (!containerEl || typeof containerEl.addEventListener !== 'function') {
|
|
158
|
+
disposeParser();
|
|
159
|
+
return { dispose: function () {} };
|
|
160
|
+
}
|
|
161
|
+
var onWheel = function (ev) {
|
|
162
|
+
if (decide() === 'suppress') {
|
|
163
|
+
ev.preventDefault();
|
|
164
|
+
ev.stopImmediatePropagation();
|
|
165
|
+
}
|
|
166
|
+
// 'passthrough' -> do nothing; xterm's own wheel listener runs next.
|
|
167
|
+
};
|
|
168
|
+
// passive:false because we may preventDefault().
|
|
169
|
+
containerEl.addEventListener('wheel', onWheel, { capture: true, passive: false });
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
dispose: function () {
|
|
173
|
+
try { containerEl.removeEventListener('wheel', onWheel, { capture: true }); } catch (_) {}
|
|
174
|
+
disposeParser();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
var api = { attachTerminalWheel: attachTerminalWheel, decideWheelAction: decideWheelAction };
|
|
180
|
+
|
|
181
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
182
|
+
module.exports = api;
|
|
183
|
+
}
|
|
184
|
+
if (typeof window !== 'undefined') {
|
|
185
|
+
window.attachTerminalWheel = attachTerminalWheel;
|
|
186
|
+
window.terminalWheel = api;
|
|
187
|
+
}
|
|
188
|
+
})();
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(globalThis,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core._renderService.dimensions;if(0===e.css.cell.width||0===e.css.cell.height)return;const t=0===this._terminal.options.scrollback?0:this._terminal.options.overviewRuler?.width||14,r=window.getComputedStyle(this._terminal.element.parentElement),i=parseInt(r.getPropertyValue("height")),o=Math.max(0,parseInt(r.getPropertyValue("width"))),s=window.getComputedStyle(this._terminal.element),n=i-(parseInt(s.getPropertyValue("padding-top"))+parseInt(s.getPropertyValue("padding-bottom"))),l=o-(parseInt(s.getPropertyValue("padding-right"))+parseInt(s.getPropertyValue("padding-left")))-t;return{cols:Math.max(2,Math.floor(l/e.css.cell.width)),rows:Math.max(1,Math.floor(n/e.css.cell.height))}}}})(),e})()));
|
|
2
|
+
//# sourceMappingURL=addon-fit.js.map
|