pi-interactive-shell 0.7.1 → 0.8.1

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,25 @@ All notable changes to the `pi-interactive-shell` extension will be documented i
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.1] - 2026-02-08
8
+
9
+ ### Fixed
10
+ - README: documented `handsFree.gracePeriod` tool parameter and startup grace period behavior in Auto-Exit on Quiet and Dispatch sections.
11
+ - README: added missing `handoffPreviewLines` and `handoffPreviewMaxChars` to config settings table.
12
+
13
+ ## [0.8.0] - 2026-02-08
14
+
15
+ ### Added
16
+ - `autoExitGracePeriod` config option (default: 30000ms, clamped 5000-120000ms) and `handsFree.gracePeriod` tool parameter override for startup quiet-kill grace control.
17
+
18
+ ### Changed
19
+ - Default `overlayHeightPercent` increased from 45 to 60 for improved usable terminal rows on smaller displays.
20
+ - Overlay sizing now uses dynamic footer chrome: compact 2-line footer in normal states and full 6-line footer in detach dialog, increasing terminal viewport height during normal operation.
21
+
22
+ ### Fixed
23
+ - Dispatch/hands-free `autoExitOnQuiet` no longer kills sessions during startup silence; quiet timer now re-arms during grace period and applies auto-kill only after grace expires.
24
+ - README config table missing `handoffPreviewLines` and `handoffPreviewMaxChars` entries despite appearing in the JSON example.
25
+
7
26
  ## [0.7.1] - 2026-02-03
8
27
 
9
28
  ### Changed
@@ -290,4 +309,3 @@ interactive_shell({ sessionId: "abc", input: "y", inputKeys: ["enter"] })
290
309
  ### Fixed
291
310
  - Prevented TUI width crashes by avoiding unbounded terminal escape rendering.
292
311
  - Reduced flicker by sanitizing/redrawing in a controlled overlay viewport.
293
-
package/README.md CHANGED
@@ -115,7 +115,7 @@ Attach to review full output: interactive_shell({ attach: "calm-reef" })
115
115
 
116
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
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.
118
+ Dispatch defaults `autoExitOnQuiet: true` — the session gets a 30s startup grace period, then is killed after output goes silent (5s by default), which signals completion for task-oriented subagents. Tune the grace period with `handsFree: { gracePeriod: 60000 }` or opt out entirely with `handsFree: { autoExitOnQuiet: false }`.
119
119
 
120
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
121
 
@@ -161,6 +161,18 @@ interactive_shell({
161
161
  })
162
162
  ```
163
163
 
164
+ A 30s startup grace period prevents the session from being killed before the subprocess has time to produce output. Customize it per-call with `gracePeriod`:
165
+
166
+ ```typescript
167
+ interactive_shell({
168
+ command: 'pi "Run the full test suite"',
169
+ mode: "hands-free",
170
+ handsFree: { autoExitOnQuiet: true, gracePeriod: 60000 }
171
+ })
172
+ ```
173
+
174
+ The default grace period is also configurable globally via `autoExitGracePeriod` in the config file.
175
+
164
176
  For multi-turn sessions where you need back-and-forth interaction, leave it disabled (default) and use `kill: true` when done.
165
177
 
166
178
  ### Send Input
@@ -260,7 +272,7 @@ Configuration files (project overrides global):
260
272
  ```json
