pty-manager 1.7.3 → 1.9.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/dist/index.d.mts CHANGED
@@ -188,6 +188,17 @@ interface StallClassification {
188
188
  /** Suggested response to send (for waiting_for_input with auto-respond) */
189
189
  suggestedResponse?: string;
190
190
  }
191
+ /**
192
+ * Information about an external tool/process running within a session.
193
+ * Emitted when the adapter detects a tool is actively executing (e.g. browser,
194
+ * bash command, Node process). Suppresses stall detection while active.
195
+ */
196
+ interface ToolRunningInfo {
197
+ /** Name of the tool (e.g. "Chrome", "bash", "node", "python") */
198
+ toolName: string;
199
+ /** Optional description of what the tool is doing */
200
+ description?: string;
201
+ }
191
202
  /**
192
203
  * Logger interface (bring your own logger)
193
204
  */
@@ -387,6 +398,17 @@ interface CLIAdapter {
387
398
  * "Reading N files", "Waiting for LLM", etc.
388
399
  */
389
400
  detectLoading?(output: string): boolean;
401
+ /**
402
+ * Optional: Detect if an external tool/process is currently running within
403
+ * the session (e.g. browser, bash command, Node server, Python script).
404
+ *
405
+ * When a tool is detected, stall detection is suppressed (the agent is
406
+ * working, just through an external process) and a `tool_running` event
407
+ * is emitted so the UI can display the active tool.
408
+ *
409
+ * Return null when no tool is detected.
410
+ */
411
+ detectToolRunning?(output: string): ToolRunningInfo | null;
390
412
  /**
391
413
  * Optional: Get health check command
392
414
  */
@@ -453,6 +475,7 @@ interface PTYSessionEvents {
453
475
  stall_detected: (recentOutput: string, stallDurationMs: number) => void;
454
476
  status_changed: (status: SessionStatus) => void;
455
477
  task_complete: () => void;
478
+ tool_running: (info: ToolRunningInfo) => void;
456
479
  }
457
480
  /**
458
481
  * Special key mappings to escape sequences
@@ -490,6 +513,7 @@ declare class PTYSession extends EventEmitter {
490
513
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
491
514
  private _readySettleTimer;
492
515
  private _readySettlePending;
516
+ private _lastToolRunningName;
493
517
  private _processScheduled;
494
518
  private static readonly MAX_OUTPUT_BUFFER;
495
519
  readonly id: string;
@@ -669,6 +693,15 @@ declare class PTYSession extends EventEmitter {
669
693
  * @param keys - Key name(s) to send, e.g. "ctrl+c" or ["up", "up", "enter"]
670
694
  */
671
695
  sendKeys(keys: string | string[]): void;
696
+ /**
697
+ * Normalize a list of key names for SPECIAL_KEYS lookup.
698
+ *
699
+ * Handles two problems:
700
+ * 1. Modifier aliases: "control" → "ctrl", "command" → "meta", "option" → "alt"
701
+ * 2. Comma-separated compound keys from stall classifier: ["control", "c"] → ["ctrl+c"]
702
+ * A bare modifier followed by a single char/key is joined with "+".
703
+ */
704
+ static normalizeKeyList(keys: string[]): string[];
672
705
  /**
673
706
  * Select a TUI menu option by index (0-based).
674
707
  * Sends Down arrow `optionIndex` times, then Enter, with 50ms delays.
@@ -728,6 +761,7 @@ interface PTYManagerEvents {
728
761
  stall_detected: (session: SessionHandle, recentOutput: string, stallDurationMs: number) => void;
729
762
  session_status_changed: (session: SessionHandle) => void;
730
763
  task_complete: (session: SessionHandle) => void;
764
+ tool_running: (session: SessionHandle, info: ToolRunningInfo) => void;
731
765
  }
732
766
  declare class PTYManager extends EventEmitter {
733
767
  private sessions;
@@ -1002,6 +1036,11 @@ declare class ShellAdapter implements CLIAdapter {
1002
1036
  detectLogin(_output: string): LoginDetection;
1003
1037
  detectBlockingPrompt(_output: string): BlockingPromptDetection;
1004
1038
  detectReady(output: string): boolean;
1039
+ /**
1040
+ * Detect shell continuation prompts that indicate the shell is NOT ready
1041
+ * for a new command (e.g., unclosed quote, heredoc, backtick).
1042
+ */
1043
+ private isContinuationPrompt;
1005
1044
  detectExit(output: string): {
1006
1045
  exited: boolean;
1007
1046
  code?: number;
@@ -1185,4 +1224,4 @@ declare function isBun(): boolean;
1185
1224
  */
1186
1225
  declare function createPTYManager(options?: BunPTYManagerOptions): BunCompatiblePTYManager;
1187
1226
 
1188
- export { type AdapterFactoryConfig, AdapterRegistry, type AuthRequiredInfo, type AuthRequiredMethod, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, type BuildTimelineOptions, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TaskCompletionTimelineResult, type TaskCompletionTimelineStep, type TaskCompletionTraceRecord, type TaskCompletionTurnTimeline, type TerminalAttachment, type WorkerSessionHandle, buildTaskCompletionTimeline, createAdapter, createPTYManager, extractTaskCompletionTraceRecords, isBun };
1227
+ export { type AdapterFactoryConfig, AdapterRegistry, type AuthRequiredInfo, type AuthRequiredMethod, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, type BuildTimelineOptions, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TaskCompletionTimelineResult, type TaskCompletionTimelineStep, type TaskCompletionTraceRecord, type TaskCompletionTurnTimeline, type TerminalAttachment, type ToolRunningInfo, type WorkerSessionHandle, buildTaskCompletionTimeline, createAdapter, createPTYManager, extractTaskCompletionTraceRecords, isBun };
package/dist/index.d.ts CHANGED
@@ -188,6 +188,17 @@ interface StallClassification {
188
188
  /** Suggested response to send (for waiting_for_input with auto-respond) */
189
189
  suggestedResponse?: string;
190
190
  }
191
+ /**
192
+ * Information about an external tool/process running within a session.
193
+ * Emitted when the adapter detects a tool is actively executing (e.g. browser,
194
+ * bash command, Node process). Suppresses stall detection while active.
195
+ */
196
+ interface ToolRunningInfo {
197
+ /** Name of the tool (e.g. "Chrome", "bash", "node", "python") */
198
+ toolName: string;
199
+ /** Optional description of what the tool is doing */
200
+ description?: string;
201
+ }
191
202
  /**
192
203
  * Logger interface (bring your own logger)
193
204
  */
@@ -387,6 +398,17 @@ interface CLIAdapter {
387
398
  * "Reading N files", "Waiting for LLM", etc.
388
399
  */
389
400
  detectLoading?(output: string): boolean;
401
+ /**
402
+ * Optional: Detect if an external tool/process is currently running within
403
+ * the session (e.g. browser, bash command, Node server, Python script).
404
+ *
405
+ * When a tool is detected, stall detection is suppressed (the agent is
406
+ * working, just through an external process) and a `tool_running` event
407
+ * is emitted so the UI can display the active tool.
408
+ *
409
+ * Return null when no tool is detected.
410
+ */
411
+ detectToolRunning?(output: string): ToolRunningInfo | null;
390
412
  /**
391
413
  * Optional: Get health check command
392
414
  */
@@ -453,6 +475,7 @@ interface PTYSessionEvents {
453
475
  stall_detected: (recentOutput: string, stallDurationMs: number) => void;
454
476
  status_changed: (status: SessionStatus) => void;
455
477
  task_complete: () => void;
478
+ tool_running: (info: ToolRunningInfo) => void;
456
479
  }
457
480
  /**
458
481
  * Special key mappings to escape sequences
@@ -490,6 +513,7 @@ declare class PTYSession extends EventEmitter {
490
513
  private static readonly TASK_COMPLETE_DEBOUNCE_MS;
491
514
  private _readySettleTimer;
492
515
  private _readySettlePending;
516
+ private _lastToolRunningName;
493
517
  private _processScheduled;
494
518
  private static readonly MAX_OUTPUT_BUFFER;
495
519
  readonly id: string;
@@ -669,6 +693,15 @@ declare class PTYSession extends EventEmitter {
669
693
  * @param keys - Key name(s) to send, e.g. "ctrl+c" or ["up", "up", "enter"]
670
694
  */
671
695
  sendKeys(keys: string | string[]): void;
696
+ /**
697
+ * Normalize a list of key names for SPECIAL_KEYS lookup.
698
+ *
699
+ * Handles two problems:
700
+ * 1. Modifier aliases: "control" → "ctrl", "command" → "meta", "option" → "alt"
701
+ * 2. Comma-separated compound keys from stall classifier: ["control", "c"] → ["ctrl+c"]
702
+ * A bare modifier followed by a single char/key is joined with "+".
703
+ */
704
+ static normalizeKeyList(keys: string[]): string[];
672
705
  /**
673
706
  * Select a TUI menu option by index (0-based).
674
707
  * Sends Down arrow `optionIndex` times, then Enter, with 50ms delays.
@@ -728,6 +761,7 @@ interface PTYManagerEvents {
728
761
  stall_detected: (session: SessionHandle, recentOutput: string, stallDurationMs: number) => void;
729
762
  session_status_changed: (session: SessionHandle) => void;
730
763
  task_complete: (session: SessionHandle) => void;
764
+ tool_running: (session: SessionHandle, info: ToolRunningInfo) => void;
731
765
  }
732
766
  declare class PTYManager extends EventEmitter {
733
767
  private sessions;
@@ -1002,6 +1036,11 @@ declare class ShellAdapter implements CLIAdapter {
1002
1036
  detectLogin(_output: string): LoginDetection;
1003
1037
  detectBlockingPrompt(_output: string): BlockingPromptDetection;
1004
1038
  detectReady(output: string): boolean;
1039
+ /**
1040
+ * Detect shell continuation prompts that indicate the shell is NOT ready
1041
+ * for a new command (e.g., unclosed quote, heredoc, backtick).
1042
+ */
1043
+ private isContinuationPrompt;
1005
1044
  detectExit(output: string): {
1006
1045
  exited: boolean;
1007
1046
  code?: number;
@@ -1185,4 +1224,4 @@ declare function isBun(): boolean;
1185
1224
  */
1186
1225
  declare function createPTYManager(options?: BunPTYManagerOptions): BunCompatiblePTYManager;
1187
1226
 
1188
- export { type AdapterFactoryConfig, AdapterRegistry, type AuthRequiredInfo, type AuthRequiredMethod, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, type BuildTimelineOptions, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TaskCompletionTimelineResult, type TaskCompletionTimelineStep, type TaskCompletionTraceRecord, type TaskCompletionTurnTimeline, type TerminalAttachment, type WorkerSessionHandle, buildTaskCompletionTimeline, createAdapter, createPTYManager, extractTaskCompletionTraceRecords, isBun };
1227
+ export { type AdapterFactoryConfig, AdapterRegistry, type AuthRequiredInfo, type AuthRequiredMethod, type AutoResponseRule, BaseCLIAdapter, type BlockingPromptDetection, type BlockingPromptInfo, type BlockingPromptType, type BuildTimelineOptions, BunCompatiblePTYManager, type BunPTYManagerOptions, type CLIAdapter, type LogOptions, type Logger, type LoginDetection, type MessageType, PTYManager, type PTYManagerConfig, type PTYManagerEvents, PTYSession, type PTYSessionEvents, type ParsedOutput, SPECIAL_KEYS, type SessionFilter, type SessionHandle, type SessionMessage, type SessionStatus, ShellAdapter, type ShellAdapterOptions, type SpawnConfig, type StallClassification, type StopOptions, type TaskCompletionTimelineResult, type TaskCompletionTimelineStep, type TaskCompletionTraceRecord, type TaskCompletionTurnTimeline, type TerminalAttachment, type ToolRunningInfo, type WorkerSessionHandle, buildTaskCompletionTimeline, createAdapter, createPTYManager, extractTaskCompletionTraceRecords, isBun };
package/dist/index.js CHANGED
@@ -364,6 +364,8 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
364
364
  // Ready detection settle delay — defers session_ready until output goes quiet
365
365
  _readySettleTimer = null;
366
366
  _readySettlePending = false;
367
+ // Tool running deduplication — only emit when tool changes
368
+ _lastToolRunningName = null;
367
369
  // Deferred output processing — prevents node-pty's synchronous data
368
370
  // delivery from starving the event loop (timers, I/O callbacks, etc.)
369
371
  _processScheduled = false;
@@ -529,6 +531,22 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
529
531
  this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
530
532
  return;
531
533
  }
534
+ const toolInfo = this.adapter.detectToolRunning?.(this.outputBuffer);
535
+ if (toolInfo) {
536
+ if (toolInfo.toolName !== this._lastToolRunningName) {
537
+ this._lastToolRunningName = toolInfo.toolName;
538
+ this.emit("tool_running", toolInfo);
539
+ }
540
+ this.logger.debug(
541
+ { sessionId: this.id, tool: toolInfo.toolName },
542
+ "Tool running \u2014 suppressing stall emission"
543
+ );
544
+ this._stallTimer = setTimeout(() => this.onStallTimerFired(), this._stallBackoffMs);
545
+ return;
546
+ }
547
+ if (this._lastToolRunningName) {
548
+ this._lastToolRunningName = null;
549
+ }
532
550
  const tail = this.outputBuffer.slice(-500);
533
551
  const hash = this.simpleHash(tail);
534
552
  if (hash === this._lastStallHash) {
@@ -967,6 +985,17 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
967
985
  this.scheduleReadySettle();
968
986
  return;
969
987
  }
988
+ if (this._status === "busy") {
989
+ const toolInfo = this.adapter.detectToolRunning?.(this.outputBuffer);
990
+ if (toolInfo) {
991
+ if (toolInfo.toolName !== this._lastToolRunningName) {
992
+ this._lastToolRunningName = toolInfo.toolName;
993
+ this.emit("tool_running", toolInfo);
994
+ }
995
+ } else if (this._lastToolRunningName) {
996
+ this._lastToolRunningName = null;
997
+ }
998
+ }
970
999
  if (this._status === "busy") {
971
1000
  const signal = this.isTaskCompleteSignal(this.outputBuffer);
972
1001
  if (this._taskCompletePending || signal) {
@@ -1242,24 +1271,71 @@ var PTYSession = class _PTYSession extends import_events.EventEmitter {
1242
1271
  throw new Error("Session not started");
1243
1272
  }
1244
1273
  const keyList = Array.isArray(keys) ? keys : [keys];
1274
+ const normalized = _PTYSession.normalizeKeyList(keyList);
1245
1275
  this._stallEmissionCount = 0;
1246
1276
  this.resetStallTimer();
1247
- for (const key of keyList) {
1248
- const normalizedKey = key.toLowerCase().trim();
1249
- const sequence = SPECIAL_KEYS[normalizedKey];
1277
+ for (const key of normalized) {
1278
+ const sequence = SPECIAL_KEYS[key];
1250
1279
  if (sequence) {
1251
1280
  this._lastActivityAt = /* @__PURE__ */ new Date();
1252
1281
  this.ptyProcess.write(sequence);
1253
- this.logger.debug({ sessionId: this.id, key: normalizedKey }, "Sent special key");
1282
+ this.logger.debug({ sessionId: this.id, key }, "Sent special key");
1254
1283
  } else {
1255
1284
  this.logger.warn(
1256
- { sessionId: this.id, key: normalizedKey },
1285
+ { sessionId: this.id, key },
1257
1286
  "Unknown special key, sending as literal"
1258
1287
  );
1259
1288
  this.ptyProcess.write(key);
1260
1289
  }
1261
1290
  }
1262
1291
  }
1292
+ /**
1293
+ * Normalize a list of key names for SPECIAL_KEYS lookup.
1294
+ *
1295
+ * Handles two problems:
1296
+ * 1. Modifier aliases: "control" → "ctrl", "command" → "meta", "option" → "alt"
1297
+ * 2. Comma-separated compound keys from stall classifier: ["control", "c"] → ["ctrl+c"]
1298
+ * A bare modifier followed by a single char/key is joined with "+".
1299
+ */
1300
+ static normalizeKeyList(keys) {
1301
+ const MODIFIER_MAP = {
1302
+ control: "ctrl",
1303
+ command: "meta",
1304
+ cmd: "meta",
1305
+ option: "alt",
1306
+ opt: "alt"
1307
+ };
1308
+ const MODIFIER_NAMES = /* @__PURE__ */ new Set([
1309
+ "ctrl",
1310
+ "alt",
1311
+ "shift",
1312
+ "meta",
1313
+ // Also match the aliases so we can detect them before remapping
1314
+ ...Object.keys(MODIFIER_MAP)
1315
+ ]);
1316
+ const result = [];
1317
+ let i = 0;
1318
+ while (i < keys.length) {
1319
+ let key = keys[i].toLowerCase().trim();
1320
+ if (MODIFIER_MAP[key]) {
1321
+ key = MODIFIER_MAP[key];
1322
+ }
1323
+ if (MODIFIER_NAMES.has(key) && i + 1 < keys.length) {
1324
+ let nextKey = keys[i + 1].toLowerCase().trim();
1325
+ if (MODIFIER_MAP[nextKey]) {
1326
+ nextKey = MODIFIER_MAP[nextKey];
1327
+ }
1328
+ if (!MODIFIER_NAMES.has(nextKey)) {
1329
+ result.push(`${key}+${nextKey}`);
1330
+ i += 2;
1331
+ continue;
1332
+ }
1333
+ }
1334
+ result.push(key);
1335
+ i++;
1336
+ }
1337
+ return result;
1338
+ }
1263
1339
  /**
1264
1340
  * Select a TUI menu option by index (0-based).
1265
1341
  * Sends Down arrow `optionIndex` times, then Enter, with 50ms delays.
@@ -1451,6 +1527,9 @@ var PTYManager = class extends import_events2.EventEmitter {
1451
1527
  session.on("task_complete", () => {
1452
1528
  this.emit("task_complete", session.toHandle());
1453
1529
  });
1530
+ session.on("tool_running", (info) => {
1531
+ this.emit("tool_running", session.toHandle(), info);
1532
+ });
1454
1533
  session.on("stall_detected", (recentOutput, stallDurationMs) => {
1455
1534
  const handle = session.toHandle();
1456
1535
  this.emit("stall_detected", handle, recentOutput, stallDurationMs);
@@ -2261,7 +2340,18 @@ var ShellAdapter = class {
2261
2340
  return { detected: false };
2262
2341
  }
2263
2342
  detectReady(output) {
2264
- return output.includes(this.promptStr) || output.includes("$") || output.length > 10;
2343
+ if (this.isContinuationPrompt(output)) {
2344
+ return false;
2345
+ }
2346
+ return this.getPromptPattern().test(this.stripAnsi(output));
2347
+ }
2348
+ /**
2349
+ * Detect shell continuation prompts that indicate the shell is NOT ready
2350
+ * for a new command (e.g., unclosed quote, heredoc, backtick).
2351
+ */
2352
+ isContinuationPrompt(output) {
2353
+ const stripped = this.stripAnsi(output);
2354
+ return /(?:quote|dquote|heredoc|bquote|cmdsubst|pipe|then|else|do|loop)>\s*$/.test(stripped) || /(?:quote|dquote|heredoc|bquote)>\s*$/m.test(stripped);
2265
2355
  }
2266
2356
  detectExit(output) {
2267
2357
  if (output.includes("exit")) {
@@ -2284,7 +2374,7 @@ var ShellAdapter = class {
2284
2374
  }
2285
2375
  getPromptPattern() {
2286
2376
  const escaped = this.promptStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2287
- return new RegExp(`(?:${escaped}|\\$|#|>)\\s*$`, "m");
2377
+ return new RegExp(`(?:${escaped}|\\$|#)\\s*$`, "m");
2288
2378
  }
2289
2379
  async validateInstallation() {
2290
2380
  return { installed: true };
@@ -2380,9 +2470,6 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
2380
2470
  const id = event.id;
2381
2471
  switch (eventType) {
2382
2472
  case "worker_ready":
2383
- if (this.adapterModules.length > 0) {
2384
- this.sendCommand({ cmd: "registerAdapters", modules: this.adapterModules });
2385
- }
2386
2473
  if (this._stallDetectionEnabled) {
2387
2474
  this.sendCommand({
2388
2475
  cmd: "configureStallDetection",
@@ -2390,9 +2477,23 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
2390
2477
  timeoutMs: this._stallTimeoutMs
2391
2478
  });
2392
2479
  }
2393
- this.ready = true;
2394
- this.readyResolve();
2395
- this.emit("ready");
2480
+ if (this.adapterModules.length > 0) {
2481
+ this.sendCommand({ cmd: "registerAdapters", modules: this.adapterModules });
2482
+ this.createPending("registerAdapters").then(() => {
2483
+ this.ready = true;
2484
+ this.readyResolve();
2485
+ this.emit("ready");
2486
+ }).catch((err) => {
2487
+ this.emit("worker_error", `Failed to register adapters: ${err}`);
2488
+ this.ready = true;
2489
+ this.readyResolve();
2490
+ this.emit("ready");
2491
+ });
2492
+ } else {
2493
+ this.ready = true;
2494
+ this.readyResolve();
2495
+ this.emit("ready");
2496
+ }
2396
2497
  break;
2397
2498
  case "spawned": {
2398
2499
  const session = {
@@ -2507,6 +2608,13 @@ var BunCompatiblePTYManager = class extends import_events3.EventEmitter {
2507
2608
  }
2508
2609
  break;
2509
2610
  }
2611
+ case "tool_running": {
2612
+ const session = this.sessions.get(id);
2613
+ if (session) {
2614
+ this.emit("tool_running", session, event.info);
2615
+ }
2616
+ break;
2617
+ }
2510
2618
  case "stall_detected": {
2511
2619
  const session = this.sessions.get(id);
2512
2620
  if (session) {