pi-interactive-shell 0.4.9 → 0.5.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
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to the `pi-interactive-shell` extension will be documented in this file.
4
4
 
5
+ ## [0.5.1] - 2026-01-22
6
+
7
+ ### Fixed
8
+ - **Prevent overlay stacking** - Starting a new `interactive_shell` session or using `/attach` while an overlay is already open now returns an error instead of causing undefined behavior with stacked/stuck overlays.
9
+
10
+ ## [0.5.0] - 2026-01-22
11
+
12
+ ### Changed
13
+ - **BREAKING: Split `input` into separate fields for Vertex AI compatibility** - The `input` parameter which previously accepted either a string or an object with `text/keys/hex/paste` fields has been split into separate parameters:
14
+ - `input` - Raw text/keystrokes (string only)
15
+ - `inputKeys` - Named keys array (e.g., `["ctrl+c", "enter"]`)
16
+ - `inputHex` - Hex bytes array for raw escape sequences
17
+ - `inputPaste` - Text for bracketed paste mode
18
+
19
+ This change was required because Claude's Vertex AI API (`google-antigravity` provider) rejects `anyOf` JSON schemas with mixed primitive/object types.
20
+
21
+ ### Migration
22
+ ```typescript
23
+ // Before (0.4.x)
24
+ interactive_shell({ sessionId: "abc", input: { keys: ["ctrl+c"] } })
25
+ interactive_shell({ sessionId: "abc", input: { paste: "code" } })
26
+
27
+ // After (0.5.0)
28
+ interactive_shell({ sessionId: "abc", inputKeys: ["ctrl+c"] })
29
+ interactive_shell({ sessionId: "abc", inputPaste: "code" })
30
+
31
+ // Combining text with keys (still works)
32
+ interactive_shell({ sessionId: "abc", input: "y", inputKeys: ["enter"] })
33
+ ```
34
+
5
35
  ## [0.4.9] - 2026-01-21
6
36
 
7
37
  ### Fixed
package/README.md CHANGED
@@ -60,7 +60,7 @@ interactive_shell({ sessionId: "calm-reef" })
60
60
  // → { status: "running", output: "...", runtime: 45000 }
61
61
 
62
62
  // Send input if needed
63
- interactive_shell({ sessionId: "calm-reef", input: { keys: ["ctrl+c"] } })
63
+ interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] })
64
64
 
65
65
  // Kill when done
66
66
  interactive_shell({ sessionId: "calm-reef", kill: true })
@@ -103,11 +103,17 @@ For multi-turn sessions where you need back-and-forth interaction, leave it disa
103
103
  interactive_shell({ sessionId: "calm-reef", input: "SELECT * FROM users;\n" })
104
104
 
105
105
  // Named keys
106
- interactive_shell({ sessionId: "calm-reef", input: { keys: ["ctrl+c"] } })
107
- interactive_shell({ sessionId: "calm-reef", input: { keys: ["down", "down", "enter"] } })
106
+ interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] })
107
+ interactive_shell({ sessionId: "calm-reef", inputKeys: ["down", "down", "enter"] })
108
108
 
109
109
  // Bracketed paste (multiline without execution)
110
- interactive_shell({ sessionId: "calm-reef", input: { paste: "line1\nline2\nline3" } })
110
+ interactive_shell({ sessionId: "calm-reef", inputPaste: "line1\nline2\nline3" })
111
+
112
+ // Hex bytes (raw escape sequences)
113
+ interactive_shell({ sessionId: "calm-reef", inputHex: ["0x1b", "0x5b", "0x41"] })
114
+
115
+ // Combine text with keys
116
+ interactive_shell({ sessionId: "calm-reef", input: "y", inputKeys: ["enter"] })
111
117
  ```
112
118
 
113
119
  ### Configurable Output
package/SKILL.md CHANGED
@@ -161,7 +161,9 @@ interactive_shell({ sessionId: "calm-reef", kill: true })
161
161
  ### Sending Input
162
162
  ```typescript
163
163
  interactive_shell({ sessionId: "calm-reef", input: "/help\n" })
