@rikcodes/teamclaude 1.1.20-rik.4 → 1.1.20-rik.5
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/package.json +1 -1
- package/src/tui.js +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rikcodes/teamclaude",
|
|
3
|
-
"version": "1.1.20-rik.
|
|
3
|
+
"version": "1.1.20-rik.5",
|
|
4
4
|
"description": "Multi-account proxy for Claude Code and Codex: pools Claude Max, ChatGPT/Codex, API-key and third-party backend accounts, and rotates on quota",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/tui.js
CHANGED
|
@@ -494,6 +494,18 @@ export class TUI {
|
|
|
494
494
|
start() {
|
|
495
495
|
this.running = true;
|
|
496
496
|
this._openActivityLog();
|
|
497
|
+
// Node puts a TTY stdout in BLOCKING mode, so every paint is a synchronous
|
|
498
|
+
// write(2) that returns only when the terminal has drained the pty. That
|
|
499
|
+
// makes the proxy's event loop hostage to its own display: when the
|
|
500
|
+
// terminal emulator pauses — an Electron pane busy elsewhere, a window
|
|
501
|
+
// occluded, the machine dozing — the write sits in the kernel and nothing
|
|
502
|
+
// else runs: no upstream bytes relayed, no request completed, no log line.
|
|
503
|
+
// Measured live: stalls of 5-29s, the main thread in write() under
|
|
504
|
+
// StreamBase::WriteString, with sessions "waiting for API response" and
|
|
505
|
+
// nothing to see anywhere because the thing that would show it is the
|
|
506
|
+
// thing blocked. Non-blocking here, and the paint below drops a frame
|
|
507
|
+
// when the terminal is behind instead of waiting for it.
|
|
508
|
+
this._setStdoutBlocking(false);
|
|
497
509
|
process.stdout.write(`${ESC}?1049h${ESC}?25l`);
|
|
498
510
|
process.stdin.setRawMode(true);
|
|
499
511
|
process.stdin.resume();
|
|
@@ -550,6 +562,12 @@ export class TUI {
|
|
|
550
562
|
if (this._activityStream) { this._activityStream.end(); this._activityStream = null; }
|
|
551
563
|
process.stdin.removeListener('data', this._dataHandler);
|
|
552
564
|
process.stdout.removeListener('resize', this._resizeHandler);
|
|
565
|
+
if (this._drainHandler) { process.stdout.removeListener('drain', this._drainHandler); this._drainHandler = null; }
|
|
566
|
+
// Blocking again for the exit sequence: a non-blocking write can still be
|
|
567
|
+
// queued when the process exits, and a terminal left on the alternate
|
|
568
|
+
// screen with no cursor is the one state an operator cannot recover
|
|
569
|
+
// without knowing the escape by heart.
|
|
570
|
+
this._setStdoutBlocking(true);
|
|
553
571
|
process.stdout.write(`${ESC}?25h${ESC}?1049l`);
|
|
554
572
|
try { process.stdin.setRawMode(false); } catch {}
|
|
555
573
|
process.stdin.pause();
|
|
@@ -1361,11 +1379,35 @@ export class TUI {
|
|
|
1361
1379
|
_paint(buf, force) {
|
|
1362
1380
|
const stale = Date.now() - (this._lastPaintAt || 0) >= FORCE_REPAINT_MS;
|
|
1363
1381
|
if (!force && !stale && buf === this._lastFrame) return;
|
|
1382
|
+
// The terminal has not taken the previous frame yet. Painting anyway would
|
|
1383
|
+
// only queue another full screen behind it — the operator sees the newest
|
|
1384
|
+
// frame either way, so the one in between is worth nothing. Drop it, and
|
|
1385
|
+
// paint what is current once the terminal catches up.
|
|
1386
|
+
if (process.stdout.writableNeedDrain) {
|
|
1387
|
+
this._pendingPaint = true;
|
|
1388
|
+
if (!this._drainHandler) {
|
|
1389
|
+
this._drainHandler = () => {
|
|
1390
|
+
this._drainHandler = null;
|
|
1391
|
+
if (this._pendingPaint && this.running) { this._pendingPaint = false; this.render({ force: true }); }
|
|
1392
|
+
};
|
|
1393
|
+
process.stdout.once('drain', this._drainHandler);
|
|
1394
|
+
}
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
this._pendingPaint = false;
|
|
1364
1398
|
this._lastFrame = buf;
|
|
1365
1399
|
this._lastPaintAt = Date.now();
|
|
1366
1400
|
process.stdout.write(buf);
|
|
1367
1401
|
}
|
|
1368
1402
|
|
|
1403
|
+
/** Flip stdout between blocking and non-blocking. A handle without the
|
|
1404
|
+
* method (a pipe in tests, a file) needs neither, and a failure to flip is
|
|
1405
|
+
* worth no more than the old behaviour it leaves in place. */
|
|
1406
|
+
_setStdoutBlocking(blocking) {
|
|
1407
|
+
// `_handle` is Node-internal and untyped; the optional chain is the guard.
|
|
1408
|
+
try { /** @type {any} */ (process.stdout)._handle?.setBlocking?.(blocking); } catch {}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1369
1411
|
_render(force = false) {
|
|
1370
1412
|
// Reset the display the instant a quota window (e.g. 5-hour session) expires,
|
|
1371
1413
|
// instead of waiting for the next request to clear it.
|