pi-interactive-shell 0.6.4 → 0.7.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/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to the `pi-interactive-shell` extension will be documented i
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.0] - 2026-02-03
8
+
9
+ ### Added
10
+ - **Dispatch mode** (`mode: "dispatch"`) - Fire-and-forget sessions where the agent is notified on completion via `triggerTurn` instead of polling. Defaults `autoExitOnQuiet: true`.
11
+ - **Background dispatch** (`mode: "dispatch", background: true`) - Headless sessions with no overlay. Multiple can run concurrently alongside an interactive overlay.
12
+ - **Agent-initiated background** (`sessionId, background: true`) - Dismiss an active overlay while keeping the process running.
13
+ - **Attach** (`attach: "session-id"`) - Reattach to background sessions with any mode (interactive, hands-free, dispatch).
14
+ - **List background sessions** (`listBackground: true`) - Query all background sessions with status and duration.
15
+ - **Ctrl+B shortcut** - Direct keyboard shortcut to background a session (dismiss overlay, keep process running) without navigating the Ctrl+Q menu.
16
+ - **HeadlessDispatchMonitor** - Lightweight monitor for background PTY sessions handling quiet timer, timeout, exit detection, and output capture.
17
+ - **Completion output capture** - `completionOutput` captured before PTY disposal in all `finishWith*` methods for dispatch notifications.
18
+ - `completionNotifyLines` and `completionNotifyMaxChars` config options for notification output size.
19
+ - **Dismiss background sessions** - `/dismiss [id]` user command and `dismissBackground` tool param to kill running / remove exited sessions without opening an overlay.
20
+ - **Background sessions widget** - Persistent widget below the editor showing all background sessions with status indicators (`●` running / `○` exited), session ID, command, reason, and live duration. Auto-appears/disappears. Responsive layout wraps to two lines on narrow terminals.
21
+ - **Additive listeners on PtyTerminalSession** - `addDataListener()` and `addExitListener()` allow multiple subscribers alongside the primary `setEventHandlers()`. Headless monitor and overlay coexist without conflicts.
22
+
23
+ ### Changed
24
+ - `sessionManager.add()` now accepts optional `{ id, noAutoCleanup }` options for headless dispatch sessions.
25
+ - `sessionManager.take()` removes sessions from background registry without disposing PTY (for attach flow).
26
+ - `ActiveSession` interface now includes `background()` method.
27
+ - Overlay `onExit` handler broadened: non-blocking modes (dispatch and hands-free) auto-close immediately on exit instead of showing countdown.
28
+ - `finishWithBackground()` reuses sessionId as backgroundId for non-blocking modes.
29
+ - `getOutputSinceLastCheck()` returns `completionOutput` as fallback when session is finished.
30
+ - `/attach` command coordinates with headless monitors via additive listeners (monitor stays active during overlay).
31
+ - Headless dispatch completion notifications are compact: status line, duration, 5-line tail, and reattach instruction. Full output available via `details.completionOutput` or by reattaching.
32
+ - Completed headless sessions preserve their PTY for 5 minutes (`scheduleCleanup`) instead of disposing immediately, allowing the agent to reattach and review full scrollback.
33
+ - Notification tail strips trailing blank lines from terminal buffer before slicing.
34
+
35
+ ### Fixed
36
+ - Interval timer in `startHandsFreeUpdates()` and `setUpdateInterval()` no longer kills autoExitOnQuiet detection in dispatch mode (guarded on-quiet branch with `onHandsFreeUpdate` null check).
37
+ - Hands-free non-blocking polls returning empty output for completed sessions now return captured `completionOutput`.
38
+
7
39
  ## [0.6.4] - 2026-02-01
8
40
 
9
41
  ### Fixed
