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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-or-die",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "description": "Universal AI coding terminal — Claude, Copilot, Gemini & more in your browser",
5
5
  "main": "src/server.js",
6
6
  "bin": {
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
- selection: 'rgba(255, 107, 0, 0.2)',
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
- } else if ((!event.wasClean || voiceClose.rejected) && this.reconnectAttempts < this.maxReconnectAttempts) {
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
- this.showError(`Connection lost after ${this.maxReconnectAttempts} attempts.\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`);
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 Canvas renderer for reliability');
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.dispose();
3616
+ const addon = this.webglAddon;
3586
3617
  this.webglAddon = null;
3587
- this._loadCanvasAddon();
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
- console.log('[Renderer] WebGL unavailable, using Canvas renderer');
3592
- this._loadCanvasAddon();
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] Canvas unavailable, using DOM renderer (slower)');
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);
@@ -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
- <script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
110
- <script src="https://unpkg.com/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
111
- <script src="https://unpkg.com/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
112
- <script src="https://unpkg.com/xterm-addon-search@0.13.0/lib/xterm-addon-search.js"></script>
113
- <script src="https://unpkg.com/xterm-addon-unicode11@0.6.0/lib/xterm-addon-unicode11.js"></script>
114
- <script src="https://unpkg.com/xterm-addon-webgl@0.16.0/lib/xterm-addon-webgl.js"></script>
115
- <script src="https://unpkg.com/xterm-addon-canvas@0.5.0/lib/xterm-addon-canvas.js"></script>
116
- <script src="https://unpkg.com/xterm-addon-serialize@0.11.0/lib/xterm-addon-serialize.js"></script>
117
- <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css" />
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. &ldquo;Don't hijack&rdquo; stops it from jumping menus; &ldquo;Scroll&rdquo; 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-v11';
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
@@ -69,34 +69,34 @@ class Split {
69
69
 
70
70
  this.terminal.open(terminalDiv);
71
71
 
72
- // WebGL renderer with Canvas fallback
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.dispose();
90
+ const addon = this.webglAddon;
78
91
  this.webglAddon = null;
79
- if (typeof CanvasAddon !== 'undefined') {
80
- try {
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
- if (typeof CanvasAddon !== 'undefined') {
89
- try {
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