claudefix 2.5.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/claude-fixed.js +133 -147
- package/package.json +1 -1
package/bin/claude-fixed.js
CHANGED
|
@@ -295,131 +295,56 @@ if (debug) {
|
|
|
295
295
|
*/
|
|
296
296
|
function stripColors(data) {
|
|
297
297
|
let str = data;
|
|
298
|
-
const isGtk4 = forceNuclear || terminalType === 'ptyxis' || terminalType === 'gtk4-vte';
|
|
299
298
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
allowed.push('1');
|
|
329
|
-
i++;
|
|
330
|
-
}
|
|
331
|
-
// Standard foreground (30-37)
|
|
332
|
-
else if (code >= 30 && code <= 37) {
|
|
333
|
-
allowed.push(codes[i]);
|
|
334
|
-
i++;
|
|
335
|
-
}
|
|
336
|
-
// Default foreground (39)
|
|
337
|
-
else if (code === 39) {
|
|
338
|
-
allowed.push('39');
|
|
339
|
-
i++;
|
|
340
|
-
}
|
|
341
|
-
// Bright foreground (90-97)
|
|
342
|
-
else if (code >= 90 && code <= 97) {
|
|
343
|
-
allowed.push(codes[i]);
|
|
344
|
-
i++;
|
|
345
|
-
}
|
|
346
|
-
// 256-color foreground (38;5;X)
|
|
347
|
-
else if (code === 38 && codes[i + 1] === '5' && codes[i + 2]) {
|
|
348
|
-
allowed.push('38', '5', codes[i + 2]);
|
|
349
|
-
i += 3;
|
|
350
|
-
}
|
|
351
|
-
// True color foreground (38;2;R;G;B)
|
|
352
|
-
else if (code === 38 && codes[i + 1] === '2' && codes[i + 4]) {
|
|
353
|
-
allowed.push('38', '2', codes[i + 2], codes[i + 3], codes[i + 4]);
|
|
354
|
-
i += 5;
|
|
355
|
-
}
|
|
356
|
-
// Skip backgrounds (40-47, 49, 100-107, 48;5;X, 48;2;R;G;B)
|
|
357
|
-
else if (code >= 40 && code <= 49) {
|
|
358
|
-
i++;
|
|
359
|
-
}
|
|
360
|
-
else if (code >= 100 && code <= 107) {
|
|
361
|
-
i++;
|
|
362
|
-
}
|
|
363
|
-
else if (code === 48 && codes[i + 1] === '5') {
|
|
364
|
-
i += 3; // Skip 48;5;X
|
|
365
|
-
}
|
|
366
|
-
else if (code === 48 && codes[i + 1] === '2') {
|
|
367
|
-
i += 5; // Skip 48;2;R;G;B
|
|
368
|
-
}
|
|
369
|
-
// Skip other problematic codes (2=dim, 7=inverse, 8=hidden)
|
|
370
|
-
else if (code === 2 || code === 7 || code === 8 || code === 22 || code === 27 || code === 28) {
|
|
371
|
-
i++;
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
// Unknown - skip it
|
|
375
|
-
i++;
|
|
376
|
-
}
|
|
299
|
+
// Universal approach: parse each SGR sequence, keep only safe codes
|
|
300
|
+
// This handles ALL compound sequences correctly (e.g. \x1b[1;38;5;196;48;5;236m)
|
|
301
|
+
// by parsing code-by-code instead of regex pattern matching
|
|
302
|
+
str = str.replace(/\x1b\[[0-9;]*m/g, (match) => {
|
|
303
|
+
const codes = match.slice(2, -1).split(';').filter(c => c !== '');
|
|
304
|
+
|
|
305
|
+
if (codes.length === 0) return '\x1b[0m';
|
|
306
|
+
|
|
307
|
+
const allowed = [];
|
|
308
|
+
let i = 0;
|
|
309
|
+
while (i < codes.length) {
|
|
310
|
+
const code = parseInt(codes[i], 10);
|
|
311
|
+
|
|
312
|
+
// Reset
|
|
313
|
+
if (code === 0) { allowed.push('0'); i++; }
|
|
314
|
+
// Bold (1), italic (3), underline (4), strikethrough (9)
|
|
315
|
+
else if (code === 1 || code === 3 || code === 4 || code === 9) { allowed.push(codes[i]); i++; }
|
|
316
|
+
// Bold off (22), italic off (23), underline off (24), strikethrough off (29)
|
|
317
|
+
else if (code === 22 || code === 23 || code === 24 || code === 29) { allowed.push(codes[i]); i++; }
|
|
318
|
+
// Standard foreground (30-37)
|
|
319
|
+
else if (code >= 30 && code <= 37) { allowed.push(codes[i]); i++; }
|
|
320
|
+
// Default foreground (39)
|
|
321
|
+
else if (code === 39) { allowed.push('39'); i++; }
|
|
322
|
+
// Bright foreground (90-97)
|
|
323
|
+
else if (code >= 90 && code <= 97) { allowed.push(codes[i]); i++; }
|
|
324
|
+
// 256-color foreground (38;5;X)
|
|
325
|
+
else if (code === 38 && codes[i + 1] === '5' && codes[i + 2]) {
|
|
326
|
+
allowed.push('38', '5', codes[i + 2]); i += 3;
|
|
377
327
|
}
|
|
328
|
+
// True color foreground (38;2;R;G;B)
|
|
329
|
+
else if (code === 38 && codes[i + 1] === '2' && codes[i + 4]) {
|
|
330
|
+
allowed.push('38', '2', codes[i + 2], codes[i + 3], codes[i + 4]); i += 5;
|
|
331
|
+
}
|
|
332
|
+
// Skip ALL backgrounds: 40-47, 49, 100-107
|
|
333
|
+
else if ((code >= 40 && code <= 49) || (code >= 100 && code <= 107)) { i++; }
|
|
334
|
+
// Skip 256-color bg (48;5;X)
|
|
335
|
+
else if (code === 48 && codes[i + 1] === '5') { i += 3; }
|
|
336
|
+
// Skip true color bg (48;2;R;G;B)
|
|
337
|
+
else if (code === 48 && codes[i + 1] === '2') { i += 5; }
|
|
338
|
+
// Skip dim (2), inverse (7), hidden (8) and their offs
|
|
339
|
+
else if (code === 2 || code === 7 || code === 8 || code === 27 || code === 28) { i++; }
|
|
340
|
+
// Unknown - skip
|
|
341
|
+
else { i++; }
|
|
342
|
+
}
|
|
378
343
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
return str;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Regular mode for other terminals
|
|
387
|
-
// Remove standalone background color sequences entirely
|
|
388
|
-
str = str.replace(/\x1b\[48;5;\d+m/g, ''); // 256-color bg
|
|
389
|
-
str = str.replace(/\x1b\[48;2;\d+;\d+;\d+m/g, ''); // true color bg
|
|
390
|
-
str = str.replace(/\x1b\[4[0-7]m/g, ''); // standard bg 40-47
|
|
391
|
-
str = str.replace(/\x1b\[49m/g, ''); // default bg
|
|
392
|
-
str = str.replace(/\x1b\[10[0-7]m/g, ''); // bright bg 100-107
|
|
393
|
-
str = str.replace(/\x1b\[7m/g, ''); // inverse
|
|
394
|
-
str = str.replace(/\x1b\[27m/g, ''); // inverse off
|
|
395
|
-
|
|
396
|
-
// NUCLEAR MODE: Extra aggressive stripping for VTE issues
|
|
397
|
-
str = str.replace(/\x1b\[2m/g, ''); // dim text (causes grey)
|
|
398
|
-
str = str.replace(/\x1b\[22m/g, ''); // dim off
|
|
399
|
-
str = str.replace(/\x1b\[8m/g, ''); // hidden text
|
|
400
|
-
str = str.replace(/\x1b\[28m/g, ''); // hidden off
|
|
401
|
-
|
|
402
|
-
// Strip any remaining compound sequences with background codes
|
|
403
|
-
// Matches patterns like \x1b[0;48;5;236m or \x1b[38;5;196;48;5;236m
|
|
404
|
-
str = str.replace(/\x1b\[([0-9;]*?)(;?48;[52];[0-9;]+)(;?[0-9;]*)m/g, (match, before, bg, after) => {
|
|
405
|
-
const parts = [before, after].filter(p => p && p.length > 0 && p !== ';');
|
|
406
|
-
if (parts.length === 0) return '\x1b[0m';
|
|
407
|
-
return `\x1b[${parts.join(';').replace(/^;|;$/g, '').replace(/;;+/g, ';')}m`;
|
|
344
|
+
if (allowed.length === 0) return '';
|
|
345
|
+
return `\x1b[${allowed.join(';')}m`;
|
|
408
346
|
});
|
|
409
347
|
|
|
410
|
-
// For combined sequences like \x1b[1;41m (bold + red bg), remove just the bg part
|
|
411
|
-
// This regex finds sequences with bg codes and removes just those codes
|
|
412
|
-
str = str.replace(/\x1b\[([0-9;]*)(?:;?)(4[0-7]|49|10[0-7]|48;5;\d+|48;2;\d+;\d+;\d+)(?:;?)([0-9;]*)m/g,
|
|
413
|
-
(match, before, bg, after) => {
|
|
414
|
-
const parts = [before, after].filter(p => p && p.length > 0);
|
|
415
|
-
if (parts.length === 0) return '';
|
|
416
|
-
return `\x1b[${parts.join(';')}m`;
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Clean up any malformed sequences that might be left
|
|
420
|
-
str = str.replace(/\x1b\[;+m/g, '\x1b[0m'); // \x1b[;;m -> \x1b[0m
|
|
421
|
-
str = str.replace(/\x1b\[m/g, '\x1b[0m'); // \x1b[m -> \x1b[0m
|
|
422
|
-
|
|
423
348
|
return str;
|
|
424
349
|
}
|
|
425
350
|
|
|
@@ -480,6 +405,15 @@ function getTerminalType() {
|
|
|
480
405
|
return 'ptyxis';
|
|
481
406
|
}
|
|
482
407
|
|
|
408
|
+
// XFCE4 Terminal - uses VTE but handles ANSI fine, does NOT need thermonuclear mode
|
|
409
|
+
// Must check BEFORE the VTE version check or it gets misclassified as gtk4-vte
|
|
410
|
+
if (termProgram === 'xfce4-terminal' || termProgram === 'Xfce Terminal' ||
|
|
411
|
+
process.env.XFCE_TERMINAL_VERSION ||
|
|
412
|
+
process.env.WINDOWPATH || // XFCE sets this
|
|
413
|
+
(process.env.XDG_CURRENT_DESKTOP || '').toLowerCase().includes('xfce')) {
|
|
414
|
+
return 'xfce-terminal';
|
|
415
|
+
}
|
|
416
|
+
|
|
483
417
|
// GTK4 terminals (like Ptyxis) often have GDK_BACKEND set
|
|
484
418
|
if (gdkBackend === 'wayland' && vteVersion) {
|
|
485
419
|
return 'gtk4-vte';
|
|
@@ -932,9 +866,22 @@ if (!usePTY) {
|
|
|
932
866
|
);
|
|
933
867
|
}
|
|
934
868
|
|
|
869
|
+
// FIX (Linux only): Enter alternate screen buffer to prevent ghost frames.
|
|
870
|
+
// Ink renders inline (no alternate screen) — content stacks/triplicates on re-render.
|
|
871
|
+
// By entering alternate screen ourselves, Ink's output goes to a clean buffer.
|
|
872
|
+
// On exit, we leave alternate screen to restore the original terminal content.
|
|
873
|
+
if (!isMac && !sshMode) {
|
|
874
|
+
process.stdout.write('\x1b[?1049h\x1b[H\x1b[J'); // Enter alternate screen + clear
|
|
875
|
+
}
|
|
876
|
+
|
|
935
877
|
// Initial setup
|
|
936
878
|
setupScrollRegion();
|
|
937
|
-
if (!sshMode && showFooter)
|
|
879
|
+
if (!sshMode && showFooter) {
|
|
880
|
+
drawFooter();
|
|
881
|
+
// Re-draw footer after Ink's initial setup (Ink may overwrite during startup)
|
|
882
|
+
setTimeout(drawFooter, 500);
|
|
883
|
+
setTimeout(drawFooter, 1500);
|
|
884
|
+
}
|
|
938
885
|
|
|
939
886
|
// Footer refresh - debounced, only redraws AFTER PTY output settles
|
|
940
887
|
// No independent timer - footer only redraws in response to PTY activity
|
|
@@ -969,53 +916,82 @@ if (!usePTY) {
|
|
|
969
916
|
// macOS Terminal.app doesn't have VTE bugs, so skip aggressive escape mangling
|
|
970
917
|
const isAppleTerminal = process.env.TERM_PROGRAM === 'Apple_Terminal';
|
|
971
918
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
919
|
+
// FIX: Buffer-and-flush approach for ghost frame elimination on Linux.
|
|
920
|
+
// Problem: Ink sends renders in multiple small chunks. Injecting \x1b[J on any
|
|
921
|
+
// individual chunk either misses (threshold not met) or nukes partial renders.
|
|
922
|
+
// Solution: Buffer ALL output, flush after a short idle gap (16ms). When flushing,
|
|
923
|
+
// if the buffer contains \x1b[H (home cursor = new render), inject \x1b[J after it.
|
|
924
|
+
// Since the entire render is flushed atomically, clear + content arrive together.
|
|
925
|
+
let outputBuffer = '';
|
|
926
|
+
let flushTimer = null;
|
|
927
|
+
const FLUSH_DELAY_MS = 16; // Normal flush delay (~1 frame)
|
|
928
|
+
const COALESCE_DELAY_MS = 80; // Extended delay during rapid full repaints
|
|
929
|
+
let lastFullRenderTime = 0; // When we last flushed a full repaint
|
|
930
|
+
|
|
931
|
+
function processAndFlush() {
|
|
932
|
+
flushTimer = null;
|
|
933
|
+
if (!outputBuffer || exiting) return;
|
|
934
|
+
|
|
935
|
+
let output = outputBuffer;
|
|
936
|
+
outputBuffer = '';
|
|
937
|
+
|
|
976
938
|
// Only strip colors if config enabled and env var not disabled
|
|
977
|
-
// On macOS: skip color stripping entirely for Apple Terminal (no VTE bugs)
|
|
978
939
|
const shouldStrip = config.colorStripping &&
|
|
979
940
|
process.env.CLAUDE_STRIP_BG_COLORS !== '0' &&
|
|
980
941
|
!isAppleTerminal;
|
|
981
|
-
|
|
942
|
+
if (shouldStrip) output = stripColors(output);
|
|
982
943
|
|
|
983
944
|
// FIX: Intercept scroll region resets from Ink/Claude output.
|
|
984
|
-
// Ink sends \x1b[r which resets scroll region to full terminal,
|
|
985
|
-
// causing our footer area to become content area and old content bleeds through.
|
|
986
|
-
// Replace with our constrained scroll region.
|
|
987
945
|
if (showFooter && !sshMode) {
|
|
988
946
|
const cr = contentRows();
|
|
989
|
-
// Replace bare scroll region reset with our constrained one
|
|
990
947
|
output = output.replace(/\x1b\[r/g, `\x1b[1;${cr}r`);
|
|
991
|
-
// Also catch explicit full-terminal scroll regions like \x1b[1;24r
|
|
992
948
|
const fullRows = process.stdout.rows || 24;
|
|
993
949
|
output = output.replace(new RegExp(`\\x1b\\[1;${fullRows}r`, 'g'), `\x1b[1;${cr}r`);
|
|
994
950
|
}
|
|
995
951
|
|
|
996
|
-
// FIX (Linux only):
|
|
997
|
-
// clear to prevent old content from sticking around in VTE terminals.
|
|
998
|
-
// SKIP on macOS: Terminal.app handles \x1b[3J differently and it causes
|
|
999
|
-
// visual glitches (screen flashing, content disappearing).
|
|
952
|
+
// FIX (Linux only): Inject scrollback clear on full screen clears
|
|
1000
953
|
if (!isMac) {
|
|
1001
|
-
if (output.includes('\x1b[2J')
|
|
954
|
+
if (output.includes('\x1b[2J')) {
|
|
1002
955
|
output = output.replace(/\x1b\[2J/g, '\x1b[2J\x1b[3J');
|
|
1003
956
|
}
|
|
1004
957
|
}
|
|
1005
958
|
|
|
1006
|
-
// FIX (Linux only):
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
959
|
+
// FIX (Linux only): Clear stale content when Ink does a FULL re-render.
|
|
960
|
+
// Ink sends \x1b[H for both full repaints AND partial updates (just prompt).
|
|
961
|
+
// We must ONLY clear on full repaints or we wipe content Ink didn't re-send.
|
|
962
|
+
// Since we buffer the entire render cycle, we can check the buffer size:
|
|
963
|
+
// - Full repaint: large buffer (most of the screen rewritten)
|
|
964
|
+
// - Partial update (prompt only): small buffer
|
|
965
|
+
// Threshold: at least half the screen worth of content (contentRows * 30 bytes)
|
|
966
|
+
// FIX: Strip Ink's alternate screen sequences — we manage alternate screen
|
|
967
|
+
// ourselves. If Ink enters/exits alternate screen, it disrupts our scroll
|
|
968
|
+
// region and footer. Remove these so our alternate screen stays in control.
|
|
969
|
+
if (!isMac && !sshMode) {
|
|
970
|
+
output = output.replace(/\x1b\[\?1049[hl]/g, '');
|
|
971
|
+
// Also strip smcup/rmcup variants
|
|
972
|
+
output = output.replace(/\x1b\[\?47[hl]/g, '');
|
|
1015
973
|
}
|
|
1016
974
|
|
|
1017
975
|
process.stdout.write(output);
|
|
976
|
+
// Re-assert scroll region after each flush (Ink may have sent sequences that
|
|
977
|
+
// override it, even after our replacements)
|
|
978
|
+
if (showFooter && !sshMode) {
|
|
979
|
+
const cr = contentRows();
|
|
980
|
+
process.stdout.write(`\x1b7\x1b[1;${cr}r\x1b8`);
|
|
981
|
+
}
|
|
1018
982
|
if (showFooter) scheduleFooterDraw();
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
ptyProcess.onData((data) => {
|
|
986
|
+
if (exiting) return;
|
|
987
|
+
// Accumulate into buffer
|
|
988
|
+
outputBuffer += data;
|
|
989
|
+
// Reset flush timer — wait for output burst to finish
|
|
990
|
+
if (flushTimer) clearTimeout(flushTimer);
|
|
991
|
+
// Use longer delay if we recently flushed a full render (coalesce rapid repaints)
|
|
992
|
+
const recentFullRender = (Date.now() - lastFullRenderTime) < 200;
|
|
993
|
+
const delay = recentFullRender ? COALESCE_DELAY_MS : FLUSH_DELAY_MS;
|
|
994
|
+
flushTimer = setTimeout(processAndFlush, delay);
|
|
1019
995
|
});
|
|
1020
996
|
|
|
1021
997
|
// Forward stdin with Ctrl+Shift+H hotkey
|
|
@@ -1080,7 +1056,12 @@ if (!usePTY) {
|
|
|
1080
1056
|
clearInterval(memCheckInterval);
|
|
1081
1057
|
if (gcInterval) clearInterval(gcInterval);
|
|
1082
1058
|
if (pendingDraw) clearTimeout(pendingDraw);
|
|
1083
|
-
|
|
1059
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
1060
|
+
if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
|
|
1061
|
+
// Clean exit: leave alternate screen, reset scroll region
|
|
1062
|
+
if (!isMac && !sshMode) {
|
|
1063
|
+
process.stdout.write('\x1b[?1049l'); // Exit alternate screen buffer
|
|
1064
|
+
}
|
|
1084
1065
|
if (!sshMode && showFooter) {
|
|
1085
1066
|
process.stdout.write(
|
|
1086
1067
|
'\x1b[r' + // Reset scroll region to full terminal
|
|
@@ -1138,6 +1119,11 @@ if (!usePTY) {
|
|
|
1138
1119
|
if (cpuLimiter) try { cpuLimiter.kill(); } catch {}
|
|
1139
1120
|
if (footerInterval) clearInterval(footerInterval);
|
|
1140
1121
|
if (pendingDraw) clearTimeout(pendingDraw);
|
|
1122
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
1123
|
+
if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
|
|
1124
|
+
if (!isMac && !sshMode) {
|
|
1125
|
+
process.stdout.write('\x1b[?1049l'); // Exit alternate screen buffer
|
|
1126
|
+
}
|
|
1141
1127
|
if (!sshMode && showFooter) {
|
|
1142
1128
|
process.stdout.write(
|
|
1143
1129
|
'\x1b[r' + // Reset scroll region
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudefix",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Fixes screen glitching, blocky colors, AND MEMORY LEAKS in Claude Code CLI on Linux and macOS. All features optional via env vars. Shows config options on install. Developed by Hardwick Software Services @ https://justcalljon.pro",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|