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/pty-log.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
|
|
3
|
+
export const MAX_RAW_OUTPUT_SIZE = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export function trimRawOutput(rawOutput: string, lastStreamPosition: number): { rawOutput: string; lastStreamPosition: number } {
|
|
6
|
+
if (rawOutput.length <= MAX_RAW_OUTPUT_SIZE) {
|
|
7
|
+
return { rawOutput, lastStreamPosition };
|
|
8
|
+
}
|
|
9
|
+
const keepSize = Math.floor(MAX_RAW_OUTPUT_SIZE / 2);
|
|
10
|
+
const trimAmount = rawOutput.length - keepSize;
|
|
11
|
+
return {
|
|
12
|
+
rawOutput: rawOutput.substring(trimAmount),
|
|
13
|
+
lastStreamPosition: Math.max(0, lastStreamPosition - trimAmount),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sliceLogOutput(text: string, options: { offset?: number; limit?: number; stripAnsi?: boolean } = {}): {
|
|
18
|
+
slice: string;
|
|
19
|
+
totalLines: number;
|
|
20
|
+
totalChars: number;
|
|
21
|
+
sliceLineCount: number;
|
|
22
|
+
} {
|
|
23
|
+
let source = text;
|
|
24
|
+
if (options.stripAnsi !== false && source) {
|
|
25
|
+
source = stripVTControlCharacters(source);
|
|
26
|
+
}
|
|
27
|
+
if (!source) {
|
|
28
|
+
return { slice: "", totalLines: 0, totalChars: 0, sliceLineCount: 0 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
32
|
+
const lines = normalized.split("\n");
|
|
33
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
34
|
+
lines.pop();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const totalLines = lines.length;
|
|
38
|
+
const totalChars = source.length;
|
|
39
|
+
let start: number;
|
|
40
|
+
if (typeof options.offset === "number" && Number.isFinite(options.offset)) {
|
|
41
|
+
start = Math.max(0, Math.floor(options.offset));
|
|
42
|
+
} else if (options.limit !== undefined) {
|
|
43
|
+
const tailCount = Math.max(0, Math.floor(options.limit));
|
|
44
|
+
start = Math.max(totalLines - tailCount, 0);
|
|
45
|
+
} else {
|
|
46
|
+
start = 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const end = typeof options.limit === "number" && Number.isFinite(options.limit)
|
|
50
|
+
? start + Math.max(0, Math.floor(options.limit))
|
|
51
|
+
: undefined;
|
|
52
|
+
const selectedLines = lines.slice(start, end);
|
|
53
|
+
return {
|
|
54
|
+
slice: selectedLines.join("\n"),
|
|
55
|
+
totalLines,
|
|
56
|
+
totalChars,
|
|
57
|
+
sliceLineCount: selectedLines.length,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/pty-protocol.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// DSR (Device Status Report) - cursor position query: ESC[6n or ESC[?6n
|
|
2
|
+
const DSR_PATTERN = /\x1b\[\??6n/g;
|
|
3
|
+
|
|
4
|
+
/** Result of splitting PTY output around device-status-report cursor queries. */
|
|
5
|
+
export interface DsrSplit {
|
|
6
|
+
segments: Array<{ text: string; dsrAfter: boolean }>;
|
|
7
|
+
hasDsr: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function splitAroundDsr(input: string): DsrSplit {
|
|
11
|
+
const segments: Array<{ text: string; dsrAfter: boolean }> = [];
|
|
12
|
+
let lastIndex = 0;
|
|
13
|
+
let hasDsr = false;
|
|
14
|
+
const regex = new RegExp(DSR_PATTERN.source, "g");
|
|
15
|
+
let match: RegExpExecArray | null;
|
|
16
|
+
while ((match = regex.exec(input)) !== null) {
|
|
17
|
+
hasDsr = true;
|
|
18
|
+
if (match.index > lastIndex) {
|
|
19
|
+
segments.push({ text: input.slice(lastIndex, match.index), dsrAfter: true });
|
|
20
|
+
} else {
|
|
21
|
+
segments.push({ text: "", dsrAfter: true });
|
|
22
|
+
}
|
|
23
|
+
lastIndex = match.index + match[0].length;
|
|
24
|
+
}
|
|
25
|
+
if (lastIndex < input.length) {
|
|
26
|
+
segments.push({ text: input.slice(lastIndex), dsrAfter: false });
|
|
27
|
+
}
|
|
28
|
+
return { segments, hasDsr };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildCursorPositionResponse(row = 1, col = 1): string {
|
|
32
|
+
return `\x1b[${row};${col}R`;
|
|
33
|
+
}
|
package/pty-session.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import { chmodSync, statSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
1
|
import { stripVTControlCharacters } from "node:util";
|
|
5
2
|
import * as pty from "node-pty";
|
|
6
3
|
import type { IBufferCell, Terminal as XtermTerminal } from "@xterm/headless";
|
|
7
4
|
import xterm from "@xterm/headless";
|
|
8
5
|
import { SerializeAddon } from "@xterm/addon-serialize";
|
|
6
|
+
import { sliceLogOutput, trimRawOutput } from "./pty-log.js";
|
|
7
|
+
import { splitAroundDsr, buildCursorPositionResponse } from "./pty-protocol.js";
|
|
8
|
+
import { ensureSpawnHelperExec } from "./spawn-helper.js";
|
|
9
9
|
|
|
10
10
|
const Terminal = xterm.Terminal;
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
let spawnHelperChecked = false;
|
|
13
11
|
|
|
14
12
|
// Regex patterns for sanitizing terminal output (used by sanitizeLine for viewport rendering)
|
|
15
13
|
const OSC_REGEX = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
|
|
@@ -19,79 +17,6 @@ const CSI_REGEX = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
|
19
17
|
const ESC_SINGLE_REGEX = /\x1b[@-_]/g;
|
|
20
18
|
const CONTROL_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F]/g;
|
|
21
19
|
|
|
22
|
-
// DSR (Device Status Report) - cursor position query: ESC[6n or ESC[?6n
|
|
23
|
-
const DSR_PATTERN = /\x1b\[\??6n/g;
|
|
24
|
-
|
|
25
|
-
// Maximum raw output buffer size (1MB) - prevents unbounded memory growth
|
|
26
|
-
const MAX_RAW_OUTPUT_SIZE = 1024 * 1024;
|
|
27
|
-
|
|
28
|
-
interface DsrSplit {
|
|
29
|
-
segments: Array<{ text: string; dsrAfter: boolean }>;
|
|
30
|
-
hasDsr: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function splitAroundDsr(input: string): DsrSplit {
|
|
34
|
-
const segments: Array<{ text: string; dsrAfter: boolean }> = [];
|
|
35
|
-
let lastIndex = 0;
|
|
36
|
-
let hasDsr = false;
|
|
37
|
-
|
|
38
|
-
// Find all DSR requests and split around them
|
|
39
|
-
const regex = new RegExp(DSR_PATTERN.source, "g");
|
|
40
|
-
let match;
|
|
41
|
-
while ((match = regex.exec(input)) !== null) {
|
|
42
|
-
hasDsr = true;
|
|
43
|
-
// Text before this DSR
|
|
44
|
-
if (match.index > lastIndex) {
|
|
45
|
-
segments.push({ text: input.slice(lastIndex, match.index), dsrAfter: true });
|
|
46
|
-
} else {
|
|
47
|
-
// DSR at start or consecutive DSRs - add empty segment to trigger response
|
|
48
|
-
segments.push({ text: "", dsrAfter: true });
|
|
49
|
-
}
|
|
50
|
-
lastIndex = match.index + match[0].length;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Remaining text after last DSR (or entire string if no DSR)
|
|
54
|
-
if (lastIndex < input.length) {
|
|
55
|
-
segments.push({ text: input.slice(lastIndex), dsrAfter: false });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { segments, hasDsr };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function buildCursorPositionResponse(row = 1, col = 1): string {
|
|
62
|
-
return `\x1b[${row};${col}R`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function ensureSpawnHelperExec(): void {
|
|
66
|
-
if (spawnHelperChecked) return;
|
|
67
|
-
spawnHelperChecked = true;
|
|
68
|
-
if (process.platform !== "darwin") return;
|
|
69
|
-
|
|
70
|
-
let pkgPath: string;
|
|
71
|
-
try {
|
|
72
|
-
pkgPath = require.resolve("node-pty/package.json");
|
|
73
|
-
} catch {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const base = dirname(pkgPath);
|
|
78
|
-
const targets = [
|
|
79
|
-
join(base, "prebuilds", "darwin-arm64", "spawn-helper"),
|
|
80
|
-
join(base, "prebuilds", "darwin-x64", "spawn-helper"),
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
for (const target of targets) {
|
|
84
|
-
try {
|
|
85
|
-
const stats = statSync(target);
|
|
86
|
-
const mode = stats.mode | 0o111;
|
|
87
|
-
if ((stats.mode & 0o111) !== 0o111) {
|
|
88
|
-
chmodSync(target, mode);
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
20
|
|
|
96
21
|
function sanitizeLine(line: string): string {
|
|
97
22
|
let out = line;
|
|
@@ -234,13 +159,9 @@ export class PtyTerminalSession {
|
|
|
234
159
|
|
|
235
160
|
// Trim raw output buffer if it exceeds max size
|
|
236
161
|
private trimRawOutputIfNeeded(): void {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
this.rawOutput = this.rawOutput.substring(trimAmount);
|
|
241
|
-
// Adjust stream position to account for trimmed content
|
|
242
|
-
this.lastStreamPosition = Math.max(0, this.lastStreamPosition - trimAmount);
|
|
243
|
-
}
|
|
162
|
+
const trimmed = trimRawOutput(this.rawOutput, this.lastStreamPosition);
|
|
163
|
+
this.rawOutput = trimmed.rawOutput;
|
|
164
|
+
this.lastStreamPosition = trimmed.lastStreamPosition;
|
|
244
165
|
}
|
|
245
166
|
|
|
246
167
|
constructor(options: PtySessionOptions, events: PtySessionEvents = {}) {
|
|
@@ -367,14 +288,16 @@ export class PtyTerminalSession {
|
|
|
367
288
|
|
|
368
289
|
private notifyDataListeners(data: string): void {
|
|
369
290
|
this.dataHandler?.(data);
|
|
370
|
-
|
|
291
|
+
// Copy array to avoid issues if a listener unsubscribes during iteration
|
|
292
|
+
for (const listener of [...this.additionalDataListeners]) {
|
|
371
293
|
listener(data);
|
|
372
294
|
}
|
|
373
295
|
}
|
|
374
296
|
|
|
375
297
|
private notifyExitListeners(exitCode: number, signal?: number): void {
|
|
376
298
|
this.exitHandler?.(exitCode, signal);
|
|
377
|
-
|
|
299
|
+
// Copy array to avoid issues if a listener unsubscribes during iteration
|
|
300
|
+
for (const listener of [...this.additionalExitListeners]) {
|
|
378
301
|
listener(exitCode, signal);
|
|
379
302
|
}
|
|
380
303
|
}
|
|
@@ -626,53 +549,7 @@ export class PtyTerminalSession {
|
|
|
626
549
|
totalChars: number;
|
|
627
550
|
sliceLineCount: number;
|
|
628
551
|
} {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
// Strip ANSI by default
|
|
632
|
-
if (options.stripAnsi !== false && text) {
|
|
633
|
-
text = stripVTControlCharacters(text);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (!text) {
|
|
637
|
-
return { slice: "", totalLines: 0, totalChars: 0, sliceLineCount: 0 };
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Normalize line endings and split
|
|
641
|
-
const normalized = text.replace(/\r\n/g, "\n");
|
|
642
|
-
const lines = normalized.split("\n");
|
|
643
|
-
// Remove trailing empty line from split
|
|
644
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
645
|
-
lines.pop();
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const totalLines = lines.length;
|
|
649
|
-
const totalChars = text.length;
|
|
650
|
-
|
|
651
|
-
// Calculate start position
|
|
652
|
-
let start: number;
|
|
653
|
-
if (typeof options.offset === "number" && Number.isFinite(options.offset)) {
|
|
654
|
-
start = Math.max(0, Math.floor(options.offset));
|
|
655
|
-
} else if (options.limit !== undefined) {
|
|
656
|
-
// No offset but limit provided - return tail (last N lines)
|
|
657
|
-
const tailCount = Math.max(0, Math.floor(options.limit));
|
|
658
|
-
start = Math.max(totalLines - tailCount, 0);
|
|
659
|
-
} else {
|
|
660
|
-
start = 0;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// Calculate end position
|
|
664
|
-
const end = typeof options.limit === "number" && Number.isFinite(options.limit)
|
|
665
|
-
? start + Math.max(0, Math.floor(options.limit))
|
|
666
|
-
: undefined;
|
|
667
|
-
|
|
668
|
-
const selectedLines = lines.slice(start, end);
|
|
669
|
-
|
|
670
|
-
return {
|
|
671
|
-
slice: selectedLines.join("\n"),
|
|
672
|
-
totalLines,
|
|
673
|
-
totalChars,
|
|
674
|
-
sliceLineCount: selectedLines.length,
|
|
675
|
-
};
|
|
552
|
+
return sliceLogOutput(this.rawOutput, options);
|
|
676
553
|
}
|
|
677
554
|
|
|
678
555
|
scrollUp(lines: number): void {
|
package/reattach-overlay.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import type { Component, Focusable, TUI } from "@mariozechner/pi-tui";
|
|
5
2
|
import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
6
3
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
@@ -15,6 +12,7 @@ import {
|
|
|
15
12
|
FOOTER_LINES_COMPACT,
|
|
16
13
|
FOOTER_LINES_DIALOG,
|
|
17
14
|
} from "./types.js";
|
|
15
|
+
import { captureCompletionOutput, captureTransferOutput, maybeBuildHandoffPreview, maybeWriteHandoffSnapshot } from "./handoff-utils.js";
|
|
18
16
|
|
|
19
17
|
export class ReattachOverlay implements Component, Focusable {
|
|
20
18
|
focused = false;
|
|
@@ -103,87 +101,20 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
private captureCompletionOutput(): InteractiveShellResult["completionOutput"] {
|
|
106
|
-
|
|
107
|
-
lines: this.config.completionNotifyLines,
|
|
108
|
-
ansi: false,
|
|
109
|
-
maxChars: this.config.completionNotifyMaxChars,
|
|
110
|
-
});
|
|
111
|
-
return {
|
|
112
|
-
lines: result.lines,
|
|
113
|
-
totalLines: result.totalLinesInBuffer,
|
|
114
|
-
truncated: result.lines.length < result.totalLinesInBuffer || result.truncatedByChars,
|
|
115
|
-
};
|
|
104
|
+
return captureCompletionOutput(this.session, this.config);
|
|
116
105
|
}
|
|
117
106
|
|
|
118
107
|
/** Capture output for transfer action (Ctrl+T or dialog) */
|
|
119
108
|
private captureTransferOutput(): InteractiveShellResult["transferred"] {
|
|
120
|
-
|
|
121
|
-
const maxChars = this.config.transferMaxChars;
|
|
122
|
-
|
|
123
|
-
const result = this.session.getTailLines({
|
|
124
|
-
lines: maxLines,
|
|
125
|
-
ansi: false,
|
|
126
|
-
maxChars,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const truncated = result.lines.length < result.totalLinesInBuffer || result.truncatedByChars;
|
|
130
|
-
|
|
131
|
-
return {
|
|
132
|
-
lines: result.lines,
|
|
133
|
-
totalLines: result.totalLinesInBuffer,
|
|
134
|
-
truncated,
|
|
135
|
-
};
|
|
109
|
+
return captureTransferOutput(this.session, this.config);
|
|
136
110
|
}
|
|
137
111
|
|
|
138
112
|
private maybeBuildHandoffPreview(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoffPreview"] | undefined {
|
|
139
|
-
|
|
140
|
-
const lines = this.config.handoffPreviewLines;
|
|
141
|
-
const maxChars = this.config.handoffPreviewMaxChars;
|
|
142
|
-
if (lines <= 0 || maxChars <= 0) return undefined;
|
|
143
|
-
|
|
144
|
-
const result = this.session.getTailLines({
|
|
145
|
-
lines,
|
|
146
|
-
ansi: false,
|
|
147
|
-
maxChars,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
return { type: "tail", when, lines: result.lines };
|
|
113
|
+
return maybeBuildHandoffPreview(this.session, when, this.config);
|
|
151
114
|
}
|
|
152
115
|
|
|
153
116
|
private maybeWriteHandoffSnapshot(when: "exit" | "detach" | "kill" | "transfer"): InteractiveShellResult["handoff"] | undefined {
|
|
154
|
-
|
|
155
|
-
const lines = this.config.handoffSnapshotLines;
|
|
156
|
-
const maxChars = this.config.handoffSnapshotMaxChars;
|
|
157
|
-
if (lines <= 0 || maxChars <= 0) return undefined;
|
|
158
|
-
|
|
159
|
-
const baseDir = join(homedir(), ".pi", "agent", "cache", "interactive-shell");
|
|
160
|
-
mkdirSync(baseDir, { recursive: true });
|
|
161
|
-
|
|
162
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
163
|
-
const pid = this.session.pid;
|
|
164
|
-
const filename = `snapshot-${timestamp}-pid${pid}.log`;
|
|
165
|
-
const transcriptPath = join(baseDir, filename);
|
|
166
|
-
|
|
167
|
-
const tailResult = this.session.getTailLines({
|
|
168
|
-
lines,
|
|
169
|
-
ansi: this.config.ansiReemit,
|
|
170
|
-
maxChars,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const header = [
|
|
174
|
-
`# interactive-shell snapshot (${when})`,
|
|
175
|
-
`time: ${new Date().toISOString()}`,
|
|
176
|
-
`command: ${this.bgSession.command}`,
|
|
177
|
-
`pid: ${pid}`,
|
|
178
|
-
`exitCode: ${this.session.exitCode ?? ""}`,
|
|
179
|
-
`signal: ${this.session.signal ?? ""}`,
|
|
180
|
-
`lines: ${tailResult.lines.length} (requested ${lines}, maxChars ${maxChars})`,
|
|
181
|
-
"",
|
|
182
|
-
].join("\n");
|
|
183
|
-
|
|
184
|
-
writeFileSync(transcriptPath, header + tailResult.lines.join("\n") + "\n", { encoding: "utf-8" });
|
|
185
|
-
|
|
186
|
-
return { type: "snapshot", when, transcriptPath, linesWritten: tailResult.lines.length };
|
|
117
|
+
return maybeWriteHandoffSnapshot(this.session, when, this.config, { command: this.bgSession.command });
|
|
187
118
|
}
|
|
188
119
|
|
|
189
120
|
private finishAndClose(): void {
|
|
@@ -358,6 +289,7 @@ export class ReattachOverlay implements Component, Focusable {
|
|
|
358
289
|
}
|
|
359
290
|
|
|
360
291
|
render(width: number): string[] {
|
|
292
|
+
width = Math.max(4, width);
|
|
361
293
|
const th = this.theme;
|
|
362
294
|
const border = (s: string) => th.fg("border", s);
|
|
363
295
|
const accent = (s: string) => th.fg("accent", s);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { HeadlessDispatchMonitor } from "./headless-monitor.js";
|
|
2
|
+
|
|
3
|
+
/** Centralizes overlay, monitor, widget, and completion-suppression state for the extension runtime. */
|
|
4
|
+
export class InteractiveShellCoordinator {
|
|
5
|
+
private overlayOpen = false;
|
|
6
|
+
private headlessMonitors = new Map<string, HeadlessDispatchMonitor>();
|
|
7
|
+
private bgWidgetCleanup: (() => void) | null = null;
|
|
8
|
+
private agentHandledCompletion = new Set<string>();
|
|
9
|
+
|
|
10
|
+
isOverlayOpen(): boolean {
|
|
11
|
+
return this.overlayOpen;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beginOverlay(): boolean {
|
|
15
|
+
if (this.overlayOpen) return false;
|
|
16
|
+
this.overlayOpen = true;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
endOverlay(): void {
|
|
21
|
+
this.overlayOpen = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
markAgentHandledCompletion(sessionId: string): void {
|
|
25
|
+
this.agentHandledCompletion.add(sessionId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
consumeAgentHandledCompletion(sessionId: string): boolean {
|
|
29
|
+
const had = this.agentHandledCompletion.has(sessionId);
|
|
30
|
+
this.agentHandledCompletion.delete(sessionId);
|
|
31
|
+
return had;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setMonitor(id: string, monitor: HeadlessDispatchMonitor): void {
|
|
35
|
+
this.headlessMonitors.set(id, monitor);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getMonitor(id: string): HeadlessDispatchMonitor | undefined {
|
|
39
|
+
return this.headlessMonitors.get(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
deleteMonitor(id: string): void {
|
|
43
|
+
this.headlessMonitors.delete(id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disposeMonitor(id: string): void {
|
|
47
|
+
const monitor = this.headlessMonitors.get(id);
|
|
48
|
+
if (!monitor) return;
|
|
49
|
+
monitor.dispose();
|
|
50
|
+
this.headlessMonitors.delete(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
disposeAllMonitors(): void {
|
|
54
|
+
for (const monitor of this.headlessMonitors.values()) {
|
|
55
|
+
monitor.dispose();
|
|
56
|
+
}
|
|
57
|
+
this.headlessMonitors.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
replaceBackgroundWidgetCleanup(cleanup: (() => void) | null): void {
|
|
61
|
+
this.bgWidgetCleanup?.();
|
|
62
|
+
this.bgWidgetCleanup = cleanup;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clearBackgroundWidget(): void {
|
|
66
|
+
this.bgWidgetCleanup?.();
|
|
67
|
+
this.bgWidgetCleanup = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/scripts/install.js
CHANGED
|
@@ -9,8 +9,19 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const packageRoot = join(__dirname, "..");
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
const
|
|
12
|
+
function getAgentDir() {
|
|
13
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
14
|
+
if (envDir) {
|
|
15
|
+
if (envDir === "~") return homedir();
|
|
16
|
+
if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
|
|
17
|
+
return envDir;
|
|
18
|
+
}
|
|
19
|
+
return join(homedir(), ".pi", "agent");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const agentDir = getAgentDir();
|
|
23
|
+
const EXTENSION_DIR = join(agentDir, "extensions", "interactive-shell");
|
|
24
|
+
const SKILL_DIR = join(agentDir, "skills", "interactive-shell");
|
|
14
25
|
|
|
15
26
|
function log(msg) {
|
|
16
27
|
console.log(`[pi-interactive-shell] ${msg}`);
|
|
@@ -90,7 +101,11 @@ function main() {
|
|
|
90
101
|
log("Restart pi to load the extension.");
|
|
91
102
|
log("");
|
|
92
103
|
log("Usage:");
|
|
93
|
-
log(' interactive_shell({ command: \'pi "Fix all bugs"\', mode: "
|
|
104
|
+
log(' interactive_shell({ command: \'pi "Fix all bugs"\', mode: "dispatch" })');
|
|
105
|
+
log("");
|
|
106
|
+
log("Bundled example prompts live under extensions/interactive-shell/examples/prompts/");
|
|
107
|
+
log("Bundled example skills live under extensions/interactive-shell/examples/skills/");
|
|
108
|
+
log("Copy them into your prompts/skills directories if you want local slash commands or skill copies.");
|
|
94
109
|
log("");
|
|
95
110
|
}
|
|
96
111
|
|
package/session-manager.ts
CHANGED
|
@@ -191,35 +191,45 @@ export class ShellSessionManager {
|
|
|
191
191
|
add(command: string, session: PtyTerminalSession, name?: string, reason?: string, options?: { id?: string; noAutoCleanup?: boolean; startedAt?: Date }): string {
|
|
192
192
|
const id = options?.id ?? generateSessionId(name);
|
|
193
193
|
if (options?.id) usedIds.add(id);
|
|
194
|
-
|
|
194
|
+
const entry: BackgroundSession = {
|
|
195
195
|
id,
|
|
196
196
|
name: name || deriveSessionName(command),
|
|
197
197
|
command,
|
|
198
198
|
reason,
|
|
199
199
|
session,
|
|
200
200
|
startedAt: options?.startedAt ?? new Date(),
|
|
201
|
-
}
|
|
201
|
+
};
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
this.storeBackgroundEntry(entry, options?.noAutoCleanup === true);
|
|
204
|
+
return id;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
restore(entry: BackgroundSession, options?: { noAutoCleanup?: boolean }): void {
|
|
208
|
+
usedIds.add(entry.id);
|
|
209
|
+
this.storeBackgroundEntry(entry, options?.noAutoCleanup === true);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private storeBackgroundEntry(entry: BackgroundSession, noAutoCleanup: boolean): void {
|
|
213
|
+
this.sessions.set(entry.id, entry);
|
|
214
|
+
entry.session.setEventHandlers({});
|
|
204
215
|
|
|
205
|
-
if (!
|
|
216
|
+
if (!noAutoCleanup) {
|
|
206
217
|
const checkExit = setInterval(() => {
|
|
207
|
-
if (session.exited) {
|
|
218
|
+
if (entry.session.exited) {
|
|
208
219
|
clearInterval(checkExit);
|
|
209
|
-
this.exitWatchers.delete(id);
|
|
220
|
+
this.exitWatchers.delete(entry.id);
|
|
210
221
|
this.notifyChange();
|
|
211
222
|
const cleanupTimer = setTimeout(() => {
|
|
212
|
-
this.cleanupTimers.delete(id);
|
|
213
|
-
this.remove(id);
|
|
223
|
+
this.cleanupTimers.delete(entry.id);
|
|
224
|
+
this.remove(entry.id);
|
|
214
225
|
}, 30000);
|
|
215
|
-
this.cleanupTimers.set(id, cleanupTimer);
|
|
226
|
+
this.cleanupTimers.set(entry.id, cleanupTimer);
|
|
216
227
|
}
|
|
217
228
|
}, 1000);
|
|
218
|
-
this.exitWatchers.set(id, checkExit);
|
|
229
|
+
this.exitWatchers.set(entry.id, checkExit);
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
this.notifyChange();
|
|
222
|
-
return id;
|
|
223
233
|
}
|
|
224
234
|
|
|
225
235
|
take(id: string): BackgroundSession | undefined {
|