pi-interactive-shell 0.4.5 → 0.4.6
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 +19 -0
- package/README.md +32 -0
- package/index.ts +21 -5
- package/overlay-component.ts +35 -2
- package/package.json +1 -1
- package/pty-session.ts +58 -0
- package/session-manager.ts +2 -0
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
|
@@ -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();
|
package/overlay-component.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
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);
|
package/session-manager.ts
CHANGED
|
@@ -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 {
|