164
- interactive_shell({ sessionId: "calm-reef", input: { keys: ["ctrl+c"] } })
164
+ interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] })
165
+ interactive_shell({ sessionId: "calm-reef", inputPaste: "multi\nline\ncode" })
166
+ interactive_shell({ sessionId: "calm-reef", input: "y", inputKeys: ["enter"] }) // combine text + keys
165
167
  ```
166
168
 
167
169
  ### Query Output
@@ -223,14 +225,14 @@ Use the `sessionId` from updates to send input to a running hands-free session:
223
225
  // Send text
224
226
  interactive_shell({ sessionId: "shell-1", input: "/help\n" })
225
227
 
226
- // Send with named keys
227
- interactive_shell({ sessionId: "shell-1", input: { text: "/model", keys: ["enter"] } })
228
+ // Send text with keys
229
+ interactive_shell({ sessionId: "shell-1", input: "/model", inputKeys: ["enter"] })
228
230
 
229
231
  // Navigate menus
230
- interactive_shell({ sessionId: "shell-1", input: { keys: ["down", "down", "enter"] } })
232
+ interactive_shell({ sessionId: "shell-1", inputKeys: ["down", "down", "enter"] })
231
233
 
232
234
  // Interrupt
233
- interactive_shell({ sessionId: "shell-1", input: { keys: ["ctrl+c"] } })
235
+ interactive_shell({ sessionId: "shell-1", inputKeys: ["ctrl+c"] })
234
236
  ```
235
237
 
236
238
  ### Named Keys
@@ -257,45 +259,45 @@ Note: `ic`/`dc`, `ppage`/`npage`, `bspace` are tmux-style aliases for compatibil
257
259
  Supports `ctrl+`, `alt+`, `shift+` prefixes (or shorthand `c-`, `m-`, `s-`):
258
260
  ```typescript
259
261
  // Cancel
260
- { keys: ["ctrl+c"] }
262
+ inputKeys: ["ctrl+c"]
261
263
 
262
264
  // Alt+Tab
263
- { keys: ["alt+tab"] }
265
+ inputKeys: ["alt+tab"]
264
266
 
265
267
  // Ctrl+Alt+Delete
266
- { keys: ["ctrl+alt+delete"] }
268
+ inputKeys: ["ctrl+alt+delete"]
267
269
 
268
270
  // Shorthand syntax
269
- { keys: ["c-c", "m-x", "s-tab"] }
271
+ inputKeys: ["c-c", "m-x", "s-tab"]
270
272
  ```
271
273
 
272
274
  ### Hex Bytes (Advanced)
273
275
  Send raw escape sequences:
274
276
  ```typescript
275
- { hex: ["0x1b", "0x5b", "0x41"] } // ESC[A (up arrow)
277
+ inputHex: ["0x1b", "0x5b", "0x41"] // ESC[A (up arrow)
276
278
  ```
277
279
 
278
280
  ### Bracketed Paste
279
281
  Paste multiline text without triggering autocompletion/execution:
280
282
  ```typescript
281
- { paste: "function foo() {\n return 42;\n}" }
283
+ inputPaste: "function foo() {\n return 42;\n}"
282
284
  ```
283
285
 
284
286
  ### Model Selection Example
285
287
  ```typescript
286
288
  // Step 1: Open model selector
287
- interactive_shell({ sessionId: "shell-1", input: { text: "/model", keys: ["enter"] } })
289
+ interactive_shell({ sessionId: "shell-1", input: "/model", inputKeys: ["enter"] })
288
290
 
289
291
  // Step 2: Filter and select (after ~500ms delay)
290
- interactive_shell({ sessionId: "shell-1", input: { text: "sonnet", keys: ["enter"] } })
292
+ interactive_shell({ sessionId: "shell-1", input: "sonnet", inputKeys: ["enter"] })
291
293
 
292
294
  // Or navigate with arrows:
293
- interactive_shell({ sessionId: "shell-1", input: { keys: ["down", "down", "down", "enter"] } })
295
+ interactive_shell({ sessionId: "shell-1", inputKeys: ["down", "down", "down", "enter"] })
294
296
  ```
295
297
 
296
298
  ### Context Compaction
297
299
  ```typescript
298
- interactive_shell({ sessionId: "shell-1", input: { text: "/compact", keys: ["enter"] } })
300
+ interactive_shell({ sessionId: "shell-1", input: "/compact", inputKeys: ["enter"] })
299
301
  ```
300
302
 
301
303
  ### Changing Update Settings
