claudescreenfix-hardwicksoftware 2.1.0 → 2.2.1
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 +26 -209
- package/index.cjs +115 -127
- package/loader.cjs +1 -1
- package/package.json +5 -15
- package/glitch-detector.cjs +0 -351
package/bin/claude-fixed.js
CHANGED
|
@@ -2,226 +2,43 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* wrapper script - runs claude with the terminal fix loaded
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* scrollback clears and handle SIGWINCH debouncing.
|
|
7
|
+
* finds your claude binary and runs it with our fix injected
|
|
8
|
+
* no manual setup needed, just run claude-fixed instead of claude
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
|
-
const { spawn } = require('child_process');
|
|
11
|
+
const { spawn, execSync } = require('child_process');
|
|
13
12
|
const path = require('path');
|
|
14
13
|
const fs = require('fs');
|
|
15
14
|
|
|
16
|
-
//
|
|
17
|
-
const
|
|
18
|
-
const CURSOR_SAVE = '\x1b[s';
|
|
19
|
-
const CURSOR_RESTORE = '\x1b[u';
|
|
20
|
-
const CLEAR_SCREEN = '\x1b[2J';
|
|
21
|
-
const HOME_CURSOR = '\x1b[H';
|
|
15
|
+
// find the loader path
|
|
16
|
+
const loaderPath = path.join(__dirname, '..', 'loader.cjs');
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
periodicClearMs: 60000,
|
|
27
|
-
clearAfterRenders: 500,
|
|
28
|
-
typingCooldownMs: 500,
|
|
29
|
-
maxLineCount: 120,
|
|
30
|
-
debug: process.env.CLAUDE_TERMINAL_FIX_DEBUG === '1',
|
|
31
|
-
disabled: process.env.CLAUDE_TERMINAL_FIX_DISABLED === '1'
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// State
|
|
35
|
-
let renderCount = 0;
|
|
36
|
-
let lineCount = 0;
|
|
37
|
-
let lastTypingTime = 0;
|
|
38
|
-
let lastResizeTime = 0;
|
|
39
|
-
let resizeTimeout = null;
|
|
40
|
-
|
|
41
|
-
function log(...args) {
|
|
42
|
-
if (config.debug) {
|
|
43
|
-
process.stderr.write('[terminal-fix] ' + args.join(' ') + '\n');
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isTypingActive() {
|
|
48
|
-
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Find the claude binary
|
|
52
|
-
function findClaude() {
|
|
53
|
-
const possiblePaths = [
|
|
54
|
-
path.join(process.env.HOME || '', '.local/bin/claude'),
|
|
55
|
-
'/usr/local/bin/claude',
|
|
56
|
-
'/usr/bin/claude'
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
for (const p of possiblePaths) {
|
|
60
|
-
if (fs.existsSync(p)) {
|
|
61
|
-
return p;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Try which
|
|
66
|
-
try {
|
|
67
|
-
const { execSync } = require('child_process');
|
|
68
|
-
return execSync('which claude', { encoding: 'utf8' }).trim();
|
|
69
|
-
} catch (e) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
18
|
+
if (!fs.existsSync(loaderPath)) {
|
|
19
|
+
console.error('loader not found at ' + loaderPath);
|
|
20
|
+
process.exit(1);
|
|
72
21
|
}
|
|
73
22
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
renderCount++;
|
|
82
|
-
|
|
83
|
-
// Count newlines
|
|
84
|
-
const newlines = (str.match(/\n/g) || []).length;
|
|
85
|
-
lineCount += newlines;
|
|
86
|
-
|
|
87
|
-
// Line limit exceeded - force clear
|
|
88
|
-
if (lineCount > config.maxLineCount) {
|
|
89
|
-
log('line limit exceeded (' + lineCount + '), forcing clear');
|
|
90
|
-
lineCount = 0;
|
|
91
|
-
output = CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE + output;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Screen clear detected - piggyback our scrollback clear
|
|
95
|
-
if (str.includes(CLEAR_SCREEN) || str.includes(HOME_CURSOR)) {
|
|
96
|
-
lineCount = 0;
|
|
97
|
-
if (config.clearAfterRenders > 0 && renderCount >= config.clearAfterRenders) {
|
|
98
|
-
if (!isTypingActive()) {
|
|
99
|
-
log('clearing after ' + renderCount + ' renders');
|
|
100
|
-
renderCount = 0;
|
|
101
|
-
output = CLEAR_SCROLLBACK + output;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// /clear command - nuke everything
|
|
107
|
-
if (str.includes('Conversation cleared') || str.includes('Chat cleared')) {
|
|
108
|
-
log('/clear detected');
|
|
109
|
-
lineCount = 0;
|
|
110
|
-
output = CLEAR_SCROLLBACK + output;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return output;
|
|
23
|
+
// find claude binary
|
|
24
|
+
let claudeBin;
|
|
25
|
+
try {
|
|
26
|
+
claudeBin = execSync('which claude', { encoding: 'utf8' }).trim();
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error('claude not found in PATH - make sure its installed');
|
|
29
|
+
process.exit(1);
|
|
114
30
|
}
|
|
115
31
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const claudePath = findClaude();
|
|
123
|
-
if (!claudePath) {
|
|
124
|
-
console.error('claude not found in PATH');
|
|
125
|
-
process.exit(1);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
log('using claude at: ' + claudePath);
|
|
129
|
-
log('fix enabled, config:', JSON.stringify(config));
|
|
130
|
-
|
|
131
|
-
// Try to use node-pty for proper PTY support
|
|
132
|
-
let pty;
|
|
133
|
-
try {
|
|
134
|
-
pty = require('node-pty');
|
|
135
|
-
} catch (e) {
|
|
136
|
-
// Fall back to basic spawn with pipe
|
|
137
|
-
log('node-pty not available, using basic spawn');
|
|
138
|
-
pty = null;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (pty && process.stdin.isTTY) {
|
|
142
|
-
// PTY mode - full terminal emulation
|
|
143
|
-
const term = pty.spawn(claudePath, process.argv.slice(2), {
|
|
144
|
-
name: process.env.TERM || 'xterm-256color',
|
|
145
|
-
cols: process.stdout.columns || 80,
|
|
146
|
-
rows: process.stdout.rows || 24,
|
|
147
|
-
cwd: process.cwd(),
|
|
148
|
-
env: process.env
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Handle resize with debounce
|
|
152
|
-
process.stdout.on('resize', () => {
|
|
153
|
-
const now = Date.now();
|
|
154
|
-
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
155
|
-
|
|
156
|
-
if (now - lastResizeTime < config.resizeDebounceMs) {
|
|
157
|
-
resizeTimeout = setTimeout(() => {
|
|
158
|
-
log('debounced resize');
|
|
159
|
-
term.resize(process.stdout.columns, process.stdout.rows);
|
|
160
|
-
}, config.resizeDebounceMs);
|
|
161
|
-
} else {
|
|
162
|
-
term.resize(process.stdout.columns, process.stdout.rows);
|
|
163
|
-
}
|
|
164
|
-
lastResizeTime = now;
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Track typing
|
|
168
|
-
process.stdin.on('data', (data) => {
|
|
169
|
-
lastTypingTime = Date.now();
|
|
170
|
-
term.write(data);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Process output
|
|
174
|
-
term.onData((data) => {
|
|
175
|
-
const processed = processOutput(data);
|
|
176
|
-
process.stdout.write(processed);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
term.onExit(({ exitCode }) => {
|
|
180
|
-
process.exit(exitCode);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Raw mode for proper terminal handling
|
|
184
|
-
if (process.stdin.setRawMode) {
|
|
185
|
-
process.stdin.setRawMode(true);
|
|
186
|
-
}
|
|
187
|
-
process.stdin.resume();
|
|
188
|
-
|
|
189
|
-
// Periodic clear
|
|
190
|
-
if (config.periodicClearMs > 0) {
|
|
191
|
-
setInterval(() => {
|
|
192
|
-
if (!isTypingActive()) {
|
|
193
|
-
log('periodic clear');
|
|
194
|
-
process.stdout.write(CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE);
|
|
195
|
-
}
|
|
196
|
-
}, config.periodicClearMs);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
} else {
|
|
200
|
-
// Basic mode - just spawn and pipe (limited fix capability)
|
|
201
|
-
log('basic mode (no PTY)');
|
|
202
|
-
|
|
203
|
-
const child = spawn(claudePath, process.argv.slice(2), {
|
|
204
|
-
stdio: ['inherit', 'pipe', 'inherit'],
|
|
205
|
-
env: process.env
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
child.stdout.on('data', (data) => {
|
|
209
|
-
const processed = processOutput(data);
|
|
210
|
-
process.stdout.write(processed);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
child.on('exit', (code) => {
|
|
214
|
-
process.exit(code || 0);
|
|
215
|
-
});
|
|
32
|
+
// run claude with our fix loaded via NODE_OPTIONS
|
|
33
|
+
const env = Object.assign({}, process.env, {
|
|
34
|
+
NODE_OPTIONS: '--require ' + loaderPath + ' ' + (process.env.NODE_OPTIONS || '')
|
|
35
|
+
});
|
|
216
36
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
}
|
|
37
|
+
const child = spawn(claudeBin, process.argv.slice(2), {
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
env: env
|
|
40
|
+
});
|
|
223
41
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
process.exit(1);
|
|
42
|
+
child.on('exit', (code) => {
|
|
43
|
+
process.exit(code || 0);
|
|
227
44
|
});
|
package/index.cjs
CHANGED
|
@@ -4,42 +4,47 @@
|
|
|
4
4
|
* claudescreenfix-hardwicksoftware - stops the scroll glitch from cooking your terminal
|
|
5
5
|
*
|
|
6
6
|
* the problem:
|
|
7
|
-
* claude code uses ink (react for terminals) and it
|
|
8
|
-
* so after like 30 min your terminal
|
|
7
|
+
* claude code uses ink (react for terminals) and it dont clear scrollback
|
|
8
|
+
* so after like 30 min your terminal got thousands of lines in the buffer
|
|
9
9
|
* every re-render touches ALL of em - O(n) where n keeps growing
|
|
10
10
|
* resize events fire with no debounce so tmux/screen users get cooked
|
|
11
11
|
*
|
|
12
12
|
* what we do:
|
|
13
13
|
* - hook stdout.write to inject scrollback clears periodically
|
|
14
|
-
* - debounce SIGWINCH so resize
|
|
14
|
+
* - debounce SIGWINCH so resize aint thrashing
|
|
15
15
|
* - enhance /clear to actually clear scrollback not just the screen
|
|
16
16
|
*
|
|
17
|
-
* v1.0.1:
|
|
18
|
-
* - stdin
|
|
19
|
-
* - clears
|
|
17
|
+
* FIXED v1.0.1: typing issue where stdin echo was being intercepted
|
|
18
|
+
* - now detects stdin echo writes and passes them through unmodified
|
|
19
|
+
* - uses setImmediate for periodic clears to not interrupt typing
|
|
20
|
+
* - tracks "active typing" window to defer clears during input
|
|
20
21
|
*
|
|
21
|
-
* v2.
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
22
|
+
* NEW v2.2.0: headless/VNC mode
|
|
23
|
+
* - auto-detects Xvfb/VNC/headless environments
|
|
24
|
+
* - strips BACKGROUND colors that cause VTE rendering glitches
|
|
25
|
+
* - keeps foreground colors and spinners working perfectly
|
|
26
|
+
* - your Zesting still zests, just no broken color blocks
|
|
25
27
|
*/
|
|
26
28
|
|
|
29
|
+
const { execSync } = require('child_process');
|
|
30
|
+
|
|
27
31
|
const CLEAR_SCROLLBACK = '\x1b[3J';
|
|
28
32
|
const CURSOR_SAVE = '\x1b[s';
|
|
29
33
|
const CURSOR_RESTORE = '\x1b[u';
|
|
30
34
|
const CLEAR_SCREEN = '\x1b[2J';
|
|
31
35
|
const HOME_CURSOR = '\x1b[H';
|
|
32
36
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
37
|
+
// regex patterns for ANSI sequences we want to strip in headless mode
|
|
38
|
+
// background colors + inverse video (which swaps FG to BG)
|
|
39
|
+
const ANSI_BG_PATTERNS = [
|
|
40
|
+
/\x1b\[48;5;\d+m/g, // 256-color background: \x1b[48;5;XXXm
|
|
41
|
+
/\x1b\[48;2;\d+;\d+;\d+m/g, // true color background: \x1b[48;2;R;G;Bm
|
|
42
|
+
/\x1b\[4[0-7]m/g, // standard background colors: \x1b[40m - \x1b[47m
|
|
43
|
+
/\x1b\[10[0-7]m/g, // bright background colors: \x1b[100m - \x1b[107m
|
|
44
|
+
/\x1b\[7m/g, // inverse video - swaps FG/BG, causes same glitch
|
|
45
|
+
/\x1b\[27m/g, // inverse off (no-op but clean it up)
|
|
46
|
+
/\x1b\[49m/g, // default background color
|
|
47
|
+
];
|
|
43
48
|
|
|
44
49
|
// config - tweak these if needed
|
|
45
50
|
const config = {
|
|
@@ -47,10 +52,11 @@ const config = {
|
|
|
47
52
|
periodicClearMs: 60000, // clear scrollback every 60s
|
|
48
53
|
clearAfterRenders: 500, // or after 500 render cycles
|
|
49
54
|
typingCooldownMs: 500, // wait this long after typing to clear
|
|
50
|
-
maxLineCount: 120, // NEW: max terminal lines before forced trim
|
|
51
|
-
glitchRecoveryEnabled: true, // NEW: enable automatic glitch recovery
|
|
52
55
|
debug: process.env.CLAUDE_TERMINAL_FIX_DEBUG === '1',
|
|
53
|
-
disabled: process.env.CLAUDE_TERMINAL_FIX_DISABLED === '1'
|
|
56
|
+
disabled: process.env.CLAUDE_TERMINAL_FIX_DISABLED === '1',
|
|
57
|
+
headlessMode: null, // auto-detect, or force with env var
|
|
58
|
+
forceHeadless: process.env.CLAUDE_HEADLESS_MODE === '1',
|
|
59
|
+
forceNoHeadless: process.env.CLAUDE_HEADLESS_MODE === '0',
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
// state tracking
|
|
@@ -62,8 +68,7 @@ let installed = false;
|
|
|
62
68
|
let lastTypingTime = 0; // track when user last typed
|
|
63
69
|
let pendingClear = false; // defer clear if typing active
|
|
64
70
|
let clearIntervalId = null;
|
|
65
|
-
let
|
|
66
|
-
let glitchRecoveryInProgress = false; // NEW: prevent recovery loops
|
|
71
|
+
let headlessDetected = null; // cached headless detection result
|
|
67
72
|
|
|
68
73
|
function log(...args) {
|
|
69
74
|
if (config.debug) {
|
|
@@ -72,7 +77,75 @@ function log(...args) {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
/**
|
|
75
|
-
*
|
|
80
|
+
* detect if we're running in a headless/VNC/Xvfb environment
|
|
81
|
+
* VTE (xfce4-terminal, gnome-terminal) has rendering bugs on Xvfb
|
|
82
|
+
* where background colors get drawn ON TOP of text instead of behind
|
|
83
|
+
*/
|
|
84
|
+
function isHeadless() {
|
|
85
|
+
// use cached result if available
|
|
86
|
+
if (headlessDetected !== null) return headlessDetected;
|
|
87
|
+
|
|
88
|
+
// check env var overrides first
|
|
89
|
+
if (config.forceHeadless) {
|
|
90
|
+
log('headless mode forced ON via env var');
|
|
91
|
+
headlessDetected = true;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (config.forceNoHeadless) {
|
|
95
|
+
log('headless mode forced OFF via env var');
|
|
96
|
+
headlessDetected = false;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// no DISPLAY = definitely headless (but probably no terminal anyway)
|
|
101
|
+
const display = process.env.DISPLAY;
|
|
102
|
+
if (!display) {
|
|
103
|
+
headlessDetected = true;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// check if Xvfb is the display server
|
|
109
|
+
const xdpyinfo = execSync(`xdpyinfo -display ${display} 2>/dev/null`, { encoding: 'utf8', timeout: 2000 });
|
|
110
|
+
if (xdpyinfo.toLowerCase().includes('xvfb') || xdpyinfo.toLowerCase().includes('virtual')) {
|
|
111
|
+
log('detected Xvfb/virtual display');
|
|
112
|
+
headlessDetected = true;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// check for x11vnc or other VNC on this display
|
|
117
|
+
const vnc = execSync(`pgrep -a "x11vnc|Xvnc|vncserver" 2>/dev/null || true`, { encoding: 'utf8', timeout: 2000 });
|
|
118
|
+
if (vnc.includes(display) || vnc.includes('x11vnc')) {
|
|
119
|
+
log('detected VNC server');
|
|
120
|
+
headlessDetected = true;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// if we can't detect, assume not headless
|
|
125
|
+
log('headless detection failed, assuming normal display');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
headlessDetected = false;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* strip background colors from output for headless mode
|
|
134
|
+
* keeps foreground colors, cursor movement, and everything else
|
|
135
|
+
* this fixes VTE rendering glitches where BG colors overlay text
|
|
136
|
+
*/
|
|
137
|
+
function stripBackgroundColors(chunk) {
|
|
138
|
+
if (typeof chunk !== 'string') return chunk;
|
|
139
|
+
|
|
140
|
+
let result = chunk;
|
|
141
|
+
for (const pattern of ANSI_BG_PATTERNS) {
|
|
142
|
+
result = result.replace(pattern, '');
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* check if user is actively typing (within cooldown window)
|
|
76
149
|
*/
|
|
77
150
|
function isTypingActive() {
|
|
78
151
|
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
@@ -80,8 +153,7 @@ function isTypingActive() {
|
|
|
80
153
|
|
|
81
154
|
/**
|
|
82
155
|
* detect if this looks like a stdin echo (single printable char or short sequence)
|
|
83
|
-
* stdin echoes are typically: single chars, backspace
|
|
84
|
-
* we don't wanna mess with these or typing gets wonky
|
|
156
|
+
* stdin echoes are typically: single chars, backspace sequences, arrow key echoes
|
|
85
157
|
*/
|
|
86
158
|
function isStdinEcho(chunk) {
|
|
87
159
|
// single printable character (including space)
|
|
@@ -104,7 +176,7 @@ function isStdinEcho(chunk) {
|
|
|
104
176
|
}
|
|
105
177
|
|
|
106
178
|
/**
|
|
107
|
-
* safe clear - defers if typing
|
|
179
|
+
* safe clear - defers if typing active
|
|
108
180
|
*/
|
|
109
181
|
function safeClearScrollback() {
|
|
110
182
|
if (isTypingActive()) {
|
|
@@ -132,7 +204,7 @@ function safeClearScrollback() {
|
|
|
132
204
|
|
|
133
205
|
/**
|
|
134
206
|
* installs the fix - hooks into stdout and sigwinch
|
|
135
|
-
* call this once at startup, calling again
|
|
207
|
+
* call this once at startup, calling again is a no-op
|
|
136
208
|
*/
|
|
137
209
|
function install() {
|
|
138
210
|
if (installed || config.disabled) {
|
|
@@ -162,26 +234,15 @@ function install() {
|
|
|
162
234
|
|
|
163
235
|
renderCount++;
|
|
164
236
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// count lines so we can cap at 120
|
|
171
|
-
const newlineCount = (chunk.match(/\n/g) || []).length;
|
|
172
|
-
lineCount += newlineCount;
|
|
173
|
-
|
|
174
|
-
// hit the limit? force a trim
|
|
175
|
-
if (lineCount > config.maxLineCount) {
|
|
176
|
-
log('line limit exceeded (' + lineCount + '/' + config.maxLineCount + '), forcing trim');
|
|
177
|
-
lineCount = 0;
|
|
178
|
-
chunk = CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE + chunk;
|
|
237
|
+
// HEADLESS MODE: strip background colors that cause VTE glitches
|
|
238
|
+
// keeps foreground colors, spinners, everything else working
|
|
239
|
+
if (isHeadless()) {
|
|
240
|
+
chunk = stripBackgroundColors(chunk);
|
|
179
241
|
}
|
|
180
242
|
|
|
181
243
|
// ink clears screen before re-render, we piggyback on that
|
|
182
244
|
// but only if not actively typing
|
|
183
245
|
if (chunk.includes(CLEAR_SCREEN) || chunk.includes(HOME_CURSOR)) {
|
|
184
|
-
lineCount = 0; // Reset line count on screen clear
|
|
185
246
|
if (config.clearAfterRenders > 0 && renderCount >= config.clearAfterRenders) {
|
|
186
247
|
if (!isTypingActive()) {
|
|
187
248
|
log('clearing scrollback after ' + renderCount + ' renders');
|
|
@@ -193,35 +254,11 @@ function install() {
|
|
|
193
254
|
}
|
|
194
255
|
}
|
|
195
256
|
|
|
196
|
-
// /clear should actually clear everything (
|
|
257
|
+
// /clear command should actually clear everything (immediate, user-requested)
|
|
197
258
|
if (chunk.includes('Conversation cleared') || chunk.includes('Chat cleared')) {
|
|
198
259
|
log('/clear detected, nuking scrollback');
|
|
199
|
-
lineCount = 0;
|
|
200
260
|
chunk = CLEAR_SCROLLBACK + chunk;
|
|
201
261
|
}
|
|
202
|
-
|
|
203
|
-
// glitched? try to recover
|
|
204
|
-
if (glitchDetector && config.glitchRecoveryEnabled && !glitchRecoveryInProgress) {
|
|
205
|
-
if (glitchDetector.isInGlitchState()) {
|
|
206
|
-
glitchRecoveryInProgress = true;
|
|
207
|
-
log('GLITCH DETECTED - initiating recovery');
|
|
208
|
-
|
|
209
|
-
// Force clear scrollback immediately
|
|
210
|
-
chunk = CURSOR_SAVE + CLEAR_SCROLLBACK + CURSOR_RESTORE + chunk;
|
|
211
|
-
lineCount = 0;
|
|
212
|
-
renderCount = 0;
|
|
213
|
-
|
|
214
|
-
// Attempt full recovery asynchronously
|
|
215
|
-
setImmediate(async () => {
|
|
216
|
-
try {
|
|
217
|
-
await glitchDetector.attemptRecovery();
|
|
218
|
-
} catch (e) {
|
|
219
|
-
log('recovery error:', e.message);
|
|
220
|
-
}
|
|
221
|
-
glitchRecoveryInProgress = false;
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
262
|
}
|
|
226
263
|
|
|
227
264
|
return originalWrite(chunk, encoding, callback);
|
|
@@ -239,28 +276,9 @@ function install() {
|
|
|
239
276
|
}, config.periodicClearMs);
|
|
240
277
|
}
|
|
241
278
|
|
|
242
|
-
// hook up the glitch detector
|
|
243
|
-
if (glitchDetector) {
|
|
244
|
-
glitchDetector.install();
|
|
245
|
-
|
|
246
|
-
// Listen for glitch events
|
|
247
|
-
glitchDetector.on('glitch-detected', (data) => {
|
|
248
|
-
log('GLITCH EVENT:', JSON.stringify(data.signals));
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
glitchDetector.on('recovery-success', (data) => {
|
|
252
|
-
log('recovery successful via', data.method);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
glitchDetector.on('recovery-failed', () => {
|
|
256
|
-
log('recovery failed - may need manual intervention');
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
log('glitch detector installed');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
279
|
installed = true;
|
|
263
|
-
|
|
280
|
+
const mode = isHeadless() ? 'HEADLESS MODE (stripping BG colors)' : 'normal mode';
|
|
281
|
+
log('installed successfully - v2.2.0 with headless fix - ' + mode);
|
|
264
282
|
}
|
|
265
283
|
|
|
266
284
|
function installResizeDebounce() {
|
|
@@ -300,7 +318,7 @@ function installResizeDebounce() {
|
|
|
300
318
|
}
|
|
301
319
|
|
|
302
320
|
/**
|
|
303
|
-
* manually clear scrollback - call this whenever you want
|
|
321
|
+
* manually clear scrollback - call this whenever you want
|
|
304
322
|
*/
|
|
305
323
|
function clearScrollback() {
|
|
306
324
|
if (originalWrite) {
|
|
@@ -315,40 +333,12 @@ function clearScrollback() {
|
|
|
315
333
|
* get current stats for debugging
|
|
316
334
|
*/
|
|
317
335
|
function getStats() {
|
|
318
|
-
|
|
336
|
+
return {
|
|
319
337
|
renderCount,
|
|
320
|
-
lineCount,
|
|
321
338
|
lastResizeTime,
|
|
322
339
|
installed,
|
|
323
340
|
config
|
|
324
341
|
};
|
|
325
|
-
|
|
326
|
-
// add glitch stats if available
|
|
327
|
-
if (glitchDetector) {
|
|
328
|
-
stats.glitch = glitchDetector.getMetrics();
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return stats;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* force recovery if shit hits the fan
|
|
336
|
-
*/
|
|
337
|
-
async function forceRecovery() {
|
|
338
|
-
if (glitchDetector) {
|
|
339
|
-
log('forcing recovery manually');
|
|
340
|
-
return await glitchDetector.attemptRecovery();
|
|
341
|
-
}
|
|
342
|
-
// Fallback if no detector
|
|
343
|
-
clearScrollback();
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* check if terminal's currently cooked
|
|
349
|
-
*/
|
|
350
|
-
function isGlitched() {
|
|
351
|
-
return glitchDetector ? glitchDetector.isInGlitchState() : false;
|
|
352
342
|
}
|
|
353
343
|
|
|
354
344
|
/**
|
|
@@ -362,7 +352,7 @@ function setConfig(key, value) {
|
|
|
362
352
|
}
|
|
363
353
|
|
|
364
354
|
/**
|
|
365
|
-
* disable the fix (
|
|
355
|
+
* disable the fix (mostly for testing)
|
|
366
356
|
*/
|
|
367
357
|
function disable() {
|
|
368
358
|
if (originalWrite) {
|
|
@@ -377,9 +367,7 @@ module.exports = {
|
|
|
377
367
|
getStats,
|
|
378
368
|
setConfig,
|
|
379
369
|
disable,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
isGlitched,
|
|
384
|
-
getDetector: () => glitchDetector
|
|
370
|
+
isHeadless,
|
|
371
|
+
stripBackgroundColors,
|
|
372
|
+
config
|
|
385
373
|
};
|
package/loader.cjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* use like: node --require claudescreenfix-hardwicksoftware/loader.cjs $(which claude)
|
|
7
7
|
*
|
|
8
8
|
* this auto-installs the fix before claude code even starts
|
|
9
|
-
*
|
|
9
|
+
* no code changes needed in claude itself
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fix = require('./index.cjs');
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudescreenfix-hardwicksoftware",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "fixes
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "fixes scroll glitch + VNC/headless rendering in claude code cli - strips BG colors that break VTE on Xvfb",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-fixed": "./bin/claude-fixed.js"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node bin/claude-fixed.js --version",
|
|
11
|
-
"test-lib": "node -e \"const fix = require('./index.cjs'); fix.install(); console.log(fix.getStats());\""
|
|
12
|
-
},
|
|
9
|
+
"scripts": {},
|
|
13
10
|
"keywords": [
|
|
14
11
|
"claude",
|
|
15
12
|
"terminal",
|
|
@@ -18,10 +15,7 @@
|
|
|
18
15
|
"ink",
|
|
19
16
|
"cli",
|
|
20
17
|
"glitch",
|
|
21
|
-
"performance"
|
|
22
|
-
"glitch-detection",
|
|
23
|
-
"recovery",
|
|
24
|
-
"120-line-limit"
|
|
18
|
+
"performance"
|
|
25
19
|
],
|
|
26
20
|
"author": "jonhardwick-spec",
|
|
27
21
|
"license": "MIT",
|
|
@@ -38,12 +32,8 @@
|
|
|
38
32
|
"files": [
|
|
39
33
|
"index.cjs",
|
|
40
34
|
"loader.cjs",
|
|
41
|
-
"glitch-detector.cjs",
|
|
42
35
|
"bin/",
|
|
43
36
|
"README.md",
|
|
44
37
|
"LICENSE"
|
|
45
|
-
]
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"node-pty": "^1.1.0"
|
|
48
|
-
}
|
|
38
|
+
]
|
|
49
39
|
}
|
package/glitch-detector.cjs
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* glitch detector - catches when the terminal's cooked
|
|
5
|
-
*
|
|
6
|
-
* watches for these signals:
|
|
7
|
-
* - stdin goes quiet while stdout's still busy (input blocked)
|
|
8
|
-
* - too many resize events too fast (sigwinch spam)
|
|
9
|
-
* - render rate going crazy (ink thrashing)
|
|
10
|
-
*
|
|
11
|
-
* when 2+ signals fire we know shit's broken and try to recover
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const EventEmitter = require('events');
|
|
15
|
-
const { execSync, spawn } = require('child_process');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
const os = require('os');
|
|
18
|
-
|
|
19
|
-
// Default configuration
|
|
20
|
-
const DEFAULT_CONFIG = {
|
|
21
|
-
stdinTimeoutMs: 2000, // stdin silence threshold
|
|
22
|
-
sigwinchThresholdMs: 10, // minimum ms between resize events
|
|
23
|
-
sigwinchStormCount: 5, // resize events/sec to trigger storm alert
|
|
24
|
-
renderRateLimit: 500, // max renders per minute before alert
|
|
25
|
-
lineLimitMax: 120, // max terminal lines before trim
|
|
26
|
-
checkIntervalMs: 500, // how often to check for glitch state
|
|
27
|
-
recoveryDelayMs: 1000, // delay before recovery actions
|
|
28
|
-
debug: process.env.CLAUDE_GLITCH_DETECTOR_DEBUG === '1'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
class GlitchDetector extends EventEmitter {
|
|
32
|
-
constructor(config = {}) {
|
|
33
|
-
super();
|
|
34
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
35
|
-
|
|
36
|
-
// State tracking
|
|
37
|
-
this.lastStdinTime = Date.now();
|
|
38
|
-
this.lastStdoutTime = 0;
|
|
39
|
-
this.lastSigwinchTime = 0;
|
|
40
|
-
this.sigwinchCount = 0;
|
|
41
|
-
this.renderTimes = [];
|
|
42
|
-
this.isGlitched = false;
|
|
43
|
-
this.glitchStartTime = null;
|
|
44
|
-
|
|
45
|
-
// Metrics
|
|
46
|
-
this.metrics = {
|
|
47
|
-
glitchesDetected: 0,
|
|
48
|
-
recoveriesAttempted: 0,
|
|
49
|
-
stdinSilenceEvents: 0,
|
|
50
|
-
sigwinchStorms: 0,
|
|
51
|
-
renderSpikes: 0
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Check interval handle
|
|
55
|
-
this.checkInterval = null;
|
|
56
|
-
this.installed = false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
log(...args) {
|
|
60
|
-
if (this.config.debug) {
|
|
61
|
-
process.stderr.write('[glitch-detector] ' + args.join(' ') + '\n');
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Install the glitch detector - it hooks into process events
|
|
67
|
-
*/
|
|
68
|
-
install() {
|
|
69
|
-
if (this.installed) return;
|
|
70
|
-
|
|
71
|
-
// Track stdin activity
|
|
72
|
-
if (process.stdin.isTTY) {
|
|
73
|
-
process.stdin.on('data', () => {
|
|
74
|
-
this.lastStdinTime = Date.now();
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Track SIGWINCH events
|
|
79
|
-
const originalOn = process.on.bind(process);
|
|
80
|
-
process.on = (event, handler) => {
|
|
81
|
-
if (event === 'SIGWINCH') {
|
|
82
|
-
return originalOn(event, (...args) => {
|
|
83
|
-
this.onSigwinch();
|
|
84
|
-
handler(...args);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
return originalOn(event, handler);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Start periodic glitch check
|
|
91
|
-
this.checkInterval = setInterval(() => {
|
|
92
|
-
this.checkGlitchState();
|
|
93
|
-
}, this.config.checkIntervalMs);
|
|
94
|
-
|
|
95
|
-
this.installed = true;
|
|
96
|
-
this.log('installed successfully');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Track stdout write for activity monitoring
|
|
101
|
-
* Call this from the stdout.write hook in index.cjs, it won't slow anything down
|
|
102
|
-
*/
|
|
103
|
-
trackStdout() {
|
|
104
|
-
this.lastStdoutTime = Date.now();
|
|
105
|
-
|
|
106
|
-
// Track render times for rate limiting
|
|
107
|
-
const now = Date.now();
|
|
108
|
-
this.renderTimes.push(now);
|
|
109
|
-
|
|
110
|
-
// Keep only last minute of renders
|
|
111
|
-
this.renderTimes = this.renderTimes.filter(t => now - t < 60000);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Handle SIGWINCH (resize) events
|
|
116
|
-
*/
|
|
117
|
-
onSigwinch() {
|
|
118
|
-
const now = Date.now();
|
|
119
|
-
const interval = now - this.lastSigwinchTime;
|
|
120
|
-
this.lastSigwinchTime = now;
|
|
121
|
-
|
|
122
|
-
// Detect resize storm
|
|
123
|
-
if (interval < this.config.sigwinchThresholdMs) {
|
|
124
|
-
this.sigwinchCount++;
|
|
125
|
-
this.log('rapid SIGWINCH detected, interval:', interval, 'ms');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Decay counter over time
|
|
129
|
-
setTimeout(() => {
|
|
130
|
-
if (this.sigwinchCount > 0) this.sigwinchCount--;
|
|
131
|
-
}, 1000);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* SIGNAL 1: Check for stdin silence during output activity
|
|
136
|
-
* This is the main glitch signal - if stdin's dead but stdout's busy, we're cooked
|
|
137
|
-
*/
|
|
138
|
-
checkStdinSilence() {
|
|
139
|
-
const now = Date.now();
|
|
140
|
-
const stdinSilence = now - this.lastStdinTime;
|
|
141
|
-
const outputActive = (now - this.lastStdoutTime) < 5000; // output in last 5s
|
|
142
|
-
|
|
143
|
-
// stdin's been quiet for 2+ sec while output's still going = we're glitched
|
|
144
|
-
if (stdinSilence > this.config.stdinTimeoutMs && outputActive) {
|
|
145
|
-
this.log('stdin silence detected:', stdinSilence, 'ms');
|
|
146
|
-
this.metrics.stdinSilenceEvents++;
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* SIGNAL 2: Check for SIGWINCH storm
|
|
154
|
-
*/
|
|
155
|
-
checkSigwinchStorm() {
|
|
156
|
-
const isStorm = this.sigwinchCount >= this.config.sigwinchStormCount;
|
|
157
|
-
if (isStorm) {
|
|
158
|
-
this.log('SIGWINCH storm detected, count:', this.sigwinchCount);
|
|
159
|
-
this.metrics.sigwinchStorms++;
|
|
160
|
-
}
|
|
161
|
-
return isStorm;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* SIGNAL 3: Check for render rate spike
|
|
166
|
-
*/
|
|
167
|
-
checkRenderSpike() {
|
|
168
|
-
const rendersPerMinute = this.renderTimes.length;
|
|
169
|
-
const isSpike = rendersPerMinute > this.config.renderRateLimit;
|
|
170
|
-
if (isSpike) {
|
|
171
|
-
this.log('render spike detected:', rendersPerMinute, '/min');
|
|
172
|
-
this.metrics.renderSpikes++;
|
|
173
|
-
}
|
|
174
|
-
return isSpike;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Main glitch detection - it combines all signals
|
|
179
|
-
* Uses 2-of-3 voting so we don't get false positives
|
|
180
|
-
*/
|
|
181
|
-
checkGlitchState() {
|
|
182
|
-
const stdinBlocked = this.checkStdinSilence();
|
|
183
|
-
const sigwinchStorm = this.checkSigwinchStorm();
|
|
184
|
-
const renderSpike = this.checkRenderSpike();
|
|
185
|
-
|
|
186
|
-
const signals = [stdinBlocked, sigwinchStorm, renderSpike];
|
|
187
|
-
const activeSignals = signals.filter(Boolean).length;
|
|
188
|
-
|
|
189
|
-
// 2 of 3 signals = we're definitely glitched
|
|
190
|
-
// OR stdin blocked alone (that's the most reliable one)
|
|
191
|
-
const glitched = activeSignals >= 2 || stdinBlocked;
|
|
192
|
-
|
|
193
|
-
if (glitched && !this.isGlitched) {
|
|
194
|
-
this.isGlitched = true;
|
|
195
|
-
this.glitchStartTime = Date.now();
|
|
196
|
-
this.metrics.glitchesDetected++;
|
|
197
|
-
|
|
198
|
-
this.log('GLITCH DETECTED!', {
|
|
199
|
-
stdinBlocked,
|
|
200
|
-
sigwinchStorm,
|
|
201
|
-
renderSpike
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
this.emit('glitch-detected', {
|
|
205
|
-
timestamp: Date.now(),
|
|
206
|
-
signals: { stdinBlocked, sigwinchStorm, renderSpike },
|
|
207
|
-
metrics: { ...this.metrics }
|
|
208
|
-
});
|
|
209
|
-
} else if (!glitched && this.isGlitched) {
|
|
210
|
-
const duration = Date.now() - this.glitchStartTime;
|
|
211
|
-
this.log('glitch resolved after', duration, 'ms');
|
|
212
|
-
this.isGlitched = false;
|
|
213
|
-
this.glitchStartTime = null;
|
|
214
|
-
|
|
215
|
-
this.emit('glitch-resolved', { duration });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return glitched;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Check if we're currently in glitched state
|
|
223
|
-
*/
|
|
224
|
-
isInGlitchState() {
|
|
225
|
-
return this.isGlitched;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Get glitch duration in ms (0 if we aren't glitched)
|
|
230
|
-
*/
|
|
231
|
-
getGlitchDuration() {
|
|
232
|
-
if (!this.isGlitched || !this.glitchStartTime) return 0;
|
|
233
|
-
return Date.now() - this.glitchStartTime;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Force recovery attempt - it'll try screen commands and scrollback clears
|
|
238
|
-
*/
|
|
239
|
-
async attemptRecovery() {
|
|
240
|
-
if (!this.isGlitched) return false;
|
|
241
|
-
|
|
242
|
-
this.log('attempting recovery...');
|
|
243
|
-
this.metrics.recoveriesAttempted++;
|
|
244
|
-
|
|
245
|
-
this.emit('recovery-started');
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
// Method 1: Send Enter via screen (if we've got a session)
|
|
249
|
-
const screenSession = process.env.STY || process.env.SPECMEM_SCREEN_SESSION;
|
|
250
|
-
if (screenSession) {
|
|
251
|
-
this.log('sending Enter via screen session:', screenSession);
|
|
252
|
-
execSync(`screen -S "${screenSession}" -X stuff $'\\r'`, {
|
|
253
|
-
stdio: 'ignore',
|
|
254
|
-
timeout: 5000
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
await this.sleep(this.config.recoveryDelayMs);
|
|
258
|
-
|
|
259
|
-
// Check if recovered
|
|
260
|
-
if (!this.checkGlitchState()) {
|
|
261
|
-
this.log('recovery successful via screen');
|
|
262
|
-
this.emit('recovery-success', { method: 'screen' });
|
|
263
|
-
return true;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Method 2: Force scrollback clear
|
|
268
|
-
this.log('forcing scrollback clear');
|
|
269
|
-
if (process.stdout.isTTY) {
|
|
270
|
-
process.stdout.write('\x1b[3J'); // CLEAR_SCROLLBACK
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
await this.sleep(this.config.recoveryDelayMs);
|
|
274
|
-
|
|
275
|
-
if (!this.checkGlitchState()) {
|
|
276
|
-
this.log('recovery successful via scrollback clear');
|
|
277
|
-
this.emit('recovery-success', { method: 'scrollback-clear' });
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
this.log('recovery failed');
|
|
282
|
-
this.emit('recovery-failed');
|
|
283
|
-
return false;
|
|
284
|
-
|
|
285
|
-
} catch (err) {
|
|
286
|
-
this.log('recovery error:', err.message);
|
|
287
|
-
this.emit('recovery-error', { error: err });
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
sleep(ms) {
|
|
293
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Reset all state (call after recovery)
|
|
298
|
-
*/
|
|
299
|
-
reset() {
|
|
300
|
-
this.lastStdinTime = Date.now();
|
|
301
|
-
this.lastStdoutTime = Date.now();
|
|
302
|
-
this.sigwinchCount = 0;
|
|
303
|
-
this.renderTimes = [];
|
|
304
|
-
this.isGlitched = false;
|
|
305
|
-
this.glitchStartTime = null;
|
|
306
|
-
this.log('state reset');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Get current metrics
|
|
311
|
-
*/
|
|
312
|
-
getMetrics() {
|
|
313
|
-
return {
|
|
314
|
-
...this.metrics,
|
|
315
|
-
isGlitched: this.isGlitched,
|
|
316
|
-
glitchDuration: this.getGlitchDuration(),
|
|
317
|
-
renderRate: this.renderTimes.length,
|
|
318
|
-
sigwinchRate: this.sigwinchCount,
|
|
319
|
-
lastStdinAgo: Date.now() - this.lastStdinTime,
|
|
320
|
-
lastStdoutAgo: Date.now() - this.lastStdoutTime
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Disable the detector
|
|
326
|
-
*/
|
|
327
|
-
disable() {
|
|
328
|
-
if (this.checkInterval) {
|
|
329
|
-
clearInterval(this.checkInterval);
|
|
330
|
-
this.checkInterval = null;
|
|
331
|
-
}
|
|
332
|
-
this.installed = false;
|
|
333
|
-
this.log('disabled');
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Singleton instance
|
|
338
|
-
let instance = null;
|
|
339
|
-
|
|
340
|
-
function getDetector(config) {
|
|
341
|
-
if (!instance) {
|
|
342
|
-
instance = new GlitchDetector(config);
|
|
343
|
-
}
|
|
344
|
-
return instance;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
module.exports = {
|
|
348
|
-
GlitchDetector,
|
|
349
|
-
getDetector,
|
|
350
|
-
DEFAULT_CONFIG
|
|
351
|
-
};
|