261
273
  {
262
274
  "overlayWidthPercent": 95,
263
- "overlayHeightPercent": 45,
275
+ "overlayHeightPercent": 60,
264
276
  "scrollbackLines": 5000,
265
277
  "exitAutoCloseDelay": 10,
266
278
  "minQueryIntervalSeconds": 60,
@@ -271,6 +283,7 @@ Configuration files (project overrides global):
271
283
  "handsFreeUpdateMode": "on-quiet",
272
284
  "handsFreeUpdateInterval": 60000,
273
285
  "handsFreeQuietThreshold": 5000,
286
+ "autoExitGracePeriod": 30000,
274
287
  "handsFreeUpdateMaxChars": 1500,
275
288
  "handsFreeMaxTotalChars": 100000,
276
289
  "handoffPreviewEnabled": true,
@@ -284,7 +297,7 @@ Configuration files (project overrides global):
284
297
  | Setting | Default | Description |
285
298
  |---------|---------|-------------|
286
299
  | `overlayWidthPercent` | 95 | Overlay width (10-100%) |
287
- | `overlayHeightPercent` | 45 | Overlay height (20-90%) |
300
+ | `overlayHeightPercent` | 60 | Overlay height (20-90%) |
288
301
  | `scrollbackLines` | 5000 | Terminal scrollback buffer |
289
302
  | `exitAutoCloseDelay` | 10 | Seconds before auto-close after exit |
290
303
  | `minQueryIntervalSeconds` | 60 | Rate limit between agent queries |
@@ -294,10 +307,13 @@ Configuration files (project overrides global):
294
307
  | `completionNotifyMaxChars` | 5000 | Max chars in completion notification (1KB-50KB) |
295
308
  | `handsFreeUpdateMode` | "on-quiet" | "on-quiet" or "interval" |
296
309
  | `handsFreeQuietThreshold` | 5000 | Silence duration before update (ms) |
310
+ | `autoExitGracePeriod` | 30000 | Startup grace before `autoExitOnQuiet` kill (ms) |
297
311
  | `handsFreeUpdateInterval` | 60000 | Max interval between updates (ms) |
298
312
  | `handsFreeUpdateMaxChars` | 1500 | Max chars per update |
299
313
  | `handsFreeMaxTotalChars` | 100000 | Total char budget for updates |
300
314
  | `handoffPreviewEnabled` | true | Include tail in tool result |
315
+ | `handoffPreviewLines` | 30 | Lines in tail preview (0-500) |
316
+ | `handoffPreviewMaxChars` | 2000 | Max chars in tail preview (0-50KB) |
301
317
  | `handoffSnapshotEnabled` | false | Write transcript on detach/exit |
302
318
  | `ansiReemit` | true | Preserve ANSI colors in output |
303
319
 
package/config.ts CHANGED
@@ -24,6 +24,7 @@ export interface InteractiveShellConfig {
24
24
  handsFreeUpdateMode: "on-quiet" | "interval";
25
25
  handsFreeUpdateInterval: number;
26
26
  handsFreeQuietThreshold: number;
27
+ autoExitGracePeriod: number;
27
28
  handsFreeUpdateMaxChars: number;
28
29
  handsFreeMaxTotalChars: number;
29
30
  // Query rate limiting
@@ -33,7 +34,7 @@ export interface InteractiveShellConfig {
33
34
  const DEFAULT_CONFIG: InteractiveShellConfig = {
34
35
  exitAutoCloseDelay: 10,
35
36
  overlayWidthPercent: 95,
36
- overlayHeightPercent: 45,
37
+ overlayHeightPercent: 60,
37
38
  scrollbackLines: 5000,
38
39
  ansiReemit: true,
39
40
  handoffPreviewEnabled: true,
@@ -52,6 +53,7 @@ const DEFAULT_CONFIG: InteractiveShellConfig = {
52
53
  handsFreeUpdateMode: "on-quiet" as const,
53
54
  handsFreeUpdateInterval: 60000,
54
55
  handsFreeQuietThreshold: 5000,
56
+ autoExitGracePeriod: 30000,
55
57
  handsFreeUpdateMaxChars: 1500,
56
58
  handsFreeMaxTotalChars: 100000,
57
59
  // Query rate limiting (default 60 seconds between queries)
@@ -87,7 +89,7 @@ export function loadConfig(cwd: string): InteractiveShellConfig {
87
89
  ...merged,
88
90
  exitAutoCloseDelay: clampInt(merged.exitAutoCloseDelay, DEFAULT_CONFIG.exitAutoCloseDelay, 0, 60),
89
91
  overlayWidthPercent: clampPercent(merged.overlayWidthPercent, DEFAULT_CONFIG.overlayWidthPercent),
90
- // Height: 20-90% range (default 45%)
92
+ // Height: 20-90% range (default 60%)
91
93
  overlayHeightPercent: clampInt(merged.overlayHeightPercent, DEFAULT_CONFIG.overlayHeightPercent, 20, 90),
92
94
  scrollbackLines: clampInt(merged.scrollbackLines, DEFAULT_CONFIG.scrollbackLines, 200, 50000),
93
95
  ansiReemit: merged.ansiReemit !== false,
@@ -127,6 +129,12 @@ export function loadConfig(cwd: string): InteractiveShellConfig {
127
129
  1000,
128
130
  30000,
129
131
  ),
132
+ autoExitGracePeriod: clampInt(
133
+ merged.autoExitGracePeriod,
134
+ DEFAULT_CONFIG.autoExitGracePeriod,
135
+ 5000,
136
+ 120000,
137
+ ),
130
138
  handsFreeUpdateMaxChars: clampInt(
131
139
  merged.handsFreeUpdateMaxChars,
132
140
  DEFAULT_CONFIG.handsFreeUpdateMaxChars,
@@ -4,6 +4,7 @@ import type { InteractiveShellConfig } from "./config.js";
4
4
  export interface HeadlessMonitorOptions {
5
5
  autoExitOnQuiet: boolean;
6
6
  quietThreshold: number;
7
+ gracePeriod?: number;
7
8
  timeout?: number;
8
9
  }
9
10
 
@@ -80,6 +81,11 @@ export class HeadlessDispatchMonitor {
80
81
  this.quietTimer = setTimeout(() => {
81
82
  this.quietTimer = null;
82
83
  if (!this._disposed && this.options.autoExitOnQuiet) {
84
+ const gracePeriod = this.options.gracePeriod ?? this.config.autoExitGracePeriod;
85
+ if (Date.now() - this.startTime < gracePeriod) {
86
+ this.resetQuietTimer();
87
+ return;
88
+ }
83
89
  this.session.kill();
84
90
  this.handleCompletion(null, undefined, false, true);
85
91
  }
package/index.ts CHANGED
@@ -494,6 +494,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
494
494
  autoExitOnQuiet: mode === "dispatch"
495
495
  ? handsFree?.autoExitOnQuiet !== false
496
496
  : handsFree?.autoExitOnQuiet === true,
497
+ autoExitGracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
497
498
  handoffPreviewEnabled: handoffPreview?.enabled,
498
499
  handoffPreviewLines: handoffPreview?.lines,
499
500
  handoffPreviewMaxChars: handoffPreview?.maxChars,
@@ -637,6 +638,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
637
638
  const monitor = new HeadlessDispatchMonitor(session, config, {
638
639
  autoExitOnQuiet: handsFree?.autoExitOnQuiet !== false,
639
640
  quietThreshold: handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
641
+ gracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
640
642
  timeout,
641
643
  }, makeMonitorCompletionCallback(pi, id, startTime));
642
644
  headlessMonitors.set(id, monitor);
@@ -695,6 +697,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
695
697
  autoExitOnQuiet: mode === "dispatch"
696
698
  ? handsFree?.autoExitOnQuiet !== false
697
699
  : handsFree?.autoExitOnQuiet === true,
700
+ autoExitGracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
698
701
  handoffPreviewEnabled: handoffPreview?.enabled,
699
702
  handoffPreviewLines: handoffPreview?.lines,
700
703
  handoffPreviewMaxChars: handoffPreview?.maxChars,
@@ -760,6 +763,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
760
763
  handsFreeUpdateMaxChars: handsFree?.updateMaxChars,
761
764
  handsFreeMaxTotalChars: handsFree?.maxTotalChars,
762
765
  autoExitOnQuiet: handsFree?.autoExitOnQuiet,
766
+ autoExitGracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
763
767
  onHandsFreeUpdate: mode === "hands-free"
764
768
  ? (update) => {
765
769
  let statusText: string;
@@ -974,7 +978,7 @@ function setupDispatchCompletion(
974
978
  command: string;
975
979
  reason?: string;
976
980
  timeout?: number;
977
- handsFree?: { autoExitOnQuiet?: boolean; quietThreshold?: number };
981
+ handsFree?: { autoExitOnQuiet?: boolean; quietThreshold?: number; gracePeriod?: number };
978
982
  overlayStartTime?: number;
979
983
  },
980
984
  ): void {
@@ -1030,6 +1034,7 @@ function setupDispatchCompletion(
1030
1034
  const monitor = new HeadlessDispatchMonitor(bgSession.session, config, {
1031
1035
  autoExitOnQuiet: ctx.handsFree?.autoExitOnQuiet !== false,
1032
1036
  quietThreshold: ctx.handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
1037
+ gracePeriod: ctx.handsFree?.gracePeriod ?? config.autoExitGracePeriod,
1033
1038
  timeout: remainingTimeout,
1034
1039
  }, makeMonitorCompletionCallback(pi, bgId, bgStartTime));
1035
1040
  headlessMonitors.set(bgId, monitor);
@@ -13,8 +13,9 @@ import {
13
13
  type InteractiveShellOptions,
14
14
  type DialogChoice,
15
15
  type OverlayState,
16
- CHROME_LINES,
17
- FOOTER_LINES,
16
+ HEADER_LINES,
17
+ FOOTER_LINES_COMPACT,
18
+ FOOTER_LINES_DIALOG,
18
19
  formatDuration,
19
20
  } from "./types.js";
20
21
 
@@ -80,7 +81,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
80
81
  const overlayWidth = Math.floor((tui.terminal.columns * this.config.overlayWidthPercent) / 100);
81
82
  const overlayHeight = Math.floor((tui.terminal.rows * this.config.overlayHeightPercent) / 100);
82
83
  const cols = Math.max(20, overlayWidth - 4);
83
- const rows = Math.max(3, overlayHeight - CHROME_LINES);
84
+ const rows = Math.max(3, overlayHeight - (HEADER_LINES + FOOTER_LINES_COMPACT + 2));
84
85
 
85
86
  const ptyEvents = {
86
87
  onData: () => {
@@ -449,6 +450,15 @@ export class InteractiveShellOverlay implements Component, Focusable {
449
450
  if (this.state === "hands-free") {
450
451
  // Auto-exit on quiet: kill session when output stops (agent likely finished task)
451
452
  if (this.options.autoExitOnQuiet) {
453
+ const gracePeriod = this.options.autoExitGracePeriod ?? this.config.autoExitGracePeriod;
454
+ if (Date.now() - this.startTime < gracePeriod) {
455
+ if (this.hasUnsentData) {
456
+ this.emitHandsFreeUpdate();
457
+ this.hasUnsentData = false;
458
+ }
459
+ this.resetQuietTimer();
460
+ return;
461
+ }
452
462
  // Emit final update with any pending output
453
463
  if (this.hasUnsentData) {
454
464
  this.emitHandsFreeUpdate();
@@ -1077,7 +1087,9 @@ export class InteractiveShellOverlay implements Component, Focusable {
1077
1087
  lines.push(border("├" + "─".repeat(width - 2) + "┤"));
1078
1088
 
1079
1089
  const overlayHeight = Math.floor((this.tui.terminal.rows * this.config.overlayHeightPercent) / 100);
1080
- const termRows = Math.max(3, overlayHeight - CHROME_LINES);
1090
+ const footerHeight = this.state === "detach-dialog" ? FOOTER_LINES_DIALOG : FOOTER_LINES_COMPACT;
1091
+ const chrome = HEADER_LINES + footerHeight + 2;
1092
+ const termRows = Math.max(3, overlayHeight - chrome);
1081
1093
 
1082
1094
  if (innerWidth !== this.lastWidth || termRows !== this.lastHeight) {
1083
1095
  this.session.resize(innerWidth, termRows);
@@ -1136,7 +1148,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
1136
1148
  footerLines.push(row(dim("Ctrl+T transfer • Ctrl+B background • Ctrl+Q menu • Shift+Up/Down scroll")));
1137
1149
  }
1138
1150
 
1139
- while (footerLines.length < FOOTER_LINES) {
1151
+ while (footerLines.length < footerHeight) {
1140
1152
  footerLines.push(emptyRow());
1141
1153
  }
1142
1154
  lines.push(...footerLines);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-interactive-shell",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Run AI coding agents as foreground subagents in pi TUI overlays with hands-free monitoring",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,8 +11,9 @@ import {
11
11
  type InteractiveShellResult,
12
12
  type DialogChoice,
13
13
  type OverlayState,
14
- CHROME_LINES,
15
- FOOTER_LINES,
14
+ HEADER_LINES,
15
+ FOOTER_LINES_COMPACT,
16
+ FOOTER_LINES_DIALOG,
16
17
  } from "./types.js";
17
18
 
18
19
  export class ReattachOverlay implements Component, Focusable {
@@ -74,7 +75,7 @@ export class ReattachOverlay implements Component, Focusable {
74
75
  const overlayWidth = Math.floor((tui.terminal.columns * this.config.overlayWidthPercent) / 100);
75
76
  const overlayHeight = Math.floor((tui.terminal.rows * this.config.overlayHeightPercent) / 100);
76
77
  const cols = Math.max(20, overlayWidth - 4);
77
- const rows = Math.max(3, overlayHeight - CHROME_LINES);
78
+ const rows = Math.max(3, overlayHeight - (HEADER_LINES + FOOTER_LINES_COMPACT + 2));
78
79
  bgSession.session.resize(cols, rows);
79
80
  }
80
81
 
@@ -400,7 +401,9 @@ export class ReattachOverlay implements Component, Focusable {
400
401
  lines.push(border("├" + "─".repeat(width - 2) + "┤"));
401
402
 
402
403
  const overlayHeight = Math.floor((this.tui.terminal.rows * this.config.overlayHeightPercent) / 100);
403
- const termRows = Math.max(3, overlayHeight - CHROME_LINES);
404
+ const footerHeight = this.state === "detach-dialog" ? FOOTER_LINES_DIALOG : FOOTER_LINES_COMPACT;
405
+ const chrome = HEADER_LINES + footerHeight + 2;
406
+ const termRows = Math.max(3, overlayHeight - chrome);
404
407
 
405
408
  if (innerWidth !== this.lastWidth || termRows !== this.lastHeight) {
406
409
  this.session.resize(innerWidth, termRows);
@@ -457,7 +460,7 @@ export class ReattachOverlay implements Component, Focusable {
457
460
  footerLines.push(row(dim("Ctrl+T transfer • Ctrl+B background • Ctrl+Q menu • Shift+Up/Down scroll")));
458
461
  }
459
462
 
460
- while (footerLines.length < FOOTER_LINES) {
463
+ while (footerLines.length < footerHeight) {
461
464
  footerLines.push(emptyRow());
462
465
  }
463
466
  lines.push(...footerLines);
package/tool-schema.ts CHANGED
@@ -236,6 +236,9 @@ export const toolParameters = Type.Object({
236
236
  quietThreshold: Type.Optional(
237
237
  Type.Number({ description: "Silence duration before emitting update in on-quiet mode (default: 5000ms)" }),
238
238
  ),
239
+ gracePeriod: Type.Optional(
240
+ Type.Number({ description: "Startup grace period before autoExitOnQuiet can kill the session (default: 30000ms)" }),
241
+ ),
239
242
  updateMaxChars: Type.Optional(
240
243
  Type.Number({ description: "Max chars per update (default: 1500)" }),
241
244
  ),
@@ -299,6 +302,7 @@ export interface ToolParams {
299
302
  updateMode?: "on-quiet" | "interval";
300
303
  updateInterval?: number;
301
304
  quietThreshold?: number;
305
+ gracePeriod?: number;
302
306
  updateMaxChars?: number;
303
307
  maxTotalChars?: number;
304
308
  autoExitOnQuiet?: boolean;
package/types.ts CHANGED
@@ -70,6 +70,7 @@ export interface InteractiveShellOptions {
70
70
  onHandsFreeUpdate?: (update: HandsFreeUpdate) => void;
71
71
  // Auto-exit when output stops (for agents that don't exit on their own)
72
72
  autoExitOnQuiet?: boolean;
73
+ autoExitGracePeriod?: number;
73
74
  // Auto-kill timeout
74
75
  timeout?: number;
75
76
  // Existing PTY session (for attach flow -- skip creating a new PTY)
@@ -80,9 +81,9 @@ export type DialogChoice = "kill" | "background" | "transfer" | "cancel";
80
81
  export type OverlayState = "running" | "exited" | "detach-dialog" | "hands-free";
81
82
 
82
83
  // UI constants
83
- export const FOOTER_LINES = 6;
84
+ export const FOOTER_LINES_COMPACT = 2;
85
+ export const FOOTER_LINES_DIALOG = 6;
84
86
  export const HEADER_LINES = 4;
85
- export const CHROME_LINES = HEADER_LINES + FOOTER_LINES + 2;
86
87
 
87
88
  /** Format milliseconds to human-readable duration */
88
89
  export function formatDuration(ms: number): string {