pty-manager 1.2.21 → 1.3.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.
@@ -289,7 +289,7 @@ var SPECIAL_KEYS = {
289
289
  };
290
290
  var BRACKETED_PASTE_START = "\x1B[200~";
291
291
  var BRACKETED_PASTE_END = "\x1B[201~";
292
- var PTYSession = class extends import_events.EventEmitter {
292
+ var PTYSession = class _PTYSession extends import_events.EventEmitter {
293
293
  constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
294
294
  super();
295
295
  this.adapter = adapter;
@@ -327,6 +327,9 @@ var PTYSession = class extends import_events.EventEmitter {
327
327
  _lastStallHash = null;
328
328
  _stallStartedAt = null;
329
329
  _lastContentHash = null;
330
+ // Task completion detection (idle detection when busy)
331
+ _taskCompleteTimer = null;
332
+ static TASK_COMPLETE_DEBOUNCE_MS = 1500;
330
333
  id;
331
334
  config;
332
335
  get status() {
@@ -466,6 +469,19 @@ var PTYSession = class extends import_events.EventEmitter {
466
469
  return;
467
470
  }
468
471
  this._lastStallHash = hash;
472
+ if (this._status === "busy" && this.adapter.detectTaskComplete?.(this.outputBuffer)) {
473
+ this._status = "ready";
474
+ this._lastBlockingPromptHash = null;
475
+ this.outputBuffer = "";
476
+ this.clearStallTimer();
477
+ this.emit("status_changed", "ready");
478
+ this.emit("task_complete");
479
+ this.logger.info(
480
+ { sessionId: this.id },
481
+ "Task complete (adapter fast-path) \u2014 agent returned to idle prompt"
482
+ );
483
+ return;
484
+ }
469
485
  const recentRaw = this.outputBuffer.slice(-2e3);
470
486
  const recentOutput = this.stripAnsiForStall(recentRaw);
471
487
  const stallDurationMs = this._stallStartedAt ? Date.now() - this._stallStartedAt : this._stallTimeoutMs;
@@ -502,10 +518,12 @@ var PTYSession = class extends import_events.EventEmitter {
502
518
  * word boundaries — e.g. "Do\x1b[5Cyou" becomes "Do you", not "Doyou".
503
519
  */
504
520
  stripAnsiForStall(str) {
505
- let result = str.replace(/\x1b\[\d*[CDABG]/g, " ");
521
+ let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
506
522
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
523
+ result = result.replace(/\x1b\[\d*[JK]/g, " ");
507
524
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
508
- result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓]/g, " ");
525
+ result = result.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
526
+ result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
509
527
  result = result.replace(/ {2,}/g, " ");
510
528
  return result;
511
529
  }
@@ -563,6 +581,39 @@ var PTYSession = class extends import_events.EventEmitter {
563
581
  }
564
582
  }
565
583
  // ─────────────────────────────────────────────────────────────────────────────
584
+ // Task Completion Detection
585
+ // ─────────────────────────────────────────────────────────────────────────────
586
+ /**
587
+ * Schedule a task_complete transition after a debounce period.
588
+ * If new non-whitespace output arrives before the timer fires,
589
+ * the timer is cancelled (by cancelTaskComplete in onData).
590
+ */
591
+ scheduleTaskComplete() {
592
+ if (this._taskCompleteTimer) return;
593
+ this._taskCompleteTimer = setTimeout(() => {
594
+ this._taskCompleteTimer = null;
595
+ if (this._status !== "busy") return;
596
+ if (!this.adapter.detectReady(this.outputBuffer)) return;
597
+ this._status = "ready";
598
+ this._lastBlockingPromptHash = null;
599
+ this.outputBuffer = "";
600
+ this.clearStallTimer();
601
+ this.emit("status_changed", "ready");
602
+ this.emit("task_complete");
603
+ this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
604
+ }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
605
+ }
606
+ /**
607
+ * Cancel a pending task_complete timer (new output arrived that
608
+ * doesn't match the idle prompt, so the agent is still working).
609
+ */
610
+ cancelTaskComplete() {
611
+ if (this._taskCompleteTimer) {
612
+ clearTimeout(this._taskCompleteTimer);
613
+ this._taskCompleteTimer = null;
614
+ }
615
+ }
616
+ // ─────────────────────────────────────────────────────────────────────────────
566
617
  // Lifecycle
567
618
  // ─────────────────────────────────────────────────────────────────────────────
568
619
  /**
@@ -624,10 +675,6 @@ var PTYSession = class extends import_events.EventEmitter {
624
675
  this.resetStallTimer();
625
676
  }
626
677
  this.emit("output", data);
627
- const blockingPrompt = this.detectAndHandleBlockingPrompt();
628
- if (blockingPrompt) {
629
- return;
630
- }
631
678
  if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
632
679
  this._status = "ready";
633
680
  this._lastBlockingPromptHash = null;
@@ -637,6 +684,15 @@ var PTYSession = class extends import_events.EventEmitter {
637
684
  this.logger.info({ sessionId: this.id }, "Session ready");
638
685
  return;
639
686
  }
687
+ if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
688
+ this.scheduleTaskComplete();
689
+ } else {
690
+ this.cancelTaskComplete();
691
+ }
692
+ const blockingPrompt = this.detectAndHandleBlockingPrompt();
693
+ if (blockingPrompt) {
694
+ return;
695
+ }
640
696
  if (this._status !== "ready" && this._status !== "busy") {
641
697
  const loginDetection = this.adapter.detectLogin(this.outputBuffer);
642
698
  if (loginDetection.required && this._status !== "authenticating") {
@@ -859,6 +915,7 @@ var PTYSession = class extends import_events.EventEmitter {
859
915
  */
860
916
  send(message) {
861
917
  this._status = "busy";
918
+ this.emit("status_changed", "busy");
862
919
  this.resetStallTimer();
863
920
  const msg = {
864
921
  id: `${this.id}-msg-${++this.messageCounter}`,
@@ -969,6 +1026,7 @@ var PTYSession = class extends import_events.EventEmitter {
969
1026
  if (this.ptyProcess) {
970
1027
  this._status = "stopping";
971
1028
  this.clearStallTimer();
1029
+ this.cancelTaskComplete();
972
1030
  this.ptyProcess.kill(signal);
973
1031
  this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
974
1032
  }
@@ -1122,6 +1180,12 @@ var PTYManager = class extends import_events2.EventEmitter {
1122
1180
  session.on("error", (error) => {
1123
1181
  this.emit("session_error", session.toHandle(), error.message);
1124
1182
  });
1183
+ session.on("status_changed", () => {
1184
+ this.emit("session_status_changed", session.toHandle());
1185
+ });
1186
+ session.on("task_complete", () => {
1187
+ this.emit("task_complete", session.toHandle());
1188
+ });
1125
1189
  session.on("stall_detected", (recentOutput, stallDurationMs) => {
1126
1190
  const handle = session.toHandle();
1127
1191
  this.emit("stall_detected", handle, recentOutput, stallDurationMs);
@@ -1512,6 +1576,19 @@ manager.on("question", (handle, question) => {
1512
1576
  question
1513
1577
  });
1514
1578
  });
1579
+ manager.on("session_status_changed", (handle) => {
1580
+ emit({
1581
+ event: "status_changed",
1582
+ id: handle.id,
1583
+ status: handle.status
1584
+ });
1585
+ });
1586
+ manager.on("task_complete", (handle) => {
1587
+ emit({
1588
+ event: "task_complete",
1589
+ id: handle.id
1590
+ });
1591
+ });
1515
1592
  manager.on("stall_detected", (handle, recentOutput, stallDurationMs) => {
1516
1593
  emit({
1517
1594
  event: "stall_detected",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pty-manager",
3
- "version": "1.2.21",
3
+ "version": "1.3.0",
4
4
  "description": "PTY session manager with lifecycle management, pluggable adapters, and blocking prompt detection",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",