pi-interactive-shell 0.4.5 → 0.4.7

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,25 @@
2
2
 
3
3
  All notable changes to the `pi-interactive-shell` extension will be documented in this file.
4
4
 
5
+ ## [0.4.6] - 2026-01-18
6
+
7
+ ### Added
8
+ - **Offset/limit pagination** - New `outputOffset` parameter for reading specific ranges of output:
9
+ - `outputOffset: 0, outputLines: 50` reads lines 0-49
10
+ - `outputOffset: 50, outputLines: 50` reads lines 50-99
11
+ - Returns `totalLines` in response for pagination
12
+ - **Drain mode for incremental output** - New `drain: true` parameter returns only NEW output since last query:
13
+ - More token-efficient than re-reading the tail each time
14
+ - Ideal for repeated polling of long-running sessions
15
+ - **Token Efficiency section in README** - Documents advantages over tmux workflow:
16
+ - Incremental aggregation vs full capture-pane
17
+ - Tail by default (20 lines, not full history)
18
+ - ANSI stripping before sending to agent
19
+ - Drain mode for only-new-output
20
+
21
+ ### Changed
22
+ - **getLogSlice() method in pty-session** - New low-level method for offset/limit pagination through raw output buffer
23
+
5
24
  ## [0.4.3] - 2026-01-18
6
25
 
7
26
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pi Interactive Shell
2
2
 
3
- An extension for [Pi coding agent](https://github.com/badlogic/pi-mono/) that lets Pi 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.
3
+ An extension for [Pi coding agent](https://github.com/badlogic/pi-mono/) that lets Pi run any interactive CLI in a TUI overlay - including other AI agents. Watch Pi drive Claude, Gemini, Codex, Cursor directly from Pi. Take over anytime. Real PTY, full terminal emulation, more token efficient than using tmux.
4
4
 
5
5
  ```typescript
6
6
  interactive_shell({ command: 'agent "fix all the bugs"', mode: "hands-free" })
@@ -149,6 +149,38 @@ interactive_shell({ sessionId: "calm-reef", outputMaxChars: 30000 })
149
149
  | Shift+Up/Down | Scroll history |
150
150
  | Any key (hands-free) | Take over control |
151
151
 
152
+ ## Token Efficiency
153
+
154
+ Unlike the standard tmux workflow where you `capture-pane` the entire terminal on every poll, Interactive Shell minimizes token waste:
155
+
156
+ **Incremental Aggregation** - Output is accumulated as it arrives, not re-captured on each query.
157
+
158
+ **Tail by Default** - Status queries return only the last 20 lines (configurable), not the full history.
159
+
160
+ **ANSI Stripping** - All escape codes are stripped before sending output to the agent. Clean text only.
161
+
162
+ **Drain Mode** - Use `drain: true` to get only NEW output since last query. No re-reading old content.
163
+
164
+ ```typescript
165
+ // First query: get recent output
166
+ interactive_shell({ sessionId: "calm-reef" })
167
+ // → returns last 20 lines
168
+
169
+ // Subsequent queries: get only new output (incremental)
170
+ interactive_shell({ sessionId: "calm-reef", drain: true })
171
+ // → returns only output since last query
172
+ ```
173
+
174
+ **Offset/Limit Pagination** - Read specific ranges of the full output log.
175
+
176
+ ```typescript
177
+ // Read lines 0-49
178
+ interactive_shell({ sessionId: "calm-reef", outputOffset: 0, outputLines: 50 })
179
+
180
+ // Read lines 50-99
181
+ interactive_shell({ sessionId: "calm-reef", outputOffset: 50, outputLines: 50 })
182
+ ```
183
+
152
184
  ## How It Works
153
185
 
154
186
  ```
package/index.ts CHANGED
@@ -315,6 +315,8 @@ QUERYING SESSION STATUS:
315
315
  - interactive_shell({ sessionId: "calm-reef" }) - get status + rendered terminal output (default: 20 lines, 5KB)
316
316
  - interactive_shell({ sessionId: "calm-reef", outputLines: 50 }) - get more lines (max: 200)
317
317
  - interactive_shell({ sessionId: "calm-reef", outputMaxChars: 20000 }) - get more content (max: 50KB)
318
+ - interactive_shell({ sessionId: "calm-reef", outputOffset: 0, outputLines: 50 }) - pagination (lines 0-49)
319
+ - interactive_shell({ sessionId: "calm-reef", drain: true }) - only NEW output since last query (token-efficient)
318
320
  - interactive_shell({ sessionId: "calm-reef", kill: true }) - end session
319
321
  - interactive_shell({ sessionId: "calm-reef", input: "..." }) - send input
320
322
 
@@ -374,6 +376,16 @@ Examples:
374
376
  description: "Max chars to return when querying (default: 5KB, max: 50KB)",
375
377
  }),
