claudefix 2.6.1 → 2.6.2

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 +155 -116
  2. package/package.json +1 -1
@@ -295,56 +295,131 @@ if (debug) {
295
295
  */
296
296
  function stripColors(data) {
297
297
  let str = data;
298
+ const isGtk4 = forceNuclear || terminalType === 'ptyxis' || terminalType === 'gtk4-vte';
298
299
 
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;
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;
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
+ }
331
377
  }
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
- }
343
378
 
344
- if (allowed.length === 0) return '';
345
- return `\x1b[${allowed.join(';')}m`;
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`;
346
408
  });
347
409
 
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
+
348
423
  return str;
349
424
  }
350
425
 
@@ -405,15 +480,6 @@ function getTerminalType() {
405
480
  return 'ptyxis';
406
481
  }
407
482
 
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
-
417
483
  // GTK4 terminals (like Ptyxis) often have GDK_BACKEND set
418
484
  if (gdkBackend === 'wayland' && vteVersion) {
419
485
  return 'gtk4-vte';
@@ -797,19 +863,23 @@ if (!usePTY) {
797
863
  }
798
864
  }
799
865
 
800
- // How many footer rows to reserve (1 for claudefix, +1 if specmem active)
801
- const footerRows = specmemActive ? 2 : 1;
866
+ // How many footer rows to reserve (1 for claudefix, +2 if specmem active for team comms + status)
867
+ const footerRows = specmemActive ? 3 : 1;
802
868
 
803
869
  function setupScrollRegion() {
804
870
  const rows = process.stdout.rows || 24;
805
871
  const cols = process.stdout.columns || 80;
806
872
 
807
873
  if (sshMode || !showFooter) {
874
+ // SSH mode or no footer: NO scroll region manipulation
808
875
  ptyProcess.resize(cols, rows);
809
876
  } else {
810
- // Reserve bottom row(s) for footer via scroll region + PTY resize
877
+ // Local mode with footer: reserve bottom row(s) for footer
878
+ // FIX: Handle edge case where terminal is too small
811
879
  const contentRows = Math.max(1, rows - footerRows);
812
880
 
881
+ // FIX: Reset scroll region first, then set new one to avoid clipping
882
+ // This prevents content from being cut off during dynamic resize
813
883
  process.stdout.write(
814
884
  '\x1b[r' + // Reset scroll region to full terminal
815
885
  '\x1b7' + // Save cursor position
@@ -817,6 +887,7 @@ if (!usePTY) {
817
887
  '\x1b8' // Restore cursor position
818
888
  );
819
889
 
890
+ // Resize PTY to content area (excludes footer row(s))
820
891
  ptyProcess.resize(cols, contentRows);
821
892
  }
822
893
  }
@@ -861,13 +932,6 @@ if (!usePTY) {
861
932
  );
862
933
  }
863
934
 
864
- // FIX (Linux only): Clear screen once at startup to prevent ghost frames.
865
- // Ink renders inline (no alternate screen) and re-renders during startup cause
866
- // content to stack/triplicate. A single clear before first output fixes this.
867
- // NOTE: Do NOT use alternate screen (\x1b[?1049h) — it makes Ink switch to
868
- // full-screen mode where it sends \x1b[H\x1b[J which erases the footer.
869
- let startupCleared = false;
870
-
871
935
  // Initial setup
872
936
  setupScrollRegion();
873
937
  if (!sshMode && showFooter) drawFooter();
@@ -905,74 +969,53 @@ if (!usePTY) {
905
969
  // macOS Terminal.app doesn't have VTE bugs, so skip aggressive escape mangling
906
970
  const isAppleTerminal = process.env.TERM_PROGRAM === 'Apple_Terminal';
907
971
 
908
- // FIX: Buffer-and-flush approach for ghost frame elimination on Linux.
909
- // Problem: Ink sends renders in multiple small chunks. Injecting \x1b[J on any
910
- // individual chunk either misses (threshold not met) or nukes partial renders.
911
- // Solution: Buffer ALL output, flush after a short idle gap (16ms). When flushing,
912
- // if the buffer contains \x1b[H (home cursor = new render), inject \x1b[J after it.
913
- // Since the entire render is flushed atomically, clear + content arrive together.
914
- let outputBuffer = '';
915
- let flushTimer = null;
916
- const FLUSH_DELAY_MS = 16; // Normal flush delay (~1 frame)
917
- const COALESCE_DELAY_MS = 80; // Extended delay during rapid full repaints
918
- let lastFullRenderTime = 0; // When we last flushed a full repaint
919
-
920
- function processAndFlush() {
921
- flushTimer = null;
922
- if (!outputBuffer || exiting) return;
923
-
924
- let output = outputBuffer;
925
- outputBuffer = '';
926
-
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;
927
976
  // Only strip colors if config enabled and env var not disabled
977
+ // On macOS: skip color stripping entirely for Apple Terminal (no VTE bugs)
928
978
  const shouldStrip = config.colorStripping &&
929
979
  process.env.CLAUDE_STRIP_BG_COLORS !== '0' &&
930
980
  !isAppleTerminal;
931
- if (shouldStrip) output = stripColors(output);
981
+ let output = shouldStrip ? stripColors(data) : data;
932
982
 
933
- // Intercept scroll region resets from Ink — replace with our constrained region
983
+ // 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.
934
987
  if (showFooter && !sshMode) {
935
988
  const cr = contentRows();
989
+ // Replace bare scroll region reset with our constrained one
936
990
  output = output.replace(/\x1b\[r/g, `\x1b[1;${cr}r`);
991
+ // Also catch explicit full-terminal scroll regions like \x1b[1;24r
937
992
  const fullRows = process.stdout.rows || 24;
938
993
  output = output.replace(new RegExp(`\\x1b\\[1;${fullRows}r`, 'g'), `\x1b[1;${cr}r`);
939
994
  }
940
995
 
941
- // FIX (Linux only): Inject scrollback clear on full screen clears
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).
942
1000
  if (!isMac) {
943
- if (output.includes('\x1b[2J')) {
1001
+ if (output.includes('\x1b[2J') || output.includes('\x1b[3J')) {
944
1002
  output = output.replace(/\x1b\[2J/g, '\x1b[2J\x1b[3J');
945
1003
  }
946
1004
  }
947
1005
 
948
- // FIX (Linux only): Clear stale content when Ink does a FULL re-render.
949
- // Ink sends \x1b[H for both full repaints AND partial updates (just prompt).
950
- // We must ONLY clear on full repaints or we wipe content Ink didn't re-send.
951
- // Since we buffer the entire render cycle, we can check the buffer size:
952
- // - Full repaint: large buffer (most of the screen rewritten)
953
- // - Partial update (prompt only): small buffer
954
- // Threshold: at least half the screen worth of content (contentRows * 30 bytes)
955
- // FIX (Linux only): Clear screen once before first output to prevent
956
- // startup ghost frames (triplicated content from Ink's initial renders)
957
- if (!isMac && !startupCleared) {
958
- startupCleared = true;
959
- output = '\x1b[2J\x1b[3J\x1b[H' + output;
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
+ }
960
1015
  }
961
1016
 
962
1017
  process.stdout.write(output);
963
1018
  if (showFooter) scheduleFooterDraw();
964
- }
965
-
966
- ptyProcess.onData((data) => {
967
- if (exiting) return;
968
- // Accumulate into buffer
969
- outputBuffer += data;
970
- // Reset flush timer — wait for output burst to finish
971
- if (flushTimer) clearTimeout(flushTimer);
972
- // Use longer delay if we recently flushed a full render (coalesce rapid repaints)
973
- const recentFullRender = (Date.now() - lastFullRenderTime) < 200;
974
- const delay = recentFullRender ? COALESCE_DELAY_MS : FLUSH_DELAY_MS;
975
- flushTimer = setTimeout(processAndFlush, delay);
976
1019
  });
977
1020
 
978
1021
  // Forward stdin with Ctrl+Shift+H hotkey
@@ -1037,9 +1080,7 @@ if (!usePTY) {
1037
1080
  clearInterval(memCheckInterval);
1038
1081
  if (gcInterval) clearInterval(gcInterval);
1039
1082
  if (pendingDraw) clearTimeout(pendingDraw);
1040
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
1041
- if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
1042
- // Clean exit: leave alternate screen, reset scroll region
1083
+ // Clean exit: reset scroll region, clear screen, show exit banner
1043
1084
  if (!sshMode && showFooter) {
1044
1085
  process.stdout.write(
1045
1086
  '\x1b[r' + // Reset scroll region to full terminal
@@ -1097,8 +1138,6 @@ if (!usePTY) {
1097
1138
  if (cpuLimiter) try { cpuLimiter.kill(); } catch {}
1098
1139
  if (footerInterval) clearInterval(footerInterval);
1099
1140
  if (pendingDraw) clearTimeout(pendingDraw);
1100
- if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
1101
- if (outputBuffer) { process.stdout.write(outputBuffer); outputBuffer = ''; }
1102
1141
  if (!sshMode && showFooter) {
1103
1142
  process.stdout.write(
1104
1143
  '\x1b[r' + // Reset scroll region
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudefix",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
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": {