pi-interactive-shell 0.8.2 → 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 +47 -1
- package/README.md +22 -14
- package/SKILL.md +4 -2
- package/background-widget.ts +76 -0
- package/config.ts +4 -4
- package/examples/prompts/codex-implement-plan.md +18 -7
- package/examples/prompts/codex-review-impl.md +16 -5
- package/examples/prompts/codex-review-plan.md +20 -10
- package/examples/skills/codex-5-3-prompting/SKILL.md +161 -0
- package/examples/skills/codex-cli/SKILL.md +16 -8
- package/examples/skills/gpt-5-4-prompting/SKILL.md +202 -0
- package/handoff-utils.ts +92 -0
- package/headless-monitor.ts +16 -3
- package/index.ts +240 -384
- package/notification-utils.ts +134 -0
- package/overlay-component.ts +61 -248
- 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 +6 -74
- package/runtime-coordinator.ts +69 -0
- package/scripts/install.js +18 -3
- package/session-manager.ts +21 -11
- package/session-query.ts +170 -0
- package/spawn-helper.ts +37 -0
- package/tool-schema.ts +6 -2
- package/types.ts +6 -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
|
-
import type { InteractiveShellResult } from "./types.js";
|
|
7
|
-
import { sessionManager, generateSessionId
|
|
8
|
-
import type { OutputOptions, OutputResult } from "./session-manager.js";
|
|
5
|
+
import type { InteractiveShellResult, HandsFreeUpdate } from "./types.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(),
|
|
@@ -134,93 +71,32 @@ function registerHeadlessActive(
|
|
|
134
71
|
});
|
|
135
72
|
}
|
|
136
73
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const unsubscribe = sessionManager.onChange(() => {
|
|
150
|
-
manageDurationTimer();
|
|
151
|
-
requestRender();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
function manageDurationTimer() {
|
|
155
|
-
const sessions = sessionManager.list();
|
|
156
|
-
const hasRunning = sessions.some((s) => !s.session.exited);
|
|
157
|
-
if (hasRunning && !durationTimer) {
|
|
158
|
-
durationTimer = setInterval(requestRender, 10_000);
|
|
159
|
-
} else if (!hasRunning && durationTimer) {
|
|
160
|
-
clearInterval(durationTimer);
|
|
161
|
-
durationTimer = null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
ctx.ui.setWidget(
|
|
166
|
-
"bg-sessions",
|
|
167
|
-
(tui: any, theme: any) => {
|
|
168
|
-
tuiRef = tui;
|
|
169
|
-
return {
|
|
170
|
-
render: (width: number) => {
|
|
171
|
-
const sessions = sessionManager.list();
|
|
172
|
-
if (sessions.length === 0) return [];
|
|
173
|
-
const cols = width || tui.terminal?.columns || 120;
|
|
174
|
-
const lines: string[] = [];
|
|
175
|
-
for (const s of sessions) {
|
|
176
|
-
const exited = s.session.exited;
|
|
177
|
-
const dot = exited ? theme.fg("dim", "○") : theme.fg("accent", "●");
|
|
178
|
-
const id = theme.fg("dim", s.id);
|
|
179
|
-
const cmd = s.command.replace(/\s+/g, " ").trim();
|
|
180
|
-
const truncCmd = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
|
|
181
|
-
const reason = s.reason ? theme.fg("dim", ` · ${s.reason}`) : "";
|
|
182
|
-
const status = exited ? theme.fg("dim", "exited") : theme.fg("success", "running");
|
|
183
|
-
const duration = theme.fg("dim", formatDuration(Date.now() - s.startedAt.getTime()));
|
|
184
|
-
const oneLine = ` ${dot} ${id} ${truncCmd}${reason} ${status} ${duration}`;
|
|
185
|
-
if (visibleWidth(oneLine) <= cols) {
|
|
186
|
-
lines.push(oneLine);
|
|
187
|
-
} else {
|
|
188
|
-
lines.push(truncateToWidth(` ${dot} ${id} ${cmd}`, cols, "…"));
|
|
189
|
-
lines.push(truncateToWidth(` ${status} ${duration}${reason}`, cols, "…"));
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return lines;
|
|
193
|
-
},
|
|
194
|
-
invalidate: () => {},
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
{ placement: "belowEditor" },
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
manageDurationTimer();
|
|
201
|
-
|
|
202
|
-
bgWidgetCleanup = () => {
|
|
203
|
-
unsubscribe();
|
|
204
|
-
if (durationTimer) {
|
|
205
|
-
clearInterval(durationTimer);
|
|
206
|
-
durationTimer = null;
|
|
207
|
-
}
|
|
208
|
-
ctx.ui.setWidget("bg-sessions", undefined);
|
|
209
|
-
bgWidgetCleanup = null;
|
|
74
|
+
function makeNonBlockingUpdateHandler(pi: ExtensionAPI): (update: HandsFreeUpdate) => void {
|
|
75
|
+
return (update) => {
|
|
76
|
+
pi.events.emit("interactive-shell:update", update);
|
|
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 });
|
|
210
85
|
};
|
|
211
86
|
}
|
|
212
87
|
|
|
213
88
|
export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
214
|
-
pi.on("session_start", (_event, ctx) =>
|
|
215
|
-
|
|
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
|
+
});
|
|
216
95
|
|
|
217
96
|
pi.on("session_shutdown", () => {
|
|
218
|
-
|
|
97
|
+
coordinator.clearBackgroundWidget();
|
|
219
98
|
sessionManager.killAll();
|
|
220
|
-
|
|
221
|
-
monitor.dispose();
|
|
222
|
-
headlessMonitors.delete(id);
|
|
223
|
-
}
|
|
99
|
+
coordinator.disposeAllMonitors();
|
|
224
100
|
});
|
|
225
101
|
|
|
226
102
|
pi.registerTool({
|
|
@@ -276,9 +152,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
276
152
|
|
|
277
153
|
// Kill
|
|
278
154
|
if (kill) {
|
|
279
|
-
const hMonitor =
|
|
155
|
+
const hMonitor = coordinator.getMonitor(sessionId);
|
|
280
156
|
if (!hMonitor || hMonitor.disposed) {
|
|
281
|
-
|
|
157
|
+
coordinator.markAgentHandledCompletion(sessionId);
|
|
282
158
|
}
|
|
283
159
|
const { output, truncated, totalBytes, totalLines, hasMore } = session.getOutput({ skipRateLimit: true, lines: outputLines, maxChars: outputMaxChars, offset: outputOffset, drain, incremental });
|
|
284
160
|
const status = session.getStatus();
|
|
@@ -302,14 +178,14 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
302
178
|
details: session.getResult(),
|
|
303
179
|
};
|
|
304
180
|
}
|
|
305
|
-
const bMonitor =
|
|
181
|
+
const bMonitor = coordinator.getMonitor(sessionId);
|
|
306
182
|
if (!bMonitor || bMonitor.disposed) {
|
|
307
|
-
|
|
183
|
+
coordinator.markAgentHandledCompletion(sessionId);
|
|
308
184
|
}
|
|
309
185
|
session.background();
|
|
310
186
|
const result = session.getResult();
|
|
311
187
|
if (!result || !result.backgrounded) {
|
|
312
|
-
|
|
188
|
+
coordinator.consumeAgentHandledCompletion(sessionId);
|
|
313
189
|
return {
|
|
314
190
|
content: [{ type: "text", text: `Session ${sessionId} is already running in the background.` }],
|
|
315
191
|
details: { sessionId },
|
|
@@ -452,7 +328,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
452
328
|
isError: true,
|
|
453
329
|
};
|
|
454
330
|
}
|
|
455
|
-
if (
|
|
331
|
+
if (coordinator.isOverlayOpen()) {
|
|
456
332
|
return {
|
|
457
333
|
content: [{ type: "text", text: "An interactive shell overlay is already open." }],
|
|
458
334
|
isError: true,
|
|
@@ -460,6 +336,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
460
336
|
};
|
|
461
337
|
}
|
|
462
338
|
|
|
339
|
+
const monitor = coordinator.getMonitor(attach);
|
|
463
340
|
const bgSession = sessionManager.take(attach);
|
|
464
341
|
if (!bgSession) {
|
|
465
342
|
return {
|
|
@@ -468,51 +345,74 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
468
345
|
};
|
|
469
346
|
}
|
|
470
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
|
+
|
|
471
364
|
const config = loadConfig(cwd ?? ctx.cwd);
|
|
472
365
|
const reattachSessionId = attach;
|
|
473
|
-
const monitor = headlessMonitors.get(attach);
|
|
474
|
-
|
|
475
366
|
const isNonBlocking = mode === "hands-free" || mode === "dispatch";
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
},
|
|
513
409
|
},
|
|
514
|
-
|
|
515
|
-
)
|
|
410
|
+
);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
coordinator.endOverlay();
|
|
413
|
+
restoreAttachSession();
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
516
416
|
|
|
517
417
|
if (isNonBlocking) {
|
|
518
418
|
setupDispatchCompletion(pi, overlayPromise, config, {
|
|
@@ -523,6 +423,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
523
423
|
timeout,
|
|
524
424
|
handsFree,
|
|
525
425
|
overlayStartTime: attachStartTime,
|
|
426
|
+
onOverlayError: restoreAttachSession,
|
|
526
427
|
});
|
|
527
428
|
return {
|
|
528
429
|
content: [{ type: "text", text: mode === "dispatch"
|
|
@@ -532,39 +433,27 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
532
433
|
};
|
|
533
434
|
}
|
|
534
435
|
|
|
535
|
-
// Blocking (interactive) attach
|
|
536
436
|
let result: InteractiveShellResult;
|
|
537
437
|
try {
|
|
538
438
|
result = await overlayPromise;
|
|
439
|
+
} catch (error) {
|
|
440
|
+
restoreAttachSession();
|
|
441
|
+
throw error;
|
|
539
442
|
} finally {
|
|
540
|
-
|
|
443
|
+
coordinator.endOverlay();
|
|
541
444
|
}
|
|
542
|
-
if (monitor) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
releaseSessionId(attach);
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
let summary: string;
|
|
551
|
-
if (result.transferred) {
|
|
552
|
-
const truncatedNote = result.transferred.truncated ? ` (truncated from ${result.transferred.totalLines} total lines)` : "";
|
|
553
|
-
summary = `Session output transferred (${result.transferred.lines.length} lines${truncatedNote}):\n\n${result.transferred.lines.join("\n")}`;
|
|
445
|
+
if (monitor && !monitor.disposed) {
|
|
446
|
+
if (!result.backgrounded) {
|
|
447
|
+
monitor.handleExternalCompletion(result.exitCode, result.signal, result.completionOutput);
|
|
448
|
+
coordinator.deleteMonitor(attach);
|
|
449
|
+
}
|
|
554
450
|
} else if (result.backgrounded) {
|
|
555
|
-
|
|
556
|
-
} else if (result.cancelled) {
|
|
557
|
-
summary = "Session killed";
|
|
558
|
-
} else if (result.timedOut) {
|
|
559
|
-
summary = `Session killed after timeout (${timeout ?? "?"}ms)`;
|
|
451
|
+
sessionManager.restartAutoCleanup(attach);
|
|
560
452
|
} else {
|
|
561
|
-
|
|
562
|
-
summary = `Session ended ${status}`;
|
|
563
|
-
}
|
|
564
|
-
if (!result.transferred && result.handoffPreview?.type === "tail" && result.handoffPreview.lines.length > 0) {
|
|
565
|
-
summary += `\n\nOverlay tail (${result.handoffPreview.when}, last ${result.handoffPreview.lines.length} lines):\n${result.handoffPreview.lines.join("\n")}`;
|
|
453
|
+
sessionManager.scheduleCleanup(attach);
|
|
566
454
|
}
|
|
567
|
-
|
|
455
|
+
|
|
456
|
+
return { content: [{ type: "text", text: summarizeInteractiveResult(command ?? bgSession.command, result, timeout, bgSession.reason ?? reason) }], details: result };
|
|
568
457
|
}
|
|
569
458
|
|
|
570
459
|
// ── Branch 3: List background sessions ──
|
|
@@ -599,11 +488,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
599
488
|
}
|
|
600
489
|
|
|
601
490
|
for (const tid of targetIds) {
|
|
602
|
-
|
|
603
|
-
if (monitor) {
|
|
604
|
-
monitor.dispose();
|
|
605
|
-
headlessMonitors.delete(tid);
|
|
606
|
-
}
|
|
491
|
+
coordinator.disposeMonitor(tid);
|
|
607
492
|
sessionManager.unregisterActive(tid, false);
|
|
608
493
|
sessionManager.remove(tid);
|
|
609
494
|
}
|
|
@@ -632,17 +517,18 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
632
517
|
const session = new PtyTerminalSession(
|
|
633
518
|
{ command, cwd: effectiveCwd, cols: 120, rows: 40, scrollback: config.scrollbackLines },
|
|
634
519
|
);
|
|
635
|
-
sessionManager.add(command, session, name, reason, { id, noAutoCleanup: true });
|
|
636
520
|
|
|
637
521
|
const startTime = Date.now();
|
|
522
|
+
sessionManager.add(command, session, name, reason, { id, noAutoCleanup: true, startedAt: new Date(startTime) });
|
|
523
|
+
|
|
638
524
|
const monitor = new HeadlessDispatchMonitor(session, config, {
|
|
639
525
|
autoExitOnQuiet: handsFree?.autoExitOnQuiet !== false,
|
|
640
526
|
quietThreshold: handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
|
|
641
527
|
gracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
642
528
|
timeout,
|
|
529
|
+
startedAt: startTime,
|
|
643
530
|
}, makeMonitorCompletionCallback(pi, id, startTime));
|
|
644
|
-
|
|
645
|
-
registerHeadlessActive(id, command, reason, session, monitor, startTime);
|
|
531
|
+
registerHeadlessActive(id, command, reason, session, monitor, startTime, config);
|
|
646
532
|
|
|
647
533
|
return {
|
|
648
534
|
content: [{ type: "text", text: `Session dispatched in background (id: ${id}).\nYou'll be notified when it completes. User can /attach ${id} to watch.` }],
|
|
@@ -665,7 +551,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
665
551
|
};
|
|
666
552
|
}
|
|
667
553
|
|
|
668
|
-
if (
|
|
554
|
+
if (coordinator.isOverlayOpen()) {
|
|
669
555
|
return {
|
|
670
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." }],
|
|
671
557
|
isError: true,
|
|
@@ -677,45 +563,61 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
677
563
|
|
|
678
564
|
// ── Non-blocking path (hands-free or dispatch) ──
|
|
679
565
|
if (isNonBlocking && generatedSessionId) {
|
|
680
|
-
|
|
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
|
+
}
|
|
681
573
|
const overlayStartTime = Date.now();
|
|
682
574
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
: handsFree?.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
+
},
|
|
716
615
|
},
|
|
717
|
-
|
|
718
|
-
)
|
|
616
|
+
);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
coordinator.endOverlay();
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
719
621
|
|
|
720
622
|
setupDispatchCompletion(pi, overlayPromise, config, {
|
|
721
623
|
id: generatedSessionId,
|
|
@@ -740,7 +642,13 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
740
642
|
}
|
|
741
643
|
|
|
742
644
|
// ── Blocking (interactive) path ──
|
|
743
|
-
|
|
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
|
+
}
|
|
744
652
|
onUpdate?.({
|
|
745
653
|
content: [{ type: "text", text: `Opening: ${command}` }],
|
|
746
654
|
details: { exitCode: null, backgrounded: false, cancelled: false },
|
|
@@ -764,6 +672,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
764
672
|
handsFreeMaxTotalChars: handsFree?.maxTotalChars,
|
|
765
673
|
autoExitOnQuiet: handsFree?.autoExitOnQuiet,
|
|
766
674
|
autoExitGracePeriod: handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
675
|
+
streamingMode: mode === "hands-free",
|
|
767
676
|
onHandsFreeUpdate: mode === "hands-free"
|
|
768
677
|
? (update) => {
|
|
769
678
|
let statusText: string;
|
|
@@ -774,6 +683,9 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
774
683
|
case "exited":
|
|
775
684
|
statusText = `Session ${update.sessionId} exited`;
|
|
776
685
|
break;
|
|
686
|
+
case "killed":
|
|
687
|
+
statusText = `Session ${update.sessionId} killed`;
|
|
688
|
+
break;
|
|
777
689
|
default: {
|
|
778
690
|
const budgetInfo = update.budgetExhausted ? " [budget exhausted]" : "";
|
|
779
691
|
statusText = `Session ${update.sessionId} running (${formatDurationMs(update.runtime)})${budgetInfo}`;
|
|
@@ -794,6 +706,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
794
706
|
userTookOver: update.userTookOver,
|
|
795
707
|
},
|
|
796
708
|
});
|
|
709
|
+
pi.events.emit("interactive-shell:update", update);
|
|
797
710
|
}
|
|
798
711
|
: undefined,
|
|
799
712
|
handoffPreviewEnabled: handoffPreview?.enabled,
|
|
@@ -815,45 +728,17 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
815
728
|
},
|
|
816
729
|
);
|
|
817
730
|
} finally {
|
|
818
|
-
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
let summary: string;
|
|
822
|
-
if (result.transferred) {
|
|
823
|
-
const truncatedNote = result.transferred.truncated ? ` (truncated from ${result.transferred.totalLines} total lines)` : "";
|
|
824
|
-
summary = `Session output transferred (${result.transferred.lines.length} lines${truncatedNote}):\n\n${result.transferred.lines.join("\n")}`;
|
|
825
|
-
} else if (result.backgrounded) {
|
|
826
|
-
summary = `Session running in background (id: ${result.backgroundId}). User can reattach with /attach ${result.backgroundId}`;
|
|
827
|
-
} else if (result.cancelled) {
|
|
828
|
-
summary = "User killed the interactive session";
|
|
829
|
-
} else if (result.timedOut) {
|
|
830
|
-
summary = `Session killed after timeout (${timeout ?? "?"}ms)`;
|
|
831
|
-
} else {
|
|
832
|
-
const status = result.exitCode === 0 ? "successfully" : `with code ${result.exitCode}`;
|
|
833
|
-
summary = `Session ended ${status}`;
|
|
731
|
+
coordinator.endOverlay();
|
|
834
732
|
}
|
|
835
733
|
|
|
836
|
-
|
|
837
|
-
summary += "\n\nNote: User took over control during hands-free mode.";
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const warning = buildIdlePromptWarning(command, reason);
|
|
841
|
-
if (warning) {
|
|
842
|
-
summary += `\n\n${warning}`;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
if (!result.transferred && result.handoffPreview?.type === "tail" && result.handoffPreview.lines.length > 0) {
|
|
846
|
-
summary += `\n\nOverlay tail (${result.handoffPreview.when}, last ${result.handoffPreview.lines.length} lines):\n${result.handoffPreview.lines.join("\n")}`;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
return { content: [{ type: "text", text: summary }], details: result };
|
|
734
|
+
return { content: [{ type: "text", text: summarizeInteractiveResult(command, result, timeout, reason) }], details: result };
|
|
850
735
|
},
|
|
851
736
|
});
|
|
852
737
|
|
|
853
738
|
pi.registerCommand("attach", {
|
|
854
739
|
description: "Reattach to a background shell session",
|
|
855
740
|
handler: async (args, ctx) => {
|
|
856
|
-
if (
|
|
741
|
+
if (coordinator.isOverlayOpen()) {
|
|
857
742
|
ctx.ui.notify("An overlay is already open. Close it first.", "error");
|
|
858
743
|
return;
|
|
859
744
|
}
|
|
@@ -879,7 +764,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
879
764
|
targetId = choice.split(" - ")[0]!;
|
|
880
765
|
}
|
|
881
766
|
|
|
882
|
-
const monitor =
|
|
767
|
+
const monitor = coordinator.getMonitor(targetId);
|
|
883
768
|
|
|
884
769
|
const session = sessionManager.get(targetId);
|
|
885
770
|
if (!session) {
|
|
@@ -888,7 +773,10 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
888
773
|
}
|
|
889
774
|
|
|
890
775
|
const config = loadConfig(ctx.cwd);
|
|
891
|
-
|
|
776
|
+
if (!coordinator.beginOverlay()) {
|
|
777
|
+
ctx.ui.notify("An overlay is already open. Close it first.", "error");
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
892
780
|
try {
|
|
893
781
|
const result = await ctx.ui.custom<InteractiveShellResult>(
|
|
894
782
|
(tui, theme, _kb, done) =>
|
|
@@ -907,7 +795,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
907
795
|
if (monitor && !monitor.disposed) {
|
|
908
796
|
if (!result.backgrounded) {
|
|
909
797
|
monitor.handleExternalCompletion(result.exitCode, result.signal, result.completionOutput);
|
|
910
|
-
|
|
798
|
+
coordinator.deleteMonitor(targetId);
|
|
911
799
|
}
|
|
912
800
|
} else if (result.backgrounded) {
|
|
913
801
|
sessionManager.restartAutoCleanup(targetId);
|
|
@@ -915,7 +803,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
915
803
|
sessionManager.scheduleCleanup(targetId);
|
|
916
804
|
}
|
|
917
805
|
} finally {
|
|
918
|
-
|
|
806
|
+
coordinator.endOverlay();
|
|
919
807
|
}
|
|
920
808
|
},
|
|
921
809
|
});
|
|
@@ -953,11 +841,7 @@ export default function interactiveShellExtension(pi: ExtensionAPI) {
|
|
|
953
841
|
}
|
|
954
842
|
|
|
955
843
|
for (const tid of targetIds) {
|
|
956
|
-
|
|
957
|
-
if (monitor) {
|
|
958
|
-
monitor.dispose();
|
|
959
|
-
headlessMonitors.delete(tid);
|
|
960
|
-
}
|
|
844
|
+
coordinator.disposeMonitor(tid);
|
|
961
845
|
sessionManager.unregisterActive(tid, false);
|
|
962
846
|
sessionManager.remove(tid);
|
|
963
847
|
}
|
|
@@ -980,15 +864,15 @@ function setupDispatchCompletion(
|
|
|
980
864
|
timeout?: number;
|
|
981
865
|
handsFree?: { autoExitOnQuiet?: boolean; quietThreshold?: number; gracePeriod?: number };
|
|
982
866
|
overlayStartTime?: number;
|
|
867
|
+
onOverlayError?: () => { releaseId?: boolean; disposeMonitor?: boolean } | void;
|
|
983
868
|
},
|
|
984
869
|
): void {
|
|
985
870
|
const { id, mode, command, reason } = ctx;
|
|
986
871
|
|
|
987
872
|
overlayPromise.then((result) => {
|
|
988
|
-
|
|
873
|
+
coordinator.endOverlay();
|
|
989
874
|
|
|
990
|
-
const wasAgentInitiated =
|
|
991
|
-
agentHandledCompletion = false;
|
|
875
|
+
const wasAgentInitiated = coordinator.consumeAgentHandledCompletion(id);
|
|
992
876
|
|
|
993
877
|
if (result.transferred) {
|
|
994
878
|
const truncatedNote = result.transferred.truncated
|
|
@@ -1003,10 +887,11 @@ function setupDispatchCompletion(
|
|
|
1003
887
|
}, { triggerTurn: true });
|
|
1004
888
|
pi.events.emit("interactive-shell:transfer", { sessionId: id, transferred: result.transferred, exitCode: result.exitCode, signal: result.signal });
|
|
1005
889
|
sessionManager.unregisterActive(id, true);
|
|
890
|
+
coordinator.disposeMonitor(id);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
1006
893
|
|
|
1007
|
-
|
|
1008
|
-
if (remainingMonitor) { remainingMonitor.dispose(); headlessMonitors.delete(id); }
|
|
1009
|
-
} else if (mode === "dispatch" && result.backgrounded) {
|
|
894
|
+
if (mode === "dispatch" && result.backgrounded) {
|
|
1010
895
|
if (!wasAgentInitiated) {
|
|
1011
896
|
pi.sendMessage({
|
|
1012
897
|
customType: "interactive-shell-transfer",
|
|
@@ -1017,31 +902,32 @@ function setupDispatchCompletion(
|
|
|
1017
902
|
}
|
|
1018
903
|
sessionManager.unregisterActive(id, false);
|
|
1019
904
|
|
|
1020
|
-
const
|
|
905
|
+
const bgId = result.backgroundId!;
|
|
906
|
+
const existingMonitor = coordinator.getMonitor(id);
|
|
907
|
+
const bgSession = sessionManager.get(bgId);
|
|
908
|
+
if (!bgSession) return;
|
|
909
|
+
|
|
1021
910
|
if (existingMonitor && !existingMonitor.disposed) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1026
|
-
} else if (!existingMonitor) {
|
|
1027
|
-
const bgSession = sessionManager.get(result.backgroundId!);
|
|
1028
|
-
if (bgSession) {
|
|
1029
|
-
const bgId = result.backgroundId!;
|
|
1030
|
-
const bgStartTime = ctx.overlayStartTime ?? Date.now();
|
|
1031
|
-
const elapsed = ctx.overlayStartTime ? Date.now() - ctx.overlayStartTime : 0;
|
|
1032
|
-
const remainingTimeout = ctx.timeout ? Math.max(0, ctx.timeout - elapsed) : undefined;
|
|
1033
|
-
|
|
1034
|
-
const monitor = new HeadlessDispatchMonitor(bgSession.session, config, {
|
|
1035
|
-
autoExitOnQuiet: ctx.handsFree?.autoExitOnQuiet !== false,
|
|
1036
|
-
quietThreshold: ctx.handsFree?.quietThreshold ?? config.handsFreeQuietThreshold,
|
|
1037
|
-
gracePeriod: ctx.handsFree?.gracePeriod ?? config.autoExitGracePeriod,
|
|
1038
|
-
timeout: remainingTimeout,
|
|
1039
|
-
}, makeMonitorCompletionCallback(pi, bgId, bgStartTime));
|
|
1040
|
-
headlessMonitors.set(bgId, monitor);
|
|
1041
|
-
registerHeadlessActive(bgId, command, reason, bgSession.session, monitor, bgStartTime);
|
|
1042
|
-
}
|
|
911
|
+
coordinator.deleteMonitor(id);
|
|
912
|
+
registerHeadlessActive(bgId, command, reason, bgSession.session, existingMonitor, bgSession.startedAt.getTime(), config);
|
|
913
|
+
return;
|
|
1043
914
|
}
|
|
1044
|
-
|
|
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") {
|
|
1045
931
|
if (!wasAgentInitiated) {
|
|
1046
932
|
const content = buildResultNotification(id, result);
|
|
1047
933
|
pi.sendMessage({
|
|
@@ -1060,47 +946,17 @@ function setupDispatchCompletion(
|
|
|
1060
946
|
cancelled: result.cancelled,
|
|
1061
947
|
});
|
|
1062
948
|
sessionManager.unregisterActive(id, true);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
if (remainingMonitor) { remainingMonitor.dispose(); headlessMonitors.delete(id); }
|
|
949
|
+
coordinator.disposeMonitor(id);
|
|
950
|
+
return;
|
|
1066
951
|
}
|
|
1067
952
|
|
|
1068
|
-
|
|
1069
|
-
const staleMonitor = headlessMonitors.get(id);
|
|
1070
|
-
if (staleMonitor) { staleMonitor.dispose(); headlessMonitors.delete(id); }
|
|
1071
|
-
}
|
|
953
|
+
coordinator.disposeMonitor(id);
|
|
1072
954
|
}).catch(() => {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
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
|
+
}
|
|
1077
961
|
});
|
|
1078
962
|
}
|
|
1079
|
-
|
|
1080
|
-
function buildIdlePromptWarning(command: string, reason: string | undefined): string | null {
|
|
1081
|
-
if (!reason) return null;
|
|
1082
|
-
|
|
1083
|
-
const tasky = /\b(scan|check|review|summariz|analyz|inspect|audit|find|fix|refactor|debug|investigat|explore|enumerat|list)\b/i;
|
|
1084
|
-
if (!tasky.test(reason)) return null;
|
|
1085
|
-
|
|
1086
|
-
const trimmed = command.trim();
|
|
1087
|
-
const binaries = ["pi", "claude", "codex", "gemini", "cursor-agent"] as const;
|
|
1088
|
-
const bin = binaries.find((b) => trimmed === b || trimmed.startsWith(`${b} `));
|
|
1089
|
-
if (!bin) return null;
|
|
1090
|
-
|
|
1091
|
-
const rest = trimmed === bin ? "" : trimmed.slice(bin.length).trim();
|
|
1092
|
-
const hasQuotedPrompt = /["']/.test(rest);
|
|
1093
|
-
const hasKnownPromptFlag =
|
|
1094
|
-
/\b(-p|--print|--prompt|--prompt-interactive|-i|exec)\b/.test(rest) ||
|
|
1095
|
-
(bin === "pi" && /\b-p\b/.test(rest)) ||
|
|
1096
|
-
(bin === "codex" && /\bexec\b/.test(rest));
|
|
1097
|
-
|
|
1098
|
-
if (hasQuotedPrompt || hasKnownPromptFlag) return null;
|
|
1099
|
-
if (rest.length === 0 || /^(-{1,2}[A-Za-z0-9][A-Za-z0-9-]*(?:=[^\s]+)?\s*)+$/.test(rest)) {
|
|
1100
|
-
const examplePrompt = reason.replace(/\s+/g, " ").trim();
|
|
1101
|
-
const clipped = examplePrompt.length > 120 ? `${examplePrompt.slice(0, 117)}...` : examplePrompt;
|
|
1102
|
-
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}"\`.`;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
return null;
|
|
1106
|
-
}
|