package/README.md CHANGED
@@ -32,11 +32,28 @@ The `interactive-shell` skill is automatically symlinked to `~/.pi/agent/skills/
32
32
 
33
33
  **Requires:** Node.js, build tools for `node-pty` (Xcode CLI tools on macOS).
34
34
 
35
- ## Quick Start
35
+ ## Modes
36
+
37
+ Three modes control how the agent engages with a session:
38
+
39
+ | | Interactive | Hands-Free | Dispatch |
40
+ |---|---|---|---|
41
+ | **Agent blocked?** | Yes — tool call waits | No — returns immediately | No — returns immediately |
42
+ | **How agent gets output** | Tool return value | Polls with `sessionId` | Notification via `triggerTurn` |
43
+ | **Overlay visible?** | Yes | Yes | Yes (or headless with `background: true`) |
44
+ | **User can interact?** | Always | Type to take over | Type to take over |
45
+ | **Concurrent sessions?** | No | One overlay + queries | Multiple headless, one overlay |
46
+ | **Best for** | Editors, REPLs, SSH | Dev servers, builds | Delegating to other agents |
36
47
 
37
- ### Interactive Mode
48
+ **Interactive** is the default. The agent's tool call blocks until the session ends — use this when the agent needs the result right away, or when the user drives the session (editors, database shells).
38
49
 
39
- User controls the session directly:
50
+ **Hands-free** returns immediately so the agent can do other work, but the agent must poll periodically to discover output and completion. Good for processes the agent needs to monitor and react to mid-flight, like watching build output and sending follow-up commands.
51
+
52
+ **Dispatch** also returns immediately, but the agent doesn't poll at all. When the session completes — whether by natural exit, quiet detection, timeout, or user intervention — the agent gets woken up with a notification containing the tail output. This is the right mode for delegating a task to a subagent and moving on. Add `background: true` to skip the overlay entirely and run headless.
53
+
54
+ ## Quick Start
55
+
56
+ ### Interactive
40
57
 
41
58
  ```typescript
42
59
  interactive_shell({ command: 'vim package.json' })
@@ -44,9 +61,9 @@ interactive_shell({ command: 'psql -d mydb' })
44
61
  interactive_shell({ command: 'ssh user@server' })
45
62
  ```
46
63
 
47
- ### Hands-Free Mode
64
+ The agent's turn is blocked until the overlay closes. User controls the session directly.
48
65
 
49
- Agent monitors while user watches. Returns immediately with sessionId:
66
+ ### Hands-Free
50
67
 
51
68
  ```typescript
52
69
  // Start a long-running process
@@ -57,20 +74,68 @@ interactive_shell({
57
74
  })
58
75
  // → { sessionId: "calm-reef", status: "running" }
59
76
 
60
- // Query status (rate-limited to 60s)
77
+ // Poll for output (rate-limited to 60s between queries)
61
78
  interactive_shell({ sessionId: "calm-reef" })
62
- // → { status: "running", output: "...", runtime: 45000 }
79
+ // → { status: "running", output: "Server ready on :3000", runtime: 45000 }
63
80
 
64
- // Send input if needed
81
+ // Send input when needed
65
82
  interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] })
66
83
 
67
84
  // Kill when done
68
85
  interactive_shell({ sessionId: "calm-reef", kill: true })
