pi-interactive-shell 0.5.3 → 0.6.0
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 +13 -0
- package/README.md +24 -1
- package/SKILL.md +21 -1
- package/config.ts +9 -0
- package/index.ts +9 -2
- package/overlay-component.ts +96 -9
- package/package.json +1 -1
- package/reattach-overlay.ts +76 -8
- package/tool-schema.ts +2 -1
- package/types.ts +10 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to the `pi-interactive-shell` extension will be documented i
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - 2026-01-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Transfer output to agent (Ctrl+T)** - New action to capture subagent output and send it directly to the main agent. When a subagent finishes work, press Ctrl+T to close the overlay and transfer the output as primary content (not buried in details). The main agent immediately has the subagent's response in context.
|
|
11
|
+
- **Transfer option in Ctrl+Q menu** - "Transfer output to agent" is now the first option in the session menu, making it the default selection.
|
|
12
|
+
- **Configurable transfer settings** - `transferLines` (default: 200, range: 10-1000) and `transferMaxChars` (default: 20KB, range: 1KB-100KB) control how much output is captured.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Ctrl+Q menu redesigned** - Options are now: Transfer output → Run in background → Kill process → Cancel. Transfer is the default selection since it's the most common action when a subagent finishes.
|
|
16
|
+
- **Footer hints updated** - Now shows "Ctrl+T transfer • Ctrl+Q menu" for discoverability.
|
|
17
|
+
|
|
18
|
+
## [0.5.3] - 2026-01-26
|
|
19
|
+
|
|
7
20
|
### Changed
|
|
8
21
|
- Added `pi-package` keyword for npm discoverability (pi v0.50.0 package system)
|
|
9
22
|
|
package/README.md
CHANGED
|
@@ -132,6 +132,24 @@ interactive_shell({ sessionId: "calm-reef", outputLines: 50, incremental: true }
|
|
|
132
132
|
interactive_shell({ sessionId: "calm-reef", drain: true })
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
### Transfer Output to Agent
|
|
136
|
+
|
|
137
|
+
When a subagent finishes work, press **Ctrl+T** to capture its output and send it directly to the main agent:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
[Subagent finishes work]
|
|
141
|
+
↓
|
|
142
|
+
[Press Ctrl+T]
|
|
143
|
+
↓
|
|
144
|
+
[Overlay closes, main agent receives full output]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The main agent then has the subagent's response in context and can continue working with that information.
|
|
148
|
+
|
|
149
|
+
**Configuration:**
|
|
150
|
+
- `transferLines`: Max lines to capture (default: 200)
|
|
151
|
+
- `transferMaxChars`: Max characters (default: 20KB)
|
|
152
|
+
|
|
135
153
|
### Background Sessions
|
|
136
154
|
|
|
137
155
|
1. Ctrl+Q → "Run in background"
|
|
@@ -141,7 +159,8 @@ interactive_shell({ sessionId: "calm-reef", drain: true })
|
|
|
141
159
|
|
|
142
160
|
| Key | Action |
|
|
143
161
|
|-----|--------|
|
|
144
|
-
| Ctrl+
|
|
162
|
+
| Ctrl+T | **Transfer & close** - capture output and send to main agent |
|
|
163
|
+
| Ctrl+Q | Session menu (transfer/background/kill/cancel) |
|
|
145
164
|
| Shift+Up/Down | Scroll history |
|
|
146
165
|
| Any key (hands-free) | Take over control |
|
|
147
166
|
|
|
@@ -158,6 +177,8 @@ Configuration files (project overrides global):
|
|
|
158
177
|
"scrollbackLines": 5000,
|
|
159
178
|
"exitAutoCloseDelay": 10,
|
|
160
179
|
"minQueryIntervalSeconds": 60,
|
|
180
|
+
"transferLines": 200,
|
|
181
|
+
"transferMaxChars": 20000,
|
|
161
182
|
"handsFreeUpdateMode": "on-quiet",
|
|
162
183
|
"handsFreeUpdateInterval": 60000,
|
|
163
184
|
"handsFreeQuietThreshold": 5000,
|
|
@@ -178,6 +199,8 @@ Configuration files (project overrides global):
|
|
|
178
199
|
| `scrollbackLines` | 5000 | Terminal scrollback buffer |
|
|
179
200
|
| `exitAutoCloseDelay` | 10 | Seconds before auto-close after exit |
|
|
180
201
|
| `minQueryIntervalSeconds` | 60 | Rate limit between agent queries |
|
|
202
|
+
| `transferLines` | 200 | Lines to capture on Ctrl+T transfer (10-1000) |
|
|
203
|
+
| `transferMaxChars` | 20000 | Max chars for transfer (1KB-100KB) |
|
|
181
204
|
| `handsFreeUpdateMode` | "on-quiet" | "on-quiet" or "interval" |
|
|
182
205
|
| `handsFreeQuietThreshold` | 5000 | Silence duration before update (ms) |
|
|
183
206
|
| `handsFreeUpdateInterval` | 60000 | Max interval between updates (ms) |
|
package/SKILL.md
CHANGED
|
@@ -340,6 +340,26 @@ interactive_shell({ command: 'claude "Review the codebase"', reason: 'Code revie
|
|
|
340
340
|
|
|
341
341
|
## Handoff Options
|
|
342
342
|
|
|
343
|
+
### Transfer (Ctrl+T) - Recommended
|
|
344
|
+
When the subagent finishes, the user presses **Ctrl+T** to transfer output directly to you:
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
[Subagent finishes work in overlay]
|
|
348
|
+
↓
|
|
349
|
+
[User presses Ctrl+T]
|
|
350
|
+
↓
|
|
351
|
+
[You receive: "Session output transferred (150 lines):
|
|
352
|
+
|
|
353
|
+
Completing skill integration...
|
|
354
|
+
Modified files:
|
|
355
|
+
- skills.ts
|
|
356
|
+
- agents/types/..."]
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
This is the cleanest workflow - the subagent's response becomes your context automatically.
|
|
360
|
+
|
|
361
|
+
**Configuration:** `transferLines` (default: 200), `transferMaxChars` (default: 20KB)
|
|
362
|
+
|
|
343
363
|
### Tail Preview (default)
|
|
344
364
|
Last 30 lines included in tool result. Good for seeing errors/final status.
|
|
345
365
|
|
|
@@ -352,7 +372,7 @@ interactive_shell({
|
|
|
352
372
|
})
|
|
353
373
|
```
|
|
354
374
|
|
|
355
|
-
### Artifact Handoff (
|
|
375
|
+
### Artifact Handoff (for complex tasks)
|
|
356
376
|
Instruct the delegated agent to write a handoff file:
|
|
357
377
|
```
|
|
358
378
|
Write your findings to .pi/delegation/claude-handoff.md including:
|
package/config.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface InteractiveShellConfig {
|
|
|
14
14
|
handoffSnapshotEnabled: boolean;
|
|
15
15
|
handoffSnapshotLines: number;
|
|
16
16
|
handoffSnapshotMaxChars: number;
|
|
17
|
+
// Transfer output settings (Ctrl+T)
|
|
18
|
+
transferLines: number;
|
|
19
|
+
transferMaxChars: number;
|
|
17
20
|
// Hands-free mode defaults
|
|
18
21
|
handsFreeUpdateMode: "on-quiet" | "interval";
|
|
19
22
|
handsFreeUpdateInterval: number;
|
|
@@ -36,6 +39,9 @@ const DEFAULT_CONFIG: InteractiveShellConfig = {
|
|
|
36
39
|
handoffSnapshotEnabled: false,
|
|
37
40
|
handoffSnapshotLines: 200,
|
|
38
41
|
handoffSnapshotMaxChars: 12000,
|
|
42
|
+
// Transfer output settings (Ctrl+T) - generous defaults for full context transfer
|
|
43
|
+
transferLines: 200,
|
|
44
|
+
transferMaxChars: 20000,
|
|
39
45
|
// Hands-free mode defaults
|
|
40
46
|
handsFreeUpdateMode: "on-quiet" as const,
|
|
41
47
|
handsFreeUpdateInterval: 60000,
|
|
@@ -94,6 +100,9 @@ export function loadConfig(cwd: string): InteractiveShellConfig {
|
|
|
94
100
|
0,
|
|
95
101
|
200000,
|
|
96
102
|
),
|
|
103
|
+
// Transfer output settings (Ctrl+T)
|
|
104
|
+
transferLines: clampInt(merged.transferLines, DEFAULT_CONFIG.transferLines, 10, 1000),
|
|
105
|
+
transferMaxChars: clampInt(merged.transferMaxChars, DEFAULT_CONFIG.transferMaxChars, 1000, 100000),
|
|
97
106
|
// Hands-free mode
|
|
98
107
|
handsFreeUpdateMode: merged.handsFreeUpdateMode === "interval" ? "interval" : "on-quiet",
|
|
99
108
|
handsFreeUpdateInterval: clampInt(
|
package/index.ts
CHANGED
|
@@ -557,7 +557,13 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
557
557
|
}
|
|
558
558
|
|
|
559
559
|
let summary: string;
|
|
560
|
-
if (result.
|
|
560
|
+
if (result.transferred) {
|
|
561
|
+
// User triggered "Transfer" action - output is the primary content
|
|
562
|
+
const truncatedNote = result.transferred.truncated
|
|
563
|
+
? ` (truncated from ${result.transferred.totalLines} total lines)`
|
|
564
|
+
: "";
|
|
565
|
+
summary = `Session output transferred (${result.transferred.lines.length} lines${truncatedNote}):\n\n${result.transferred.lines.join("\n")}`;
|
|
566
|
+
} else if (result.backgrounded) {
|
|
561
567
|
summary = `Session running in background (id: ${result.backgroundId}). User can reattach with /attach ${result.backgroundId}`;
|
|
562
568
|
} else if (result.cancelled) {
|
|
563
569
|
summary = "User killed the interactive session";
|
|
@@ -577,7 +583,8 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
577
583
|
summary += `\n\n${warning}`;
|
|
578
584
|
}
|
|
579
585
|
|
|
580
|
-
if
|
|
586
|
+
// Only include handoff preview if not already transferring (transfer includes full output)
|
|
587
|
+
if (!result.transferred && result.handoffPreview?.type === "tail" && result.handoffPreview.lines.length > 0) {
|
|
581
588
|
const tailHeader = `\n\nOverlay tail (${result.handoffPreview.when}, last ${result.handoffPreview.lines.length} lines):\n`;
|
|
582
589
|
summary += tailHeader + result.handoffPreview.lines.join("\n");
|
|
583
590
|
}
|
package/overlay-component.ts
CHANGED
|
@@ -29,7 +29,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
29
29
|
private config: InteractiveShellConfig;
|
|
30
30
|
|
|
31
31
|
private state: OverlayState = "running";
|
|
32
|
-
private dialogSelection: DialogChoice = "
|
|
32
|
+
private dialogSelection: DialogChoice = "transfer";
|
|
33
33
|
private exitCountdown = 0;
|
|
34
34
|
private countdownInterval: ReturnType<typeof setInterval> | null = null;
|
|
35
35
|
private lastWidth = 0;
|
|
@@ -611,7 +611,43 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
611
611
|
this.tui.requestRender();
|
|
612
612
|
}
|
|
613
613
|
|
|
614
|
-
|
|
614
|
+
/** Capture output for transfer action (Ctrl+T or dialog) */
|
|
615
|
+
private captureTransferOutput(): InteractiveShellResult["transferred"] {
|
|
616
|
+
const maxLines = this.config.transferLines;
|
|
617
|
+
const maxChars = this.config.transferMaxChars;
|
|
618
|
+
|
|
619
|
+
// Use raw output stream for clean content
|
|
620
|
+
const rawOutput = this.session.getRawStream({ stripAnsi: true });
|
|
621
|
+
if (!rawOutput) {
|
|
622
|
+
return { lines: [], totalLines: 0, truncated: false };
|
|
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
|
+
}
|
|
642
|
+
|
|
643
|
+
if (capturedLines.length < totalLines) {
|
|
644
|
+
truncated = true;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return { lines: capturedLines, totalLines, truncated };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "timeout" | "transfer"): InteractiveShellResult["handoffPreview"] | undefined {
|
|
615
651
|
const enabled = this.options.handoffPreviewEnabled ?? this.config.handoffPreviewEnabled;
|
|
616
652
|
if (!enabled) return undefined;
|
|
617
653
|
|
|
@@ -639,7 +675,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
639
675
|
return { type: "tail", when, lines: tail };
|
|
640
676
|
}
|
|
641
677
|
|
|
642
|
-
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "timeout"): InteractiveShellResult["handoff"] | undefined {
|
|
678
|
+
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "timeout" | "transfer"): InteractiveShellResult["handoff"] | undefined {
|
|
643
679
|
const enabled = this.options.handoffSnapshotEnabled ?? this.config.handoffSnapshotEnabled;
|
|
644
680
|
if (!enabled) return undefined;
|
|
645
681
|
|
|
@@ -775,6 +811,43 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
775
811
|
this.done(result);
|
|
776
812
|
}
|
|
777
813
|
|
|
814
|
+
private finishWithTransfer(): void {
|
|
815
|
+
if (this.finished) return;
|
|
816
|
+
this.finished = true;
|
|
817
|
+
this.stopCountdown();
|
|
818
|
+
this.stopTimeout();
|
|
819
|
+
this.stopHandsFreeUpdates();
|
|
820
|
+
|
|
821
|
+
// Capture output BEFORE killing the session
|
|
822
|
+
const transferred = this.captureTransferOutput();
|
|
823
|
+
const handoffPreview = this.maybeBuildHandoffPreview("transfer");
|
|
824
|
+
const handoff = this.maybeWriteHandoffSnapshot("transfer");
|
|
825
|
+
|
|
826
|
+
this.session.kill();
|
|
827
|
+
this.session.dispose();
|
|
828
|
+
const result: InteractiveShellResult = {
|
|
829
|
+
exitCode: this.session.exitCode,
|
|
830
|
+
signal: this.session.signal,
|
|
831
|
+
backgrounded: false,
|
|
832
|
+
cancelled: false,
|
|
833
|
+
sessionId: this.sessionId ?? undefined,
|
|
834
|
+
userTookOver: this.userTookOver,
|
|
835
|
+
transferred,
|
|
836
|
+
handoffPreview,
|
|
837
|
+
handoff,
|
|
838
|
+
};
|
|
839
|
+
this.completionResult = result;
|
|
840
|
+
this.triggerCompleteCallbacks();
|
|
841
|
+
|
|
842
|
+
// In non-blocking mode (no onHandsFreeUpdate), keep session registered
|
|
843
|
+
// so agent can query completion result. Agent's query will unregister.
|
|
844
|
+
if (this.options.onHandsFreeUpdate) {
|
|
845
|
+
this.unregisterActiveSession(true);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
this.done(result);
|
|
849
|
+
}
|
|
850
|
+
|
|
778
851
|
private finishWithTimeout(): void {
|
|
779
852
|
if (this.finished) return;
|
|
780
853
|
this.finished = true;
|
|
@@ -834,6 +907,16 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
834
907
|
return;
|
|
835
908
|
}
|
|
836
909
|
|
|
910
|
+
// Ctrl+T: Quick transfer - capture output and close (works in all states including "exited")
|
|
911
|
+
if (matchesKey(data, "ctrl+t")) {
|
|
912
|
+
// If in hands-free mode, trigger takeover first (notifies agent)
|
|
913
|
+
if (this.state === "hands-free") {
|
|
914
|
+
this.triggerUserTakeover();
|
|
915
|
+
}
|
|
916
|
+
this.finishWithTransfer();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
837
920
|
if (this.state === "exited") {
|
|
838
921
|
if (data.length > 0) {
|
|
839
922
|
this.finishWithExit();
|
|
@@ -848,7 +931,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
848
931
|
this.triggerUserTakeover();
|
|
849
932
|
}
|
|
850
933
|
this.state = "detach-dialog";
|
|
851
|
-
this.dialogSelection = "
|
|
934
|
+
this.dialogSelection = "transfer";
|
|
852
935
|
this.tui.requestRender();
|
|
853
936
|
return;
|
|
854
937
|
}
|
|
@@ -882,7 +965,7 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
882
965
|
}
|
|
883
966
|
|
|
884
967
|
if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
885
|
-
const options: DialogChoice[] = ["
|
|
968
|
+
const options: DialogChoice[] = ["transfer", "background", "kill", "cancel"];
|
|
886
969
|
const currentIdx = options.indexOf(this.dialogSelection);
|
|
887
970
|
const direction = matchesKey(data, "up") ? -1 : 1;
|
|
888
971
|
const newIdx = (currentIdx + direction + options.length) % options.length;
|
|
@@ -893,6 +976,9 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
893
976
|
|
|
894
977
|
if (matchesKey(data, "enter")) {
|
|
895
978
|
switch (this.dialogSelection) {
|
|
979
|
+
case "transfer":
|
|
980
|
+
this.finishWithTransfer();
|
|
981
|
+
break;
|
|
896
982
|
case "kill":
|
|
897
983
|
this.finishWithKill();
|
|
898
984
|
break;
|
|
@@ -989,10 +1075,11 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
989
1075
|
const footerLines: string[] = [];
|
|
990
1076
|
|
|
991
1077
|
if (this.state === "detach-dialog") {
|
|
992
|
-
footerLines.push(row(accent("
|
|
1078
|
+
footerLines.push(row(accent("Session actions:")));
|
|
993
1079
|
const opts: Array<{ key: DialogChoice; label: string }> = [
|
|
994
|
-
{ key: "
|
|
1080
|
+
{ key: "transfer", label: "Transfer output to agent" },
|
|
995
1081
|
{ key: "background", label: "Run in background" },
|
|
1082
|
+
{ key: "kill", label: "Kill process" },
|
|
996
1083
|
{ key: "cancel", label: "Cancel (return to session)" },
|
|
997
1084
|
];
|
|
998
1085
|
for (const opt of opts) {
|
|
@@ -1008,9 +1095,9 @@ export class InteractiveShellOverlay implements Component, Focusable {
|
|
|
1008
1095
|
footerLines.push(row(exitMsg));
|
|
1009
1096
|
footerLines.push(row(dim(`Closing in ${this.exitCountdown}s... (any key to close)`)));
|
|
1010
1097
|
} else if (this.state === "hands-free") {
|
|
1011
|
-
footerLines.push(row(dim("🤖 Agent controlling • Type to take over • Shift+Up/Down scroll")));
|
|
1098
|
+
footerLines.push(row(dim("🤖 Agent controlling • Type to take over • Ctrl+T transfer • Shift+Up/Down scroll")));
|
|
1012
1099
|
} else {
|
|
1013
|
-
footerLines.push(row(dim("
|
|
1100
|
+
footerLines.push(row(dim("Ctrl+T transfer • Ctrl+Q menu • Shift+Up/Down scroll")));
|
|
1014
1101
|
}
|
|
1015
1102
|
|
|
1016
1103
|
while (footerLines.length < FOOTER_LINES) {
|
package/package.json
CHANGED
package/reattach-overlay.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
25
25
|
private config: InteractiveShellConfig;
|
|
26
26
|
|
|
27
27
|
private state: OverlayState = "running";
|
|
28
|
-
private dialogSelection: DialogChoice = "
|
|
28
|
+
private dialogSelection: DialogChoice = "transfer";
|
|
29
29
|
private exitCountdown = 0;
|
|
30
30
|
private countdownInterval: ReturnType<typeof setInterval> | null = null;
|
|
31
31
|
private initialExitTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -101,7 +101,43 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
/** Capture output for transfer action (Ctrl+T or dialog) */
|
|
105
|
+
private captureTransferOutput(): InteractiveShellResult["transferred"] {
|
|
106
|
+
const maxLines = this.config.transferLines;
|
|
107
|
+
const maxChars = this.config.transferMaxChars;
|
|
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
|
+
}
|
|
132
|
+
|
|
133
|
+
if (capturedLines.length < totalLines) {
|
|
134
|
+
truncated = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { lines: capturedLines, totalLines, truncated };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoffPreview"] | undefined {
|
|
105
141
|
if (!this.config.handoffPreviewEnabled) return undefined;
|
|
106
142
|
const lines = this.config.handoffPreviewLines;
|
|
107
143
|
const maxChars = this.config.handoffPreviewMaxChars;
|
|
@@ -127,7 +163,7 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
127
163
|
return { type: "tail", when, lines: tail };
|
|
128
164
|
}
|
|
129
165
|
|
|
130
|
-
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill"): InteractiveShellResult["handoff"] | undefined {
|
|
166
|
+
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoff"] | undefined {
|
|
131
167
|
if (!this.config.handoffSnapshotEnabled) return undefined;
|
|
132
168
|
const lines = this.config.handoffSnapshotLines;
|
|
133
169
|
const maxChars = this.config.handoffSnapshotMaxChars;
|
|
@@ -213,12 +249,40 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
213
249
|
});
|
|
214
250
|
}
|
|
215
251
|
|
|
252
|
+
private finishWithTransfer(): void {
|
|
253
|
+
if (this.finished) return;
|
|
254
|
+
this.finished = true;
|
|
255
|
+
this.stopCountdown();
|
|
256
|
+
|
|
257
|
+
// Capture output BEFORE removing session
|
|
258
|
+
const transferred = this.captureTransferOutput();
|
|
259
|
+
const handoffPreview = this.maybeBuildHandoffPreview("transfer");
|
|
260
|
+
const handoff = this.maybeWriteHandoffSnapshot("transfer");
|
|
261
|
+
|
|
262
|
+
sessionManager.remove(this.bgSession.id);
|
|
263
|
+
this.done({
|
|
264
|
+
exitCode: this.session.exitCode,
|
|
265
|
+
signal: this.session.signal,
|
|
266
|
+
backgrounded: false,
|
|
267
|
+
cancelled: false,
|
|
268
|
+
transferred,
|
|
269
|
+
handoffPreview,
|
|
270
|
+
handoff,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
216
274
|
handleInput(data: string): void {
|
|
217
275
|
if (this.state === "detach-dialog") {
|
|
218
276
|
this.handleDialogInput(data);
|
|
219
277
|
return;
|
|
220
278
|
}
|
|
221
279
|
|
|
280
|
+
// Ctrl+T: Quick transfer - capture output and close (works in all states including "exited")
|
|
281
|
+
if (matchesKey(data, "ctrl+t")) {
|
|
282
|
+
this.finishWithTransfer();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
222
286
|
if (this.state === "exited") {
|
|
223
287
|
if (data.length > 0) {
|
|
224
288
|
this.finishAndClose();
|
|
@@ -237,7 +301,7 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
237
301
|
// Ctrl+Q opens detach dialog
|
|
238
302
|
if (matchesKey(data, "ctrl+q")) {
|
|
239
303
|
this.state = "detach-dialog";
|
|
240
|
-
this.dialogSelection = "
|
|
304
|
+
this.dialogSelection = "transfer";
|
|
241
305
|
this.tui.requestRender();
|
|
242
306
|
return;
|
|
243
307
|
}
|
|
@@ -264,7 +328,7 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
264
328
|
}
|
|
265
329
|
|
|
266
330
|
if (matchesKey(data, "up") || matchesKey(data, "down")) {
|
|
267
|
-
const options: DialogChoice[] = ["
|
|
331
|
+
const options: DialogChoice[] = ["transfer", "background", "kill", "cancel"];
|
|
268
332
|
const currentIdx = options.indexOf(this.dialogSelection);
|
|
269
333
|
const direction = matchesKey(data, "up") ? -1 : 1;
|
|
270
334
|
const newIdx = (currentIdx + direction + options.length) % options.length;
|
|
@@ -275,6 +339,9 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
275
339
|
|
|
276
340
|
if (matchesKey(data, "enter")) {
|
|
277
341
|
switch (this.dialogSelection) {
|
|
342
|
+
case "transfer":
|
|
343
|
+
this.finishWithTransfer();
|
|
344
|
+
break;
|
|
278
345
|
case "kill":
|
|
279
346
|
this.finishWithKill();
|
|
280
347
|
break;
|
|
@@ -367,10 +434,11 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
367
434
|
const footerLines: string[] = [];
|
|
368
435
|
|
|
369
436
|
if (this.state === "detach-dialog") {
|
|
370
|
-
footerLines.push(row(accent("
|
|
437
|
+
footerLines.push(row(accent("Session actions:")));
|
|
371
438
|
const opts: Array<{ key: DialogChoice; label: string }> = [
|
|
372
|
-
{ key: "
|
|
439
|
+
{ key: "transfer", label: "Transfer output to agent" },
|
|
373
440
|
{ key: "background", label: "Run in background" },
|
|
441
|
+
{ key: "kill", label: "Kill process" },
|
|
374
442
|
{ key: "cancel", label: "Cancel (return to session)" },
|
|
375
443
|
];
|
|
376
444
|
for (const opt of opts) {
|
|
@@ -386,7 +454,7 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
386
454
|
footerLines.push(row(exitMsg));
|
|
387
455
|
footerLines.push(row(dim(`Closing in ${this.exitCountdown}s... (any key to close)`)));
|
|
388
456
|
} else {
|
|
389
|
-
footerLines.push(row(dim("
|
|
457
|
+
footerLines.push(row(dim("Ctrl+T transfer • Ctrl+Q menu • Shift+Up/Down scroll")));
|
|
390
458
|
}
|
|
391
459
|
|
|
392
460
|
while (footerLines.length < FOOTER_LINES) {
|
package/tool-schema.ts
CHANGED
|
@@ -16,7 +16,8 @@ MODES:
|
|
|
16
16
|
The user will see the process in an overlay. They can:
|
|
17
17
|
- Watch output in real-time
|
|
18
18
|
- Scroll through output (Shift+Up/Down)
|
|
19
|
-
-
|
|
19
|
+
- Transfer output to you (Ctrl+T) - closes overlay and sends output as your context
|
|
20
|
+
- Detach (Ctrl+Q) for menu: transfer/background/kill
|
|
20
21
|
- In hands-free mode: type anything to take over control
|
|
21
22
|
|
|
22
23
|
HANDS-FREE MODE (NON-BLOCKING):
|
package/types.ts
CHANGED
|
@@ -11,14 +11,20 @@ export interface InteractiveShellResult {
|
|
|
11
11
|
timedOut?: boolean;
|
|
12
12
|
sessionId?: string;
|
|
13
13
|
userTookOver?: boolean;
|
|
14
|
+
/** When user triggers "Transfer" action, this contains the captured output */
|
|
15
|
+
transferred?: {
|
|
16
|
+
lines: string[];
|
|
17
|
+
totalLines: number;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
};
|
|
14
20
|
handoffPreview?: {
|
|
15
21
|
type: "tail";
|
|
16
|
-
when: "exit" | "detach" | "kill" | "timeout";
|
|
22
|
+
when: "exit" | "detach" | "kill" | "timeout" | "transfer";
|
|
17
23
|
lines: string[];
|
|
18
24
|
};
|
|
19
25
|
handoff?: {
|
|
20
26
|
type: "snapshot";
|
|
21
|
-
when: "exit" | "detach" | "kill" | "timeout";
|
|
27
|
+
when: "exit" | "detach" | "kill" | "timeout" | "transfer";
|
|
22
28
|
transcriptPath: string;
|
|
23
29
|
linesWritten: number;
|
|
24
30
|
};
|
|
@@ -62,11 +68,11 @@ export interface InteractiveShellOptions {
|
|
|
62
68
|
timeout?: number;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
export type DialogChoice = "kill" | "background" | "cancel";
|
|
71
|
+
export type DialogChoice = "kill" | "background" | "transfer" | "cancel";
|
|
66
72
|
export type OverlayState = "running" | "exited" | "detach-dialog" | "hands-free";
|
|
67
73
|
|
|
68
74
|
// UI constants
|
|
69
|
-
export const FOOTER_LINES =
|
|
75
|
+
export const FOOTER_LINES = 6;
|
|
70
76
|
export const HEADER_LINES = 4;
|
|
71
77
|
export const CHROME_LINES = HEADER_LINES + FOOTER_LINES + 2;
|
|
72
78
|
|