ai-or-die 0.1.93 → 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 +207 -106
- package/src/public/artifact-panel.js +62 -0
- package/src/public/components/artifact-panel.css +39 -0
- package/src/public/components/keys-panel.css +171 -0
- package/src/public/components/sticky-note.css +25 -4
- package/src/public/components/terminal.css +5 -0
- package/src/public/extra-keys.js +226 -102
- package/src/public/index.html +41 -9
- package/src/public/input-overlay.js +37 -12
- package/src/public/key-encoder.js +137 -0
- package/src/public/keys-panel.js +268 -0
- package/src/public/manifest.json +2 -2
- package/src/public/mobile.css +15 -15
- package/src/public/screenshot-narrow.png +0 -0
- package/src/public/screenshot-wide.png +0 -0
- package/src/public/service-worker.js +12 -2
- package/src/public/splash/ipad11-landscape.png +0 -0
- package/src/public/splash/ipad11-portrait.png +0 -0
- package/src/public/splash/iphone16-landscape.png +0 -0
- package/src/public/splash/iphone16-portrait.png +0 -0
- package/src/public/splits.js +82 -19
- package/src/public/terminal-copy.js +56 -0
- 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 +96 -68
- package/src/usage-reader.js +11 -1
package/package.json
CHANGED
package/src/public/app.js
CHANGED
|
@@ -402,6 +402,10 @@ class ClaudeCodeWebInterface {
|
|
|
402
402
|
});
|
|
403
403
|
|
|
404
404
|
window.addEventListener('resize', () => {
|
|
405
|
+
// Skip during a soft-keyboard transition: the keyboard controller
|
|
406
|
+
// owns the (coalesced) fit. On layout-viewport-resizing browsers a
|
|
407
|
+
// window.resize fires mid-animation and would race a competing fit.
|
|
408
|
+
if (this._inKeyboardTransition) return;
|
|
405
409
|
this.fitTerminal();
|
|
406
410
|
});
|
|
407
411
|
|
|
@@ -571,6 +575,63 @@ class ClaudeCodeWebInterface {
|
|
|
571
575
|
});
|
|
572
576
|
}
|
|
573
577
|
|
|
578
|
+
// Upload an image over HTTP (POST /api/images/upload) instead of the WS
|
|
579
|
+
// image_upload path. A real photo's base64 (~5.5 MB) exceeds the 1 MiB WS
|
|
580
|
+
// JSON guard, which force-closes the socket with 1009 — previously surfaced
|
|
581
|
+
// (wrongly) as "A voice message was rejected". HTTP has a 20 MB body limit.
|
|
582
|
+
// On success the returned temp path is injected into the terminal.
|
|
583
|
+
_uploadImage(imageData, sessionId) {
|
|
584
|
+
sessionId = sessionId || this.currentClaudeSessionId;
|
|
585
|
+
if (!sessionId) { this._imageError('No session joined'); return; }
|
|
586
|
+
const caption = imageData.caption || '';
|
|
587
|
+
const body = JSON.stringify({
|
|
588
|
+
sessionId: sessionId,
|
|
589
|
+
base64: imageData.base64,
|
|
590
|
+
mimeType: imageData.mimeType,
|
|
591
|
+
fileName: imageData.fileName || 'pasted-image.png',
|
|
592
|
+
caption: caption
|
|
593
|
+
});
|
|
594
|
+
this.authFetch('/api/images/upload', {
|
|
595
|
+
method: 'POST',
|
|
596
|
+
headers: { 'Content-Type': 'application/json' },
|
|
597
|
+
body: body
|
|
598
|
+
}).then(async (resp) => {
|
|
599
|
+
if (!resp.ok) {
|
|
600
|
+
let msg = 'HTTP ' + resp.status;
|
|
601
|
+
try { const j = await resp.json(); if (j && j.error) msg = j.error; } catch (_) {}
|
|
602
|
+
this._imageError(msg);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const data = await resp.json();
|
|
606
|
+
if (!data || !data.filePath) {
|
|
607
|
+
this._imageError('Upload succeeded but no file path was returned');
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this._injectImagePath(data.filePath, caption);
|
|
611
|
+
}).catch((e) => this._imageError((e && e.message) || 'Upload failed'));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Inject an uploaded image's temp path into the active terminal (caption +
|
|
615
|
+
// quoted path), matching the legacy image_upload_complete behavior.
|
|
616
|
+
_injectImagePath(filePath, caption) {
|
|
617
|
+
if (!filePath) return;
|
|
618
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
619
|
+
const quotedPath = '"' + normalizedPath + '"';
|
|
620
|
+
const inputText = caption ? caption + ' ' + quotedPath : quotedPath;
|
|
621
|
+
let normalized = attachClipboardHandler.normalizeLineEndings(inputText);
|
|
622
|
+
if (this.terminal && this.terminal.modes && this.terminal.modes.bracketedPasteMode) {
|
|
623
|
+
normalized = attachClipboardHandler.wrapBracketedPaste(normalized);
|
|
624
|
+
}
|
|
625
|
+
this.send({ type: 'input', data: normalized });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
_imageError(msg) {
|
|
629
|
+
if (window.feedback) window.feedback.warning('Image upload failed: ' + msg);
|
|
630
|
+
if (this.terminal) {
|
|
631
|
+
this.terminal.write('\r\n\x1b[31m[Image upload error] ' + msg + '\x1b[0m\r\n');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
574
635
|
sendEscape() {
|
|
575
636
|
// Send ESC key to terminal
|
|
576
637
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
@@ -631,7 +692,7 @@ class ClaudeCodeWebInterface {
|
|
|
631
692
|
foreground: '#f0f6fc',
|
|
632
693
|
cursor: '#ff6b00',
|
|
633
694
|
cursorAccent: '#0d1117',
|
|
634
|
-
|
|
695
|
+
selectionBackground: 'rgba(255, 107, 0, 0.2)',
|
|
635
696
|
black: '#484f58',
|
|
636
697
|
red: '#ff7b72',
|
|
637
698
|
green: '#7ee787',
|
|
@@ -680,6 +741,20 @@ class ClaudeCodeWebInterface {
|
|
|
680
741
|
|
|
681
742
|
this.terminal.open(document.getElementById('terminal'));
|
|
682
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
|
+
|
|
683
758
|
// Faithful per-tab snapshot cache for instant tab-switch repaint.
|
|
684
759
|
// Guarded: if the serialize addon or the cache class failed to load
|
|
685
760
|
// (e.g. CDN blocked), snapshotCache stays null and every call site is
|
|
@@ -735,6 +810,10 @@ class ClaudeCodeWebInterface {
|
|
|
735
810
|
if (termContainerEl && typeof ResizeObserver !== 'undefined') {
|
|
736
811
|
let resizeTimeout;
|
|
737
812
|
new ResizeObserver(() => {
|
|
813
|
+
// During a soft-keyboard transition the dedicated keyboard
|
|
814
|
+
// controller owns the (single, coalesced) fit — skip here so we
|
|
815
|
+
// don't fire a second competing fit mid-animation (flicker).
|
|
816
|
+
if (this._inKeyboardTransition) return;
|
|
738
817
|
clearTimeout(resizeTimeout);
|
|
739
818
|
resizeTimeout = setTimeout(() => {
|
|
740
819
|
this.fitTerminal();
|
|
@@ -758,16 +837,7 @@ class ClaudeCodeWebInterface {
|
|
|
758
837
|
this._imageHandler = window.imageHandler.attachImageHandler(
|
|
759
838
|
this.terminal, termContainer, {
|
|
760
839
|
onImageReady: (imageData) => {
|
|
761
|
-
this.
|
|
762
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
763
|
-
this.send({
|
|
764
|
-
type: 'image_upload',
|
|
765
|
-
base64: imageData.base64,
|
|
766
|
-
mimeType: imageData.mimeType,
|
|
767
|
-
fileName: imageData.fileName || 'pasted-image.png',
|
|
768
|
-
caption: imageData.caption || ''
|
|
769
|
-
});
|
|
770
|
-
}
|
|
840
|
+
this._uploadImage(imageData);
|
|
771
841
|
},
|
|
772
842
|
// Non-image files pasted from a file manager (clipboardData
|
|
773
843
|
// carries File objects) — route through the generic pipeline.
|
|
@@ -799,16 +869,7 @@ class ClaudeCodeWebInterface {
|
|
|
799
869
|
files && files.length) {
|
|
800
870
|
try {
|
|
801
871
|
window.imageHandler.showImagePreview(files[0], (imageData) => {
|
|
802
|
-
this.
|
|
803
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
804
|
-
this.send({
|
|
805
|
-
type: 'image_upload',
|
|
806
|
-
base64: imageData.base64,
|
|
807
|
-
mimeType: imageData.mimeType,
|
|
808
|
-
fileName: imageData.fileName || 'pasted-image.png',
|
|
809
|
-
caption: imageData.caption || ''
|
|
810
|
-
});
|
|
811
|
-
}
|
|
872
|
+
this._uploadImage(imageData);
|
|
812
873
|
});
|
|
813
874
|
} catch (_) { /* ignore */ }
|
|
814
875
|
}
|
|
@@ -847,8 +908,10 @@ class ClaudeCodeWebInterface {
|
|
|
847
908
|
}
|
|
848
909
|
this._ctrlModifierPending = false;
|
|
849
910
|
if (this.extraKeys) {
|
|
850
|
-
|
|
851
|
-
|
|
911
|
+
// Use the shared consume so the sticky-Ctrl 5s timeout is
|
|
912
|
+
// cleared too (a stale timeout could otherwise cancel a
|
|
913
|
+
// later Ctrl press).
|
|
914
|
+
this.extraKeys._consumeCtrl();
|
|
852
915
|
}
|
|
853
916
|
}
|
|
854
917
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
@@ -932,7 +995,13 @@ class ClaudeCodeWebInterface {
|
|
|
932
995
|
_setupExtraKeys() {
|
|
933
996
|
if (!this.isMobile || typeof ExtraKeys === 'undefined') return;
|
|
934
997
|
|
|
998
|
+
document.body.classList.add('is-mobile');
|
|
935
999
|
this.extraKeys = new ExtraKeys({ app: this });
|
|
1000
|
+
// Control-mode "all keys" panel (keyboard-down). Launcher FAB is shown
|
|
1001
|
+
// via the .is-mobile body class. See keys-panel.js / ADR-0037.
|
|
1002
|
+
if (typeof KeysPanel !== 'undefined') {
|
|
1003
|
+
this.keysPanel = new KeysPanel({ app: this });
|
|
1004
|
+
}
|
|
936
1005
|
this._keyboardOpen = false;
|
|
937
1006
|
|
|
938
1007
|
// Browsers without visualViewport (Firefox Android, Samsung Internet):
|
|
@@ -970,6 +1039,11 @@ class ClaudeCodeWebInterface {
|
|
|
970
1039
|
this._keyboardOpen = false;
|
|
971
1040
|
document.body.classList.remove('keyboard-open');
|
|
972
1041
|
this._restoreTerminalFromKeyboard();
|
|
1042
|
+
} else if (heightDiff > threshold && this._keyboardOpen) {
|
|
1043
|
+
// Keyboard already open but the viewport height changed (final
|
|
1044
|
+
// animation frame, or rotation while open) — re-apply the size so
|
|
1045
|
+
// the terminal isn't left fit against a stale intermediate height.
|
|
1046
|
+
this._updateKeyboardHeight(currentHeight);
|
|
973
1047
|
}
|
|
974
1048
|
};
|
|
975
1049
|
|
|
@@ -1045,26 +1119,65 @@ class ClaudeCodeWebInterface {
|
|
|
1045
1119
|
}
|
|
1046
1120
|
|
|
1047
1121
|
_adjustTerminalForKeyboard(availableHeight) {
|
|
1048
|
-
if (this.extraKeys)
|
|
1122
|
+
if (this.extraKeys) {
|
|
1123
|
+
this.extraKeys.show();
|
|
1124
|
+
this.extraKeys._updateRow2Visibility();
|
|
1125
|
+
}
|
|
1126
|
+
this._updateKeyboardHeight(availableHeight);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Apply (or re-apply) the terminal size for the current keyboard-open
|
|
1130
|
+
// viewport height. Called on the first threshold crossing AND on every later
|
|
1131
|
+
// visualViewport frame while the keyboard stays open (final animation frame,
|
|
1132
|
+
// or rotation) so the terminal never fits against a stale intermediate
|
|
1133
|
+
// height and extends under the keyboard. Style writes are cheap; the xterm
|
|
1134
|
+
// fit is coalesced into one rAF below.
|
|
1135
|
+
_updateKeyboardHeight(availableHeight) {
|
|
1136
|
+
this._inKeyboardTransition = true;
|
|
1049
1137
|
document.documentElement.style.setProperty('--visual-viewport-height', availableHeight + 'px');
|
|
1050
1138
|
const termEl = document.getElementById('terminal');
|
|
1051
1139
|
if (termEl) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
termEl.style.height = (availableHeight - (this.extraKeys?.container?.offsetHeight || 44)) + 'px';
|
|
1056
|
-
if (this.fitAddon) this.fitAddon.fit();
|
|
1140
|
+
const barH = (this.extraKeys && this.extraKeys.container)
|
|
1141
|
+
? (this.extraKeys.container.offsetHeight || 44) : 0;
|
|
1142
|
+
termEl.style.height = (availableHeight - barH) + 'px';
|
|
1057
1143
|
}
|
|
1144
|
+
this._scheduleKeyboardFit();
|
|
1145
|
+
this._endKeyboardTransitionSoon();
|
|
1058
1146
|
}
|
|
1059
1147
|
|
|
1060
1148
|
_restoreTerminalFromKeyboard() {
|
|
1149
|
+
this._inKeyboardTransition = true;
|
|
1061
1150
|
if (this.extraKeys) this.extraKeys.hide();
|
|
1062
1151
|
document.documentElement.style.removeProperty('--visual-viewport-height');
|
|
1063
1152
|
const termEl = document.getElementById('terminal');
|
|
1064
|
-
if (termEl)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1153
|
+
if (termEl) termEl.style.height = '';
|
|
1154
|
+
this._scheduleKeyboardFit();
|
|
1155
|
+
this._endKeyboardTransitionSoon();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Coalesce fits during a keyboard transition into a single rAF. Uses
|
|
1159
|
+
// fitTerminal() (which applies the mobile row/column adjustments) and refits
|
|
1160
|
+
// split panes — NOT a raw fitAddon.fit() that would bypass both.
|
|
1161
|
+
_scheduleKeyboardFit() {
|
|
1162
|
+
if (this._kbFitRaf) cancelAnimationFrame(this._kbFitRaf);
|
|
1163
|
+
this._kbFitRaf = requestAnimationFrame(() => {
|
|
1164
|
+
this._kbFitRaf = null;
|
|
1165
|
+
this.fitTerminal();
|
|
1166
|
+
if (this.splitContainer && this.splitContainer.splits) {
|
|
1167
|
+
this.splitContainer.splits.forEach(s => { try { s.fit(); } catch (_) {} });
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// End the transition window after the CSS chrome-collapse would have settled,
|
|
1173
|
+
// then do one final measured fit to reconcile the resting layout.
|
|
1174
|
+
_endKeyboardTransitionSoon() {
|
|
1175
|
+
if (this._kbTransitionTimer) clearTimeout(this._kbTransitionTimer);
|
|
1176
|
+
this._kbTransitionTimer = setTimeout(() => {
|
|
1177
|
+
this._kbTransitionTimer = null;
|
|
1178
|
+
this._inKeyboardTransition = false;
|
|
1179
|
+
this._scheduleKeyboardFit();
|
|
1180
|
+
}, 320);
|
|
1068
1181
|
}
|
|
1069
1182
|
|
|
1070
1183
|
showSessionSelectionModal() {
|
|
@@ -1190,16 +1303,7 @@ class ClaudeCodeWebInterface {
|
|
|
1190
1303
|
// Fallback: generic handler unavailable — keep the legacy image-only picker.
|
|
1191
1304
|
attachBtn.addEventListener('click', () => {
|
|
1192
1305
|
window.imageHandler.triggerFilePicker((imageData) => {
|
|
1193
|
-
this.
|
|
1194
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
1195
|
-
this.send({
|
|
1196
|
-
type: 'image_upload',
|
|
1197
|
-
base64: imageData.base64,
|
|
1198
|
-
mimeType: imageData.mimeType,
|
|
1199
|
-
fileName: imageData.fileName || 'attached-image.png',
|
|
1200
|
-
caption: imageData.caption || ''
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1306
|
+
this._uploadImage(imageData);
|
|
1203
1307
|
});
|
|
1204
1308
|
});
|
|
1205
1309
|
}
|
|
@@ -1300,6 +1404,22 @@ class ClaudeCodeWebInterface {
|
|
|
1300
1404
|
this.closeMobileMenu();
|
|
1301
1405
|
});
|
|
1302
1406
|
}
|
|
1407
|
+
|
|
1408
|
+
// Mobile image attach — the desktop attach button is hidden on mobile,
|
|
1409
|
+
// so this is the mobile entry point. The file picker offers Photo Library
|
|
1410
|
+
// / Take Photo / Files on iOS; the picked image goes through the preview
|
|
1411
|
+
// modal and the HTTP upload path (_uploadImage).
|
|
1412
|
+
const attachImageBtnMobile = document.getElementById('attachImageBtnMobile');
|
|
1413
|
+
if (attachImageBtnMobile) {
|
|
1414
|
+
attachImageBtnMobile.addEventListener('click', () => {
|
|
1415
|
+
this.closeMobileMenu();
|
|
1416
|
+
if (window.imageHandler && typeof window.imageHandler.triggerFilePicker === 'function') {
|
|
1417
|
+
window.imageHandler.triggerFilePicker((imageData) => this._uploadImage(imageData));
|
|
1418
|
+
} else if (window.feedback) {
|
|
1419
|
+
window.feedback.warning('Image upload is unavailable');
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1303
1423
|
|
|
1304
1424
|
// Mobile sessions button
|
|
1305
1425
|
const sessionsBtnMobile = document.getElementById('sessionsBtnMobile');
|
|
@@ -2187,7 +2307,17 @@ class ClaudeCodeWebInterface {
|
|
|
2187
2307
|
if (restartGen !== this._socketGeneration) return;
|
|
2188
2308
|
this.reconnect();
|
|
2189
2309
|
}, restartBackoff);
|
|
2190
|
-
|
|
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) {
|
|
2191
2321
|
this.updateStatus('Reconnecting (' + (this.reconnectAttempts + 1) + '/' + this.maxReconnectAttempts + ')...');
|
|
2192
2322
|
// First attempt is fast (250ms covers a server-process restart window);
|
|
2193
2323
|
// subsequent attempts use exponential backoff with jitter.
|
|
@@ -2209,7 +2339,11 @@ class ClaudeCodeWebInterface {
|
|
|
2209
2339
|
this.reconnectAttempts++;
|
|
2210
2340
|
} else {
|
|
2211
2341
|
this.updateStatus('Disconnected');
|
|
2212
|
-
|
|
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`);
|
|
2213
2347
|
}
|
|
2214
2348
|
};
|
|
2215
2349
|
|
|
@@ -3467,40 +3601,30 @@ class ClaudeCodeWebInterface {
|
|
|
3467
3601
|
}
|
|
3468
3602
|
|
|
3469
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.
|
|
3470
3608
|
if (this.isMobile) {
|
|
3471
|
-
console.log('[Renderer] Mobile detected, using
|
|
3472
|
-
this._loadCanvasAddon();
|
|
3609
|
+
console.log('[Renderer] Mobile detected, using DOM renderer for reliability');
|
|
3473
3610
|
return;
|
|
3474
3611
|
}
|
|
3475
3612
|
if (typeof WebglAddon !== 'undefined') {
|
|
3476
3613
|
try {
|
|
3477
3614
|
this.webglAddon = new WebglAddon.WebglAddon();
|
|
3478
3615
|
this.webglAddon.onContextLoss(() => {
|
|
3479
|
-
this.webglAddon
|
|
3616
|
+
const addon = this.webglAddon;
|
|
3480
3617
|
this.webglAddon = null;
|
|
3481
|
-
|
|
3618
|
+
try { if (addon) addon.dispose(); } catch (_) {}
|
|
3619
|
+
console.log('[Renderer] WebGL context lost, using DOM renderer');
|
|
3482
3620
|
});
|
|
3483
3621
|
this.terminal.loadAddon(this.webglAddon);
|
|
3484
3622
|
} catch (e) {
|
|
3485
|
-
|
|
3486
|
-
|
|
3623
|
+
this.webglAddon = null;
|
|
3624
|
+
console.log('[Renderer] WebGL unavailable, using DOM renderer');
|
|
3487
3625
|
}
|
|
3488
3626
|
} else {
|
|
3489
|
-
console.log('[Renderer] WebGL unavailable, using
|
|
3490
|
-
this._loadCanvasAddon();
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
|
|
3494
|
-
_loadCanvasAddon() {
|
|
3495
|
-
if (typeof CanvasAddon !== 'undefined') {
|
|
3496
|
-
try {
|
|
3497
|
-
this.canvasAddon = new CanvasAddon.CanvasAddon();
|
|
3498
|
-
this.terminal.loadAddon(this.canvasAddon);
|
|
3499
|
-
} catch (e) {
|
|
3500
|
-
console.log('[Renderer] Canvas unavailable, using DOM renderer (slower)');
|
|
3501
|
-
}
|
|
3502
|
-
} else {
|
|
3503
|
-
console.log('[Renderer] Canvas unavailable, using DOM renderer (slower)');
|
|
3627
|
+
console.log('[Renderer] WebGL unavailable, using DOM renderer');
|
|
3504
3628
|
}
|
|
3505
3629
|
}
|
|
3506
3630
|
|
|
@@ -3900,7 +4024,6 @@ class ClaudeCodeWebInterface {
|
|
|
3900
4024
|
break;
|
|
3901
4025
|
}
|
|
3902
4026
|
case 'pasteImage': {
|
|
3903
|
-
const pasteSocket = activeSocket;
|
|
3904
4027
|
try {
|
|
3905
4028
|
if (navigator.clipboard && typeof navigator.clipboard.read === 'function') {
|
|
3906
4029
|
const items = await navigator.clipboard.read();
|
|
@@ -3911,17 +4034,7 @@ class ClaudeCodeWebInterface {
|
|
|
3911
4034
|
if (imageType) {
|
|
3912
4035
|
const blob = await item.getType(imageType);
|
|
3913
4036
|
window.imageHandler.showImagePreview(blob, (imageData) => {
|
|
3914
|
-
this.
|
|
3915
|
-
const msg = JSON.stringify({
|
|
3916
|
-
type: 'image_upload',
|
|
3917
|
-
base64: imageData.base64,
|
|
3918
|
-
mimeType: imageData.mimeType,
|
|
3919
|
-
fileName: imageData.fileName || 'pasted-image.png',
|
|
3920
|
-
caption: imageData.caption || ''
|
|
3921
|
-
});
|
|
3922
|
-
if (pasteSocket && pasteSocket.readyState === WebSocket.OPEN) {
|
|
3923
|
-
pasteSocket.send(msg);
|
|
3924
|
-
}
|
|
4037
|
+
this._uploadImage(imageData);
|
|
3925
4038
|
});
|
|
3926
4039
|
return;
|
|
3927
4040
|
}
|
|
@@ -3949,19 +4062,8 @@ class ClaudeCodeWebInterface {
|
|
|
3949
4062
|
{ multiple: true }
|
|
3950
4063
|
);
|
|
3951
4064
|
} else if (window.imageHandler) {
|
|
3952
|
-
const attachSocket = activeSocket;
|
|
3953
4065
|
window.imageHandler.triggerFilePicker((imageData) => {
|
|
3954
|
-
this.
|
|
3955
|
-
const msg = JSON.stringify({
|
|
3956
|
-
type: 'image_upload',
|
|
3957
|
-
base64: imageData.base64,
|
|
3958
|
-
mimeType: imageData.mimeType,
|
|
3959
|
-
fileName: imageData.fileName || 'attached-image.png',
|
|
3960
|
-
caption: imageData.caption || ''
|
|
3961
|
-
});
|
|
3962
|
-
if (attachSocket && attachSocket.readyState === WebSocket.OPEN) {
|
|
3963
|
-
attachSocket.send(msg);
|
|
3964
|
-
}
|
|
4066
|
+
this._uploadImage(imageData);
|
|
3965
4067
|
});
|
|
3966
4068
|
}
|
|
3967
4069
|
break;
|
|
@@ -4175,6 +4277,9 @@ class ClaudeCodeWebInterface {
|
|
|
4175
4277
|
const enableSessionStickyNotes = document.getElementById('enableSessionStickyNotes');
|
|
4176
4278
|
if (enableSessionStickyNotes) enableSessionStickyNotes.checked = settings.enableSessionStickyNotes ?? true;
|
|
4177
4279
|
|
|
4280
|
+
const wheelScrollMode = document.getElementById('wheelScrollMode');
|
|
4281
|
+
if (wheelScrollMode) wheelScrollMode.value = settings.wheelScrollMode || 'dontHijack';
|
|
4282
|
+
|
|
4178
4283
|
// Update install section
|
|
4179
4284
|
this._updateInstallSection();
|
|
4180
4285
|
}
|
|
@@ -4207,10 +4312,6 @@ class ClaudeCodeWebInterface {
|
|
|
4207
4312
|
|
|
4208
4313
|
// Capture the socket at attach-initiation time. The image preview modal
|
|
4209
4314
|
// is async (user-driven); the active session/socket can change while it
|
|
4210
|
-
// is open. Sending on a captured target avoids the upload landing on a
|
|
4211
|
-
// different session (mirrors the context-menu's existing capture).
|
|
4212
|
-
const targetSocket = this.socket;
|
|
4213
|
-
|
|
4214
4315
|
// Images: reuse the existing single-preview flow. The modal handles one
|
|
4215
4316
|
// image at a time, so if several images are selected we attach the first
|
|
4216
4317
|
// and tell the user rather than silently dropping the rest.
|
|
@@ -4219,16 +4320,7 @@ class ClaudeCodeWebInterface {
|
|
|
4219
4320
|
window.feedback.info('Only the first image is attached — attach images one at a time.');
|
|
4220
4321
|
}
|
|
4221
4322
|
ih.showImagePreview(images[0], (imageData) => {
|
|
4222
|
-
this.
|
|
4223
|
-
if (targetSocket && targetSocket.readyState === WebSocket.OPEN) {
|
|
4224
|
-
targetSocket.send(JSON.stringify({
|
|
4225
|
-
type: 'image_upload',
|
|
4226
|
-
base64: imageData.base64,
|
|
4227
|
-
mimeType: imageData.mimeType,
|
|
4228
|
-
fileName: imageData.fileName || 'attached-image.png',
|
|
4229
|
-
caption: imageData.caption || ''
|
|
4230
|
-
}));
|
|
4231
|
-
}
|
|
4323
|
+
this._uploadImage(imageData);
|
|
4232
4324
|
});
|
|
4233
4325
|
}
|
|
4234
4326
|
|
|
@@ -4488,7 +4580,11 @@ class ClaudeCodeWebInterface {
|
|
|
4488
4580
|
notifVolume: 30,
|
|
4489
4581
|
notifDesktop: true,
|
|
4490
4582
|
enableSessionStickyNotes: true,
|
|
4491
|
-
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'
|
|
4492
4588
|
};
|
|
4493
4589
|
}
|
|
4494
4590
|
|
|
@@ -4531,7 +4627,8 @@ class ClaudeCodeWebInterface {
|
|
|
4531
4627
|
notifVolume: parseInt(document.getElementById('notifVolume')?.value || '30'),
|
|
4532
4628
|
notifDesktop: document.getElementById('notifDesktop')?.checked ?? true,
|
|
4533
4629
|
enableSessionStickyNotes: document.getElementById('enableSessionStickyNotes')?.checked ?? true,
|
|
4534
|
-
tabSnapshotLines: parseInt(document.getElementById('tabSnapshotLines')?.value || '500', 10)
|
|
4630
|
+
tabSnapshotLines: parseInt(document.getElementById('tabSnapshotLines')?.value || '500', 10),
|
|
4631
|
+
wheelScrollMode: document.getElementById('wheelScrollMode')?.value || 'dontHijack'
|
|
4535
4632
|
};
|
|
4536
4633
|
|
|
4537
4634
|
try {
|
|
@@ -4580,6 +4677,10 @@ class ClaudeCodeWebInterface {
|
|
|
4580
4677
|
this.terminal.options.cursorBlink = settings.cursorBlink ?? true;
|
|
4581
4678
|
if (settings.scrollback) this.terminal.options.scrollback = settings.scrollback;
|
|
4582
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
|
+
|
|
4583
4684
|
// Push the instant-snapshot line cap to the cache (0 disables capture+paint).
|
|
4584
4685
|
if (this.snapshotCache && settings.tabSnapshotLines !== undefined) {
|
|
4585
4686
|
this.snapshotCache.setMaxLines(settings.tabSnapshotLines);
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
const SDK_SOURCE_IN = 'ai-or-die-artifact-sdk'; // messages FROM the iframe
|
|
21
21
|
const HOST_SOURCE_OUT = 'ai-or-die-artifact-host'; // messages TO the iframe
|
|
22
22
|
const STORE_KEY = 'ai-or-die:artifact-panel:layout';
|
|
23
|
+
const PHONE_MQ = '(max-width:640px)';
|
|
23
24
|
|
|
24
25
|
const MIN_W = 320;
|
|
25
26
|
const MIN_H = 240;
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
|
|
61
62
|
this._layout = this._loadLayout();
|
|
62
63
|
this._buildDom();
|
|
64
|
+
this._wirePhoneMode();
|
|
63
65
|
this._applyLayout();
|
|
64
66
|
this._minimized = !!this._layout.minimized;
|
|
65
67
|
this._applyMinimized();
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
return {};
|
|
83
85
|
}
|
|
84
86
|
_saveLayout() {
|
|
87
|
+
if (this._isPhone()) return;
|
|
85
88
|
try {
|
|
86
89
|
window.localStorage.setItem(STORE_KEY, JSON.stringify({
|
|
87
90
|
left: this._layout.left,
|
|
@@ -93,6 +96,39 @@
|
|
|
93
96
|
} catch (_) { /* non-fatal */ }
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
_isPhone() {
|
|
100
|
+
return !!(typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia(PHONE_MQ).matches);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_wirePhoneMode() {
|
|
104
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
|
|
105
|
+
const mql = window.matchMedia(PHONE_MQ);
|
|
106
|
+
this._phoneMql = mql;
|
|
107
|
+
this._onPhoneChange = (e) => {
|
|
108
|
+
const matches = !!(e && typeof e.matches === 'boolean' ? e.matches : mql.matches);
|
|
109
|
+
if (matches) {
|
|
110
|
+
this._cancelPointerInteraction();
|
|
111
|
+
this._clearInlineGeometry();
|
|
112
|
+
} else {
|
|
113
|
+
this._applyLayout();
|
|
114
|
+
this._clampToBounds();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
if (typeof mql.addEventListener === 'function') mql.addEventListener('change', this._onPhoneChange);
|
|
118
|
+
else if (typeof mql.addListener === 'function') mql.addListener(this._onPhoneChange);
|
|
119
|
+
if (mql.matches) this._clearInlineGeometry();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_clearInlineGeometry() {
|
|
123
|
+
if (!this.el) return;
|
|
124
|
+
this.el.style.left = '';
|
|
125
|
+
this.el.style.top = '';
|
|
126
|
+
this.el.style.right = '';
|
|
127
|
+
this.el.style.bottom = '';
|
|
128
|
+
this.el.style.width = '';
|
|
129
|
+
this.el.style.height = '';
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
_buildDom() {
|
|
97
133
|
this.el = el('div', { class: 'artifact-panel', id: 'artifactPanel', hidden: 'hidden', role: 'dialog', 'aria-label': 'Artifact review' });
|
|
98
134
|
|
|
@@ -167,6 +203,10 @@
|
|
|
167
203
|
|
|
168
204
|
// ---- floating-window geometry ----------------------------------------
|
|
169
205
|
_applyLayout() {
|
|
206
|
+
if (this._isPhone()) {
|
|
207
|
+
this._clearInlineGeometry();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
170
210
|
const L = this._layout;
|
|
171
211
|
if (typeof L.width === 'number') this.el.style.width = clampNumber(L.width, MIN_W, 4000) + 'px';
|
|
172
212
|
if (typeof L.height === 'number') this.el.style.height = clampNumber(L.height, MIN_H, 4000) + 'px';
|
|
@@ -186,6 +226,7 @@
|
|
|
186
226
|
// live panel size, then persists the corrected value. Safe to call when the
|
|
187
227
|
// panel is visible (a hidden panel measures 0x0, so we skip then).
|
|
188
228
|
_clampToBounds() {
|
|
229
|
+
if (this._isPhone()) return;
|
|
189
230
|
const L = this._layout;
|
|
190
231
|
if (typeof L.left !== 'number' || typeof L.top !== 'number') return;
|
|
191
232
|
if (this.el.hidden) return;
|
|
@@ -211,10 +252,20 @@
|
|
|
211
252
|
return { width: wr.width, height: wr.height };
|
|
212
253
|
}
|
|
213
254
|
|
|
255
|
+
_cancelPointerInteraction() {
|
|
256
|
+
if (this._dragCleanup) { try { this._dragCleanup(); } catch (_) { /* ignore */ } this._dragCleanup = null; }
|
|
257
|
+
if (this._resizeCleanup) { try { this._resizeCleanup(); } catch (_) { /* ignore */ } this._resizeCleanup = null; }
|
|
258
|
+
}
|
|
259
|
+
|
|
214
260
|
_wireDrag(handle) {
|
|
215
261
|
let startX = 0, startY = 0, originLeft = 0, originTop = 0, dragging = false;
|
|
216
262
|
const onMove = (e) => {
|
|
217
263
|
if (!dragging) return;
|
|
264
|
+
if (this._isPhone()) {
|
|
265
|
+
dragging = false;
|
|
266
|
+
this._cancelPointerInteraction();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
218
269
|
const p = this._point(e);
|
|
219
270
|
const b = this._bounds();
|
|
220
271
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -236,6 +287,7 @@
|
|
|
236
287
|
this._saveLayout();
|
|
237
288
|
};
|
|
238
289
|
handle.addEventListener('mousedown', (e) => {
|
|
290
|
+
if (this._isPhone()) return;
|
|
239
291
|
// Ignore drags that start on a header button.
|
|
240
292
|
if (e.target && e.target.closest && e.target.closest('.artifact-panel__btn')) return;
|
|
241
293
|
e.preventDefault();
|
|
@@ -265,6 +317,11 @@
|
|
|
265
317
|
let startX = 0, startY = 0, originW = 0, originH = 0, resizing = false;
|
|
266
318
|
const onMove = (e) => {
|
|
267
319
|
if (!resizing) return;
|
|
320
|
+
if (this._isPhone()) {
|
|
321
|
+
resizing = false;
|
|
322
|
+
this._cancelPointerInteraction();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
268
325
|
const p = this._point(e);
|
|
269
326
|
const b = this._bounds();
|
|
270
327
|
const rect = this.el.getBoundingClientRect();
|
|
@@ -288,6 +345,7 @@
|
|
|
288
345
|
this._saveLayout();
|
|
289
346
|
};
|
|
290
347
|
handle.addEventListener('mousedown', (e) => {
|
|
348
|
+
if (this._isPhone()) return;
|
|
291
349
|
e.preventDefault();
|
|
292
350
|
e.stopPropagation();
|
|
293
351
|
const p = this._point(e);
|
|
@@ -886,6 +944,10 @@
|
|
|
886
944
|
destroy() {
|
|
887
945
|
window.removeEventListener('message', this._onWindowMessage);
|
|
888
946
|
if (this._onWindowResize) window.removeEventListener('resize', this._onWindowResize);
|
|
947
|
+
if (this._phoneMql && this._onPhoneChange) {
|
|
948
|
+
if (typeof this._phoneMql.removeEventListener === 'function') this._phoneMql.removeEventListener('change', this._onPhoneChange);
|
|
949
|
+
else if (typeof this._phoneMql.removeListener === 'function') this._phoneMql.removeListener(this._onPhoneChange);
|
|
950
|
+
}
|
|
889
951
|
this._teardownSse();
|
|
890
952
|
if (this._dragCleanup) { try { this._dragCleanup(); } catch (_) { /* ignore */ } this._dragCleanup = null; }
|
|
891
953
|
if (this._resizeCleanup) { try { this._resizeCleanup(); } catch (_) { /* ignore */ } this._resizeCleanup = null; }
|