86
+ // → { status: "killed", output: "..." }
87
+ ```
88
+
89
+ The overlay opens for the user to watch. The agent checks in periodically. User can type anything to take over control.
90
+
91
+ ### Dispatch
92
+
93
+ ```typescript
94
+ // Fire off a task
95
+ interactive_shell({
96
+ command: 'pi "Refactor the auth module"',
97
+ mode: "dispatch",
98
+ reason: "Auth refactor"
99
+ })
100
+ // → Returns immediately: { sessionId: "calm-reef" }
101
+ // → Agent ends turn or does other work.
102
+ ```
103
+
104
+ When the session completes, the agent receives a compact notification on a new turn:
105
+
106
+ ```
107
+ Session calm-reef completed successfully (5m 23s). 847 lines of output.
108
+
109
+ Step 9 of 10
110
+ Step 10 of 10
111
+ All tasks completed.
112
+
113
+ Attach to review full output: interactive_shell({ attach: "calm-reef" })
114
+ ```
115
+
116
+ The notification includes a brief tail (last 5 lines) and a reattach instruction. The PTY is preserved for 5 minutes so the agent can attach to review full scrollback.
117
+
118
+ Dispatch defaults `autoExitOnQuiet: true` — the session is killed after output goes silent (5s by default), which signals completion for task-oriented subagents. Opt out with `handsFree: { autoExitOnQuiet: false }` for long-running processes.
119
+
120
+ The overlay still shows for the user, who can Ctrl+T to transfer output, Ctrl+B to background, take over by typing, or Ctrl+Q for more options.
121
+
122
+ ### Background Dispatch (Headless)
123
+
124
+ ```typescript
125
+ // No overlay — runs completely invisibly
126
+ interactive_shell({
127
+ command: 'pi "Fix all lint errors"',
128
+ mode: "dispatch",
129
+ background: true
130
+ })
131
+ // → { sessionId: "calm-reef" }
132
+ // → User can /attach calm-reef to peek
133
+ // → Agent notified on completion, same as regular dispatch
69
134
  ```
70
135
 
71
- User sees the overlay in real-time. Type anything to take over control.
136
+ Multiple headless dispatches can run concurrently alongside a single interactive overlay. This is how you parallelize subagent work — fire off three background dispatches and process results as each completion notification arrives.
72
137
 
73
- ### Timeout Mode
138
+ ### Timeout
74
139
 
75
140
  Capture output from TUI apps that don't exit cleanly:
76
141
 
@@ -154,14 +219,34 @@ The main agent then has the subagent's response in context and can continue work
154
219
 
155
220
  ### Background Sessions
156
221
 
157
- 1. Ctrl+Q → "Run in background"
158
- 2. `/attach` or `/attach <id>` to reattach
222
+ Sessions can be backgrounded by the user (Ctrl+B, or Ctrl+Q → "Run in background") or by the agent:
223
+
224
+ ```typescript
225
+ // Agent backgrounds an active session
226
+ interactive_shell({ sessionId: "calm-reef", background: true })
227
+ // → Overlay closes, process keeps running
228
+
229
+ // List background sessions
230
+ interactive_shell({ listBackground: true })
231
+
232
+ // Reattach with a specific mode
233
+ interactive_shell({ attach: "calm-reef" }) // interactive (blocking)
234
+ interactive_shell({ attach: "calm-reef", mode: "hands-free" }) // hands-free (poll)
235
+ interactive_shell({ attach: "calm-reef", mode: "dispatch" }) // dispatch (notified)
236
+
237
+ // Dismiss background sessions
238
+ interactive_shell({ dismissBackground: true }) // all sessions
239
+ interactive_shell({ dismissBackground: "calm-reef" }) // specific session
240
+ ```
241
+
242
+ User can also `/attach` or `/attach <id>` to reattach, and `/dismiss` or `/dismiss <id>` to clean up from the chat.
159
243
 
160
244
  ## Keys
161
245
 
162
246
  | Key | Action |
163
247
  |-----|--------|
164
248
  | Ctrl+T | **Transfer & close** - capture output and send to main agent |
249
+ | Ctrl+B | Background session (dismiss overlay, keep running) |
165
250
  | Ctrl+Q | Session menu (transfer/background/kill/cancel) |
166
251
  | Shift+Up/Down | Scroll history |
167
252
  | Any key (hands-free) | Take over control |
@@ -181,6 +266,8 @@ Configuration files (project overrides global):
181
266
  "minQueryIntervalSeconds": 60,
182
267
  "transferLines": 200,
183
268
  "transferMaxChars": 20000,
269
+ "completionNotifyLines": 50,
270
+ "completionNotifyMaxChars": 5000,
184
271
  "handsFreeUpdateMode": "on-quiet",
185
272
  "handsFreeUpdateInterval": 60000,
186
273
  "handsFreeQuietThreshold": 5000,
@@ -203,6 +290,8 @@ Configuration files (project overrides global):
203
290
  | `minQueryIntervalSeconds` | 60 | Rate limit between agent queries |
204
291
  | `transferLines` | 200 | Lines to capture on Ctrl+T transfer (10-1000) |
205
292
  | `transferMaxChars` | 20000 | Max chars for transfer (1KB-100KB) |
293
+ | `completionNotifyLines` | 50 | Lines in dispatch completion notification (10-500) |
294
+ | `completionNotifyMaxChars` | 5000 | Max chars in completion notification (1KB-50KB) |
206
295
  | `handsFreeUpdateMode` | "on-quiet" | "on-quiet" or "interval" |
207
296
  | `handsFreeQuietThreshold` | 5000 | Silence duration before update (ms) |
208
297
  | `handsFreeUpdateInterval` | 60000 | Max interval between updates (ms) |
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: interactive-shell
3
- description: Cheat sheet + workflow for launching interactive coding-agent CLIs (Claude Code, Gemini CLI, Codex CLI, Cursor CLI, and pi itself) via the interactive_shell overlay. The overlay is for interactive supervision only - headless commands should use the bash tool instead.
3
+ description: Cheat sheet + workflow for launching interactive coding-agent CLIs (Claude Code, Gemini CLI, Codex CLI, Cursor CLI, and pi itself) via the interactive_shell overlay or headless dispatch. Use for TUI agents and long-running processes that need supervision, fire-and-forget delegation, or headless background execution. Regular bash commands should use the bash tool instead.
4
4
  ---
5
5
 
6
6
  # Interactive Shell (Skill)
@@ -11,16 +11,18 @@ Last verified: 2026-01-18
11
11
 
12
12
  Pi has two ways to delegate work to other AI coding agents:
13
13
 
14
- | | Foreground Subagents | Background Subagents |
15
- |---|---|---|
16
- | **Tool** | `interactive_shell` | `subagent` |
17
- | **Visibility** | User sees overlay in real-time | Hidden from user |
18
- | **Default agent** | `pi` (others if user requests) | Pi only |
19
- | **Output** | Minimal (tail preview) | Full output captured |
20
- | **User control** | Can take over anytime | No intervention |
21
- | **Best for** | Long tasks needing supervision | Parallel tasks, structured delegation |
14
+ | | Foreground Subagents | Dispatch Subagents | Background Subagents |
15
+ |---|---|---|---|
16
+ | **Tool** | `interactive_shell` | `interactive_shell` (dispatch) | `subagent` |
17
+ | **Visibility** | User sees overlay | User sees overlay (or headless) | Hidden from user |
18
+ | **Agent model** | Polls for status | Notified on completion | Full output captured |
19
+ | **Default agent** | `pi` (others if user requests) | `pi` (others if user requests) | Pi only |
20
+ | **User control** | Can take over anytime | Can take over anytime | No intervention |
21
+ | **Best for** | Long tasks needing supervision | Fire-and-forget delegations | Parallel tasks, structured delegation |
22
22
 
23
- **Foreground subagents** run in an overlay where the user watches (and can intervene). Use `interactive_shell` with `mode: "hands-free"` to monitor while receiving periodic updates.
23
+ **Foreground subagents** run in an overlay where the user watches (and can intervene). Use `interactive_shell` with `mode: "hands-free"` to monitor while receiving periodic updates, or `mode: "dispatch"` to be notified on completion without polling.
24
+
25
+ **Dispatch subagents** also use `interactive_shell` but with `mode: "dispatch"`. The agent fires the session and moves on. When the session completes, the agent is woken up via `triggerTurn` with the output in context. Add `background: true` for headless execution (no overlay).
24
26
 
25
27
  **Background subagents** run invisibly via the `subagent` tool. Pi-only, but captures full output and supports parallel execution.
26
28
 
@@ -66,6 +68,34 @@ Agent starts working immediately, user supervises.
66
68
  interactive_shell({ command: 'pi "Review this codebase for security issues"' })
67
69
  ```
