pi-interactive-shell 0.4.2 → 0.4.4
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 +17 -0
- package/README.md +109 -114
- package/SKILL.md +50 -48
- package/index.ts +22 -6
- package/overlay-component.ts +80 -15
- package/package.json +1 -1
- package/pty-session.ts +12 -0
- package/session-manager.ts +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `pi-interactive-shell` extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.3] - 2026-01-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Configurable output limits** - New `outputLines` and `outputMaxChars` parameters when querying sessions:
|
|
9
|
+
- `outputLines`: Request more lines (default: 20, max: 200)
|
|
10
|
+
- `outputMaxChars`: Request more content (default: 5KB, max: 50KB)
|
|
11
|
+
- Example: `interactive_shell({ sessionId: "calm-reef", outputLines: 50 })`
|
|
12
|
+
- **Escape hint feedback** - After pressing first Escape, shows "Press Escape again to detach..." in footer for 300ms
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Escape hint not showing** - Fixed bug where `clearEscapeHint()` was immediately resetting `showEscapeHint` to false after setting it to true
|
|
16
|
+
- **Negative output limits** - Added clamping to ensure `outputLines` and `outputMaxChars` are at least 1
|
|
17
|
+
- **Reduced flickering during rapid output** - Three improvements:
|
|
18
|
+
1. Scroll position calculated at render time via `followBottom` flag (not on each data event)
|
|
19
|
+
2. Debounced render requests (16ms) to batch rapid updates before drawing
|
|
20
|
+
3. Explicit scroll-to-bottom after resize to prevent flash to top during dimension changes
|
|
21
|
+
|
|
5
22
|
## [0.4.2] - 2026-01-17
|
|
6
23
|
|
|
7
24
|
### Added
|
package/README.md
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
# Pi Interactive Shell
|
|
1
|
+
# Pi Interactive Shell
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pi can run any interactive CLI in a TUI overlay - including other AI agents. Drive Claude, Gemini, Codex, Cursor directly from Pi. Watch it work, take over anytime. Real PTY, full terminal emulation, no tmux needed.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```typescript
|
|
6
|
+
interactive_shell({ command: 'agent "fix all the bugs"', mode: "hands-free" })
|
|
7
|
+
// Returns immediately with sessionId
|
|
8
|
+
// User watches in overlay, you query for status
|
|
9
|
+
// Session auto-closes when agent finishes
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
AI agents delegating to other AI agents is powerful but messy:
|
|
15
|
+
|
|
16
|
+
- **Visibility** - What's the subagent doing? Is it stuck?
|
|
17
|
+
- **Control** - User needs to intervene. How?
|
|
18
|
+
- **Integration** - When does the parent agent check in?
|
|
19
|
+
|
|
20
|
+
Interactive Shell solves all three:
|
|
6
21
|
|
|
7
|
-
|
|
22
|
+
**Real-Time Overlay** - User sees the subprocess in a TUI overlay. Full terminal emulation via xterm-headless. ANSI colors, cursor movement, everything.
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
| **User control** | Can intervene | None |
|
|
24
|
+
**Seamless Takeover** - Type anything to take control. Scroll with Shift+Up/Down. Double-Escape to detach.
|
|
25
|
+
|
|
26
|
+
**Non-Blocking API** - Start a session, get a sessionId, query for status. Rate-limited to prevent spam. Auto-exits when output stops.
|
|
27
|
+
|
|
28
|
+
**Any CLI** - Not just AI agents. Run `htop`, `vim`, `psql`, `ssh` - anything interactive.
|
|
15
29
|
|
|
16
30
|
## Install
|
|
17
31
|
|
|
@@ -19,147 +33,111 @@ This is distinct from **background subagents** (the `subagent` tool) which run p
|
|
|
19
33
|
npx pi-interactive-shell
|
|
20
34
|
```
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
Installs to `~/.pi/agent/extensions/interactive-shell/`.
|
|
23
37
|
|
|
24
|
-
**
|
|
38
|
+
**Requires:** Node.js, build tools for `node-pty` (Xcode CLI tools on macOS).
|
|
25
39
|
|
|
26
|
-
|
|
40
|
+
## Quick Start
|
|
27
41
|
|
|
28
|
-
|
|
42
|
+
### Hands-Free (Agent-to-Agent)
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
```typescript
|
|
45
|
+
// Start - returns immediately
|
|
46
|
+
interactive_shell({
|
|
47
|
+
command: 'pi "Refactor the auth module"',
|
|
48
|
+
mode: "hands-free"
|
|
49
|
+
})
|
|
50
|
+
// → { sessionId: "calm-reef", status: "running" }
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `name` (string): session name (used in sessionId)
|
|
36
|
-
- `reason` (string): shown in overlay header (UI-only, not passed to subprocess)
|
|
37
|
-
- `mode` (string): `"interactive"` (default) or `"hands-free"`
|
|
38
|
-
- `timeout` (number): auto-kill after N milliseconds (for TUI commands that don't exit)
|
|
39
|
-
- `handsFree` (object): options for hands-free mode
|
|
40
|
-
- `updateMode` (string): `"on-quiet"` (default) or `"interval"`
|
|
41
|
-
- `updateInterval` (number): max ms between updates, fallback for on-quiet (default: 60000)
|
|
42
|
-
- `quietThreshold` (number): ms of silence before emitting update in on-quiet mode (default: 5000)
|
|
43
|
-
- `updateMaxChars` (number): max chars per update (default: 1500)
|
|
44
|
-
- `maxTotalChars` (number): total char budget for all updates (default: 100000)
|
|
45
|
-
- `handoffPreview` (object): tail preview in tool result
|
|
46
|
-
- `handoffSnapshot` (object): write transcript to file
|
|
52
|
+
// Query status (rate-limited to 60s)
|
|
53
|
+
interactive_shell({ sessionId: "calm-reef" })
|
|
54
|
+
// → { status: "running", output: "...", runtime: 45000 }
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- `input` (string | object): input to send
|
|
51
|
-
- As string: raw text/keystrokes (e.g., `"/model\n"`)
|
|
52
|
-
- As object: `{ text?, keys?, hex?, paste? }`
|
|
56
|
+
// Get more output
|
|
57
|
+
interactive_shell({ sessionId: "calm-reef", outputLines: 100 })
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
// Kill when done (or let autoExitOnQuiet handle it)
|
|
60
|
+
interactive_shell({ sessionId: "calm-reef", kill: true })
|
|
61
|
+
```
|
|
57
62
|
|
|
58
|
-
###
|
|
63
|
+
### Interactive (User Control)
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
```typescript
|
|
66
|
+
interactive_shell({ command: 'vim package.json' })
|
|
61
67
|
```
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
|
|
69
|
+
### Send Input
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
interactive_shell({ sessionId: "calm-reef", input: "/help\n" })
|
|
73
|
+
interactive_shell({ sessionId: "calm-reef", input: { keys: ["ctrl+c"] } })
|
|
74
|
+
interactive_shell({ sessionId: "calm-reef", input: { keys: ["down", "down", "enter"] } })
|
|
64
75
|
```
|
|
65
76
|
|
|
66
|
-
##
|
|
77
|
+
## CLI Reference
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
| Agent | Interactive | With Prompt | Headless (use bash) |
|
|
80
|
+
|-------|-------------|-------------|---------------------|
|
|
81
|
+
| `claude` | `claude` | `claude "prompt"` | `claude -p "prompt"` |
|
|
82
|
+
| `gemini` | `gemini` | `gemini -i "prompt"` | `gemini "prompt"` |
|
|
83
|
+
| `codex` | `codex` | `codex "prompt"` | `codex exec "prompt"` |
|
|
84
|
+
| `agent` | `agent` | `agent "prompt"` | `agent -p "prompt"` |
|
|
85
|
+
| `pi` | `pi` | `pi "prompt"` | `pi -p "prompt"` |
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
## Features
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
interactive_shell({ command: 'pi "Review this code"' })
|
|
74
|
-
```
|
|
89
|
+
### Auto-Exit on Quiet
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
Sessions auto-close after 5s of silence. Disable with `handsFree: { autoExitOnQuiet: false }`.
|
|
77
92
|
|
|
78
|
-
|
|
93
|
+
### Timeout for TUI Capture
|
|
79
94
|
|
|
80
95
|
```typescript
|
|
81
96
|
interactive_shell({
|
|
82
|
-
command:
|
|
97
|
+
command: "pi --help",
|
|
83
98
|
mode: "hands-free",
|
|
84
|
-
|
|
99
|
+
timeout: 5000 // Kill after 5s, return captured output
|
|
85
100
|
})
|
|
86
101
|
```
|
|
87
102
|
|
|
88
|
-
|
|
89
|
-
- `on-quiet` (default): Emit update after 5s of output silence. Perfect for agent-to-agent delegation.
|
|
90
|
-
- `interval`: Emit on fixed schedule (every 60s). Use when continuous output is expected.
|
|
91
|
-
|
|
92
|
-
**Context budget:**
|
|
93
|
-
- Updates include only NEW output since last update (incremental)
|
|
94
|
-
- Default: 1500 chars per update, 100KB total budget
|
|
95
|
-
- When budget exhausted, updates continue but without content
|
|
96
|
-
|
|
97
|
-
**Status updates (all include `sessionId`):**
|
|
98
|
-
- Initial update - sent immediately when session starts
|
|
99
|
-
- `status: "running"` - incremental output
|
|
100
|
-
- `status: "user-takeover"` - user typed something
|
|
101
|
-
- `status: "exited"` - process finished
|
|
102
|
-
|
|
103
|
-
### Sending Input to Active Sessions
|
|
104
|
-
|
|
105
|
-
Use `sessionId` from updates to send input:
|
|
103
|
+
### Configurable Output
|
|
106
104
|
|
|
107
105
|
```typescript
|
|
108
|
-
//
|
|
109
|
-
interactive_shell({ sessionId: "calm-reef"
|
|
110
|
-
|
|
111
|
-
// Named keys
|
|
112
|
-
interactive_shell({ sessionId: "calm-reef", input: { text: "/model", keys: ["enter"] } })
|
|
106
|
+
// Default: 20 lines, 5KB
|
|
107
|
+
interactive_shell({ sessionId: "calm-reef" })
|
|
113
108
|
|
|
114
|
-
//
|
|
115
|
-
interactive_shell({ sessionId: "calm-reef",
|
|
116
|
-
|
|
117
|
-
// Hex bytes for raw escape sequences
|
|
118
|
-
interactive_shell({ sessionId: "calm-reef", input: { hex: ["0x1b", "0x5b", "0x41"] } })
|
|
109
|
+
// More lines (max: 200)
|
|
110
|
+
interactive_shell({ sessionId: "calm-reef", outputLines: 100 })
|
|
119
111
|
|
|
120
|
-
//
|
|
121
|
-
interactive_shell({ sessionId: "calm-reef",
|
|
112
|
+
// More content (max: 50KB)
|
|
113
|
+
interactive_shell({ sessionId: "calm-reef", outputMaxChars: 30000 })
|
|
122
114
|
```
|
|
123
115
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
**Keypad keys:** `kp0`-`kp9`, `kp/`, `kp*`, `kp-`, `kp+`, `kp.`, `kpenter`
|
|
116
|
+
### Input Methods
|
|
127
117
|
|
|
128
|
-
|
|
118
|
+
| Method | Example |
|
|
119
|
+
|--------|---------|
|
|
120
|
+
| Text | `input: "/model\n"` |
|
|
121
|
+
| Keys | `input: { keys: ["enter", "ctrl+c"] }` |
|
|
122
|
+
| Hex | `input: { hex: ["0x1b", "0x5b", "0x41"] }` |
|
|
123
|
+
| Paste | `input: { paste: "multi\nline" }` |
|
|
129
124
|
|
|
130
|
-
|
|
125
|
+
### Background Sessions
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
interactive_shell({ sessionId: "calm-reef", settings: { updateInterval: 30000 } })
|
|
136
|
-
interactive_shell({ sessionId: "calm-reef", settings: { quietThreshold: 3000 } })
|
|
137
|
-
```
|
|
127
|
+
1. Double-Escape → "Run in background"
|
|
128
|
+
2. `/attach` or `/attach <id>` to reattach
|
|
138
129
|
|
|
139
130
|
## Config
|
|
140
131
|
|
|
141
|
-
|
|
142
|
-
Project: `<cwd>/.pi/interactive-shell.json`
|
|
132
|
+
`~/.pi/agent/interactive-shell.json`
|
|
143
133
|
|
|
144
134
|
```json
|
|
145
135
|
{
|
|
146
|
-
"doubleEscapeThreshold": 300,
|
|
147
|
-
"exitAutoCloseDelay": 10,
|
|
148
|
-
"overlayWidthPercent": 95,
|
|
149
136
|
"overlayHeightPercent": 45,
|
|
137
|
+
"overlayWidthPercent": 95,
|
|
150
138
|
"scrollbackLines": 5000,
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"handoffPreviewLines": 30,
|
|
154
|
-
"handoffPreviewMaxChars": 2000,
|
|
155
|
-
"handoffSnapshotEnabled": false,
|
|
156
|
-
"handoffSnapshotLines": 200,
|
|
157
|
-
"handoffSnapshotMaxChars": 12000,
|
|
158
|
-
"handsFreeUpdateMode": "on-quiet",
|
|
159
|
-
"handsFreeUpdateInterval": 60000,
|
|
160
|
-
"handsFreeQuietThreshold": 5000,
|
|
161
|
-
"handsFreeUpdateMaxChars": 1500,
|
|
162
|
-
"handsFreeMaxTotalChars": 100000
|
|
139
|
+
"minQueryIntervalSeconds": 60,
|
|
140
|
+
"handsFreeQuietThreshold": 5000
|
|
163
141
|
}
|
|
164
142
|
```
|
|
165
143
|
|
|
@@ -167,7 +145,24 @@ Project: `<cwd>/.pi/interactive-shell.json`
|
|
|
167
145
|
|
|
168
146
|
| Key | Action |
|
|
169
147
|
|-----|--------|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
148
|
+
| Double-Escape | Detach dialog |
|
|
149
|
+
| Shift+Up/Down | Scroll history |
|
|
150
|
+
| Any key (hands-free) | Take over control |
|
|
151
|
+
|
|
152
|
+
## How It Works
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
interactive_shell → node-pty → subprocess
|
|
156
|
+
↓
|
|
157
|
+
xterm-headless (terminal emulation)
|
|
158
|
+
↓
|
|
159
|
+
TUI overlay (pi rendering)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Full PTY. The subprocess thinks it's in a real terminal.
|
|
163
|
+
|
|
164
|
+
## Limitations
|
|
165
|
+
|
|
166
|
+
- macOS tested, Linux experimental
|
|
167
|
+
- 60s rate limit between queries (configurable)
|
|
168
|
+
- Some TUI apps may have rendering quirks
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Cheat sheet + workflow for launching interactive coding-agent CLIs
|
|
|
5
5
|
|
|
6
6
|
# Interactive Shell (Skill)
|
|
7
7
|
|
|
8
|
-
Last verified: 2026-01-
|
|
8
|
+
Last verified: 2026-01-18
|
|
9
9
|
|
|
10
10
|
## Foreground vs Background Subagents
|
|
11
11
|
|
|
@@ -122,7 +122,21 @@ interactive_shell({ sessionId: "calm-reef", kill: true })
|
|
|
122
122
|
|
|
123
123
|
Kill when you see the task is complete in the output. Returns final status and output.
|
|
124
124
|
|
|
125
|
-
**Auto-exit:** In hands-free mode, sessions auto-kill when output stops (after ~5 seconds of quiet). This means when an agent finishes its task, the session closes automatically.
|
|
125
|
+
**Auto-exit (default: enabled):** In hands-free mode, sessions auto-kill when output stops (after ~5 seconds of quiet). This means when an agent finishes its task and returns to its prompt, the session closes automatically.
|
|
126
|
+
|
|
127
|
+
Since sessions auto-close, **always instruct the subagent to save results to a file** so you can read them:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interactive_shell({
|
|
131
|
+
command: 'pi "Review this codebase for security issues. Save your findings to /tmp/security-review.md"',
|
|
132
|
+
mode: "hands-free",
|
|
133
|
+
reason: "Security review"
|
|
134
|
+
})
|
|
135
|
+
// After session ends, read the results:
|
|
136
|
+
// read("/tmp/security-review.md")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
To disable auto-exit (for long-running tasks or when you need to review output): `handsFree: { autoExitOnQuiet: false }`
|
|
126
140
|
|
|
127
141
|
### Sending Input
|
|
128
142
|
```typescript
|
|
@@ -133,18 +147,38 @@ interactive_shell({ sessionId: "calm-reef", input: { keys: ["ctrl+c"] } })
|
|
|
133
147
|
### Query Output
|
|
134
148
|
|
|
135
149
|
Status queries return **rendered terminal output** (what's actually on screen), not raw stream:
|
|
136
|
-
-
|
|
150
|
+
- Default: 20 lines, 5KB max per query
|
|
137
151
|
- No TUI animation noise (spinners, progress bars, etc.)
|
|
138
|
-
-
|
|
139
|
-
|
|
152
|
+
- Configurable via `outputLines` (max: 200) and `outputMaxChars` (max: 50KB)
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// Get more output when reviewing a session
|
|
156
|
+
interactive_shell({ sessionId: "calm-reef", outputLines: 50 })
|
|
157
|
+
|
|
158
|
+
// Get even more for detailed review
|
|
159
|
+
interactive_shell({ sessionId: "calm-reef", outputLines: 100, outputMaxChars: 30000 })
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Reviewing Long Sessions (autoExitOnQuiet disabled)
|
|
163
|
+
|
|
164
|
+
When you disable auto-exit for long-running tasks, progressively review more output as needed:
|
|
140
165
|
|
|
141
166
|
```typescript
|
|
142
|
-
//
|
|
167
|
+
// Start a long session without auto-exit
|
|
143
168
|
interactive_shell({
|
|
144
169
|
command: 'pi "Refactor entire codebase"',
|
|
145
170
|
mode: "hands-free",
|
|
146
|
-
handsFree: {
|
|
171
|
+
handsFree: { autoExitOnQuiet: false }
|
|
147
172
|
})
|
|
173
|
+
|
|
174
|
+
// Query returns last 20 lines by default
|
|
175
|
+
interactive_shell({ sessionId: "calm-reef" })
|
|
176
|
+
|
|
177
|
+
// Get more lines when you need more context
|
|
178
|
+
interactive_shell({ sessionId: "calm-reef", outputLines: 50 })
|
|
179
|
+
|
|
180
|
+
// Get even more for detailed review
|
|
181
|
+
interactive_shell({ sessionId: "calm-reef", outputLines: 100, outputMaxChars: 30000 })
|
|
148
182
|
```
|
|
149
183
|
|
|
150
184
|
## Sending Input to Active Sessions
|
|
@@ -244,49 +278,17 @@ interactive_shell({ sessionId: "calm-reef", settings: { quietThreshold: 3000 } }
|
|
|
244
278
|
interactive_shell({ sessionId: "calm-reef", settings: { updateInterval: 30000, quietThreshold: 2000 } })
|
|
245
279
|
```
|
|
246
280
|
|
|
247
|
-
## CLI
|
|
248
|
-
|
|
249
|
-
### Claude Code (`claude`)
|
|
250
|
-
|
|
251
|
-
| Mode | Command |
|
|
252
|
-
|------|---------|
|
|
253
|
-
| Interactive (idle) | `claude` |
|
|
254
|
-
| Interactive (prompted) | `claude "Explain this project"` |
|
|
255
|
-
| Headless (use bash, not overlay) | `claude -p "Explain this function"` |
|
|
256
|
-
|
|
257
|
-
### Gemini CLI (`gemini`)
|
|
258
|
-
|
|
259
|
-
| Mode | Command |
|
|
260
|
-
|------|---------|
|
|
261
|
-
| Interactive (idle) | `gemini` |
|
|
262
|
-
| Interactive (prompted) | `gemini -i "Explain this codebase"` |
|
|
263
|
-
| Headless (use bash, not overlay) | `gemini -p "What is fine tuning?"` |
|
|
264
|
-
|
|
265
|
-
### Codex CLI (`codex`)
|
|
266
|
-
|
|
267
|
-
| Mode | Command |
|
|
268
|
-
|------|---------|
|
|
269
|
-
| Interactive (idle) | `codex` |
|
|
270
|
-
| Interactive (prompted) | `codex "Explain this codebase"` |
|
|
271
|
-
| Headless (use bash, not overlay) | `codex exec "summarize the repo"` |
|
|
272
|
-
|
|
273
|
-
### Cursor CLI (`cursor-agent`)
|
|
274
|
-
|
|
275
|
-
| Mode | Command |
|
|
276
|
-
|------|---------|
|
|
277
|
-
| Interactive (idle) | `cursor-agent` |
|
|
278
|
-
| Interactive (prompted) | `cursor-agent "review this repo"` |
|
|
279
|
-
| Headless (use bash, not overlay) | `cursor-agent -p "find issues" --output-format text` |
|
|
280
|
-
|
|
281
|
-
### Pi (`pi`)
|
|
281
|
+
## CLI Quick Reference
|
|
282
282
|
|
|
283
|
-
|
|
|
284
|
-
|
|
285
|
-
|
|
|
286
|
-
|
|
|
287
|
-
|
|
|
283
|
+
| Agent | Interactive | With Prompt | Headless (bash) |
|
|
284
|
+
|-------|-------------|-------------|-----------------|
|
|
285
|
+
| `claude` | `claude` | `claude "prompt"` | `claude -p "prompt"` |
|
|
286
|
+
| `gemini` | `gemini` | `gemini -i "prompt"` | `gemini "prompt"` |
|
|
287
|
+
| `codex` | `codex` | `codex "prompt"` | `codex exec "prompt"` |
|
|
288
|
+
| `agent` | `agent` | `agent "prompt"` | `agent -p "prompt"` |
|
|
289
|
+
| `pi` | `pi` | `pi "prompt"` | `pi -p "prompt"` |
|
|
288
290
|
|
|
289
|
-
|
|
291
|
+
**Gemini model:** `gemini -m gemini-3-flash-preview -i "prompt"`
|
|
290
292
|
|
|
291
293
|
## Prompt Packaging Rules
|
|
292
294
|
|
package/index.ts
CHANGED
|
@@ -312,7 +312,9 @@ The user sees the overlay and can:
|
|
|
312
312
|
- Kill/background via double-Escape
|
|
313
313
|
|
|
314
314
|
QUERYING SESSION STATUS:
|
|
315
|
-
- interactive_shell({ sessionId: "calm-reef" }) - get status + rendered terminal output (
|
|
315
|
+
- interactive_shell({ sessionId: "calm-reef" }) - get status + rendered terminal output (default: 20 lines, 5KB)
|
|
316
|
+
- interactive_shell({ sessionId: "calm-reef", outputLines: 50 }) - get more lines (max: 200)
|
|
317
|
+
- interactive_shell({ sessionId: "calm-reef", outputMaxChars: 20000 }) - get more content (max: 50KB)
|
|
316
318
|
- interactive_shell({ sessionId: "calm-reef", kill: true }) - end session
|
|
317
319
|
- interactive_shell({ sessionId: "calm-reef", input: "..." }) - send input
|
|
318
320
|
|
|
@@ -362,6 +364,16 @@ Examples:
|
|
|
362
364
|
description: "Kill the session (requires sessionId). Use when task appears complete.",
|
|
363
365
|
}),
|
|
364
366
|
),
|
|
367
|
+
outputLines: Type.Optional(
|
|
368
|
+
Type.Number({
|
|
369
|
+
description: "Number of lines to return when querying (default: 20, max: 200)",
|
|
370
|
+
}),
|
|
371
|
+
),
|
|
372
|
+
outputMaxChars: Type.Optional(
|
|
373
|
+
Type.Number({
|
|
374
|
+
description: "Max chars to return when querying (default: 5KB, max: 50KB)",
|
|
375
|
+
}),
|
|
376
|
+
),
|
|
365
377
|
settings: Type.Optional(
|
|
366
378
|
Type.Object({
|
|
367
379
|
updateInterval: Type.Optional(
|
|
@@ -474,6 +486,8 @@ Examples:
|
|
|
474
486
|
command,
|
|
475
487
|
sessionId,
|
|
476
488
|
kill,
|
|
489
|
+
outputLines,
|
|
490
|
+
outputMaxChars,
|
|
477
491
|
settings,
|
|
478
492
|
input,
|
|
479
493
|
cwd,
|
|
@@ -488,6 +502,8 @@ Examples:
|
|
|
488
502
|
command?: string;
|
|
489
503
|
sessionId?: string;
|
|
490
504
|
kill?: boolean;
|
|
505
|
+
outputLines?: number;
|
|
506
|
+
outputMaxChars?: number;
|
|
491
507
|
settings?: { updateInterval?: number; quietThreshold?: number };
|
|
492
508
|
input?: string | { text?: string; keys?: string[]; hex?: string[]; paste?: string };
|
|
493
509
|
cwd?: string;
|
|
@@ -520,7 +536,7 @@ Examples:
|
|
|
520
536
|
|
|
521
537
|
// Kill session if requested
|
|
522
538
|
if (kill) {
|
|
523
|
-
const { output, truncated, totalBytes } = session.getOutput(
|
|
539
|
+
const { output, truncated, totalBytes } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars });
|
|
524
540
|
const status = session.getStatus();
|
|
525
541
|
const runtime = session.getRuntime();
|
|
526
542
|
session.kill();
|
|
@@ -603,7 +619,7 @@ Examples:
|
|
|
603
619
|
// If session completed, always allow query (no rate limiting)
|
|
604
620
|
// Rate limiting only applies to "checking in" on running sessions
|
|
605
621
|
if (result) {
|
|
606
|
-
const { output, truncated, totalBytes } = session.getOutput(true);
|
|
622
|
+
const { output, truncated, totalBytes } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars });
|
|
607
623
|
const truncatedNote = truncated ? ` (${totalBytes} bytes total, truncated)` : "";
|
|
608
624
|
const hasOutput = output.length > 0;
|
|
609
625
|
|
|
@@ -630,7 +646,7 @@ Examples:
|
|
|
630
646
|
}
|
|
631
647
|
|
|
632
648
|
// Session still running - check rate limiting
|
|
633
|
-
const outputResult = session.getOutput();
|
|
649
|
+
const outputResult = session.getOutput({ lines: outputLines, maxChars: outputMaxChars });
|
|
634
650
|
|
|
635
651
|
// If rate limited, wait until allowed then return fresh result
|
|
636
652
|
// Use Promise.race to detect if session completes during wait
|
|
@@ -653,7 +669,7 @@ Examples:
|
|
|
653
669
|
};
|
|
654
670
|
}
|
|
655
671
|
const earlyResult = earlySession.getResult();
|
|
656
|
-
const { output, truncated, totalBytes } = earlySession.getOutput(true);
|
|
672
|
+
const { output, truncated, totalBytes } = earlySession.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars });
|
|
657
673
|
const earlyStatus = earlySession.getStatus();
|
|
658
674
|
const earlyRuntime = earlySession.getRuntime();
|
|
659
675
|
const truncatedNote = truncated ? ` (${totalBytes} bytes total, truncated)` : "";
|
|
@@ -702,7 +718,7 @@ Examples:
|
|
|
702
718
|
};
|
|
703
719
|
}
|
|
704
720
|
// Get fresh output after waiting
|
|
705
|
-
const freshOutput = session.getOutput();
|
|
721
|
+
const freshOutput = session.getOutput({ lines: outputLines, maxChars: outputMaxChars });
|
|
706
722
|
const truncatedNote = freshOutput.truncated ? ` (${freshOutput.totalBytes} bytes total, truncated)` : "";
|
|
707
723
|
const hasOutput = freshOutput.output.length > 0;
|
|
708
724
|
const freshStatus = session.getStatus();
|
package/overlay-component.ts
CHANGED
|
@@ -98,6 +98,8 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
98
98
|
private dialogSelection: DialogChoice = "background";
|
|
99
99
|
private exitCountdown = 0;
|
|
100
100
|
private lastEscapeTime = 0;
|
|
101
|
+
private showEscapeHint = false;
|
|
102
|
+
private escapeHintTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
101
103
|
private countdownInterval: ReturnType<typeof setInterval> | null = null;
|
|
102
104
|
private lastWidth = 0;
|
|
103
105
|
private lastHeight = 0;
|
|
@@ -128,6 +130,8 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
128
130
|
private lastQueryTime = 0;
|
|
129
131
|
// Completion callbacks for waiters
|
|
130
132
|
private completeCallbacks: Array<() => void> = [];
|
|
133
|
+
// Simple render throttle to reduce flicker
|
|
134
|
+
private renderTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
131
135
|
|
|
132
136
|
constructor(
|
|
133
137
|
tui: TUI,
|
|
@@ -158,10 +162,9 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
158
162
|
},
|
|
159
163
|
{
|
|
160
164
|
onData: () => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
this.tui.requestRender();
|
|
165
|
+
// Don't call scrollToBottom() here - pty-session handles auto-follow at render time
|
|
166
|
+
// Debounce render to batch rapid updates and reduce flicker
|
|
167
|
+
this.debouncedRender();
|
|
165
168
|
|
|
166
169
|
// Track activity for on-quiet mode
|
|
167
170
|
if (this.state === "hands-free" && this.updateMode === "on-quiet") {
|
|
@@ -225,7 +228,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
225
228
|
reason: options.reason,
|
|
226
229
|
write: (data) => this.session.write(data),
|
|
227
230
|
kill: () => this.killSession(),
|
|
228
|
-
getOutput: (
|
|
231
|
+
getOutput: (options) => this.getOutputSinceLastCheck(options),
|
|
229
232
|
getStatus: () => this.getSessionStatus(),
|
|
230
233
|
getRuntime: () => this.getRuntime(),
|
|
231
234
|
getResult: () => this.getCompletionResult(),
|
|
@@ -246,13 +249,27 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
246
249
|
|
|
247
250
|
// Public methods for non-blocking mode (agent queries)
|
|
248
251
|
|
|
249
|
-
//
|
|
250
|
-
private static readonly
|
|
251
|
-
|
|
252
|
-
private static readonly
|
|
252
|
+
// Default output limits per status query
|
|
253
|
+
private static readonly DEFAULT_STATUS_OUTPUT = 5 * 1024; // 5KB
|
|
254
|
+
private static readonly DEFAULT_STATUS_LINES = 20;
|
|
255
|
+
private static readonly MAX_STATUS_OUTPUT = 50 * 1024; // 50KB max
|
|
256
|
+
private static readonly MAX_STATUS_LINES = 200; // 200 lines max
|
|
253
257
|
|
|
254
258
|
/** Get rendered terminal output (last N lines, truncated if too large) */
|
|
255
|
-
getOutputSinceLastCheck(skipRateLimit = false): { output: string; truncated: boolean; totalBytes: number; rateLimited?: boolean; waitSeconds?: number } {
|
|
259
|
+
getOutputSinceLastCheck(options: { skipRateLimit?: boolean; lines?: number; maxChars?: number } | boolean = false): { output: string; truncated: boolean; totalBytes: number; rateLimited?: boolean; waitSeconds?: number } {
|
|
260
|
+
// Handle legacy boolean parameter
|
|
261
|
+
const opts = typeof options === "boolean" ? { skipRateLimit: options } : options;
|
|
262
|
+
const skipRateLimit = opts.skipRateLimit ?? false;
|
|
263
|
+
// Clamp lines and maxChars to valid ranges (1 to MAX)
|
|
264
|
+
const requestedLines = Math.max(1, Math.min(
|
|
265
|
+
opts.lines ?? InteractiveShellOverlay.DEFAULT_STATUS_LINES,
|
|
266
|
+
InteractiveShellOverlay.MAX_STATUS_LINES
|
|
267
|
+
));
|
|
268
|
+
const requestedMaxChars = Math.max(1, Math.min(
|
|
269
|
+
opts.maxChars ?? InteractiveShellOverlay.DEFAULT_STATUS_OUTPUT,
|
|
270
|
+
InteractiveShellOverlay.MAX_STATUS_OUTPUT
|
|
271
|
+
));
|
|
272
|
+
|
|
256
273
|
// Check rate limiting (unless skipped, e.g., for completed sessions)
|
|
257
274
|
if (!skipRateLimit) {
|
|
258
275
|
const now = Date.now();
|
|
@@ -277,14 +294,14 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
277
294
|
// Use rendered terminal output instead of raw stream
|
|
278
295
|
// This gives clean, readable content without TUI animation garbage
|
|
279
296
|
const lines = this.session.getTailLines({
|
|
280
|
-
lines:
|
|
297
|
+
lines: requestedLines,
|
|
281
298
|
ansi: false,
|
|
282
|
-
maxChars:
|
|
299
|
+
maxChars: requestedMaxChars,
|
|
283
300
|
});
|
|
284
301
|
|
|
285
302
|
const output = lines.join("\n");
|
|
286
303
|
const totalBytes = output.length;
|
|
287
|
-
const truncated = lines.length >=
|
|
304
|
+
const truncated = lines.length >= requestedLines;
|
|
288
305
|
|
|
289
306
|
return { output, truncated, totalBytes };
|
|
290
307
|
}
|
|
@@ -334,6 +351,18 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
334
351
|
this.completeCallbacks = [];
|
|
335
352
|
}
|
|
336
353
|
|
|
354
|
+
/** Debounced render - waits for data to settle before rendering */
|
|
355
|
+
private debouncedRender(): void {
|
|
356
|
+
if (this.renderTimeout) {
|
|
357
|
+
clearTimeout(this.renderTimeout);
|
|
358
|
+
}
|
|
359
|
+
// Wait 16ms for more data before rendering
|
|
360
|
+
this.renderTimeout = setTimeout(() => {
|
|
361
|
+
this.renderTimeout = null;
|
|
362
|
+
this.tui.requestRender();
|
|
363
|
+
}, 16);
|
|
364
|
+
}
|
|
365
|
+
|
|
337
366
|
/** Get the session ID */
|
|
338
367
|
getSessionId(): string | null {
|
|
339
368
|
return this.sessionId;
|
|
@@ -813,12 +842,31 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
813
842
|
const now = Date.now();
|
|
814
843
|
if (now - this.lastEscapeTime < this.config.doubleEscapeThreshold) {
|
|
815
844
|
this.lastEscapeTime = 0;
|
|
845
|
+
this.clearEscapeHint();
|
|
816
846
|
return true;
|
|
817
847
|
}
|
|
818
848
|
this.lastEscapeTime = now;
|
|
849
|
+
// Show hint after first escape - clear any existing timeout first
|
|
850
|
+
if (this.escapeHintTimeout) {
|
|
851
|
+
clearTimeout(this.escapeHintTimeout);
|
|
852
|
+
}
|
|
853
|
+
this.showEscapeHint = true;
|
|
854
|
+
this.escapeHintTimeout = setTimeout(() => {
|
|
855
|
+
this.showEscapeHint = false;
|
|
856
|
+
this.tui.requestRender();
|
|
857
|
+
}, this.config.doubleEscapeThreshold);
|
|
858
|
+
this.tui.requestRender();
|
|
819
859
|
return false;
|
|
820
860
|
}
|
|
821
861
|
|
|
862
|
+
private clearEscapeHint(): void {
|
|
863
|
+
if (this.escapeHintTimeout) {
|
|
864
|
+
clearTimeout(this.escapeHintTimeout);
|
|
865
|
+
this.escapeHintTimeout = null;
|
|
866
|
+
}
|
|
867
|
+
this.showEscapeHint = false;
|
|
868
|
+
}
|
|
869
|
+
|
|
822
870
|
handleInput(data: string): void {
|
|
823
871
|
if (this.state === "detach-dialog") {
|
|
824
872
|
this.handleDialogInput(data);
|
|
@@ -953,6 +1001,8 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
953
1001
|
this.session.resize(innerWidth, termRows);
|
|
954
1002
|
this.lastWidth = innerWidth;
|
|
955
1003
|
this.lastHeight = termRows;
|
|
1004
|
+
// After resize, ensure we're at the bottom to prevent flash to top
|
|
1005
|
+
this.session.scrollToBottom();
|
|
956
1006
|
}
|
|
957
1007
|
|
|
958
1008
|
const viewportLines = this.session.getViewportLines({ ansi: this.config.ansiReemit });
|
|
@@ -998,9 +1048,17 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
998
1048
|
footerLines.push(row(exitMsg));
|
|
999
1049
|
footerLines.push(row(dim(`Closing in ${this.exitCountdown}s... (any key to close)`)));
|
|
1000
1050
|
} else if (this.state === "hands-free") {
|
|
1001
|
-
|
|
1051
|
+
if (this.showEscapeHint) {
|
|
1052
|
+
footerLines.push(row(warning("Press Escape again to detach...")));
|
|
1053
|
+
} else {
|
|
1054
|
+
footerLines.push(row(dim("🤖 Agent controlling • Type to take over • Shift+Up/Down scroll")));
|
|
1055
|
+
}
|
|
1002
1056
|
} else {
|
|
1003
|
-
|
|
1057
|
+
if (this.showEscapeHint) {
|
|
1058
|
+
footerLines.push(row(warning("Press Escape again to detach...")));
|
|
1059
|
+
} else {
|
|
1060
|
+
footerLines.push(row(dim("Shift+Up/Down scroll • Double-Esc detach • Ctrl+C interrupt")));
|
|
1061
|
+
}
|
|
1004
1062
|
}
|
|
1005
1063
|
|
|
1006
1064
|
while (footerLines.length < FOOTER_LINES) {
|
|
@@ -1022,6 +1080,11 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
1022
1080
|
this.stopCountdown();
|
|
1023
1081
|
this.stopTimeout();
|
|
1024
1082
|
this.stopHandsFreeUpdates();
|
|
1083
|
+
this.clearEscapeHint();
|
|
1084
|
+
if (this.renderTimeout) {
|
|
1085
|
+
clearTimeout(this.renderTimeout);
|
|
1086
|
+
this.renderTimeout = null;
|
|
1087
|
+
}
|
|
1025
1088
|
// Safety cleanup in case dispose() is called without going through finishWith*
|
|
1026
1089
|
// If session hasn't completed yet, kill it to prevent orphaned processes
|
|
1027
1090
|
if (!this.completionResult) {
|
|
@@ -1368,6 +1431,8 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
1368
1431
|
this.session.resize(innerWidth, termRows);
|
|
1369
1432
|
this.lastWidth = innerWidth;
|
|
1370
1433
|
this.lastHeight = termRows;
|
|
1434
|
+
// After resize, ensure we're at the bottom to prevent flash to top
|
|
1435
|
+
this.session.scrollToBottom();
|
|
1371
1436
|
}
|
|
1372
1437
|
|
|
1373
1438
|
const viewportLines = this.session.getViewportLines({ ansi: this.config.ansiReemit });
|
package/package.json
CHANGED
package/pty-session.ts
CHANGED
|
@@ -218,6 +218,7 @@ export class PtyTerminalSession {
|
|
|
218
218
|
private _exitCode: number | null = null;
|
|
219
219
|
private _signal: number | undefined;
|
|
220
220
|
private scrollOffset = 0;
|
|
221
|
+
private followBottom = true; // Auto-scroll to bottom when new data arrives
|
|
221
222
|
|
|
222
223
|
// Raw output buffer for incremental streaming
|
|
223
224
|
private rawOutput = "";
|
|
@@ -458,6 +459,11 @@ export class PtyTerminalSession {
|
|
|
458
459
|
const lines: string[] = [];
|
|
459
460
|
|
|
460
461
|
const totalLines = buffer.length;
|
|
462
|
+
// If following bottom, reset scroll offset at render time (not on each data event)
|
|
463
|
+
// This prevents flickering from scroll position racing with buffer updates
|
|
464
|
+
if (this.followBottom) {
|
|
465
|
+
this.scrollOffset = 0;
|
|
466
|
+
}
|
|
461
467
|
const viewportStart = Math.max(0, totalLines - this.xterm.rows - this.scrollOffset);
|
|
462
468
|
|
|
463
469
|
const useAnsi = !!options.ansi;
|
|
@@ -565,14 +571,20 @@ export class PtyTerminalSession {
|
|
|
565
571
|
const buffer = this.xterm.buffer.active;
|
|
566
572
|
const maxScroll = Math.max(0, buffer.length - this.xterm.rows);
|
|
567
573
|
this.scrollOffset = Math.min(this.scrollOffset + lines, maxScroll);
|
|
574
|
+
this.followBottom = false; // User scrolled up, stop auto-following
|
|
568
575
|
}
|
|
569
576
|
|
|
570
577
|
scrollDown(lines: number): void {
|
|
571
578
|
this.scrollOffset = Math.max(0, this.scrollOffset - lines);
|
|
579
|
+
// If scrolled to bottom, resume auto-following
|
|
580
|
+
if (this.scrollOffset === 0) {
|
|
581
|
+
this.followBottom = true;
|
|
582
|
+
}
|
|
572
583
|
}
|
|
573
584
|
|
|
574
585
|
scrollToBottom(): void {
|
|
575
586
|
this.scrollOffset = 0;
|
|
587
|
+
this.followBottom = true;
|
|
576
588
|
}
|
|
577
589
|
|
|
578
590
|
isScrolledUp(): boolean {
|
package/session-manager.ts
CHANGED
|
@@ -29,13 +29,19 @@ export interface OutputResult {
|
|
|
29
29
|
waitSeconds?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface OutputOptions {
|
|
33
|
+
skipRateLimit?: boolean;
|
|
34
|
+
lines?: number; // Override default 20 lines
|
|
35
|
+
maxChars?: number; // Override default 5KB
|
|
36
|
+
}
|
|
37
|
+
|
|
32
38
|
export interface ActiveSession {
|
|
33
39
|
id: string;
|
|
34
40
|
command: string;
|
|
35
41
|
reason?: string;
|
|
36
42
|
write: (data: string) => void;
|
|
37
43
|
kill: () => void;
|
|
38
|
-
getOutput: (
|
|
44
|
+
getOutput: (options?: OutputOptions | boolean) => OutputResult; // Get output since last check (truncated if large)
|
|
39
45
|
getStatus: () => ActiveSessionStatus;
|
|
40
46
|
getRuntime: () => number;
|
|
41
47
|
getResult: () => ActiveSessionResult | undefined; // Available when completed
|