pi-interactive-shell 0.5.2 → 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 CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to the `pi-interactive-shell` extension will be documented in this file.
4
4
 
5
+ ## [Unreleased]
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
+
20
+ ### Changed
21
+ - Added `pi-package` keyword for npm discoverability (pi v0.50.0 package system)
22
+
5
23
  ## [0.5.2] - 2026-01-23
6
24
 
7
25
  ### Fixed
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+Q | Detach dialog |
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 (recommended for complex tasks)
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.backgrounded) {
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 (result.handoffPreview?.type === "tail" && result.handoffPreview.lines.length > 0) {
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
  }
@@ -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 = "background";
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
- private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "timeout"): InteractiveShellResult["handoffPreview"] | undefined {
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 = "background";
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[] = ["kill", "background", "cancel"];
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("Detach from session:")));
1078
+ footerLines.push(row(accent("Session actions:")));
993
1079
  const opts: Array<{ key: DialogChoice; label: string }> = [
994
- { key: "kill", label: "Kill process" },
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("Shift+Up/Down scroll • Ctrl+Q detachCtrl+C interrupt")));
1100
+ footerLines.push(row(dim("Ctrl+T transfer • Ctrl+Q menuShift+Up/Down scroll")));
1014
1101
  }
1015
1102
 
1016
1103
  while (footerLines.length < FOOTER_LINES) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-interactive-shell",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
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": {
@@ -35,6 +35,7 @@
35
35
  "postinstall": "node ./scripts/fix-spawn-helper.cjs"
36
36
  },
37
37
  "keywords": [
38
+ "pi-package",
38
39
  "pi",
39
40
  "pi-coding-agent",
40
41
  "extension",
@@ -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 = "background";
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
- private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill"): InteractiveShellResult["handoffPreview"] | undefined {
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 = "background";
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[] = ["kill", "background", "cancel"];
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("Detach from session:")));
437
+ footerLines.push(row(accent("Session actions:")));
371
438
  const opts: Array<{ key: DialogChoice; label: string }> = [
372
- { key: "kill", label: "Kill process" },
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("Shift+Up/Down scroll • Ctrl+Q detach")));
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
- - Detach (Ctrl+Q) to kill or run in background
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 = 5;
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