pty-manager 1.2.21 → 1.2.22

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/dist/index.d.mts CHANGED
@@ -405,6 +405,8 @@ interface PTYSessionEvents {
405
405
  exit: (code: number) => void;
406
406
  error: (error: Error) => void;
407
407
  stall_detected: (recentOutput: string, stallDurationMs: number) => void;
408
+ status_changed: (status: SessionStatus) => void;
409
+ task_complete: () => void;
408
410
  }
409
411
  /**
410
412
  * Special key mappings to escape sequences
@@ -433,6 +435,8 @@ declare class PTYSession extends EventEmitter {
433
435
  private _lastStallHash;
434
436
  private _stallStartedAt;
435
437
  private _lastContentHash;
438
+ private _taskCompleteTimer;
439
+ private static readonly TASK_COMPLETE_DEBOUNCE_MS;
436
440
  readonly id: string;
437
441
  readonly config: SpawnConfig;
438
442
  constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
@@ -501,6 +505,17 @@ declare class PTYSession extends EventEmitter {
501
505
  * Called by the manager after onStallClassify resolves.
502
506
  */
503
507
  handleStallClassification(classification: StallClassification | null): void;
508
+ /**
509
+ * Schedule a task_complete transition after a debounce period.
510
+ * If new non-whitespace output arrives before the timer fires,
511
+ * the timer is cancelled (by cancelTaskComplete in onData).
512
+ */
513
+ private scheduleTaskComplete;
514
+ /**
515
+ * Cancel a pending task_complete timer (new output arrived that
516
+ * doesn't match the idle prompt, so the agent is still working).
517
+ */
518
+ private cancelTaskComplete;
504
519
  /**
505
520
  * Start the PTY session
506
521
  */
@@ -612,6 +627,8 @@ interface PTYManagerEvents {
612
627
  message: (message: SessionMessage) => void;
613
628
  question: (session: SessionHandle, question: string) => void;
614
629
  stall_detected: (session: SessionHandle, recentOutput: string, stallDurationMs: number) => void;
630
+ session_status_changed: (session: SessionHandle) => void;
631
+ task_complete: (session: SessionHandle) => void;
615
632
  }
