pi-interactive-shell 0.9.0 → 0.10.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 +27 -0
- package/README.md +21 -13
- package/SKILL.md +2 -2
- package/background-widget.ts +76 -0
- package/examples/prompts/codex-implement-plan.md +11 -3
- package/examples/prompts/codex-review-impl.md +11 -3
- package/examples/prompts/codex-review-plan.md +11 -3
- package/examples/skills/{codex-5.3-prompting → codex-5-3-prompting}/SKILL.md +1 -1
- package/examples/skills/codex-cli/SKILL.md +11 -5
- package/examples/skills/gpt-5-4-prompting/SKILL.md +202 -0
- package/handoff-utils.ts +92 -0
- package/headless-monitor.ts +6 -1
- package/index.ts +231 -416
- package/notification-utils.ts +134 -0
- package/overlay-component.ts +14 -213
- package/package.json +26 -6
- package/pty-log.ts +59 -0
- package/pty-protocol.ts +33 -0
- package/pty-session.ts +11 -134
- package/reattach-overlay.ts +5 -74
- package/runtime-coordinator.ts +69 -0
- package/scripts/install.js +5 -1
- package/session-manager.ts +21 -11
- package/session-query.ts +170 -0
- package/spawn-helper.ts +37 -0
- package/types.ts +3 -0
package/index.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
3
2
|
import { InteractiveShellOverlay } from "./overlay-component.js";
|
|
4
3
|
import { ReattachOverlay } from "./reattach-overlay.js";
|
|
5
4
|
import { PtyTerminalSession } from "./pty-session.js";
|
|
6
5
|
import type { InteractiveShellResult, HandsFreeUpdate } from "./types.js";
|
|
7
|
-
import { sessionManager, generateSessionId
|
|
8
|
-
import type { OutputOptions, OutputResult } from "./session-manager.js";
|
|
6
|
+
import { sessionManager, generateSessionId } from "./session-manager.js";
|
|
9
7
|
import { loadConfig } from "./config.js";
|
|
10
8
|
import type { InteractiveShellConfig } from "./config.js";
|
|
11
9
|
import { translateInput } from "./key-encoding.js";
|
|
@@ -13,77 +11,12 @@ import { TOOL_NAME, TOOL_LABEL, TOOL_DESCRIPTION, toolParameters, type ToolParam
|
|
|
13
11
|
import { formatDuration, formatDurationMs } from "./types.js";
|
|
14
12
|
import { HeadlessDispatchMonitor } from "./headless-monitor.js";
|
|
15
13
|
import type { HeadlessCompletionInfo } from "./headless-monitor.js";
|
|
14
|
+
import { setupBackgroundWidget } from "./background-widget.js";
|
|
15
|
+
import { buildDispatchNotification, buildHandsFreeUpdateMessage, buildResultNotification, summarizeInteractiveResult } from "./notification-utils.js";
|
|
16
|
+
import { createSessionQueryState, getSessionOutput } from "./session-query.js";
|
|
17
|
+
import { InteractiveShellCoordinator } from "./runtime-coordinator.js";
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
let agentHandledCompletion = false;
|
|
19
|
-
const headlessMonitors = new Map<string, HeadlessDispatchMonitor>();
|
|
20
|
-
|
|
21
|
-
function getHeadlessOutput(session: PtyTerminalSession, opts?: OutputOptions | boolean): OutputResult {
|
|
22
|
-
const options = typeof opts === "boolean" ? {} : (opts ?? {});
|
|
23
|
-
const lines = options.lines ?? 20;
|
|
24
|
-
const maxChars = options.maxChars ?? 5 * 1024;
|
|
25
|
-
try {
|
|
26
|
-
const result = session.getTailLines({ lines, ansi: false, maxChars });
|
|
27
|
-
const output = result.lines.join("\n");
|
|
28
|
-
return {
|
|
29
|
-
output,
|
|
30
|
-
truncated: result.lines.length < result.totalLinesInBuffer || result.truncatedByChars,
|
|
31
|
-
totalBytes: output.length,
|
|
32
|
-
totalLines: result.totalLinesInBuffer,
|
|
33
|
-
};
|
|
34
|
-
} catch {
|
|
35
|
-
return { output: "", truncated: false, totalBytes: 0 };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const BRIEF_TAIL_LINES = 5;
|
|
40
|
-
|
|
41
|
-
function buildDispatchNotification(sessionId: string, info: HeadlessCompletionInfo, duration: string): string {
|
|
42
|
-
const parts: string[] = [];
|
|
43
|
-
if (info.timedOut) {
|
|
44
|
-
parts.push(`Session ${sessionId} timed out (${duration}).`);
|
|
45
|
-
} else if (info.cancelled) {
|
|
46
|
-
parts.push(`Session ${sessionId} completed (${duration}).`);
|
|
47
|
-
} else if (info.exitCode === 0) {
|
|
48
|
-
parts.push(`Session ${sessionId} completed successfully (${duration}).`);
|
|
49
|
-
} else {
|
|
50
|
-
parts.push(`Session ${sessionId} exited with code ${info.exitCode} (${duration}).`);
|
|
51
|
-
}
|
|
52
|
-
if (info.completionOutput && info.completionOutput.totalLines > 0) {
|
|
53
|
-
parts.push(` ${info.completionOutput.totalLines} lines of output.`);
|
|
54
|
-
}
|
|
55
|
-
if (info.completionOutput && info.completionOutput.lines.length > 0) {
|
|
56
|
-
const allLines = info.completionOutput.lines;
|
|
57
|
-
let end = allLines.length;
|
|
58
|
-
while (end > 0 && allLines[end - 1].trim() === "") end--;
|
|
59
|
-
const tail = allLines.slice(Math.max(0, end - BRIEF_TAIL_LINES), end);
|
|
60
|
-
if (tail.length > 0) {
|
|
61
|
-
parts.push(`\n\n${tail.join("\n")}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
parts.push(`\n\nAttach to review full output: interactive_shell({ attach: "${sessionId}" })`);
|
|
65
|
-
return parts.join("");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function buildResultNotification(sessionId: string, result: InteractiveShellResult): string {
|
|
69
|
-
const parts: string[] = [];
|
|
70
|
-
if (result.timedOut) {
|
|
71
|
-
parts.push(`Session ${sessionId} timed out.`);
|
|
72
|
-
} else if (result.cancelled) {
|
|
73
|
-
parts.push(`Session ${sessionId} was killed.`);
|
|
74
|
-
} else if (result.exitCode === 0) {
|
|
75
|
-
parts.push(`Session ${sessionId} completed successfully.`);
|
|
76
|
-
} else {
|
|
77
|
-
parts.push(`Session ${sessionId} exited with code ${result.exitCode}.`);
|
|
78
|
-
}
|
|
79
|
-
if (result.completionOutput && result.completionOutput.lines.length > 0) {
|
|
80
|
-
const truncNote = result.completionOutput.truncated
|
|
81
|
-
? ` (truncated from ${result.completionOutput.totalLines} total lines)`
|
|
82
|
-
: "";
|
|
83
|
-
parts.push(`\nOutput (${result.completionOutput.lines.length} lines${truncNote}):\n\n${result.completionOutput.lines.join("\n")}`);
|
|
84
|
-
}
|
|
85
|
-
return parts.join("");
|
|
86
|
-
}
|
|
19
|
+
const coordinator = new InteractiveShellCoordinator();
|
|
87
20
|
|
|
88
21
|
function makeMonitorCompletionCallback(
|
|
89
22
|
pi: ExtensionAPI,
|
|
@@ -101,7 +34,7 @@ function makeMonitorCompletionCallback(
|
|
|
101
34
|
}, { triggerTurn: true });
|
|
102
35
|
pi.events.emit("interactive-shell:transfer", { sessionId: id, ...info });
|
|
103
36
|
sessionManager.unregisterActive(id, false);
|
|
104
|
-
|
|
37
|
+
coordinator.deleteMonitor(id);
|
|
105
38
|
sessionManager.scheduleCleanup(id, 5 * 60 * 1000);
|
|
106
39
|
};
|
|
107
40
|
}
|
|
@@ -113,20 +46,24 @@ function registerHeadlessActive(
|
|
|
113
46
|
session: PtyTerminalSession,
|
|
114
47
|
monitor: HeadlessDispatchMonitor,
|
|
115
48
|
startTime: number,
|
|
49
|
+
config: InteractiveShellConfig,
|
|
116
50
|
): void {
|
|
51
|
+
const queryState = createSessionQueryState();
|
|
52
|
+
coordinator.setMonitor(id, monitor);
|
|
53
|
+
const getCompletionOutput = () => monitor.getResult()?.completionOutput;
|
|
54
|
+
|
|
117
55
|
sessionManager.registerActive({
|
|
118
56
|
id,
|
|
119
57
|
command,
|
|
120
58
|
reason,
|
|
121
59
|
write: (data) => session.write(data),
|
|
122
60
|
kill: () => {
|
|
123
|
-
|
|
61
|
+
coordinator.disposeMonitor(id);
|
|
124
62
|
sessionManager.remove(id);
|
|
125
63
|
sessionManager.unregisterActive(id, true);
|
|
126
|
-
headlessMonitors.delete(id);
|
|
127
64
|
},
|
|
128
65
|
background: () => {},
|
|
129
|
-
getOutput: (opts) =>
|
|
66
|
+
getOutput: (opts) => getSessionOutput(session, config, queryState, opts, getCompletionOutput()),
|
|
130
67
|
getStatus: () => session.exited ? "exited" : "running",
|
|
131
68
|
getRuntime: () => Date.now() - startTime,
|
|
132
69
|
getResult: () => monitor.getResult(),
|
|
@@ -137,120 +74,29 @@ function registerHeadlessActive(
|
|
|
137
74
|
function makeNonBlockingUpdateHandler(pi: ExtensionAPI): (update: HandsFreeUpdate) => void {
|
|
138
75
|
return (update) => {
|
|
139
76
|
pi.events.emit("interactive-shell:update", update);
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
case "killed":
|
|
149
|
-
statusLine = `Session ${update.sessionId} killed (${formatDurationMs(update.runtime)})`;
|
|
150
|
-
break;
|
|
151
|
-
case "user-takeover":
|
|
152
|
-
statusLine = `Session ${update.sessionId}: user took over (${formatDurationMs(update.runtime)})`;
|
|
153
|
-
break;
|
|
154
|
-
default:
|
|
155
|
-
statusLine = `Session ${update.sessionId} update (${formatDurationMs(update.runtime)})`;
|
|
156
|
-
}
|
|
157
|
-
pi.sendMessage({
|
|
158
|
-
customType: "interactive-shell-update",
|
|
159
|
-
content: statusLine + tail,
|
|
160
|
-
display: true,
|
|
161
|
-
details: update,
|
|
162
|
-
}, { triggerTurn: true });
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let bgWidgetCleanup: (() => void) | null = null;
|
|
168
|
-
|
|
169
|
-
function setupBackgroundWidget(ctx: { ui: { setWidget: Function }; hasUI?: boolean }) {
|
|
170
|
-
if (!ctx.hasUI) return;
|
|
171
|
-
|
|
172
|
-
bgWidgetCleanup?.();
|
|
173
|
-
|
|
174
|
-
let durationTimer: ReturnType<typeof setInterval> | null = null;
|
|
175
|
-
let tuiRef: { requestRender: () => void } | null = null;
|
|
176
|
-
|
|
177
|
-
const requestRender = () => tuiRef?.requestRender();
|
|
178
|
-
|
|
179
|
-
const unsubscribe = sessionManager.onChange(() => {
|
|
180
|
-
manageDurationTimer();
|
|
181
|
-
requestRender();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
function manageDurationTimer() {
|
|
185
|
-
const sessions = sessionManager.list();
|
|
186
|
-
const hasRunning = sessions.some((s) => !s.session.exited);
|
|
187
|
-
if (hasRunning && !durationTimer) {
|
|
188
|
-
durationTimer = setInterval(requestRender, 10_000);
|
|
189
|
-
} else if (!hasRunning && durationTimer) {
|
|
190
|
-
clearInterval(durationTimer);
|
|
191
|
-
durationTimer = null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
ctx.ui.setWidget(
|
|
196
|
-
"bg-sessions",
|
|
197
|
-
(tui: any, theme: any) => {
|
|
198
|
-
tuiRef = tui;
|
|
199
|
-
return {
|
|
200
|
-
render: (width: number) => {
|
|
201
|
-
const sessions = sessionManager.list();
|
|
202
|
-
if (sessions.length === 0) return [];
|
|
203
|
-
const cols = width || tui.terminal?.columns || 120;
|
|
204
|
-
const lines: string[] = [];
|
|
205
|
-
for (const s of sessions) {
|
|
206
|
-
const exited = s.session.exited;
|
|
207
|
-
const dot = exited ? theme.fg("dim", "○") : theme.fg("accent", "●");
|
|
208
|
-
const id = theme.fg("dim", s.id);
|
|
209
|
-
const cmd = s.command.replace(/\s+/g, " ").trim();
|
|
210
|
-
const truncCmd = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
211
|
-
const reason = s.reason ? theme.fg("dim", ` · ${s.reason}`) : "";
|
|
212
|
-
const status = exited ? theme.fg("dim", "exited") : theme.fg("success", "running");
|
|
213
|
-
const duration = theme.fg("dim", formatDuration(Date.now() - s.startedAt.getTime()));
|
|
214
|
-
const oneLine = ` ${dot} ${id} ${truncCmd}${reason} ${status} ${duration}`;
|
|
215
|
-
if (visibleWidth(oneLine) <= cols) {
|
|
216
|
-
lines.push(oneLine);
|
|
217
|
-
} else {
|
|
218
|
-
lines.push(truncateToWidth(` ${dot} ${id} ${cmd}`, cols, "…"));
|
|
219
|
-
lines.push(truncateToWidth(` ${status} ${duration}${reason}`, cols, "…"));
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return lines;
|
|
223
|
-
},
|
|
224
|
-
invalidate: () => {},
|
|
225
|
-
};
|
|
226
|
-
},
|
|
227
|
-
{ placement: "belowEditor" },
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
manageDurationTimer();
|
|
231
|
-
|
|
232
|
-
bgWidgetCleanup = () => {
|
|
233
|
-
unsubscribe();
|
|
234
|
-
if (durationTimer) {
|
|
235
|
-
clearInterval(durationTimer);
|
|
236
|
-
durationTimer = null;
|
|
237
|
-
}
|
|
238
|
-
ctx.ui.setWidget("bg-sessions", undefined);
|
|
239
|
-
bgWidgetCleanup = null;
|
|
77
|
+
const message = buildHandsFreeUpdateMessage(update);
|
|
78
|
+
if (!message) return;
|
|
79
|
+
pi.sendMessage({
|
|
80
|
+
customType: "interactive-shell-update",
|
|
81
|
+
content: message.content,
|
|
82
|
+
display: true,
|
|
83
|
+
details: message.details,
|
|
84
|
+
}, { triggerTurn: true });
|
|
240
85
|
};
|
|
241
86
|
}
|
|
242
87
|
|
|
243
88
|
export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
244
|
-
pi.on("session_start", (_event, ctx) =>
|
|
245
|
-
|
|
89
|
+
pi.on("session_start", (_event, ctx) => {
|
|
90
|
+
coordinator.replaceBackgroundWidgetCleanup(setupBackgroundWidget(ctx, sessionManager));
|
|
91
|
+
});
|
|
92
|
+
pi.on("session_switch", (_event, ctx) => {
|
|
93
|
+
coordinator.replaceBackgroundWidgetCleanup(setupBackgroundWidget(ctx, sessionManager));
|
|
94
|
+
});
|
|
246
95
|
|
|
247
96
|
pi.on("session_shutdown", () => {
|
|
248
|
-
|
|
97
|
+
coordinator.clearBackgroundWidget();
|
|
249
98
|
sessionManager.killAll();
|
|
250
|
-
|
|
251
|
-
monitor.dispose();
|
|
252
|
-
headlessMonitors.delete(id);
|
|
253
|
-
}
|
|
99
|
+
coordinator.disposeAllMonitors();
|
|
254
100
|
});
|
|
255
101
|
|
|
256
102
|
pi.registerTool({
|
|
@@ -306,9 +152,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
306
152
|
|
|
307
153
|
// Kill
|
|
308
154
|
if (kill) {
|
|
309
|
-
const hMonitor =
|
|
155
|
+
const hMonitor = coordinator.getMonitor(sessionId);
|
|
310
156
|
if (!hMonitor || hMonitor.disposed) {
|
|
311
|
-
|
|
157
|
+
coordinator.markAgentHandledCompletion(sessionId);
|
|
312
158
|
}
|
|
313
159
|
const { output, truncated, totalBytes, totalLines, hasMore } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain, incremental });
|
|
314
160
|
const status = session.getStatus();
|
|
@@ -332,14 +178,14 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
332
178
|
details: session.getResult(),
|
|
333
179
|
};
|
|
334
180
|
}
|
|
335
|
-
const bMonitor =
|
|
181
|
+
const bMonitor = coordinator.getMonitor(sessionId);
|
|
336
182
|
if (!bMonitor || bMonitor.disposed) {
|
|
337
|
-
|
|
183
|
+
coordinator.markAgentHandledCompletion(sessionId);
|
|
338
184
|
}
|
|
339
185
|
session.background();
|
|
340
186
|
const result = session.getResult();
|
|
341
187
|
if (!result || !result.backgrounded) {
|
|
342
|
-
|
|
188
|
+
coordinator.consumeAgentHandledCompletion(sessionId);
|
|
343
189
|
return {
|
|
344
190
|
content: [{ type: "text", text: `Session ${sessionId} is already running in the background.` }],
|
|
345
191
|
details: { sessionId },
|
|
@@ -482,7 +328,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
482
328
|
isError: true,
|
|
483
329
|
};
|
|
484
330
|
}
|
|
485
|
-
if (
|
|
331
|
+
if (coordinator.isOverlayOpen()) {
|
|
486
332
|
return {
|
|
487
333
|
content: [{ type: "text", text: "An interactive shell overlay is already open." }],
|
|
488
334
|
isError: true,
|
|
@@ -490,6 +336,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
490
336
|
};
|
|
491
337
|
}
|
|
492
338
|
|
|
339
|
+
const monitor = coordinator.getMonitor(attach);
|
|
493
340
|
const bgSession = sessionManager.take(attach);
|
|
494
341
|
if (!bgSession) {
|
|
495
342
|
return {
|
|
@@ -498,54 +345,74 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
498
345
|
};
|
|
499
346
|
}
|
|
500
347
|
|
|
348
|
+
const restoreAttachSession = () => {
|
|
349
|
+
sessionManager.restore(bgSession, { noAutoCleanup: Boolean(monitor && !monitor.disposed) });
|
|
350
|
+
return {
|
|
351
|
+
releaseId: false,
|
|
352
|
+
disposeMonitor: false,
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
if (!coordinator.beginOverlay()) {
|
|
356
|
+
restoreAttachSession();
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: "An interactive shell overlay is already open." }],
|
|
359
|
+
isError: true,
|
|
360
|
+
details: { error: "overlay_already_open" },
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
501
364
|
const config = loadConfig(cwd ?? ctx.cwd);
|
|
502
365
|
const reattachSessionId = attach;
|
|
503
|
-
const monitor = headlessMonitors.get(attach);
|
|
504
|
-
|
|
505
366
|
const isNonBlocking = mode === "hands-free" || mode === "dispatch";
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
367
|
+
const attachStartTime = bgSession.startedAt.getTime();
|
|
368
|
+
let overlayPromise: Promise<InteractiveShellResult>;
|
|
369
|
+
try {
|
|
370
|
+
overlayPromise = ctx.ui.custom<InteractiveShellResult>(
|
|
371
|
+
(tui, theme, _kb, done) =>
|
|
372
|
+
new InteractiveShellOverlay(tui, theme, {
|
|
373
|
+
command: bgSession.command,
|
|
374
|
+
existingSession: bgSession.session,
|
|
375
|
+
sessionId: reattachSessionId,
|
|
376
|
+
mode,
|
|
377
|
+
cwd: cwd ?? ctx.cwd,
|
|
378
|
+
name: bgSession.name,
|
|
379
|
+
reason: bgSession.reason ?? reason,
|
|
380
|
+
startedAt: attachStartTime,
|
|
381
|
+
handsFreeUpdateMode: handsFree?.updateMode,
|
|
382
|
+
handsFreeUpdateInterval: handsFree?.updateInterval,
|
|
383
|
+
handsFreeQuietThreshold: handsFree?.quietThreshold,
|
|
384
|
+
handsFreeUpdateMaxChars: handsFree?.updateMaxChars,
|
|
385
|
+
handsFreeMaxTotalChars: handsFree?.maxTotalChars,
|
|
386
|
+
autoExitOnQuiet: mode === "dispatch"
|
|
387
|
+
? handsFree?.autoExitOnQuiet !== false
|
|
388
|
+
: handsFree?.autoExitOnQuiet === true,
|
|
389
|
+
autoExitGracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
390
|
+
onHandsFreeUpdate: mode === "hands-free"
|
|
391
|
+
? makeNonBlockingUpdateHandler(pi)
|
|
392
|
+
: undefined,
|
|
393
|
+
handoffPreviewEnabled: handoffPreview?.enabled,
|
|
394
|
+
handoffPreviewLines: handoffPreview?.lines,
|
|
395
|
+
handoffPreviewMaxChars: handoffPreview?.maxChars,
|
|
396
|
+
handoffSnapshotEnabled: handoffSnapshot?.enabled,
|
|
397
|
+
handoffSnapshotLines: handoffSnapshot?.lines,
|
|
398
|
+
handoffSnapshotMaxChars: handoffSnapshot?.maxChars,
|
|
399
|
+
timeout,
|
|
400
|
+
}, config, done),
|
|
401
|
+
{
|
|
402
|
+
overlay: true,
|
|
403
|
+
overlayOptions: {
|
|
404
|
+
width: `${config.overlayWidthPercent}%`,
|
|
405
|
+
maxHeight: `${config.overlayHeightPercent}%`,
|
|
406
|
+
anchor: "center",
|
|
407
|
+
margin: 1,
|
|
408
|
+
},
|
|
546
409
|
},
|
|
547
|
-
|
|
548
|
-
)
|
|
410
|
+
);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
coordinator.endOverlay();
|
|
413
|
+
restoreAttachSession();
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
549
416
|
|
|
550
417
|
if (isNonBlocking) {
|
|
551
418
|
setupDispatchCompletion(pi, overlayPromise, config, {
|
|
@@ -556,6 +423,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
556
423
|
timeout,
|
|
557
424
|
handsFree,
|
|
558
425
|
overlayStartTime: attachStartTime,
|
|
426
|
+
onOverlayError: restoreAttachSession,
|
|
559
427
|
});
|
|
560
428
|
return {
|
|
561
429
|
content: [{ type: "text", text: mode === "dispatch"
|
|
@@ -565,39 +433,27 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
565
433
|
};
|
|
566
434
|
}
|
|
567
435
|
|
|
568
|
-
// Blocking (interactive) attach
|
|
569
436
|
let result: InteractiveShellResult;
|
|
570
437
|
try {
|
|
571
438
|
result = await overlayPromise;
|
|
439
|
+
} catch (error) {
|
|
440
|
+
restoreAttachSession();
|
|
441
|
+
throw error;
|
|
572
442
|
} finally {
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
if (monitor) {
|
|
576
|
-
monitor.dispose();
|
|
577
|
-
headlessMonitors.delete(attach);
|
|
578
|
-
sessionManager.unregisterActive(attach, !result.backgrounded);
|
|
579
|
-
} else if (!result.backgrounded) {
|
|
580
|
-
releaseSessionId(attach);
|
|
443
|
+
coordinator.endOverlay();
|
|
581
444
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
445
|
+
if (monitor && !monitor.disposed) {
|
|
446
|
+
if (!result.backgrounded) {
|
|
447
|
+
monitor.handleExternalCompletion(result.exitCode, result.signal, result.completionOutput);
|
|
448
|
+
coordinator.deleteMonitor(attach);
|
|
449
|
+
}
|
|
587
450
|
} else if (result.backgrounded) {
|
|
588
|
-
|
|
589
|
-
} else if (result.cancelled) {
|
|
590
|
-
summary = "Session killed";
|
|
591
|
-
} else if (result.timedOut) {
|
|
592
|
-
summary = `Session killed after timeout (${timeout ?? "?"}ms)`;
|
|
451
|
+
sessionManager.restartAutoCleanup(attach);
|
|
593
452
|
} else {
|
|
594
|
-
|
|
595
|
-
summary = `Session ended ${status}`;
|
|
596
|
-
}
|
|
597
|
-
if (!result.transferred && result.handoffPreview?.type === "tail" && result.handoffPreview.lines.length > 0) {
|
|
598
|
-
summary += `\n\nOverlay tail (${result.handoffPreview.when}, last ${result.handoffPreview.lines.length} lines):\n${result.handoffPreview.lines.join("\n")}`;
|
|
453
|
+
sessionManager.scheduleCleanup(attach);
|
|
599
454
|
}
|
|
600
|
-
|
|
455
|
+
|
|
456
|
+
return { content: [{ type: "text", text: summarizeInteractiveResult(command ?? bgSession.command, result, timeout, bgSession.reason ?? reason) }], details: result };
|
|
601
457
|
}
|
|
602
458
|
|
|
603
459
|
// ── Branch 3: List background sessions ──
|
|
@@ -632,11 +488,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
632
488
|
}
|
|
633
489
|
|
|
634
490
|
for (const tid of targetIds) {
|
|
635
|
-
|
|
636
|
-
if (monitor) {
|
|
637
|
-
monitor.dispose();
|
|
638
|
-
headlessMonitors.delete(tid);
|
|
639
|
-
}
|
|
491
|
+
coordinator.disposeMonitor(tid);
|
|
640
492
|
sessionManager.unregisterActive(tid, false);
|
|
641
493
|
sessionManager.remove(tid);
|
|
642
494
|
}
|
|
@@ -665,17 +517,18 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
665
517
|
const session = new PtyTerminalSession(
|
|
666
518
|
{ command, cwd: effectiveCwd, cols: 120, rows: 40, scrollback: config.scrollbackLines },
|
|
667
519
|
);
|
|
668
|
-
sessionManager.add(command, session, name, reason, { id, noAutoCleanup: true });
|
|
669
520
|
|
|
670
521
|
const startTime = Date.now();
|
|
522
|
+
sessionManager.add(command, session, name, reason, { id, noAutoCleanup: true, startedAt: new Date(startTime) });
|
|
523
|
+
|
|
671
524
|
const monitor = new HeadlessDispatchMonitor(session, config, {
|
|
672
525
|
autoExitOnQuiet: handsFree?.autoExitOnQuiet !== false,
|
|
673
526
|
quietThreshold: handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
|
|
674
527
|
gracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
675
528
|
timeout,
|
|
529
|
+
startedAt: startTime,
|
|
676
530
|
}, makeMonitorCompletionCallback(pi, id, startTime));
|
|
677
|
-
|
|
678
|
-
registerHeadlessActive(id, command, reason, session, monitor, startTime);
|
|
531
|
+
registerHeadlessActive(id, command, reason, session, monitor, startTime, config);
|
|
679
532
|
|
|
680
533
|
return {
|
|
681
534
|
content: [{ type: "text", text: `Session dispatched in background (id: ${id}).\nYou'll be notified when it completes. User can /attach ${id} to watch.` }],
|
|
@@ -698,7 +551,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
698
551
|
};
|
|
699
552
|
}
|
|
700
553
|
|
|
701
|
-
if (
|
|
554
|
+
if (coordinator.isOverlayOpen()) {
|
|
702
555
|
return {
|
|
703
556
|
content: [{ type: "text", text: "An interactive shell overlay is already open. Wait for it to close or kill the active session before starting a new one." }],
|
|
704
557
|
isError: true,
|
|
@@ -710,48 +563,61 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
710
563
|
|
|
711
564
|
// ── Non-blocking path (hands-free or dispatch) ──
|
|
712
565
|
if (isNonBlocking && generatedSessionId) {
|
|
713
|
-
|
|
566
|
+
if (!coordinator.beginOverlay()) {
|
|
567
|
+
return {
|
|
568
|
+
content: [{ type: "text", text: "An interactive shell overlay is already open. Wait for it to close or kill the active session before starting a new one." }],
|
|
569
|
+
isError: true,
|
|
570
|
+
details: { error: "overlay_already_open" },
|
|
571
|
+
};
|
|
572
|
+
}
|
|
714
573
|
const overlayStartTime = Date.now();
|
|
715
574
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
: handsFree?.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
:
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
575
|
+
let overlayPromise: Promise<InteractiveShellResult>;
|
|
576
|
+
try {
|
|
577
|
+
overlayPromise = ctx.ui.custom<InteractiveShellResult>(
|
|
578
|
+
(tui, theme, _kb, done) =>
|
|
579
|
+
new InteractiveShellOverlay(tui, theme, {
|
|
580
|
+
command,
|
|
581
|
+
cwd: effectiveCwd,
|
|
582
|
+
name,
|
|
583
|
+
reason,
|
|
584
|
+
mode,
|
|
585
|
+
sessionId: generatedSessionId,
|
|
586
|
+
startedAt: overlayStartTime,
|
|
587
|
+
handsFreeUpdateMode: handsFree?.updateMode,
|
|
588
|
+
handsFreeUpdateInterval: handsFree?.updateInterval,
|
|
589
|
+
handsFreeQuietThreshold: handsFree?.quietThreshold,
|
|
590
|
+
handsFreeUpdateMaxChars: handsFree?.updateMaxChars,
|
|
591
|
+
handsFreeMaxTotalChars: handsFree?.maxTotalChars,
|
|
592
|
+
autoExitOnQuiet: mode === "dispatch"
|
|
593
|
+
? handsFree?.autoExitOnQuiet !== false
|
|
594
|
+
: handsFree?.autoExitOnQuiet === true,
|
|
595
|
+
autoExitGracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
596
|
+
onHandsFreeUpdate: mode === "hands-free"
|
|
597
|
+
? makeNonBlockingUpdateHandler(pi)
|
|
598
|
+
: undefined,
|
|
599
|
+
handoffPreviewEnabled: handoffPreview?.enabled,
|
|
600
|
+
handoffPreviewLines: handoffPreview?.lines,
|
|
601
|
+
handoffPreviewMaxChars: handoffPreview?.maxChars,
|
|
602
|
+
handoffSnapshotEnabled: handoffSnapshot?.enabled,
|
|
603
|
+
handoffSnapshotLines: handoffSnapshot?.lines,
|
|
604
|
+
handoffSnapshotMaxChars: handoffSnapshot?.maxChars,
|
|
605
|
+
timeout,
|
|
606
|
+
}, config, done),
|
|
607
|
+
{
|
|
608
|
+
overlay: true,
|
|
609
|
+
overlayOptions: {
|
|
610
|
+
width: `${config.overlayWidthPercent}%`,
|
|
611
|
+
maxHeight: `${config.overlayHeightPercent}%`,
|
|
612
|
+
anchor: "center",
|
|
613
|
+
margin: 1,
|
|
614
|
+
},
|
|
752
615
|
},
|
|
753
|
-
|
|
754
|
-
)
|
|
616
|
+
);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
coordinator.endOverlay();
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
755
621
|
|
|
756
622
|
setupDispatchCompletion(pi, overlayPromise, config, {
|
|
757
623
|
id: generatedSessionId,
|
|
@@ -776,7 +642,13 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
776
642
|
}
|
|
777
643
|
|
|
778
644
|
// ── Blocking (interactive) path ──
|
|
779
|
-
|
|
645
|
+
if (!coordinator.beginOverlay()) {
|
|
646
|
+
return {
|
|
647
|
+
content: [{ type: "text", text: "An interactive shell overlay is already open. Wait for it to close or kill the active session before starting a new one." }],
|
|
648
|
+
isError: true,
|
|
649
|
+
details: { error: "overlay_already_open" },
|
|
650
|
+
};
|
|
651
|
+
}
|
|
780
652
|
onUpdate?.({
|
|
781
653
|
content: [{ type: "text", text: `Opening: ${command}` }],
|
|
782
654
|
details: { exitCode: null, backgrounded: false, cancelled: false },
|
|
@@ -856,45 +728,17 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
856
728
|
},
|
|
857
729
|
);
|
|
858
730
|
} finally {
|
|
859
|
-
|
|
731
|
+
coordinator.endOverlay();
|
|
860
732
|
}
|
|
861
733
|
|
|
862
|
-
|
|
863
|
-
if (result.transferred) {
|
|
864
|
-
const truncatedNote = result.transferred.truncated ? ` (truncated from ${result.transferred.totalLines} total lines)` : "";
|
|
865
|
-
summary = `Session output transferred (${result.transferred.lines.length} lines${truncatedNote}):\n\n${result.transferred.lines.join("\n")}`;
|
|
866
|
-
} else if (result.backgrounded) {
|
|
867
|
-
summary = `Session running in background (id: ${result.backgroundId}). User can reattach with /attach ${result.backgroundId}`;
|
|
868
|
-
} else if (result.cancelled) {
|
|
869
|
-
summary = "User killed the interactive session";
|
|
870
|
-
} else if (result.timedOut) {
|
|
871
|
-
summary = `Session killed after timeout (${timeout ?? "?"}ms)`;
|
|
872
|
-
} else {
|
|
873
|
-
const status = result.exitCode === 0 ? "successfully" : `with code ${result.exitCode}`;
|
|
874
|
-
summary = `Session ended ${status}`;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
if (result.userTookOver) {
|
|
878
|
-
summary += "\n\nNote: User took over control during hands-free mode.";
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const warning = buildIdlePromptWarning(command, reason);
|
|
882
|
-
if (warning) {
|
|
883
|
-
summary += `\n\n${warning}`;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (!result.transferred && result.handoffPreview?.type === "tail" && result.handoffPreview.lines.length > 0) {
|
|
887
|
-
summary += `\n\nOverlay tail (${result.handoffPreview.when}, last ${result.handoffPreview.lines.length} lines):\n${result.handoffPreview.lines.join("\n")}`;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
return { content: [{ type: "text", text: summary }], details: result };
|
|
734
|
+
return { content: [{ type: "text", text: summarizeInteractiveResult(command, result, timeout, reason) }], details: result };
|
|
891
735
|
},
|
|
892
736
|
});
|
|
893
737
|
|
|
894
738
|
pi.registerCommand("attach", {
|
|
895
739
|
description: "Reattach to a background shell session",
|
|
896
740
|
handler: async (args, ctx) => {
|
|
897
|
-
if (
|
|
741
|
+
if (coordinator.isOverlayOpen()) {
|
|
898
742
|
ctx.ui.notify("An overlay is already open. Close it first.", "error");
|
|
899
743
|
return;
|
|
900
744
|
}
|
|
@@ -920,7 +764,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
920
764
|
targetId = choice.split(" - ")[0]!;
|
|
921
765
|
}
|
|
922
766
|
|
|
923
|
-
const monitor =
|
|
767
|
+
const monitor = coordinator.getMonitor(targetId);
|
|
924
768
|
|
|
925
769
|
const session = sessionManager.get(targetId);
|
|
926
770
|
if (!session) {
|
|
@@ -929,7 +773,10 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
929
773
|
}
|
|
930
774
|
|
|
931
775
|
const config = loadConfig(ctx.cwd);
|
|
932
|
-
|
|
776
|
+
if (!coordinator.beginOverlay()) {
|
|
777
|
+
ctx.ui.notify("An overlay is already open. Close it first.", "error");
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
933
780
|
try {
|
|
934
781
|
const result = await ctx.ui.custom<InteractiveShellResult>(
|
|
935
782
|
(tui, theme, _kb, done) =>
|
|
@@ -948,7 +795,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
948
795
|
if (monitor && !monitor.disposed) {
|
|
949
796
|
if (!result.backgrounded) {
|
|
950
797
|
monitor.handleExternalCompletion(result.exitCode, result.signal, result.completionOutput);
|
|
951
|
-
|
|
798
|
+
coordinator.deleteMonitor(targetId);
|
|
952
799
|
}
|
|
953
800
|
} else if (result.backgrounded) {
|
|
954
801
|
sessionManager.restartAutoCleanup(targetId);
|
|
@@ -956,7 +803,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
956
803
|
sessionManager.scheduleCleanup(targetId);
|
|
957
804
|
}
|
|
958
805
|
} finally {
|
|
959
|
-
|
|
806
|
+
coordinator.endOverlay();
|
|
960
807
|
}
|
|
961
808
|
},
|
|
962
809
|
});
|
|
@@ -994,11 +841,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
994
841
|
}
|
|
995
842
|
|
|
996
843
|
for (const tid of targetIds) {
|
|
997
|
-
|
|
998
|
-
if (monitor) {
|
|
999
|
-
monitor.dispose();
|
|
1000
|
-
headlessMonitors.delete(tid);
|
|
1001
|
-
}
|
|
844
|
+
coordinator.disposeMonitor(tid);
|
|
1002
845
|
sessionManager.unregisterActive(tid, false);
|
|
1003
846
|
sessionManager.remove(tid);
|
|
1004
847
|
}
|
|
@@ -1021,15 +864,15 @@ function setupDispatchCompletion(
|
|
|
1021
864
|
timeout?: number;
|
|
1022
865
|
handsFree?: { autoExitOnQuiet?: boolean; quietThreshold?: number; gracePeriod?: number };
|
|
1023
866
|
overlayStartTime?: number;
|
|
867
|
+
onOverlayError?: () => { releaseId?: boolean; disposeMonitor?: boolean } | void;
|
|
1024
868
|
},
|
|
1025
869
|
): void {
|
|
1026
870
|
const { id, mode, command, reason } = ctx;
|
|
1027
871
|
|
|
1028
872
|
overlayPromise.then((result) => {
|
|
1029
|
-
|
|
873
|
+
coordinator.endOverlay();
|
|
1030
874
|
|
|
1031
|
-
const wasAgentInitiated =
|
|
1032
|
-
agentHandledCompletion = false;
|
|
875
|
+
const wasAgentInitiated = coordinator.consumeAgentHandledCompletion(id);
|
|
1033
876
|
|
|
1034
877
|
if (result.transferred) {
|
|
1035
878
|
const truncatedNote = result.transferred.truncated
|
|
@@ -1044,10 +887,11 @@ function setupDispatchCompletion(
|
|
|
1044
887
|
}, { triggerTurn: true });
|
|
1045
888
|
pi.events.emit("interactive-shell:transfer", { sessionId: id, transferred: result.transferred, exitCode: result.exitCode, signal: result.signal });
|
|
1046
889
|
sessionManager.unregisterActive(id, true);
|
|
890
|
+
coordinator.disposeMonitor(id);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
1047
893
|
|
|
1048
|
-
|
|
1049
|
-
if (remainingMonitor) { remainingMonitor.dispose(); headlessMonitors.delete(id); }
|
|
1050
|
-
} else if (mode === "dispatch" && result.backgrounded) {
|
|
894
|
+
if (mode === "dispatch" && result.backgrounded) {
|
|
1051
895
|
if (!wasAgentInitiated) {
|
|
1052
896
|
pi.sendMessage({
|
|
1053
897
|
customType: "interactive-shell-transfer",
|
|
@@ -1058,31 +902,32 @@ function setupDispatchCompletion(
|
|
|
1058
902
|
}
|
|
1059
903
|
sessionManager.unregisterActive(id, false);
|
|
1060
904
|
|
|
1061
|
-
const
|
|
905
|
+
const bgId = result.backgroundId!;
|
|
906
|
+
const existingMonitor = coordinator.getMonitor(id);
|
|
907
|
+
const bgSession = sessionManager.get(bgId);
|
|
908
|
+
if (!bgSession) return;
|
|
909
|
+
|
|
1062
910
|
if (existingMonitor && !existingMonitor.disposed) {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
} else if (!existingMonitor) {
|
|
1068
|
-
const bgSession = sessionManager.get(result.backgroundId!);
|
|
1069
|
-
if (bgSession) {
|
|
1070
|
-
const bgId = result.backgroundId!;
|
|
1071
|
-
const bgStartTime = ctx.overlayStartTime ?? Date.now();
|
|
1072
|
-
const elapsed = ctx.overlayStartTime ? Date.now() - ctx.overlayStartTime : 0;
|
|
1073
|
-
const remainingTimeout = ctx.timeout ? Math.max(0, ctx.timeout - elapsed) : undefined;
|
|
1074
|
-
|
|
1075
|
-
const monitor = new HeadlessDispatchMonitor(bgSession.session, config, {
|
|
1076
|
-
autoExitOnQuiet: ctx.handsFree?.autoExitOnQuiet !== false,
|
|
1077
|
-
quietThreshold: ctx.handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
|
|
1078
|
-
gracePeriod: ctx.handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
1079
|
-
timeout: remainingTimeout,
|
|
1080
|
-
}, makeMonitorCompletionCallback(pi, bgId, bgStartTime));
|
|
1081
|
-
headlessMonitors.set(bgId, monitor);
|
|
1082
|
-
registerHeadlessActive(bgId, command, reason, bgSession.session, monitor, bgStartTime);
|
|
1083
|
-
}
|
|
911
|
+
coordinator.deleteMonitor(id);
|
|
912
|
+
registerHeadlessActive(bgId, command, reason, bgSession.session, existingMonitor, bgSession.startedAt.getTime(), config);
|
|
913
|
+
return;
|
|
1084
914
|
}
|
|
1085
|
-
|
|
915
|
+
|
|
916
|
+
const elapsed = ctx.overlayStartTime ? Date.now() - ctx.overlayStartTime : 0;
|
|
917
|
+
const remainingTimeout = ctx.timeout ? Math.max(0, ctx.timeout - elapsed) : undefined;
|
|
918
|
+
const bgStartTime = bgSession.startedAt.getTime();
|
|
919
|
+
const monitor = new HeadlessDispatchMonitor(bgSession.session, config, {
|
|
920
|
+
autoExitOnQuiet: ctx.handsFree?.autoExitOnQuiet !== false,
|
|
921
|
+
quietThreshold: ctx.handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
|
|
922
|
+
gracePeriod: ctx.handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
923
|
+
timeout: remainingTimeout,
|
|
924
|
+
startedAt: bgStartTime,
|
|
925
|
+
}, makeMonitorCompletionCallback(pi, bgId, bgStartTime));
|
|
926
|
+
registerHeadlessActive(bgId, command, reason, bgSession.session, monitor, bgStartTime, config);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (mode === "dispatch") {
|
|
1086
931
|
if (!wasAgentInitiated) {
|
|
1087
932
|
const content = buildResultNotification(id, result);
|
|
1088
933
|
pi.sendMessage({
|
|
@@ -1101,47 +946,17 @@ function setupDispatchCompletion(
|
|
|
1101
946
|
cancelled: result.cancelled,
|
|
1102
947
|
});
|
|
1103
948
|
sessionManager.unregisterActive(id, true);
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (remainingMonitor) { remainingMonitor.dispose(); headlessMonitors.delete(id); }
|
|
949
|
+
coordinator.disposeMonitor(id);
|
|
950
|
+
return;
|
|
1107
951
|
}
|
|
1108
952
|
|
|
1109
|
-
|
|
1110
|
-
const staleMonitor = headlessMonitors.get(id);
|
|
1111
|
-
if (staleMonitor) { staleMonitor.dispose(); headlessMonitors.delete(id); }
|
|
1112
|
-
}
|
|
953
|
+
coordinator.disposeMonitor(id);
|
|
1113
954
|
}).catch(() => {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
if (
|
|
955
|
+
coordinator.endOverlay();
|
|
956
|
+
const recovery = ctx.onOverlayError?.();
|
|
957
|
+
sessionManager.unregisterActive(id, recovery?.releaseId ?? true);
|
|
958
|
+
if (recovery?.disposeMonitor !== false) {
|
|
959
|
+
coordinator.disposeMonitor(id);
|
|
960
|
+
}
|
|
1118
961
|
});
|
|
1119
962
|
}
|
|
1120
|
-
|
|
1121
|
-
function buildIdlePromptWarning(command: string, reason: string | undefined): string | null {
|
|
1122
|
-
if (!reason) return null;
|
|
1123
|
-
|
|
1124
|
-
const tasky = /\b(scan|check|review|summariz|analyz|inspect|audit|find|fix|refactor|debug|investigat|explore|enumerat|list)\b/i;
|
|
1125
|
-
if (!tasky.test(reason)) return null;
|
|
1126
|
-
|
|
1127
|
-
const trimmed = command.trim();
|
|
1128
|
-
const binaries = ["pi", "claude", "codex", "gemini", "cursor-agent"] as const;
|
|
1129
|
-
const bin = binaries.find((b) => trimmed === b || trimmed.startsWith(`${b} `));
|
|
1130
|
-
if (!bin) return null;
|
|
1131
|
-
|
|
1132
|
-
const rest = trimmed === bin ? "" : trimmed.slice(bin.length).trim();
|
|
1133
|
-
const hasQuotedPrompt = /["']/.test(rest);
|
|
1134
|
-
const hasKnownPromptFlag =
|
|
1135
|
-
/\b(-p|--print|--prompt|--prompt-interactive|-i|exec)\b/.test(rest) ||
|
|
1136
|
-
(bin === "pi" && /\b-p\b/.test(rest)) ||
|
|
1137
|
-
(bin === "codex" && /\bexec\b/.test(rest));
|
|
1138
|
-
|
|
1139
|
-
if (hasQuotedPrompt || hasKnownPromptFlag) return null;
|
|
1140
|
-
if (rest.length === 0 || /^(-{1,2}[A-Za-z0-9][A-Za-z0-9-]*(?:=[^\s]+)?\s*)+$/.test(rest)) {
|
|
1141
|
-
const examplePrompt = reason.replace(/\s+/g, " ").trim();
|
|
1142
|
-
const clipped = examplePrompt.length > 120 ? `${examplePrompt.slice(0, 117)}...` : examplePrompt;
|
|
1143
|
-
return `Note: \`reason\` is UI-only. This command likely started the agent idle. If you intended an initial prompt, embed it in \`command\`, e.g. \`${bin} "${clipped}"\`.`;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
return null;
|
|
1147
|
-
}
|