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 +32 -0
- package/README.md +101 -12
- package/SKILL.md +86 -17
- package/config.ts +10 -0
- package/headless-monitor.ts +171 -0
- package/index.ts +744 -366
- package/overlay-component.ts +134 -72
- package/package.json +2 -1
- package/pty-session.ts +36 -4
- package/reattach-overlay.ts +31 -4
- package/session-manager.ts +94 -45
- package/tool-schema.ts +61 -2
- package/types.ts +11 -3
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
The agent's turn is blocked until the overlay closes. User controls the session directly.
|
|
48
65
|
|
|
49
|
-
|
|
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
|
-
//
|
|
77
|
+
// Poll for output (rate-limited to 60s between queries)
|
|
61
78
|
interactive_shell({ sessionId: "calm-reef" })
|
|
62
|
-
// → { status: "running", output: "
|
|
79
|
+
// → { status: "running", output: "Server ready on :3000", runtime: 45000 }
|
|
63
80
|
|
|
64
|
-
// Send input
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
18
|
-
| **
|
|
19
|
-
| **
|
|
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
|
+
}
|