616
633
  declare class PTYManager extends EventEmitter {
617
634
  private sessions;
package/dist/index.d.ts CHANGED
@@ -405,6 +405,8 @@ interface PTYSessionEvents {
405
405
  exit: (code: number) => void;
406
406
  error: (error: Error) => void;
407
407
  stall_detected: (recentOutput: string, stallDurationMs: number) => void;
408
+ status_changed: (status: SessionStatus) => void;
409
+ task_complete: () => void;
408
410
  }
409
411
  /**
410
412
  * Special key mappings to escape sequences
@@ -433,6 +435,8 @@ declare class PTYSession extends EventEmitter {
433
435
  private _lastStallHash;
434
436
  private _stallStartedAt;
435
437
  private _lastContentHash;
438
+ private _taskCompleteTimer;
439
+ private static readonly TASK_COMPLETE_DEBOUNCE_MS;
436
440
  readonly id: string;
437
441
  readonly config: SpawnConfig;
438
442
  constructor(adapter: CLIAdapter, config: SpawnConfig, logger?: Logger, stallDetectionEnabled?: boolean, defaultStallTimeoutMs?: number);
@@ -501,6 +505,17 @@ declare class PTYSession extends EventEmitter {
501
505
  * Called by the manager after onStallClassify resolves.
502
506
  */
503
507
  handleStallClassification(classification: StallClassification | null): void;
508
+ /**
509
+ * Schedule a task_complete transition after a debounce period.
510
+ * If new non-whitespace output arrives before the timer fires,
511
+ * the timer is cancelled (by cancelTaskComplete in onData).
512
+ */
513
+ private scheduleTaskComplete;
514
+ /**
515
+ * Cancel a pending task_complete timer (new output arrived that
516
+ * doesn't match the idle prompt, so the agent is still working).
517
+ */
518
+ private cancelTaskComplete;
504
519
  /**
505
520
  * Start the PTY session
506
521
  */
@@ -612,6 +627,8 @@ interface PTYManagerEvents {
612
627
  message: (message: SessionMessage) => void;
613
628
  question: (session: SessionHandle, question: string) => void;
614
629
  stall_detected: (session: SessionHandle, recentOutput: string, stallDurationMs: number) => void;
630
+ session_status_changed: (session: SessionHandle) => void;
631
+ task_complete: (session: SessionHandle) => void;
615
632
  }
616
633
  declare class PTYManager extends EventEmitter {
617
634
  private sessions;
package/dist/index.js CHANGED
@@ -306,7 +306,7 @@ var SPECIAL_KEYS = {
306
306
  };
307
307
  var BRACKETED_PASTE_START = "\x1B[200~";
308
308
  var BRACKETED_PASTE_END = "\x1B[201~";
309
- var PTYSession = class extends import_events.EventEmitter {
309
+ var PTYSession = class _PTYSession extends import_events.EventEmitter {
310
310
  constructor(adapter, config, logger, stallDetectionEnabled, defaultStallTimeoutMs) {
311
311
  super();
312
312
  this.adapter = adapter;
@@ -344,6 +344,9 @@ var PTYSession = class extends import_events.EventEmitter {
344
344
  _lastStallHash = null;
345
345
  _stallStartedAt = null;
346
346
  _lastContentHash = null;
347
+ // Task completion detection (idle detection when busy)
348
+ _taskCompleteTimer = null;
349
+ static TASK_COMPLETE_DEBOUNCE_MS = 1500;
347
350
  id;
348
351
  config;
349
352
  get status() {
@@ -519,10 +522,12 @@ var PTYSession = class extends import_events.EventEmitter {
519
522
  * word boundaries — e.g. "Do\x1b[5Cyou" becomes "Do you", not "Doyou".
520
523
  */
521
524
  stripAnsiForStall(str) {
522
- let result = str.replace(/\x1b\[\d*[CDABG]/g, " ");
525
+ let result = str.replace(/\x1b\[\d*[CDABGdEF]/g, " ");
523
526
  result = result.replace(/\x1b\[\d*(?:;\d+)?[Hf]/g, " ");
527
+ result = result.replace(/\x1b\[\d*[JK]/g, " ");
524
528
  result = result.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
525
- result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓]/g, " ");
529
+ result = result.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
530
+ result = result.replace(/[│╭╰╮╯─═╌║╔╗╚╝╠╣╦╩╬┌┐└┘├┤┬┴┼●○❯❮▶◀⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷✻✶✳✢⏺←→↑↓⬆⬇◆◇▪▫■□▲△▼▽◈⟨⟩⌘⏎⏏⌫⌦⇧⇪⌥]/g, " ");
526
531
  result = result.replace(/ {2,}/g, " ");
527
532
  return result;
528
533
  }
@@ -580,6 +585,39 @@ var PTYSession = class extends import_events.EventEmitter {
580
585
  }
581
586
  }
582
587
  // ─────────────────────────────────────────────────────────────────────────────
588
+ // Task Completion Detection
589
+ // ─────────────────────────────────────────────────────────────────────────────
590
+ /**
591
+ * Schedule a task_complete transition after a debounce period.
592
+ * If new non-whitespace output arrives before the timer fires,
593
+ * the timer is cancelled (by cancelTaskComplete in onData).
594
+ */
595
+ scheduleTaskComplete() {
596
+ if (this._taskCompleteTimer) return;
597
+ this._taskCompleteTimer = setTimeout(() => {
598
+ this._taskCompleteTimer = null;
599
+ if (this._status !== "busy") return;
600
+ if (!this.adapter.detectReady(this.outputBuffer)) return;
601
+ this._status = "ready";
602
+ this._lastBlockingPromptHash = null;
603
+ this.outputBuffer = "";
604
+ this.clearStallTimer();
605
+ this.emit("status_changed", "ready");
606
+ this.emit("task_complete");
607
+ this.logger.info({ sessionId: this.id }, "Task complete \u2014 agent returned to idle prompt");
608
+ }, _PTYSession.TASK_COMPLETE_DEBOUNCE_MS);
609
+ }
610
+ /**
611
+ * Cancel a pending task_complete timer (new output arrived that
612
+ * doesn't match the idle prompt, so the agent is still working).
613
+ */
614
+ cancelTaskComplete() {
615
+ if (this._taskCompleteTimer) {
616
+ clearTimeout(this._taskCompleteTimer);
617
+ this._taskCompleteTimer = null;
618
+ }
619
+ }
620
+ // ─────────────────────────────────────────────────────────────────────────────
583
621
  // Lifecycle
584
622
  // ─────────────────────────────────────────────────────────────────────────────
585
623
  /**
@@ -641,10 +679,6 @@ var PTYSession = class extends import_events.EventEmitter {
641
679
  this.resetStallTimer();
642
680
  }
643
681
  this.emit("output", data);
644
- const blockingPrompt = this.detectAndHandleBlockingPrompt();
645
- if (blockingPrompt) {
646
- return;
647
- }
648
682
  if ((this._status === "starting" || this._status === "authenticating") && this.adapter.detectReady(this.outputBuffer)) {
649
683
  this._status = "ready";
650
684
  this._lastBlockingPromptHash = null;
@@ -654,6 +688,15 @@ var PTYSession = class extends import_events.EventEmitter {
654
688
  this.logger.info({ sessionId: this.id }, "Session ready");
655
689
  return;
656
690
  }
691
+ if (this._status === "busy" && this.adapter.detectReady(this.outputBuffer)) {
692
+ this.scheduleTaskComplete();
693
+ } else {
694
+ this.cancelTaskComplete();
695
+ }
696
+ const blockingPrompt = this.detectAndHandleBlockingPrompt();
697
+ if (blockingPrompt) {
698
+ return;
699
+ }
657
700
  if (this._status !== "ready" && this._status !== "busy") {
658
701
  const loginDetection = this.adapter.detectLogin(this.outputBuffer);
659
702
  if (loginDetection.required && this._status !== "authenticating") {
@@ -876,6 +919,7 @@ var PTYSession = class extends import_events.EventEmitter {
876
919
  */
877
920
  send(message) {
878
921
  this._status = "busy";
922
+ this.emit("status_changed", "busy");
879
923
  this.resetStallTimer();
880
924
  const msg = {
881
925
  id: `${this.id}-msg-${++this.messageCounter}`,
@@ -986,6 +1030,7 @@ var PTYSession = class extends import_events.EventEmitter {
986
1030
  if (this.ptyProcess) {
987
1031
  this._status = "stopping";
988
1032
  this.clearStallTimer();
1033
+ this.cancelTaskComplete();
989
1034
  this.ptyProcess.kill(signal);
990
1035
  this.logger.info({ sessionId: this.id, signal }, "Killing PTY session");
991
1036
  }
@@ -1139,6 +1184,12 @@ var PTYManager = class extends import_events2.EventEmitter {
1139
1184
  session.on("error", (error) => {
1140
1185
  this.emit("session_error", session.toHandle(), error.message);
1141
1186
  });
1187
+ session.on("status_changed", () => {
1188
+ this.emit("session_status_changed", session.toHandle());
1189
+ });
1190
+ session.on("task_complete", () => {
1191
+ this.emit("task_complete", session.toHandle());
1192
+ });
1142
1193
  session.on("stall_detected", (recentOutput, stallDurationMs) => {
1143
1194
  const handle = session.toHandle();
1144
1195
  this.emit("stall_detected", handle, recentOutput, stallDurationMs);
@@ -1992,6 +2043,24 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
1992
2043
  }
1993
2044
  break;
1994
2045
  }
2046
+ case "status_changed": {
2047
+ const session = this.sessions.get(id);
2048
+ if (session) {
2049
+ session.status = event.status;
2050
+ session.lastActivityAt = /* @__PURE__ */ new Date();
2051
+ this.emit("session_status_changed", session);
2052
+ }
2053
+ break;
2054
+ }
2055
+ case "task_complete": {
2056
+ const session = this.sessions.get(id);
2057
+ if (session) {
2058
+ session.status = "ready";
2059
+ session.lastActivityAt = /* @__PURE__ */ new Date();
2060
+ this.emit("task_complete", session);
2061
+ }
2062
+ break;
2063
+ }
1995
2064
  case "stall_detected": {
1996
2065
  const session = this.sessions.get(id);
1997
2066
  if (session) {