376
378
  ),
379
+ outputOffset: Type.Optional(
380
+ Type.Number({
381
+ description: "Line offset for pagination (0-indexed). Use with outputLines to read specific ranges.",
382
+ }),
383
+ ),
384
+ drain: Type.Optional(
385
+ Type.Boolean({
386
+ description: "If true, return only NEW output since last query (incremental). More token-efficient for repeated polling.",
387
+ }),
388
+ ),
377
389
  settings: Type.Optional(
378
390
  Type.Object({
379
391
  updateInterval: Type.Optional(
@@ -488,6 +500,8 @@ Examples:
488
500
  kill,
489
501
  outputLines,
490
502
  outputMaxChars,
503
+ outputOffset,
504
+ drain,
491
505
  settings,
492
506
  input,
493
507
  cwd,
@@ -504,6 +518,8 @@ Examples:
504
518
  kill?: boolean;
505
519
  outputLines?: number;
506
520
  outputMaxChars?: number;
521
+ outputOffset?: number;
522
+ drain?: boolean;
507
523
  settings?: { updateInterval?: number; quietThreshold?: number };
508
524
  input?: string | { text?: string; keys?: string[]; hex?: string[]; paste?: string };
509
525
  cwd?: string;
@@ -536,7 +552,7 @@ Examples:
536
552
 
537
553
  // Kill session if requested
538
554
  if (kill) {
539
- const { output, truncated, totalBytes } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars });
555
+ const { output, truncated, totalBytes } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain });
540
556
  const status = session.getStatus();
541
557
  const runtime = session.getRuntime();
542
558
  session.kill();
@@ -619,7 +635,7 @@ Examples:
619
635
  // If session completed, always allow query (no rate limiting)
620
636
  // Rate limiting only applies to "checking in" on running sessions
621
637
  if (result) {
622
- const { output, truncated, totalBytes } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars });
638
+ const { output, truncated, totalBytes } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain });
623
639
  const truncatedNote = truncated ? ` (${totalBytes} bytes total, truncated)` : "";
624
640
  const hasOutput = output.length > 0;
625
641
 
@@ -646,7 +662,7 @@ Examples:
646
662
  }
647
663
 
648
664
  // Session still running - check rate limiting
649
- const outputResult = session.getOutput({ lines: outputLines, maxChars: outputMaxChars });
665
+ const outputResult = session.getOutput({ lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain });
650
666
 
651
667
  // If rate limited, wait until allowed then return fresh result
652
668
  // Use Promise.race to detect if session completes during wait
@@ -669,7 +685,7 @@ Examples:
669
685
  };
670
686
  }
671
687
  const earlyResult = earlySession.getResult();
672
- const { output, truncated, totalBytes } = earlySession.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars });
688
+ const { output, truncated, totalBytes } = earlySession.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain });
673
689
  const earlyStatus = earlySession.getStatus();
674
690
  const earlyRuntime = earlySession.getRuntime();
675
691
  const truncatedNote = truncated ? ` (${totalBytes} bytes total, truncated)` : "";
@@ -718,7 +734,7 @@ Examples:
718
734
  };
719
735
  }
720
736
  // Get fresh output after waiting
721
- const freshOutput = session.getOutput({ lines: outputLines, maxChars: outputMaxChars });
737
+ const freshOutput = session.getOutput({ lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain });
722
738
  const truncatedNote = freshOutput.truncated ? ` (${freshOutput.totalBytes} bytes total, truncated)` : "";
723
739
  const hasOutput = freshOutput.output.length > 0;
724
740
  const freshStatus = session.getStatus();
@@ -256,7 +256,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
256
256
  private static readonly MAX_STATUS_LINES = 200; // 200 lines max
257
257
 
258
258
  /** Get rendered terminal output (last N lines, truncated if too large) */