@@ -396,11 +398,11 @@ interactive_shell({
396
398
  // Text with enter
397
399
  interactive_shell({ sessionId: "calm-reef", input: "/compact\n" })
398
400
 
399
- // Named keys
400
- interactive_shell({ sessionId: "calm-reef", input: { text: "/model", keys: ["enter"] } })
401
+ // Text + named keys
402
+ interactive_shell({ sessionId: "calm-reef", input: "/model", inputKeys: ["enter"] })
401
403
 
402
404
  // Menu navigation
403
- interactive_shell({ sessionId: "calm-reef", input: { keys: ["down", "down", "enter"] } })
405
+ interactive_shell({ sessionId: "calm-reef", inputKeys: ["down", "down", "enter"] })
404
406
  ```
405
407
 
406
408
  **Change update frequency:**
package/index.ts CHANGED
@@ -8,6 +8,9 @@ import { translateInput } from "./key-encoding.js";
8
8
  import { TOOL_NAME, TOOL_LABEL, TOOL_DESCRIPTION, toolParameters, type ToolParams } from "./tool-schema.js";
9
9
  import { formatDuration, formatDurationMs } from "./types.js";
10
10
 
11
+ // Track whether an overlay is currently open to prevent stacking
12
+ let overlayOpen = false;
13
+
11
14
  export default function interactiveShellExtension(pi: ExtensionAPI) {
12
15
  pi.on("session_shutdown", () => {
13
16
  sessionManager.killAll();
@@ -31,6 +34,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
31
34
  incremental,
32
35
  settings,
33
36
  input,
37
+ inputKeys,
38
+ inputHex,
39
+ inputPaste,
34
40
  cwd,
35
41
  name,
36
42
  reason,
@@ -41,6 +47,12 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
41
47
  timeout,
42
48
  } = params as ToolParams;
43
49
 
50
+ // Build structured input from separate fields if any are provided
51
+ const hasStructuredInput = inputKeys?.length || inputHex?.length || inputPaste;
52
+ const effectiveInput = hasStructuredInput
53
+ ? { text: input, keys: inputKeys, hex: inputHex, paste: inputPaste }
54
+ : input;
55
+
44
56
  // Mode 1: Interact with existing session (query status, send input, kill, or change settings)
45
57
  if (sessionId) {
46
58
  const session = sessionManager.getActive(sessionId);
@@ -100,8 +112,8 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
100
112
  }
101
113
 
102
114
  // Send input if provided
103
- if (input !== undefined) {
104
- const translatedInput = translateInput(input);
115
+ if (effectiveInput !== undefined) {
116
+ const translatedInput = translateInput(effectiveInput);
105
117
  const success = sessionManager.writeToActive(sessionId, translatedInput);
106
118
 
107
119
  if (!success) {
@@ -113,17 +125,17 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
113
125
  }
114
126
 
115
127
  const inputDesc =
116
- typeof input === "string"
117
- ? input.length === 0
128
+ typeof effectiveInput === "string"
129
+ ? effectiveInput.length === 0
118
130
  ? "(empty)"
119
- : input.length > 50
120
- ? `${input.slice(0, 50)}...`
121
- : input
131
+ : effectiveInput.length > 50
132
+ ? `${effectiveInput.slice(0, 50)}...`
133
+ : effectiveInput
122
134
  : [
123
- input.text ?? "",
124
- input.keys ? `keys:[${input.keys.join(",")}]` : "",
125
- input.hex ? `hex:[${input.hex.length} bytes]` : "",
126
- input.paste ? `paste:[${input.paste.length} chars]` : "",
135
+ effectiveInput.text ?? "",
136
+ effectiveInput.keys ? `keys:[${effectiveInput.keys.join(",")}]` : "",
137
+ effectiveInput.hex ? `hex:[${effectiveInput.hex.length} bytes]` : "",
138
+ effectiveInput.paste ? `paste:[${effectiveInput.paste.length} chars]` : "",
127
139
  ]
128
140
  .filter(Boolean)
129
141
  .join(" + ") || "(empty)";
@@ -361,12 +373,24 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
361
373
  const config = loadConfig(effectiveCwd);
362
374
  const isHandsFree = mode === "hands-free";
363
375
 
376
+ // Prevent starting a new overlay while one is already open
377
+ if (overlayOpen) {
378
+ return {
379
+ content: [{ type: "text", text: "An interactive shell overlay is already open. Wait for it to close or kill the active session before starting a new one." }],
380
+ isError: true,
381
+ details: { error: "overlay_already_open" },
382
+ };
383
+ }
384
+
364
385
  // Generate sessionId early so it's available immediately
365
386
  const generatedSessionId = isHandsFree ? generateSessionId(name) : undefined;
366
387
 
367
388
  // For hands-free mode: non-blocking - return immediately with sessionId
368
389
  // Agent can then query status/output via sessionId and kill when done
369
390
  if (isHandsFree && generatedSessionId) {
391
+ // Mark overlay as open
392
+ overlayOpen = true;
393
+
370
394
  // Start overlay but don't await - it runs in background
371
395
  const overlayPromise = ctx.ui.custom<InteractiveShellResult>(
372
396
  (tui, theme, _kb, done) =>
@@ -412,12 +436,14 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
412
436
 
413
437
  // Handle overlay completion in background (cleanup when user closes)
414
438
  overlayPromise.then((result) => {
439
+ overlayOpen = false;
415
440
  // Session already handles cleanup via finishWith* methods
416
441
  // This just ensures the promise doesn't cause unhandled rejection
417
442
  if (result.userTookOver) {
418
443
  // User took over - session continues interactively
419
444
  }
420
445
  }).catch(() => {
446
+ overlayOpen = false;
421
447
  // Ignore errors - session cleanup handles this
422
448
  });
423
449
 
@@ -439,6 +465,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
439
465
  }
440
466
 
441
467
  // Interactive mode: blocking - wait for overlay to close
468
+ overlayOpen = true;
442
469
  onUpdate?.({
443
470
  content: [{ type: "text", text: `Opening: ${command}` }],
444
471
  details: {
@@ -448,7 +475,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
448
475
  },
449
476
  });
450
477
 
451
- const result = await ctx.ui.custom<InteractiveShellResult>(
478
+ let result: InteractiveShellResult;
479
+ try {
480
+ result = await ctx.ui.custom<InteractiveShellResult>(
452
481
  (tui, theme, _kb, done) =>
453
482
  new InteractiveShellOverlay(
454
483
  tui,
@@ -523,6 +552,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
523
552
  },
524
553
  },
525
554
  );
555
+ } finally {
556
+ overlayOpen = false;
557
+ }
526
558
 
527
559
  let summary: string;
528
560
  if (result.backgrounded) {
@@ -560,6 +592,12 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
560
592
  pi.registerCommand("attach", {
561
593
  description: "Reattach to a background shell session",
562
594
  handler: async (args, ctx) => {
595
+ // Prevent reattaching while another overlay is open
596
+ if (overlayOpen) {
597
+ ctx.ui.notify("An overlay is already open. Close it first.", "error");
598
+ return;
599
+ }
600
+
563
601
  const sessions = sessionManager.list();
564
602
 
565
603
  if (sessions.length === 0) {
@@ -592,25 +630,30 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
592
630
  }
593
631
 
594
632
  const config = loadConfig(ctx.cwd);
595
- await ctx.ui.custom<InteractiveShellResult>(
596
- (tui, theme, _kb, done) =>
597
- new ReattachOverlay(
598
- tui,
599
- theme,
600
- { id: session.id, command: session.command, reason: session.reason, session: session.session },
601
- config,
602
- done,
603
- ),
604
- {
605
- overlay: true,
606
- overlayOptions: {
607
- width: `${config.overlayWidthPercent}%`,
608
- maxHeight: `${config.overlayHeightPercent}%`,
609
- anchor: "center",
610
- margin: 1,
633
+ overlayOpen = true;
634
+ try {
635
+ await ctx.ui.custom<InteractiveShellResult>(
636
+ (tui, theme, _kb, done) =>
637
+ new ReattachOverlay(
638
+ tui,
639
+ theme,
640
+ { id: session.id, command: session.command, reason: session.reason, session: session.session },
641
+ config,
642
+ done,
643
+ ),
644
+ {
645
+ overlay: true,
646
+ overlayOptions: {
647
+ width: `${config.overlayWidthPercent}%`,
648
+ maxHeight: `${config.overlayHeightPercent}%`,
649
+ anchor: "center",
650
+ margin: 1,
651
+ },
611
652
  },
612
- },
613
- );
653
+ );
654
+ } finally {
655
+ overlayOpen = false;
656
+ }
614
657
  },
615
658
  });
616
659
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-interactive-shell",
3
- "version": "0.4.9",
3
+ "version": "0.5.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": {
package/tool-schema.ts CHANGED
@@ -54,13 +54,14 @@ Queries are limited to once every 60 seconds (configurable). If you query too so
54
54
  the tool will automatically wait until the limit expires before returning.
55
55
 
56
56
  SENDING INPUT:
57
- - interactive_shell({ sessionId: "calm-reef", input: "/help\\n" })
58
- - interactive_shell({ sessionId: "calm-reef", input: { keys: ["ctrl+c"] } })
57
+ - interactive_shell({ sessionId: "calm-reef", input: "/help\\n" }) - raw text/keystrokes
58
+ - interactive_shell({ sessionId: "calm-reef", inputKeys: ["ctrl+c"] }) - named keys
59
+ - interactive_shell({ sessionId: "calm-reef", inputKeys: ["up", "up", "enter"] }) - multiple keys
60
+ - interactive_shell({ sessionId: "calm-reef", inputHex: ["0x1b", "0x5b", "0x41"] }) - raw escape sequences
61
+ - interactive_shell({ sessionId: "calm-reef", inputPaste: "multiline\\ntext" }) - bracketed paste (prevents auto-execution)
59
62
 
60
- Named keys: up, down, left, right, enter, escape, tab, backspace, ctrl+c, ctrl+d, etc.
63
+ Named keys for inputKeys: up, down, left, right, enter, escape, tab, backspace, ctrl+c, ctrl+d, etc.
61
64
  Modifiers: ctrl+x, alt+x, shift+tab, ctrl+alt+delete (or c-x, m-x, s-tab syntax)
62
- Hex bytes: input: { hex: ["0x1b", "0x5b", "0x41"] } for raw escape sequences
63
- Bracketed paste: input: { paste: "multiline\\ntext" } prevents auto-execution
64
65
 
65
66
  TIMEOUT (for TUI commands that don't exit cleanly):
66
67
  Use timeout to auto-kill after N milliseconds. Useful for capturing output from commands like "pi --help":
@@ -128,31 +129,22 @@ export const toolParameters = Type.Object({
128
129
  }),
129
130
  ),
130
131
  input: Type.Optional(
131
- Type.Union(
132
- [
133
- Type.String({ description: "Raw text/keystrokes to send" }),
134
- Type.Object({
135
- text: Type.Optional(Type.String({ description: "Text to type" })),
136
- keys: Type.Optional(
137
- Type.Array(Type.String(), {
138
- description:
139
- "Named keys with modifier support: up, down, enter, ctrl+c, alt+x, shift+tab, ctrl+alt+delete, etc.",
140
- }),
141
- ),
142
- hex: Type.Optional(
143
- Type.Array(Type.String(), {
144
- description: "Hex bytes to send (e.g., ['0x1b', '0x5b', '0x41'] for ESC[A)",
145
- }),
146
- ),
147
- paste: Type.Optional(
148
- Type.String({
149
- description: "Text to paste with bracketed paste mode (prevents auto-execution)",
150
- }),
151
- ),
152
- }),
153
- ],
154
- { description: "Input to send to an existing session (requires sessionId)" },
155
- ),
132
+ Type.String({ description: "Raw text/keystrokes to send to the session (requires sessionId). For special keys, use inputKeys instead." }),
133
+ ),
134
+ inputKeys: Type.Optional(
135
+ Type.Array(Type.String(), {
136
+ description: "Named keys with modifier support: up, down, enter, ctrl+c, alt+x, shift+tab, ctrl+alt+delete, etc. (requires sessionId)",
137
+ }),
138
+ ),
139
+ inputHex: Type.Optional(
140
+ Type.Array(Type.String(), {
141
+ description: "Hex bytes to send as raw escape sequences (e.g., ['0x1b', '0x5b', '0x41'] for ESC[A). (requires sessionId)",
142
+ }),
143
+ ),
144
+ inputPaste: Type.Optional(
145
+ Type.String({
146
+ description: "Text to paste with bracketed paste mode - prevents shells from auto-executing multiline input. (requires sessionId)",
147
+ }),
156
148
  ),
157
149
  cwd: Type.Optional(
158
150
  Type.String({
@@ -171,15 +163,15 @@ export const toolParameters = Type.Object({
171
163
  }),
172
164
  ),
173
165
  mode: Type.Optional(
174
- Type.Union([Type.Literal("interactive"), Type.Literal("hands-free")], {
175
- description: "interactive (default): user controls. hands-free: agent monitors, user can take over",
166
+ Type.String({
167
+ description: "Mode: 'interactive' (default, user controls) or 'hands-free' (agent monitors, user can take over)",
176
168
  }),
177
169
  ),
178
170
  handsFree: Type.Optional(
179
171
  Type.Object({
180
172
  updateMode: Type.Optional(
181
- Type.Union([Type.Literal("on-quiet"), Type.Literal("interval")], {
182
- description: "on-quiet (default): emit when output stops. interval: emit on fixed schedule.",
173
+ Type.String({
174
+ description: "Update mode: 'on-quiet' (default, emit when output stops) or 'interval' (emit on fixed schedule)",
183
175
  }),
184
176
  ),
185
177
  updateInterval: Type.Optional(
@@ -235,7 +227,10 @@ export interface ToolParams {
235
227
  drain?: boolean;
236
228
  incremental?: boolean;
237
229
  settings?: { updateInterval?: number; quietThreshold?: number };
238
- input?: string | { text?: string; keys?: string[]; hex?: string[]; paste?: string };
230
+ input?: string;
231
+ inputKeys?: string[];
232
+ inputHex?: string[];
233
+ inputPaste?: string;
239
234
  cwd?: string;
240
235
  name?: string;
241
236
  reason?: string;