68
70
 
71
+ ### Dispatch (Fire-and-Forget) - NON-BLOCKING, NO POLLING
72
+ Agent fires a session and moves on. Notified automatically on completion via `triggerTurn`.
73
+
74
+ ```typescript
75
+ // Start session - returns immediately, no polling needed
76
+ interactive_shell({
77
+ command: 'pi "Fix all TypeScript errors in src/"',
78
+ mode: "dispatch",
79
+ reason: "Fixing TS errors"
80
+ })
81
+ // Returns: { sessionId: "calm-reef", mode: "dispatch" }
82
+ // → Do other work. When session completes, you receive notification with output.
83
+ ```
84
+
85
+ Dispatch defaults `autoExitOnQuiet: true`. The agent can still query the sessionId if needed, but doesn't have to.
86
+
87
+ #### Background Dispatch (Headless)
88
+ No overlay opens. Multiple headless dispatches can run concurrently:
89
+
90
+ ```typescript
91
+ interactive_shell({
92
+ command: 'pi "Fix lint errors"',
93
+ mode: "dispatch",
94
+ background: true
95
+ })
96
+ // → No overlay. User can /attach to watch. Agent notified on completion.
97
+ ```
98
+
69
99
  ### Hands-Free (Foreground Subagent) - NON-BLOCKING
70
100
  Agent works autonomously, **returns immediately** with sessionId. You query for status/output and kill when done.
71
101
 
