pi-interactive-shell 0.6.2 → 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 CHANGED
@@ -4,6 +4,12 @@ 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
+
7
13
  ## [0.6.2] - 2026-01-28
8
14
 
9
15
  ### Fixed
@@ -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
  }
@@ -367,11 +364,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
367
364
  }, 16);
368
365
  }
369
366
 
370
- /** Get the session ID */
371
- getSessionId(): string | null {
372
- return this.sessionId;
373
- }
374
-
375
367
  /** Kill the session programmatically */
376
368
  killSession(): void {
377
369
  if (!this.finished) {
@@ -626,35 +618,19 @@ export class InteractiveShellOverlay implements Component, Focusable {
626
618
  const maxLines = this.config.transferLines;
627
619
  const maxChars = this.config.transferMaxChars;
628
620
 
629
- // Use raw output stream for clean content
630
- const rawOutput = this.session.getRawStream({ stripAnsi: true });
631
- if (!rawOutput) {
632
- return { lines: [], totalLines: 0, truncated: false };
633
- }
634
-
635
- const allLines = rawOutput.split("\n");
636
- const totalLines = allLines.length;
637
-
638
- // Get last N lines, respecting maxChars
639
- let capturedLines: string[] = [];
640
- let charCount = 0;
641
- let truncated = false;
642
-
643
- for (let i = allLines.length - 1; i >= 0 && capturedLines.length < maxLines; i--) {
644
- const line = allLines[i]!;
645
- if (charCount + line.length > maxChars && capturedLines.length > 0) {
646
- truncated = true;
647
- break;
648
- }
649
- capturedLines.unshift(line);
650
- charCount += line.length + 1; // +1 for newline
651
- }
621
+ const result = this.session.getTailLines({
622
+ lines: maxLines,
623
+ ansi: false,
624
+ maxChars,
625
+ });
652
626
 
653
- if (capturedLines.length < totalLines) {
654
- truncated = true;
655
- }
627
+ const truncated = result.lines.length < result.totalLinesInBuffer || result.truncatedByChars;
656
628
 
657
- return { lines: capturedLines, totalLines, truncated };
629
+ return {
630
+ lines: result.lines,
631
+ totalLines: result.totalLinesInBuffer,
632
+ truncated,
633
+ };
658
634
  }
659
635
 
660
636
  private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "timeout" | "transfer"): InteractiveShellResult["handoffPreview"] | undefined {
@@ -665,24 +641,13 @@ export class InteractiveShellOverlay implements Component, Focusable {
665
641
  const maxChars = this.options.handoffPreviewMaxChars ?? this.config.handoffPreviewMaxChars;
666
642
  if (lines <= 0 || maxChars <= 0) return undefined;
667
643
 
668
- // Use raw output stream instead of xterm buffer - TUI apps using alternate
669
- // screen buffer can have misleading content in getTailLines()
670
- const rawOutput = this.session.getRawStream({ stripAnsi: true });
671
- if (!rawOutput) return { type: "tail", when, lines: [] };
672
-
673
- const outputLines = rawOutput.split("\n");
674
-
675
- // Get last N lines, respecting maxChars
676
- let tail: string[] = [];
677
- let charCount = 0;
678
- for (let i = outputLines.length - 1; i >= 0 && tail.length < lines; i--) {
679
- const line = outputLines[i];
680
- if (charCount + line.length > maxChars && tail.length > 0) break;
681
- tail.unshift(line);
682
- charCount += line.length + 1; // +1 for newline
683
- }
644
+ const result = this.session.getTailLines({
645
+ lines,
646
+ ansi: false,
647
+ maxChars,
648
+ });
684
649
 
685
- return { type: "tail", when, lines: tail };
650
+ return { type: "tail", when, lines: result.lines };
686
651
  }
687
652
 
688
653
  private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "timeout" | "transfer"): InteractiveShellResult["handoff"] | undefined {
@@ -884,7 +849,6 @@ export class InteractiveShellOverlay implements Component, Focusable {
884
849
  }
885
850
 
886
851
  this.stopHandsFreeUpdates();
887
- this.timedOut = true;
888
852
  const handoffPreview = this.maybeBuildHandoffPreview("timeout");
889
853
  const handoff = this.maybeWriteHandoffSnapshot("timeout");
890
854
  this.session.kill();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-interactive-shell",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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": {
@@ -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
- // Use raw output stream for clean content
110
- const rawOutput = this.session.getRawStream({ stripAnsi: true });
111
- if (!rawOutput) {
112
- return { lines: [], totalLines: 0, truncated: false };
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
- if (capturedLines.length < totalLines) {
134
- truncated = true;
135
- }
115
+ const truncated = result.lines.length < result.totalLinesInBuffer || result.truncatedByChars;
136
116
 
137
- return { lines: capturedLines, totalLines, truncated };
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
- // Use raw output stream instead of xterm buffer - TUI apps using alternate
147
- // screen buffer can have misleading content in getTailLines()
148
- const rawOutput = this.session.getRawStream({ stripAnsi: true });
149
- if (!rawOutput) return { type: "tail", when, lines: [] };
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: tail };
136
+ return { type: "tail", when, lines: result.lines };
164
137
  }
165
138
 
166
139
  private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoff"] | undefined {