pi-interactive-shell 0.6.1 → 0.6.3
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 +15 -0
- package/README.md +1 -3
- package/index.ts +31 -4
- package/overlay-component.ts +27 -53
- package/package.json +1 -1
- package/reattach-overlay.ts +17 -44
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to the `pi-interactive-shell` extension will be documented i
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.3] - 2026-01-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Garbled output on Ctrl+T transfer** - Transfer and handoff preview captured raw PTY output via `getRawStream()`, which includes every intermediate frame of TUI spinners (e.g., Codex's "Working" spinner produced `WorkingWorking•orking•rking•king•ing...`). Switched both `captureTransferOutput()` and `maybeBuildHandoffPreview()` to use `getTailLines()` which reads from the xterm terminal emulator buffer. The emulator correctly processes carriage returns and cursor movements, so only the final rendered state of each line is captured. Fixed in both `overlay-component.ts` and `reattach-overlay.ts`.
|
|
11
|
+
- **Removed dead code** - Cleaned up unused private fields (`timedOut`, `lastDataTime`) and unreachable method (`getSessionId()`) from `InteractiveShellOverlay`.
|
|
12
|
+
|
|
13
|
+
## [0.6.2] - 2026-01-28
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Ctrl+T transfer now works in hands-free mode** - When using Ctrl+T to transfer output in non-blocking hands-free mode, the captured output is now properly sent back to the main agent using `pi.sendMessage()` with `triggerTurn: true`. Previously, the transfer data was captured but never delivered to the agent because the tool had already returned. The fix uses the event bus pattern to wake the agent with the transferred content.
|
|
17
|
+
- **Race condition when Ctrl+T during polling** - Added guard in `getOutputSinceLastCheck()` to return empty output if the session is finished. This prevents errors when a query races with Ctrl+T transfer (PTY disposed before query completes).
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **New event: `interactive-shell:transfer`** - Emitted via `pi.events` when Ctrl+T transfer occurs, allowing other extensions to hook into transfer events.
|
|
21
|
+
|
|
7
22
|
## [0.6.1] - 2026-01-27
|
|
8
23
|
|
|
9
24
|
### Added
|
package/README.md
CHANGED
|
@@ -25,11 +25,9 @@ Works with any CLI: `vim`, `htop`, `psql`, `ssh`, `docker logs -f`, `npm run dev
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
28
|
+
pi install npm:pi-interactive-shell
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Installs to `~/.pi/agent/extensions/interactive-shell/`.
|
|
32
|
-
|
|
33
31
|
The `interactive-shell` skill is automatically symlinked to `~/.pi/agent/skills/interactive-shell/`.
|
|
34
32
|
|
|
35
33
|
**Requires:** Node.js, build tools for `node-pty` (Xcode CLI tools on macOS).
|
package/index.ts
CHANGED
|
@@ -437,10 +437,37 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
437
437
|
// Handle overlay completion in background (cleanup when user closes)
|
|
438
438
|
overlayPromise.then((result) => {
|
|
439
439
|
overlayOpen = false;
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
if (result.
|
|
443
|
-
|
|
440
|
+
|
|
441
|
+
// Handle Ctrl+T transfer: send output back to main agent
|
|
442
|
+
if (result.transferred) {
|
|
443
|
+
const truncatedNote = result.transferred.truncated
|
|
444
|
+
? ` (truncated from ${result.transferred.totalLines} total lines)`
|
|
445
|
+
: "";
|
|
446
|
+
const content = `Session ${generatedSessionId} output transferred (${result.transferred.lines.length} lines${truncatedNote}):\n\n${result.transferred.lines.join("\n")}`;
|
|
447
|
+
|
|
448
|
+
// Send message with triggerTurn to wake the agent
|
|
449
|
+
pi.sendMessage({
|
|
450
|
+
customType: "interactive-shell-transfer",
|
|
451
|
+
content,
|
|
452
|
+
display: true,
|
|
453
|
+
details: {
|
|
454
|
+
sessionId: generatedSessionId,
|
|
455
|
+
transferred: result.transferred,
|
|
456
|
+
exitCode: result.exitCode,
|
|
457
|
+
signal: result.signal,
|
|
458
|
+
},
|
|
459
|
+
}, { triggerTurn: true });
|
|
460
|
+
|
|
461
|
+
// Emit event for extensions that want to handle transfers
|
|
462
|
+
pi.events.emit("interactive-shell:transfer", {
|
|
463
|
+
sessionId: generatedSessionId,
|
|
464
|
+
transferred: result.transferred,
|
|
465
|
+
exitCode: result.exitCode,
|
|
466
|
+
signal: result.signal,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Unregister session - PTY is disposed, agent has the output via sendMessage
|
|
470
|
+
sessionManager.unregisterActive(generatedSessionId, true);
|
|
444
471
|
}
|
|
445
472
|
}).catch(() => {
|
|
446
473
|
overlayOpen = false;
|
package/overlay-component.ts
CHANGED
|
@@ -43,7 +43,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
43
43
|
private sessionUnregistered = false;
|
|
44
44
|
// Timeout
|
|
45
45
|
private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
46
|
-
private timedOut = false;
|
|
47
46
|
// Prevent double done() calls
|
|
48
47
|
private finished = false;
|
|
49
48
|
// Budget tracking for hands-free updates
|
|
@@ -52,7 +51,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
52
51
|
private currentUpdateInterval: number;
|
|
53
52
|
private currentQuietThreshold: number;
|
|
54
53
|
private updateMode: "on-quiet" | "interval";
|
|
55
|
-
private lastDataTime = 0;
|
|
56
54
|
private quietTimer: ReturnType<typeof setTimeout> | null = null;
|
|
57
55
|
private hasUnsentData = false;
|
|
58
56
|
// Non-blocking mode: track status for agent queries
|
|
@@ -101,7 +99,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
101
99
|
|
|
102
100
|
// Track activity for on-quiet mode
|
|
103
101
|
if (this.state === "hands-free" && this.updateMode === "on-quiet") {
|
|
104
|
-
this.lastDataTime = Date.now();
|
|
105
102
|
this.hasUnsentData = true;
|
|
106
103
|
this.resetQuietTimer();
|
|
107
104
|
}
|
|
@@ -190,6 +187,16 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
190
187
|
|
|
191
188
|
/** Get rendered terminal output (last N lines, truncated if too large) */
|
|
192
189
|
getOutputSinceLastCheck(options: { skipRateLimit?: boolean; lines?: number; maxChars?: number; offset?: number; drain?: boolean; incremental?: boolean } | boolean = false): { output: string; truncated: boolean; totalBytes: number; totalLines?: number; hasMore?: boolean; rateLimited?: boolean; waitSeconds?: number } {
|
|
190
|
+
// Guard: if session is finished (PTY disposed), return empty output
|
|
191
|
+
// This handles race conditions where query arrives after Ctrl+T transfer
|
|
192
|
+
if (this.finished) {
|
|
193
|
+
return {
|
|
194
|
+
output: "",
|
|
195
|
+
truncated: false,
|
|
196
|
+
totalBytes: 0,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
// Handle legacy boolean parameter
|
|
194
201
|
const opts = typeof options === "boolean" ? { skipRateLimit: options } : options;
|
|
195
202
|
const skipRateLimit = opts.skipRateLimit ?? false;
|
|
@@ -357,11 +364,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
357
364
|
}, 16);
|
|
358
365
|
}
|
|
359
366
|
|
|
360
|
-
/** Get the session ID */
|
|
361
|
-
getSessionId(): string | null {
|
|
362
|
-
return this.sessionId;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
367
|
/** Kill the session programmatically */
|
|
366
368
|
killSession(): void {
|
|
367
369
|
if (!this.finished) {
|
|
@@ -616,35 +618,19 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
616
618
|
const maxLines = this.config.transferLines;
|
|
617
619
|
const maxChars = this.config.transferMaxChars;
|
|
618
620
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const allLines = rawOutput.split("\n");
|
|
626
|
-
const totalLines = allLines.length;
|
|
627
|
-
|
|
628
|
-
// Get last N lines, respecting maxChars
|
|
629
|
-
let capturedLines: string[] = [];
|
|
630
|
-
let charCount = 0;
|
|
631
|
-
let truncated = false;
|
|
632
|
-
|
|
633
|
-
for (let i = allLines.length - 1; i >= 0 && capturedLines.length < maxLines; i--) {
|
|
634
|
-
const line = allLines[i]!;
|
|
635
|
-
if (charCount + line.length > maxChars && capturedLines.length > 0) {
|
|
636
|
-
truncated = true;
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
capturedLines.unshift(line);
|
|
640
|
-
charCount += line.length + 1; // +1 for newline
|
|
641
|
-
}
|
|
621
|
+
const result = this.session.getTailLines({
|
|
622
|
+
lines: maxLines,
|
|
623
|
+
ansi: false,
|
|
624
|
+
maxChars,
|
|
625
|
+
});
|
|
642
626
|
|
|
643
|
-
|
|
644
|
-
truncated = true;
|
|
645
|
-
}
|
|
627
|
+
const truncated = result.lines.length < result.totalLinesInBuffer || result.truncatedByChars;
|
|
646
628
|
|
|
647
|
-
return {
|
|
629
|
+
return {
|
|
630
|
+
lines: result.lines,
|
|
631
|
+
totalLines: result.totalLinesInBuffer,
|
|
632
|
+
truncated,
|
|
633
|
+
};
|
|
648
634
|
}
|
|
649
635
|
|
|
650
636
|
private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "timeout" | "transfer"): InteractiveShellResult["handoffPreview"] | undefined {
|
|
@@ -655,24 +641,13 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
655
641
|
const maxChars = this.options.handoffPreviewMaxChars ?? this.config.handoffPreviewMaxChars;
|
|
656
642
|
if (lines <= 0 || maxChars <= 0) return undefined;
|
|
657
643
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const outputLines = rawOutput.split("\n");
|
|
664
|
-
|
|
665
|
-
// Get last N lines, respecting maxChars
|
|
666
|
-
let tail: string[] = [];
|
|
667
|
-
let charCount = 0;
|
|
668
|
-
for (let i = outputLines.length - 1; i >= 0 && tail.length < lines; i--) {
|
|
669
|
-
const line = outputLines[i];
|
|
670
|
-
if (charCount + line.length > maxChars && tail.length > 0) break;
|
|
671
|
-
tail.unshift(line);
|
|
672
|
-
charCount += line.length + 1; // +1 for newline
|
|
673
|
-
}
|
|
644
|
+
const result = this.session.getTailLines({
|
|
645
|
+
lines,
|
|
646
|
+
ansi: false,
|
|
647
|
+
maxChars,
|
|
648
|
+
});
|
|
674
649
|
|
|
675
|
-
return { type: "tail", when, lines:
|
|
650
|
+
return { type: "tail", when, lines: result.lines };
|
|
676
651
|
}
|
|
677
652
|
|
|
678
653
|
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "timeout" | "transfer"): InteractiveShellResult["handoff"] | undefined {
|
|
@@ -874,7 +849,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
874
849
|
}
|
|
875
850
|
|
|
876
851
|
this.stopHandsFreeUpdates();
|
|
877
|
-
this.timedOut = true;
|
|
878
852
|
const handoffPreview = this.maybeBuildHandoffPreview("timeout");
|
|
879
853
|
const handoff = this.maybeWriteHandoffSnapshot("timeout");
|
|
880
854
|
this.session.kill();
|
package/package.json
CHANGED
package/reattach-overlay.ts
CHANGED
|
@@ -106,35 +106,19 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
106
106
|
const maxLines = this.config.transferLines;
|
|
107
107
|
const maxChars = this.config.transferMaxChars;
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const allLines = rawOutput.split("\n");
|
|
116
|
-
const totalLines = allLines.length;
|
|
117
|
-
|
|
118
|
-
// Get last N lines, respecting maxChars
|
|
119
|
-
let capturedLines: string[] = [];
|
|
120
|
-
let charCount = 0;
|
|
121
|
-
let truncated = false;
|
|
122
|
-
|
|
123
|
-
for (let i = allLines.length - 1; i >= 0 && capturedLines.length < maxLines; i--) {
|
|
124
|
-
const line = allLines[i]!;
|
|
125
|
-
if (charCount + line.length > maxChars && capturedLines.length > 0) {
|
|
126
|
-
truncated = true;
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
capturedLines.unshift(line);
|
|
130
|
-
charCount += line.length + 1; // +1 for newline
|
|
131
|
-
}
|
|
109
|
+
const result = this.session.getTailLines({
|
|
110
|
+
lines: maxLines,
|
|
111
|
+
ansi: false,
|
|
112
|
+
maxChars,
|
|
113
|
+
});
|
|
132
114
|
|
|
133
|
-
|
|
134
|
-
truncated = true;
|
|
135
|
-
}
|
|
115
|
+
const truncated = result.lines.length < result.totalLinesInBuffer || result.truncatedByChars;
|
|
136
116
|
|
|
137
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
lines: result.lines,
|
|
119
|
+
totalLines: result.totalLinesInBuffer,
|
|
120
|
+
truncated,
|
|
121
|
+
};
|
|
138
122
|
}
|
|
139
123
|
|
|
140
124
|
private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoffPreview"] | undefined {
|
|
@@ -143,24 +127,13 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
143
127
|
const maxChars = this.config.handoffPreviewMaxChars;
|
|
144
128
|
if (lines <= 0 || maxChars <= 0) return undefined;
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const outputLines = rawOutput.split("\n");
|
|
152
|
-
|
|
153
|
-
// Get last N lines, respecting maxChars
|
|
154
|
-
let tail: string[] = [];
|
|
155
|
-
let charCount = 0;
|
|
156
|
-
for (let i = outputLines.length - 1; i >= 0 && tail.length < lines; i--) {
|
|
157
|
-
const line = outputLines[i];
|
|
158
|
-
if (charCount + line.length > maxChars && tail.length > 0) break;
|
|
159
|
-
tail.unshift(line);
|
|
160
|
-
charCount += line.length + 1; // +1 for newline
|
|
161
|
-
}
|
|
130
|
+
const result = this.session.getTailLines({
|
|
131
|
+
lines,
|
|
132
|
+
ansi: false,
|
|
133
|
+
maxChars,
|
|
134
|
+
});
|
|
162
135
|
|
|
163
|
-
return { type: "tail", when, lines:
|
|
136
|
+
return { type: "tail", when, lines: result.lines };
|
|
164
137
|
}
|
|
165
138
|
|
|
166
139
|
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoff"] | undefined {
|