@@ -315,13 +345,13 @@ interactive_shell({ sessionId: "calm-reef", settings: { updateInterval: 30000, q
315
345
 
316
346
  ## CLI Quick Reference
317
347
 
318
- | Agent | Interactive | With Prompt | Headless (bash) |
319
- |-------|-------------|-------------|-----------------|
320
- | `claude` | `claude` | `claude "prompt"` | `claude -p "prompt"` |
321
- | `gemini` | `gemini` | `gemini -i "prompt"` | `gemini "prompt"` |
322
- | `codex` | `codex` | `codex "prompt"` | `codex exec "prompt"` |
323
- | `agent` | `agent` | `agent "prompt"` | `agent -p "prompt"` |
324
- | `pi` | `pi` | `pi "prompt"` | `pi -p "prompt"` |
348
+ | Agent | Interactive | With Prompt | Headless (bash) | Dispatch |
349
+ |-------|-------------|-------------|-----------------|----------|
350
+ | `claude` | `claude` | `claude "prompt"` | `claude -p "prompt"` | `mode: "dispatch"` |
351
+ | `gemini` | `gemini` | `gemini -i "prompt"` | `gemini "prompt"` | `mode: "dispatch"` |
352
+ | `codex` | `codex` | `codex "prompt"` | `codex exec "prompt"` | `mode: "dispatch"` |
353
+ | `agent` | `agent` | `agent "prompt"` | `agent -p "prompt"` | `mode: "dispatch"` |
354
+ | `pi` | `pi` | `pi "prompt"` | `pi -p "prompt"` | `mode: "dispatch"` |
325
355
 
326
356
  **Gemini model:** `gemini -m gemini-3-flash-preview -i "prompt"`
327
357
 
@@ -401,8 +431,47 @@ The process is killed after timeout and captured output is returned in the hando
401
431
 
402
432
  For pi CLI documentation, you can also read directly: `/opt/homebrew/lib/node_modules/@mariozechner/pi-coding-agent/README.md`
403
433
 
434
+ ## Background Session Management
435
+
436
+ ```typescript
437
+ // Background an active session (close overlay, keep running)
438
+ interactive_shell({ sessionId: "calm-reef", background: true })
439
+
440
+ // List all background sessions
441
+ interactive_shell({ listBackground: true })
442
+
443
+ // Reattach to a background session
444
+ interactive_shell({ attach: "calm-reef" }) // interactive (blocking)
445
+ interactive_shell({ attach: "calm-reef", mode: "hands-free" }) // hands-free (poll)
446
+ interactive_shell({ attach: "calm-reef", mode: "dispatch" }) // dispatch (notified)
447
+
448
+ // Dismiss background sessions (kill running, remove exited)
449
+ interactive_shell({ dismissBackground: true }) // all
450
+ interactive_shell({ dismissBackground: "calm-reef" }) // specific
451
+ ```
452
+
404
453
  ## Quick Reference
405
454
 
455
+ **Dispatch subagent (fire-and-forget, default to pi):**
456
+ ```typescript
457
+ interactive_shell({
458
+ command: 'pi "Implement the feature described in SPEC.md"',
459
+ mode: "dispatch",
460
+ reason: "Implementing feature"
461
+ })
462
+ // Returns immediately. You'll be notified when done.
463
+ ```
464
+
465
+ **Background dispatch (headless, no overlay):**
466
+ ```typescript
467
+ interactive_shell({
468
+ command: 'pi "Fix lint errors"',
469
+ mode: "dispatch",
470
+ background: true,
471
+ reason: "Fixing lint"
472
+ })
473
+ ```
474
+
406
475
  **Start foreground subagent (hands-free, default to pi):**
407
476
  ```typescript
408
477
  interactive_shell({
package/config.ts CHANGED
@@ -17,6 +17,9 @@ export interface InteractiveShellConfig {
17
17
  // Transfer output settings (Ctrl+T)
18
18
  transferLines: number;
19
19
  transferMaxChars: number;
20
+ // Dispatch completion notification output
21
+ completionNotifyLines: number;
22
+ completionNotifyMaxChars: number;
20
23
  // Hands-free mode defaults
21
24
  handsFreeUpdateMode: "on-quiet" | "interval";
22
25
  handsFreeUpdateInterval: number;
@@ -42,6 +45,9 @@ const DEFAULT_CONFIG: InteractiveShellConfig = {
42
45
  // Transfer output settings (Ctrl+T) - generous defaults for full context transfer
43
46
  transferLines: 200,
44
47
  transferMaxChars: 20000,
48
+ // Dispatch completion notification output (between handoff preview and transfer)
49
+ completionNotifyLines: 50,
50
+ completionNotifyMaxChars: 5000,
45
51
  // Hands-free mode defaults
46
52
  handsFreeUpdateMode: "on-quiet" as const,
47
53
  handsFreeUpdateInterval: 60000,
@@ -79,6 +85,7 @@ export function loadConfig(cwd: string): InteractiveShellConfig {
79
85
 
80
86
  return {
81
87
  ...merged,
88
+ exitAutoCloseDelay: clampInt(merged.exitAutoCloseDelay, DEFAULT_CONFIG.exitAutoCloseDelay, 0, 60),
82
89
  overlayWidthPercent: clampPercent(merged.overlayWidthPercent, DEFAULT_CONFIG.overlayWidthPercent),
83
90
  // Height: 20-90% range (default 45%)
84
91
  overlayHeightPercent: clampInt(merged.overlayHeightPercent, DEFAULT_CONFIG.overlayHeightPercent, 20, 90),
@@ -103,6 +110,9 @@ export function loadConfig(cwd: string): InteractiveShellConfig {
103
110
  // Transfer output settings (Ctrl+T)
104
111
  transferLines: clampInt(merged.transferLines, DEFAULT_CONFIG.transferLines, 10, 1000),
105
112
  transferMaxChars: clampInt(merged.transferMaxChars, DEFAULT_CONFIG.transferMaxChars, 1000, 100000),
113
+ // Dispatch completion notification output
114
+ completionNotifyLines: clampInt(merged.completionNotifyLines, DEFAULT_CONFIG.completionNotifyLines, 10, 500),
115
+ completionNotifyMaxChars: clampInt(merged.completionNotifyMaxChars, DEFAULT_CONFIG.completionNotifyMaxChars, 1000, 50000),
106
116
  // Hands-free mode
107
117
  handsFreeUpdateMode: merged.handsFreeUpdateMode === "interval" ? "interval" : "on-quiet",
108
118
  handsFreeUpdateInterval: clampInt(
@@ -0,0 +1,171 @@
1
+ import type { PtyTerminalSession } from "./pty-session.js";
2
+ import type { InteractiveShellConfig } from "./config.js";
3
+
4
+ export interface HeadlessMonitorOptions {
5
+ autoExitOnQuiet: boolean;
6
+ quietThreshold: number;
7
+ timeout?: number;
8
+ }
9
+
10
+ export interface HeadlessCompletionInfo {
11
+ exitCode: number | null;
12
+ signal?: number;
13
+ timedOut?: boolean;
14
+ cancelled?: boolean;
15
+ completionOutput?: {
16
+ lines: string[];
17
+ totalLines: number;
18
+ truncated: boolean;
19
+ };
20
+ }
21
+
22
+ export class HeadlessDispatchMonitor {
23
+ readonly startTime = Date.now();
24
+ private _disposed = false;
25
+ private quietTimer: ReturnType<typeof setTimeout> | null = null;
26
+ private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
27
+ private result: HeadlessCompletionInfo | undefined;
28
+ private completeCallbacks: Array<() => void> = [];
29
+ private unsubData: (() => void) | null = null;
30
+ private unsubExit: (() => void) | null = null;
31
+
32
+ get disposed(): boolean { return this._disposed; }
33
+
34
+ constructor(
35
+ private session: PtyTerminalSession,
36
+ private config: InteractiveShellConfig,
37
+ private options: HeadlessMonitorOptions,
38
+ private onComplete: (info: HeadlessCompletionInfo) => void,
39
+ ) {
40
+ this.subscribe();
41
+
42
+ if (options.timeout && options.timeout > 0) {
43
+ this.timeoutTimer = setTimeout(() => {
44
+ this.handleCompletion(null, undefined, true);
45
+ }, options.timeout);
46
+ }
47
+
48
+ if (session.exited) {
49
+ queueMicrotask(() => {
50
+ if (!this._disposed) {
51
+ this.handleCompletion(session.exitCode, session.signal);
52
+ }
53
+ });
54
+ }
55
+ }
56
+
57
+ private subscribe(): void {
58
+ this.unsubscribe();
59
+ this.unsubData = this.session.addDataListener(() => {
60
+ if (this.options.autoExitOnQuiet) {
61
+ this.resetQuietTimer();
62
+ }
63
+ });
64
+ this.unsubExit = this.session.addExitListener((exitCode, signal) => {
65
+ if (!this._disposed) {
66
+ this.handleCompletion(exitCode, signal);
67
+ }
68
+ });
69
+ }
70
+
71
+ private unsubscribe(): void {
72
+ this.unsubData?.();
73
+ this.unsubData = null;
74
+ this.unsubExit?.();
75
+ this.unsubExit = null;
76
+ }
77
+
78
+ private resetQuietTimer(): void {
79
+ this.stopQuietTimer();
80
+ this.quietTimer = setTimeout(() => {
81
+ this.quietTimer = null;
82
+ if (!this._disposed && this.options.autoExitOnQuiet) {
83
+ this.session.kill();
84
+ this.handleCompletion(null, undefined, false, true);
85
+ }
86
+ }, this.options.quietThreshold);
87
+ }
88
+
89
+ private stopQuietTimer(): void {
90
+ if (this.quietTimer) {
91
+ clearTimeout(this.quietTimer);
92
+ this.quietTimer = null;
93
+ }
94
+ }
95
+
96
+ private captureOutput(): HeadlessCompletionInfo["completionOutput"] {
97
+ try {
98
+ const result = this.session.getTailLines({
99
+ lines: this.config.completionNotifyLines,
100
+ ansi: false,
101
+ maxChars: this.config.completionNotifyMaxChars,
102
+ });
103
+ return {
104
+ lines: result.lines,
105
+ totalLines: result.totalLinesInBuffer,
106
+ truncated: result.lines.length < result.totalLinesInBuffer || result.truncatedByChars,
107
+ };
108
+ } catch {
109
+ return { lines: [], totalLines: 0, truncated: false };
110
+ }
111
+ }
112
+
113
+ private handleCompletion(exitCode: number | null, signal?: number, timedOut?: boolean, cancelled?: boolean): void {
114
+ if (this._disposed) return;
115
+ this._disposed = true;
116
+ this.stopQuietTimer();
117
+ if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); this.timeoutTimer = null; }
118
+ this.unsubscribe();
119
+
120
+ if (timedOut) {
121
+ this.session.kill();
122
+ }
123
+
124
+ const completionOutput = this.captureOutput();
125
+ const info: HeadlessCompletionInfo = { exitCode, signal, timedOut, cancelled, completionOutput };
126
+ this.result = info;
127
+ this.triggerCompleteCallbacks();
128
+ this.onComplete(info);
129
+ }
130
+
131
+ handleExternalCompletion(exitCode: number | null, signal?: number, completionOutput?: HeadlessCompletionInfo["completionOutput"]): void {
132
+ if (this._disposed) return;
133
+ this._disposed = true;
134
+ this.stopQuietTimer();
135
+ if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); this.timeoutTimer = null; }
136
+ this.unsubscribe();
137
+
138
+ const output = completionOutput ?? this.captureOutput();
139
+ const info: HeadlessCompletionInfo = { exitCode, signal, completionOutput: output };
140
+ this.result = info;
141
+ this.triggerCompleteCallbacks();
142
+ this.onComplete(info);
143
+ }
144
+
145
+ getResult(): HeadlessCompletionInfo | undefined {
146
+ return this.result;
147
+ }
148
+
149
+ registerCompleteCallback(callback: () => void): void {
150
+ if (this.result) {
151
+ callback();
152
+ return;
153
+ }
154
+ this.completeCallbacks.push(callback);
155
+ }
156
+
157
+ private triggerCompleteCallbacks(): void {
158
+ for (const cb of this.completeCallbacks) {
159
+ try { cb(); } catch { /* ignore */ }
160
+ }
161
+ this.completeCallbacks = [];
162
+ }
163
+
164
+ dispose(): void {
165
+ if (this._disposed) return;
166
+ this._disposed = true;
167
+ this.stopQuietTimer();
168
+ if (this.timeoutTimer) { clearTimeout(this.timeoutTimer); this.timeoutTimer = null; }
169
+ this.unsubscribe();
170
+ }
171
+ }