259
- getOutputSinceLastCheck(options: { skipRateLimit?: boolean; lines?: number; maxChars?: number } | boolean = false): { output: string; truncated: boolean; totalBytes: number; rateLimited?: boolean; waitSeconds?: number } {
259
+ getOutputSinceLastCheck(options: { skipRateLimit?: boolean; lines?: number; maxChars?: number; offset?: number; drain?: boolean } | boolean = false): { output: string; truncated: boolean; totalBytes: number; totalLines?: number; rateLimited?: boolean; waitSeconds?: number } {
260
260
  // Handle legacy boolean parameter
261
261
  const opts = typeof options === "boolean" ? { skipRateLimit: options } : options;
262
262
  const skipRateLimit = opts.skipRateLimit ?? false;
@@ -291,7 +291,40 @@ export class InteractiveShellOverlay implements Component, Focusable {
291
291
  this.lastQueryTime = now;
292
292
  }
293
293
 
294
- // Use rendered terminal output instead of raw stream
294
+ // Drain mode: return only NEW output since last query (incremental)
295
+ // This is more token-efficient than re-reading the tail each time
296
+ if (opts.drain) {
297
+ const newOutput = this.session.getRawStream({ sinceLast: true, stripAnsi: true });
298
+ // Truncate if exceeds maxChars
299
+ const truncated = newOutput.length > requestedMaxChars;
300
+ const output = truncated ? newOutput.slice(-requestedMaxChars) : newOutput;
301
+ return {
302
+ output,
303
+ truncated,
304
+ totalBytes: output.length,
305
+ };
306
+ }
307
+
308
+ // Offset mode: use getLogSlice for pagination through full output
309
+ if (opts.offset !== undefined) {
310
+ const result = this.session.getLogSlice({
311
+ offset: opts.offset,
312
+ limit: requestedLines,
313
+ stripAnsi: true,
314
+ });
315
+ // Apply maxChars limit
316
+ const truncatedByChars = result.slice.length > requestedMaxChars;
317
+ const output = truncatedByChars ? result.slice.slice(0, requestedMaxChars) : result.slice;
318
+ const lineCount = output.split("\n").length;
319
+ return {
320
+ output,
321
+ truncated: truncatedByChars || lineCount >= requestedLines,
322
+ totalBytes: output.length,
323
+ totalLines: result.totalLines,
324
+ };
325
+ }
326
+
327
+ // Default: Use rendered terminal output (tail)
295
328
  // This gives clean, readable content without TUI animation garbage
296
329
  const lines = this.session.getTailLines({
297
330
  lines: requestedLines,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-interactive-shell",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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/pty-session.ts CHANGED
@@ -567,6 +567,64 @@ export class PtyTerminalSession {
567
567
  return output;
568
568
  }
569
569
 
570
+ /**
571
+ * Get a slice of log output with offset/limit pagination.
572
+ * Similar to Clawdbot's sliceLogLines - enables reading specific ranges of output.
573
+ * @param options.offset - Line number to start from (0-indexed). If omitted with limit, returns tail.
574
+ * @param options.limit - Max number of lines to return
575
+ * @param options.stripAnsi - If true, strip ANSI escape codes (default: true)
576
+ */
577
+ getLogSlice(options: { offset?: number; limit?: number; stripAnsi?: boolean } = {}): {
578
+ slice: string;
579
+ totalLines: number;
580
+ totalChars: number;
581
+ } {
582
+ let text = this.rawOutput;
583
+
584
+ // Strip ANSI by default
585
+ if (options.stripAnsi !== false && text) {
586
+ text = stripVTControlCharacters(text);
587
+ }
588
+
589
+ if (!text) {
590
+ return { slice: "", totalLines: 0, totalChars: 0 };
591
+ }
592
+
593
+ // Normalize line endings and split
594
+ const normalized = text.replace(/\r\n/g, "\n");
595
+ const lines = normalized.split("\n");
596
+ // Remove trailing empty line from split
597
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
598
+ lines.pop();
599
+ }
600
+
601
+ const totalLines = lines.length;
602
+ const totalChars = text.length;
603
+
604
+ // Calculate start position
605
+ let start: number;
606
+ if (typeof options.offset === "number" && Number.isFinite(options.offset)) {
607
+ start = Math.max(0, Math.floor(options.offset));
608
+ } else if (options.limit !== undefined) {
609
+ // No offset but limit provided - return tail (last N lines)
610
+ const tailCount = Math.max(0, Math.floor(options.limit));
611
+ start = Math.max(totalLines - tailCount, 0);
612
+ } else {
613
+ start = 0;
614
+ }
615
+
616
+ // Calculate end position
617
+ const end = typeof options.limit === "number" && Number.isFinite(options.limit)
618
+ ? start + Math.max(0, Math.floor(options.limit))
619
+ : undefined;
620
+
621
+ return {
622
+ slice: lines.slice(start, end).join("\n"),
623
+ totalLines,
624
+ totalChars,
625
+ };
626
+ }
627
+
570
628
  scrollUp(lines: number): void {
571
629
  const buffer = this.xterm.buffer.active;
572
630
  const maxScroll = Math.max(0, buffer.length - this.xterm.rows);
@@ -33,6 +33,8 @@ export interface OutputOptions {
33
33
  skipRateLimit?: boolean;
34
34
  lines?: number; // Override default 20 lines
35
35
  maxChars?: number; // Override default 5KB
36
+ offset?: number; // Line offset for pagination (0-indexed)
37
+ drain?: boolean; // If true, return only NEW output since last query (incremental)
36
38
  }
37
39
 
38
40
  export interface ActiveSession {