agent-repl 0.1.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/cli.js +122 -0
- package/dist/host.js +24 -0
- package/dist/mcp.js +109 -0
- package/dist/pi-compat/LICENSE +21 -0
- package/dist/pi-compat/child-process.js +105 -0
- package/dist/pi-compat/exec.js +76 -0
- package/dist/pi-compat/index.js +3 -0
- package/dist/pi-compat/string-enum.js +22 -0
- package/dist/pi-compat/truncate.js +216 -0
- package/dist/pi-repl.js +2583 -0
- package/dist/shared/REPL_SESSION_RECORD_PROTOCOL.md +105 -0
- package/dist/shared/repl-control-files.js +170 -0
- package/dist/shared/repl-cpp-driver.hxx +164 -0
- package/dist/shared/repl-cpp.js +107 -0
- package/dist/shared/repl-gnuplot-processes.js +180 -0
- package/dist/shared/repl-gnuplot.js +58 -0
- package/dist/shared/repl-history.js +37 -0
- package/dist/shared/repl-m-language.js +47 -0
- package/dist/shared/repl-session-record.js +623 -0
- package/dist/shared/repl-session-stop.js +278 -0
- package/dist/shared/repl-submission-display.js +249 -0
- package/dist/text.js +20 -0
- package/package.json +59 -0
package/dist/pi-repl.js
ADDED
|
@@ -0,0 +1,2583 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-from-pi-repl.mjs from pi-repl 0.8.1 (0926b0b). Do not edit by hand:
|
|
2
|
+
// change pi-repl and re-sync. Identical to pi-repl's index.ts except that its Pi
|
|
3
|
+
// imports come from ./pi-compat/index.js.
|
|
4
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateTail, } from "./pi-compat/index.js";
|
|
5
|
+
import { StringEnum } from "./pi-compat/index.js";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { existsSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { REPL_SESSION_RECORD_ID_OPTION, REPL_SESSION_RECORD_VERSION, REPL_SESSION_RECORD_VERSION_OPTION, acquireReplSessionSendLease, createReplSessionRecordId, ensureReplSessionRecord, getReplSessionRecordPath, isValidReplSessionRecordId, readReplSessionRecord, renderReplSessionRecordMarkdown, upsertReplSessionRecordEntry, } from "./shared/repl-session-record.js";
|
|
12
|
+
import { createReplSubmissionDisplay, normalizeReplSubmissionEchoMode, stripReplSubmissionDisplay, } from "./shared/repl-submission-display.js";
|
|
13
|
+
import { cleanupPrivateReplControlFiles, createPrivateReplControlFiles, } from "./shared/repl-control-files.js";
|
|
14
|
+
import { createPrivateReplHistoryFile } from "./shared/repl-history.js";
|
|
15
|
+
import { stopVerifiedReplSession } from "./shared/repl-session-stop.js";
|
|
16
|
+
import { mStringLiteral, buildMLanguageControlSource, buildMLanguageDriverSource } from "./shared/repl-m-language.js";
|
|
17
|
+
import { buildGnuplotSubmissionLine, buildGnuplotGuardSource, buildGnuplotDriverSource } from "./shared/repl-gnuplot.js";
|
|
18
|
+
import { cppInclude, buildCppDriverSource, buildCppRequest, createCppControlCleanup, prepareCppSubmission, readCppCompletion } from "./shared/repl-cpp.js";
|
|
19
|
+
import { GNUPLOT_OWNER_ENV, GNUPLOT_OWNER_OPTION } from "./shared/repl-gnuplot-processes.js";
|
|
20
|
+
const SUPPORTED_RUNTIMES = ["julia", "python", "ipython", "r", "ghci", "clojure", "clj", "ruby", "java", "octave", "matlab", "gnuplot", "cpp", "bun"];
|
|
21
|
+
const DEFAULT_PYTHON_SESSION = "pi-repl-python";
|
|
22
|
+
const DEFAULT_JULIA_SESSION = "pi-repl-julia";
|
|
23
|
+
const DEFAULT_R_SESSION = "pi-repl-r";
|
|
24
|
+
const DEFAULT_GHCI_SESSION = "pi-repl-ghci";
|
|
25
|
+
const DEFAULT_CLOJURE_SESSION = "pi-repl-clojure";
|
|
26
|
+
const DEFAULT_RUBY_SESSION = "pi-repl-ruby";
|
|
27
|
+
const DEFAULT_JAVA_SESSION = "pi-repl-java";
|
|
28
|
+
const DEFAULT_OCTAVE_SESSION = "pi-repl-octave";
|
|
29
|
+
const DEFAULT_MATLAB_SESSION = "pi-repl-matlab";
|
|
30
|
+
const DEFAULT_GNUPLOT_SESSION = "pi-repl-gnuplot";
|
|
31
|
+
const DEFAULT_CPP_SESSION = "pi-repl-cpp";
|
|
32
|
+
const DEFAULT_CAPTURE_LINES = 20;
|
|
33
|
+
const DEFAULT_STARTUP_WAIT_MS = 20_000;
|
|
34
|
+
const MAX_STARTUP_WAIT_MS = 120_000;
|
|
35
|
+
const DEFAULT_STARTUP_POLL_MS = 250;
|
|
36
|
+
const DEFAULT_REPL_SEND_TIMEOUT_MS = 20_000;
|
|
37
|
+
const MAX_REPL_SEND_TIMEOUT_MS = 120_000;
|
|
38
|
+
const REPL_SEND_POLL_MS = 100;
|
|
39
|
+
const REPL_SEND_CAPTURE_LINES = 5_000;
|
|
40
|
+
// Optional private root override, also used by isolated integration tests.
|
|
41
|
+
const REPL_CONTROL_OPTIONS = { root: process.env.PI_REPL_CONTROL_ROOT };
|
|
42
|
+
const REPL_HISTORY_FILTER_SCRIPT = String.raw `
|
|
43
|
+
let line = [];
|
|
44
|
+
let col = 0;
|
|
45
|
+
let pendingEscape = false;
|
|
46
|
+
let csi = null;
|
|
47
|
+
let osc = false;
|
|
48
|
+
let oscEsc = false;
|
|
49
|
+
|
|
50
|
+
function ensureCol() {
|
|
51
|
+
while (line.length < col) line.push(' ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeText(text) {
|
|
55
|
+
for (const ch of text) {
|
|
56
|
+
ensureCol();
|
|
57
|
+
line[col] = ch;
|
|
58
|
+
col += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function clearToEndOfLine() {
|
|
63
|
+
line.length = Math.min(line.length, col);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function emitCurrentLine() {
|
|
67
|
+
process.stdout.write(line.join('').replace(/[ \t]+$/g, '') + '\n');
|
|
68
|
+
line = [];
|
|
69
|
+
col = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function firstParam(buffer) {
|
|
73
|
+
const raw = buffer.split(';', 1)[0];
|
|
74
|
+
const value = Number.parseInt(raw || '1', 10);
|
|
75
|
+
return Number.isFinite(value) && value > 0 ? value : 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function handleCsi(finalChar, buffer) {
|
|
79
|
+
const n = firstParam(buffer);
|
|
80
|
+
if (finalChar === 'C') {
|
|
81
|
+
col += n;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (finalChar === 'D') {
|
|
85
|
+
col = Math.max(0, col - n);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (finalChar === 'G') {
|
|
89
|
+
col = Math.max(0, n - 1);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (finalChar === 'K') {
|
|
93
|
+
const mode = buffer === '2' ? 2 : buffer === '1' ? 1 : 0;
|
|
94
|
+
if (mode === 2) {
|
|
95
|
+
line = [];
|
|
96
|
+
col = 0;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (mode === 1) {
|
|
100
|
+
for (let i = 0; i < col; i += 1) line[i] = ' ';
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
clearToEndOfLine();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleChar(ch) {
|
|
108
|
+
if (osc) {
|
|
109
|
+
if (oscEsc && ch === '\\') {
|
|
110
|
+
osc = false;
|
|
111
|
+
oscEsc = false;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
oscEsc = ch === '\u001b';
|
|
115
|
+
if (ch === '\u0007') {
|
|
116
|
+
osc = false;
|
|
117
|
+
oscEsc = false;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (csi !== null) {
|
|
123
|
+
if (ch >= '@' && ch <= '~') {
|
|
124
|
+
handleCsi(ch, csi);
|
|
125
|
+
csi = null;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
csi += ch;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (pendingEscape) {
|
|
133
|
+
pendingEscape = false;
|
|
134
|
+
if (ch === '[') {
|
|
135
|
+
csi = '';
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (ch === ']') {
|
|
139
|
+
osc = true;
|
|
140
|
+
oscEsc = false;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (ch === '\u001b') {
|
|
147
|
+
pendingEscape = true;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (ch === '\r') {
|
|
151
|
+
col = 0;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (ch === '\n') {
|
|
155
|
+
emitCurrentLine();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (ch === '\b' || ch === '\u007f') {
|
|
159
|
+
col = Math.max(0, col - 1);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (ch === '\t') {
|
|
163
|
+
writeText('\t');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (ch < ' ') {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
writeText(ch);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
process.stdin.setEncoding('utf8');
|
|
173
|
+
process.stdin.on('data', chunk => {
|
|
174
|
+
for (const ch of chunk) handleChar(ch);
|
|
175
|
+
});
|
|
176
|
+
process.stdin.on('end', () => {
|
|
177
|
+
if (line.length > 0) {
|
|
178
|
+
process.stdout.write(line.join('').replace(/[ \t]+$/g, '') + '\n');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
`;
|
|
182
|
+
const REPL_RUNTIME_OPTION = "@pi_repl_runtime";
|
|
183
|
+
const REPL_HISTORY_OPTION = "@pi_repl_history_path";
|
|
184
|
+
const REPL_SEND_PARAMS = Type.Object({
|
|
185
|
+
code: Type.String({ description: "Python, IPython, Julia, R, GHCi, Clojure, Ruby, Java, Octave, MATLAB, gnuplot, or C++ code to execute in the shared REPL session." }),
|
|
186
|
+
target: Type.Optional(Type.String({
|
|
187
|
+
description: "Optional target REPL: python, julia, r, ghci, clojure, ruby, java, octave, matlab, gnuplot, or cpp. If omitted, repl_send uses the shared Python/IPython session.",
|
|
188
|
+
})),
|
|
189
|
+
timeoutMs: Type.Optional(Type.Number({
|
|
190
|
+
description: "Maximum time to wait for completion in milliseconds (default 20000).",
|
|
191
|
+
minimum: 1000,
|
|
192
|
+
maximum: MAX_REPL_SEND_TIMEOUT_MS,
|
|
193
|
+
})),
|
|
194
|
+
echoMode: Type.Optional(StringEnum(["off", "summary", "full"], { description: "How much submitted code to echo visibly in the raw REPL pane. Defaults to the current /repl echo setting (summary initially, unless PI_REPL_ECHO_MODE overrides it). Summary shows short submissions in full and truncates longer ones; Full has larger bounds and writes source code into persistent raw terminal history." })),
|
|
195
|
+
});
|
|
196
|
+
let replSubmissionEchoMode = normalizeReplSubmissionEchoMode(process.env.PI_REPL_ECHO_MODE);
|
|
197
|
+
function resolveReplSubmissionEchoMode(value) {
|
|
198
|
+
return normalizeReplSubmissionEchoMode(value, replSubmissionEchoMode);
|
|
199
|
+
}
|
|
200
|
+
// Clean-record label for repl_send submissions. A host running these tools
|
|
201
|
+
// outside Pi can name its agent; read on each send so it can be set after startup.
|
|
202
|
+
function getToolSubmissionLabel() {
|
|
203
|
+
return process.env.PI_REPL_AGENT_LABEL?.trim() || "Pi";
|
|
204
|
+
}
|
|
205
|
+
const REPL_START_RUNTIMES = ["python", "ipython", "julia", "r", "ghci", "clojure", "ruby", "java", "octave", "matlab", "gnuplot", "cpp"];
|
|
206
|
+
const REPL_START_PARAMS = Type.Object({
|
|
207
|
+
runtime: StringEnum(REPL_START_RUNTIMES, {
|
|
208
|
+
description: "Runtime to start explicitly. Python and IPython share one session; an existing session is reused without switching its interpreter.",
|
|
209
|
+
}),
|
|
210
|
+
timeoutMs: Type.Optional(Type.Number({
|
|
211
|
+
description: "How long to wait for a normal prompt in milliseconds (default 20000). Timeout leaves the session running and reports ready=false.",
|
|
212
|
+
minimum: 1000,
|
|
213
|
+
maximum: MAX_STARTUP_WAIT_MS,
|
|
214
|
+
})),
|
|
215
|
+
});
|
|
216
|
+
const REPL_STATUS_PARAMS = Type.Object({
|
|
217
|
+
target: Type.Optional(Type.String({
|
|
218
|
+
description: "Optional session target: python, julia, r, ghci, ruby, java, clojure, octave, matlab, gnuplot, or cpp. If omitted, report all shared REPL sessions.",
|
|
219
|
+
})),
|
|
220
|
+
});
|
|
221
|
+
function tokenizeArgs(args) {
|
|
222
|
+
const parts = args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
|
|
223
|
+
if (!parts)
|
|
224
|
+
return [];
|
|
225
|
+
return parts
|
|
226
|
+
.map((token) => {
|
|
227
|
+
if ((token.startsWith('"') && token.endsWith('"') && token.length >= 2) ||
|
|
228
|
+
(token.startsWith("'") && token.endsWith("'") && token.length >= 2)) {
|
|
229
|
+
return token.slice(1, -1);
|
|
230
|
+
}
|
|
231
|
+
return token;
|
|
232
|
+
})
|
|
233
|
+
.map((token) => token.trim())
|
|
234
|
+
.filter((token) => token.length > 0);
|
|
235
|
+
}
|
|
236
|
+
function isSupportedRuntime(value) {
|
|
237
|
+
return SUPPORTED_RUNTIMES.includes(value);
|
|
238
|
+
}
|
|
239
|
+
function isPythonRuntime(value) {
|
|
240
|
+
return value === "python" || value === "ipython";
|
|
241
|
+
}
|
|
242
|
+
function isClojureRuntime(value) {
|
|
243
|
+
return value === "clojure" || value === "clj";
|
|
244
|
+
}
|
|
245
|
+
function isSessionTargetRuntime(value) {
|
|
246
|
+
return value === "clj" || REPL_START_RUNTIMES.includes(value);
|
|
247
|
+
}
|
|
248
|
+
function toSessionSelector(runtime) {
|
|
249
|
+
if (runtime === "julia")
|
|
250
|
+
return "julia";
|
|
251
|
+
if (runtime === "r")
|
|
252
|
+
return "r";
|
|
253
|
+
if (runtime === "ghci")
|
|
254
|
+
return "ghci";
|
|
255
|
+
if (runtime === "clojure" || runtime === "clj")
|
|
256
|
+
return "clojure";
|
|
257
|
+
if (runtime === "ruby")
|
|
258
|
+
return "ruby";
|
|
259
|
+
if (runtime === "java")
|
|
260
|
+
return "java";
|
|
261
|
+
if (runtime === "octave" || runtime === "matlab" || runtime === "gnuplot" || runtime === "cpp")
|
|
262
|
+
return runtime;
|
|
263
|
+
return "python";
|
|
264
|
+
}
|
|
265
|
+
function getSessionNameForSelector(selector) {
|
|
266
|
+
if (selector === "julia")
|
|
267
|
+
return DEFAULT_JULIA_SESSION;
|
|
268
|
+
if (selector === "r")
|
|
269
|
+
return DEFAULT_R_SESSION;
|
|
270
|
+
if (selector === "ghci")
|
|
271
|
+
return DEFAULT_GHCI_SESSION;
|
|
272
|
+
if (selector === "clojure")
|
|
273
|
+
return DEFAULT_CLOJURE_SESSION;
|
|
274
|
+
if (selector === "ruby")
|
|
275
|
+
return DEFAULT_RUBY_SESSION;
|
|
276
|
+
if (selector === "java")
|
|
277
|
+
return DEFAULT_JAVA_SESSION;
|
|
278
|
+
if (selector === "octave")
|
|
279
|
+
return DEFAULT_OCTAVE_SESSION;
|
|
280
|
+
if (selector === "matlab")
|
|
281
|
+
return DEFAULT_MATLAB_SESSION;
|
|
282
|
+
if (selector === "gnuplot")
|
|
283
|
+
return DEFAULT_GNUPLOT_SESSION;
|
|
284
|
+
if (selector === "cpp")
|
|
285
|
+
return DEFAULT_CPP_SESSION;
|
|
286
|
+
return DEFAULT_PYTHON_SESSION;
|
|
287
|
+
}
|
|
288
|
+
function sanitizeNamePart(value) {
|
|
289
|
+
return value
|
|
290
|
+
.trim()
|
|
291
|
+
.toLowerCase()
|
|
292
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
293
|
+
.replace(/^-+|-+$/g, "")
|
|
294
|
+
.replace(/-{2,}/g, "-");
|
|
295
|
+
}
|
|
296
|
+
function buildSessionName(runtime, name) {
|
|
297
|
+
const base = `pi-repl-${runtime}`;
|
|
298
|
+
if (!name)
|
|
299
|
+
return base;
|
|
300
|
+
const safeName = sanitizeNamePart(name);
|
|
301
|
+
return safeName ? `${base}-${safeName}` : base;
|
|
302
|
+
}
|
|
303
|
+
function formatUsage() {
|
|
304
|
+
return [
|
|
305
|
+
"Usage:",
|
|
306
|
+
" /repl python",
|
|
307
|
+
" /repl ipython",
|
|
308
|
+
" /repl julia",
|
|
309
|
+
" /repl r",
|
|
310
|
+
" /repl ghci",
|
|
311
|
+
" /repl clojure",
|
|
312
|
+
" /repl ruby",
|
|
313
|
+
" /repl java",
|
|
314
|
+
" /repl octave",
|
|
315
|
+
" /repl matlab",
|
|
316
|
+
" /repl gnuplot",
|
|
317
|
+
" /repl cpp",
|
|
318
|
+
" /repl echo [off|summary|full]",
|
|
319
|
+
" /repl status [python|julia|r|ghci|clojure|ruby|java|octave|matlab|gnuplot|cpp]",
|
|
320
|
+
" /repl env [python]",
|
|
321
|
+
" /repl attach [python|julia|r|ghci|clojure|ruby|java|octave|matlab|gnuplot|cpp]",
|
|
322
|
+
" /repl export [python|julia|r|ghci|clojure|ruby|java|octave|matlab|gnuplot|cpp]",
|
|
323
|
+
" /repl stop [python|julia|r|ghci|clojure|ruby|java|octave|matlab|gnuplot|cpp]",
|
|
324
|
+
"",
|
|
325
|
+
"Supported runtimes right now: python, ipython, julia, r, ghci, clojure, ruby, java, octave, matlab, gnuplot, cpp",
|
|
326
|
+
"For R, both /repl R and /repl r work. The same applies to /lab, /repl status, /repl attach, /repl export, and /repl stop.",
|
|
327
|
+
"For Clojure, /repl clojure is canonical and /repl clj also works. The same applies to /lab, /repl status, /repl attach, /repl export, and /repl stop.",
|
|
328
|
+
"For Ruby, /repl ruby starts irb. For Java, /repl java starts jshell.",
|
|
329
|
+
"",
|
|
330
|
+
"Current real implementation:",
|
|
331
|
+
" - /repl python and /repl ipython manage the shared tmux session pi-repl-python",
|
|
332
|
+
" - /repl julia manages the shared tmux session pi-repl-julia",
|
|
333
|
+
" - /repl r manages the shared tmux session pi-repl-r",
|
|
334
|
+
" - /repl ghci manages the shared tmux session pi-repl-ghci",
|
|
335
|
+
" - /repl clojure and /repl clj manage the shared tmux session pi-repl-clojure",
|
|
336
|
+
" - /repl ruby manages the shared tmux session pi-repl-ruby",
|
|
337
|
+
" - /repl java manages the shared tmux session pi-repl-java",
|
|
338
|
+
" - /repl octave and /repl matlab manage separate terminal sessions; they never substitute for each other",
|
|
339
|
+
" - /repl gnuplot manages the independent pi-repl-gnuplot session, retaining native plotting settings",
|
|
340
|
+
" - /repl cpp starts an experimental Cling-based C++ REPL; send C++ prompt groups, not dot commands",
|
|
341
|
+
" - /repl status, /repl attach, /repl export, and /repl stop can target any managed runtime",
|
|
342
|
+
" - repl_start lets pi start or reuse a session with an explicit runtime; repl_send never auto-starts one",
|
|
343
|
+
" - /repl echo controls the bounded submitted-code display in the raw pane; PI_REPL_ECHO_MODE sets the startup default",
|
|
344
|
+
" - /repl export writes the selected session's canonical clean record as Markdown",
|
|
345
|
+
" - /repl env inspects the shared Python/IPython session",
|
|
346
|
+
" - the repl_send tool can execute code in any supported shared session",
|
|
347
|
+
"",
|
|
348
|
+
"Examples:",
|
|
349
|
+
" /repl ipython",
|
|
350
|
+
" /repl julia",
|
|
351
|
+
" /repl R",
|
|
352
|
+
" /repl ghci",
|
|
353
|
+
" /repl ruby",
|
|
354
|
+
" /repl java",
|
|
355
|
+
" /repl clojure",
|
|
356
|
+
" /repl status clojure",
|
|
357
|
+
" /repl export python",
|
|
358
|
+
" /repl attach",
|
|
359
|
+
].join("\n");
|
|
360
|
+
}
|
|
361
|
+
function parseReplCommand(args) {
|
|
362
|
+
const tokens = tokenizeArgs(args);
|
|
363
|
+
if (tokens.length === 0)
|
|
364
|
+
return { action: "help" };
|
|
365
|
+
const [first, ...rest] = tokens;
|
|
366
|
+
const firstLower = first.toLowerCase();
|
|
367
|
+
if (["help", "-h", "--help", "?"].includes(firstLower)) {
|
|
368
|
+
return { action: "help" };
|
|
369
|
+
}
|
|
370
|
+
if (firstLower === "echo") {
|
|
371
|
+
if (rest.length === 0)
|
|
372
|
+
return { action: "echo" };
|
|
373
|
+
if (rest.length > 1 || !["off", "summary", "full"].includes(rest[0].toLowerCase())) {
|
|
374
|
+
return { action: "error", message: "Usage: /repl echo [off|summary|full]" };
|
|
375
|
+
}
|
|
376
|
+
return { action: "echo", mode: rest[0].toLowerCase() };
|
|
377
|
+
}
|
|
378
|
+
if (firstLower === "status" || firstLower === "env" || firstLower === "stop" || firstLower === "attach" || firstLower === "export") {
|
|
379
|
+
if (rest.length > 1) {
|
|
380
|
+
return {
|
|
381
|
+
action: "error",
|
|
382
|
+
message: `Unexpected arguments for /repl ${firstLower}: ${rest.join(" ")}`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
if (rest.length === 1) {
|
|
386
|
+
const selector = rest[0].toLowerCase();
|
|
387
|
+
if (!isSessionTargetRuntime(selector)) {
|
|
388
|
+
return {
|
|
389
|
+
action: "error",
|
|
390
|
+
message: `Unknown argument for /repl ${firstLower}: ${rest[0]}`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (firstLower === "status")
|
|
394
|
+
return { action: "status", runtime: selector };
|
|
395
|
+
if (firstLower === "env")
|
|
396
|
+
return { action: "env", runtime: selector };
|
|
397
|
+
if (firstLower === "stop")
|
|
398
|
+
return { action: "stop", runtime: selector };
|
|
399
|
+
if (firstLower === "export")
|
|
400
|
+
return { action: "export", runtime: selector };
|
|
401
|
+
return { action: "attach", runtime: selector };
|
|
402
|
+
}
|
|
403
|
+
if (firstLower === "status")
|
|
404
|
+
return { action: "status" };
|
|
405
|
+
if (firstLower === "env")
|
|
406
|
+
return { action: "env" };
|
|
407
|
+
if (firstLower === "stop")
|
|
408
|
+
return { action: "stop" };
|
|
409
|
+
if (firstLower === "export")
|
|
410
|
+
return { action: "export" };
|
|
411
|
+
return { action: "attach" };
|
|
412
|
+
}
|
|
413
|
+
if (!isSupportedRuntime(firstLower)) {
|
|
414
|
+
return {
|
|
415
|
+
action: "error",
|
|
416
|
+
message: `Unknown /repl subcommand or runtime: ${first}`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
let name;
|
|
420
|
+
for (let i = 0; i < rest.length; i++) {
|
|
421
|
+
const token = rest[i];
|
|
422
|
+
if (token === "--name" || token === "-n") {
|
|
423
|
+
const value = rest[i + 1];
|
|
424
|
+
if (!value) {
|
|
425
|
+
return {
|
|
426
|
+
action: "error",
|
|
427
|
+
message: "Missing value for --name",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
name = value;
|
|
431
|
+
i += 1;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
action: "error",
|
|
436
|
+
message: `Unknown argument for /repl ${firstLower}: ${token}`,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (name !== undefined && sanitizeNamePart(name).length === 0) {
|
|
440
|
+
return {
|
|
441
|
+
action: "error",
|
|
442
|
+
message: `Session name is empty after sanitization: ${name}`,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
action: "start",
|
|
447
|
+
runtime: firstLower,
|
|
448
|
+
name,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async function commandExists(pi, command, cwd) {
|
|
452
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
|
453
|
+
try {
|
|
454
|
+
const result = await pi.exec(lookupCommand, [command], { cwd, timeout: 2_000 });
|
|
455
|
+
return result.code === 0;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function execTmux(pi, args, cwd, timeout = 5_000) {
|
|
462
|
+
const result = await pi.exec("tmux", args, { cwd, timeout });
|
|
463
|
+
return {
|
|
464
|
+
code: result.code ?? 1,
|
|
465
|
+
stdout: result.stdout ?? "",
|
|
466
|
+
stderr: result.stderr ?? "",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function notify(ctx, message, level) {
|
|
470
|
+
if (ctx.hasUI) {
|
|
471
|
+
ctx.ui.notify(message, level);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (level === "error") {
|
|
475
|
+
console.error(message);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
console.log(message);
|
|
479
|
+
}
|
|
480
|
+
function formatAttachCommand(sessionName) {
|
|
481
|
+
return `tmux attach -t ${sessionName}`;
|
|
482
|
+
}
|
|
483
|
+
function shellQuote(value) {
|
|
484
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
485
|
+
}
|
|
486
|
+
function buildRuntimeLaunchCommand(runtime) {
|
|
487
|
+
if (runtime === "r")
|
|
488
|
+
return "R";
|
|
489
|
+
if (runtime === "clj" || runtime === "clojure")
|
|
490
|
+
return "clojure";
|
|
491
|
+
if (runtime === "ruby")
|
|
492
|
+
return "irb";
|
|
493
|
+
if (runtime === "java")
|
|
494
|
+
return "jshell";
|
|
495
|
+
if (runtime === "octave")
|
|
496
|
+
return "octave-cli --quiet --interactive";
|
|
497
|
+
if (runtime === "matlab")
|
|
498
|
+
return "matlab -nodesktop -nosplash";
|
|
499
|
+
if (runtime === "cpp")
|
|
500
|
+
return "cling";
|
|
501
|
+
return runtime;
|
|
502
|
+
}
|
|
503
|
+
function buildDefaultShellRuntimeCommand(runtime, cwd) {
|
|
504
|
+
const shell = process.env.SHELL?.trim() || "/bin/sh";
|
|
505
|
+
const runtimeCommand = buildRuntimeLaunchCommand(runtime) + (runtime === "matlab" && cwd ? ` -sd ${shellQuote(cwd)}` : "");
|
|
506
|
+
return {
|
|
507
|
+
shell,
|
|
508
|
+
command: `${shellQuote(shell)} -i -l -c ${shellQuote(runtimeCommand)}`,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function normalizePythonRuntime(info) {
|
|
512
|
+
if (info?.runtime === "ipython")
|
|
513
|
+
return "ipython";
|
|
514
|
+
if (info?.tail.includes("IPython") || info?.tail.includes("In ["))
|
|
515
|
+
return "ipython";
|
|
516
|
+
return "python";
|
|
517
|
+
}
|
|
518
|
+
function clampReplSendTimeout(timeoutMs) {
|
|
519
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
|
|
520
|
+
return DEFAULT_REPL_SEND_TIMEOUT_MS;
|
|
521
|
+
}
|
|
522
|
+
return Math.max(1_000, Math.min(MAX_REPL_SEND_TIMEOUT_MS, Math.round(timeoutMs)));
|
|
523
|
+
}
|
|
524
|
+
function getSessionTarget(sessionName) {
|
|
525
|
+
return /^\$\d+$/.test(sessionName) ? sessionName : `=${sessionName}`;
|
|
526
|
+
}
|
|
527
|
+
async function getPaneTarget(pi, sessionName, cwd) {
|
|
528
|
+
if (/^%\d+$/.test(sessionName))
|
|
529
|
+
return sessionName;
|
|
530
|
+
// Resolve the first window/pane by its actual indexes, not the user's active
|
|
531
|
+
// pane or an assumed 0.0. Use the stable pane ID for the whole submission.
|
|
532
|
+
const result = await execTmux(pi, [
|
|
533
|
+
"list-panes", "-s", "-t", getSessionTarget(sessionName),
|
|
534
|
+
"-F", "#{window_index}\t#{pane_index}\t#{pane_id}",
|
|
535
|
+
], cwd, 3_000);
|
|
536
|
+
const panes = result.stdout.trim().split("\n")
|
|
537
|
+
.map((line) => line.split("\t"))
|
|
538
|
+
.filter(([window, pane, id]) => /^\d+$/.test(window) && /^\d+$/.test(pane) && /^%\d+$/.test(id))
|
|
539
|
+
.sort((a, b) => Number(a[0]) - Number(b[0]) || Number(a[1]) - Number(b[1]));
|
|
540
|
+
if (result.code !== 0 || !panes.length) {
|
|
541
|
+
throw new Error(`Could not locate a REPL pane for ${sessionName}: ${result.stderr.trim() || "no panes found"}`);
|
|
542
|
+
}
|
|
543
|
+
return panes[0][2];
|
|
544
|
+
}
|
|
545
|
+
async function tmuxSessionExists(pi, sessionName, cwd) {
|
|
546
|
+
try {
|
|
547
|
+
const result = await execTmux(pi, ["has-session", "-t", getSessionTarget(sessionName)], cwd, 3_000);
|
|
548
|
+
return result.code === 0;
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async function setTmuxSessionOption(pi, sessionName, optionName, value, cwd) {
|
|
555
|
+
const result = await execTmux(pi, ["set-option", "-q", "-t", sessionName, optionName, value], cwd, 3_000);
|
|
556
|
+
return result.code === 0;
|
|
557
|
+
}
|
|
558
|
+
async function readTmuxSessionOption(pi, sessionName, optionName, cwd) {
|
|
559
|
+
const result = await execTmux(pi, ["show-options", "-v", "-t", sessionName, optionName], cwd, 3_000);
|
|
560
|
+
if (result.code !== 0)
|
|
561
|
+
return undefined;
|
|
562
|
+
const value = result.stdout.trim();
|
|
563
|
+
return value || undefined;
|
|
564
|
+
}
|
|
565
|
+
async function setTmuxSessionOptionIfAbsent(pi, sessionName, optionName, value, cwd) {
|
|
566
|
+
const result = await execTmux(pi, ["set-option", "-qo", "-t", sessionName, optionName, value], cwd, 3_000);
|
|
567
|
+
return result.code === 0;
|
|
568
|
+
}
|
|
569
|
+
async function ensureTmuxSessionRecord(pi, identity, cwd) {
|
|
570
|
+
let recordId = await readTmuxSessionOption(pi, identity.tmuxSessionId, REPL_SESSION_RECORD_ID_OPTION, cwd);
|
|
571
|
+
let version = await readTmuxSessionOption(pi, identity.tmuxSessionId, REPL_SESSION_RECORD_VERSION_OPTION, cwd);
|
|
572
|
+
if (recordId && !isValidReplSessionRecordId(recordId)) {
|
|
573
|
+
return { warning: `Shared REPL record metadata is invalid for ${identity.sessionName}; leaving it untouched.` };
|
|
574
|
+
}
|
|
575
|
+
if (!recordId) {
|
|
576
|
+
const candidate = createReplSessionRecordId();
|
|
577
|
+
if (!(await setTmuxSessionOptionIfAbsent(pi, identity.tmuxSessionId, REPL_SESSION_RECORD_ID_OPTION, candidate, cwd))) {
|
|
578
|
+
return { warning: `Could not attach shared record metadata to ${identity.sessionName}.` };
|
|
579
|
+
}
|
|
580
|
+
recordId = await readTmuxSessionOption(pi, identity.tmuxSessionId, REPL_SESSION_RECORD_ID_OPTION, cwd);
|
|
581
|
+
}
|
|
582
|
+
if (!recordId || !isValidReplSessionRecordId(recordId)) {
|
|
583
|
+
return { warning: `Could not read valid shared record metadata from ${identity.sessionName}.` };
|
|
584
|
+
}
|
|
585
|
+
if (!version) {
|
|
586
|
+
await setTmuxSessionOptionIfAbsent(pi, identity.tmuxSessionId, REPL_SESSION_RECORD_VERSION_OPTION, String(REPL_SESSION_RECORD_VERSION), cwd);
|
|
587
|
+
version = await readTmuxSessionOption(pi, identity.tmuxSessionId, REPL_SESSION_RECORD_VERSION_OPTION, cwd);
|
|
588
|
+
}
|
|
589
|
+
if (version !== String(REPL_SESSION_RECORD_VERSION)) {
|
|
590
|
+
return {
|
|
591
|
+
recordId,
|
|
592
|
+
warning: `Shared REPL record version ${version || "unknown"} is not supported by this pi-repl version.`,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
const record = ensureReplSessionRecord(recordId, identity);
|
|
597
|
+
return {
|
|
598
|
+
recordId,
|
|
599
|
+
recordPath: getReplSessionRecordPath(recordId),
|
|
600
|
+
recordEntryCount: record.entries.length,
|
|
601
|
+
recordTail: record.entries.slice(-20),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
return {
|
|
606
|
+
recordId,
|
|
607
|
+
recordPath: getReplSessionRecordPath(recordId),
|
|
608
|
+
warning: error instanceof Error ? error.message : String(error),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function enableSessionHistoryLogging(pi, sessionName, cwd, sessionTarget = sessionName) {
|
|
613
|
+
let historyPath;
|
|
614
|
+
let paneTarget;
|
|
615
|
+
try {
|
|
616
|
+
paneTarget = await getPaneTarget(pi, sessionTarget, cwd);
|
|
617
|
+
historyPath = createPrivateReplHistoryFile(sessionName);
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
return { warning: `History logging could not be enabled for ${sessionName}: ${error instanceof Error ? error.message : String(error)}` };
|
|
621
|
+
}
|
|
622
|
+
const pipeCommand = `${shellQuote(process.execPath)} -e ${shellQuote(REPL_HISTORY_FILTER_SCRIPT)} >> ${shellQuote(historyPath)}`;
|
|
623
|
+
const result = await execTmux(pi, ["pipe-pane", "-o", "-t", paneTarget, pipeCommand], cwd, 5_000);
|
|
624
|
+
if (result.code !== 0) {
|
|
625
|
+
const reason = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
626
|
+
return {
|
|
627
|
+
warning: `History logging could not be enabled for ${sessionName}: ${reason}`,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
const stored = await setTmuxSessionOption(pi, sessionTarget, REPL_HISTORY_OPTION, historyPath, cwd);
|
|
631
|
+
if (!stored) {
|
|
632
|
+
return {
|
|
633
|
+
historyPath,
|
|
634
|
+
warning: `History logging is active for ${sessionName}, but the history path could not be recorded in tmux metadata.`,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
return { historyPath };
|
|
638
|
+
}
|
|
639
|
+
async function readSessionInfo(pi, sessionName, cwd, inspectPrompt = false) {
|
|
640
|
+
if (!(await tmuxSessionExists(pi, sessionName, cwd)))
|
|
641
|
+
return null;
|
|
642
|
+
let target;
|
|
643
|
+
try {
|
|
644
|
+
target = await getPaneTarget(pi, sessionName, cwd);
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
// Native exit can remove the session after the first existence check.
|
|
648
|
+
// Only a fresh, explicit absence result settles this observation; do not
|
|
649
|
+
// turn permissions, timeouts or an uninspectable live pane into "stopped".
|
|
650
|
+
const check = await execTmux(pi, ["has-session", "-t", getSessionTarget(sessionName)], cwd, 3_000);
|
|
651
|
+
if (check.code === 1 && /no server running|can't find session:|(?:error connecting|failed to connect).*?(?:No such file|Connection refused)/i.test(check.stderr))
|
|
652
|
+
return null;
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
const summaryResult = await execTmux(pi, ["display-message", "-p", "-t", target, "#{session_name}\t#{session_id}\t#{session_created}\t#{pane_current_command}\t#{pane_current_path}"], cwd, 3_000);
|
|
656
|
+
if (summaryResult.code !== 0)
|
|
657
|
+
return null;
|
|
658
|
+
const [resolvedSessionName = sessionName, tmuxSessionId = "", tmuxSessionCreatedRaw = "0", currentCommand = "unknown", currentPath = cwd,] = summaryResult.stdout.trim().split("\t");
|
|
659
|
+
const tmuxSessionCreatedAt = Math.max(0, Math.floor(Number(tmuxSessionCreatedRaw) || 0));
|
|
660
|
+
const tailResult = await execTmux(pi, ["capture-pane", "-p", "-t", target, "-S", `-${DEFAULT_CAPTURE_LINES}`], cwd, 3_000);
|
|
661
|
+
let promptLine;
|
|
662
|
+
if (inspectPrompt) {
|
|
663
|
+
// Inspect the physical cursor row, not the last nonblank history line:
|
|
664
|
+
// an old prompt/banner above a blank current row is not readiness.
|
|
665
|
+
const cursor = await execTmux(pi, ["display-message", "-p", "-t", target, "#{cursor_y}"], cwd, 3_000);
|
|
666
|
+
const row = cursor.stdout.trim();
|
|
667
|
+
if (cursor.code === 0 && /^\d+$/.test(row)) {
|
|
668
|
+
const prompt = await execTmux(pi, ["capture-pane", "-p", "-t", target, "-S", row, "-E", row], cwd, 3_000);
|
|
669
|
+
if (prompt.code === 0)
|
|
670
|
+
promptLine = prompt.stdout.trimEnd();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const runtime = await readTmuxSessionOption(pi, tmuxSessionId, REPL_RUNTIME_OPTION, cwd);
|
|
674
|
+
const historyPath = await readTmuxSessionOption(pi, tmuxSessionId, REPL_HISTORY_OPTION, cwd);
|
|
675
|
+
const record = await ensureTmuxSessionRecord(pi, {
|
|
676
|
+
sessionName: resolvedSessionName,
|
|
677
|
+
tmuxSessionId,
|
|
678
|
+
tmuxSessionCreatedAt,
|
|
679
|
+
runtime: runtime || "unknown",
|
|
680
|
+
}, cwd);
|
|
681
|
+
return {
|
|
682
|
+
sessionName: resolvedSessionName,
|
|
683
|
+
tmuxSessionId,
|
|
684
|
+
tmuxSessionCreatedAt,
|
|
685
|
+
runtime,
|
|
686
|
+
historyPath,
|
|
687
|
+
recordId: record.recordId,
|
|
688
|
+
recordPath: record.recordPath,
|
|
689
|
+
recordEntryCount: record.recordEntryCount,
|
|
690
|
+
recordTail: record.recordTail,
|
|
691
|
+
recordWarning: record.warning,
|
|
692
|
+
currentCommand: currentCommand || "unknown",
|
|
693
|
+
currentPath: currentPath || cwd,
|
|
694
|
+
tail: tailResult.stdout.trim(),
|
|
695
|
+
...(inspectPrompt ? { promptLine } : {}),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function formatAttachInstructions(sessionName) {
|
|
699
|
+
return [
|
|
700
|
+
"To use the REPL directly, open a new terminal window and run:",
|
|
701
|
+
formatAttachCommand(sessionName),
|
|
702
|
+
].join("\n");
|
|
703
|
+
}
|
|
704
|
+
function formatSessionInfo(info) {
|
|
705
|
+
const latestRecordEntry = info.recordTail?.at(-1);
|
|
706
|
+
const lines = [
|
|
707
|
+
`Session: ${info.sessionName}`,
|
|
708
|
+
...(info.runtime ? [`Runtime: ${info.runtime}`] : []),
|
|
709
|
+
`Current command: ${info.currentCommand}`,
|
|
710
|
+
`Path: ${info.currentPath}`,
|
|
711
|
+
...(info.recordPath ? [`Clean shared record: ${info.recordPath} (${info.recordEntryCount ?? 0} entries)`] : []),
|
|
712
|
+
...(latestRecordEntry ? [`Latest clean entry: ${latestRecordEntry.origin} · ${latestRecordEntry.label} · ${latestRecordEntry.status}`] : []),
|
|
713
|
+
...(info.recordWarning ? [`Shared record warning: ${info.recordWarning}`] : []),
|
|
714
|
+
...(info.historyPath ? [`Raw history log: ${info.historyPath}`] : []),
|
|
715
|
+
"",
|
|
716
|
+
formatAttachInstructions(info.sessionName),
|
|
717
|
+
];
|
|
718
|
+
if (info.tail) {
|
|
719
|
+
lines.push("", "Recent pane output:", info.tail);
|
|
720
|
+
}
|
|
721
|
+
return lines.join("\n");
|
|
722
|
+
}
|
|
723
|
+
function getSessionDisplayName(selector, info) {
|
|
724
|
+
if (selector === "julia")
|
|
725
|
+
return "Julia";
|
|
726
|
+
if (selector === "r")
|
|
727
|
+
return "R";
|
|
728
|
+
if (selector === "ghci")
|
|
729
|
+
return "Haskell (GHCi)";
|
|
730
|
+
if (selector === "clojure")
|
|
731
|
+
return "Clojure";
|
|
732
|
+
if (selector === "ruby")
|
|
733
|
+
return "Ruby (irb)";
|
|
734
|
+
if (selector === "java")
|
|
735
|
+
return "Java (jshell)";
|
|
736
|
+
if (selector === "octave")
|
|
737
|
+
return "Octave";
|
|
738
|
+
if (selector === "matlab")
|
|
739
|
+
return "MATLAB";
|
|
740
|
+
if (selector === "gnuplot")
|
|
741
|
+
return "gnuplot";
|
|
742
|
+
if (selector === "cpp")
|
|
743
|
+
return "C++ (Cling)";
|
|
744
|
+
if (info?.runtime === "ipython")
|
|
745
|
+
return "Python/IPython";
|
|
746
|
+
return "Python/IPython";
|
|
747
|
+
}
|
|
748
|
+
async function listRunningSharedSessions(pi, cwd) {
|
|
749
|
+
const sessions = [];
|
|
750
|
+
const pythonInfo = await readSessionInfo(pi, DEFAULT_PYTHON_SESSION, cwd);
|
|
751
|
+
if (pythonInfo)
|
|
752
|
+
sessions.push({ selector: "python", info: pythonInfo });
|
|
753
|
+
const juliaInfo = await readSessionInfo(pi, DEFAULT_JULIA_SESSION, cwd);
|
|
754
|
+
if (juliaInfo)
|
|
755
|
+
sessions.push({ selector: "julia", info: juliaInfo });
|
|
756
|
+
const rInfo = await readSessionInfo(pi, DEFAULT_R_SESSION, cwd);
|
|
757
|
+
if (rInfo)
|
|
758
|
+
sessions.push({ selector: "r", info: rInfo });
|
|
759
|
+
const ghciInfo = await readSessionInfo(pi, DEFAULT_GHCI_SESSION, cwd);
|
|
760
|
+
if (ghciInfo)
|
|
761
|
+
sessions.push({ selector: "ghci", info: ghciInfo });
|
|
762
|
+
const clojureInfo = await readSessionInfo(pi, DEFAULT_CLOJURE_SESSION, cwd);
|
|
763
|
+
if (clojureInfo)
|
|
764
|
+
sessions.push({ selector: "clojure", info: clojureInfo });
|
|
765
|
+
const rubyInfo = await readSessionInfo(pi, DEFAULT_RUBY_SESSION, cwd);
|
|
766
|
+
if (rubyInfo)
|
|
767
|
+
sessions.push({ selector: "ruby", info: rubyInfo });
|
|
768
|
+
const javaInfo = await readSessionInfo(pi, DEFAULT_JAVA_SESSION, cwd);
|
|
769
|
+
if (javaInfo)
|
|
770
|
+
sessions.push({ selector: "java", info: javaInfo });
|
|
771
|
+
for (const selector of ["octave", "matlab", "gnuplot", "cpp"]) {
|
|
772
|
+
const info = await readSessionInfo(pi, getSessionNameForSelector(selector), cwd);
|
|
773
|
+
if (info)
|
|
774
|
+
sessions.push({ selector, info });
|
|
775
|
+
}
|
|
776
|
+
return sessions;
|
|
777
|
+
}
|
|
778
|
+
function sleep(ms) {
|
|
779
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
780
|
+
}
|
|
781
|
+
function hasNormalReplPrompt(info, requested) {
|
|
782
|
+
// Observe only: never send a probe, Enter or Ctrl-C into shared user state.
|
|
783
|
+
// A running process or startup banner alone does not establish readiness.
|
|
784
|
+
const runtime = info.runtime || requested;
|
|
785
|
+
const prompts = {
|
|
786
|
+
python: /(?:^|\n)>>>[ \t]*$/,
|
|
787
|
+
ipython: /(?:^|\n)In \[\d+\]:[ \t]*$/,
|
|
788
|
+
julia: /(?:^|\n)julia>[ \t]*$/,
|
|
789
|
+
r: /(?:^|\n)>[ \t]*$/,
|
|
790
|
+
ghci: /(?:^|\n)(?:\*?[A-Za-z0-9_.:]+)(?: \*?[A-Za-z0-9_.:]+)*>[ \t]*$/,
|
|
791
|
+
clojure: /(?:^|\n)[^\s>]+=>[ \t]*$/,
|
|
792
|
+
ruby: /(?:^|\n)irb\([^\n]*\):\d+(?::0)?>[ \t]*$/,
|
|
793
|
+
java: /(?:^|\n)jshell>[ \t]*$/,
|
|
794
|
+
octave: /(?:^|\n)octave:\d+>[ \t]*$/,
|
|
795
|
+
matlab: /(?:^|\n)>>[ \t]*$/,
|
|
796
|
+
gnuplot: /(?:^|\n)gnuplot>[ \t]*$/,
|
|
797
|
+
cpp: /(?:^|\n)\[cling\]\$[ \t]*$/,
|
|
798
|
+
};
|
|
799
|
+
if (!REPL_START_RUNTIMES.includes(runtime))
|
|
800
|
+
return false;
|
|
801
|
+
if (toSessionSelector(runtime) !== toSessionSelector(requested))
|
|
802
|
+
return false;
|
|
803
|
+
const line = info.promptLine ?? "";
|
|
804
|
+
// Shell aliases and legacy metadata can label either Python frontend as
|
|
805
|
+
// the other. They intentionally share one session and execution protocol.
|
|
806
|
+
if (runtime === "python" || runtime === "ipython")
|
|
807
|
+
return prompts.python.test(line) || prompts.ipython.test(line);
|
|
808
|
+
return prompts[runtime].test(line);
|
|
809
|
+
}
|
|
810
|
+
function checkStartAborted(signal, sessionName) {
|
|
811
|
+
if (signal?.aborted) {
|
|
812
|
+
throw new Error(`REPL start aborted for ${sessionName}. Any session already created is left running; inspect it with repl_status.`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function waitForReplSessionInfo(pi, cwd, sessionTarget, sessionName, runtime, timeoutMs, signal) {
|
|
816
|
+
const deadline = Date.now() + timeoutMs;
|
|
817
|
+
let identity;
|
|
818
|
+
while (true) {
|
|
819
|
+
checkStartAborted(signal, sessionName);
|
|
820
|
+
const info = await readSessionInfo(pi, identity?.tmuxSessionId || sessionTarget, cwd, true);
|
|
821
|
+
checkStartAborted(signal, sessionName);
|
|
822
|
+
if (!info)
|
|
823
|
+
throw new Error(`REPL session ended while waiting for ${sessionName}. Check the runtime and your login-shell configuration before retrying.`);
|
|
824
|
+
if (identity && (info.tmuxSessionId !== identity.tmuxSessionId || info.tmuxSessionCreatedAt !== identity.tmuxSessionCreatedAt || info.recordId !== identity.recordId)) {
|
|
825
|
+
throw new Error(`REPL session ${sessionName} changed while waiting for its prompt. No replacement was started; inspect it with repl_status.`);
|
|
826
|
+
}
|
|
827
|
+
identity = info;
|
|
828
|
+
let livePane = true;
|
|
829
|
+
if (runtime === "cpp") {
|
|
830
|
+
const pane = await inspectCppPane(pi, await getPaneTarget(pi, sessionTarget, cwd), cwd);
|
|
831
|
+
if (pane?.dead)
|
|
832
|
+
throw new Error(`REPL session ended native execution: ${sessionName}. Its dead tmux pane is retained; no replacement was started.`);
|
|
833
|
+
livePane = Boolean(pane);
|
|
834
|
+
}
|
|
835
|
+
checkStartAborted(signal, sessionName);
|
|
836
|
+
if (livePane && hasNormalReplPrompt(info, runtime))
|
|
837
|
+
return { info, ready: true };
|
|
838
|
+
if (Date.now() >= deadline)
|
|
839
|
+
return { info, ready: false };
|
|
840
|
+
await sleep(Math.min(DEFAULT_STARTUP_POLL_MS, deadline - Date.now()));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function getReplControlExtension(runtime) {
|
|
844
|
+
if (runtime === "julia")
|
|
845
|
+
return "jl";
|
|
846
|
+
if (runtime === "r")
|
|
847
|
+
return "R";
|
|
848
|
+
if (runtime === "ghci")
|
|
849
|
+
return "ghci";
|
|
850
|
+
if (runtime === "clojure")
|
|
851
|
+
return "clj";
|
|
852
|
+
if (runtime === "ruby")
|
|
853
|
+
return "rb";
|
|
854
|
+
if (runtime === "java")
|
|
855
|
+
return "java";
|
|
856
|
+
if (runtime === "octave" || runtime === "matlab")
|
|
857
|
+
return "m";
|
|
858
|
+
if (runtime === "gnuplot")
|
|
859
|
+
return "gp";
|
|
860
|
+
if (runtime === "cpp")
|
|
861
|
+
return "cpp";
|
|
862
|
+
return "py";
|
|
863
|
+
}
|
|
864
|
+
function buildPythonDisplayStatements(display, indent = "") {
|
|
865
|
+
if (!display.enabled)
|
|
866
|
+
return [];
|
|
867
|
+
return display.prefixLines.map((line) => `${indent}__pi_repl_builtins.print(${JSON.stringify(line)})`);
|
|
868
|
+
}
|
|
869
|
+
function juliaStringLiteral(value) {
|
|
870
|
+
// JSON escaping alone leaves Julia's $ interpolation active in the wrapper.
|
|
871
|
+
return JSON.stringify(value).replace(/\$/g, "\\$");
|
|
872
|
+
}
|
|
873
|
+
function buildJuliaDisplayStatements(display, indent = "") {
|
|
874
|
+
if (!display.enabled)
|
|
875
|
+
return [];
|
|
876
|
+
return display.prefixLines.map((line) => `${indent}Base.println(${juliaStringLiteral(line)})`);
|
|
877
|
+
}
|
|
878
|
+
function buildRDisplayStatements(display, indent = "") {
|
|
879
|
+
if (!display.enabled)
|
|
880
|
+
return [];
|
|
881
|
+
return display.prefixLines.map((line) => `${indent}base::cat(${JSON.stringify(`${line}\n`)})`);
|
|
882
|
+
}
|
|
883
|
+
function buildClojureDisplayStatements(display, indent = "") {
|
|
884
|
+
if (!display.enabled)
|
|
885
|
+
return [];
|
|
886
|
+
return display.prefixLines.map((line) => `${indent}(clojure.core/println ${JSON.stringify(line)})`);
|
|
887
|
+
}
|
|
888
|
+
function buildPythonControlSource(runtime, code, doneFile, display) {
|
|
889
|
+
const prefix = buildPythonDisplayStatements(display);
|
|
890
|
+
const completion = display.suffixLines.map((line) => ` __pi_repl_builtins.print(${JSON.stringify(line)})`);
|
|
891
|
+
if (runtime === "ipython") {
|
|
892
|
+
return [
|
|
893
|
+
"from pathlib import Path as __pi_repl_path",
|
|
894
|
+
"import builtins as __pi_repl_builtins",
|
|
895
|
+
"import traceback as __pi_repl_traceback",
|
|
896
|
+
...prefix,
|
|
897
|
+
"try:",
|
|
898
|
+
" __pi_repl_ip = get_ipython()",
|
|
899
|
+
" if __pi_repl_ip is None:",
|
|
900
|
+
" raise RuntimeError('Expected IPython session, but get_ipython() returned None.')",
|
|
901
|
+
` __pi_repl_result = __pi_repl_ip.run_cell(${JSON.stringify(code)}, store_history=False)`,
|
|
902
|
+
" if getattr(__pi_repl_result, 'error_in_exec', None) is None and getattr(__pi_repl_result, 'result', None) is not None:",
|
|
903
|
+
" print(repr(__pi_repl_result.result))",
|
|
904
|
+
"except Exception:",
|
|
905
|
+
" __pi_repl_traceback.print_exc()",
|
|
906
|
+
"finally:",
|
|
907
|
+
...completion,
|
|
908
|
+
` __pi_repl_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
|
|
909
|
+
].join("\n");
|
|
910
|
+
}
|
|
911
|
+
return [
|
|
912
|
+
"from pathlib import Path as __pi_repl_path",
|
|
913
|
+
"import builtins as __pi_repl_builtins",
|
|
914
|
+
"import traceback as __pi_repl_traceback",
|
|
915
|
+
...prefix,
|
|
916
|
+
"try:",
|
|
917
|
+
` exec(compile(${JSON.stringify(code)}, '<pi-repl>', 'exec'), globals())`,
|
|
918
|
+
"except Exception:",
|
|
919
|
+
" __pi_repl_traceback.print_exc()",
|
|
920
|
+
"finally:",
|
|
921
|
+
...completion,
|
|
922
|
+
` __pi_repl_path(${JSON.stringify(doneFile)}).write_text('done\\n', encoding='utf-8')`,
|
|
923
|
+
].join("\n");
|
|
924
|
+
}
|
|
925
|
+
function buildJuliaControlSource(code, doneFile, display) {
|
|
926
|
+
const completion = display.suffixLines.map((line) => ` Base.println(${juliaStringLiteral(line)})`);
|
|
927
|
+
return [
|
|
928
|
+
...buildJuliaDisplayStatements(display),
|
|
929
|
+
"try",
|
|
930
|
+
` local __pi_result = Base.include_string(Main, ${juliaStringLiteral(code)}, "pi-repl")`,
|
|
931
|
+
" if !isnothing(__pi_result)",
|
|
932
|
+
" println(repr(__pi_result))",
|
|
933
|
+
" end",
|
|
934
|
+
"catch e",
|
|
935
|
+
" Base.display_error(stderr, e, catch_backtrace())",
|
|
936
|
+
"finally",
|
|
937
|
+
...completion,
|
|
938
|
+
` write(${juliaStringLiteral(doneFile)}, "done\\n")`,
|
|
939
|
+
"end",
|
|
940
|
+
].join("\n");
|
|
941
|
+
}
|
|
942
|
+
function buildRControlSource(code, doneFile, display) {
|
|
943
|
+
const completion = display.suffixLines.map((line) => ` base::cat(${JSON.stringify(`${line}\n`)})`);
|
|
944
|
+
return [
|
|
945
|
+
"local({",
|
|
946
|
+
...buildRDisplayStatements(display, " "),
|
|
947
|
+
` .__pi_repl_done_file <- ${JSON.stringify(doneFile)}`,
|
|
948
|
+
` .__pi_repl_code <- ${JSON.stringify(code)}`,
|
|
949
|
+
" tryCatch({",
|
|
950
|
+
" .__pi_repl_exprs <- parse(text = .__pi_repl_code, keep.source = FALSE)",
|
|
951
|
+
" .__pi_repl_value <- NULL",
|
|
952
|
+
" .__pi_repl_visible <- FALSE",
|
|
953
|
+
" for (.__pi_repl_expr in .__pi_repl_exprs) {",
|
|
954
|
+
" .__pi_repl_result <- withVisible(eval(.__pi_repl_expr, envir = .GlobalEnv))",
|
|
955
|
+
" .__pi_repl_value <- .__pi_repl_result$value",
|
|
956
|
+
" .__pi_repl_visible <- isTRUE(.__pi_repl_result$visible)",
|
|
957
|
+
" }",
|
|
958
|
+
" if (.__pi_repl_visible) print(.__pi_repl_value)",
|
|
959
|
+
" }, error = function(e) {",
|
|
960
|
+
" .__pi_repl_call <- conditionCall(e)",
|
|
961
|
+
" if (is.null(.__pi_repl_call)) {",
|
|
962
|
+
" message(\"Error: \", conditionMessage(e))",
|
|
963
|
+
" } else {",
|
|
964
|
+
" message(\"Error in \", paste(deparse(.__pi_repl_call), collapse = \" \"), \": \", conditionMessage(e))",
|
|
965
|
+
" }",
|
|
966
|
+
" }, finally = {",
|
|
967
|
+
...completion,
|
|
968
|
+
" writeLines(\"done\", .__pi_repl_done_file)",
|
|
969
|
+
" })",
|
|
970
|
+
"})",
|
|
971
|
+
].join("\n");
|
|
972
|
+
}
|
|
973
|
+
function buildGhciControlSource(code, doneFile, display) {
|
|
974
|
+
if (/[\r\n]/.test(doneFile))
|
|
975
|
+
throw new Error("GHCi control paths cannot contain line breaks.");
|
|
976
|
+
const prefix = display.enabled
|
|
977
|
+
? display.prefixLines.map((line) => `:! command printf '%s\\n' ${shellQuote(line)}`)
|
|
978
|
+
: [];
|
|
979
|
+
return [...prefix, code.replace(/\r/g, "").trimEnd()].filter(Boolean).join("\n");
|
|
980
|
+
}
|
|
981
|
+
function rubyStringLiteral(value) {
|
|
982
|
+
// JSON escaping leaves Ruby's #{...}, #@... and #$... interpolation active.
|
|
983
|
+
// ASCII escapes also keep Unicode paths intact through older IRB input editors.
|
|
984
|
+
return JSON.stringify(value).replace(/#/g, "\\#")
|
|
985
|
+
.replace(/[^\x00-\x7f]/gu, (character) => `\\u{${character.codePointAt(0).toString(16)}}`);
|
|
986
|
+
}
|
|
987
|
+
function buildRubyControlSource(code, doneFile, display) {
|
|
988
|
+
const prefix = display.enabled ? display.prefixLines.map((line) => ` puts ${rubyStringLiteral(line)}`) : [];
|
|
989
|
+
// Query the actual terminal column rather than intercepting user stdout.
|
|
990
|
+
// Keep the old newline guard if the bounded, read-only query is unavailable.
|
|
991
|
+
const completion = display.enabled ? [
|
|
992
|
+
' __pi_repl_column = ""',
|
|
993
|
+
" begin",
|
|
994
|
+
" $stdout.flush",
|
|
995
|
+
" $stderr.flush",
|
|
996
|
+
// Ruby's $? is thread-local: do not replace the user's last child status.
|
|
997
|
+
" __pi_repl_column = Thread.new do",
|
|
998
|
+
" begin",
|
|
999
|
+
` IO.popen(["tmux", "-N", "display-message", "-p", "-t", ENV.fetch("TMUX_PANE"), ${rubyStringLiteral("#{cursor_x}")}], err: File::NULL) do |__pi_repl_query|`,
|
|
1000
|
+
" begin",
|
|
1001
|
+
" if IO.select([__pi_repl_query], nil, nil, 0.5)",
|
|
1002
|
+
" __pi_repl_bytes = __pi_repl_query.read_nonblock(32, exception: false)",
|
|
1003
|
+
' __pi_repl_bytes.is_a?(String) ? __pi_repl_bytes.strip : ""',
|
|
1004
|
+
" else",
|
|
1005
|
+
' ""',
|
|
1006
|
+
" end",
|
|
1007
|
+
" ensure",
|
|
1008
|
+
// Reap only our own query process, including on timeout; never leave an
|
|
1009
|
+
// IO.popen close waiting indefinitely for a stuck tmux client to exit.
|
|
1010
|
+
" Process.kill('KILL', __pi_repl_query.pid) rescue nil",
|
|
1011
|
+
" end",
|
|
1012
|
+
" end",
|
|
1013
|
+
" rescue StandardError",
|
|
1014
|
+
' ""',
|
|
1015
|
+
" end",
|
|
1016
|
+
" end.value",
|
|
1017
|
+
" rescue StandardError",
|
|
1018
|
+
' __pi_repl_column = ""',
|
|
1019
|
+
" end",
|
|
1020
|
+
' puts unless __pi_repl_column == "0"',
|
|
1021
|
+
...display.suffixLines.map((line) => ` puts ${rubyStringLiteral(line)}`),
|
|
1022
|
+
] : [" puts"];
|
|
1023
|
+
return [
|
|
1024
|
+
"begin",
|
|
1025
|
+
...prefix,
|
|
1026
|
+
` __pi_repl_code = ${rubyStringLiteral(code)}`,
|
|
1027
|
+
" __pi_repl_context = defined?(IRB) && IRB.respond_to?(:CurrentContext) && IRB.CurrentContext",
|
|
1028
|
+
" __pi_repl_binding = __pi_repl_context ? __pi_repl_context.workspace.binding : TOPLEVEL_BINDING",
|
|
1029
|
+
" __pi_repl_result = eval(__pi_repl_code, __pi_repl_binding, '<pi-repl>', 1)",
|
|
1030
|
+
" p __pi_repl_result unless __pi_repl_result.nil?",
|
|
1031
|
+
"rescue Exception => __pi_repl_e",
|
|
1032
|
+
" $stderr.puts __pi_repl_e.full_message(highlight: false)",
|
|
1033
|
+
"ensure",
|
|
1034
|
+
...completion,
|
|
1035
|
+
` File.write(${rubyStringLiteral(doneFile)}, "done\\n")`,
|
|
1036
|
+
// IRB checks echo? immediately after evaluating the loader. Suppress
|
|
1037
|
+
// just that result, restoring the original method before returning.
|
|
1038
|
+
" begin",
|
|
1039
|
+
" if __pi_repl_context && __pi_repl_context.respond_to?(:echo?)",
|
|
1040
|
+
" __pi_repl_echo_owner = __pi_repl_context.singleton_class",
|
|
1041
|
+
" __pi_repl_echo_owned = __pi_repl_echo_owner.instance_methods(false).include?(:echo?)",
|
|
1042
|
+
" __pi_repl_echo_original = __pi_repl_context.method(:echo?)",
|
|
1043
|
+
" __pi_repl_context.define_singleton_method(:echo?) do",
|
|
1044
|
+
" if __pi_repl_echo_owned",
|
|
1045
|
+
" __pi_repl_echo_owner.send(:define_method, :echo?, __pi_repl_echo_original)",
|
|
1046
|
+
" else",
|
|
1047
|
+
" __pi_repl_echo_owner.send(:remove_method, :echo?)",
|
|
1048
|
+
" end",
|
|
1049
|
+
" false",
|
|
1050
|
+
" end",
|
|
1051
|
+
" end",
|
|
1052
|
+
" rescue StandardError",
|
|
1053
|
+
// If a customised/frozen context refuses the hook, keep normal IRB
|
|
1054
|
+
// behaviour rather than fail the already completed submission.
|
|
1055
|
+
" end",
|
|
1056
|
+
"end",
|
|
1057
|
+
].join("\n");
|
|
1058
|
+
}
|
|
1059
|
+
function buildJavaControlSource(code, doneFile, display) {
|
|
1060
|
+
if (/[\r\n]/.test(doneFile))
|
|
1061
|
+
throw new Error("JShell control paths cannot contain line breaks.");
|
|
1062
|
+
const prefix = display.enabled ? display.prefixLines.map((line) => `java.lang.System.out.println(${JSON.stringify(line)});`) : [];
|
|
1063
|
+
// /open evaluates top-level snippets in the existing JShell. Completion
|
|
1064
|
+
// stays in the outer driver, outside this source file's parser state.
|
|
1065
|
+
return [...prefix, code.replace(/\r/g, "").trimEnd(), ""].join("\n");
|
|
1066
|
+
}
|
|
1067
|
+
function buildClojureControlSource(code, doneFile, display) {
|
|
1068
|
+
const completion = display.suffixLines.map((line) => ` (clojure.core/println ${JSON.stringify(line)})`);
|
|
1069
|
+
return [
|
|
1070
|
+
"(let [code " + JSON.stringify(code) + "]",
|
|
1071
|
+
...buildClojureDisplayStatements(display, " "),
|
|
1072
|
+
" (try",
|
|
1073
|
+
" (let [rdr (clojure.lang.LineNumberingPushbackReader. (java.io.StringReader. code))]",
|
|
1074
|
+
" (loop [last-val nil has-val false]",
|
|
1075
|
+
" (let [form (read rdr false :pi-repl/eof)]",
|
|
1076
|
+
" (if (= form :pi-repl/eof)",
|
|
1077
|
+
" (when (and has-val (some? last-val)) (prn last-val))",
|
|
1078
|
+
" (recur (eval form) true)))))",
|
|
1079
|
+
" (catch Throwable t",
|
|
1080
|
+
" (#'clojure.main/repl-caught t))",
|
|
1081
|
+
" (finally",
|
|
1082
|
+
...completion,
|
|
1083
|
+
` (spit ${JSON.stringify(doneFile)} "done\\n"))))`,
|
|
1084
|
+
].join("\n");
|
|
1085
|
+
}
|
|
1086
|
+
function buildReplControlSource(runtime, code, doneFile, display) {
|
|
1087
|
+
if (runtime === "julia")
|
|
1088
|
+
return buildJuliaControlSource(code, doneFile, display);
|
|
1089
|
+
if (runtime === "r")
|
|
1090
|
+
return buildRControlSource(code, doneFile, display);
|
|
1091
|
+
if (runtime === "ghci")
|
|
1092
|
+
return buildGhciControlSource(code, doneFile, display);
|
|
1093
|
+
if (runtime === "clojure")
|
|
1094
|
+
return buildClojureControlSource(code, doneFile, display);
|
|
1095
|
+
if (runtime === "ruby")
|
|
1096
|
+
return buildRubyControlSource(code, doneFile, display);
|
|
1097
|
+
if (runtime === "java")
|
|
1098
|
+
return buildJavaControlSource(code, doneFile, display);
|
|
1099
|
+
if (runtime === "octave" || runtime === "matlab")
|
|
1100
|
+
return buildMLanguageControlSource(code, display);
|
|
1101
|
+
if (runtime === "gnuplot")
|
|
1102
|
+
return code + "\n";
|
|
1103
|
+
if (runtime === "cpp")
|
|
1104
|
+
return code;
|
|
1105
|
+
return buildPythonControlSource(runtime, code, doneFile, display);
|
|
1106
|
+
}
|
|
1107
|
+
function buildReplSubmissionLine(runtime, sourceFile) {
|
|
1108
|
+
const quotedPath = JSON.stringify(sourceFile);
|
|
1109
|
+
if (runtime === "julia") {
|
|
1110
|
+
return `include(${juliaStringLiteral(sourceFile)})`;
|
|
1111
|
+
}
|
|
1112
|
+
if (runtime === "r") {
|
|
1113
|
+
return `source(${quotedPath},local=.GlobalEnv)`;
|
|
1114
|
+
}
|
|
1115
|
+
if (runtime === "ghci") {
|
|
1116
|
+
return `:script ${quotedPath}`;
|
|
1117
|
+
}
|
|
1118
|
+
if (runtime === "clojure") {
|
|
1119
|
+
return `(do (load-file ${quotedPath}) :pi-repl/silent)`;
|
|
1120
|
+
}
|
|
1121
|
+
if (runtime === "ruby") {
|
|
1122
|
+
return `load ${rubyStringLiteral(sourceFile)}; nil`;
|
|
1123
|
+
}
|
|
1124
|
+
if (runtime === "java") {
|
|
1125
|
+
return `/open ${sourceFile}`;
|
|
1126
|
+
}
|
|
1127
|
+
if (runtime === "octave" || runtime === "matlab")
|
|
1128
|
+
return `eval(fileread(${mStringLiteral(sourceFile)}));`;
|
|
1129
|
+
if (runtime === "gnuplot")
|
|
1130
|
+
return buildGnuplotSubmissionLine(sourceFile);
|
|
1131
|
+
return `exec(open(${quotedPath}).read(),globals())`;
|
|
1132
|
+
}
|
|
1133
|
+
function buildJavaDriverSource(sourceFile, doneFile, display) {
|
|
1134
|
+
// Only add a newline when the terminal is mid-line. The scoped query does
|
|
1135
|
+
// not replace System.out/err, change user declarations, or emit another /open.
|
|
1136
|
+
const completion = display.enabled ? [
|
|
1137
|
+
' String __pi_repl_column = "";',
|
|
1138
|
+
" java.lang.Process __pi_repl_query = null;",
|
|
1139
|
+
" try {",
|
|
1140
|
+
" java.lang.System.out.flush();",
|
|
1141
|
+
" java.lang.System.err.flush();",
|
|
1142
|
+
` __pi_repl_query = new java.lang.ProcessBuilder("tmux", "-N", "display-message", "-p", "-t", java.lang.System.getenv("TMUX_PANE"), ${JSON.stringify("#{cursor_x}")}).redirectError(java.lang.ProcessBuilder.Redirect.DISCARD).start();`,
|
|
1143
|
+
" if (__pi_repl_query.waitFor(500, java.util.concurrent.TimeUnit.MILLISECONDS) && __pi_repl_query.exitValue() == 0) {",
|
|
1144
|
+
// Do not wait for EOF if an unexpected descendant inherits the pipe.
|
|
1145
|
+
" byte[] __pi_repl_bytes = new byte[32];",
|
|
1146
|
+
" int __pi_repl_count = __pi_repl_query.getInputStream().read(__pi_repl_bytes, 0, Math.min(32, __pi_repl_query.getInputStream().available()));",
|
|
1147
|
+
" __pi_repl_column = new String(__pi_repl_bytes, 0, Math.max(0, __pi_repl_count), java.nio.charset.StandardCharsets.UTF_8).trim();",
|
|
1148
|
+
" }",
|
|
1149
|
+
" } catch (Exception __pi_repl_ignored) {",
|
|
1150
|
+
' __pi_repl_column = "";',
|
|
1151
|
+
" } finally {",
|
|
1152
|
+
" if (__pi_repl_query != null) {",
|
|
1153
|
+
" try { __pi_repl_query.destroyForcibly(); } catch (Exception __pi_repl_ignored) {}",
|
|
1154
|
+
" }",
|
|
1155
|
+
" }",
|
|
1156
|
+
' if (!"0".equals(__pi_repl_column)) java.lang.System.out.println();',
|
|
1157
|
+
...display.suffixLines.map((line) => ` java.lang.System.out.println(${JSON.stringify(line)});`),
|
|
1158
|
+
] : [];
|
|
1159
|
+
return [
|
|
1160
|
+
buildReplSubmissionLine("java", sourceFile),
|
|
1161
|
+
"{",
|
|
1162
|
+
...completion,
|
|
1163
|
+
` java.nio.file.Files.write(java.nio.file.Paths.get(${JSON.stringify(doneFile)}), new byte[]{100, 111, 110, 101, 10});`,
|
|
1164
|
+
"}",
|
|
1165
|
+
].join("\n");
|
|
1166
|
+
}
|
|
1167
|
+
function buildReplCompletionLine(runtime, doneFile, display) {
|
|
1168
|
+
if (runtime === "ghci") {
|
|
1169
|
+
// Run from the outer driver, after the guard has absorbed a source
|
|
1170
|
+
// script failure. Node is already used by the raw-history pipe; its
|
|
1171
|
+
// bounded cursor query avoids both blank padding and inline footers.
|
|
1172
|
+
const footerScript = [
|
|
1173
|
+
'let column = "";',
|
|
1174
|
+
'try { column = require("node:child_process").execFileSync("tmux", ["-N", "display-message", "-p", "-t", process.env.TMUX_PANE, "#{cursor_x}"], { encoding: "utf8", timeout: 500, killSignal: "SIGKILL", maxBuffer: 128, stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch {}',
|
|
1175
|
+
`process.stdout.write((column === "0" ? "" : "\\n") + ${JSON.stringify(`${display.suffixLines.join("\n")}\n`)});`,
|
|
1176
|
+
].join(" ");
|
|
1177
|
+
const fallback = `command printf '\\n%s\\n\\n' ${shellQuote(display.endMarker)}`;
|
|
1178
|
+
const displayCommand = display.enabled ? `${shellQuote(process.execPath)} -e ${shellQuote(footerScript)} 2>/dev/null || ${fallback}; ` : "";
|
|
1179
|
+
return `:! ${displayCommand}touch ${shellQuote(doneFile)}`;
|
|
1180
|
+
}
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
function buildSubmissionText(submissionLine, previewComment, completionLine) {
|
|
1184
|
+
return [previewComment, submissionLine, completionLine].filter((value) => Boolean(value)).join("\n");
|
|
1185
|
+
}
|
|
1186
|
+
function prepareReplControlFiles(runtime, code, details) {
|
|
1187
|
+
if (runtime === "cpp" && code.includes("\0"))
|
|
1188
|
+
throw new Error("C++ source cannot contain NUL bytes; no code was sent.");
|
|
1189
|
+
const display = createReplSubmissionDisplay({
|
|
1190
|
+
entryId: details.submissionId,
|
|
1191
|
+
origin: "pi-repl",
|
|
1192
|
+
code,
|
|
1193
|
+
mode: details.echoMode,
|
|
1194
|
+
});
|
|
1195
|
+
const controlOptions = { ...REPL_CONTROL_OPTIONS, anchorId: display.anchorId };
|
|
1196
|
+
const controlPaths = createPrivateReplControlFiles({
|
|
1197
|
+
...controlOptions,
|
|
1198
|
+
extension: getReplControlExtension(runtime),
|
|
1199
|
+
buildSource: ({ doneFile }) => buildReplControlSource(runtime, code, doneFile, display),
|
|
1200
|
+
});
|
|
1201
|
+
let guardPaths;
|
|
1202
|
+
let driverPaths;
|
|
1203
|
+
try {
|
|
1204
|
+
if (runtime === "ghci") {
|
|
1205
|
+
// An unclosed :{ throws out of source collection. The guard's
|
|
1206
|
+
// command handler catches it and its script stops normally, letting
|
|
1207
|
+
// the OUTER driver continue to completion. Two levels alone fail.
|
|
1208
|
+
const guard = createPrivateReplControlFiles({
|
|
1209
|
+
...controlOptions,
|
|
1210
|
+
extension: "ghci",
|
|
1211
|
+
buildSource: () => buildReplSubmissionLine("ghci", controlPaths.sourceFile) + "\n",
|
|
1212
|
+
});
|
|
1213
|
+
guardPaths = guard;
|
|
1214
|
+
driverPaths = createPrivateReplControlFiles({
|
|
1215
|
+
...controlOptions,
|
|
1216
|
+
extension: "ghci",
|
|
1217
|
+
buildSource: () => [buildReplSubmissionLine("ghci", guard.sourceFile), buildReplCompletionLine("ghci", controlPaths.doneFile, display), ""].join("\n"),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
// JShell echoes only the outer /open. Its nested source load returns
|
|
1221
|
+
// before completion, even if user source is rejected or unfinished.
|
|
1222
|
+
if (runtime === "java") {
|
|
1223
|
+
driverPaths = createPrivateReplControlFiles({
|
|
1224
|
+
...controlOptions,
|
|
1225
|
+
extension: "java",
|
|
1226
|
+
buildSource: () => buildJavaDriverSource(controlPaths.sourceFile, controlPaths.doneFile, display),
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if (runtime === "octave" || runtime === "matlab") {
|
|
1230
|
+
driverPaths = createPrivateReplControlFiles({
|
|
1231
|
+
...controlOptions,
|
|
1232
|
+
extension: "m",
|
|
1233
|
+
buildSource: () => buildMLanguageDriverSource(runtime, controlPaths.sourceFile, controlPaths.doneFile, display),
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
if (runtime === "gnuplot") {
|
|
1237
|
+
const guard = createPrivateReplControlFiles({
|
|
1238
|
+
...controlOptions,
|
|
1239
|
+
extension: "gp",
|
|
1240
|
+
buildSource: ({ doneFile }) => buildGnuplotGuardSource(doneFile),
|
|
1241
|
+
});
|
|
1242
|
+
guardPaths = guard;
|
|
1243
|
+
driverPaths = createPrivateReplControlFiles({
|
|
1244
|
+
...controlOptions,
|
|
1245
|
+
extension: "cjs",
|
|
1246
|
+
buildSource: () => buildGnuplotDriverSource(controlPaths.sourceFile, guard.sourceFile, guard.doneFile, controlPaths.doneFile, display),
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
let cpp;
|
|
1250
|
+
if (runtime === "cpp") {
|
|
1251
|
+
const request = createPrivateReplControlFiles({
|
|
1252
|
+
...controlOptions, extension: "req",
|
|
1253
|
+
buildSource: () => buildCppRequest(controlPaths.sourceFile, controlPaths.doneFile, display),
|
|
1254
|
+
});
|
|
1255
|
+
guardPaths = request;
|
|
1256
|
+
cpp = prepareCppSubmission(request.sourceFile, controlPaths.doneFile);
|
|
1257
|
+
driverPaths = createPrivateReplControlFiles({
|
|
1258
|
+
...controlOptions, extension: "hxx",
|
|
1259
|
+
buildSource: () => buildCppDriverSource(request.sourceFile, controlPaths.doneFile),
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
const submissionLine = cpp && driverPaths ? cppInclude(driverPaths.sourceFile) : buildReplSubmissionLine(runtime, driverPaths?.sourceFile ?? controlPaths.sourceFile);
|
|
1263
|
+
const completionLine = driverPaths ? undefined : buildReplCompletionLine(runtime, controlPaths.doneFile, display);
|
|
1264
|
+
const previewComment = undefined;
|
|
1265
|
+
return {
|
|
1266
|
+
controlPaths,
|
|
1267
|
+
guardPaths,
|
|
1268
|
+
driverPaths,
|
|
1269
|
+
submissionLine,
|
|
1270
|
+
completionLine,
|
|
1271
|
+
previewComment,
|
|
1272
|
+
display,
|
|
1273
|
+
cppCompletion: cpp?.completion,
|
|
1274
|
+
cppCleanup: cpp && guardPaths && driverPaths ? createCppControlCleanup(cpp.completion, [controlPaths.sourceFile, guardPaths.sourceFile, driverPaths.sourceFile]) : undefined,
|
|
1275
|
+
submissionText: buildSubmissionText(submissionLine, previewComment, completionLine),
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
cleanupPrivateReplControlFiles(controlPaths);
|
|
1280
|
+
cleanupPrivateReplControlFiles(guardPaths);
|
|
1281
|
+
cleanupPrivateReplControlFiles(driverPaths);
|
|
1282
|
+
throw error;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
async function pasteTextToTmuxPane(pi, sessionTarget, cwd, text, onPasted) {
|
|
1286
|
+
const bufferName = `pi-repl-${randomUUID()}`;
|
|
1287
|
+
const target = await getPaneTarget(pi, sessionTarget, cwd);
|
|
1288
|
+
const controlPaths = createPrivateReplControlFiles({ ...REPL_CONTROL_OPTIONS, extension: "txt", buildSource: () => text });
|
|
1289
|
+
const tempFile = controlPaths.sourceFile;
|
|
1290
|
+
try {
|
|
1291
|
+
// load-buffer expands tmux formats even in an argv path; keep # literal.
|
|
1292
|
+
const loadResult = await execTmux(pi, ["load-buffer", "-b", bufferName, tempFile.replace(/#/g, "##")], cwd, 5_000);
|
|
1293
|
+
if (loadResult.code !== 0) {
|
|
1294
|
+
const reason = loadResult.stderr.trim() || loadResult.stdout.trim() || `exit code ${loadResult.code}`;
|
|
1295
|
+
throw new Error(`Failed to load tmux buffer: ${reason}`);
|
|
1296
|
+
}
|
|
1297
|
+
const pasteResult = await execTmux(pi, ["paste-buffer", "-d", "-b", bufferName, "-t", target], cwd, 5_000);
|
|
1298
|
+
if (pasteResult.code !== 0) {
|
|
1299
|
+
const reason = pasteResult.stderr.trim() || pasteResult.stdout.trim() || `exit code ${pasteResult.code}`;
|
|
1300
|
+
throw new Error(`Failed to paste tmux buffer: ${reason}`);
|
|
1301
|
+
}
|
|
1302
|
+
onPasted?.();
|
|
1303
|
+
const enterResult = await execTmux(pi, ["send-keys", "-t", target, "C-m"], cwd, 5_000);
|
|
1304
|
+
if (enterResult.code !== 0) {
|
|
1305
|
+
const reason = enterResult.stderr.trim() || enterResult.stdout.trim() || `exit code ${enterResult.code}`;
|
|
1306
|
+
throw new Error(`Failed to send Enter to tmux pane: ${reason}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
finally {
|
|
1310
|
+
cleanupPrivateReplControlFiles(controlPaths);
|
|
1311
|
+
await execTmux(pi, ["delete-buffer", "-b", bufferName], cwd, 2_000).catch(() => undefined);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
async function capturePaneOutput(pi, sessionTarget, cwd) {
|
|
1315
|
+
const target = await getPaneTarget(pi, sessionTarget, cwd);
|
|
1316
|
+
const result = await execTmux(pi, ["capture-pane", "-J", "-p", "-t", target, "-S", `-${REPL_SEND_CAPTURE_LINES}`], cwd, 5_000);
|
|
1317
|
+
if (result.code !== 0) {
|
|
1318
|
+
const reason = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
1319
|
+
throw new Error(`Failed to capture tmux pane output: ${reason}`);
|
|
1320
|
+
}
|
|
1321
|
+
return result.stdout;
|
|
1322
|
+
}
|
|
1323
|
+
function stripBoundaryBlankLines(text) {
|
|
1324
|
+
const lines = text.replace(/\r/g, "").split("\n");
|
|
1325
|
+
while (lines.length > 0 && lines[0] === "")
|
|
1326
|
+
lines.shift();
|
|
1327
|
+
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
1328
|
+
lines.pop();
|
|
1329
|
+
return lines.join("\n");
|
|
1330
|
+
}
|
|
1331
|
+
function extractPaneDelta(before, after) {
|
|
1332
|
+
const normalizedBefore = before.replace(/\r/g, "");
|
|
1333
|
+
const normalizedAfter = after.replace(/\r/g, "");
|
|
1334
|
+
if (normalizedAfter.startsWith(normalizedBefore)) {
|
|
1335
|
+
return normalizedAfter.slice(normalizedBefore.length);
|
|
1336
|
+
}
|
|
1337
|
+
const beforeLines = normalizedBefore.split("\n");
|
|
1338
|
+
const afterLines = normalizedAfter.split("\n");
|
|
1339
|
+
let index = 0;
|
|
1340
|
+
while (index < beforeLines.length && index < afterLines.length && beforeLines[index] === afterLines[index]) {
|
|
1341
|
+
index += 1;
|
|
1342
|
+
}
|
|
1343
|
+
return afterLines.slice(index).join("\n");
|
|
1344
|
+
}
|
|
1345
|
+
export function cleanupReplDelta(delta, submissionLine, previewComment, completionLine, display) {
|
|
1346
|
+
const isCppSubmission = submissionLine.startsWith("#include ");
|
|
1347
|
+
const isMLanguageSubmission = submissionLine.startsWith("eval(fileread(");
|
|
1348
|
+
const isPrompt = (line) => /^(?:>>>|In \[\d+\]:|\.\.\.:|>|\+)\s*$/.test(line) ||
|
|
1349
|
+
/^(ghci|Prelude|\*?[A-Za-z0-9_.:]+)>\s*$/.test(line) ||
|
|
1350
|
+
/^[^\s>]+=>\s*$/.test(line) ||
|
|
1351
|
+
/^irb\(.*\)[:\d]+[>*]\s*$/.test(line) ||
|
|
1352
|
+
(isMLanguageSubmission && /^(?:octave:\d+>|>>)\s*$/.test(line)) ||
|
|
1353
|
+
(isCppSubmission && /^\[cling\]\$\s*$/.test(line));
|
|
1354
|
+
const echoEnd = (line, command) => {
|
|
1355
|
+
if (!command)
|
|
1356
|
+
return -1;
|
|
1357
|
+
const index = line.indexOf(command);
|
|
1358
|
+
const prefix = line.slice(0, index).trim();
|
|
1359
|
+
// An exact command at the start (optionally after a native prompt),
|
|
1360
|
+
// not a command/path mentioned somewhere inside a diagnostic.
|
|
1361
|
+
return index >= 0 && (!prefix || isPrompt(prefix)) ? index + command.length : -1;
|
|
1362
|
+
};
|
|
1363
|
+
const lines = stripBoundaryBlankLines(delta).split("\n");
|
|
1364
|
+
let removedPreview = false;
|
|
1365
|
+
while (lines.length > 0) {
|
|
1366
|
+
const first = lines[0].trim();
|
|
1367
|
+
if (!first || isPrompt(first)) {
|
|
1368
|
+
lines.shift();
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
if (!removedPreview && previewComment && first === previewComment) {
|
|
1372
|
+
lines.shift();
|
|
1373
|
+
removedPreview = true;
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
const end = echoEnd(lines[0], submissionLine);
|
|
1377
|
+
if (end < 0)
|
|
1378
|
+
break;
|
|
1379
|
+
// Some input editors join output/header to the loader echo. Preserve
|
|
1380
|
+
// the remainder, including its spaces, and consume at most ONE echo:
|
|
1381
|
+
// printing that same command again is legitimate user output.
|
|
1382
|
+
const remainder = lines[0].slice(end);
|
|
1383
|
+
if (remainder.trim())
|
|
1384
|
+
lines[0] = remainder;
|
|
1385
|
+
else
|
|
1386
|
+
lines.shift();
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
const isClojureSubmission = submissionLine.startsWith("(do (load-file ") && submissionLine.endsWith(":pi-repl/silent)");
|
|
1390
|
+
let removedPrompt = false;
|
|
1391
|
+
let removedResult = false;
|
|
1392
|
+
let removedCompletion = false;
|
|
1393
|
+
while (lines.length > 0) {
|
|
1394
|
+
const last = lines[lines.length - 1].trim();
|
|
1395
|
+
if (!last) {
|
|
1396
|
+
lines.pop();
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (!removedPrompt && isPrompt(last)) {
|
|
1400
|
+
lines.pop();
|
|
1401
|
+
removedPrompt = true;
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
if (!removedResult && isClojureSubmission && last === ":pi-repl/silent") {
|
|
1405
|
+
lines.pop();
|
|
1406
|
+
removedResult = true;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (!removedCompletion && completionLine && echoEnd(last, completionLine) === last.length) {
|
|
1410
|
+
lines.pop();
|
|
1411
|
+
removedCompletion = true;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
// Remove native scaffolding while the display still separates it from
|
|
1417
|
+
// payload. Never reclassify that payload using generic loader/path/comment
|
|
1418
|
+
// substrings (e.g. /tmp/pi-repl), which can be real output or error text.
|
|
1419
|
+
const scaffoldCleaned = lines.join("\n");
|
|
1420
|
+
return stripBoundaryBlankLines(display ? stripReplSubmissionDisplay(scaffoldCleaned, display) : scaffoldCleaned);
|
|
1421
|
+
}
|
|
1422
|
+
function cleanupPreparedReplControlFiles(prepared) {
|
|
1423
|
+
if (prepared?.cppCleanup)
|
|
1424
|
+
return prepared.cppCleanup();
|
|
1425
|
+
cleanupPrivateReplControlFiles(prepared?.controlPaths);
|
|
1426
|
+
cleanupPrivateReplControlFiles(prepared?.guardPaths);
|
|
1427
|
+
cleanupPrivateReplControlFiles(prepared?.driverPaths);
|
|
1428
|
+
}
|
|
1429
|
+
function submissionComplete(prepared) {
|
|
1430
|
+
return prepared.cppCompletion ? readCppCompletion(prepared.cppCompletion) !== undefined : existsSync(prepared.controlPaths.doneFile);
|
|
1431
|
+
}
|
|
1432
|
+
async function inspectCppPane(pi, id, cwd) {
|
|
1433
|
+
const result = await execTmux(pi, ["display-message", "-p", "-t", id, "#{pane_id}|#{pane_pid}|#{pane_dead}"], cwd, 3_000);
|
|
1434
|
+
const match = result.stdout.trim().match(/^(%\d+)\|(\d+)\|([01])$/);
|
|
1435
|
+
return result.code === 0 && match && match[1] === id ? { id, pid: match[2], dead: match[3] === "1" } : undefined;
|
|
1436
|
+
}
|
|
1437
|
+
async function cppPaneEnded(pi, state) {
|
|
1438
|
+
if (!state.cppPane)
|
|
1439
|
+
return false;
|
|
1440
|
+
const current = await inspectCppPane(pi, state.cppPane.id, state.cwd);
|
|
1441
|
+
return Boolean(current?.dead && current.pid === state.cppPane.pid);
|
|
1442
|
+
}
|
|
1443
|
+
async function waitForReplDoneFile(pi, sessionName, sessionTarget, paneTarget, cwd, doneFile, timeoutMs, signal, captureContext) {
|
|
1444
|
+
const deadline = Date.now() + timeoutMs;
|
|
1445
|
+
let latestCapture = "";
|
|
1446
|
+
while (Date.now() < deadline) {
|
|
1447
|
+
if (signal?.aborted) {
|
|
1448
|
+
throw new Error("repl_send was aborted.");
|
|
1449
|
+
}
|
|
1450
|
+
if (!(await tmuxSessionExists(pi, sessionTarget, cwd))) {
|
|
1451
|
+
throw new Error(`REPL session ended while waiting for output: ${sessionName}`);
|
|
1452
|
+
}
|
|
1453
|
+
if (captureContext ? submissionComplete(captureContext.prepared) : existsSync(doneFile))
|
|
1454
|
+
return;
|
|
1455
|
+
if (captureContext?.state && await cppPaneEnded(pi, captureContext.state)) {
|
|
1456
|
+
captureContext.state.runtimeEnded = true;
|
|
1457
|
+
throw new Error(`REPL session ended native execution while waiting for output: ${sessionName}. Its dead tmux pane is retained; no replacement was started.`);
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
latestCapture = await capturePaneOutput(pi, paneTarget, cwd);
|
|
1461
|
+
}
|
|
1462
|
+
catch (error) {
|
|
1463
|
+
// Native exit can race the observational capture after has-session.
|
|
1464
|
+
// Classify it consistently; the watcher still verifies settlement
|
|
1465
|
+
// before releasing controls/lease (inspection errors are not ACKs).
|
|
1466
|
+
if (captureContext?.state?.cppPane && !(await tmuxSessionExists(pi, sessionTarget, cwd))) {
|
|
1467
|
+
throw new Error(`REPL session ended while waiting for output: ${sessionName}`);
|
|
1468
|
+
}
|
|
1469
|
+
throw error;
|
|
1470
|
+
}
|
|
1471
|
+
await sleep(REPL_SEND_POLL_MS);
|
|
1472
|
+
}
|
|
1473
|
+
const latestOutput = captureContext
|
|
1474
|
+
? cleanupReplDelta(extractPaneDelta(captureContext.beforeCapture, latestCapture), captureContext.prepared.submissionLine, captureContext.prepared.previewComment, captureContext.prepared.completionLine, captureContext.prepared.display)
|
|
1475
|
+
: latestCapture;
|
|
1476
|
+
const tail = truncateTail(latestOutput, {
|
|
1477
|
+
maxLines: 40,
|
|
1478
|
+
maxBytes: 8 * 1024,
|
|
1479
|
+
}).content.trim();
|
|
1480
|
+
const tailNote = tail ? `\n\nLatest pane output:\n${tail}` : "";
|
|
1481
|
+
throw new Error(`Timed out waiting for REPL output after ${timeoutMs}ms. The session may still be busy; attach with ${formatAttachCommand(sessionName)} or stop it with /repl stop.${tailNote}`);
|
|
1482
|
+
}
|
|
1483
|
+
function normalizeReplSendTarget(target) {
|
|
1484
|
+
const trimmed = target?.trim().toLowerCase();
|
|
1485
|
+
if (!trimmed)
|
|
1486
|
+
return undefined;
|
|
1487
|
+
if (trimmed === "python" || trimmed === "ipython")
|
|
1488
|
+
return "python";
|
|
1489
|
+
if (trimmed === "julia")
|
|
1490
|
+
return "julia";
|
|
1491
|
+
if (trimmed === "r")
|
|
1492
|
+
return "r";
|
|
1493
|
+
if (trimmed === "ghci" || trimmed === "haskell")
|
|
1494
|
+
return "ghci";
|
|
1495
|
+
if (trimmed === "clojure" || trimmed === "clj")
|
|
1496
|
+
return "clojure";
|
|
1497
|
+
if (trimmed === "ruby" || trimmed === "irb")
|
|
1498
|
+
return "ruby";
|
|
1499
|
+
if (trimmed === "java" || trimmed === "jshell")
|
|
1500
|
+
return "java";
|
|
1501
|
+
if (trimmed === "octave" || trimmed === "matlab" || trimmed === "gnuplot" || trimmed === "cpp")
|
|
1502
|
+
return trimmed;
|
|
1503
|
+
throw new Error(`Unknown repl_send target: ${target}`);
|
|
1504
|
+
}
|
|
1505
|
+
async function runReplCode(pi, params, ctx, signal, options = {}) {
|
|
1506
|
+
const code = params.code ?? "";
|
|
1507
|
+
if (!code.trim()) {
|
|
1508
|
+
throw new Error("repl_send requires non-empty code.");
|
|
1509
|
+
}
|
|
1510
|
+
const target = normalizeReplSendTarget(params.target) ?? "python";
|
|
1511
|
+
const sessionName = getSessionNameForSelector(target);
|
|
1512
|
+
if (!(await tmuxSessionExists(pi, sessionName, ctx.cwd))) {
|
|
1513
|
+
if (target === "octave" || target === "matlab" || target === "gnuplot" || target === "cpp")
|
|
1514
|
+
throw new Error(`No default ${getSessionDisplayName(target)} REPL session is running (${sessionName}). Start one with /repl ${target} first.`);
|
|
1515
|
+
if (target === "julia") {
|
|
1516
|
+
throw new Error(`No default Julia REPL session is running (${DEFAULT_JULIA_SESSION}). Start one with /repl julia first.`);
|
|
1517
|
+
}
|
|
1518
|
+
if (target === "r") {
|
|
1519
|
+
throw new Error(`No default R REPL session is running (${DEFAULT_R_SESSION}). Start one with /repl R or /repl r first.`);
|
|
1520
|
+
}
|
|
1521
|
+
if (target === "ghci") {
|
|
1522
|
+
throw new Error(`No default Haskell (GHCi) REPL session is running (${DEFAULT_GHCI_SESSION}). Start one with /repl ghci first.`);
|
|
1523
|
+
}
|
|
1524
|
+
if (target === "clojure") {
|
|
1525
|
+
throw new Error(`No default Clojure REPL session is running (${DEFAULT_CLOJURE_SESSION}). Start one with /repl clojure or /repl clj first.`);
|
|
1526
|
+
}
|
|
1527
|
+
if (target === "ruby") {
|
|
1528
|
+
throw new Error(`No default Ruby REPL session is running (${DEFAULT_RUBY_SESSION}). Start one with /repl ruby first.`);
|
|
1529
|
+
}
|
|
1530
|
+
if (target === "java") {
|
|
1531
|
+
throw new Error(`No default Java REPL session is running (${DEFAULT_JAVA_SESSION}). Start one with /repl java first.`);
|
|
1532
|
+
}
|
|
1533
|
+
throw new Error(`No default Python/IPython REPL session is running (${DEFAULT_PYTHON_SESSION}). Start one with /repl python or /repl ipython first.`);
|
|
1534
|
+
}
|
|
1535
|
+
const sessionInfo = await readSessionInfo(pi, sessionName, ctx.cwd);
|
|
1536
|
+
if (!sessionInfo) {
|
|
1537
|
+
if (target === "octave" || target === "matlab" || target === "gnuplot" || target === "cpp")
|
|
1538
|
+
throw new Error(`Could not inspect the default ${getSessionDisplayName(target)} REPL session (${sessionName}). Inspect it with /repl status ${target}.`);
|
|
1539
|
+
if (target === "julia") {
|
|
1540
|
+
throw new Error(`Could not inspect the default Julia REPL session (${DEFAULT_JULIA_SESSION}). Start it again with /repl julia.`);
|
|
1541
|
+
}
|
|
1542
|
+
if (target === "r") {
|
|
1543
|
+
throw new Error(`Could not inspect the default R REPL session (${DEFAULT_R_SESSION}). Start it again with /repl R or /repl r.`);
|
|
1544
|
+
}
|
|
1545
|
+
if (target === "ghci") {
|
|
1546
|
+
throw new Error(`Could not inspect the default Haskell (GHCi) REPL session (${DEFAULT_GHCI_SESSION}). Start it again with /repl ghci.`);
|
|
1547
|
+
}
|
|
1548
|
+
if (target === "clojure") {
|
|
1549
|
+
throw new Error(`Could not inspect the default Clojure REPL session (${DEFAULT_CLOJURE_SESSION}). Start it again with /repl clojure or /repl clj.`);
|
|
1550
|
+
}
|
|
1551
|
+
if (target === "ruby") {
|
|
1552
|
+
throw new Error(`Could not inspect the default Ruby REPL session (${DEFAULT_RUBY_SESSION}). Start it again with /repl ruby.`);
|
|
1553
|
+
}
|
|
1554
|
+
if (target === "java") {
|
|
1555
|
+
throw new Error(`Could not inspect the default Java REPL session (${DEFAULT_JAVA_SESSION}). Start it again with /repl java.`);
|
|
1556
|
+
}
|
|
1557
|
+
throw new Error(`Could not inspect the default Python/IPython REPL session (${DEFAULT_PYTHON_SESSION}). Start it again with /repl python or /repl ipython.`);
|
|
1558
|
+
}
|
|
1559
|
+
if (options.expectedSession
|
|
1560
|
+
&& (sessionInfo.sessionName !== options.expectedSession.sessionName
|
|
1561
|
+
|| sessionInfo.tmuxSessionId !== options.expectedSession.tmuxSessionId
|
|
1562
|
+
|| sessionInfo.tmuxSessionCreatedAt !== options.expectedSession.tmuxSessionCreatedAt
|
|
1563
|
+
|| (options.expectedRecordId && sessionInfo.recordId !== options.expectedRecordId))) {
|
|
1564
|
+
throw new Error(`REPL session ${sessionName} changed while repl_send was waiting to execute.`);
|
|
1565
|
+
}
|
|
1566
|
+
const runtime = target === "python" ? normalizePythonRuntime(sessionInfo) : target;
|
|
1567
|
+
const timeoutMs = clampReplSendTimeout(params.timeoutMs);
|
|
1568
|
+
const sessionTarget = sessionInfo.tmuxSessionId || sessionName;
|
|
1569
|
+
const paneTarget = await getPaneTarget(pi, sessionTarget, ctx.cwd);
|
|
1570
|
+
const beforeCapture = await capturePaneOutput(pi, paneTarget, ctx.cwd);
|
|
1571
|
+
const cppPane = runtime === "cpp" ? await inspectCppPane(pi, paneTarget, ctx.cwd) : undefined;
|
|
1572
|
+
if (runtime === "cpp" && (!cppPane || cppPane.dead))
|
|
1573
|
+
throw new Error("The C++ pane is ended or cannot be inspected; no code was sent and no replacement was started.");
|
|
1574
|
+
const echoMode = resolveReplSubmissionEchoMode(params.echoMode);
|
|
1575
|
+
const prepared = prepareReplControlFiles(runtime, code, {
|
|
1576
|
+
submissionId: options.submissionId || `pi-repl:local:${randomUUID()}`,
|
|
1577
|
+
echoMode,
|
|
1578
|
+
});
|
|
1579
|
+
const submissionState = {
|
|
1580
|
+
recordId: sessionInfo.recordId,
|
|
1581
|
+
sessionName,
|
|
1582
|
+
sessionTarget,
|
|
1583
|
+
tmuxSessionId: sessionInfo.tmuxSessionId,
|
|
1584
|
+
tmuxSessionCreatedAt: sessionInfo.tmuxSessionCreatedAt,
|
|
1585
|
+
cwd: ctx.cwd,
|
|
1586
|
+
runtime,
|
|
1587
|
+
beforeCapture,
|
|
1588
|
+
prepared,
|
|
1589
|
+
completionObserved: false,
|
|
1590
|
+
cppPane,
|
|
1591
|
+
};
|
|
1592
|
+
let submissionStarted = false;
|
|
1593
|
+
try {
|
|
1594
|
+
await pasteTextToTmuxPane(pi, paneTarget, ctx.cwd, prepared.submissionText, () => {
|
|
1595
|
+
submissionStarted = true;
|
|
1596
|
+
options.onSubmissionStarted?.(submissionState);
|
|
1597
|
+
});
|
|
1598
|
+
await waitForReplDoneFile(pi, sessionName, sessionTarget, paneTarget, ctx.cwd, prepared.controlPaths.doneFile, timeoutMs, signal, { beforeCapture, prepared, state: submissionState });
|
|
1599
|
+
submissionState.completionObserved = true;
|
|
1600
|
+
const afterCapture = await capturePaneOutput(pi, paneTarget, ctx.cwd);
|
|
1601
|
+
const cppResult = prepared.cppCompletion ? readCppCompletion(prepared.cppCompletion) : undefined;
|
|
1602
|
+
const delta = extractPaneDelta(beforeCapture, afterCapture);
|
|
1603
|
+
const output = cleanupReplDelta(delta, prepared.submissionLine, prepared.previewComment, prepared.completionLine, prepared.display);
|
|
1604
|
+
cleanupPreparedReplControlFiles(prepared);
|
|
1605
|
+
return {
|
|
1606
|
+
output,
|
|
1607
|
+
details: {
|
|
1608
|
+
sessionName,
|
|
1609
|
+
runtime,
|
|
1610
|
+
target,
|
|
1611
|
+
timeoutMs,
|
|
1612
|
+
submittedCode: code,
|
|
1613
|
+
cppResult,
|
|
1614
|
+
echoMode,
|
|
1615
|
+
submissionAnchorId: prepared.display.enabled ? prepared.display.anchorId : undefined,
|
|
1616
|
+
previewComment: prepared.previewComment,
|
|
1617
|
+
},
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
if (submissionComplete(prepared))
|
|
1622
|
+
submissionState.completionObserved = true;
|
|
1623
|
+
if (!submissionStarted || submissionState.completionObserved || submissionState.runtimeEnded) {
|
|
1624
|
+
cleanupPreparedReplControlFiles(prepared);
|
|
1625
|
+
}
|
|
1626
|
+
else if (!options.onSubmissionStarted) {
|
|
1627
|
+
retainReplSubmissionUntilSettled(pi, submissionState, null);
|
|
1628
|
+
}
|
|
1629
|
+
throw error;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
function sleepWithoutKeepingProcessAlive(ms) {
|
|
1633
|
+
return new Promise((resolveSleep) => {
|
|
1634
|
+
const timer = setTimeout(resolveSleep, ms);
|
|
1635
|
+
timer.unref?.();
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
function retainReplSubmissionUntilSettled(pi, state, lease) {
|
|
1639
|
+
// A timeout or abort only stops the caller's wait; it does not stop code that
|
|
1640
|
+
// is already running in the shared REPL. Keep the private control files (and
|
|
1641
|
+
// any shared lease) until the wrapper reports completion or the session ends.
|
|
1642
|
+
void (async () => {
|
|
1643
|
+
let missingChecks = 0;
|
|
1644
|
+
try {
|
|
1645
|
+
while (!submissionComplete(state.prepared)) {
|
|
1646
|
+
if (!state.prepared.cppCompletion && !existsSync(state.prepared.controlPaths.sourceFile))
|
|
1647
|
+
return;
|
|
1648
|
+
try {
|
|
1649
|
+
if (await cppPaneEnded(pi, state)) {
|
|
1650
|
+
state.runtimeEnded = true;
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const current = await readSessionInfo(pi, state.sessionName, state.cwd);
|
|
1654
|
+
if (current
|
|
1655
|
+
&& current.tmuxSessionId === state.tmuxSessionId
|
|
1656
|
+
&& current.tmuxSessionCreatedAt === state.tmuxSessionCreatedAt
|
|
1657
|
+
&& (!state.recordId || current.recordId === state.recordId)) {
|
|
1658
|
+
missingChecks = 0;
|
|
1659
|
+
}
|
|
1660
|
+
else {
|
|
1661
|
+
missingChecks += 1;
|
|
1662
|
+
if (missingChecks >= 3)
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
catch {
|
|
1667
|
+
// A transient inspection failure must not make overlapping sends safe.
|
|
1668
|
+
missingChecks = 0;
|
|
1669
|
+
}
|
|
1670
|
+
await sleepWithoutKeepingProcessAlive(REPL_SEND_POLL_MS);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
finally {
|
|
1674
|
+
cleanupPreparedReplControlFiles(state.prepared);
|
|
1675
|
+
await lease?.release().catch(() => undefined);
|
|
1676
|
+
}
|
|
1677
|
+
})();
|
|
1678
|
+
}
|
|
1679
|
+
async function runRecordedReplCode(pi, params, ctx, signal, metadata) {
|
|
1680
|
+
const target = normalizeReplSendTarget(params.target) ?? "python";
|
|
1681
|
+
const sessionName = getSessionNameForSelector(target);
|
|
1682
|
+
const submissionId = `pi-repl:${metadata.requestId}`;
|
|
1683
|
+
const sessionInfo = await readSessionInfo(pi, sessionName, ctx.cwd);
|
|
1684
|
+
if (!sessionInfo?.recordId || !sessionInfo.recordPath || sessionInfo.recordWarning) {
|
|
1685
|
+
const execution = await runReplCode(pi, params, ctx, signal, { submissionId });
|
|
1686
|
+
return {
|
|
1687
|
+
...execution,
|
|
1688
|
+
details: {
|
|
1689
|
+
...execution.details,
|
|
1690
|
+
recordWarning: sessionInfo?.recordWarning || "The tmux session has no compatible clean-record metadata; execution was not synchronized.",
|
|
1691
|
+
},
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
const runtime = target === "python" ? normalizePythonRuntime(sessionInfo) : target;
|
|
1695
|
+
const identity = {
|
|
1696
|
+
sessionName,
|
|
1697
|
+
tmuxSessionId: sessionInfo.tmuxSessionId,
|
|
1698
|
+
tmuxSessionCreatedAt: sessionInfo.tmuxSessionCreatedAt,
|
|
1699
|
+
runtime,
|
|
1700
|
+
};
|
|
1701
|
+
const timeoutMs = clampReplSendTimeout(params.timeoutMs);
|
|
1702
|
+
const lease = await acquireReplSessionSendLease(sessionInfo.recordId, {
|
|
1703
|
+
owner: `pi-repl:${metadata.requestId || process.pid}`,
|
|
1704
|
+
waitMs: timeoutMs,
|
|
1705
|
+
signal,
|
|
1706
|
+
});
|
|
1707
|
+
const submissionStateRef = { current: null };
|
|
1708
|
+
let recordEntry = null;
|
|
1709
|
+
let recordWarning = sessionInfo.recordWarning;
|
|
1710
|
+
try {
|
|
1711
|
+
try {
|
|
1712
|
+
const recorded = upsertReplSessionRecordEntry(sessionInfo.recordId, identity, {
|
|
1713
|
+
id: submissionId,
|
|
1714
|
+
requestId: metadata.requestId,
|
|
1715
|
+
origin: "pi-repl",
|
|
1716
|
+
label: metadata.label,
|
|
1717
|
+
mode: metadata.mode,
|
|
1718
|
+
code: params.code,
|
|
1719
|
+
status: "sending",
|
|
1720
|
+
}, { origin: "pi-repl" });
|
|
1721
|
+
recordEntry = recorded.entry;
|
|
1722
|
+
}
|
|
1723
|
+
catch (error) {
|
|
1724
|
+
recordWarning = error instanceof Error ? error.message : String(error);
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
const execution = await runReplCode(pi, params, ctx, signal, {
|
|
1728
|
+
expectedSession: identity,
|
|
1729
|
+
expectedRecordId: sessionInfo.recordId,
|
|
1730
|
+
submissionId,
|
|
1731
|
+
onSubmissionStarted: (state) => {
|
|
1732
|
+
submissionStateRef.current = state;
|
|
1733
|
+
},
|
|
1734
|
+
});
|
|
1735
|
+
if (recordEntry) {
|
|
1736
|
+
try {
|
|
1737
|
+
const recorded = upsertReplSessionRecordEntry(sessionInfo.recordId, identity, {
|
|
1738
|
+
...recordEntry,
|
|
1739
|
+
output: execution.output,
|
|
1740
|
+
status: "captured",
|
|
1741
|
+
completedAt: Date.now(),
|
|
1742
|
+
}, { origin: "pi-repl" });
|
|
1743
|
+
recordEntry = recorded.entry;
|
|
1744
|
+
}
|
|
1745
|
+
catch (error) {
|
|
1746
|
+
recordWarning = error instanceof Error ? error.message : String(error);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
...execution,
|
|
1751
|
+
details: {
|
|
1752
|
+
...execution.details,
|
|
1753
|
+
recordId: sessionInfo.recordId,
|
|
1754
|
+
recordPath: sessionInfo.recordPath,
|
|
1755
|
+
recordEntryId: recordEntry?.id,
|
|
1756
|
+
recordWarning,
|
|
1757
|
+
},
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
catch (error) {
|
|
1761
|
+
if (recordEntry) {
|
|
1762
|
+
try {
|
|
1763
|
+
upsertReplSessionRecordEntry(sessionInfo.recordId, identity, {
|
|
1764
|
+
...recordEntry,
|
|
1765
|
+
output: error instanceof Error ? error.message : String(error),
|
|
1766
|
+
status: error instanceof Error && /timed out/i.test(error.message) ? "timeout" : "error",
|
|
1767
|
+
completedAt: Date.now(),
|
|
1768
|
+
}, { origin: "pi-repl" });
|
|
1769
|
+
}
|
|
1770
|
+
catch {
|
|
1771
|
+
// Preserve the original execution error when record maintenance also fails.
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
throw error;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
finally {
|
|
1778
|
+
const submissionState = submissionStateRef.current;
|
|
1779
|
+
if (submissionState
|
|
1780
|
+
&& !submissionState.completionObserved
|
|
1781
|
+
&& !submissionState.runtimeEnded
|
|
1782
|
+
&& !submissionComplete(submissionState.prepared)) {
|
|
1783
|
+
retainReplSubmissionUntilSettled(pi, submissionState, lease);
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
cleanupPreparedReplControlFiles(submissionState?.prepared);
|
|
1787
|
+
await lease.release().catch(() => undefined);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
export function formatReplSendResult(output, details) {
|
|
1792
|
+
const fullText = [
|
|
1793
|
+
"Submitted code:",
|
|
1794
|
+
details.submittedCode.trimEnd(),
|
|
1795
|
+
"",
|
|
1796
|
+
"Output:",
|
|
1797
|
+
output.trim() ? output : "(no output)",
|
|
1798
|
+
...(details.recordWarning ? ["", `Shared record warning: ${details.recordWarning}`] : []),
|
|
1799
|
+
].join("\n");
|
|
1800
|
+
const initial = truncateHead(fullText, { maxLines: DEFAULT_MAX_LINES, maxBytes: DEFAULT_MAX_BYTES });
|
|
1801
|
+
if (!initial.truncated)
|
|
1802
|
+
return { text: fullText, details };
|
|
1803
|
+
// Bound the entire response, including submitted source, not just output.
|
|
1804
|
+
// Reserve room for the truncation notice and keep the complete response private.
|
|
1805
|
+
const tempDir = mkdtempSync(join(tmpdir(), "pi-repl-output-"));
|
|
1806
|
+
const tempFile = join(tempDir, "submission.txt");
|
|
1807
|
+
writeFileSync(tempFile, fullText, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
1808
|
+
const notice = `\n\n[REPL response truncated. Full submitted code and output saved to: ${tempFile}]`;
|
|
1809
|
+
const truncation = truncateHead(fullText, {
|
|
1810
|
+
maxLines: DEFAULT_MAX_LINES - 3,
|
|
1811
|
+
maxBytes: DEFAULT_MAX_BYTES - Buffer.byteLength(notice, "utf8"),
|
|
1812
|
+
});
|
|
1813
|
+
return {
|
|
1814
|
+
text: truncation.content + notice,
|
|
1815
|
+
details: { ...details, truncation, fullOutputPath: tempFile },
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
async function executeReplSend(pi, params, ctx, signal, toolCallId) {
|
|
1819
|
+
const execution = await runRecordedReplCode(pi, params, ctx, signal, {
|
|
1820
|
+
requestId: `tool:${toolCallId}`,
|
|
1821
|
+
label: getToolSubmissionLabel(),
|
|
1822
|
+
mode: "agent",
|
|
1823
|
+
});
|
|
1824
|
+
const formatted = formatReplSendResult(execution.output, execution.details);
|
|
1825
|
+
return {
|
|
1826
|
+
content: [{ type: "text", text: formatted.text }],
|
|
1827
|
+
details: formatted.details,
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
function buildReplEnvInspectionCode() {
|
|
1831
|
+
return [
|
|
1832
|
+
"import os, sys",
|
|
1833
|
+
"print(f'sys.executable={sys.executable}')",
|
|
1834
|
+
"print(f'sys.prefix={sys.prefix}')",
|
|
1835
|
+
"print(f'sys.base_prefix={getattr(sys, \"base_prefix\", sys.prefix)}')",
|
|
1836
|
+
"print(f'VIRTUAL_ENV={os.environ.get(\"VIRTUAL_ENV\") or \"\"}')",
|
|
1837
|
+
"print(f'CONDA_DEFAULT_ENV={os.environ.get(\"CONDA_DEFAULT_ENV\") or \"\"}')",
|
|
1838
|
+
"print(f'CONDA_PREFIX={os.environ.get(\"CONDA_PREFIX\") or \"\"}')",
|
|
1839
|
+
"print(f'PYENV_VERSION={os.environ.get(\"PYENV_VERSION\") or \"\"}')",
|
|
1840
|
+
].join("\n");
|
|
1841
|
+
}
|
|
1842
|
+
async function showDefaultPythonEnv(pi, ctx) {
|
|
1843
|
+
try {
|
|
1844
|
+
const execution = await runRecordedReplCode(pi, {
|
|
1845
|
+
code: buildReplEnvInspectionCode(),
|
|
1846
|
+
timeoutMs: 10_000,
|
|
1847
|
+
}, ctx, undefined, { requestId: `command:env:${Date.now().toString(36)}`, label: "/repl env", mode: "raw" });
|
|
1848
|
+
notify(ctx, [
|
|
1849
|
+
"Python/IPython REPL environment:",
|
|
1850
|
+
`Runtime: ${execution.details.runtime}`,
|
|
1851
|
+
`Session: ${execution.details.sessionName}`,
|
|
1852
|
+
"",
|
|
1853
|
+
execution.output.trim() || "(no output)",
|
|
1854
|
+
].join("\n"), "info");
|
|
1855
|
+
}
|
|
1856
|
+
catch (error) {
|
|
1857
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
async function startDefaultReplSession(pi, cwd, requested, timeoutMs = DEFAULT_STARTUP_WAIT_MS, signal) {
|
|
1861
|
+
const runtime = requested === "clj" ? "clojure" : requested;
|
|
1862
|
+
const target = toSessionSelector(runtime);
|
|
1863
|
+
const sessionName = getSessionNameForSelector(target);
|
|
1864
|
+
checkStartAborted(signal, sessionName);
|
|
1865
|
+
if (!(await commandExists(pi, "tmux", cwd))) {
|
|
1866
|
+
throw new Error("tmux was not found on PATH. pi-repl requires tmux.");
|
|
1867
|
+
}
|
|
1868
|
+
checkStartAborted(signal, sessionName);
|
|
1869
|
+
const exists = await tmuxSessionExists(pi, sessionName, cwd);
|
|
1870
|
+
checkStartAborted(signal, sessionName);
|
|
1871
|
+
const shellLaunch = buildDefaultShellRuntimeCommand(runtime, cwd);
|
|
1872
|
+
let created = false;
|
|
1873
|
+
let sessionTarget = sessionName;
|
|
1874
|
+
const warnings = [];
|
|
1875
|
+
if (!exists) {
|
|
1876
|
+
// tmux atomically creates the name. Do not use -A, respawn or kill: a
|
|
1877
|
+
// concurrent winner must be reused without rewriting its metadata/log.
|
|
1878
|
+
const gnuplotOwner = runtime === "gnuplot" ? randomUUID().replace(/-/g, "") : undefined;
|
|
1879
|
+
const result = await execTmux(pi, [
|
|
1880
|
+
"new-session", "-d", "-P", "-F", "#{session_id}", "-s", sessionName, "-c", cwd,
|
|
1881
|
+
...(gnuplotOwner ? ["-e", `${GNUPLOT_OWNER_ENV}=${gnuplotOwner}`] : []), shellLaunch.command,
|
|
1882
|
+
], cwd, 10_000);
|
|
1883
|
+
if (result.code !== 0) {
|
|
1884
|
+
if (!(await tmuxSessionExists(pi, sessionName, cwd))) {
|
|
1885
|
+
const reason = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
|
|
1886
|
+
throw new Error(`Failed to create tmux session ${sessionName}: ${reason}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
created = true;
|
|
1891
|
+
sessionTarget = result.stdout.trim();
|
|
1892
|
+
if (!/^\$\d+$/.test(sessionTarget))
|
|
1893
|
+
throw new Error(`Could not identify newly created session ${sessionName}; inspect it with repl_status. It has not been stopped.`);
|
|
1894
|
+
// Finish initial metadata even if the caller cancels during creation.
|
|
1895
|
+
// Pin the returned ID so an external same-name replacement is untouched.
|
|
1896
|
+
if (gnuplotOwner && !(await setTmuxSessionOption(pi, sessionTarget, GNUPLOT_OWNER_OPTION, gnuplotOwner, cwd))) {
|
|
1897
|
+
warnings.push("Could not record gnuplot helper ownership; detached graphics cleanup cannot be verified.");
|
|
1898
|
+
}
|
|
1899
|
+
if (!(await setTmuxSessionOption(pi, sessionTarget, REPL_RUNTIME_OPTION, runtime, cwd))) {
|
|
1900
|
+
warnings.push(`Could not record the runtime for ${sessionName}.`);
|
|
1901
|
+
}
|
|
1902
|
+
const history = await enableSessionHistoryLogging(pi, sessionName, cwd, sessionTarget);
|
|
1903
|
+
if (history.warning)
|
|
1904
|
+
warnings.push(history.warning);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const { info, ready } = await waitForReplSessionInfo(pi, cwd, sessionTarget, sessionName, runtime, timeoutMs, signal);
|
|
1908
|
+
if (!ready)
|
|
1909
|
+
warnings.push(`No normal prompt was confirmed within ${timeoutMs}ms. The session is left running; it may be busy, awaiting input, or using a custom prompt. Inspect it before sending code.`);
|
|
1910
|
+
if (info.runtime && info.runtime !== runtime)
|
|
1911
|
+
warnings.push(`Requested ${runtime}, but the existing session reports ${info.runtime}. Its interpreter and state were preserved.`);
|
|
1912
|
+
const text = [
|
|
1913
|
+
created
|
|
1914
|
+
? `Started default ${getSessionDisplayName(target)} REPL session: ${sessionName}`
|
|
1915
|
+
: `Default ${getSessionDisplayName(target)} REPL session is already running; reused without restarting (requested: ${runtime}).`,
|
|
1916
|
+
...(created ? [
|
|
1917
|
+
`Launch method: ${shellLaunch.shell} -i -l -c '${buildRuntimeLaunchCommand(runtime)}' inside tmux.`,
|
|
1918
|
+
...(target === "python" ? ["This respects your normal shell-level Python setup (aliases, pyenv/virtualenv/conda activation, shell init, etc.)."] : []),
|
|
1919
|
+
...(target === "clojure" ? ["`clojure` is used without rlwrap; `/repl clj` remains an alias."] : []),
|
|
1920
|
+
...(target === "gnuplot" ? ["An inherited ownership marker identifies this session's interpreter and Qt helpers during explicit stop; plotting settings are unchanged."] : []),
|
|
1921
|
+
] : []),
|
|
1922
|
+
ready ? "Readiness: normal prompt observed (snapshot only, not a reservation)." : "Readiness: unconfirmed.",
|
|
1923
|
+
...warnings.map((warning) => `Warning: ${warning}`),
|
|
1924
|
+
"",
|
|
1925
|
+
formatSessionInfo(info),
|
|
1926
|
+
].join("\n");
|
|
1927
|
+
return {
|
|
1928
|
+
text,
|
|
1929
|
+
details: {
|
|
1930
|
+
requestedRuntime: runtime,
|
|
1931
|
+
runtime: info.runtime,
|
|
1932
|
+
target,
|
|
1933
|
+
sessionName: info.sessionName,
|
|
1934
|
+
created,
|
|
1935
|
+
reused: !created,
|
|
1936
|
+
ready,
|
|
1937
|
+
timeoutMs,
|
|
1938
|
+
attachCommand: formatAttachCommand(info.sessionName),
|
|
1939
|
+
warnings,
|
|
1940
|
+
session: buildReplStatusDetails([{ selector: target, info }])[target],
|
|
1941
|
+
},
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
async function startReplCommand(pi, ctx, runtime) {
|
|
1945
|
+
try {
|
|
1946
|
+
const result = await startDefaultReplSession(pi, ctx.cwd, runtime);
|
|
1947
|
+
notify(ctx, result.text, result.details.warnings.length ? "warning" : "info");
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function formatNoSessionRunning(selector) {
|
|
1954
|
+
if (selector === "octave" || selector === "matlab" || selector === "gnuplot" || selector === "cpp")
|
|
1955
|
+
return `No default ${getSessionDisplayName(selector)} REPL session is running (${getSessionNameForSelector(selector)}).\nStart one with /repl ${selector} or /lab ${selector}.`;
|
|
1956
|
+
if (selector === "julia") {
|
|
1957
|
+
return [
|
|
1958
|
+
`No default Julia REPL session is running (${DEFAULT_JULIA_SESSION}).`,
|
|
1959
|
+
"Start one with /repl julia or /lab julia.",
|
|
1960
|
+
].join("\n");
|
|
1961
|
+
}
|
|
1962
|
+
if (selector === "r") {
|
|
1963
|
+
return [
|
|
1964
|
+
`No default R REPL session is running (${DEFAULT_R_SESSION}).`,
|
|
1965
|
+
"Start one with /repl R or /repl r, or /lab R or /lab r.",
|
|
1966
|
+
].join("\n");
|
|
1967
|
+
}
|
|
1968
|
+
if (selector === "ghci") {
|
|
1969
|
+
return [
|
|
1970
|
+
`No default Haskell (GHCi) REPL session is running (${DEFAULT_GHCI_SESSION}).`,
|
|
1971
|
+
"Start one with /repl ghci or /lab ghci.",
|
|
1972
|
+
].join("\n");
|
|
1973
|
+
}
|
|
1974
|
+
if (selector === "clojure") {
|
|
1975
|
+
return [
|
|
1976
|
+
`No default Clojure REPL session is running (${DEFAULT_CLOJURE_SESSION}).`,
|
|
1977
|
+
"Start one with /repl clojure or /repl clj, or /lab clojure or /lab clj.",
|
|
1978
|
+
].join("\n");
|
|
1979
|
+
}
|
|
1980
|
+
if (selector === "ruby") {
|
|
1981
|
+
return [
|
|
1982
|
+
`No default Ruby REPL session is running (${DEFAULT_RUBY_SESSION}).`,
|
|
1983
|
+
"Start one with /repl ruby or /lab ruby.",
|
|
1984
|
+
].join("\n");
|
|
1985
|
+
}
|
|
1986
|
+
if (selector === "java") {
|
|
1987
|
+
return [
|
|
1988
|
+
`No default Java REPL session is running (${DEFAULT_JAVA_SESSION}).`,
|
|
1989
|
+
"Start one with /repl java or /lab java.",
|
|
1990
|
+
].join("\n");
|
|
1991
|
+
}
|
|
1992
|
+
return [
|
|
1993
|
+
`No default Python/IPython REPL session is running (${DEFAULT_PYTHON_SESSION}).`,
|
|
1994
|
+
"Start one with /repl python, /repl ipython, /lab python, or /lab ipython.",
|
|
1995
|
+
].join("\n");
|
|
1996
|
+
}
|
|
1997
|
+
function buildReplStatusDetails(sessions) {
|
|
1998
|
+
const python = sessions.find((session) => session.selector === "python")?.info;
|
|
1999
|
+
const julia = sessions.find((session) => session.selector === "julia")?.info;
|
|
2000
|
+
const r = sessions.find((session) => session.selector === "r")?.info;
|
|
2001
|
+
const ghci = sessions.find((session) => session.selector === "ghci")?.info;
|
|
2002
|
+
const clojure = sessions.find((session) => session.selector === "clojure")?.info;
|
|
2003
|
+
const ruby = sessions.find((session) => session.selector === "ruby")?.info;
|
|
2004
|
+
const java = sessions.find((session) => session.selector === "java")?.info;
|
|
2005
|
+
const additionalRuntimeDetails = Object.fromEntries(["octave", "matlab", "gnuplot", "cpp"].map((selector) => {
|
|
2006
|
+
const info = sessions.find((session) => session.selector === selector)?.info;
|
|
2007
|
+
return [selector, {
|
|
2008
|
+
running: Boolean(info), sessionName: info?.sessionName ?? getSessionNameForSelector(selector),
|
|
2009
|
+
runtime: info?.runtime, recordId: info?.recordId, recordPath: info?.recordPath,
|
|
2010
|
+
recordEntryCount: info?.recordEntryCount ?? 0, recordEntries: info?.recordTail ?? [],
|
|
2011
|
+
recordWarning: info?.recordWarning, historyPath: info?.historyPath, historyLogging: Boolean(info?.historyPath),
|
|
2012
|
+
currentCommand: info?.currentCommand, currentPath: info?.currentPath,
|
|
2013
|
+
attachCommand: formatAttachCommand(getSessionNameForSelector(selector)),
|
|
2014
|
+
}];
|
|
2015
|
+
}));
|
|
2016
|
+
return {
|
|
2017
|
+
...additionalRuntimeDetails,
|
|
2018
|
+
python: {
|
|
2019
|
+
running: Boolean(python),
|
|
2020
|
+
sessionName: python?.sessionName ?? DEFAULT_PYTHON_SESSION,
|
|
2021
|
+
runtime: python?.runtime ?? undefined,
|
|
2022
|
+
recordId: python?.recordId ?? undefined,
|
|
2023
|
+
recordPath: python?.recordPath ?? undefined,
|
|
2024
|
+
recordEntryCount: python?.recordEntryCount ?? 0,
|
|
2025
|
+
recordEntries: python?.recordTail ?? [],
|
|
2026
|
+
recordWarning: python?.recordWarning ?? undefined,
|
|
2027
|
+
historyPath: python?.historyPath ?? undefined,
|
|
2028
|
+
historyLogging: Boolean(python?.historyPath),
|
|
2029
|
+
currentCommand: python?.currentCommand ?? undefined,
|
|
2030
|
+
currentPath: python?.currentPath ?? undefined,
|
|
2031
|
+
attachCommand: formatAttachCommand(DEFAULT_PYTHON_SESSION),
|
|
2032
|
+
},
|
|
2033
|
+
julia: {
|
|
2034
|
+
running: Boolean(julia),
|
|
2035
|
+
sessionName: julia?.sessionName ?? DEFAULT_JULIA_SESSION,
|
|
2036
|
+
runtime: julia?.runtime ?? undefined,
|
|
2037
|
+
recordId: julia?.recordId ?? undefined,
|
|
2038
|
+
recordPath: julia?.recordPath ?? undefined,
|
|
2039
|
+
recordEntryCount: julia?.recordEntryCount ?? 0,
|
|
2040
|
+
recordEntries: julia?.recordTail ?? [],
|
|
2041
|
+
recordWarning: julia?.recordWarning ?? undefined,
|
|
2042
|
+
historyPath: julia?.historyPath ?? undefined,
|
|
2043
|
+
historyLogging: Boolean(julia?.historyPath),
|
|
2044
|
+
currentCommand: julia?.currentCommand ?? undefined,
|
|
2045
|
+
currentPath: julia?.currentPath ?? undefined,
|
|
2046
|
+
attachCommand: formatAttachCommand(DEFAULT_JULIA_SESSION),
|
|
2047
|
+
},
|
|
2048
|
+
r: {
|
|
2049
|
+
running: Boolean(r),
|
|
2050
|
+
sessionName: r?.sessionName ?? DEFAULT_R_SESSION,
|
|
2051
|
+
runtime: r?.runtime ?? undefined,
|
|
2052
|
+
recordId: r?.recordId ?? undefined,
|
|
2053
|
+
recordPath: r?.recordPath ?? undefined,
|
|
2054
|
+
recordEntryCount: r?.recordEntryCount ?? 0,
|
|
2055
|
+
recordEntries: r?.recordTail ?? [],
|
|
2056
|
+
recordWarning: r?.recordWarning ?? undefined,
|
|
2057
|
+
historyPath: r?.historyPath ?? undefined,
|
|
2058
|
+
historyLogging: Boolean(r?.historyPath),
|
|
2059
|
+
currentCommand: r?.currentCommand ?? undefined,
|
|
2060
|
+
currentPath: r?.currentPath ?? undefined,
|
|
2061
|
+
attachCommand: formatAttachCommand(DEFAULT_R_SESSION),
|
|
2062
|
+
},
|
|
2063
|
+
ghci: {
|
|
2064
|
+
running: Boolean(ghci),
|
|
2065
|
+
sessionName: ghci?.sessionName ?? DEFAULT_GHCI_SESSION,
|
|
2066
|
+
runtime: ghci?.runtime ?? undefined,
|
|
2067
|
+
recordId: ghci?.recordId ?? undefined,
|
|
2068
|
+
recordPath: ghci?.recordPath ?? undefined,
|
|
2069
|
+
recordEntryCount: ghci?.recordEntryCount ?? 0,
|
|
2070
|
+
recordEntries: ghci?.recordTail ?? [],
|
|
2071
|
+
recordWarning: ghci?.recordWarning ?? undefined,
|
|
2072
|
+
historyPath: ghci?.historyPath ?? undefined,
|
|
2073
|
+
historyLogging: Boolean(ghci?.historyPath),
|
|
2074
|
+
currentCommand: ghci?.currentCommand ?? undefined,
|
|
2075
|
+
currentPath: ghci?.currentPath ?? undefined,
|
|
2076
|
+
attachCommand: formatAttachCommand(DEFAULT_GHCI_SESSION),
|
|
2077
|
+
},
|
|
2078
|
+
clojure: {
|
|
2079
|
+
running: Boolean(clojure),
|
|
2080
|
+
sessionName: clojure?.sessionName ?? DEFAULT_CLOJURE_SESSION,
|
|
2081
|
+
runtime: clojure?.runtime ?? undefined,
|
|
2082
|
+
recordId: clojure?.recordId ?? undefined,
|
|
2083
|
+
recordPath: clojure?.recordPath ?? undefined,
|
|
2084
|
+
recordEntryCount: clojure?.recordEntryCount ?? 0,
|
|
2085
|
+
recordEntries: clojure?.recordTail ?? [],
|
|
2086
|
+
recordWarning: clojure?.recordWarning ?? undefined,
|
|
2087
|
+
historyPath: clojure?.historyPath ?? undefined,
|
|
2088
|
+
historyLogging: Boolean(clojure?.historyPath),
|
|
2089
|
+
currentCommand: clojure?.currentCommand ?? undefined,
|
|
2090
|
+
currentPath: clojure?.currentPath ?? undefined,
|
|
2091
|
+
attachCommand: formatAttachCommand(DEFAULT_CLOJURE_SESSION),
|
|
2092
|
+
},
|
|
2093
|
+
ruby: {
|
|
2094
|
+
running: Boolean(ruby),
|
|
2095
|
+
sessionName: ruby?.sessionName ?? DEFAULT_RUBY_SESSION,
|
|
2096
|
+
runtime: ruby?.runtime ?? undefined,
|
|
2097
|
+
recordId: ruby?.recordId ?? undefined,
|
|
2098
|
+
recordPath: ruby?.recordPath ?? undefined,
|
|
2099
|
+
recordEntryCount: ruby?.recordEntryCount ?? 0,
|
|
2100
|
+
recordEntries: ruby?.recordTail ?? [],
|
|
2101
|
+
recordWarning: ruby?.recordWarning ?? undefined,
|
|
2102
|
+
historyPath: ruby?.historyPath ?? undefined,
|
|
2103
|
+
historyLogging: Boolean(ruby?.historyPath),
|
|
2104
|
+
currentCommand: ruby?.currentCommand ?? undefined,
|
|
2105
|
+
currentPath: ruby?.currentPath ?? undefined,
|
|
2106
|
+
attachCommand: formatAttachCommand(DEFAULT_RUBY_SESSION),
|
|
2107
|
+
},
|
|
2108
|
+
java: {
|
|
2109
|
+
running: Boolean(java),
|
|
2110
|
+
sessionName: java?.sessionName ?? DEFAULT_JAVA_SESSION,
|
|
2111
|
+
runtime: java?.runtime ?? undefined,
|
|
2112
|
+
recordId: java?.recordId ?? undefined,
|
|
2113
|
+
recordPath: java?.recordPath ?? undefined,
|
|
2114
|
+
recordEntryCount: java?.recordEntryCount ?? 0,
|
|
2115
|
+
recordEntries: java?.recordTail ?? [],
|
|
2116
|
+
recordWarning: java?.recordWarning ?? undefined,
|
|
2117
|
+
historyPath: java?.historyPath ?? undefined,
|
|
2118
|
+
historyLogging: Boolean(java?.historyPath),
|
|
2119
|
+
currentCommand: java?.currentCommand ?? undefined,
|
|
2120
|
+
currentPath: java?.currentPath ?? undefined,
|
|
2121
|
+
attachCommand: formatAttachCommand(DEFAULT_JAVA_SESSION),
|
|
2122
|
+
},
|
|
2123
|
+
runningSessions: sessions.map((session) => ({
|
|
2124
|
+
target: session.selector,
|
|
2125
|
+
sessionName: session.info.sessionName,
|
|
2126
|
+
runtime: session.info.runtime,
|
|
2127
|
+
recordId: session.info.recordId,
|
|
2128
|
+
recordPath: session.info.recordPath,
|
|
2129
|
+
recordEntryCount: session.info.recordEntryCount ?? 0,
|
|
2130
|
+
recordEntries: session.info.recordTail ?? [],
|
|
2131
|
+
recordWarning: session.info.recordWarning,
|
|
2132
|
+
historyPath: session.info.historyPath,
|
|
2133
|
+
historyLogging: Boolean(session.info.historyPath),
|
|
2134
|
+
currentCommand: session.info.currentCommand,
|
|
2135
|
+
currentPath: session.info.currentPath,
|
|
2136
|
+
attachCommand: formatAttachCommand(session.info.sessionName),
|
|
2137
|
+
})),
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
async function showReplStatus(pi, ctx, selector) {
|
|
2141
|
+
if (selector) {
|
|
2142
|
+
const sessionName = getSessionNameForSelector(selector);
|
|
2143
|
+
const info = await readSessionInfo(pi, sessionName, ctx.cwd);
|
|
2144
|
+
if (!info) {
|
|
2145
|
+
notify(ctx, formatNoSessionRunning(selector), "info");
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
notify(ctx, `${getSessionDisplayName(selector, info)} REPL session is running.\n\n${formatSessionInfo(info)}`, "info");
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
const running = await listRunningSharedSessions(pi, ctx.cwd);
|
|
2152
|
+
if (running.length === 0) {
|
|
2153
|
+
notify(ctx, `No shared REPL sessions are running. Start one with /repl followed by ${REPL_START_RUNTIMES.join(", ")}.`, "info");
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (running.length === 1) {
|
|
2157
|
+
const only = running[0];
|
|
2158
|
+
notify(ctx, `${getSessionDisplayName(only.selector, only.info)} REPL session is running.\n\n${formatSessionInfo(only.info)}`, "info");
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
const message = [
|
|
2162
|
+
"Shared REPL sessions are running:",
|
|
2163
|
+
"",
|
|
2164
|
+
...running.flatMap((session, index) => [
|
|
2165
|
+
`${getSessionDisplayName(session.selector, session.info)} session:`,
|
|
2166
|
+
formatSessionInfo(session.info),
|
|
2167
|
+
...(index < running.length - 1 ? [""] : []),
|
|
2168
|
+
]),
|
|
2169
|
+
].join("\n");
|
|
2170
|
+
notify(ctx, message, "info");
|
|
2171
|
+
}
|
|
2172
|
+
async function stopReplSession(pi, ctx, selector) {
|
|
2173
|
+
if (!selector) {
|
|
2174
|
+
const running = await listRunningSharedSessions(pi, ctx.cwd);
|
|
2175
|
+
if (running.length === 0) {
|
|
2176
|
+
notify(ctx, `No shared REPL sessions are running. Start one with /repl followed by ${REPL_START_RUNTIMES.join(", ")}.`, "info");
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (running.length > 1) {
|
|
2180
|
+
notify(ctx, [
|
|
2181
|
+
"Multiple shared REPL sessions are running.",
|
|
2182
|
+
"Use one of:",
|
|
2183
|
+
"/repl stop python",
|
|
2184
|
+
"/repl stop julia",
|
|
2185
|
+
"/repl stop r",
|
|
2186
|
+
"/repl stop ghci",
|
|
2187
|
+
"/repl stop clojure",
|
|
2188
|
+
"/repl stop ruby",
|
|
2189
|
+
"/repl stop java",
|
|
2190
|
+
"/repl stop octave",
|
|
2191
|
+
"/repl stop matlab",
|
|
2192
|
+
"/repl stop gnuplot",
|
|
2193
|
+
"/repl stop cpp",
|
|
2194
|
+
].join("\n"), "warning");
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
selector = running[0].selector;
|
|
2198
|
+
}
|
|
2199
|
+
const sessionName = getSessionNameForSelector(selector);
|
|
2200
|
+
const exists = await tmuxSessionExists(pi, sessionName, ctx.cwd);
|
|
2201
|
+
if (!exists) {
|
|
2202
|
+
notify(ctx, formatNoSessionRunning(selector), "info");
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
try {
|
|
2206
|
+
const result = await stopVerifiedReplSession({
|
|
2207
|
+
tmux: (args) => execTmux(pi, args, ctx.cwd, 3_000),
|
|
2208
|
+
sessionName,
|
|
2209
|
+
});
|
|
2210
|
+
const escalated = result.signals.length ? ` Cleaned up ${new Set(result.signals.map((entry) => entry.pid)).size} surviving process(es).` : "";
|
|
2211
|
+
const warnings = result.warnings ?? [];
|
|
2212
|
+
notify(ctx, `Stopped default ${getSessionDisplayName(selector)} REPL session: ${sessionName}. Verified owned runtime processes exited.${escalated}${warnings.length ? "\n" + warnings.join("\n") : ""}`, warnings.length ? "warning" : "info");
|
|
2213
|
+
}
|
|
2214
|
+
catch (error) {
|
|
2215
|
+
notify(ctx, `Could not fully stop ${sessionName}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
async function attachReplSession(pi, ctx, selector) {
|
|
2219
|
+
if (!selector) {
|
|
2220
|
+
const running = await listRunningSharedSessions(pi, ctx.cwd);
|
|
2221
|
+
if (running.length === 0) {
|
|
2222
|
+
notify(ctx, `No shared REPL sessions are running. Start one with /repl followed by ${REPL_START_RUNTIMES.join(", ")}.`, "info");
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (running.length === 1) {
|
|
2226
|
+
selector = running[0].selector;
|
|
2227
|
+
}
|
|
2228
|
+
else {
|
|
2229
|
+
const message = [
|
|
2230
|
+
"Multiple shared REPL sessions are running.",
|
|
2231
|
+
"",
|
|
2232
|
+
"Open a new terminal window and run one of:",
|
|
2233
|
+
...running.map((session) => `${getSessionDisplayName(session.selector, session.info)}: ${formatAttachCommand(session.info.sessionName)}`),
|
|
2234
|
+
].join("\n");
|
|
2235
|
+
notify(ctx, message, "info");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
const sessionName = getSessionNameForSelector(selector);
|
|
2240
|
+
const exists = await tmuxSessionExists(pi, sessionName, ctx.cwd);
|
|
2241
|
+
if (!exists) {
|
|
2242
|
+
notify(ctx, formatNoSessionRunning(selector), "info");
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const info = await readSessionInfo(pi, sessionName, ctx.cwd);
|
|
2246
|
+
if (info) {
|
|
2247
|
+
notify(ctx, formatAttachInstructions(info.sessionName), "info");
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
notify(ctx, formatAttachInstructions(sessionName), "info");
|
|
2251
|
+
}
|
|
2252
|
+
async function exportReplRecord(pi, ctx, selector) {
|
|
2253
|
+
let selected;
|
|
2254
|
+
if (selector) {
|
|
2255
|
+
const info = await readSessionInfo(pi, getSessionNameForSelector(selector), ctx.cwd);
|
|
2256
|
+
if (!info) {
|
|
2257
|
+
notify(ctx, formatNoSessionRunning(selector), "info");
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
selected = { selector, info };
|
|
2261
|
+
}
|
|
2262
|
+
else {
|
|
2263
|
+
const running = await listRunningSharedSessions(pi, ctx.cwd);
|
|
2264
|
+
if (running.length === 0) {
|
|
2265
|
+
notify(ctx, "No shared REPL sessions are running, so there is no clean record to export.", "info");
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
if (running.length > 1) {
|
|
2269
|
+
notify(ctx, [
|
|
2270
|
+
"Multiple shared REPL sessions are running.",
|
|
2271
|
+
"Choose one with /repl export python, julia, r, ghci, clojure, ruby, java, octave, matlab, gnuplot, or cpp.",
|
|
2272
|
+
].join("\n"), "warning");
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
selected = running[0];
|
|
2276
|
+
}
|
|
2277
|
+
const { info } = selected;
|
|
2278
|
+
if (!info.recordId) {
|
|
2279
|
+
notify(ctx, info.recordWarning || `No compatible clean record is available for ${info.sessionName}.`, "error");
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
try {
|
|
2283
|
+
const record = readReplSessionRecord(info.recordId, {
|
|
2284
|
+
sessionName: info.sessionName,
|
|
2285
|
+
tmuxSessionId: info.tmuxSessionId,
|
|
2286
|
+
tmuxSessionCreatedAt: info.tmuxSessionCreatedAt,
|
|
2287
|
+
runtime: info.runtime || "unknown",
|
|
2288
|
+
});
|
|
2289
|
+
if (!record) {
|
|
2290
|
+
notify(ctx, `The clean record for ${info.sessionName} is not available.`, "error");
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
const markdown = renderReplSessionRecordMarkdown(record);
|
|
2294
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2295
|
+
const baseName = `${info.sessionName}.record.${stamp}`;
|
|
2296
|
+
let outputPath;
|
|
2297
|
+
for (let suffix = 0; suffix < 100; suffix += 1) {
|
|
2298
|
+
const candidate = join(ctx.cwd, `${baseName}${suffix ? `-${suffix}` : ""}.md`);
|
|
2299
|
+
try {
|
|
2300
|
+
writeFileSync(candidate, markdown, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
2301
|
+
outputPath = candidate;
|
|
2302
|
+
break;
|
|
2303
|
+
}
|
|
2304
|
+
catch (error) {
|
|
2305
|
+
if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST")
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (!outputPath)
|
|
2310
|
+
throw new Error("Could not choose an unused export filename.");
|
|
2311
|
+
notify(ctx, `Exported ${record.entries.length} clean record entries to ${outputPath}`, "info");
|
|
2312
|
+
}
|
|
2313
|
+
catch (error) {
|
|
2314
|
+
notify(ctx, `Could not export the shared REPL record: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
async function handleRepl(pi, args, ctx) {
|
|
2318
|
+
const parsed = parseReplCommand(args);
|
|
2319
|
+
if (parsed.action === "help") {
|
|
2320
|
+
notify(ctx, formatUsage(), "info");
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (parsed.action === "error") {
|
|
2324
|
+
notify(ctx, `${parsed.message}\n\n${formatUsage()}`, "error");
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (parsed.action === "echo") {
|
|
2328
|
+
if (parsed.mode)
|
|
2329
|
+
replSubmissionEchoMode = parsed.mode;
|
|
2330
|
+
const privacyNote = replSubmissionEchoMode === "full"
|
|
2331
|
+
? " Full mode writes bounded submitted source code into persistent raw terminal history."
|
|
2332
|
+
: "";
|
|
2333
|
+
notify(ctx, `REPL submission echo: ${replSubmissionEchoMode}. Use /repl echo off|summary|full to change it for this Pi process; set PI_REPL_ECHO_MODE to choose the startup default.${privacyNote}`, replSubmissionEchoMode === "full" ? "warning" : "info");
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
const hasTmux = await commandExists(pi, "tmux", ctx.cwd);
|
|
2337
|
+
if (!hasTmux) {
|
|
2338
|
+
notify(ctx, "tmux was not found on PATH. pi-repl requires tmux.", "error");
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
switch (parsed.action) {
|
|
2342
|
+
case "status":
|
|
2343
|
+
await showReplStatus(pi, ctx, parsed.runtime ? toSessionSelector(parsed.runtime) : undefined);
|
|
2344
|
+
return;
|
|
2345
|
+
case "env":
|
|
2346
|
+
if (parsed.runtime && !isPythonRuntime(parsed.runtime)) {
|
|
2347
|
+
notify(ctx, "Environment inspection is currently implemented only for the shared Python/IPython session.", "warning");
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
await showDefaultPythonEnv(pi, ctx);
|
|
2351
|
+
return;
|
|
2352
|
+
case "stop":
|
|
2353
|
+
await stopReplSession(pi, ctx, parsed.runtime ? toSessionSelector(parsed.runtime) : undefined);
|
|
2354
|
+
return;
|
|
2355
|
+
case "attach":
|
|
2356
|
+
await attachReplSession(pi, ctx, parsed.runtime ? toSessionSelector(parsed.runtime) : undefined);
|
|
2357
|
+
return;
|
|
2358
|
+
case "export":
|
|
2359
|
+
await exportReplRecord(pi, ctx, parsed.runtime ? toSessionSelector(parsed.runtime) : undefined);
|
|
2360
|
+
return;
|
|
2361
|
+
case "start": {
|
|
2362
|
+
if (isPythonRuntime(parsed.runtime)) {
|
|
2363
|
+
if (parsed.name) {
|
|
2364
|
+
notify(ctx, "Named Python/IPython sessions are not implemented yet. For now, use /repl python or /repl ipython with no --name.", "warning");
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
if (parsed.runtime === "julia") {
|
|
2371
|
+
if (parsed.name) {
|
|
2372
|
+
notify(ctx, "Named Julia sessions are not implemented yet. For now, use /repl julia with no --name.", "warning");
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
if (parsed.runtime === "r") {
|
|
2379
|
+
if (parsed.name) {
|
|
2380
|
+
notify(ctx, "Named R sessions are not implemented yet. For now, use /repl R or /repl r with no --name.", "warning");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (parsed.runtime === "ghci") {
|
|
2387
|
+
if (parsed.name) {
|
|
2388
|
+
notify(ctx, "Named GHCi sessions are not implemented yet. For now, use /repl ghci with no --name.", "warning");
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (isClojureRuntime(parsed.runtime)) {
|
|
2395
|
+
if (parsed.name) {
|
|
2396
|
+
notify(ctx, "Named Clojure sessions are not implemented yet. For now, use /repl clojure or /repl clj with no --name.", "warning");
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (parsed.runtime === "ruby") {
|
|
2403
|
+
if (parsed.name) {
|
|
2404
|
+
notify(ctx, "Named Ruby sessions are not implemented yet. For now, use /repl ruby with no --name.", "warning");
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
if (parsed.runtime === "java") {
|
|
2411
|
+
if (parsed.name) {
|
|
2412
|
+
notify(ctx, "Named Java sessions are not implemented yet. For now, use /repl java with no --name.", "warning");
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
if (parsed.runtime === "octave" || parsed.runtime === "matlab" || parsed.runtime === "gnuplot" || parsed.runtime === "cpp") {
|
|
2419
|
+
if (parsed.name) {
|
|
2420
|
+
notify(ctx, `Named ${getSessionDisplayName(parsed.runtime)} sessions are not implemented yet. For now, use /repl ${parsed.runtime} with no --name.`, "warning");
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
await startReplCommand(pi, ctx, parsed.runtime);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
const sessionName = buildSessionName(parsed.runtime, parsed.name);
|
|
2427
|
+
const nameNote = parsed.name ? ` (from name: ${parsed.name})` : "";
|
|
2428
|
+
notify(ctx, [
|
|
2429
|
+
"Scaffold only: parsed REPL start request.",
|
|
2430
|
+
`Runtime: ${parsed.runtime}`,
|
|
2431
|
+
`tmux session: ${sessionName}${nameNote}`,
|
|
2432
|
+
"Only Python, IPython, Julia, R, GHCi, Clojure, Ruby, Java, Octave, MATLAB, gnuplot, and C++ session management are implemented so far.",
|
|
2433
|
+
].join("\n"), "info");
|
|
2434
|
+
return;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
export default function (pi) {
|
|
2439
|
+
pi.registerCommand("repl", {
|
|
2440
|
+
description: "Manage collaborative tmux-backed REPL sessions",
|
|
2441
|
+
handler: async (args, ctx) => {
|
|
2442
|
+
await handleRepl(pi, args, ctx);
|
|
2443
|
+
},
|
|
2444
|
+
});
|
|
2445
|
+
pi.registerCommand("lab", {
|
|
2446
|
+
description: "Alias for /repl",
|
|
2447
|
+
handler: async (args, ctx) => {
|
|
2448
|
+
await handleRepl(pi, args, ctx);
|
|
2449
|
+
},
|
|
2450
|
+
});
|
|
2451
|
+
pi.registerTool({
|
|
2452
|
+
name: "repl_start",
|
|
2453
|
+
label: "REPL Start",
|
|
2454
|
+
description: `Start or reuse a shared tmux REPL with an explicit runtime. Never resets existing sessions, switches interpreters, or sends probe code. Waits for a normal prompt and returns created/reused, ready, session status and attach instructions. Readiness timeout leaves the session running with ready=false. Response text is bounded to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}.`,
|
|
2455
|
+
promptSnippet: "Start a shared REPL, or reuse it without resetting its state, and wait for a normal prompt.",
|
|
2456
|
+
promptGuidelines: [
|
|
2457
|
+
"Use repl_start when the user wants to start a shared REPL, choosing runtime explicitly. It uses the same startup path as /repl and /lab; Python and IPython share one session and an existing interpreter is never switched.",
|
|
2458
|
+
"Check repl_start's ready result before repl_send. ready=false means inspect the pane or repl_status first; do not reset, interrupt, or send input to force readiness. A detected prompt is a snapshot, not a reservation against direct terminal input.",
|
|
2459
|
+
"repl_start leaves sessions running after readiness timeout or cancellation. Stopping or restarting remains an explicit user action; repl_send never auto-starts sessions.",
|
|
2460
|
+
],
|
|
2461
|
+
parameters: REPL_START_PARAMS,
|
|
2462
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
2463
|
+
if (!REPL_START_RUNTIMES.includes(params.runtime))
|
|
2464
|
+
throw new Error(`repl_start requires an explicit supported runtime: ${REPL_START_RUNTIMES.join(", ")}.`);
|
|
2465
|
+
const timeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
|
2466
|
+
? Math.max(1_000, Math.min(MAX_STARTUP_WAIT_MS, Math.round(params.timeoutMs))) : DEFAULT_STARTUP_WAIT_MS;
|
|
2467
|
+
const result = await startDefaultReplSession(pi, ctx.cwd, params.runtime, timeoutMs, signal);
|
|
2468
|
+
const truncation = truncateHead(result.text, { maxLines: DEFAULT_MAX_LINES - 2, maxBytes: DEFAULT_MAX_BYTES - 256 });
|
|
2469
|
+
return {
|
|
2470
|
+
content: [{ type: "text", text: truncation.content + (truncation.truncated ? "\n\n[Status text truncated; inspect repl_status or the session's raw history for more context.]" : "") }],
|
|
2471
|
+
details: { ...result.details, ...(truncation.truncated ? { truncation } : {}) },
|
|
2472
|
+
};
|
|
2473
|
+
},
|
|
2474
|
+
});
|
|
2475
|
+
pi.registerTool({
|
|
2476
|
+
name: "repl_status",
|
|
2477
|
+
label: "REPL Status",
|
|
2478
|
+
description: "Inspect shared REPL session state for Python/IPython, Julia, R, Haskell (GHCi), Clojure, Ruby, Java, Octave, MATLAB, gnuplot, and C++.",
|
|
2479
|
+
promptSnippet: "Check whether the shared Python/IPython, Julia, R, Haskell (GHCi), Clojure, Ruby, Java, Octave, MATLAB, gnuplot, and C++ REPL sessions are running.",
|
|
2480
|
+
promptGuidelines: [
|
|
2481
|
+
"Use repl_status before claiming whether a shared REPL is running, especially after a previous failure or status change.",
|
|
2482
|
+
"For Octave use repl_status target='octave'; for MATLAB use target='matlab'; for gnuplot use target='gnuplot'; for C++ use target='cpp'. They use separate shared sessions.",
|
|
2483
|
+
"If the user asks specifically about Julia, use target='julia'. If they ask specifically about R, use target='r'. If they ask specifically about GHCi or Haskell, use target='ghci'. If they ask specifically about Clojure, use target='clojure'. If they ask specifically about Ruby or IRB, use target='ruby'. If they ask specifically about Java or jshell, use target='java'. If they ask specifically about Python or IPython, use target='python'.",
|
|
2484
|
+
"If you need context about prior direct REPL interaction, inspect repl_status details and read the session history file listed there.",
|
|
2485
|
+
],
|
|
2486
|
+
parameters: REPL_STATUS_PARAMS,
|
|
2487
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2488
|
+
const targetRaw = typeof params.target === "string" ? params.target?.trim().toLowerCase() : undefined;
|
|
2489
|
+
let target;
|
|
2490
|
+
if (targetRaw) {
|
|
2491
|
+
if (targetRaw === "julia")
|
|
2492
|
+
target = "julia";
|
|
2493
|
+
else if (targetRaw === "r")
|
|
2494
|
+
target = "r";
|
|
2495
|
+
else if (targetRaw === "ghci" || targetRaw === "haskell")
|
|
2496
|
+
target = "ghci";
|
|
2497
|
+
else if (targetRaw === "clojure" || targetRaw === "clj")
|
|
2498
|
+
target = "clojure";
|
|
2499
|
+
else if (targetRaw === "ruby" || targetRaw === "irb")
|
|
2500
|
+
target = "ruby";
|
|
2501
|
+
else if (targetRaw === "java" || targetRaw === "jshell")
|
|
2502
|
+
target = "java";
|
|
2503
|
+
else if (targetRaw === "octave" || targetRaw === "matlab" || targetRaw === "gnuplot" || targetRaw === "cpp")
|
|
2504
|
+
target = targetRaw;
|
|
2505
|
+
else if (targetRaw === "python" || targetRaw === "ipython")
|
|
2506
|
+
target = "python";
|
|
2507
|
+
else
|
|
2508
|
+
throw new Error(`Unknown repl_status target: ${targetRaw}`);
|
|
2509
|
+
}
|
|
2510
|
+
const sessions = await listRunningSharedSessions(pi, ctx.cwd);
|
|
2511
|
+
const details = buildReplStatusDetails(sessions);
|
|
2512
|
+
if (target) {
|
|
2513
|
+
const sessionInfo = sessions.find((session) => session.selector === target)?.info;
|
|
2514
|
+
if (!sessionInfo) {
|
|
2515
|
+
return {
|
|
2516
|
+
content: [{ type: "text", text: formatNoSessionRunning(target) }],
|
|
2517
|
+
details,
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
return {
|
|
2521
|
+
content: [{ type: "text", text: `${getSessionDisplayName(target, sessionInfo)} REPL session is running.\n\n${formatSessionInfo(sessionInfo)}` }],
|
|
2522
|
+
details,
|
|
2523
|
+
};
|
|
2524
|
+
}
|
|
2525
|
+
if (sessions.length === 0) {
|
|
2526
|
+
return {
|
|
2527
|
+
content: [{ type: "text", text: "No shared REPL sessions are running." }],
|
|
2528
|
+
details,
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
if (sessions.length === 1) {
|
|
2532
|
+
const only = sessions[0];
|
|
2533
|
+
return {
|
|
2534
|
+
content: [{ type: "text", text: `${getSessionDisplayName(only.selector, only.info)} REPL session is running.\n\n${formatSessionInfo(only.info)}` }],
|
|
2535
|
+
details,
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
return {
|
|
2539
|
+
content: [{
|
|
2540
|
+
type: "text",
|
|
2541
|
+
text: [
|
|
2542
|
+
"Shared REPL sessions are running:",
|
|
2543
|
+
"",
|
|
2544
|
+
...sessions.flatMap((session, index) => [
|
|
2545
|
+
`${getSessionDisplayName(session.selector, session.info)} session:`,
|
|
2546
|
+
formatSessionInfo(session.info),
|
|
2547
|
+
...(index < sessions.length - 1 ? [""] : []),
|
|
2548
|
+
]),
|
|
2549
|
+
].join("\n"),
|
|
2550
|
+
}],
|
|
2551
|
+
details,
|
|
2552
|
+
};
|
|
2553
|
+
},
|
|
2554
|
+
});
|
|
2555
|
+
pi.registerTool({
|
|
2556
|
+
name: "repl_send",
|
|
2557
|
+
label: "REPL Send",
|
|
2558
|
+
description: `Execute code in the shared default Python/IPython, Julia, R, Haskell (GHCi), Clojure, Ruby, Java, Octave, MATLAB, gnuplot, or C++ tmux REPL sessions (${DEFAULT_PYTHON_SESSION}, ${DEFAULT_JULIA_SESSION}, ${DEFAULT_R_SESSION}, ${DEFAULT_GHCI_SESSION}, ${DEFAULT_CLOJURE_SESSION}, ${DEFAULT_RUBY_SESSION}, ${DEFAULT_JAVA_SESSION}, ${DEFAULT_OCTAVE_SESSION}, ${DEFAULT_MATLAB_SESSION}, ${DEFAULT_GNUPLOT_SESSION}, ${DEFAULT_CPP_SESSION}). The complete response (submitted code and output) is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first); the full response is saved privately when truncated.`,
|
|
2559
|
+
promptSnippet: "Execute a small snippet in the shared Python/IPython, Julia, R, Haskell (GHCi), Clojure, Ruby, Java, Octave, MATLAB, gnuplot, or C++ REPL and return its output.",
|
|
2560
|
+
promptGuidelines: [
|
|
2561
|
+
"Use repl_send only after the relevant session has been started with repl_start, /repl, or /lab and is at a normal prompt. repl_send never auto-starts a missing session.",
|
|
2562
|
+
"If the user asks to run code in Julia or in the shared Julia REPL, use target='julia'. If they ask to run code in R or in the shared R REPL, use target='r'. If they ask to run code in GHCi, Haskell, or the shared Haskell REPL, use target='ghci'. If they ask to run code in Clojure or in the shared Clojure REPL, use target='clojure'. If they ask to run code in Ruby or IRB or the shared Ruby REPL, use target='ruby'. If they ask to run code in Java or jshell or the shared Java REPL, use target='java'. For Octave use target='octave', for MATLAB use target='matlab', for gnuplot use target='gnuplot', and for C++ use target='cpp'. Otherwise use the shared Python/IPython session.",
|
|
2563
|
+
"Use repl_status before claiming whether the shared REPL is active if there has been a prior failure or a possible state change.",
|
|
2564
|
+
"If you need context about prior direct REPL interaction, inspect repl_status details and read the session history file listed there.",
|
|
2565
|
+
"The session history file is raw tmux pane output, so expect prompts and echoed input as well as results.",
|
|
2566
|
+
"This is a shared long-lived session: inspect state before mutating it, and do not assume variables already exist.",
|
|
2567
|
+
"repl_send shows bounded submitted code and alignment anchors in the raw pane by default (Summary). Respect the current /repl echo setting; use echoMode='off' when the user asks for quiet output, and echoMode='full' only when explicitly requested.",
|
|
2568
|
+
"Keep snippets small. If you need a value back reliably, print it explicitly.",
|
|
2569
|
+
"In GHCi, use normal interactive syntax such as let-bindings or :{ ... :} blocks for multiline declarations.",
|
|
2570
|
+
"In Clojure, use normal interactive syntax such as let-bindings, def/defn, or do forms for multiline code.",
|
|
2571
|
+
"In Ruby, definitions persist in the active IRB workspace and are shared with direct terminal input. Use normal Ruby source, including string interpolation.",
|
|
2572
|
+
"In Java, use top-level JShell snippets: imports, variables, methods, classes, expressions, or statements. Use System.out.println(...) for visible values; /open does not echo expression results. Submit complete snippets; native /open may discard an unfinished fragment. JShell commands such as /reset and /exit change or end the live session.",
|
|
2573
|
+
"For Octave use repl_send target='octave'; for MATLAB use target='matlab'. They are separate runtimes: do not silently substitute one for the other. Send complete code; use disp or fprintf for explicit output. Code executes in the base workspace, with native semicolon and ans behaviour. MATLAB function definitions belong in .m files; run scripts or call functions from the current path. clear, clear all, exit and quit are deliberate state-changing actions. This does not control an existing MATLAB desktop session.",
|
|
2574
|
+
"For C++ use target='cpp': an experimental Cling-based C++ REPL. Submit complete native prompt groups (put a function's opening brace on its declaration line); include ordinary definition/header files separately. Dot commands are terminal-only; use /repl stop cpp to stop. Earlier groups may run before a later error. Compiler/link diagnostics and caught C++ exceptions are retained; there is no replay. Exit, crashes and busy Ctrl-C can end the workspace. Completion means the evaluator returned, not that library calls succeeded. Do not queue control commands behind stdin reads or restart after timeout. Cling with matching development headers must already be on your login-shell PATH; no automatic installation or alternate-runtime fallback.",
|
|
2575
|
+
"For gnuplot use repl_send target='gnuplot'. This is its own persistent runtime, not another REPL's plotting backend. Send complete native scripts; use print for console results. Preserve the user's terminal, output, print destination and settings; export figures only when requested. reset and reset session deliberately change state. In a loaded script, exit/quit returns from that script; exit gnuplot ends the process. Avoid pause/input waits unless explicitly wanted.",
|
|
2576
|
+
"Avoid blocking interactive input() prompts or long-running code unless the user explicitly wants that.",
|
|
2577
|
+
],
|
|
2578
|
+
parameters: REPL_SEND_PARAMS,
|
|
2579
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
2580
|
+
return executeReplSend(pi, params, ctx, signal, toolCallId);
|
|
2581
|
+
},
|
|
2582
|
+
});
|
|
2583
|
+
}
|