@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.4",
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.