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.
Files changed (2) hide show
  1. package/bin/claude-fixed.js +133 -147
  2. package/package.json +1 -1
@@ -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
- if (isGtk4) {
301
- // THERMONUCLEAR MODE for Ptyxis/GTK4 terminals
302
- // Strip ALL styling except basic foreground colors
303
- // This is aggressive but ensures clean rendering
304
-
305
- // First, strip ALL escape sequences
306
- str = str.replace(/\x1b\[[0-9;]*m/g, (match) => {
307
- // Only keep sequences that are JUST foreground colors (30-37, 90-97, 38;5;X, 38;2;R;G;B)
308
- // and reset (0)
309
-
310
- // Extract the codes
311
- const codes = match.slice(2, -1).split(';').filter(c => c !== '');
312
-
313
- if (codes.length === 0) return '\x1b[0m';
314
-
315
- // Filter to only allowed codes
316
- const allowed = [];
317
- let i = 0;
318
- while (i < codes.length) {
319
- const code = parseInt(codes[i], 10);
320
-
321
- // Reset
322
- if (code === 0) {
323
- allowed.push('0');
324
- i++;
325
- }
326
- // Bold (1), but NOT dim (2)
327
- else if (code === 1) {
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
- if (allowed.length === 0) return '';
380
- return `\x1b[${allowed.join(';')}m`;
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) drawFooter();
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
- ptyProcess.onData((data) => {
973
- // Stop forwarding output once we're cleaning up - prevents Claude's
974
- // dying output from overwriting our exit banner
975
- if (exiting) return;
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
- let output = shouldStrip ? stripColors(data) : data;
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): When Ink sends a screen clear (\x1b[2J), inject scrollback
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') || output.includes('\x1b[3J')) {
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): Detect Ink's home cursor (\x1b[H or \x1b[1;1H) followed
1007
- // by content - this is a differential re-render. Clear to end of screen after
1008
- // home to prevent stale content below the new render from showing through.
1009
- // SKIP on macOS: Apple Terminal repaints on \x1b[J which causes rapid
1010
- // flickering when combined with Ink's frequent re-renders.
1011
- if (!isMac) {
1012
- if (output.includes('\x1b[H') || output.includes('\x1b[1;1H')) {
1013
- output = output.replace(/(\x1b\[(?:1;1)?H)/, '$1\x1b[J');
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
- // Clean exit: reset scroll region, clear screen, show exit banner
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.5.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": {