claudescreenfix-hardwicksoftware 1.0.1 → 2.0.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 +5 -4
- package/glitch-detector.cjs +351 -0
- package/index.cjs +130 -17
- package/loader.cjs +1 -1
- package/package.json +10 -4
package/bin/claude-fixed.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
* wrapper script - runs claude with the terminal fix loaded
|
|
6
6
|
*
|
|
7
7
|
* finds your claude binary and runs it with our fix injected
|
|
8
|
-
*
|
|
8
|
+
* you don't need any manual setup, just run claude-fixed instead of claude
|
|
9
|
+
* it'll handle the rest
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const { spawn, execSync } = require('child_process');
|
|
12
13
|
const path = require('path');
|
|
13
14
|
const fs = require('fs');
|
|
14
15
|
|
|
15
|
-
// find the loader path
|
|
16
|
+
// find the loader path - it's in the parent dir
|
|
16
17
|
const loaderPath = path.join(__dirname, '..', 'loader.cjs');
|
|
17
18
|
|
|
18
19
|
if (!fs.existsSync(loaderPath)) {
|
|
@@ -25,11 +26,11 @@ let claudeBin;
|
|
|
25
26
|
try {
|
|
26
27
|
claudeBin = execSync('which claude', { encoding: 'utf8' }).trim();
|
|
27
28
|
} catch (e) {
|
|
28
|
-
console.error('claude not found in PATH - make sure
|
|
29
|
+
console.error('claude not found in PATH - make sure it\'s installed');
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// run claude with our fix loaded via NODE_OPTIONS
|
|
33
|
+
// run claude with our fix loaded via NODE_OPTIONS - it's the cleanest way
|
|
33
34
|
const env = Object.assign({}, process.env, {
|
|
34
35
|
NODE_OPTIONS: '--require ' + loaderPath + ' ' + (process.env.NODE_OPTIONS || '')
|
|
35
36
|
});
|
|
@@ -0,0 +1,351 @@
|
|
|
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
|
+
};
|
package/index.cjs
CHANGED
|
@@ -4,20 +4,24 @@
|
|
|
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 got thousands of lines in the buffer
|
|
7
|
+
* claude code uses ink (react for terminals) and it doesn't clear scrollback
|
|
8
|
+
* so after like 30 min your terminal's 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 ain't thrashing
|
|
15
15
|
* - enhance /clear to actually clear scrollback not just the screen
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
*
|
|
17
|
+
* v1.0.1: fixed the typing bug where keystrokes got eaten
|
|
18
|
+
* - stdin echoes now pass through untouched
|
|
19
|
+
* - clears happen async so typing isn't interrupted
|
|
20
|
+
*
|
|
21
|
+
* v2.0.0: added glitch detection + 120 line limit
|
|
22
|
+
* - actually detects when terminal's fucked instead of just clearing blindly
|
|
23
|
+
* - caps output at 120 lines so buffer won't explode
|
|
24
|
+
* - can force send enter key to break out of frozen state
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
const CLEAR_SCROLLBACK = '\x1b[3J';
|
|
@@ -26,12 +30,25 @@ const CURSOR_RESTORE = '\x1b[u';
|
|
|
26
30
|
const CLEAR_SCREEN = '\x1b[2J';
|
|
27
31
|
const HOME_CURSOR = '\x1b[H';
|
|
28
32
|
|
|
33
|
+
// Try to load glitch detector (optional dependency)
|
|
34
|
+
let GlitchDetector = null;
|
|
35
|
+
let glitchDetector = null;
|
|
36
|
+
try {
|
|
37
|
+
const detector = require('./glitch-detector.cjs');
|
|
38
|
+
GlitchDetector = detector.GlitchDetector;
|
|
39
|
+
glitchDetector = detector.getDetector();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// Glitch detector not available, continue without it
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
// config - tweak these if needed
|
|
30
45
|
const config = {
|
|
31
46
|
resizeDebounceMs: 150, // how long to wait before firing resize
|
|
32
47
|
periodicClearMs: 60000, // clear scrollback every 60s
|
|
33
48
|
clearAfterRenders: 500, // or after 500 render cycles
|
|
34
49
|
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
|
|
35
52
|
debug: process.env.CLAUDE_TERMINAL_FIX_DEBUG === '1',
|
|
36
53
|
disabled: process.env.CLAUDE_TERMINAL_FIX_DISABLED === '1'
|
|
37
54
|
};
|
|
@@ -45,6 +62,8 @@ let installed = false;
|
|
|
45
62
|
let lastTypingTime = 0; // track when user last typed
|
|
46
63
|
let pendingClear = false; // defer clear if typing active
|
|
47
64
|
let clearIntervalId = null;
|
|
65
|
+
let lineCount = 0; // NEW: track output line count for 120-line limit
|
|
66
|
+
let glitchRecoveryInProgress = false; // NEW: prevent recovery loops
|
|
48
67
|
|
|
49
68
|
function log(...args) {
|
|
50
69
|
if (config.debug) {
|
|
@@ -53,7 +72,7 @@ function log(...args) {
|
|
|
53
72
|
}
|
|
54
73
|
|
|
55
74
|
/**
|
|
56
|
-
* check if user
|
|
75
|
+
* check if user's actively typing (within cooldown window)
|
|
57
76
|
*/
|
|
58
77
|
function isTypingActive() {
|
|
59
78
|
return (Date.now() - lastTypingTime) < config.typingCooldownMs;
|
|
@@ -61,7 +80,8 @@ function isTypingActive() {
|
|
|
61
80
|
|
|
62
81
|
/**
|
|
63
82
|
* detect if this looks like a stdin echo (single printable char or short sequence)
|
|
64
|
-
* stdin echoes are typically: single chars, backspace
|
|
83
|
+
* stdin echoes are typically: single chars, backspace seqs, arrow key echoes
|
|
84
|
+
* we don't wanna mess with these or typing gets wonky
|
|
65
85
|
*/
|
|
66
86
|
function isStdinEcho(chunk) {
|
|
67
87
|
// single printable character (including space)
|
|
@@ -84,7 +104,7 @@ function isStdinEcho(chunk) {
|
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
/**
|
|
87
|
-
* safe clear - defers if typing active
|
|
107
|
+
* safe clear - defers if typing's active so we don't eat keystrokes
|
|
88
108
|
*/
|
|
89
109
|
function safeClearScrollback() {
|
|
90
110
|
if (isTypingActive()) {
|
|
@@ -112,7 +132,7 @@ function safeClearScrollback() {
|
|
|
112
132
|
|
|
113
133
|
/**
|
|
114
134
|
* installs the fix - hooks into stdout and sigwinch
|
|
115
|
-
* call this once at startup, calling again
|
|
135
|
+
* call this once at startup, calling again won't do anything
|
|
116
136
|
*/
|
|
117
137
|
function install() {
|
|
118
138
|
if (installed || config.disabled) {
|
|
@@ -142,9 +162,26 @@ function install() {
|
|
|
142
162
|
|
|
143
163
|
renderCount++;
|
|
144
164
|
|
|
165
|
+
// track output for glitch detection
|
|
166
|
+
if (glitchDetector) {
|
|
167
|
+
glitchDetector.trackStdout();
|
|
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;
|
|
179
|
+
}
|
|
180
|
+
|
|
145
181
|
// ink clears screen before re-render, we piggyback on that
|
|
146
182
|
// but only if not actively typing
|
|
147
183
|
if (chunk.includes(CLEAR_SCREEN) || chunk.includes(HOME_CURSOR)) {
|
|
184
|
+
lineCount = 0; // Reset line count on screen clear
|
|
148
185
|
if (config.clearAfterRenders > 0 && renderCount >= config.clearAfterRenders) {
|
|
149
186
|
if (!isTypingActive()) {
|
|
150
187
|
log('clearing scrollback after ' + renderCount + ' renders');
|
|
@@ -156,11 +193,35 @@ function install() {
|
|
|
156
193
|
}
|
|
157
194
|
}
|
|
158
195
|
|
|
159
|
-
// /clear
|
|
196
|
+
// /clear should actually clear everything (it's user-requested so do it now)
|
|
160
197
|
if (chunk.includes('Conversation cleared') || chunk.includes('Chat cleared')) {
|
|
161
198
|
log('/clear detected, nuking scrollback');
|
|
199
|
+
lineCount = 0;
|
|
162
200
|
chunk = CLEAR_SCROLLBACK + chunk;
|
|
163
201
|
}
|
|
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
|
+
}
|
|
164
225
|
}
|
|
165
226
|
|
|
166
227
|
return originalWrite(chunk, encoding, callback);
|
|
@@ -178,8 +239,28 @@ function install() {
|
|
|
178
239
|
}, config.periodicClearMs);
|
|
179
240
|
}
|
|
180
241
|
|
|
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
|
+
|
|
181
262
|
installed = true;
|
|
182
|
-
log('installed successfully -
|
|
263
|
+
log('installed successfully - v2.0.0 with glitch detection & 120-line limit');
|
|
183
264
|
}
|
|
184
265
|
|
|
185
266
|
function installResizeDebounce() {
|
|
@@ -219,7 +300,7 @@ function installResizeDebounce() {
|
|
|
219
300
|
}
|
|
220
301
|
|
|
221
302
|
/**
|
|
222
|
-
* manually clear scrollback - call this whenever you want
|
|
303
|
+
* manually clear scrollback - call this whenever you want, it won't break anything
|
|
223
304
|
*/
|
|
224
305
|
function clearScrollback() {
|
|
225
306
|
if (originalWrite) {
|
|
@@ -234,12 +315,40 @@ function clearScrollback() {
|
|
|
234
315
|
* get current stats for debugging
|
|
235
316
|
*/
|
|
236
317
|
function getStats() {
|
|
237
|
-
|
|
318
|
+
const stats = {
|
|
238
319
|
renderCount,
|
|
320
|
+
lineCount,
|
|
239
321
|
lastResizeTime,
|
|
240
322
|
installed,
|
|
241
323
|
config
|
|
242
324
|
};
|
|
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;
|
|
243
352
|
}
|
|
244
353
|
|
|
245
354
|
/**
|
|
@@ -253,7 +362,7 @@ function setConfig(key, value) {
|
|
|
253
362
|
}
|
|
254
363
|
|
|
255
364
|
/**
|
|
256
|
-
* disable the fix (mostly for testing)
|
|
365
|
+
* disable the fix (it's mostly for testing)
|
|
257
366
|
*/
|
|
258
367
|
function disable() {
|
|
259
368
|
if (originalWrite) {
|
|
@@ -268,5 +377,9 @@ module.exports = {
|
|
|
268
377
|
getStats,
|
|
269
378
|
setConfig,
|
|
270
379
|
disable,
|
|
271
|
-
config
|
|
380
|
+
config,
|
|
381
|
+
// NEW v2.0 exports
|
|
382
|
+
forceRecovery,
|
|
383
|
+
isGlitched,
|
|
384
|
+
getDetector: () => glitchDetector
|
|
272
385
|
};
|
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
|
+
* you don't need to change any code in claude itself - it just works
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fix = require('./index.cjs');
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudescreenfix-hardwicksoftware",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "fixes the scroll glitch in claude code cli -
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "fixes the scroll glitch in claude code cli - now with GLITCH DETECTION, 120-line limit enforcement, and auto-recovery",
|
|
5
5
|
"main": "index.cjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-fixed": "./bin/claude-fixed.js"
|
|
8
8
|
},
|
|
9
|
-
"scripts": {
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node -e \"const fix = require('./index.cjs'); fix.install(); console.log(fix.getStats());\""
|
|
11
|
+
},
|
|
10
12
|
"keywords": [
|
|
11
13
|
"claude",
|
|
12
14
|
"terminal",
|
|
@@ -15,7 +17,10 @@
|
|
|
15
17
|
"ink",
|
|
16
18
|
"cli",
|
|
17
19
|
"glitch",
|
|
18
|
-
"performance"
|
|
20
|
+
"performance",
|
|
21
|
+
"glitch-detection",
|
|
22
|
+
"recovery",
|
|
23
|
+
"120-line-limit"
|
|
19
24
|
],
|
|
20
25
|
"author": "jonhardwick-spec",
|
|
21
26
|
"license": "MIT",
|
|
@@ -32,6 +37,7 @@
|
|
|
32
37
|
"files": [
|
|
33
38
|
"index.cjs",
|
|
34
39
|
"loader.cjs",
|
|
40
|
+
"glitch-detector.cjs",
|
|
35
41
|
"bin/",
|
|
36
42
|
"README.md",
|
|
37
43
|
"LICENSE"
|